The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1298 lines
39KB

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