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 CodeEditorComponent::CodeEditorLine
30 {
31 public:
CodeEditorLine()32     CodeEditorLine() noexcept {}
33 
update(CodeDocument & codeDoc,int lineNum,CodeDocument::Iterator & source,CodeTokeniser * tokeniser,const int tabSpaces,const CodeDocument::Position & selStart,const CodeDocument::Position & selEnd)34     bool update (CodeDocument& codeDoc, int lineNum,
35                  CodeDocument::Iterator& source,
36                  CodeTokeniser* tokeniser, const int tabSpaces,
37                  const CodeDocument::Position& selStart,
38                  const CodeDocument::Position& selEnd)
39     {
40         Array<SyntaxToken> newTokens;
41         newTokens.ensureStorageAllocated (8);
42 
43         if (tokeniser == nullptr)
44         {
45             auto line = codeDoc.getLine (lineNum);
46             addToken (newTokens, line, line.length(), -1);
47         }
48         else if (lineNum < codeDoc.getNumLines())
49         {
50             const CodeDocument::Position pos (codeDoc, lineNum, 0);
51             createTokens (pos.getPosition(), pos.getLineText(),
52                           source, *tokeniser, newTokens);
53         }
54 
55         replaceTabsWithSpaces (newTokens, tabSpaces);
56 
57         int newHighlightStart = 0;
58         int newHighlightEnd = 0;
59 
60         if (selStart.getLineNumber() <= lineNum && selEnd.getLineNumber() >= lineNum)
61         {
62             auto line = codeDoc.getLine (lineNum);
63 
64             CodeDocument::Position lineStart (codeDoc, lineNum, 0), lineEnd (codeDoc, lineNum + 1, 0);
65             newHighlightStart = indexToColumn (jmax (0, selStart.getPosition() - lineStart.getPosition()),
66                                                line, tabSpaces);
67             newHighlightEnd = indexToColumn (jmin (lineEnd.getPosition() - lineStart.getPosition(), selEnd.getPosition() - lineStart.getPosition()),
68                                              line, tabSpaces);
69         }
70 
71         if (newHighlightStart != highlightColumnStart || newHighlightEnd != highlightColumnEnd)
72         {
73             highlightColumnStart = newHighlightStart;
74             highlightColumnEnd = newHighlightEnd;
75         }
76         else if (tokens == newTokens)
77         {
78             return false;
79         }
80 
81         tokens.swapWith (newTokens);
82         return true;
83     }
84 
getHighlightArea(RectangleList<float> & area,float x,int y,int lineH,float characterWidth) const85     void getHighlightArea (RectangleList<float>& area, float x, int y, int lineH, float characterWidth) const
86     {
87         if (highlightColumnStart < highlightColumnEnd)
88             area.add (Rectangle<float> (x + (float) highlightColumnStart * characterWidth - 1.0f, (float) y - 0.5f,
89                                         (float) (highlightColumnEnd - highlightColumnStart) * characterWidth + 1.5f, (float) lineH + 1.0f));
90     }
91 
draw(CodeEditorComponent & owner,Graphics & g,const Font & fontToUse,const float rightClip,const float x,const int y,const int lineH,const float characterWidth) const92     void draw (CodeEditorComponent& owner, Graphics& g, const Font& fontToUse,
93                const float rightClip, const float x, const int y,
94                const int lineH, const float characterWidth) const
95     {
96         AttributedString as;
97         as.setJustification (Justification::centredLeft);
98 
99         int column = 0;
100 
101         for (auto& token : tokens)
102         {
103             const float tokenX = x + (float) column * characterWidth;
104             if (tokenX > rightClip)
105                 break;
106 
107             as.append (token.text.initialSectionNotContaining ("\r\n"), fontToUse, owner.getColourForTokenType (token.tokenType));
108             column += token.length;
109         }
110 
111         as.draw (g, { x, (float) y, (float) column * characterWidth + 10.0f, (float) lineH });
112     }
113 
114 private:
115     struct SyntaxToken
116     {
SyntaxTokenjuce::CodeEditorComponent::CodeEditorLine::SyntaxToken117         SyntaxToken (const String& t, const int len, const int type) noexcept
118             : text (t), length (len), tokenType (type)
119         {}
120 
operator ==juce::CodeEditorComponent::CodeEditorLine::SyntaxToken121         bool operator== (const SyntaxToken& other) const noexcept
122         {
123             return tokenType == other.tokenType
124                     && length == other.length
125                     && text == other.text;
126         }
127 
128         String text;
129         int length;
130         int tokenType;
131     };
132 
133     Array<SyntaxToken> tokens;
134     int highlightColumnStart = 0, highlightColumnEnd = 0;
135 
createTokens(int startPosition,const String & lineText,CodeDocument::Iterator & source,CodeTokeniser & tokeniser,Array<SyntaxToken> & newTokens)136     static void createTokens (int startPosition, const String& lineText,
137                               CodeDocument::Iterator& source,
138                               CodeTokeniser& tokeniser,
139                               Array<SyntaxToken>& newTokens)
140     {
141         CodeDocument::Iterator lastIterator (source);
142         const int lineLength = lineText.length();
143 
144         for (;;)
145         {
146             int tokenType = tokeniser.readNextToken (source);
147             int tokenStart = lastIterator.getPosition();
148             int tokenEnd = source.getPosition();
149 
150             if (tokenEnd <= tokenStart)
151                 break;
152 
153             tokenEnd -= startPosition;
154 
155             if (tokenEnd > 0)
156             {
157                 tokenStart -= startPosition;
158                 const int start = jmax (0, tokenStart);
159                 addToken (newTokens, lineText.substring (start, tokenEnd),
160                           tokenEnd - start, tokenType);
161 
162                 if (tokenEnd >= lineLength)
163                     break;
164             }
165 
166             lastIterator = source;
167         }
168 
169         source = lastIterator;
170     }
171 
replaceTabsWithSpaces(Array<SyntaxToken> & tokens,const int spacesPerTab)172     static void replaceTabsWithSpaces (Array<SyntaxToken>& tokens, const int spacesPerTab)
173     {
174         int x = 0;
175 
176         for (auto& t : tokens)
177         {
178             for (;;)
179             {
180                 const int tabPos = t.text.indexOfChar ('\t');
181                 if (tabPos < 0)
182                     break;
183 
184                 const int spacesNeeded = spacesPerTab - ((tabPos + x) % spacesPerTab);
185                 t.text = t.text.replaceSection (tabPos, 1, String::repeatedString (" ", spacesNeeded));
186                 t.length = t.text.length();
187             }
188 
189             x += t.length;
190         }
191     }
192 
indexToColumn(int index,const String & line,int tabSpaces) const193     int indexToColumn (int index, const String& line, int tabSpaces) const noexcept
194     {
195         jassert (index <= line.length());
196 
197         auto t = line.getCharPointer();
198         int col = 0;
199 
200         for (int i = 0; i < index; ++i)
201         {
202             if (t.getAndAdvance() != '\t')
203                 ++col;
204             else
205                 col += tabSpaces - (col % tabSpaces);
206         }
207 
208         return col;
209     }
210 
addToken(Array<SyntaxToken> & dest,const String & text,int length,int type)211     static void addToken (Array<SyntaxToken>& dest, const String& text, int length, int type)
212     {
213         if (length > 1000)
214         {
215             // subdivide very long tokens to avoid unwieldy glyph sequences
216             addToken (dest, text.substring (0, length / 2), length / 2, type);
217             addToken (dest, text.substring (length / 2), length - length / 2, type);
218         }
219         else
220         {
221             dest.add (SyntaxToken (text, length, type));
222         }
223     }
224 };
225 
226 namespace CodeEditorHelpers
227 {
findFirstNonWhitespaceChar(StringRef line)228     static int findFirstNonWhitespaceChar (StringRef line) noexcept
229     {
230         auto t = line.text;
231         int i = 0;
232 
233         while (! t.isEmpty())
234         {
235             if (! t.isWhitespace())
236                 return i;
237 
238             ++t;
239             ++i;
240         }
241 
242         return 0;
243     }
244 }
245 
246 //==============================================================================
247 class CodeEditorComponent::Pimpl   : public Timer,
248                                      public AsyncUpdater,
249                                      public ScrollBar::Listener,
250                                      public CodeDocument::Listener
251 {
252 public:
Pimpl(CodeEditorComponent & ed)253     Pimpl (CodeEditorComponent& ed) : owner (ed) {}
254 
255 private:
256     CodeEditorComponent& owner;
257 
timerCallback()258     void timerCallback() override        { owner.newTransaction(); }
handleAsyncUpdate()259     void handleAsyncUpdate() override    { owner.rebuildLineTokens(); }
260 
scrollBarMoved(ScrollBar * scrollBarThatHasMoved,double newRangeStart)261     void scrollBarMoved (ScrollBar* scrollBarThatHasMoved, double newRangeStart) override
262     {
263         if (scrollBarThatHasMoved->isVertical())
264             owner.scrollToLineInternal ((int) newRangeStart);
265         else
266             owner.scrollToColumnInternal (newRangeStart);
267     }
268 
codeDocumentTextInserted(const String & newText,int pos)269     void codeDocumentTextInserted (const String& newText, int pos) override
270     {
271         owner.codeDocumentChanged (pos, pos + newText.length());
272     }
273 
codeDocumentTextDeleted(int start,int end)274     void codeDocumentTextDeleted (int start, int end) override
275     {
276         owner.codeDocumentChanged (start, end);
277     }
278 
279     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl)
280 };
281 
282 //==============================================================================
283 class CodeEditorComponent::GutterComponent  : public Component
284 {
285 public:
GutterComponent()286     GutterComponent() {}
287 
paint(Graphics & g)288     void paint (Graphics& g) override
289     {
290         jassert (dynamic_cast<CodeEditorComponent*> (getParentComponent()) != nullptr);
291         auto& editor = *static_cast<CodeEditorComponent*> (getParentComponent());
292 
293         g.fillAll (editor.findColour (CodeEditorComponent::backgroundColourId)
294                     .overlaidWith (editor.findColour (lineNumberBackgroundId)));
295 
296         auto clip = g.getClipBounds();
297         const int lineH = editor.lineHeight;
298         const float lineHeightFloat = (float) lineH;
299         const int firstLineToDraw = jmax (0, clip.getY() / lineH);
300         const int lastLineToDraw = jmin (editor.lines.size(), clip.getBottom() / lineH + 1,
301                                          lastNumLines - editor.firstLineOnScreen);
302 
303         auto lineNumberFont = editor.getFont().withHeight (jmin (13.0f, lineHeightFloat * 0.8f));
304         auto w = (float) getWidth() - 2.0f;
305         GlyphArrangement ga;
306 
307         for (int i = firstLineToDraw; i < lastLineToDraw; ++i)
308             ga.addFittedText (lineNumberFont, String (editor.firstLineOnScreen + i + 1),
309                               0, (float) (lineH * i), w, lineHeightFloat,
310                               Justification::centredRight, 1, 0.2f);
311 
312         g.setColour (editor.findColour (lineNumberTextId));
313         ga.draw (g);
314     }
315 
documentChanged(CodeDocument & doc,int newFirstLine)316     void documentChanged (CodeDocument& doc, int newFirstLine)
317     {
318         auto newNumLines = doc.getNumLines();
319 
320         if (newNumLines != lastNumLines || firstLine != newFirstLine)
321         {
322             firstLine = newFirstLine;
323             lastNumLines = newNumLines;
324             repaint();
325         }
326     }
327 
328 private:
329     int firstLine = 0, lastNumLines = 0;
330 };
331 
332 
333 //==============================================================================
CodeEditorComponent(CodeDocument & doc,CodeTokeniser * const tokeniser)334 CodeEditorComponent::CodeEditorComponent (CodeDocument& doc, CodeTokeniser* const tokeniser)
335     : document (doc),
336       caretPos (doc, 0, 0),
337       selectionStart (doc, 0, 0),
338       selectionEnd (doc, 0, 0),
339       codeTokeniser (tokeniser)
340 {
341     pimpl.reset (new Pimpl (*this));
342 
343     caretPos.setPositionMaintained (true);
344     selectionStart.setPositionMaintained (true);
345     selectionEnd.setPositionMaintained (true);
346 
347     setOpaque (true);
348     setMouseCursor (MouseCursor::IBeamCursor);
349     setWantsKeyboardFocus (true);
350 
351     lookAndFeelChanged();
352     addAndMakeVisible (caret.get());
353 
354     addAndMakeVisible (verticalScrollBar);
355     verticalScrollBar.setSingleStepSize (1.0);
356 
357     addAndMakeVisible (horizontalScrollBar);
358     horizontalScrollBar.setSingleStepSize (1.0);
359 
360     Font f (12.0f);
361     f.setTypefaceName (Font::getDefaultMonospacedFontName());
362     setFont (f);
363 
364     if (codeTokeniser != nullptr)
365         setColourScheme (codeTokeniser->getDefaultColourScheme());
366 
367     setLineNumbersShown (true);
368 
369     verticalScrollBar.addListener (pimpl.get());
370     horizontalScrollBar.addListener (pimpl.get());
371     document.addListener (pimpl.get());
372 }
373 
~CodeEditorComponent()374 CodeEditorComponent::~CodeEditorComponent()
375 {
376     document.removeListener (pimpl.get());
377 }
378 
getGutterSize() const379 int CodeEditorComponent::getGutterSize() const noexcept
380 {
381     return showLineNumbers ? 35 : 5;
382 }
383 
loadContent(const String & newContent)384 void CodeEditorComponent::loadContent (const String& newContent)
385 {
386     clearCachedIterators (0);
387     document.replaceAllContent (newContent);
388     document.clearUndoHistory();
389     document.setSavePoint();
390     caretPos.setPosition (0);
391     selectionStart.setPosition (0);
392     selectionEnd.setPosition (0);
393     scrollToLine (0);
394 }
395 
isTextInputActive() const396 bool CodeEditorComponent::isTextInputActive() const
397 {
398     return true;
399 }
400 
setTemporaryUnderlining(const Array<Range<int>> &)401 void CodeEditorComponent::setTemporaryUnderlining (const Array<Range<int>>&)
402 {
403     jassertfalse; // TODO Windows IME not yet supported for this comp..
404 }
405 
getCaretRectangle()406 Rectangle<int> CodeEditorComponent::getCaretRectangle()
407 {
408     return getLocalArea (caret.get(), caret->getLocalBounds());
409 }
410 
setLineNumbersShown(const bool shouldBeShown)411 void CodeEditorComponent::setLineNumbersShown (const bool shouldBeShown)
412 {
413     if (showLineNumbers != shouldBeShown)
414     {
415         showLineNumbers = shouldBeShown;
416         gutter.reset();
417 
418         if (shouldBeShown)
419         {
420             gutter.reset (new GutterComponent());
421             addAndMakeVisible (gutter.get());
422         }
423 
424         resized();
425     }
426 }
427 
setReadOnly(bool b)428 void CodeEditorComponent::setReadOnly (bool b) noexcept
429 {
430     if (readOnly != b)
431     {
432         readOnly = b;
433 
434         if (b)
435             removeChildComponent (caret.get());
436         else
437             addAndMakeVisible (caret.get());
438     }
439 }
440 
441 //==============================================================================
resized()442 void CodeEditorComponent::resized()
443 {
444     auto visibleWidth = getWidth() - scrollbarThickness - getGutterSize();
445     linesOnScreen   = jmax (1, (getHeight() - scrollbarThickness) / lineHeight);
446     columnsOnScreen = jmax (1, (int) ((float) visibleWidth / charWidth));
447     lines.clear();
448     rebuildLineTokens();
449     updateCaretPosition();
450 
451     if (gutter != nullptr)
452         gutter->setBounds (0, 0, getGutterSize() - 2, getHeight());
453 
454     verticalScrollBar.setBounds (getWidth() - scrollbarThickness, 0,
455                                  scrollbarThickness, getHeight() - scrollbarThickness);
456 
457     horizontalScrollBar.setBounds (getGutterSize(), getHeight() - scrollbarThickness,
458                                    visibleWidth, scrollbarThickness);
459     updateScrollBars();
460 }
461 
paint(Graphics & g)462 void CodeEditorComponent::paint (Graphics& g)
463 {
464     g.fillAll (findColour (CodeEditorComponent::backgroundColourId));
465 
466     auto gutterSize = getGutterSize();
467     auto bottom = horizontalScrollBar.isVisible() ? horizontalScrollBar.getY() : getHeight();
468     auto right  = verticalScrollBar.isVisible()   ? verticalScrollBar.getX()   : getWidth();
469 
470     g.reduceClipRegion (gutterSize, 0, right - gutterSize, bottom);
471 
472     g.setFont (font);
473 
474     auto clip = g.getClipBounds();
475     auto firstLineToDraw = jmax (0, clip.getY() / lineHeight);
476     auto lastLineToDraw  = jmin (lines.size(), clip.getBottom() / lineHeight + 1);
477     auto x = (float) (gutterSize - xOffset * charWidth);
478     auto rightClip = (float) clip.getRight();
479 
480     {
481         RectangleList<float> highlightArea;
482 
483         for (int i = firstLineToDraw; i < lastLineToDraw; ++i)
484             lines.getUnchecked(i)->getHighlightArea (highlightArea, x, lineHeight * i, lineHeight, charWidth);
485 
486         g.setColour (findColour (CodeEditorComponent::highlightColourId));
487         g.fillRectList (highlightArea);
488     }
489 
490     for (int i = firstLineToDraw; i < lastLineToDraw; ++i)
491         lines.getUnchecked(i)->draw (*this, g, font, rightClip, x, lineHeight * i, lineHeight, charWidth);
492 }
493 
setScrollbarThickness(const int thickness)494 void CodeEditorComponent::setScrollbarThickness (const int thickness)
495 {
496     if (scrollbarThickness != thickness)
497     {
498         scrollbarThickness = thickness;
499         resized();
500     }
501 }
502 
rebuildLineTokensAsync()503 void CodeEditorComponent::rebuildLineTokensAsync()
504 {
505     pimpl->triggerAsyncUpdate();
506 }
507 
rebuildLineTokens()508 void CodeEditorComponent::rebuildLineTokens()
509 {
510     pimpl->cancelPendingUpdate();
511 
512     auto numNeeded = linesOnScreen + 1;
513     auto minLineToRepaint = numNeeded;
514     int maxLineToRepaint = 0;
515 
516     if (numNeeded != lines.size())
517     {
518         lines.clear();
519 
520         for (int i = numNeeded; --i >= 0;)
521             lines.add (new CodeEditorLine());
522 
523         minLineToRepaint = 0;
524         maxLineToRepaint = numNeeded;
525     }
526 
527     jassert (numNeeded == lines.size());
528 
529     CodeDocument::Iterator source (document);
530     getIteratorForPosition (CodeDocument::Position (document, firstLineOnScreen, 0).getPosition(), source);
531 
532     for (int i = 0; i < numNeeded; ++i)
533     {
534         if (lines.getUnchecked(i)->update (document, firstLineOnScreen + i, source, codeTokeniser,
535                                            spacesPerTab, selectionStart, selectionEnd))
536         {
537             minLineToRepaint = jmin (minLineToRepaint, i);
538             maxLineToRepaint = jmax (maxLineToRepaint, i);
539         }
540     }
541 
542     if (minLineToRepaint <= maxLineToRepaint)
543         repaint (0, lineHeight * minLineToRepaint - 1,
544                  verticalScrollBar.getX(), lineHeight * (1 + maxLineToRepaint - minLineToRepaint) + 2);
545 
546     if (gutter != nullptr)
547         gutter->documentChanged (document, firstLineOnScreen);
548 }
549 
codeDocumentChanged(const int startIndex,const int endIndex)550 void CodeEditorComponent::codeDocumentChanged (const int startIndex, const int endIndex)
551 {
552     const CodeDocument::Position affectedTextStart (document, startIndex);
553     const CodeDocument::Position affectedTextEnd (document, endIndex);
554 
555     retokenise (startIndex, endIndex);
556 
557     updateCaretPosition();
558     columnToTryToMaintain = -1;
559 
560     if (affectedTextEnd.getPosition() >= selectionStart.getPosition()
561          && affectedTextStart.getPosition() <= selectionEnd.getPosition())
562         deselectAll();
563 
564     if (shouldFollowDocumentChanges)
565         if (caretPos.getPosition() > affectedTextEnd.getPosition()
566             || caretPos.getPosition() < affectedTextStart.getPosition())
567             moveCaretTo (affectedTextStart, false);
568 
569     updateScrollBars();
570 }
571 
retokenise(int startIndex,int endIndex)572 void CodeEditorComponent::retokenise (int startIndex, int endIndex)
573 {
574     const CodeDocument::Position affectedTextStart (document, startIndex);
575     juce::ignoreUnused (endIndex); // Leave room for more efficient impl in future.
576 
577     clearCachedIterators (affectedTextStart.getLineNumber());
578 
579     rebuildLineTokensAsync();
580 }
581 
582 //==============================================================================
updateCaretPosition()583 void CodeEditorComponent::updateCaretPosition()
584 {
585     caret->setCaretPosition (getCharacterBounds (getCaretPos()));
586 }
587 
moveCaretTo(const CodeDocument::Position & newPos,const bool highlighting)588 void CodeEditorComponent::moveCaretTo (const CodeDocument::Position& newPos, const bool highlighting)
589 {
590     caretPos = newPos;
591     columnToTryToMaintain = -1;
592     bool selectionWasActive = isHighlightActive();
593 
594     if (highlighting)
595     {
596         if (dragType == notDragging)
597         {
598             if (std::abs (caretPos.getPosition() - selectionStart.getPosition())
599                   < std::abs (caretPos.getPosition() - selectionEnd.getPosition()))
600                 dragType = draggingSelectionStart;
601             else
602                 dragType = draggingSelectionEnd;
603         }
604 
605         if (dragType == draggingSelectionStart)
606         {
607             selectionStart = caretPos;
608 
609             if (selectionEnd.getPosition() < selectionStart.getPosition())
610             {
611                 auto temp = selectionStart;
612                 selectionStart = selectionEnd;
613                 selectionEnd = temp;
614 
615                 dragType = draggingSelectionEnd;
616             }
617         }
618         else
619         {
620             selectionEnd = caretPos;
621 
622             if (selectionEnd.getPosition() < selectionStart.getPosition())
623             {
624                 auto temp = selectionStart;
625                 selectionStart = selectionEnd;
626                 selectionEnd = temp;
627 
628                 dragType = draggingSelectionStart;
629             }
630         }
631 
632         rebuildLineTokensAsync();
633     }
634     else
635     {
636         deselectAll();
637     }
638 
639     updateCaretPosition();
640     scrollToKeepCaretOnScreen();
641     updateScrollBars();
642     caretPositionMoved();
643 
644     if (appCommandManager != nullptr && selectionWasActive != isHighlightActive())
645         appCommandManager->commandStatusChanged();
646 }
647 
deselectAll()648 void CodeEditorComponent::deselectAll()
649 {
650     if (isHighlightActive())
651         rebuildLineTokensAsync();
652 
653     selectionStart = caretPos;
654     selectionEnd = caretPos;
655     dragType = notDragging;
656 }
657 
updateScrollBars()658 void CodeEditorComponent::updateScrollBars()
659 {
660     verticalScrollBar.setRangeLimits (0, jmax (document.getNumLines(), firstLineOnScreen + linesOnScreen));
661     verticalScrollBar.setCurrentRange (firstLineOnScreen, linesOnScreen);
662 
663     horizontalScrollBar.setRangeLimits (0, jmax ((double) document.getMaximumLineLength(), xOffset + columnsOnScreen));
664     horizontalScrollBar.setCurrentRange (xOffset, columnsOnScreen);
665 }
666 
scrollToLineInternal(int newFirstLineOnScreen)667 void CodeEditorComponent::scrollToLineInternal (int newFirstLineOnScreen)
668 {
669     newFirstLineOnScreen = jlimit (0, jmax (0, document.getNumLines() - 1),
670                                    newFirstLineOnScreen);
671 
672     if (newFirstLineOnScreen != firstLineOnScreen)
673     {
674         firstLineOnScreen = newFirstLineOnScreen;
675         updateCaretPosition();
676 
677         updateCachedIterators (firstLineOnScreen);
678         rebuildLineTokensAsync();
679         pimpl->handleUpdateNowIfNeeded();
680 
681         editorViewportPositionChanged();
682     }
683 }
684 
scrollToColumnInternal(double column)685 void CodeEditorComponent::scrollToColumnInternal (double column)
686 {
687     const double newOffset = jlimit (0.0, document.getMaximumLineLength() + 3.0, column);
688 
689     if (xOffset != newOffset)
690     {
691         xOffset = newOffset;
692         updateCaretPosition();
693         repaint();
694     }
695 }
696 
scrollToLine(int newFirstLineOnScreen)697 void CodeEditorComponent::scrollToLine (int newFirstLineOnScreen)
698 {
699     scrollToLineInternal (newFirstLineOnScreen);
700     updateScrollBars();
701 }
702 
scrollToColumn(int newFirstColumnOnScreen)703 void CodeEditorComponent::scrollToColumn (int newFirstColumnOnScreen)
704 {
705     scrollToColumnInternal (newFirstColumnOnScreen);
706     updateScrollBars();
707 }
708 
scrollBy(int deltaLines)709 void CodeEditorComponent::scrollBy (int deltaLines)
710 {
711     scrollToLine (firstLineOnScreen + deltaLines);
712 }
713 
scrollToKeepLinesOnScreen(Range<int> rangeToShow)714 void CodeEditorComponent::scrollToKeepLinesOnScreen (Range<int> rangeToShow)
715 {
716     if (rangeToShow.getStart() < firstLineOnScreen)
717         scrollBy (rangeToShow.getStart() - firstLineOnScreen);
718     else if (rangeToShow.getEnd() >= firstLineOnScreen + linesOnScreen)
719         scrollBy (rangeToShow.getEnd() - (firstLineOnScreen + linesOnScreen - 1));
720 }
721 
scrollToKeepCaretOnScreen()722 void CodeEditorComponent::scrollToKeepCaretOnScreen()
723 {
724     if (getWidth() > 0 && getHeight() > 0)
725     {
726         auto caretLine = caretPos.getLineNumber();
727         scrollToKeepLinesOnScreen ({ caretLine, caretLine });
728 
729         auto column = indexToColumn (caretPos.getLineNumber(), caretPos.getIndexInLine());
730 
731         if (column >= xOffset + columnsOnScreen - 1)
732             scrollToColumn (column + 1 - columnsOnScreen);
733         else if (column < xOffset)
734             scrollToColumn (column);
735     }
736 }
737 
getCharacterBounds(const CodeDocument::Position & pos) const738 Rectangle<int> CodeEditorComponent::getCharacterBounds (const CodeDocument::Position& pos) const
739 {
740     return { roundToInt ((getGutterSize() - xOffset * charWidth) + (float) indexToColumn (pos.getLineNumber(), pos.getIndexInLine()) * charWidth),
741              (pos.getLineNumber() - firstLineOnScreen) * lineHeight,
742              roundToInt (charWidth),
743              lineHeight };
744 }
745 
getPositionAt(int x,int y)746 CodeDocument::Position CodeEditorComponent::getPositionAt (int x, int y)
747 {
748     const int line = y / lineHeight + firstLineOnScreen;
749     const int column = roundToInt ((x - (getGutterSize() - xOffset * charWidth)) / charWidth);
750     const int index = columnToIndex (line, column);
751 
752     return CodeDocument::Position (document, line, index);
753 }
754 
755 //==============================================================================
insertTextAtCaret(const String & newText)756 void CodeEditorComponent::insertTextAtCaret (const String& newText)
757 {
758     insertText (newText);
759 }
760 
insertText(const String & newText)761 void CodeEditorComponent::insertText (const String& newText)
762 {
763     if (! readOnly)
764     {
765         document.deleteSection (selectionStart, selectionEnd);
766 
767         if (newText.isNotEmpty())
768             document.insertText (caretPos, newText);
769 
770         scrollToKeepCaretOnScreen();
771         caretPositionMoved();
772     }
773 }
774 
insertTabAtCaret()775 void CodeEditorComponent::insertTabAtCaret()
776 {
777     if (! readOnly)
778     {
779         if (CharacterFunctions::isWhitespace (caretPos.getCharacter())
780              && caretPos.getLineNumber() == caretPos.movedBy (1).getLineNumber())
781         {
782             moveCaretTo (document.findWordBreakAfter (caretPos), false);
783         }
784 
785         if (useSpacesForTabs)
786         {
787             auto caretCol = indexToColumn (caretPos.getLineNumber(), caretPos.getIndexInLine());
788             auto spacesNeeded = spacesPerTab - (caretCol % spacesPerTab);
789             insertTextAtCaret (String::repeatedString (" ", spacesNeeded));
790         }
791         else
792         {
793             insertTextAtCaret ("\t");
794         }
795     }
796 }
797 
deleteWhitespaceBackwardsToTabStop()798 bool CodeEditorComponent::deleteWhitespaceBackwardsToTabStop()
799 {
800     if (getHighlightedRegion().isEmpty() && ! readOnly)
801     {
802         for (;;)
803         {
804             auto currentColumn = indexToColumn (caretPos.getLineNumber(), caretPos.getIndexInLine());
805 
806             if (currentColumn <= 0 || (currentColumn % spacesPerTab) == 0)
807                 break;
808 
809             moveCaretLeft (false, true);
810         }
811 
812         auto selected = getTextInRange (getHighlightedRegion());
813 
814         if (selected.isNotEmpty() && selected.trim().isEmpty())
815         {
816             cut();
817             return true;
818         }
819     }
820 
821     return false;
822 }
823 
indentSelection()824 void CodeEditorComponent::indentSelection()     { indentSelectedLines ( spacesPerTab); }
unindentSelection()825 void CodeEditorComponent::unindentSelection()   { indentSelectedLines (-spacesPerTab); }
826 
indentSelectedLines(const int spacesToAdd)827 void CodeEditorComponent::indentSelectedLines (const int spacesToAdd)
828 {
829     if (! readOnly)
830     {
831         newTransaction();
832 
833         CodeDocument::Position oldSelectionStart (selectionStart), oldSelectionEnd (selectionEnd), oldCaret (caretPos);
834         oldSelectionStart.setPositionMaintained (true);
835         oldSelectionEnd.setPositionMaintained (true);
836         oldCaret.setPositionMaintained (true);
837 
838         const int lineStart = selectionStart.getLineNumber();
839         int lineEnd = selectionEnd.getLineNumber();
840 
841         if (lineEnd > lineStart && selectionEnd.getIndexInLine() == 0)
842             --lineEnd;
843 
844         for (int line = lineStart; line <= lineEnd; ++line)
845         {
846             auto lineText = document.getLine (line);
847             auto nonWhitespaceStart = CodeEditorHelpers::findFirstNonWhitespaceChar (lineText);
848 
849             if (nonWhitespaceStart > 0 || lineText.trimStart().isNotEmpty())
850             {
851                 const CodeDocument::Position wsStart (document, line, 0);
852                 const CodeDocument::Position wsEnd   (document, line, nonWhitespaceStart);
853 
854                 const int numLeadingSpaces = indexToColumn (line, wsEnd.getIndexInLine());
855                 const int newNumLeadingSpaces = jmax (0, numLeadingSpaces + spacesToAdd);
856 
857                 if (newNumLeadingSpaces != numLeadingSpaces)
858                 {
859                     document.deleteSection (wsStart, wsEnd);
860                     document.insertText (wsStart, getTabString (newNumLeadingSpaces));
861                 }
862             }
863         }
864 
865         selectionStart = oldSelectionStart;
866         selectionEnd = oldSelectionEnd;
867         caretPos = oldCaret;
868     }
869 }
870 
cut()871 void CodeEditorComponent::cut()
872 {
873     insertText ({});
874 }
875 
copyToClipboard()876 bool CodeEditorComponent::copyToClipboard()
877 {
878     newTransaction();
879     auto selection = document.getTextBetween (selectionStart, selectionEnd);
880 
881     if (selection.isNotEmpty())
882         SystemClipboard::copyTextToClipboard (selection);
883 
884     return true;
885 }
886 
cutToClipboard()887 bool CodeEditorComponent::cutToClipboard()
888 {
889     copyToClipboard();
890     cut();
891     newTransaction();
892     return true;
893 }
894 
pasteFromClipboard()895 bool CodeEditorComponent::pasteFromClipboard()
896 {
897     newTransaction();
898     auto clip = SystemClipboard::getTextFromClipboard();
899 
900     if (clip.isNotEmpty())
901         insertText (clip);
902 
903     newTransaction();
904     return true;
905 }
906 
moveCaretLeft(const bool moveInWholeWordSteps,const bool selecting)907 bool CodeEditorComponent::moveCaretLeft (const bool moveInWholeWordSteps, const bool selecting)
908 {
909     newTransaction();
910 
911     if (selecting && dragType == notDragging)
912     {
913         selectRegion (CodeDocument::Position (selectionEnd), CodeDocument::Position (selectionStart));
914         dragType = draggingSelectionStart;
915     }
916 
917     if (isHighlightActive() && ! (selecting || moveInWholeWordSteps))
918     {
919         moveCaretTo (selectionStart, false);
920         return true;
921     }
922 
923     if (moveInWholeWordSteps)
924         moveCaretTo (document.findWordBreakBefore (caretPos), selecting);
925     else
926         moveCaretTo (caretPos.movedBy (-1), selecting);
927 
928     return true;
929 }
930 
moveCaretRight(const bool moveInWholeWordSteps,const bool selecting)931 bool CodeEditorComponent::moveCaretRight (const bool moveInWholeWordSteps, const bool selecting)
932 {
933     newTransaction();
934 
935     if (selecting && dragType == notDragging)
936     {
937         selectRegion (CodeDocument::Position (selectionStart), CodeDocument::Position (selectionEnd));
938         dragType = draggingSelectionEnd;
939     }
940 
941     if (isHighlightActive() && ! (selecting || moveInWholeWordSteps))
942     {
943         moveCaretTo (selectionEnd, false);
944         return true;
945     }
946 
947     if (moveInWholeWordSteps)
948         moveCaretTo (document.findWordBreakAfter (caretPos), selecting);
949     else
950         moveCaretTo (caretPos.movedBy (1), selecting);
951 
952     return true;
953 }
954 
moveLineDelta(const int delta,const bool selecting)955 void CodeEditorComponent::moveLineDelta (const int delta, const bool selecting)
956 {
957     CodeDocument::Position pos (caretPos);
958     auto newLineNum = pos.getLineNumber() + delta;
959 
960     if (columnToTryToMaintain < 0)
961         columnToTryToMaintain = indexToColumn (pos.getLineNumber(), pos.getIndexInLine());
962 
963     pos.setLineAndIndex (newLineNum, columnToIndex (newLineNum, columnToTryToMaintain));
964 
965     auto colToMaintain = columnToTryToMaintain;
966     moveCaretTo (pos, selecting);
967     columnToTryToMaintain = colToMaintain;
968 }
969 
moveCaretDown(const bool selecting)970 bool CodeEditorComponent::moveCaretDown (const bool selecting)
971 {
972     newTransaction();
973 
974     if (caretPos.getLineNumber() == document.getNumLines() - 1)
975         moveCaretTo (CodeDocument::Position (document, std::numeric_limits<int>::max(), std::numeric_limits<int>::max()), selecting);
976     else
977         moveLineDelta (1, selecting);
978 
979     return true;
980 }
981 
moveCaretUp(const bool selecting)982 bool CodeEditorComponent::moveCaretUp (const bool selecting)
983 {
984     newTransaction();
985 
986     if (caretPos.getLineNumber() == 0)
987         moveCaretTo (CodeDocument::Position (document, 0, 0), selecting);
988     else
989         moveLineDelta (-1, selecting);
990 
991     return true;
992 }
993 
pageDown(const bool selecting)994 bool CodeEditorComponent::pageDown (const bool selecting)
995 {
996     newTransaction();
997     scrollBy (jlimit (0, linesOnScreen, 1 + document.getNumLines() - firstLineOnScreen - linesOnScreen));
998     moveLineDelta (linesOnScreen, selecting);
999     return true;
1000 }
1001 
pageUp(const bool selecting)1002 bool CodeEditorComponent::pageUp (const bool selecting)
1003 {
1004     newTransaction();
1005     scrollBy (-linesOnScreen);
1006     moveLineDelta (-linesOnScreen, selecting);
1007     return true;
1008 }
1009 
scrollUp()1010 bool CodeEditorComponent::scrollUp()
1011 {
1012     newTransaction();
1013     scrollBy (1);
1014 
1015     if (caretPos.getLineNumber() < firstLineOnScreen)
1016         moveLineDelta (1, false);
1017 
1018     return true;
1019 }
1020 
scrollDown()1021 bool CodeEditorComponent::scrollDown()
1022 {
1023     newTransaction();
1024     scrollBy (-1);
1025 
1026     if (caretPos.getLineNumber() >= firstLineOnScreen + linesOnScreen)
1027         moveLineDelta (-1, false);
1028 
1029     return true;
1030 }
1031 
moveCaretToTop(const bool selecting)1032 bool CodeEditorComponent::moveCaretToTop (const bool selecting)
1033 {
1034     newTransaction();
1035     moveCaretTo (CodeDocument::Position (document, 0, 0), selecting);
1036     return true;
1037 }
1038 
moveCaretToStartOfLine(const bool selecting)1039 bool CodeEditorComponent::moveCaretToStartOfLine (const bool selecting)
1040 {
1041     newTransaction();
1042 
1043     int index = CodeEditorHelpers::findFirstNonWhitespaceChar (caretPos.getLineText());
1044 
1045     if (index >= caretPos.getIndexInLine() && caretPos.getIndexInLine() > 0)
1046         index = 0;
1047 
1048     moveCaretTo (CodeDocument::Position (document, caretPos.getLineNumber(), index), selecting);
1049     return true;
1050 }
1051 
moveCaretToEnd(const bool selecting)1052 bool CodeEditorComponent::moveCaretToEnd (const bool selecting)
1053 {
1054     newTransaction();
1055     moveCaretTo (CodeDocument::Position (document, std::numeric_limits<int>::max(),
1056                                          std::numeric_limits<int>::max()), selecting);
1057     return true;
1058 }
1059 
moveCaretToEndOfLine(const bool selecting)1060 bool CodeEditorComponent::moveCaretToEndOfLine (const bool selecting)
1061 {
1062     newTransaction();
1063     moveCaretTo (CodeDocument::Position (document, caretPos.getLineNumber(),
1064                                          std::numeric_limits<int>::max()), selecting);
1065     return true;
1066 }
1067 
deleteBackwards(const bool moveInWholeWordSteps)1068 bool CodeEditorComponent::deleteBackwards (const bool moveInWholeWordSteps)
1069 {
1070     if (moveInWholeWordSteps)
1071     {
1072         cut(); // in case something is already highlighted
1073         moveCaretTo (document.findWordBreakBefore (caretPos), true);
1074     }
1075     else if (selectionStart == selectionEnd && ! skipBackwardsToPreviousTab())
1076     {
1077         selectionStart.moveBy (-1);
1078     }
1079 
1080     cut();
1081     return true;
1082 }
1083 
skipBackwardsToPreviousTab()1084 bool CodeEditorComponent::skipBackwardsToPreviousTab()
1085 {
1086     auto currentLineText = caretPos.getLineText().removeCharacters ("\r\n");
1087     auto currentIndex = caretPos.getIndexInLine();
1088 
1089     if (currentLineText.isNotEmpty() && currentLineText.length() == currentIndex)
1090     {
1091         const int currentLine = caretPos.getLineNumber();
1092         const int currentColumn = indexToColumn (currentLine, currentIndex);
1093         const int previousTabColumn = (currentColumn - 1) - ((currentColumn - 1) % spacesPerTab);
1094         const int previousTabIndex = columnToIndex (currentLine, previousTabColumn);
1095 
1096         if (currentLineText.substring (previousTabIndex, currentIndex).trim().isEmpty())
1097         {
1098             selectionStart.moveBy (previousTabIndex - currentIndex);
1099             return true;
1100         }
1101     }
1102 
1103     return false;
1104 }
1105 
deleteForwards(const bool moveInWholeWordSteps)1106 bool CodeEditorComponent::deleteForwards (const bool moveInWholeWordSteps)
1107 {
1108     if (moveInWholeWordSteps)
1109     {
1110         cut(); // in case something is already highlighted
1111         moveCaretTo (document.findWordBreakAfter (caretPos), true);
1112     }
1113     else
1114     {
1115         if (selectionStart == selectionEnd)
1116             selectionEnd.moveBy (1);
1117         else
1118             newTransaction();
1119     }
1120 
1121     cut();
1122     return true;
1123 }
1124 
selectAll()1125 bool CodeEditorComponent::selectAll()
1126 {
1127     newTransaction();
1128     selectRegion (CodeDocument::Position (document, std::numeric_limits<int>::max(),
1129                                           std::numeric_limits<int>::max()),
1130                   CodeDocument::Position (document, 0, 0));
1131     return true;
1132 }
1133 
selectRegion(const CodeDocument::Position & start,const CodeDocument::Position & end)1134 void CodeEditorComponent::selectRegion (const CodeDocument::Position& start,
1135                                         const CodeDocument::Position& end)
1136 {
1137     moveCaretTo (start, false);
1138     moveCaretTo (end, true);
1139 }
1140 
1141 //==============================================================================
undo()1142 bool CodeEditorComponent::undo()
1143 {
1144     if (readOnly)
1145         return false;
1146 
1147     ScopedValueSetter<bool> svs (shouldFollowDocumentChanges, true, false);
1148     document.undo();
1149     scrollToKeepCaretOnScreen();
1150     return true;
1151 }
1152 
redo()1153 bool CodeEditorComponent::redo()
1154 {
1155     if (readOnly)
1156         return false;
1157 
1158     ScopedValueSetter<bool> svs (shouldFollowDocumentChanges, true, false);
1159     document.redo();
1160     scrollToKeepCaretOnScreen();
1161     return true;
1162 }
1163 
newTransaction()1164 void CodeEditorComponent::newTransaction()
1165 {
1166     document.newTransaction();
1167     pimpl->startTimer (600);
1168 }
1169 
setCommandManager(ApplicationCommandManager * newManager)1170 void CodeEditorComponent::setCommandManager (ApplicationCommandManager* newManager) noexcept
1171 {
1172     appCommandManager = newManager;
1173 }
1174 
1175 //==============================================================================
getHighlightedRegion() const1176 Range<int> CodeEditorComponent::getHighlightedRegion() const
1177 {
1178     return { selectionStart.getPosition(),
1179              selectionEnd.getPosition() };
1180 }
1181 
isHighlightActive() const1182 bool CodeEditorComponent::isHighlightActive() const noexcept
1183 {
1184     return selectionStart != selectionEnd;
1185 }
1186 
setHighlightedRegion(const Range<int> & newRange)1187 void CodeEditorComponent::setHighlightedRegion (const Range<int>& newRange)
1188 {
1189     selectRegion (CodeDocument::Position (document, newRange.getStart()),
1190                   CodeDocument::Position (document, newRange.getEnd()));
1191 }
1192 
getTextInRange(const Range<int> & range) const1193 String CodeEditorComponent::getTextInRange (const Range<int>& range) const
1194 {
1195     return document.getTextBetween (CodeDocument::Position (document, range.getStart()),
1196                                     CodeDocument::Position (document, range.getEnd()));
1197 }
1198 
1199 //==============================================================================
keyPressed(const KeyPress & key)1200 bool CodeEditorComponent::keyPressed (const KeyPress& key)
1201 {
1202     if (! TextEditorKeyMapper<CodeEditorComponent>::invokeKeyFunction (*this, key))
1203     {
1204         if (readOnly)
1205             return false;
1206 
1207         if (key == KeyPress::tabKey || key.getTextCharacter() == '\t')      handleTabKey();
1208         else if (key == KeyPress::returnKey)                                handleReturnKey();
1209         else if (key == KeyPress::escapeKey)                                handleEscapeKey();
1210         else if (key == KeyPress ('[', ModifierKeys::commandModifier, 0))   unindentSelection();
1211         else if (key == KeyPress (']', ModifierKeys::commandModifier, 0))   indentSelection();
1212         else if (key.getTextCharacter() >= ' ')                             insertTextAtCaret (String::charToString (key.getTextCharacter()));
1213         else                                                                return false;
1214     }
1215 
1216     pimpl->handleUpdateNowIfNeeded();
1217     return true;
1218 }
1219 
handleReturnKey()1220 void CodeEditorComponent::handleReturnKey()
1221 {
1222     insertTextAtCaret (document.getNewLineCharacters());
1223 }
1224 
handleTabKey()1225 void CodeEditorComponent::handleTabKey()
1226 {
1227     insertTabAtCaret();
1228 }
1229 
handleEscapeKey()1230 void CodeEditorComponent::handleEscapeKey()
1231 {
1232     newTransaction();
1233 }
1234 
editorViewportPositionChanged()1235 void CodeEditorComponent::editorViewportPositionChanged()
1236 {
1237 }
1238 
caretPositionMoved()1239 void CodeEditorComponent::caretPositionMoved()
1240 {
1241 }
1242 
1243 //==============================================================================
getNextCommandTarget()1244 ApplicationCommandTarget* CodeEditorComponent::getNextCommandTarget()
1245 {
1246     return findFirstTargetParentComponent();
1247 }
1248 
getAllCommands(Array<CommandID> & commands)1249 void CodeEditorComponent::getAllCommands (Array<CommandID>& commands)
1250 {
1251     const CommandID ids[] = { StandardApplicationCommandIDs::cut,
1252                               StandardApplicationCommandIDs::copy,
1253                               StandardApplicationCommandIDs::paste,
1254                               StandardApplicationCommandIDs::del,
1255                               StandardApplicationCommandIDs::selectAll,
1256                               StandardApplicationCommandIDs::undo,
1257                               StandardApplicationCommandIDs::redo };
1258 
1259     commands.addArray (ids, numElementsInArray (ids));
1260 }
1261 
getCommandInfo(const CommandID commandID,ApplicationCommandInfo & result)1262 void CodeEditorComponent::getCommandInfo (const CommandID commandID, ApplicationCommandInfo& result)
1263 {
1264     const bool anythingSelected = isHighlightActive();
1265 
1266     switch (commandID)
1267     {
1268         case StandardApplicationCommandIDs::cut:
1269             result.setInfo (TRANS ("Cut"), TRANS ("Copies the currently selected text to the clipboard and deletes it."), "Editing", 0);
1270             result.setActive (anythingSelected && ! readOnly);
1271             result.defaultKeypresses.add (KeyPress ('x', ModifierKeys::commandModifier, 0));
1272             break;
1273 
1274         case StandardApplicationCommandIDs::copy:
1275             result.setInfo (TRANS ("Copy"), TRANS ("Copies the currently selected text to the clipboard."), "Editing", 0);
1276             result.setActive (anythingSelected);
1277             result.defaultKeypresses.add (KeyPress ('c', ModifierKeys::commandModifier, 0));
1278             break;
1279 
1280         case StandardApplicationCommandIDs::paste:
1281             result.setInfo (TRANS ("Paste"), TRANS ("Inserts text from the clipboard."), "Editing", 0);
1282             result.setActive (! readOnly);
1283             result.defaultKeypresses.add (KeyPress ('v', ModifierKeys::commandModifier, 0));
1284             break;
1285 
1286         case StandardApplicationCommandIDs::del:
1287             result.setInfo (TRANS ("Delete"), TRANS ("Deletes any selected text."), "Editing", 0);
1288             result.setActive (anythingSelected && ! readOnly);
1289             break;
1290 
1291         case StandardApplicationCommandIDs::selectAll:
1292             result.setInfo (TRANS ("Select All"), TRANS ("Selects all the text in the editor."), "Editing", 0);
1293             result.defaultKeypresses.add (KeyPress ('a', ModifierKeys::commandModifier, 0));
1294             break;
1295 
1296         case StandardApplicationCommandIDs::undo:
1297             result.setInfo (TRANS ("Undo"), TRANS ("Undo"), "Editing", 0);
1298             result.defaultKeypresses.add (KeyPress ('z', ModifierKeys::commandModifier, 0));
1299             result.setActive (document.getUndoManager().canUndo() && ! readOnly);
1300             break;
1301 
1302         case StandardApplicationCommandIDs::redo:
1303             result.setInfo (TRANS ("Redo"), TRANS ("Redo"), "Editing", 0);
1304             result.defaultKeypresses.add (KeyPress ('z', ModifierKeys::commandModifier | ModifierKeys::shiftModifier, 0));
1305             result.setActive (document.getUndoManager().canRedo() && ! readOnly);
1306             break;
1307 
1308         default:
1309             break;
1310     }
1311 }
1312 
perform(const InvocationInfo & info)1313 bool CodeEditorComponent::perform (const InvocationInfo& info)
1314 {
1315     return performCommand (info.commandID);
1316 }
1317 
lookAndFeelChanged()1318 void CodeEditorComponent::lookAndFeelChanged()
1319 {
1320     caret.reset (getLookAndFeel().createCaretComponent (this));
1321 }
1322 
performCommand(const CommandID commandID)1323 bool CodeEditorComponent::performCommand (const CommandID commandID)
1324 {
1325     switch (commandID)
1326     {
1327         case StandardApplicationCommandIDs::cut:        cutToClipboard(); break;
1328         case StandardApplicationCommandIDs::copy:       copyToClipboard(); break;
1329         case StandardApplicationCommandIDs::paste:      pasteFromClipboard(); break;
1330         case StandardApplicationCommandIDs::del:        cut(); break;
1331         case StandardApplicationCommandIDs::selectAll:  selectAll(); break;
1332         case StandardApplicationCommandIDs::undo:       undo(); break;
1333         case StandardApplicationCommandIDs::redo:       redo(); break;
1334         default:                                        return false;
1335     }
1336 
1337     return true;
1338 }
1339 
1340 //==============================================================================
addPopupMenuItems(PopupMenu & m,const MouseEvent *)1341 void CodeEditorComponent::addPopupMenuItems (PopupMenu& m, const MouseEvent*)
1342 {
1343     m.addItem (StandardApplicationCommandIDs::cut,   TRANS ("Cut"), isHighlightActive() && ! readOnly);
1344     m.addItem (StandardApplicationCommandIDs::copy,  TRANS ("Copy"), ! getHighlightedRegion().isEmpty());
1345     m.addItem (StandardApplicationCommandIDs::paste, TRANS ("Paste"), ! readOnly);
1346     m.addItem (StandardApplicationCommandIDs::del,   TRANS ("Delete"), ! readOnly);
1347     m.addSeparator();
1348     m.addItem (StandardApplicationCommandIDs::selectAll, TRANS ("Select All"));
1349     m.addSeparator();
1350     m.addItem (StandardApplicationCommandIDs::undo,  TRANS ("Undo"), document.getUndoManager().canUndo());
1351     m.addItem (StandardApplicationCommandIDs::redo,  TRANS ("Redo"), document.getUndoManager().canRedo());
1352 }
1353 
performPopupMenuAction(const int menuItemID)1354 void CodeEditorComponent::performPopupMenuAction (const int menuItemID)
1355 {
1356     performCommand (menuItemID);
1357 }
1358 
codeEditorMenuCallback(int menuResult,CodeEditorComponent * editor)1359 static void codeEditorMenuCallback (int menuResult, CodeEditorComponent* editor)
1360 {
1361     if (editor != nullptr && menuResult != 0)
1362         editor->performPopupMenuAction (menuResult);
1363 }
1364 
1365 //==============================================================================
mouseDown(const MouseEvent & e)1366 void CodeEditorComponent::mouseDown (const MouseEvent& e)
1367 {
1368     newTransaction();
1369     dragType = notDragging;
1370 
1371     if (e.mods.isPopupMenu())
1372     {
1373         setMouseCursor (MouseCursor::NormalCursor);
1374 
1375         if (getHighlightedRegion().isEmpty())
1376         {
1377             CodeDocument::Position start, end;
1378             document.findTokenContaining (getPositionAt (e.x, e.y), start, end);
1379 
1380             if (start.getPosition() < end.getPosition())
1381                 selectRegion (start, end);
1382         }
1383 
1384         PopupMenu m;
1385         m.setLookAndFeel (&getLookAndFeel());
1386         addPopupMenuItems (m, &e);
1387 
1388         m.showMenuAsync (PopupMenu::Options(),
1389                          ModalCallbackFunction::forComponent (codeEditorMenuCallback, this));
1390     }
1391     else
1392     {
1393         beginDragAutoRepeat (100);
1394         moveCaretTo (getPositionAt (e.x, e.y), e.mods.isShiftDown());
1395     }
1396 }
1397 
mouseDrag(const MouseEvent & e)1398 void CodeEditorComponent::mouseDrag (const MouseEvent& e)
1399 {
1400     if (! e.mods.isPopupMenu())
1401         moveCaretTo (getPositionAt (e.x, e.y), true);
1402 }
1403 
mouseUp(const MouseEvent &)1404 void CodeEditorComponent::mouseUp (const MouseEvent&)
1405 {
1406     newTransaction();
1407     beginDragAutoRepeat (0);
1408     dragType = notDragging;
1409     setMouseCursor (MouseCursor::IBeamCursor);
1410 }
1411 
mouseDoubleClick(const MouseEvent & e)1412 void CodeEditorComponent::mouseDoubleClick (const MouseEvent& e)
1413 {
1414     CodeDocument::Position tokenStart (getPositionAt (e.x, e.y));
1415     CodeDocument::Position tokenEnd (tokenStart);
1416 
1417     if (e.getNumberOfClicks() > 2)
1418         document.findLineContaining (tokenStart, tokenStart, tokenEnd);
1419     else
1420         document.findTokenContaining (tokenStart, tokenStart, tokenEnd);
1421 
1422     selectRegion (tokenStart, tokenEnd);
1423     dragType = notDragging;
1424 }
1425 
mouseWheelMove(const MouseEvent & e,const MouseWheelDetails & wheel)1426 void CodeEditorComponent::mouseWheelMove (const MouseEvent& e, const MouseWheelDetails& wheel)
1427 {
1428     if ((verticalScrollBar.isVisible() && wheel.deltaY != 0.0f)
1429          || (horizontalScrollBar.isVisible() && wheel.deltaX != 0.0f))
1430     {
1431         {
1432             MouseWheelDetails w (wheel);
1433             w.deltaX = 0;
1434             verticalScrollBar.mouseWheelMove (e, w);
1435         }
1436 
1437         {
1438             MouseWheelDetails w (wheel);
1439             w.deltaY = 0;
1440             horizontalScrollBar.mouseWheelMove (e, w);
1441         }
1442     }
1443     else
1444     {
1445         Component::mouseWheelMove (e, wheel);
1446     }
1447 }
1448 
1449 //==============================================================================
focusGained(FocusChangeType)1450 void CodeEditorComponent::focusGained (FocusChangeType)     { updateCaretPosition(); }
focusLost(FocusChangeType)1451 void CodeEditorComponent::focusLost (FocusChangeType)       { updateCaretPosition(); }
1452 
1453 //==============================================================================
setTabSize(const int numSpaces,const bool insertSpaces)1454 void CodeEditorComponent::setTabSize (const int numSpaces, const bool insertSpaces)
1455 {
1456     useSpacesForTabs = insertSpaces;
1457 
1458     if (spacesPerTab != numSpaces)
1459     {
1460         spacesPerTab = numSpaces;
1461         rebuildLineTokensAsync();
1462     }
1463 }
1464 
getTabString(const int numSpaces) const1465 String CodeEditorComponent::getTabString (const int numSpaces) const
1466 {
1467     return String::repeatedString (useSpacesForTabs ? " " : "\t",
1468                                    useSpacesForTabs ? numSpaces
1469                                                     : (numSpaces / spacesPerTab));
1470 }
1471 
indexToColumn(int lineNum,int index) const1472 int CodeEditorComponent::indexToColumn (int lineNum, int index) const noexcept
1473 {
1474     auto line = document.getLine (lineNum);
1475     auto t = line.getCharPointer();
1476     int col = 0;
1477 
1478     for (int i = 0; i < index; ++i)
1479     {
1480         if (t.isEmpty())
1481         {
1482             jassertfalse;
1483             break;
1484         }
1485 
1486         if (t.getAndAdvance() != '\t')
1487             ++col;
1488         else
1489             col += getTabSize() - (col % getTabSize());
1490     }
1491 
1492     return col;
1493 }
1494 
columnToIndex(int lineNum,int column) const1495 int CodeEditorComponent::columnToIndex (int lineNum, int column) const noexcept
1496 {
1497     auto line = document.getLine (lineNum);
1498     auto t = line.getCharPointer();
1499     int i = 0, col = 0;
1500 
1501     while (! t.isEmpty())
1502     {
1503         if (t.getAndAdvance() != '\t')
1504             ++col;
1505         else
1506             col += getTabSize() - (col % getTabSize());
1507 
1508         if (col > column)
1509             break;
1510 
1511         ++i;
1512     }
1513 
1514     return i;
1515 }
1516 
1517 //==============================================================================
setFont(const Font & newFont)1518 void CodeEditorComponent::setFont (const Font& newFont)
1519 {
1520     font = newFont;
1521     charWidth = font.getStringWidthFloat ("0");
1522     lineHeight = roundToInt (font.getHeight());
1523     resized();
1524 }
1525 
set(const String & name,Colour colour)1526 void CodeEditorComponent::ColourScheme::set (const String& name, Colour colour)
1527 {
1528     for (auto& tt : types)
1529     {
1530         if (tt.name == name)
1531         {
1532             tt.colour = colour;
1533             return;
1534         }
1535     }
1536 
1537     TokenType tt;
1538     tt.name = name;
1539     tt.colour = colour;
1540     types.add (tt);
1541 }
1542 
setColourScheme(const ColourScheme & scheme)1543 void CodeEditorComponent::setColourScheme (const ColourScheme& scheme)
1544 {
1545     colourScheme = scheme;
1546     repaint();
1547 }
1548 
getColourForTokenType(const int tokenType) const1549 Colour CodeEditorComponent::getColourForTokenType (const int tokenType) const
1550 {
1551     return isPositiveAndBelow (tokenType, colourScheme.types.size())
1552                 ? colourScheme.types.getReference (tokenType).colour
1553                 : findColour (CodeEditorComponent::defaultTextColourId);
1554 }
1555 
clearCachedIterators(const int firstLineToBeInvalid)1556 void CodeEditorComponent::clearCachedIterators (const int firstLineToBeInvalid)
1557 {
1558     int i;
1559     for (i = cachedIterators.size(); --i >= 0;)
1560         if (cachedIterators.getUnchecked (i).getLine() < firstLineToBeInvalid)
1561             break;
1562 
1563     cachedIterators.removeRange (jmax (0, i - 1), cachedIterators.size());
1564 }
1565 
updateCachedIterators(int maxLineNum)1566 void CodeEditorComponent::updateCachedIterators (int maxLineNum)
1567 {
1568     const int maxNumCachedPositions = 5000;
1569     const int linesBetweenCachedSources = jmax (10, document.getNumLines() / maxNumCachedPositions);
1570 
1571     if (cachedIterators.size() == 0)
1572         cachedIterators.add (CodeDocument::Iterator (document));
1573 
1574     if (codeTokeniser != nullptr)
1575     {
1576         for (;;)
1577         {
1578             const auto last = cachedIterators.getLast();
1579 
1580             if (last.getLine() >= maxLineNum)
1581                 break;
1582 
1583             cachedIterators.add (CodeDocument::Iterator (last));
1584             auto& t = cachedIterators.getReference (cachedIterators.size() - 1);
1585             const int targetLine = jmin (maxLineNum, last.getLine() + linesBetweenCachedSources);
1586 
1587             for (;;)
1588             {
1589                 codeTokeniser->readNextToken (t);
1590 
1591                 if (t.getLine() >= targetLine)
1592                     break;
1593 
1594                 if (t.isEOF())
1595                     return;
1596             }
1597         }
1598     }
1599 }
1600 
getIteratorForPosition(int position,CodeDocument::Iterator & source)1601 void CodeEditorComponent::getIteratorForPosition (int position, CodeDocument::Iterator& source)
1602 {
1603     if (codeTokeniser != nullptr)
1604     {
1605         for (int i = cachedIterators.size(); --i >= 0;)
1606         {
1607             auto& t = cachedIterators.getReference (i);
1608 
1609             if (t.getPosition() <= position)
1610             {
1611                 source = t;
1612                 break;
1613             }
1614         }
1615 
1616         while (source.getPosition() < position)
1617         {
1618             const CodeDocument::Iterator original (source);
1619             codeTokeniser->readNextToken (source);
1620 
1621             if (source.getPosition() > position || source.isEOF())
1622             {
1623                 source = original;
1624                 break;
1625             }
1626         }
1627     }
1628 }
1629 
State(const CodeEditorComponent & editor)1630 CodeEditorComponent::State::State (const CodeEditorComponent& editor)
1631     : lastTopLine (editor.getFirstLineOnScreen()),
1632       lastCaretPos (editor.getCaretPos().getPosition()),
1633       lastSelectionEnd (lastCaretPos)
1634 {
1635     auto selection = editor.getHighlightedRegion();
1636 
1637     if (lastCaretPos == selection.getStart())
1638         lastSelectionEnd = selection.getEnd();
1639     else
1640         lastSelectionEnd = selection.getStart();
1641 }
1642 
State(const State & other)1643 CodeEditorComponent::State::State (const State& other) noexcept
1644     : lastTopLine (other.lastTopLine),
1645       lastCaretPos (other.lastCaretPos),
1646       lastSelectionEnd (other.lastSelectionEnd)
1647 {
1648 }
1649 
restoreState(CodeEditorComponent & editor) const1650 void CodeEditorComponent::State::restoreState (CodeEditorComponent& editor) const
1651 {
1652     editor.selectRegion (CodeDocument::Position (editor.getDocument(), lastSelectionEnd),
1653                          CodeDocument::Position (editor.getDocument(), lastCaretPos));
1654 
1655     if (lastTopLine > 0 && lastTopLine < editor.getDocument().getNumLines())
1656         editor.scrollToLine (lastTopLine);
1657 }
1658 
State(const String & s)1659 CodeEditorComponent::State::State (const String& s)
1660 {
1661     auto tokens = StringArray::fromTokens (s, ":", {});
1662 
1663     lastTopLine      = tokens[0].getIntValue();
1664     lastCaretPos     = tokens[1].getIntValue();
1665     lastSelectionEnd = tokens[2].getIntValue();
1666 }
1667 
toString() const1668 String CodeEditorComponent::State::toString() const
1669 {
1670     return String (lastTopLine) + ":" + String (lastCaretPos) + ":" + String (lastSelectionEnd);
1671 }
1672 
1673 } // namespace juce
1674