1 // Written in the D programming language. 2 3 /** 4 This module contains implementation of editable text content. 5 6 7 Synopsis: 8 9 ---- 10 import dlangui.core.editable; 11 12 ---- 13 14 Copyright: Vadim Lopatin, 2014 15 License: Boost License 1.0 16 Authors: Vadim Lopatin, coolreader.org@gmail.com 17 */ 18 module dlangui.core.editable; 19 20 import dlangui.core.logger; 21 import dlangui.core.signals; 22 import dlangui.core.collections; 23 import dlangui.core.linestream; 24 import dlangui.core.streams; 25 import std.algorithm; 26 import std.conv : to; 27 static import std.uni; 28 29 // uncomment FileFormats debug symbol to dump file formats for loaded/saved files. 30 //debug = FileFormats; 31 32 immutable dchar EOL = '\n'; 33 34 const ubyte TOKEN_CATEGORY_SHIFT = 4; 35 const ubyte TOKEN_CATEGORY_MASK = 0xF0; // token category 0..15 36 const ubyte TOKEN_SUBCATEGORY_MASK = 0x0F; // token subcategory 0..15 37 const ubyte TOKEN_UNKNOWN = 0; 38 39 /* 40 Bit mask: 41 7654 3210 42 cccc ssss 43 | | 44 | \ ssss = token subcategory 45 | 46 \ cccc = token category 47 48 */ 49 /// token category for syntax highlight 50 enum TokenCategory : ubyte { 51 WhiteSpace = (0 << TOKEN_CATEGORY_SHIFT), 52 WhiteSpace_Space = (0 << TOKEN_CATEGORY_SHIFT) | 1, 53 WhiteSpace_Tab = (0 << TOKEN_CATEGORY_SHIFT) | 2, 54 55 Comment = (1 << TOKEN_CATEGORY_SHIFT), 56 Comment_SingleLine = (1 << TOKEN_CATEGORY_SHIFT) | 1, // single line comment 57 Comment_SingleLineDoc = (1 << TOKEN_CATEGORY_SHIFT) | 2,// documentation in single line comment 58 Comment_MultyLine = (1 << TOKEN_CATEGORY_SHIFT) | 3, // multiline coment 59 Comment_MultyLineDoc = (1 << TOKEN_CATEGORY_SHIFT) | 4, // documentation in multiline comment 60 Comment_Documentation = (1 << TOKEN_CATEGORY_SHIFT) | 5,// documentation comment 61 62 Identifier = (2 << TOKEN_CATEGORY_SHIFT), // identifier (exact subcategory is unknown) 63 Identifier_Class = (2 << TOKEN_CATEGORY_SHIFT) | 1, // class name 64 Identifier_Struct = (2 << TOKEN_CATEGORY_SHIFT) | 2, // struct name 65 Identifier_Local = (2 << TOKEN_CATEGORY_SHIFT) | 3, // local variable 66 Identifier_Member = (2 << TOKEN_CATEGORY_SHIFT) | 4, // struct or class member 67 Identifier_Deprecated = (2 << TOKEN_CATEGORY_SHIFT) | 15, // usage of this identifier is deprecated 68 /// string literal 69 String = (3 << TOKEN_CATEGORY_SHIFT), 70 /// character literal 71 Character = (4 << TOKEN_CATEGORY_SHIFT), 72 /// integer literal 73 Integer = (5 << TOKEN_CATEGORY_SHIFT), 74 /// floating point number literal 75 Float = (6 << TOKEN_CATEGORY_SHIFT), 76 /// keyword 77 Keyword = (7 << TOKEN_CATEGORY_SHIFT), 78 /// operator 79 Op = (8 << TOKEN_CATEGORY_SHIFT), 80 // add more here 81 //.... 82 /// error - unparsed character sequence 83 Error = (15 << TOKEN_CATEGORY_SHIFT), 84 /// invalid token - generic 85 Error_InvalidToken = (15 << TOKEN_CATEGORY_SHIFT) | 1, 86 /// invalid number token - error occured while parsing number 87 Error_InvalidNumber = (15 << TOKEN_CATEGORY_SHIFT) | 2, 88 /// invalid string token - error occured while parsing string 89 Error_InvalidString = (15 << TOKEN_CATEGORY_SHIFT) | 3, 90 /// invalid identifier token - error occured while parsing identifier 91 Error_InvalidIdentifier = (15 << TOKEN_CATEGORY_SHIFT) | 4, 92 /// invalid comment token - error occured while parsing comment 93 Error_InvalidComment = (15 << TOKEN_CATEGORY_SHIFT) | 7, 94 /// invalid comment token - error occured while parsing comment 95 Error_InvalidOp = (15 << TOKEN_CATEGORY_SHIFT) | 8, 96 } 97 98 /// extracts token category, clearing subcategory 99 ubyte tokenCategory(ubyte t) { 100 return t & 0xF0; 101 } 102 103 /// split dstring by delimiters 104 dstring[] splitDString(dstring source, dchar delimiter = EOL) { 105 int start = 0; 106 dstring[] res; 107 for (int i = 0; i <= source.length; i++) { 108 if (i == source.length || source[i] == delimiter) { 109 if (i >= start) { 110 dchar prevchar = i > 1 && i > start + 1 ? source[i - 1] : 0; 111 int end = i; 112 if (delimiter == EOL && prevchar == '\r') // windows CR/LF 113 end--; 114 dstring line = i > start ? cast(dstring)(source[start .. end].dup) : ""d; 115 res ~= line; 116 } 117 start = i + 1; 118 } 119 } 120 return res; 121 } 122 123 version (Windows) { 124 immutable dstring SYSTEM_DEFAULT_EOL = "\r\n"; 125 } else { 126 immutable dstring SYSTEM_DEFAULT_EOL = "\n"; 127 } 128 129 /// concat strings from array using delimiter 130 dstring concatDStrings(dstring[] lines, dstring delimiter = SYSTEM_DEFAULT_EOL) { 131 dchar[] buf; 132 foreach(i, line; lines) { 133 if(i > 0) 134 buf ~= delimiter; 135 buf ~= line; 136 } 137 return cast(dstring)buf; 138 } 139 140 /// replace end of lines with spaces 141 dstring replaceEolsWithSpaces(dstring source) { 142 dchar[] buf; 143 dchar lastch; 144 foreach(ch; source) { 145 if (ch == '\r') { 146 buf ~= ' '; 147 } else if (ch == '\n') { 148 if (lastch != '\r') 149 buf ~= ' '; 150 } else { 151 buf ~= ch; 152 } 153 lastch = ch; 154 } 155 return cast(dstring)buf; 156 } 157 158 /// text content position 159 struct TextPosition { 160 /// line number, zero based 161 int line; 162 /// character position in line (0 == before first character) 163 int pos; 164 /// compares two positions 165 int opCmp(ref const TextPosition v) const { 166 if (line < v.line) 167 return -1; 168 if (line > v.line) 169 return 1; 170 if (pos < v.pos) 171 return -1; 172 if (pos > v.pos) 173 return 1; 174 return 0; 175 } 176 bool opEquals(ref inout TextPosition v) inout { 177 return line == v.line && pos == v.pos; 178 } 179 @property string toString() const { 180 return to!string(line) ~ ":" ~ to!string(pos); 181 } 182 /// adds deltaPos to position and returns result 183 TextPosition offset(int deltaPos) { 184 return TextPosition(line, pos + deltaPos); 185 } 186 } 187 188 /// text content range 189 struct TextRange { 190 TextPosition start; 191 TextPosition end; 192 bool intersects(const ref TextRange v) const { 193 if (start >= v.end) 194 return false; 195 if (end <= v.start) 196 return false; 197 return true; 198 } 199 /// returns true if position is inside this range 200 bool isInside(TextPosition p) const { 201 return start <= p && end > p; 202 } 203 /// returns true if position is inside this range or right after this range 204 bool isInsideOrNext(TextPosition p) const { 205 return start <= p && end >= p; 206 } 207 /// returns true if range is empty 208 @property bool empty() const { 209 return end <= start; 210 } 211 /// returns true if start and end located at the same line 212 @property bool singleLine() const { 213 return end.line == start.line; 214 } 215 /// returns count of lines in range 216 @property int lines() const { 217 return end.line - start.line + 1; 218 } 219 @property string toString() const { 220 return "[" ~ start.toString ~ ":" ~ end.toString ~ "]"; 221 } 222 } 223 224 /// action performed with editable contents 225 enum EditAction { 226 /// insert content into specified position (range.start) 227 //Insert, 228 /// delete content in range 229 //Delete, 230 /// replace range content with new content 231 Replace, 232 233 /// replace whole content 234 ReplaceContent, 235 /// saved content 236 SaveContent, 237 } 238 239 /// values for editable line state 240 enum EditStateMark : ubyte { 241 /// content is unchanged - e.g. after loading from file 242 unchanged, 243 /// content is changed and not yet saved 244 changed, 245 /// content is changed, but already saved to file 246 saved, 247 } 248 249 /// edit operation details for EditableContent 250 class EditOperation { 251 protected EditAction _action; 252 /// action performed 253 @property EditAction action() { return _action; } 254 protected TextRange _range; 255 256 /// source range to replace with new content 257 @property ref TextRange range() { return _range; } 258 protected TextRange _newRange; 259 260 /// new range after operation applied 261 @property ref TextRange newRange() { return _newRange; } 262 @property void newRange(TextRange range) { _newRange = range; } 263 264 /// new content for range (if required for this action) 265 protected dstring[] _content; 266 @property ref dstring[] content() { return _content; } 267 268 /// line edit marks for old range 269 protected EditStateMark[] _oldEditMarks; 270 @property ref EditStateMark[] oldEditMarks() { return _oldEditMarks; } 271 @property void oldEditMarks(EditStateMark[] marks) { _oldEditMarks = marks; } 272 273 /// old content for range 274 protected dstring[] _oldContent; 275 @property ref dstring[] oldContent() { return _oldContent; } 276 @property void oldContent(dstring[] content) { _oldContent = content; } 277 278 this(EditAction action) { 279 _action = action; 280 } 281 this(EditAction action, TextPosition pos, dstring text) { 282 this(action, TextRange(pos, pos), text); 283 } 284 this(EditAction action, TextRange range, dstring text) { 285 _action = action; 286 _range = range; 287 _content.length = 1; 288 _content[0] = text.dup; 289 } 290 this(EditAction action, TextRange range, dstring[] text) { 291 _action = action; 292 _range = range; 293 _content.length = text.length; 294 foreach(i; 0 .. text.length) 295 _content[i] = text[i].dup; 296 //_content = text; 297 } 298 /// try to merge two operations (simple entering of characters in the same line), return true if succeded 299 bool merge(EditOperation op) { 300 if (_range.start.line != op._range.start.line) // both ops whould be on the same line 301 return false; 302 if (_content.length != 1 || op._content.length != 1) // both ops should operate the same line 303 return false; 304 // appending of single character 305 if (_range.empty && op._range.empty && op._content[0].length == 1 && _newRange.end.pos == op._range.start.pos) { 306 _content[0] ~= op._content[0]; 307 _newRange.end.pos++; 308 return true; 309 } 310 // removing single character 311 if (_newRange.empty && op._newRange.empty && op._oldContent[0].length == 1) { 312 if (_newRange.end.pos == op.range.end.pos) { 313 // removed char before 314 _range.start.pos--; 315 _newRange.start.pos--; 316 _newRange.end.pos--; 317 _oldContent[0] = (op._oldContent[0].dup ~ _oldContent[0].dup).dup; 318 return true; 319 } else if (_newRange.end.pos == op._range.start.pos) { 320 // removed char after 321 _range.end.pos++; 322 _oldContent[0] = (_oldContent[0].dup ~ op._oldContent[0].dup).dup; 323 return true; 324 } 325 } 326 return false; 327 } 328 329 //void saved() { 330 // for (int i = 0; i < _oldEditMarks.length; i++) { 331 // if (_oldEditMarks[i] == EditStateMark.changed) 332 // _oldEditMarks[i] = EditStateMark.saved; 333 // } 334 //} 335 void modified(bool all = true) { 336 foreach(i; 0 .. _oldEditMarks.length) { 337 if (all || _oldEditMarks[i] == EditStateMark.saved) 338 _oldEditMarks[i] = EditStateMark.changed; 339 } 340 } 341 342 /// return true if it's insert new line operation 343 @property bool isInsertNewLine() { 344 return content.length == 2 && content[0].length == 0 && content[1].length == 0; 345 } 346 347 /// if new content is single char, return it, otherwise return 0 348 @property dchar singleChar() { 349 return content.length == 1 && content[0].length == 1 ? content[0][0] : 0; 350 } 351 } 352 353 /// Undo/Redo buffer 354 class UndoBuffer { 355 protected Collection!EditOperation _undoList; 356 protected Collection!EditOperation _redoList; 357 358 /// returns true if buffer contains any undo items 359 @property bool hasUndo() { 360 return !_undoList.empty; 361 } 362 363 /// returns true if buffer contains any redo items 364 @property bool hasRedo() { 365 return !_redoList.empty; 366 } 367 368 /// adds undo operation 369 void saveForUndo(EditOperation op) { 370 _redoList.clear(); 371 if (!_undoList.empty) { 372 if (_undoList.back.merge(op)) { 373 //_undoList.back.modified(); 374 return; // merged - no need to add new operation 375 } 376 } 377 _undoList.pushBack(op); 378 } 379 380 /// returns operation to be undone (put it to redo), null if no undo ops available 381 EditOperation undo() { 382 if (!hasUndo) 383 return null; // no undo operations 384 EditOperation res = _undoList.popBack(); 385 _redoList.pushBack(res); 386 return res; 387 } 388 389 /// returns operation to be redone (put it to undo), null if no undo ops available 390 EditOperation redo() { 391 if (!hasRedo) 392 return null; // no undo operations 393 EditOperation res = _redoList.popBack(); 394 _undoList.pushBack(res); 395 return res; 396 } 397 398 /// clears both undo and redo buffers 399 void clear() { 400 _undoList.clear(); 401 _redoList.clear(); 402 _savedState = null; 403 } 404 405 protected EditOperation _savedState; 406 407 /// current state is saved 408 void saved() { 409 _savedState = _undoList.peekBack; 410 foreach(i; 0 .. _undoList.length) { 411 _undoList[i].modified(); 412 } 413 foreach(i; 0 .. _redoList.length) { 414 _redoList[i].modified(); 415 } 416 } 417 418 /// returns true if saved state is in redo buffer 419 bool savedInRedo() { 420 if (!_savedState) 421 return false; 422 foreach(i; 0 .. _redoList.length) { 423 if (_savedState is _redoList[i]) 424 return true; 425 } 426 return false; 427 } 428 429 /// returns true if content has been changed since last saved() or clear() call 430 @property bool modified() { 431 return _savedState !is _undoList.peekBack; 432 } 433 } 434 435 /// Editable Content change listener 436 interface EditableContentListener { 437 void onContentChange(EditableContent content, EditOperation operation, ref TextRange rangeBefore, ref TextRange rangeAfter, Object source); 438 } 439 440 interface EditableContentMarksChangeListener { 441 void onMarksChange(EditableContent content, LineIcon[] movedMarks, LineIcon[] removedMarks); 442 } 443 444 /// TokenCategory holder 445 alias TokenProp = ubyte; 446 /// TokenCategory string 447 alias TokenPropString = TokenProp[]; 448 449 struct LineSpan { 450 /// start index of line 451 int start; 452 /// number of lines it spans 453 int len; 454 /// the wrapping points 455 WrapPoint[] wrapPoints; 456 /// the wrapped text 457 dstring[] wrappedContent; 458 459 enum WrapPointInfo : bool { 460 Position, 461 Width, 462 } 463 464 ///Adds up either positions or widths to a wrapLine 465 int accumulation(int wrapLine, bool wrapPointInfo) 466 { 467 int total; 468 for (int i; i < wrapLine; i++) 469 { 470 if (i < this.wrapPoints.length - 1) 471 { 472 int curVal; 473 curVal = wrapPointInfo ? this.wrapPoints[i].wrapWidth : this.wrapPoints[i].wrapPos; 474 total += curVal; 475 } 476 } 477 return total; 478 } 479 } 480 481 ///Holds info about a word wrapping point 482 struct WrapPoint { 483 ///The relative wrapping position (related to TextPosition.pos) 484 int wrapPos; 485 ///The associated calculated width of the wrapLine 486 int wrapWidth; 487 } 488 489 /// interface for custom syntax highlight, comments toggling, smart indents, and other language dependent features for source code editors 490 interface SyntaxSupport { 491 492 /// returns editable content 493 @property EditableContent content(); 494 /// set editable content 495 @property SyntaxSupport content(EditableContent content); 496 497 /// categorize characters in content by token types 498 void updateHighlight(dstring[] lines, TokenPropString[] props, int changeStartLine, int changeEndLine); 499 500 /// return true if toggle line comment is supported for file type 501 @property bool supportsToggleLineComment(); 502 /// return true if can toggle line comments for specified text range 503 bool canToggleLineComment(TextRange range); 504 /// toggle line comments for specified text range 505 void toggleLineComment(TextRange range, Object source); 506 507 /// return true if toggle block comment is supported for file type 508 @property bool supportsToggleBlockComment(); 509 /// return true if can toggle block comments for specified text range 510 bool canToggleBlockComment(TextRange range); 511 /// toggle block comments for specified text range 512 void toggleBlockComment(TextRange range, Object source); 513 514 /// returns paired bracket {} () [] for char at position p, returns paired char position or p if not found or not bracket 515 TextPosition findPairedBracket(TextPosition p); 516 517 /// returns true if smart indent is supported 518 bool supportsSmartIndents(); 519 /// apply smart indent after edit operation, if needed 520 void applySmartIndent(EditOperation op, Object source); 521 } 522 523 /// measure line text (tabs, spaces, and nonspace positions) 524 struct TextLineMeasure { 525 /// line length 526 int len; 527 /// first non-space index in line 528 int firstNonSpace = -1; 529 /// first non-space position according to tab size 530 int firstNonSpaceX; 531 /// last non-space character index in line 532 int lastNonSpace = -1; 533 /// last non-space position based on tab size 534 int lastNonSpaceX; 535 /// true if line has zero length or consists of spaces and tabs only 536 @property bool empty() { return len == 0 || firstNonSpace < 0; } 537 } 538 539 /// editable plain text (singleline/multiline) 540 class EditableContent { 541 542 this(bool multiline) { 543 _multiline = multiline; 544 _lines.length = 1; // initial state: single empty line 545 _editMarks.length = 1; 546 _undoBuffer = new UndoBuffer(); 547 } 548 549 @property bool modified() { 550 return _undoBuffer.modified; 551 } 552 553 protected UndoBuffer _undoBuffer; 554 555 protected SyntaxSupport _syntaxSupport; 556 557 @property SyntaxSupport syntaxSupport() { 558 return _syntaxSupport; 559 } 560 561 @property EditableContent syntaxSupport(SyntaxSupport syntaxSupport) { 562 _syntaxSupport = syntaxSupport; 563 if (_syntaxSupport) { 564 _syntaxSupport.content = this; 565 updateTokenProps(0, cast(int)_lines.length); 566 } 567 return this; 568 } 569 570 @property const(dstring[]) lines() { 571 return _lines; 572 } 573 574 /// returns true if content has syntax highlight handler set 575 @property bool hasSyntaxHighlight() { 576 return _syntaxSupport !is null; 577 } 578 579 protected bool _readOnly; 580 581 @property bool readOnly() { 582 return _readOnly; 583 } 584 585 @property void readOnly(bool readOnly) { 586 _readOnly = readOnly; 587 } 588 589 protected LineIcons _lineIcons; 590 @property ref LineIcons lineIcons() { return _lineIcons; } 591 592 protected int _tabSize = 4; 593 protected bool _useSpacesForTabs = true; 594 /// returns tab size (in number of spaces) 595 @property int tabSize() { 596 return _tabSize; 597 } 598 /// sets tab size (in number of spaces) 599 @property EditableContent tabSize(int newTabSize) { 600 if (newTabSize < 1) 601 newTabSize = 1; 602 else if (newTabSize > 16) 603 newTabSize = 16; 604 _tabSize = newTabSize; 605 return this; 606 } 607 /// when true, spaces will be inserted instead of tabs 608 @property bool useSpacesForTabs() { 609 return _useSpacesForTabs; 610 } 611 /// set new Tab key behavior flag: when true, spaces will be inserted instead of tabs 612 @property EditableContent useSpacesForTabs(bool useSpacesForTabs) { 613 _useSpacesForTabs = useSpacesForTabs; 614 return this; 615 } 616 617 /// true if smart indents are supported 618 @property bool supportsSmartIndents() { return _syntaxSupport && _syntaxSupport.supportsSmartIndents; } 619 620 protected bool _smartIndents; 621 /// true if smart indents are enabled 622 @property bool smartIndents() { return _smartIndents; } 623 /// set smart indents enabled flag 624 @property EditableContent smartIndents(bool enabled) { _smartIndents = enabled; return this; } 625 626 protected bool _smartIndentsAfterPaste; 627 /// true if smart indents are enabled 628 @property bool smartIndentsAfterPaste() { return _smartIndentsAfterPaste; } 629 /// set smart indents enabled flag 630 @property EditableContent smartIndentsAfterPaste(bool enabled) { _smartIndentsAfterPaste = enabled; return this; } 631 632 /// listeners for edit operations 633 Signal!EditableContentListener contentChanged; 634 /// listeners for mark changes after edit operation 635 Signal!EditableContentMarksChangeListener marksChanged; 636 637 protected bool _multiline; 638 /// returns true if miltyline content is supported 639 @property bool multiline() { return _multiline; } 640 641 /// text content by lines 642 protected dstring[] _lines; 643 /// token properties by lines - for syntax highlight 644 protected TokenPropString[] _tokenProps; 645 646 /// line edit marks 647 protected EditStateMark[] _editMarks; 648 @property EditStateMark[] editMarks() { return _editMarks; } 649 650 /// returns all lines concatenated delimited by '\n' 651 @property dstring text() const { 652 if (_lines.length == 0) 653 return ""; 654 if (_lines.length == 1) 655 return _lines[0]; 656 // concat lines 657 dchar[] buf; 658 foreach(index, item;_lines) { 659 if (index) 660 buf ~= EOL; 661 buf ~= item; 662 } 663 return cast(dstring)buf; 664 } 665 666 /// append one or more lines at end 667 void appendLines(dstring[] lines...) { 668 TextRange rangeBefore; 669 rangeBefore.start = rangeBefore.end = lineEnd(_lines.length ? cast(int)_lines.length - 1 : 0); 670 EditOperation op = new EditOperation(EditAction.Replace, rangeBefore, lines); 671 performOperation(op, this); 672 } 673 674 static alias isAlphaForWordSelection = isAlNum; 675 676 /// get word bounds by position 677 TextRange wordBounds(TextPosition pos) { 678 TextRange res; 679 res.start = pos; 680 res.end = pos; 681 if (pos.line < 0 || pos.line >= _lines.length) 682 return res; 683 dstring s = line(pos.line); 684 int p = pos.pos; 685 if (p < 0 || p > s.length || s.length == 0) 686 return res; 687 dchar leftChar = p > 0 ? s[p - 1] : 0; 688 dchar rightChar = p < s.length - 1 ? s[p + 1] : 0; 689 dchar centerChar = p < s.length ? s[p] : 0; 690 if (isAlphaForWordSelection(centerChar)) { 691 // ok 692 } else if (isAlphaForWordSelection(leftChar)) { 693 p--; 694 } else if (isAlphaForWordSelection(rightChar)) { 695 p++; 696 } else { 697 return res; 698 } 699 int start = p; 700 int end = p; 701 while (start > 0 && isAlphaForWordSelection(s[start - 1])) 702 start--; 703 while (end + 1 < s.length && isAlphaForWordSelection(s[end + 1])) 704 end++; 705 end++; 706 res.start.pos = start; 707 res.end.pos = end; 708 return res; 709 } 710 711 /// call listener to say that whole content is replaced e.g. by loading from file 712 void notifyContentReplaced() { 713 clearEditMarks(); 714 TextRange rangeBefore; 715 TextRange rangeAfter; 716 // notify about content change 717 handleContentChange(new EditOperation(EditAction.ReplaceContent), rangeBefore, rangeAfter, this); 718 } 719 720 /// call listener to say that content is saved 721 void notifyContentSaved() { 722 // mark all changed lines as saved 723 foreach(i; 0 .. _editMarks.length) { 724 if (_editMarks[i] == EditStateMark.changed) 725 _editMarks[i] = EditStateMark.saved; 726 } 727 TextRange rangeBefore; 728 TextRange rangeAfter; 729 // notify about content change 730 handleContentChange(new EditOperation(EditAction.SaveContent), rangeBefore, rangeAfter, this); 731 } 732 733 bool findMatchedBraces(TextPosition p, out TextRange range) { 734 if (!_syntaxSupport) 735 return false; 736 TextPosition p2 = _syntaxSupport.findPairedBracket(p); 737 if (p == p2) 738 return false; 739 if (p < p2) { 740 range.start = p; 741 range.end = p2; 742 } else { 743 range.start = p2; 744 range.end = p; 745 } 746 return true; 747 } 748 749 protected void updateTokenProps(int startLine, int endLine) { 750 clearTokenProps(startLine, endLine); 751 if (_syntaxSupport) { 752 _syntaxSupport.updateHighlight(_lines, _tokenProps, startLine, endLine); 753 } 754 } 755 756 protected void markChangedLines(int startLine, int endLine) { 757 foreach(i; startLine .. endLine) { 758 _editMarks[i] = EditStateMark.changed; 759 } 760 } 761 762 /// set props arrays size equal to text line sizes, bit fill with unknown token 763 protected void clearTokenProps(int startLine, int endLine) { 764 foreach(i; startLine .. endLine) { 765 if (hasSyntaxHighlight) { 766 int len = cast(int)_lines[i].length; 767 _tokenProps[i].length = len; 768 foreach(j; 0 .. len) 769 _tokenProps[i][j] = TOKEN_UNKNOWN; 770 } else { 771 _tokenProps[i] = null; // no token props 772 } 773 } 774 } 775 776 void clearEditMarks() { 777 _editMarks.length = _lines.length; 778 foreach(i; 0 .. _editMarks.length) 779 _editMarks[i] = EditStateMark.unchanged; 780 } 781 782 /// replace whole text with another content 783 @property EditableContent text(dstring newContent) { 784 clearUndo(); 785 _lines.length = 0; 786 if (_multiline) { 787 _lines = splitDString(newContent); 788 _tokenProps.length = _lines.length; 789 updateTokenProps(0, cast(int)_lines.length); 790 } else { 791 _lines.length = 1; 792 _lines[0] = replaceEolsWithSpaces(newContent); 793 _tokenProps.length = 1; 794 updateTokenProps(0, cast(int)_lines.length); 795 } 796 clearEditMarks(); 797 notifyContentReplaced(); 798 return this; 799 } 800 801 /// clear content 802 void clear() { 803 clearUndo(); 804 clearEditMarks(); 805 _lines.length = 0; 806 } 807 808 809 /// returns line count 810 @property int length() { return cast(int)_lines.length; } 811 dstring opIndex(int index) { 812 return line(index); 813 } 814 815 /// returns line text by index, "" if index is out of bounds 816 dstring line(int index) { 817 return index >= 0 && index < _lines.length ? _lines[index] : ""d; 818 } 819 820 /// returns character at position lineIndex, pos 821 dchar opIndex(int lineIndex, int pos) { 822 dstring s = line(lineIndex); 823 if (pos >= 0 && pos < s.length) 824 return s[pos]; 825 return 0; 826 } 827 /// returns character at position lineIndex, pos 828 dchar opIndex(TextPosition p) { 829 dstring s = line(p.line); 830 if (p.pos >= 0 && p.pos < s.length) 831 return s[p.pos]; 832 return 0; 833 } 834 835 /// returns line token properties one item per character (index is 0 based line number) 836 TokenPropString lineTokenProps(int index) { 837 return index >= 0 && index < _tokenProps.length ? _tokenProps[index] : null; 838 } 839 840 /// returns token properties character position 841 TokenProp tokenProp(TextPosition p) { 842 return p.line >= 0 && p.line < _tokenProps.length && p.pos >= 0 && p.pos < _tokenProps[p.line].length ? _tokenProps[p.line][p.pos] : 0; 843 } 844 845 /// returns position for end of last line 846 @property TextPosition endOfFile() { 847 return TextPosition(cast(int)_lines.length - 1, cast(int)_lines[$-1].length); 848 } 849 850 /// returns access to line edit mark by line index (0 based) 851 ref EditStateMark editMark(int index) { 852 assert (index >= 0 && index < _editMarks.length); 853 return _editMarks[index]; 854 } 855 856 /// returns text position for end of line lineIndex 857 TextPosition lineEnd(int lineIndex) { 858 return TextPosition(lineIndex, lineLength(lineIndex)); 859 } 860 861 /// returns text position for begin of line lineIndex (if lineIndex > number of lines, returns end of last line) 862 TextPosition lineBegin(int lineIndex) { 863 if (lineIndex >= _lines.length) 864 return lineEnd(lineIndex - 1); 865 return TextPosition(lineIndex, 0); 866 } 867 868 /// returns previous character position 869 TextPosition prevCharPos(TextPosition p) { 870 if (p.line < 0) 871 return TextPosition(0, 0); 872 p.pos--; 873 for (;;) { 874 if (p.line < 0) 875 return TextPosition(0, 0); 876 if (p.pos >= 0 && p.pos < lineLength(p.line)) 877 return p; 878 p.line--; 879 p.pos = lineLength(p.line) - 1; 880 } 881 } 882 883 /// returns previous character position 884 TextPosition nextCharPos(TextPosition p) { 885 TextPosition eof = endOfFile(); 886 if (p >= eof) 887 return eof; 888 p.pos++; 889 for (;;) { 890 if (p >= eof) 891 return eof; 892 if (p.pos >= 0 && p.pos < lineLength(p.line)) 893 return p; 894 p.line++; 895 p.pos = 0; 896 } 897 } 898 899 /// returns text range for whole line lineIndex 900 TextRange lineRange(int lineIndex) { 901 return TextRange(TextPosition(lineIndex, 0), lineIndex < cast(int)_lines.length - 1 ? lineBegin(lineIndex + 1) : lineEnd(lineIndex)); 902 } 903 904 /// find nearest next tab position 905 int nextTab(int pos) { 906 return (pos + tabSize) / tabSize * tabSize; 907 } 908 909 /// to return information about line space positions 910 static struct LineWhiteSpace { 911 int firstNonSpaceIndex = -1; 912 int firstNonSpaceColumn = -1; 913 int lastNonSpaceIndex = -1; 914 int lastNonSpaceColumn = -1; 915 @property bool empty() { return firstNonSpaceColumn < 0; } 916 } 917 918 LineWhiteSpace getLineWhiteSpace(int lineIndex) { 919 LineWhiteSpace res; 920 if (lineIndex < 0 || lineIndex >= _lines.length) 921 return res; 922 dstring s = _lines[lineIndex]; 923 int x = 0; 924 for (int i = 0; i < s.length; i++) { 925 dchar ch = s[i]; 926 if (ch == '\t') { 927 x = (x + _tabSize) / _tabSize * _tabSize; 928 } else if (ch == ' ') { 929 x++; 930 } else { 931 if (res.firstNonSpaceIndex < 0) { 932 res.firstNonSpaceIndex = i; 933 res.firstNonSpaceColumn = x; 934 } 935 res.lastNonSpaceIndex = i; 936 res.lastNonSpaceColumn = x; 937 x++; 938 } 939 } 940 return res; 941 } 942 943 /// returns spaces/tabs for filling from the beginning of line to specified position 944 dstring fillSpace(int pos) { 945 dchar[] buf; 946 int x = 0; 947 while (x + tabSize <= pos) { 948 if (useSpacesForTabs) { 949 foreach(i; 0 .. tabSize) 950 buf ~= ' '; 951 } else { 952 buf ~= '\t'; 953 } 954 x += tabSize; 955 } 956 while (x < pos) { 957 buf ~= ' '; 958 x++; 959 } 960 return cast(dstring)buf; 961 } 962 963 /// measures line non-space start and end positions 964 TextLineMeasure measureLine(int lineIndex) { 965 TextLineMeasure res; 966 dstring s = _lines[lineIndex]; 967 res.len = cast(int)s.length; 968 if (lineIndex < 0 || lineIndex >= _lines.length) 969 return res; 970 int x = 0; 971 for (int i = 0; i < s.length; i++) { 972 dchar ch = s[i]; 973 if (ch == ' ') { 974 x++; 975 } else if (ch == '\t') { 976 x = (x + _tabSize) / _tabSize * _tabSize; 977 } else { 978 if (res.firstNonSpace < 0) { 979 res.firstNonSpace = i; 980 res.firstNonSpaceX = x; 981 } 982 res.lastNonSpace = i; 983 res.lastNonSpaceX = x; 984 x++; 985 } 986 } 987 return res; 988 } 989 990 /// return true if line with index lineIndex is empty (has length 0 or consists only of spaces and tabs) 991 bool lineIsEmpty(int lineIndex) { 992 if (lineIndex < 0 || lineIndex >= _lines.length) 993 return true; 994 dstring s = _lines[lineIndex]; 995 foreach(ch; s) 996 if (ch != ' ' && ch != '\t') 997 return false; 998 return true; 999 } 1000 1001 /// corrent range to cover full lines 1002 TextRange fullLinesRange(TextRange r) { 1003 r.start.pos = 0; 1004 if (r.end.pos > 0 || r.start.line == r.end.line) 1005 r.end = lineBegin(r.end.line + 1); 1006 return r; 1007 } 1008 1009 /// returns position before first non-space character of line, returns 0 position if no non-space chars 1010 TextPosition firstNonSpace(int lineIndex) { 1011 dstring s = line(lineIndex); 1012 for (int i = 0; i < s.length; i++) 1013 if (s[i] != ' ' && s[i] != '\t') 1014 return TextPosition(lineIndex, i); 1015 return TextPosition(lineIndex, 0); 1016 } 1017 1018 /// returns position after last non-space character of line, returns 0 position if no non-space chars on line 1019 TextPosition lastNonSpace(int lineIndex) { 1020 dstring s = line(lineIndex); 1021 for (int i = cast(int)s.length - 1; i >= 0; i--) 1022 if (s[i] != ' ' && s[i] != '\t') 1023 return TextPosition(lineIndex, i + 1); 1024 return TextPosition(lineIndex, 0); 1025 } 1026 1027 /// returns text position for end of line lineIndex 1028 int lineLength(int lineIndex) { 1029 return lineIndex >= 0 && lineIndex < _lines.length ? cast(int)_lines[lineIndex].length : 0; 1030 } 1031 1032 /// returns maximum length of line 1033 int maxLineLength() { 1034 int m = 0; 1035 foreach(s; _lines) 1036 if (m < s.length) 1037 m = cast(int)s.length; 1038 return m; 1039 } 1040 1041 void handleContentChange(EditOperation op, ref TextRange rangeBefore, ref TextRange rangeAfter, Object source) { 1042 // update highlight if necessary 1043 updateTokenProps(rangeAfter.start.line, rangeAfter.end.line + 1); 1044 LineIcon[] moved; 1045 LineIcon[] removed; 1046 if (_lineIcons.updateLinePositions(rangeBefore, rangeAfter, moved, removed)) { 1047 if (marksChanged.assigned) 1048 marksChanged(this, moved, removed); 1049 } 1050 // call listeners 1051 if (contentChanged.assigned) 1052 contentChanged(this, op, rangeBefore, rangeAfter, source); 1053 } 1054 1055 /// return edit marks for specified range 1056 EditStateMark[] rangeMarks(TextRange range) { 1057 EditStateMark[] res; 1058 if (range.empty) { 1059 res ~= EditStateMark.unchanged; 1060 return res; 1061 } 1062 for (int lineIndex = range.start.line; lineIndex <= range.end.line; lineIndex++) { 1063 res ~= _editMarks[lineIndex]; 1064 } 1065 return res; 1066 } 1067 1068 /// return text for specified range 1069 dstring[] rangeText(TextRange range) { 1070 dstring[] res; 1071 if (range.empty) { 1072 res ~= ""d; 1073 return res; 1074 } 1075 for (int lineIndex = range.start.line; lineIndex <= range.end.line; lineIndex++) { 1076 dstring lineText = line(lineIndex); 1077 dstring lineFragment = lineText; 1078 int startchar = (lineIndex == range.start.line) ? range.start.pos : 0; 1079 int endchar = (lineIndex == range.end.line) ? range.end.pos : cast(int)lineText.length; 1080 if (endchar > lineText.length) 1081 endchar = cast(int)lineText.length; 1082 if (endchar <= startchar) 1083 lineFragment = ""d; 1084 else if (startchar != 0 || endchar != lineText.length) 1085 lineFragment = lineText[startchar .. endchar].dup; 1086 res ~= lineFragment; 1087 } 1088 return res; 1089 } 1090 1091 /// when position is out of content bounds, fix it to nearest valid position 1092 void correctPosition(ref TextPosition position) { 1093 if (position.line >= length) { 1094 position.line = length - 1; 1095 position.pos = lineLength(position.line); 1096 } 1097 if (position.line < 0) { 1098 position.line = 0; 1099 position.pos = 0; 1100 } 1101 int currentLineLength = lineLength(position.line); 1102 if (position.pos > currentLineLength) 1103 position.pos = currentLineLength; 1104 if (position.pos < 0) 1105 position.pos = 0; 1106 } 1107 1108 /// when range positions is out of content bounds, fix it to nearest valid position 1109 void correctRange(ref TextRange range) { 1110 correctPosition(range.start); 1111 correctPosition(range.end); 1112 } 1113 1114 /// removes removedCount lines starting from start 1115 protected void removeLines(int start, int removedCount) { 1116 int end = start + removedCount; 1117 assert(removedCount > 0 && start >= 0 && end > 0 && start < _lines.length && end <= _lines.length); 1118 for (int i = start; i < _lines.length - removedCount; i++) { 1119 _lines[i] = _lines[i + removedCount]; 1120 _tokenProps[i] = _tokenProps[i + removedCount]; 1121 _editMarks[i] = _editMarks[i + removedCount]; 1122 } 1123 for (int i = cast(int)_lines.length - removedCount; i < _lines.length; i++) { 1124 _lines[i] = null; // free unused line references 1125 _tokenProps[i] = null; // free unused line references 1126 _editMarks[i] = EditStateMark.unchanged; // free unused line references 1127 } 1128 _lines.length -= removedCount; 1129 _tokenProps.length = _lines.length; 1130 _editMarks.length = _lines.length; 1131 } 1132 1133 /// inserts count empty lines at specified position 1134 protected void insertLines(int start, int count) 1135 in { assert(count > 0); } 1136 do { 1137 _lines.length += count; 1138 _tokenProps.length = _lines.length; 1139 _editMarks.length = _lines.length; 1140 for (int i = cast(int)_lines.length - 1; i >= start + count; i--) { 1141 _lines[i] = _lines[i - count]; 1142 _tokenProps[i] = _tokenProps[i - count]; 1143 _editMarks[i] = _editMarks[i - count]; 1144 } 1145 foreach(i; start .. start + count) { 1146 _lines[i] = ""d; 1147 _tokenProps[i] = null; 1148 _editMarks[i] = EditStateMark.changed; 1149 } 1150 } 1151 1152 /// inserts or removes lines, removes text in range 1153 protected void replaceRange(TextRange before, TextRange after, dstring[] newContent, EditStateMark[] marks = null) { 1154 dstring firstLineBefore = line(before.start.line); 1155 dstring lastLineBefore = before.singleLine ? firstLineBefore : line(before.end.line); 1156 dstring firstLineHead = before.start.pos > 0 && before.start.pos <= firstLineBefore.length ? firstLineBefore[0..before.start.pos] : ""d; 1157 dstring lastLineTail = before.end.pos >= 0 && before.end.pos < lastLineBefore.length ? lastLineBefore[before.end.pos .. $] : ""d; 1158 1159 int linesBefore = before.lines; 1160 int linesAfter = after.lines; 1161 if (linesBefore < linesAfter) { 1162 // add more lines 1163 insertLines(before.start.line + 1, linesAfter - linesBefore); 1164 } else if (linesBefore > linesAfter) { 1165 // remove extra lines 1166 removeLines(before.start.line + 1, linesBefore - linesAfter); 1167 } 1168 foreach(int i; after.start.line .. after.end.line + 1) { 1169 if (marks) { 1170 //if (i - after.start.line < marks.length) 1171 _editMarks[i] = marks[i - after.start.line]; 1172 } 1173 dstring newline = newContent[i - after.start.line]; 1174 if (i == after.start.line && i == after.end.line) { 1175 dchar[] buf; 1176 buf ~= firstLineHead; 1177 buf ~= newline; 1178 buf ~= lastLineTail; 1179 //Log.d("merging lines ", firstLineHead, " ", newline, " ", lastLineTail); 1180 _lines[i] = cast(dstring)buf; 1181 clearTokenProps(i, i + 1); 1182 if (!marks) 1183 markChangedLines(i, i + 1); 1184 //Log.d("merge result: ", _lines[i]); 1185 } else if (i == after.start.line) { 1186 dchar[] buf; 1187 buf ~= firstLineHead; 1188 buf ~= newline; 1189 _lines[i] = cast(dstring)buf; 1190 clearTokenProps(i, i + 1); 1191 if (!marks) 1192 markChangedLines(i, i + 1); 1193 } else if (i == after.end.line) { 1194 dchar[] buf; 1195 buf ~= newline; 1196 buf ~= lastLineTail; 1197 _lines[i] = cast(dstring)buf; 1198 clearTokenProps(i, i + 1); 1199 if (!marks) 1200 markChangedLines(i, i + 1); 1201 } else { 1202 _lines[i] = newline; // no dup needed 1203 clearTokenProps(i, i + 1); 1204 if (!marks) 1205 markChangedLines(i, i + 1); 1206 } 1207 } 1208 } 1209 1210 1211 static alias isDigit = std.uni.isNumber; 1212 static bool isAlpha(dchar ch) pure nothrow { 1213 static import std.uni; 1214 return std.uni.isAlpha(ch) || ch == '_'; 1215 } 1216 static bool isAlNum(dchar ch) pure nothrow { 1217 static import std.uni; 1218 return isDigit(ch) || isAlpha(ch); 1219 } 1220 static bool isLowerAlpha(dchar ch) pure nothrow { 1221 static import std.uni; 1222 return std.uni.isLower(ch) || ch == '_'; 1223 } 1224 static alias isUpperAlpha = std.uni.isUpper; 1225 static bool isPunct(dchar ch) pure nothrow { 1226 switch(ch) { 1227 case '.': 1228 case ',': 1229 case ';': 1230 case '?': 1231 case '!': 1232 return true; 1233 default: 1234 return false; 1235 } 1236 } 1237 static bool isBracket(dchar ch) pure nothrow { 1238 switch(ch) { 1239 case '(': 1240 case ')': 1241 case '[': 1242 case ']': 1243 case '{': 1244 case '}': 1245 return true; 1246 default: 1247 return false; 1248 } 1249 } 1250 1251 static bool isWordBound(dchar thischar, dchar nextchar) { 1252 return (isAlNum(thischar) && !isAlNum(nextchar)) 1253 || (isPunct(thischar) && !isPunct(nextchar)) 1254 || (isBracket(thischar) && !isBracket(nextchar)) 1255 || (thischar != ' ' && nextchar == ' '); 1256 } 1257 1258 /// change text position to nearest word bound (direction < 0 - back, > 0 - forward) 1259 TextPosition moveByWord(TextPosition p, int direction, bool camelCasePartsAsWords) { 1260 correctPosition(p); 1261 TextPosition firstns = firstNonSpace(p.line); // before first non space 1262 TextPosition lastns = lastNonSpace(p.line); // after last non space 1263 int linelen = lineLength(p.line); // line length 1264 if (direction < 0) { 1265 // back 1266 if (p.pos <= 0) { 1267 // beginning of line - move to prev line 1268 if (p.line > 0) 1269 p = lastNonSpace(p.line - 1); 1270 } else if (p.pos <= firstns.pos) { // before first nonspace 1271 // to beginning of line 1272 p.pos = 0; 1273 } else { 1274 dstring txt = line(p.line); 1275 int found = -1; 1276 for (int i = p.pos - 1; i > 0; i--) { 1277 // check if position i + 1 is after word end 1278 dchar thischar = i >= 0 && i < linelen ? txt[i] : ' '; 1279 if (thischar == '\t') 1280 thischar = ' '; 1281 dchar nextchar = i - 1 >= 0 && i - 1 < linelen ? txt[i - 1] : ' '; 1282 if (nextchar == '\t') 1283 nextchar = ' '; 1284 if (isWordBound(thischar, nextchar) 1285 || (camelCasePartsAsWords && isUpperAlpha(thischar) && isLowerAlpha(nextchar))) { 1286 found = i; 1287 break; 1288 } 1289 } 1290 if (found >= 0) 1291 p.pos = found; 1292 else 1293 p.pos = 0; 1294 } 1295 } else if (direction > 0) { 1296 // forward 1297 if (p.pos >= linelen) { 1298 // last position of line 1299 if (p.line < length - 1) 1300 p = firstNonSpace(p.line + 1); 1301 } else if (p.pos >= lastns.pos) { // before first nonspace 1302 // to beginning of line 1303 p.pos = linelen; 1304 } else { 1305 dstring txt = line(p.line); 1306 int found = -1; 1307 for (int i = p.pos; i < linelen; i++) { 1308 // check if position i + 1 is after word end 1309 dchar thischar = txt[i]; 1310 if (thischar == '\t') 1311 thischar = ' '; 1312 dchar nextchar = i < linelen - 1 ? txt[i + 1] : ' '; 1313 if (nextchar == '\t') 1314 nextchar = ' '; 1315 if (isWordBound(thischar, nextchar) 1316 || (camelCasePartsAsWords && isLowerAlpha(thischar) && isUpperAlpha(nextchar))) { 1317 found = i + 1; 1318 break; 1319 } 1320 } 1321 if (found >= 0) 1322 p.pos = found; 1323 else 1324 p.pos = linelen; 1325 } 1326 } 1327 return p; 1328 } 1329 1330 /// edit content 1331 bool performOperation(EditOperation op, Object source) { 1332 if (_readOnly) 1333 throw new Exception("content is readonly"); 1334 if (op.action == EditAction.Replace) { 1335 TextRange rangeBefore = op.range; 1336 assert(rangeBefore.start <= rangeBefore.end); 1337 //correctRange(rangeBefore); 1338 dstring[] oldcontent = rangeText(rangeBefore); 1339 EditStateMark[] oldmarks = rangeMarks(rangeBefore); 1340 dstring[] newcontent = op.content; 1341 if (newcontent.length == 0) 1342 newcontent ~= ""d; 1343 TextRange rangeAfter = op.range; 1344 rangeAfter.end = rangeAfter.start; 1345 if (newcontent.length > 1) { 1346 // different lines 1347 rangeAfter.end.line = rangeAfter.start.line + cast(int)newcontent.length - 1; 1348 rangeAfter.end.pos = cast(int)newcontent[$ - 1].length; 1349 } else { 1350 // same line 1351 rangeAfter.end.pos = rangeAfter.start.pos + cast(int)newcontent[0].length; 1352 } 1353 assert(rangeAfter.start <= rangeAfter.end); 1354 op.newRange = rangeAfter; 1355 op.oldContent = oldcontent; 1356 op.oldEditMarks = oldmarks; 1357 replaceRange(rangeBefore, rangeAfter, newcontent); 1358 _undoBuffer.saveForUndo(op); 1359 handleContentChange(op, rangeBefore, rangeAfter, source); 1360 return true; 1361 } 1362 return false; 1363 } 1364 1365 /// return true if there is at least one operation in undo buffer 1366 @property bool hasUndo() { 1367 return _undoBuffer.hasUndo; 1368 } 1369 /// return true if there is at least one operation in redo buffer 1370 @property bool hasRedo() { 1371 return _undoBuffer.hasRedo; 1372 } 1373 /// undoes last change 1374 bool undo(Object source) { 1375 if (!hasUndo) 1376 return false; 1377 if (_readOnly) 1378 throw new Exception("content is readonly"); 1379 EditOperation op = _undoBuffer.undo(); 1380 TextRange rangeBefore = op.newRange; 1381 dstring[] oldcontent = op.content; 1382 dstring[] newcontent = op.oldContent; 1383 EditStateMark[] newmarks = op.oldEditMarks; //_undoBuffer.savedInUndo() ? : null; 1384 TextRange rangeAfter = op.range; 1385 //Log.d("Undoing op rangeBefore=", rangeBefore, " contentBefore=`", oldcontent, "` rangeAfter=", rangeAfter, " contentAfter=`", newcontent, "`"); 1386 replaceRange(rangeBefore, rangeAfter, newcontent, newmarks); 1387 handleContentChange(op, rangeBefore, rangeAfter, source ? source : this); 1388 return true; 1389 } 1390 1391 /// redoes last undone change 1392 bool redo(Object source) { 1393 if (!hasRedo) 1394 return false; 1395 if (_readOnly) 1396 throw new Exception("content is readonly"); 1397 EditOperation op = _undoBuffer.redo(); 1398 TextRange rangeBefore = op.range; 1399 dstring[] oldcontent = op.oldContent; 1400 dstring[] newcontent = op.content; 1401 TextRange rangeAfter = op.newRange; 1402 //Log.d("Redoing op rangeBefore=", rangeBefore, " contentBefore=`", oldcontent, "` rangeAfter=", rangeAfter, " contentAfter=`", newcontent, "`"); 1403 replaceRange(rangeBefore, rangeAfter, newcontent); 1404 handleContentChange(op, rangeBefore, rangeAfter, source ? source : this); 1405 return true; 1406 } 1407 /// clear undo/redp history 1408 void clearUndo() { 1409 _undoBuffer.clear(); 1410 } 1411 1412 protected string _filename; 1413 protected TextFileFormat _format; 1414 1415 /// file used to load editor content 1416 @property string filename() { 1417 return _filename; 1418 } 1419 1420 1421 /// load content form input stream 1422 bool load(InputStream f, string fname = null) { 1423 import dlangui.core.linestream; 1424 clear(); 1425 _filename = fname; 1426 _format = TextFileFormat.init; 1427 try { 1428 LineStream lines = LineStream.create(f, fname); 1429 for (;;) { 1430 dchar[] s = lines.readLine(); 1431 if (s is null) 1432 break; 1433 int pos = cast(int)(_lines.length++); 1434 _tokenProps.length = _lines.length; 1435 _lines[pos] = s.dup; 1436 clearTokenProps(pos, pos + 1); 1437 } 1438 if (lines.errorCode != 0) { 1439 clear(); 1440 Log.e("Error ", lines.errorCode, " ", lines.errorMessage, " -- at line ", lines.errorLine, " position ", lines.errorPos); 1441 notifyContentReplaced(); 1442 return false; 1443 } 1444 // EOF 1445 _format = lines.textFormat; 1446 _undoBuffer.clear(); 1447 debug(FileFormats)Log.d("loaded file:", filename, " format detected:", _format); 1448 notifyContentReplaced(); 1449 return true; 1450 } catch (Exception e) { 1451 Log.e("Exception while trying to read file ", fname, " ", e.toString); 1452 clear(); 1453 notifyContentReplaced(); 1454 return false; 1455 } 1456 } 1457 /// load content from file 1458 bool load(string filename) { 1459 import std.file : exists, isFile; 1460 import std.exception : ErrnoException; 1461 clear(); 1462 if (!filename.exists || !filename.isFile) { 1463 Log.e("Editable.load: File not found ", filename); 1464 return false; 1465 } 1466 try { 1467 InputStream f; 1468 f = new FileInputStream(filename); 1469 scope(exit) { f.close(); } 1470 bool res = load(f, filename); 1471 return res; 1472 } catch (ErrnoException e) { 1473 Log.e("Editable.load: Exception while trying to read file ", filename, " ", e.toString); 1474 clear(); 1475 return false; 1476 } catch (Exception e) { 1477 Log.e("Editable.load: Exception while trying to read file ", filename, " ", e.toString); 1478 clear(); 1479 return false; 1480 } 1481 } 1482 /// save to output stream in specified format 1483 bool save(OutputStream stream, string filename, TextFileFormat format) { 1484 if (!filename) 1485 filename = _filename; 1486 _format = format; 1487 import dlangui.core.linestream; 1488 try { 1489 debug(FileFormats)Log.d("creating output stream, file=", filename, " format=", format); 1490 OutputLineStream writer = new OutputLineStream(stream, filename, format); 1491 scope(exit) { writer.close(); } 1492 for (int i = 0; i < _lines.length; i++) { 1493 writer.writeLine(_lines[i]); 1494 } 1495 _undoBuffer.saved(); 1496 notifyContentSaved(); 1497 return true; 1498 } catch (Exception e) { 1499 Log.e("Exception while trying to write file ", filename, " ", e.toString); 1500 return false; 1501 } 1502 } 1503 /// save to output stream in current format 1504 bool save(OutputStream stream, string filename) { 1505 return save(stream, filename, _format); 1506 } 1507 /// save to file in specified format 1508 bool save(string filename, TextFileFormat format) { 1509 if (!filename) 1510 filename = _filename; 1511 try { 1512 OutputStream f = new FileOutputStream(filename); 1513 scope(exit) { f.close(); } 1514 return save(f, filename, format); 1515 } catch (Exception e) { 1516 Log.e("Exception while trying to save file ", filename, " ", e.toString); 1517 return false; 1518 } 1519 } 1520 /// save to file in current format 1521 bool save(string filename = null) { 1522 return save(filename, _format); 1523 } 1524 } 1525 1526 /// types of text editor line icon marks (bookmark / breakpoint / error / ...) 1527 enum LineIconType : int { 1528 /// bookmark 1529 bookmark, 1530 /// breakpoint mark 1531 breakpoint, 1532 /// error mark 1533 error, 1534 } 1535 1536 /// text editor line icon 1537 class LineIcon { 1538 /// mark type 1539 LineIconType type; 1540 /// line number 1541 int line; 1542 /// arbitrary parameter 1543 Object objectParam; 1544 /// empty 1545 this() { 1546 } 1547 this(LineIconType type, int line, Object obj = null) { 1548 this.type = type; 1549 this.line = line; 1550 this.objectParam = obj; 1551 } 1552 } 1553 1554 /// text editor line icon list 1555 struct LineIcons { 1556 private LineIcon[] _items; 1557 private int _len; 1558 1559 /// returns count of items 1560 @property int length() { return _len; } 1561 /// returns item by index, or null if index out of bounds 1562 LineIcon opIndex(int index) { 1563 if (index < 0 || index >= _len) 1564 return null; 1565 return _items[index]; 1566 } 1567 private void insert(LineIcon icon, int index) { 1568 if (index < 0) 1569 index = 0; 1570 if (index > _len) 1571 index = _len; 1572 if (_items.length <= index) 1573 _items.length = index + 16; 1574 if (index < _len) { 1575 for (size_t i = _len; i > index; i--) 1576 _items[i] = _items[i - 1]; 1577 } 1578 _items[index] = icon; 1579 _len++; 1580 } 1581 private int findSortedIndex(int line, LineIconType type) { 1582 // TODO: use binary search 1583 for (int i = 0; i < _len; i++) { 1584 if (_items[i].line > line || _items[i].type > type) { 1585 return i; 1586 } 1587 } 1588 return _len; 1589 } 1590 /// add icon mark 1591 void add(LineIcon icon) { 1592 int index = findSortedIndex(icon.line, icon.type); 1593 insert(icon, index); 1594 } 1595 /// add all icons from list 1596 void addAll(LineIcon[] list) { 1597 foreach(item; list) 1598 add(item); 1599 } 1600 /// remove icon mark by index 1601 LineIcon remove(int index) { 1602 if (index < 0 || index >= _len) 1603 return null; 1604 LineIcon res = _items[index]; 1605 for (int i = index; i < _len - 1; i++) 1606 _items[i] = _items[i + 1]; 1607 _items[_len] = null; 1608 _len--; 1609 return res; 1610 } 1611 1612 /// remove icon mark 1613 LineIcon remove(LineIcon icon) { 1614 // same object 1615 for (int i = 0; i < _len; i++) { 1616 if (_items[i] is icon) 1617 return remove(i); 1618 } 1619 // has the same objectParam 1620 for (int i = 0; i < _len; i++) { 1621 if (_items[i].objectParam !is null && icon.objectParam !is null && _items[i].objectParam is icon.objectParam) 1622 return remove(i); 1623 } 1624 // has same line and type 1625 for (int i = 0; i < _len; i++) { 1626 if (_items[i].line == icon.line && _items[i].type == icon.type) 1627 return remove(i); 1628 } 1629 return null; 1630 } 1631 1632 /// remove all icon marks of specified type, return true if any of items removed 1633 bool removeByType(LineIconType type) { 1634 bool res = false; 1635 for (int i = _len - 1; i >= 0; i--) { 1636 if (_items[i].type == type) { 1637 remove(i); 1638 res = true; 1639 } 1640 } 1641 return res; 1642 } 1643 /// get array of icons of specified type 1644 LineIcon[] findByType(LineIconType type) { 1645 LineIcon[] res; 1646 for (int i = 0; i < _len; i++) { 1647 if (_items[i].type == type) 1648 res ~= _items[i]; 1649 } 1650 return res; 1651 } 1652 /// get array of icons of specified type 1653 LineIcon findByLineAndType(int line, LineIconType type) { 1654 for (int i = 0; i < _len; i++) { 1655 if (_items[i].type == type && _items[i].line == line) 1656 return _items[i]; 1657 } 1658 return null; 1659 } 1660 /// update mark position lines after text change, returns true if any of marks were moved or removed 1661 bool updateLinePositions(TextRange rangeBefore, TextRange rangeAfter, ref LineIcon[] moved, ref LineIcon[] removed) { 1662 moved = null; 1663 removed = null; 1664 bool res = false; 1665 for (int i = _len - 1; i >= 0; i--) { 1666 LineIcon item = _items[i]; 1667 if (rangeBefore.start.line > item.line && rangeAfter.start.line > item.line) 1668 continue; // line is before ranges 1669 else if (rangeBefore.start.line < item.line || rangeAfter.start.line < item.line) { 1670 // line is fully after change 1671 int deltaLines = rangeAfter.end.line - rangeBefore.end.line; 1672 if (!deltaLines) 1673 continue; 1674 if (deltaLines < 0 && rangeBefore.end.line >= item.line && rangeAfter.end.line < item.line) { 1675 // remove 1676 removed ~= item; 1677 remove(i); 1678 res = true; 1679 } else { 1680 // move 1681 item.line += deltaLines; 1682 moved ~= item; 1683 res = true; 1684 } 1685 } 1686 } 1687 return res; 1688 } 1689 1690 LineIcon findNext(LineIconType type, int line, int direction) { 1691 LineIcon firstBefore; 1692 LineIcon firstAfter; 1693 if (direction < 0) { 1694 // backward 1695 for (int i = _len - 1; i >= 0; i--) { 1696 LineIcon item = _items[i]; 1697 if (item.type != type) 1698 continue; 1699 if (!firstBefore && item.line >= line) 1700 firstBefore = item; 1701 else if (!firstAfter && item.line < line) 1702 firstAfter = item; 1703 } 1704 } else { 1705 // forward 1706 for (int i = 0; i < _len; i++) { 1707 LineIcon item = _items[i]; 1708 if (item.type != type) 1709 continue; 1710 if (!firstBefore && item.line <= line) 1711 firstBefore = item; 1712 else if (!firstAfter && item.line > line) 1713 firstAfter = item; 1714 } 1715 } 1716 if (firstAfter) 1717 return firstAfter; 1718 return firstBefore; 1719 } 1720 1721 @property bool hasBookmarks() { 1722 for (int i = 0; i < _len; i++) { 1723 if (_items[i].type == LineIconType.bookmark) 1724 return true; 1725 } 1726 return false; 1727 } 1728 1729 void toggleBookmark(int line) { 1730 LineIcon existing = findByLineAndType(line, LineIconType.bookmark); 1731 if (existing) 1732 remove(existing); 1733 else 1734 add(new LineIcon(LineIconType.bookmark, line)); 1735 } 1736 } 1737