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