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