1 /*
2 ==============================================================================
3
4 This file is part of the JUCE library.
5 Copyright (c) 2020 - Raw Material Software Limited
6
7 JUCE is an open source library subject to commercial or open-source
8 licensing.
9
10 By using JUCE, you agree to the terms of both the JUCE 6 End-User License
11 Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
12
13 End User License Agreement: www.juce.com/juce-6-licence
14 Privacy Policy: www.juce.com/juce-privacy-policy
15
16 Or: You may also use this code under the terms of the GPL v3 (see
17 www.gnu.org/licenses).
18
19 JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
20 EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
21 DISCLAIMED.
22
23 ==============================================================================
24 */
25
26 namespace juce
27 {
28
29 class CodeDocumentLine
30 {
31 public:
CodeDocumentLine(const String::CharPointerType startOfLine,const String::CharPointerType endOfLine,const int lineLen,const int numNewLineChars,const int startInFile)32 CodeDocumentLine (const String::CharPointerType startOfLine,
33 const String::CharPointerType endOfLine,
34 const int lineLen,
35 const int numNewLineChars,
36 const int startInFile)
37 : line (startOfLine, endOfLine),
38 lineStartInFile (startInFile),
39 lineLength (lineLen),
40 lineLengthWithoutNewLines (lineLen - numNewLineChars)
41 {
42 }
43
createLines(Array<CodeDocumentLine * > & newLines,StringRef text)44 static void createLines (Array<CodeDocumentLine*>& newLines, StringRef text)
45 {
46 auto t = text.text;
47 int charNumInFile = 0;
48 bool finished = false;
49
50 while (! (finished || t.isEmpty()))
51 {
52 auto startOfLine = t;
53 auto startOfLineInFile = charNumInFile;
54 int lineLength = 0;
55 int numNewLineChars = 0;
56
57 for (;;)
58 {
59 auto c = t.getAndAdvance();
60
61 if (c == 0)
62 {
63 finished = true;
64 break;
65 }
66
67 ++charNumInFile;
68 ++lineLength;
69
70 if (c == '\r')
71 {
72 ++numNewLineChars;
73
74 if (*t == '\n')
75 {
76 ++t;
77 ++charNumInFile;
78 ++lineLength;
79 ++numNewLineChars;
80 }
81
82 break;
83 }
84
85 if (c == '\n')
86 {
87 ++numNewLineChars;
88 break;
89 }
90 }
91
92 newLines.add (new CodeDocumentLine (startOfLine, t, lineLength,
93 numNewLineChars, startOfLineInFile));
94 }
95
96 jassert (charNumInFile == text.length());
97 }
98
endsWithLineBreak() const99 bool endsWithLineBreak() const noexcept
100 {
101 return lineLengthWithoutNewLines != lineLength;
102 }
103
updateLength()104 void updateLength() noexcept
105 {
106 lineLength = 0;
107 lineLengthWithoutNewLines = 0;
108
109 for (auto t = line.getCharPointer();;)
110 {
111 auto c = t.getAndAdvance();
112
113 if (c == 0)
114 break;
115
116 ++lineLength;
117
118 if (c != '\n' && c != '\r')
119 lineLengthWithoutNewLines = lineLength;
120 }
121 }
122
123 String line;
124 int lineStartInFile, lineLength, lineLengthWithoutNewLines;
125 };
126
127 //==============================================================================
Iterator(const CodeDocument & doc)128 CodeDocument::Iterator::Iterator (const CodeDocument& doc) noexcept
129 : document (&doc)
130 {}
131
Iterator(CodeDocument::Position p)132 CodeDocument::Iterator::Iterator (CodeDocument::Position p) noexcept
133 : document (p.owner),
134 line (p.getLineNumber()),
135 position (p.getPosition())
136 {
137 reinitialiseCharPtr();
138
139 for (int i = 0; i < p.getIndexInLine(); ++i)
140 {
141 charPointer.getAndAdvance();
142
143 if (charPointer.isEmpty())
144 {
145 position -= (p.getIndexInLine() - i);
146 break;
147 }
148 }
149 }
150
Iterator()151 CodeDocument::Iterator::Iterator() noexcept
152 : document (nullptr)
153 {
154 }
155
~Iterator()156 CodeDocument::Iterator::~Iterator() noexcept {}
157
158
reinitialiseCharPtr() const159 bool CodeDocument::Iterator::reinitialiseCharPtr() const
160 {
161 /** You're trying to use a default constructed iterator. Bad idea! */
162 jassert (document != nullptr);
163
164 if (charPointer.getAddress() == nullptr)
165 {
166 if (auto* l = document->lines[line])
167 charPointer = l->line.getCharPointer();
168 else
169 return false;
170 }
171
172 return true;
173 }
174
nextChar()175 juce_wchar CodeDocument::Iterator::nextChar() noexcept
176 {
177 for (;;)
178 {
179 if (! reinitialiseCharPtr())
180 return 0;
181
182 if (auto result = charPointer.getAndAdvance())
183 {
184 if (charPointer.isEmpty())
185 {
186 ++line;
187 charPointer = nullptr;
188 }
189
190 ++position;
191 return result;
192 }
193
194 ++line;
195 charPointer = nullptr;
196 }
197 }
198
skip()199 void CodeDocument::Iterator::skip() noexcept
200 {
201 nextChar();
202 }
203
skipToEndOfLine()204 void CodeDocument::Iterator::skipToEndOfLine() noexcept
205 {
206 if (! reinitialiseCharPtr())
207 return;
208
209 position += (int) charPointer.length();
210 ++line;
211 charPointer = nullptr;
212 }
213
skipToStartOfLine()214 void CodeDocument::Iterator::skipToStartOfLine() noexcept
215 {
216 if (! reinitialiseCharPtr())
217 return;
218
219 if (auto* l = document->lines [line])
220 {
221 auto startPtr = l->line.getCharPointer();
222 position -= (int) startPtr.lengthUpTo (charPointer);
223 charPointer = startPtr;
224 }
225 }
226
peekNextChar() const227 juce_wchar CodeDocument::Iterator::peekNextChar() const noexcept
228 {
229 if (! reinitialiseCharPtr())
230 return 0;
231
232 if (auto c = *charPointer)
233 return c;
234
235 if (auto* l = document->lines [line + 1])
236 return l->line[0];
237
238 return 0;
239 }
240
previousChar()241 juce_wchar CodeDocument::Iterator::previousChar() noexcept
242 {
243 if (! reinitialiseCharPtr())
244 return 0;
245
246 for (;;)
247 {
248 if (auto* l = document->lines[line])
249 {
250 if (charPointer != l->line.getCharPointer())
251 {
252 --position;
253 --charPointer;
254 break;
255 }
256 }
257
258 if (line == 0)
259 return 0;
260
261 --line;
262
263 if (auto* prev = document->lines[line])
264 charPointer = prev->line.getCharPointer().findTerminatingNull();
265 }
266
267 return *charPointer;
268 }
269
peekPreviousChar() const270 juce_wchar CodeDocument::Iterator::peekPreviousChar() const noexcept
271 {
272 if (! reinitialiseCharPtr())
273 return 0;
274
275 if (auto* l = document->lines[line])
276 {
277 if (charPointer != l->line.getCharPointer())
278 return *(charPointer - 1);
279
280 if (auto* prev = document->lines[line - 1])
281 return *(prev->line.getCharPointer().findTerminatingNull() - 1);
282 }
283
284 return 0;
285 }
286
skipWhitespace()287 void CodeDocument::Iterator::skipWhitespace() noexcept
288 {
289 while (CharacterFunctions::isWhitespace (peekNextChar()))
290 skip();
291 }
292
isEOF() const293 bool CodeDocument::Iterator::isEOF() const noexcept
294 {
295 return charPointer.getAddress() == nullptr && line >= document->lines.size();
296 }
297
isSOF() const298 bool CodeDocument::Iterator::isSOF() const noexcept
299 {
300 return position == 0;
301 }
302
toPosition() const303 CodeDocument::Position CodeDocument::Iterator::toPosition() const
304 {
305 if (auto* l = document->lines[line])
306 {
307 reinitialiseCharPtr();
308 int indexInLine = 0;
309 auto linePtr = l->line.getCharPointer();
310
311 while (linePtr != charPointer && ! linePtr.isEmpty())
312 {
313 ++indexInLine;
314 ++linePtr;
315 }
316
317 return CodeDocument::Position (*document, line, indexInLine);
318 }
319
320 if (isEOF())
321 {
322 if (auto* last = document->lines.getLast())
323 {
324 auto lineIndex = document->lines.size() - 1;
325 return CodeDocument::Position (*document, lineIndex, last->lineLength);
326 }
327 }
328
329 return CodeDocument::Position (*document, 0, 0);
330 }
331
332 //==============================================================================
Position()333 CodeDocument::Position::Position() noexcept
334 {
335 }
336
Position(const CodeDocument & ownerDocument,const int lineNum,const int index)337 CodeDocument::Position::Position (const CodeDocument& ownerDocument,
338 const int lineNum, const int index) noexcept
339 : owner (const_cast<CodeDocument*> (&ownerDocument)),
340 line (lineNum), indexInLine (index)
341 {
342 setLineAndIndex (lineNum, index);
343 }
344
Position(const CodeDocument & ownerDocument,int pos)345 CodeDocument::Position::Position (const CodeDocument& ownerDocument, int pos) noexcept
346 : owner (const_cast<CodeDocument*> (&ownerDocument))
347 {
348 setPosition (pos);
349 }
350
Position(const Position & other)351 CodeDocument::Position::Position (const Position& other) noexcept
352 : owner (other.owner), characterPos (other.characterPos), line (other.line),
353 indexInLine (other.indexInLine)
354 {
355 jassert (*this == other);
356 }
357
~Position()358 CodeDocument::Position::~Position()
359 {
360 setPositionMaintained (false);
361 }
362
operator =(const Position & other)363 CodeDocument::Position& CodeDocument::Position::operator= (const Position& other)
364 {
365 if (this != &other)
366 {
367 const bool wasPositionMaintained = positionMaintained;
368 if (owner != other.owner)
369 setPositionMaintained (false);
370
371 owner = other.owner;
372 line = other.line;
373 indexInLine = other.indexInLine;
374 characterPos = other.characterPos;
375 setPositionMaintained (wasPositionMaintained);
376
377 jassert (*this == other);
378 }
379
380 return *this;
381 }
382
operator ==(const Position & other) const383 bool CodeDocument::Position::operator== (const Position& other) const noexcept
384 {
385 jassert ((characterPos == other.characterPos)
386 == (line == other.line && indexInLine == other.indexInLine));
387
388 return characterPos == other.characterPos
389 && line == other.line
390 && indexInLine == other.indexInLine
391 && owner == other.owner;
392 }
393
operator !=(const Position & other) const394 bool CodeDocument::Position::operator!= (const Position& other) const noexcept
395 {
396 return ! operator== (other);
397 }
398
setLineAndIndex(const int newLineNum,const int newIndexInLine)399 void CodeDocument::Position::setLineAndIndex (const int newLineNum, const int newIndexInLine)
400 {
401 jassert (owner != nullptr);
402
403 if (owner->lines.size() == 0)
404 {
405 line = 0;
406 indexInLine = 0;
407 characterPos = 0;
408 }
409 else
410 {
411 if (newLineNum >= owner->lines.size())
412 {
413 line = owner->lines.size() - 1;
414
415 auto& l = *owner->lines.getUnchecked (line);
416 indexInLine = l.lineLengthWithoutNewLines;
417 characterPos = l.lineStartInFile + indexInLine;
418 }
419 else
420 {
421 line = jmax (0, newLineNum);
422
423 auto& l = *owner->lines.getUnchecked (line);
424
425 if (l.lineLengthWithoutNewLines > 0)
426 indexInLine = jlimit (0, l.lineLengthWithoutNewLines, newIndexInLine);
427 else
428 indexInLine = 0;
429
430 characterPos = l.lineStartInFile + indexInLine;
431 }
432 }
433 }
434
setPosition(const int newPosition)435 void CodeDocument::Position::setPosition (const int newPosition)
436 {
437 jassert (owner != nullptr);
438
439 line = 0;
440 indexInLine = 0;
441 characterPos = 0;
442
443 if (newPosition > 0)
444 {
445 int lineStart = 0;
446 auto lineEnd = owner->lines.size();
447
448 for (;;)
449 {
450 if (lineEnd - lineStart < 4)
451 {
452 for (int i = lineStart; i < lineEnd; ++i)
453 {
454 auto& l = *owner->lines.getUnchecked (i);
455 auto index = newPosition - l.lineStartInFile;
456
457 if (index >= 0 && (index < l.lineLength || i == lineEnd - 1))
458 {
459 line = i;
460 indexInLine = jmin (l.lineLengthWithoutNewLines, index);
461 characterPos = l.lineStartInFile + indexInLine;
462 }
463 }
464
465 break;
466 }
467 else
468 {
469 auto midIndex = (lineStart + lineEnd + 1) / 2;
470
471 if (newPosition >= owner->lines.getUnchecked (midIndex)->lineStartInFile)
472 lineStart = midIndex;
473 else
474 lineEnd = midIndex;
475 }
476 }
477 }
478 }
479
moveBy(int characterDelta)480 void CodeDocument::Position::moveBy (int characterDelta)
481 {
482 jassert (owner != nullptr);
483
484 if (characterDelta == 1)
485 {
486 setPosition (getPosition());
487
488 // If moving right, make sure we don't get stuck between the \r and \n characters..
489 if (line < owner->lines.size())
490 {
491 auto& l = *owner->lines.getUnchecked (line);
492
493 if (indexInLine + characterDelta < l.lineLength
494 && indexInLine + characterDelta >= l.lineLengthWithoutNewLines + 1)
495 ++characterDelta;
496 }
497 }
498
499 setPosition (characterPos + characterDelta);
500 }
501
movedBy(const int characterDelta) const502 CodeDocument::Position CodeDocument::Position::movedBy (const int characterDelta) const
503 {
504 CodeDocument::Position p (*this);
505 p.moveBy (characterDelta);
506 return p;
507 }
508
movedByLines(const int deltaLines) const509 CodeDocument::Position CodeDocument::Position::movedByLines (const int deltaLines) const
510 {
511 CodeDocument::Position p (*this);
512 p.setLineAndIndex (getLineNumber() + deltaLines, getIndexInLine());
513 return p;
514 }
515
getCharacter() const516 juce_wchar CodeDocument::Position::getCharacter() const
517 {
518 if (auto* l = owner->lines [line])
519 return l->line [getIndexInLine()];
520
521 return 0;
522 }
523
getLineText() const524 String CodeDocument::Position::getLineText() const
525 {
526 if (auto* l = owner->lines [line])
527 return l->line;
528
529 return {};
530 }
531
setPositionMaintained(const bool isMaintained)532 void CodeDocument::Position::setPositionMaintained (const bool isMaintained)
533 {
534 if (isMaintained != positionMaintained)
535 {
536 positionMaintained = isMaintained;
537
538 if (owner != nullptr)
539 {
540 if (isMaintained)
541 {
542 jassert (! owner->positionsToMaintain.contains (this));
543 owner->positionsToMaintain.add (this);
544 }
545 else
546 {
547 // If this happens, you may have deleted the document while there are Position objects that are still using it...
548 jassert (owner->positionsToMaintain.contains (this));
549 owner->positionsToMaintain.removeFirstMatchingValue (this);
550 }
551 }
552 }
553 }
554
555 //==============================================================================
CodeDocument()556 CodeDocument::CodeDocument() : undoManager (std::numeric_limits<int>::max(), 10000)
557 {
558 }
559
~CodeDocument()560 CodeDocument::~CodeDocument()
561 {
562 }
563
getAllContent() const564 String CodeDocument::getAllContent() const
565 {
566 return getTextBetween (Position (*this, 0),
567 Position (*this, lines.size(), 0));
568 }
569
getTextBetween(const Position & start,const Position & end) const570 String CodeDocument::getTextBetween (const Position& start, const Position& end) const
571 {
572 if (end.getPosition() <= start.getPosition())
573 return {};
574
575 auto startLine = start.getLineNumber();
576 auto endLine = end.getLineNumber();
577
578 if (startLine == endLine)
579 {
580 if (auto* line = lines [startLine])
581 return line->line.substring (start.getIndexInLine(), end.getIndexInLine());
582
583 return {};
584 }
585
586 MemoryOutputStream mo;
587 mo.preallocate ((size_t) (end.getPosition() - start.getPosition() + 4));
588
589 auto maxLine = jmin (lines.size() - 1, endLine);
590
591 for (int i = jmax (0, startLine); i <= maxLine; ++i)
592 {
593 auto& line = *lines.getUnchecked(i);
594 auto len = line.lineLength;
595
596 if (i == startLine)
597 {
598 auto index = start.getIndexInLine();
599 mo << line.line.substring (index, len);
600 }
601 else if (i == endLine)
602 {
603 len = end.getIndexInLine();
604 mo << line.line.substring (0, len);
605 }
606 else
607 {
608 mo << line.line;
609 }
610 }
611
612 return mo.toUTF8();
613 }
614
getNumCharacters() const615 int CodeDocument::getNumCharacters() const noexcept
616 {
617 if (auto* lastLine = lines.getLast())
618 return lastLine->lineStartInFile + lastLine->lineLength;
619
620 return 0;
621 }
622
getLine(const int lineIndex) const623 String CodeDocument::getLine (const int lineIndex) const noexcept
624 {
625 if (auto* line = lines[lineIndex])
626 return line->line;
627
628 return {};
629 }
630
getMaximumLineLength()631 int CodeDocument::getMaximumLineLength() noexcept
632 {
633 if (maximumLineLength < 0)
634 {
635 maximumLineLength = 0;
636
637 for (auto* l : lines)
638 maximumLineLength = jmax (maximumLineLength, l->lineLength);
639 }
640
641 return maximumLineLength;
642 }
643
deleteSection(const Position & startPosition,const Position & endPosition)644 void CodeDocument::deleteSection (const Position& startPosition, const Position& endPosition)
645 {
646 deleteSection (startPosition.getPosition(), endPosition.getPosition());
647 }
648
deleteSection(const int start,const int end)649 void CodeDocument::deleteSection (const int start, const int end)
650 {
651 remove (start, end, true);
652 }
653
insertText(const Position & position,const String & text)654 void CodeDocument::insertText (const Position& position, const String& text)
655 {
656 insertText (position.getPosition(), text);
657 }
658
insertText(const int insertIndex,const String & text)659 void CodeDocument::insertText (const int insertIndex, const String& text)
660 {
661 insert (text, insertIndex, true);
662 }
663
replaceSection(const int start,const int end,const String & newText)664 void CodeDocument::replaceSection (const int start, const int end, const String& newText)
665 {
666 insertText (end, newText);
667 deleteSection (start, end);
668 }
669
applyChanges(const String & newContent)670 void CodeDocument::applyChanges (const String& newContent)
671 {
672 const String corrected (StringArray::fromLines (newContent)
673 .joinIntoString (newLineChars));
674
675 TextDiff diff (getAllContent(), corrected);
676
677 for (auto& c : diff.changes)
678 {
679 if (c.isDeletion())
680 remove (c.start, c.start + c.length, true);
681 else
682 insert (c.insertedText, c.start, true);
683 }
684 }
685
replaceAllContent(const String & newContent)686 void CodeDocument::replaceAllContent (const String& newContent)
687 {
688 remove (0, getNumCharacters(), true);
689 insert (newContent, 0, true);
690 }
691
loadFromStream(InputStream & stream)692 bool CodeDocument::loadFromStream (InputStream& stream)
693 {
694 remove (0, getNumCharacters(), false);
695 insert (stream.readEntireStreamAsString(), 0, false);
696 setSavePoint();
697 clearUndoHistory();
698 return true;
699 }
700
writeToStream(OutputStream & stream)701 bool CodeDocument::writeToStream (OutputStream& stream)
702 {
703 for (auto* l : lines)
704 {
705 auto temp = l->line; // use a copy to avoid bloating the memory footprint of the stored string.
706 const char* utf8 = temp.toUTF8();
707
708 if (! stream.write (utf8, strlen (utf8)))
709 return false;
710 }
711
712 return true;
713 }
714
setNewLineCharacters(const String & newChars)715 void CodeDocument::setNewLineCharacters (const String& newChars) noexcept
716 {
717 jassert (newChars == "\r\n" || newChars == "\n" || newChars == "\r");
718 newLineChars = newChars;
719 }
720
newTransaction()721 void CodeDocument::newTransaction()
722 {
723 undoManager.beginNewTransaction (String());
724 }
725
undo()726 void CodeDocument::undo()
727 {
728 newTransaction();
729 undoManager.undo();
730 }
731
redo()732 void CodeDocument::redo()
733 {
734 undoManager.redo();
735 }
736
clearUndoHistory()737 void CodeDocument::clearUndoHistory()
738 {
739 undoManager.clearUndoHistory();
740 }
741
setSavePoint()742 void CodeDocument::setSavePoint() noexcept
743 {
744 indexOfSavedState = currentActionIndex;
745 }
746
hasChangedSinceSavePoint() const747 bool CodeDocument::hasChangedSinceSavePoint() const noexcept
748 {
749 return currentActionIndex != indexOfSavedState;
750 }
751
752 //==============================================================================
getCharacterType(juce_wchar character)753 static int getCharacterType (juce_wchar character) noexcept
754 {
755 return (CharacterFunctions::isLetterOrDigit (character) || character == '_')
756 ? 2 : (CharacterFunctions::isWhitespace (character) ? 0 : 1);
757 }
758
findWordBreakAfter(const Position & position) const759 CodeDocument::Position CodeDocument::findWordBreakAfter (const Position& position) const noexcept
760 {
761 auto p = position;
762 const int maxDistance = 256;
763 int i = 0;
764
765 while (i < maxDistance
766 && CharacterFunctions::isWhitespace (p.getCharacter())
767 && (i == 0 || (p.getCharacter() != '\n'
768 && p.getCharacter() != '\r')))
769 {
770 ++i;
771 p.moveBy (1);
772 }
773
774 if (i == 0)
775 {
776 auto type = getCharacterType (p.getCharacter());
777
778 while (i < maxDistance && type == getCharacterType (p.getCharacter()))
779 {
780 ++i;
781 p.moveBy (1);
782 }
783
784 while (i < maxDistance
785 && CharacterFunctions::isWhitespace (p.getCharacter())
786 && (i == 0 || (p.getCharacter() != '\n'
787 && p.getCharacter() != '\r')))
788 {
789 ++i;
790 p.moveBy (1);
791 }
792 }
793
794 return p;
795 }
796
findWordBreakBefore(const Position & position) const797 CodeDocument::Position CodeDocument::findWordBreakBefore (const Position& position) const noexcept
798 {
799 auto p = position;
800 const int maxDistance = 256;
801 int i = 0;
802 bool stoppedAtLineStart = false;
803
804 while (i < maxDistance)
805 {
806 auto c = p.movedBy (-1).getCharacter();
807
808 if (c == '\r' || c == '\n')
809 {
810 stoppedAtLineStart = true;
811
812 if (i > 0)
813 break;
814 }
815
816 if (! CharacterFunctions::isWhitespace (c))
817 break;
818
819 p.moveBy (-1);
820 ++i;
821 }
822
823 if (i < maxDistance && ! stoppedAtLineStart)
824 {
825 auto type = getCharacterType (p.movedBy (-1).getCharacter());
826
827 while (i < maxDistance && type == getCharacterType (p.movedBy (-1).getCharacter()))
828 {
829 p.moveBy (-1);
830 ++i;
831 }
832 }
833
834 return p;
835 }
836
findTokenContaining(const Position & pos,Position & start,Position & end) const837 void CodeDocument::findTokenContaining (const Position& pos, Position& start, Position& end) const noexcept
838 {
839 auto isTokenCharacter = [] (juce_wchar c) { return CharacterFunctions::isLetterOrDigit (c) || c == '.' || c == '_'; };
840
841 end = pos;
842 while (isTokenCharacter (end.getCharacter()))
843 end.moveBy (1);
844
845 start = end;
846 while (start.getIndexInLine() > 0
847 && isTokenCharacter (start.movedBy (-1).getCharacter()))
848 start.moveBy (-1);
849 }
850
findLineContaining(const Position & pos,Position & s,Position & e) const851 void CodeDocument::findLineContaining (const Position& pos, Position& s, Position& e) const noexcept
852 {
853 s.setLineAndIndex (pos.getLineNumber(), 0);
854 e.setLineAndIndex (pos.getLineNumber() + 1, 0);
855 }
856
checkLastLineStatus()857 void CodeDocument::checkLastLineStatus()
858 {
859 while (lines.size() > 0
860 && lines.getLast()->lineLength == 0
861 && (lines.size() == 1 || ! lines.getUnchecked (lines.size() - 2)->endsWithLineBreak()))
862 {
863 // remove any empty lines at the end if the preceding line doesn't end in a newline.
864 lines.removeLast();
865 }
866
867 const CodeDocumentLine* const lastLine = lines.getLast();
868
869 if (lastLine != nullptr && lastLine->endsWithLineBreak())
870 {
871 // check that there's an empty line at the end if the preceding one ends in a newline..
872 lines.add (new CodeDocumentLine (StringRef(), StringRef(), 0, 0,
873 lastLine->lineStartInFile + lastLine->lineLength));
874 }
875 }
876
877 //==============================================================================
addListener(CodeDocument::Listener * l)878 void CodeDocument::addListener (CodeDocument::Listener* l) { listeners.add (l); }
removeListener(CodeDocument::Listener * l)879 void CodeDocument::removeListener (CodeDocument::Listener* l) { listeners.remove (l); }
880
881 //==============================================================================
882 struct CodeDocument::InsertAction : public UndoableAction
883 {
InsertActionjuce::CodeDocument::InsertAction884 InsertAction (CodeDocument& doc, const String& t, const int pos) noexcept
885 : owner (doc), text (t), insertPos (pos)
886 {
887 }
888
performjuce::CodeDocument::InsertAction889 bool perform() override
890 {
891 owner.currentActionIndex++;
892 owner.insert (text, insertPos, false);
893 return true;
894 }
895
undojuce::CodeDocument::InsertAction896 bool undo() override
897 {
898 owner.currentActionIndex--;
899 owner.remove (insertPos, insertPos + text.length(), false);
900 return true;
901 }
902
getSizeInUnitsjuce::CodeDocument::InsertAction903 int getSizeInUnits() override { return text.length() + 32; }
904
905 CodeDocument& owner;
906 const String text;
907 const int insertPos;
908
909 JUCE_DECLARE_NON_COPYABLE (InsertAction)
910 };
911
insert(const String & text,const int insertPos,const bool undoable)912 void CodeDocument::insert (const String& text, const int insertPos, const bool undoable)
913 {
914 if (text.isNotEmpty())
915 {
916 if (undoable)
917 {
918 undoManager.perform (new InsertAction (*this, text, insertPos));
919 }
920 else
921 {
922 Position pos (*this, insertPos);
923 auto firstAffectedLine = pos.getLineNumber();
924
925 auto* firstLine = lines[firstAffectedLine];
926 auto textInsideOriginalLine = text;
927
928 if (firstLine != nullptr)
929 {
930 auto index = pos.getIndexInLine();
931 textInsideOriginalLine = firstLine->line.substring (0, index)
932 + textInsideOriginalLine
933 + firstLine->line.substring (index);
934 }
935
936 maximumLineLength = -1;
937 Array<CodeDocumentLine*> newLines;
938 CodeDocumentLine::createLines (newLines, textInsideOriginalLine);
939 jassert (newLines.size() > 0);
940
941 auto* newFirstLine = newLines.getUnchecked (0);
942 newFirstLine->lineStartInFile = firstLine != nullptr ? firstLine->lineStartInFile : 0;
943 lines.set (firstAffectedLine, newFirstLine);
944
945 if (newLines.size() > 1)
946 lines.insertArray (firstAffectedLine + 1, newLines.getRawDataPointer() + 1, newLines.size() - 1);
947
948 int lineStart = newFirstLine->lineStartInFile;
949
950 for (int i = firstAffectedLine; i < lines.size(); ++i)
951 {
952 auto& l = *lines.getUnchecked (i);
953 l.lineStartInFile = lineStart;
954 lineStart += l.lineLength;
955 }
956
957 checkLastLineStatus();
958 auto newTextLength = text.length();
959
960 for (auto* p : positionsToMaintain)
961 if (p->getPosition() >= insertPos)
962 p->setPosition (p->getPosition() + newTextLength);
963
964 listeners.call ([&] (Listener& l) { l.codeDocumentTextInserted (text, insertPos); });
965 }
966 }
967 }
968
969 //==============================================================================
970 struct CodeDocument::DeleteAction : public UndoableAction
971 {
DeleteActionjuce::CodeDocument::DeleteAction972 DeleteAction (CodeDocument& doc, int start, int end) noexcept
973 : owner (doc), startPos (start), endPos (end),
974 removedText (doc.getTextBetween (CodeDocument::Position (doc, start),
975 CodeDocument::Position (doc, end)))
976 {
977 }
978
performjuce::CodeDocument::DeleteAction979 bool perform() override
980 {
981 owner.currentActionIndex++;
982 owner.remove (startPos, endPos, false);
983 return true;
984 }
985
undojuce::CodeDocument::DeleteAction986 bool undo() override
987 {
988 owner.currentActionIndex--;
989 owner.insert (removedText, startPos, false);
990 return true;
991 }
992
getSizeInUnitsjuce::CodeDocument::DeleteAction993 int getSizeInUnits() override { return (endPos - startPos) + 32; }
994
995 CodeDocument& owner;
996 const int startPos, endPos;
997 const String removedText;
998
999 JUCE_DECLARE_NON_COPYABLE (DeleteAction)
1000 };
1001
remove(const int startPos,const int endPos,const bool undoable)1002 void CodeDocument::remove (const int startPos, const int endPos, const bool undoable)
1003 {
1004 if (endPos <= startPos)
1005 return;
1006
1007 if (undoable)
1008 {
1009 undoManager.perform (new DeleteAction (*this, startPos, endPos));
1010 }
1011 else
1012 {
1013 Position startPosition (*this, startPos);
1014 Position endPosition (*this, endPos);
1015
1016 maximumLineLength = -1;
1017 auto firstAffectedLine = startPosition.getLineNumber();
1018 auto endLine = endPosition.getLineNumber();
1019 auto& firstLine = *lines.getUnchecked (firstAffectedLine);
1020
1021 if (firstAffectedLine == endLine)
1022 {
1023 firstLine.line = firstLine.line.substring (0, startPosition.getIndexInLine())
1024 + firstLine.line.substring (endPosition.getIndexInLine());
1025 firstLine.updateLength();
1026 }
1027 else
1028 {
1029 auto& lastLine = *lines.getUnchecked (endLine);
1030
1031 firstLine.line = firstLine.line.substring (0, startPosition.getIndexInLine())
1032 + lastLine.line.substring (endPosition.getIndexInLine());
1033 firstLine.updateLength();
1034
1035 int numLinesToRemove = endLine - firstAffectedLine;
1036 lines.removeRange (firstAffectedLine + 1, numLinesToRemove);
1037 }
1038
1039 for (int i = firstAffectedLine + 1; i < lines.size(); ++i)
1040 {
1041 auto& l = *lines.getUnchecked (i);
1042 auto& previousLine = *lines.getUnchecked (i - 1);
1043 l.lineStartInFile = previousLine.lineStartInFile + previousLine.lineLength;
1044 }
1045
1046 checkLastLineStatus();
1047 auto totalChars = getNumCharacters();
1048
1049 for (auto* p : positionsToMaintain)
1050 {
1051 if (p->getPosition() > startPosition.getPosition())
1052 p->setPosition (jmax (startPos, p->getPosition() + startPos - endPos));
1053
1054 if (p->getPosition() > totalChars)
1055 p->setPosition (totalChars);
1056 }
1057
1058 listeners.call ([=] (Listener& l) { l.codeDocumentTextDeleted (startPos, endPos); });
1059 }
1060 }
1061
1062 //==============================================================================
1063 //==============================================================================
1064 #if JUCE_UNIT_TESTS
1065
1066 struct CodeDocumentTest : public UnitTest
1067 {
CodeDocumentTestjuce::CodeDocumentTest1068 CodeDocumentTest()
1069 : UnitTest ("CodeDocument", UnitTestCategories::text)
1070 {}
1071
runTestjuce::CodeDocumentTest1072 void runTest() override
1073 {
1074 const juce::String jabberwocky ("'Twas brillig, and the slithy toves\n"
1075 "Did gyre and gimble in the wabe;\n"
1076 "All mimsy were the borogoves,\n"
1077 "And the mome raths outgrabe.\n\n"
1078
1079 "'Beware the Jabberwock, my son!\n"
1080 "The jaws that bite, the claws that catch!\n"
1081 "Beware the Jubjub bird, and shun\n"
1082 "The frumious Bandersnatch!'");
1083
1084 {
1085 beginTest ("Basic checks");
1086 CodeDocument d;
1087 d.replaceAllContent (jabberwocky);
1088
1089 expectEquals (d.getNumLines(), 9);
1090 expect (d.getLine (0).startsWith ("'Twas brillig"));
1091 expect (d.getLine (2).startsWith ("All mimsy"));
1092 expectEquals (d.getLine (4), String ("\n"));
1093 }
1094
1095 {
1096 beginTest ("Insert/replace/delete");
1097
1098 CodeDocument d;
1099 d.replaceAllContent (jabberwocky);
1100
1101 d.insertText (CodeDocument::Position (d, 0, 6), "very ");
1102 expect (d.getLine (0).startsWith ("'Twas very brillig"),
1103 "Insert text within a line");
1104
1105 d.replaceSection (74, 83, "Quite hungry");
1106 expectEquals (d.getLine (2), String ("Quite hungry were the borogoves,\n"),
1107 "Replace section at start of line");
1108
1109 d.replaceSection (11, 18, "cold");
1110 expectEquals (d.getLine (0), String ("'Twas very cold, and the slithy toves\n"),
1111 "Replace section within a line");
1112
1113 d.deleteSection (CodeDocument::Position (d, 2, 0), CodeDocument::Position (d, 2, 6));
1114 expectEquals (d.getLine (2), String ("hungry were the borogoves,\n"),
1115 "Delete section within a line");
1116
1117 d.deleteSection (CodeDocument::Position (d, 2, 6), CodeDocument::Position (d, 5, 11));
1118 expectEquals (d.getLine (2), String ("hungry Jabberwock, my son!\n"),
1119 "Delete section across multiple line");
1120 }
1121
1122 {
1123 beginTest ("Line splitting and joining");
1124
1125 CodeDocument d;
1126 d.replaceAllContent (jabberwocky);
1127 expectEquals (d.getNumLines(), 9);
1128
1129 const String splitComment ("Adding a newline should split a line into two.");
1130 d.insertText (49, "\n");
1131
1132 expectEquals (d.getNumLines(), 10, splitComment);
1133 expectEquals (d.getLine (1), String ("Did gyre and \n"), splitComment);
1134 expectEquals (d.getLine (2), String ("gimble in the wabe;\n"), splitComment);
1135
1136 const String joinComment ("Removing a newline should join two lines.");
1137 d.deleteSection (CodeDocument::Position (d, 0, 35),
1138 CodeDocument::Position (d, 1, 0));
1139
1140 expectEquals (d.getNumLines(), 9, joinComment);
1141 expectEquals (d.getLine (0), String ("'Twas brillig, and the slithy tovesDid gyre and \n"), joinComment);
1142 expectEquals (d.getLine (1), String ("gimble in the wabe;\n"), joinComment);
1143 }
1144
1145 {
1146 beginTest ("Undo/redo");
1147
1148 CodeDocument d;
1149 d.replaceAllContent (jabberwocky);
1150 d.newTransaction();
1151 d.insertText (30, "INSERT1");
1152 d.newTransaction();
1153 d.insertText (70, "INSERT2");
1154 d.undo();
1155
1156 expect (d.getAllContent().contains ("INSERT1"), "1st edit should remain.");
1157 expect (! d.getAllContent().contains ("INSERT2"), "2nd edit should be undone.");
1158
1159 d.redo();
1160 expect (d.getAllContent().contains ("INSERT2"), "2nd edit should be redone.");
1161
1162 d.newTransaction();
1163 d.deleteSection (25, 90);
1164 expect (! d.getAllContent().contains ("INSERT1"), "1st edit should be deleted.");
1165 expect (! d.getAllContent().contains ("INSERT2"), "2nd edit should be deleted.");
1166 d.undo();
1167 expect (d.getAllContent().contains ("INSERT1"), "1st edit should be restored.");
1168 expect (d.getAllContent().contains ("INSERT2"), "1st edit should be restored.");
1169
1170 d.undo();
1171 d.undo();
1172 expectEquals (d.getAllContent(), jabberwocky, "Original document should be restored.");
1173 }
1174
1175 {
1176 beginTest ("Positions");
1177
1178 CodeDocument d;
1179 d.replaceAllContent (jabberwocky);
1180
1181 {
1182 const String comment ("Keeps negative positions inside document.");
1183 CodeDocument::Position p1 (d, 0, -3);
1184 CodeDocument::Position p2 (d, -8);
1185 expectEquals (p1.getLineNumber(), 0, comment);
1186 expectEquals (p1.getIndexInLine(), 0, comment);
1187 expectEquals (p1.getCharacter(), juce_wchar ('\''), comment);
1188 expectEquals (p2.getLineNumber(), 0, comment);
1189 expectEquals (p2.getIndexInLine(), 0, comment);
1190 expectEquals (p2.getCharacter(), juce_wchar ('\''), comment);
1191 }
1192
1193 {
1194 const String comment ("Moving by character handles newlines correctly.");
1195 CodeDocument::Position p1 (d, 0, 35);
1196 p1.moveBy (1);
1197 expectEquals (p1.getLineNumber(), 1, comment);
1198 expectEquals (p1.getIndexInLine(), 0, comment);
1199 p1.moveBy (75);
1200 expectEquals (p1.getLineNumber(), 3, comment);
1201 }
1202
1203 {
1204 const String comment1 ("setPositionMaintained tracks position.");
1205 const String comment2 ("setPositionMaintained tracks position following undos.");
1206
1207 CodeDocument::Position p1 (d, 3, 0);
1208 p1.setPositionMaintained (true);
1209 expectEquals (p1.getCharacter(), juce_wchar ('A'), comment1);
1210
1211 d.newTransaction();
1212 d.insertText (p1, "INSERT1");
1213
1214 expectEquals (p1.getCharacter(), juce_wchar ('A'), comment1);
1215 expectEquals (p1.getLineNumber(), 3, comment1);
1216 expectEquals (p1.getIndexInLine(), 7, comment1);
1217 d.undo();
1218 expectEquals (p1.getIndexInLine(), 0, comment2);
1219
1220 d.newTransaction();
1221 d.insertText (15, "\n");
1222
1223 expectEquals (p1.getLineNumber(), 4, comment1);
1224 d.undo();
1225 expectEquals (p1.getLineNumber(), 3, comment2);
1226 }
1227 }
1228
1229 {
1230 beginTest ("Iterators");
1231
1232 CodeDocument d;
1233 d.replaceAllContent (jabberwocky);
1234
1235 {
1236 const String comment1 ("Basic iteration.");
1237 const String comment2 ("Reverse iteration.");
1238 const String comment3 ("Reverse iteration stops at doc start.");
1239 const String comment4 ("Check iteration across line boundaries.");
1240
1241 CodeDocument::Iterator it (d);
1242 expectEquals (it.peekNextChar(), juce_wchar ('\''), comment1);
1243 expectEquals (it.nextChar(), juce_wchar ('\''), comment1);
1244 expectEquals (it.nextChar(), juce_wchar ('T'), comment1);
1245 expectEquals (it.nextChar(), juce_wchar ('w'), comment1);
1246 expectEquals (it.peekNextChar(), juce_wchar ('a'), comment2);
1247 expectEquals (it.previousChar(), juce_wchar ('w'), comment2);
1248 expectEquals (it.previousChar(), juce_wchar ('T'), comment2);
1249 expectEquals (it.previousChar(), juce_wchar ('\''), comment2);
1250 expectEquals (it.previousChar(), juce_wchar (0), comment3);
1251 expect (it.isSOF(), comment3);
1252
1253 while (it.peekNextChar() != juce_wchar ('D')) // "Did gyre..."
1254 it.nextChar();
1255
1256 expectEquals (it.nextChar(), juce_wchar ('D'), comment3);
1257 expectEquals (it.peekNextChar(), juce_wchar ('i'), comment3);
1258 expectEquals (it.previousChar(), juce_wchar ('D'), comment3);
1259 expectEquals (it.previousChar(), juce_wchar ('\n'), comment3);
1260 expectEquals (it.previousChar(), juce_wchar ('s'), comment3);
1261 }
1262
1263 {
1264 const String comment1 ("Iterator created from CodeDocument::Position objects.");
1265 const String comment2 ("CodeDocument::Position created from Iterator objects.");
1266 const String comment3 ("CodeDocument::Position created from EOF Iterator objects.");
1267
1268 CodeDocument::Position p (d, 6, 0); // "The jaws..."
1269 CodeDocument::Iterator it (p);
1270
1271 expectEquals (it.nextChar(), juce_wchar ('T'), comment1);
1272 expectEquals (it.nextChar(), juce_wchar ('h'), comment1);
1273 expectEquals (it.previousChar(), juce_wchar ('h'), comment1);
1274 expectEquals (it.previousChar(), juce_wchar ('T'), comment1);
1275 expectEquals (it.previousChar(), juce_wchar ('\n'), comment1);
1276 expectEquals (it.previousChar(), juce_wchar ('!'), comment1);
1277
1278 const auto p2 = it.toPosition();
1279 expectEquals (p2.getLineNumber(), 5, comment2);
1280 expectEquals (p2.getIndexInLine(), 30, comment2);
1281
1282 while (! it.isEOF())
1283 it.nextChar();
1284
1285 const auto p3 = it.toPosition();
1286 expectEquals (p3.getLineNumber(), d.getNumLines() - 1, comment3);
1287 expectEquals (p3.getIndexInLine(), d.getLine (d.getNumLines() - 1).length(), comment3);
1288 }
1289 }
1290 }
1291 };
1292
1293 static CodeDocumentTest codeDocumentTests;
1294
1295 #endif
1296
1297 } // namespace juce
1298