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