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 } 454 455 /// interface for custom syntax highlight, comments toggling, smart indents, and other language dependent features for source code editors 456 interface SyntaxSupport { 457 458 /// returns editable content 459 @property EditableContent content(); 460 /// set editable content 461 @property SyntaxSupport content(EditableContent content); 462 463 /// categorize characters in content by token types 464 void updateHighlight(dstring[] lines, TokenPropString[] props, int changeStartLine, int changeEndLine); 465 466 /// return true if toggle line comment is supported for file type 467 @property bool supportsToggleLineComment(); 468 /// return true if can toggle line comments for specified text range 469 bool canToggleLineComment(TextRange range); 470 /// toggle line comments for specified text range 471 void toggleLineComment(TextRange range, Object source); 472 473 /// return true if toggle block comment is supported for file type 474 @property bool supportsToggleBlockComment(); 475 /// return true if can toggle block comments for specified text range 476 bool canToggleBlockComment(TextRange range); 477 /// toggle block comments for specified text range 478 void toggleBlockComment(TextRange range, Object source); 479 480 /// returns paired bracket {} () [] for char at position p, returns paired char position or p if not found or not bracket 481 TextPosition findPairedBracket(TextPosition p); 482 483 /// returns true if smart indent is supported 484 bool supportsSmartIndents(); 485 /// apply smart indent after edit operation, if needed 486 void applySmartIndent(EditOperation op, Object source); 487 } 488 489 /// measure line text (tabs, spaces, and nonspace positions) 490 struct TextLineMeasure { 491 /// line length 492 int len; 493 /// first non-space index in line 494 int firstNonSpace = -1; 495 /// first non-space position according to tab size 496 int firstNonSpaceX; 497 /// last non-space character index in line 498 int lastNonSpace = -1; 499 /// last non-space position based on tab size 500 int lastNonSpaceX; 501 /// true if line has zero length or consists of spaces and tabs only 502 @property bool empty() { return len == 0 || firstNonSpace < 0; } 503 } 504 505 /// editable plain text (singleline/multiline) 506 class EditableContent { 507 508 this(bool multiline) { 509 _multiline = multiline; 510 _lines.length = 1; // initial state: single empty line 511 _editMarks.length = 1; 512 _undoBuffer = new UndoBuffer(); 513 } 514 515 @property bool modified() { 516 return _undoBuffer.modified; 517 } 518 519 protected UndoBuffer _undoBuffer; 520 521 protected SyntaxSupport _syntaxSupport; 522 523 @property SyntaxSupport syntaxSupport() { 524 return _syntaxSupport; 525 } 526 527 @property EditableContent syntaxSupport(SyntaxSupport syntaxSupport) { 528 _syntaxSupport = syntaxSupport; 529 if (_syntaxSupport) { 530 _syntaxSupport.content = this; 531 updateTokenProps(0, cast(int)_lines.length); 532 } 533 return this; 534 } 535 536 @property const(dstring[]) lines() { 537 return _lines; 538 } 539 540 /// returns true if content has syntax highlight handler set 541 @property bool hasSyntaxHighlight() { 542 return _syntaxSupport !is null; 543 } 544 545 protected bool _readOnly; 546 547 @property bool readOnly() { 548 return _readOnly; 549 } 550 551 @property void readOnly(bool readOnly) { 552 _readOnly = readOnly; 553 } 554 555 protected LineIcons _lineIcons; 556 @property ref LineIcons lineIcons() { return _lineIcons; } 557 558 protected int _tabSize = 4; 559 protected bool _useSpacesForTabs = true; 560 /// returns tab size (in number of spaces) 561 @property int tabSize() { 562 return _tabSize; 563 } 564 /// sets tab size (in number of spaces) 565 @property EditableContent tabSize(int newTabSize) { 566 if (newTabSize < 1) 567 newTabSize = 1; 568 else if (newTabSize > 16) 569 newTabSize = 16; 570 _tabSize = newTabSize; 571 return this; 572 } 573 /// when true, spaces will be inserted instead of tabs 574 @property bool useSpacesForTabs() { 575 return _useSpacesForTabs; 576 } 577 /// set new Tab key behavior flag: when true, spaces will be inserted instead of tabs 578 @property EditableContent useSpacesForTabs(bool useSpacesForTabs) { 579 _useSpacesForTabs = useSpacesForTabs; 580 return this; 581 } 582 583 /// true if smart indents are supported 584 @property bool supportsSmartIndents() { return _syntaxSupport && _syntaxSupport.supportsSmartIndents; } 585 586 protected bool _smartIndents; 587 /// true if smart indents are enabled 588 @property bool smartIndents() { return _smartIndents; } 589 /// set smart indents enabled flag 590 @property EditableContent smartIndents(bool enabled) { _smartIndents = enabled; return this; } 591 592 protected bool _smartIndentsAfterPaste; 593 /// true if smart indents are enabled 594 @property bool smartIndentsAfterPaste() { return _smartIndentsAfterPaste; } 595 /// set smart indents enabled flag 596 @property EditableContent smartIndentsAfterPaste(bool enabled) { _smartIndentsAfterPaste = enabled; return this; } 597 598 /// listeners for edit operations 599 Signal!EditableContentListener contentChanged; 600 /// listeners for mark changes after edit operation 601 Signal!EditableContentMarksChangeListener marksChanged; 602 603 protected bool _multiline; 604 /// returns true if miltyline content is supported 605 @property bool multiline() { return _multiline; } 606 607 /// text content by lines 608 protected dstring[] _lines; 609 /// token properties by lines - for syntax highlight 610 protected TokenPropString[] _tokenProps; 611 612 /// line edit marks 613 protected EditStateMark[] _editMarks; 614 @property EditStateMark[] editMarks() { return _editMarks; } 615 616 /// returns all lines concatenated delimited by '\n' 617 @property dstring text() { 618 if (_lines.length == 0) 619 return ""; 620 if (_lines.length == 1) 621 return _lines[0]; 622 // concat lines 623 dchar[] buf; 624 foreach(index, item;_lines) { 625 if (index) 626 buf ~= EOL; 627 buf ~= item; 628 } 629 return cast(dstring)buf; 630 } 631 632 /// append one or more lines at end 633 void appendLines(dstring[] lines...) { 634 TextRange rangeBefore; 635 rangeBefore.start = rangeBefore.end = lineEnd(_lines.length ? cast(int)_lines.length - 1 : 0); 636 EditOperation op = new EditOperation(EditAction.Replace, rangeBefore, lines); 637 performOperation(op, this); 638 } 639 640 static alias isAlphaForWordSelection = isAlNum; 641 642 /// get word bounds by position 643 TextRange wordBounds(TextPosition pos) { 644 TextRange res; 645 res.start = pos; 646 res.end = pos; 647 if (pos.line < 0 || pos.line >= _lines.length) 648 return res; 649 dstring s = line(pos.line); 650 int p = pos.pos; 651 if (p < 0 || p > s.length || s.length == 0) 652 return res; 653 dchar leftChar = p > 0 ? s[p - 1] : 0; 654 dchar rightChar = p < s.length - 1 ? s[p + 1] : 0; 655 dchar centerChar = p < s.length ? s[p] : 0; 656 if (isAlphaForWordSelection(centerChar)) { 657 // ok 658 } else if (isAlphaForWordSelection(leftChar)) { 659 p--; 660 } else if (isAlphaForWordSelection(rightChar)) { 661 p++; 662 } else { 663 return res; 664 } 665 int start = p; 666 int end = p; 667 while (start > 0 && isAlphaForWordSelection(s[start - 1])) 668 start--; 669 while (end + 1 < s.length && isAlphaForWordSelection(s[end + 1])) 670 end++; 671 end++; 672 res.start.pos = start; 673 res.end.pos = end; 674 return res; 675 } 676 677 /// call listener to say that whole content is replaced e.g. by loading from file 678 void notifyContentReplaced() { 679 clearEditMarks(); 680 TextRange rangeBefore; 681 TextRange rangeAfter; 682 // notify about content change 683 handleContentChange(new EditOperation(EditAction.ReplaceContent), rangeBefore, rangeAfter, this); 684 } 685 686 /// call listener to say that content is saved 687 void notifyContentSaved() { 688 // mark all changed lines as saved 689 foreach(i; 0 .. _editMarks.length) { 690 if (_editMarks[i] == EditStateMark.changed) 691 _editMarks[i] = EditStateMark.saved; 692 } 693 TextRange rangeBefore; 694 TextRange rangeAfter; 695 // notify about content change 696 handleContentChange(new EditOperation(EditAction.SaveContent), rangeBefore, rangeAfter, this); 697 } 698 699 bool findMatchedBraces(TextPosition p, out TextRange range) { 700 if (!_syntaxSupport) 701 return false; 702 TextPosition p2 = _syntaxSupport.findPairedBracket(p); 703 if (p == p2) 704 return false; 705 if (p < p2) { 706 range.start = p; 707 range.end = p2; 708 } else { 709 range.start = p2; 710 range.end = p; 711 } 712 return true; 713 } 714 715 protected void updateTokenProps(int startLine, int endLine) { 716 clearTokenProps(startLine, endLine); 717 if (_syntaxSupport) { 718 _syntaxSupport.updateHighlight(_lines, _tokenProps, startLine, endLine); 719 } 720 } 721 722 protected void markChangedLines(int startLine, int endLine) { 723 foreach(i; startLine .. endLine) { 724 _editMarks[i] = EditStateMark.changed; 725 } 726 } 727 728 /// set props arrays size equal to text line sizes, bit fill with unknown token 729 protected void clearTokenProps(int startLine, int endLine) { 730 foreach(i; startLine .. endLine) { 731 if (hasSyntaxHighlight) { 732 int len = cast(int)_lines[i].length; 733 _tokenProps[i].length = len; 734 foreach(j; 0 .. len) 735 _tokenProps[i][j] = TOKEN_UNKNOWN; 736 } else { 737 _tokenProps[i] = null; // no token props 738 } 739 } 740 } 741 742 void clearEditMarks() { 743 _editMarks.length = _lines.length; 744 foreach(i; 0 .. _editMarks.length) 745 _editMarks[i] = EditStateMark.unchanged; 746 } 747 748 /// replace whole text with another content 749 @property EditableContent text(dstring newContent) { 750 clearUndo(); 751 _lines.length = 0; 752 if (_multiline) { 753 _lines = splitDString(newContent); 754 _tokenProps.length = _lines.length; 755 updateTokenProps(0, cast(int)_lines.length); 756 } else { 757 _lines.length = 1; 758 _lines[0] = replaceEolsWithSpaces(newContent); 759 _tokenProps.length = 1; 760 updateTokenProps(0, cast(int)_lines.length); 761 } 762 clearEditMarks(); 763 notifyContentReplaced(); 764 return this; 765 } 766 767 /// clear content 768 void clear() { 769 clearUndo(); 770 clearEditMarks(); 771 _lines.length = 0; 772 } 773 774 775 /// returns line count 776 @property int length() { return cast(int)_lines.length; } 777 dstring opIndex(int index) { 778 return line(index); 779 } 780 781 /// returns line text by index, "" if index is out of bounds 782 dstring line(int index) { 783 return index >= 0 && index < _lines.length ? _lines[index] : ""d; 784 } 785 786 /// returns character at position lineIndex, pos 787 dchar opIndex(int lineIndex, int pos) { 788 dstring s = line(lineIndex); 789 if (pos >= 0 && pos < s.length) 790 return s[pos]; 791 return 0; 792 } 793 /// returns character at position lineIndex, pos 794 dchar opIndex(TextPosition p) { 795 dstring s = line(p.line); 796 if (p.pos >= 0 && p.pos < s.length) 797 return s[p.pos]; 798 return 0; 799 } 800 801 /// returns line token properties one item per character (index is 0 based line number) 802 TokenPropString lineTokenProps(int index) { 803 return index >= 0 && index < _tokenProps.length ? _tokenProps[index] : null; 804 } 805 806 /// returns token properties character position 807 TokenProp tokenProp(TextPosition p) { 808 return p.line >= 0 && p.line < _tokenProps.length && p.pos >= 0 && p.pos < _tokenProps[p.line].length ? _tokenProps[p.line][p.pos] : 0; 809 } 810 811 /// returns position for end of last line 812 @property TextPosition endOfFile() { 813 return TextPosition(cast(int)_lines.length - 1, cast(int)_lines[$-1].length); 814 } 815 816 /// returns access to line edit mark by line index (0 based) 817 ref EditStateMark editMark(int index) { 818 assert (index >= 0 && index < _editMarks.length); 819 return _editMarks[index]; 820 } 821 822 /// returns text position for end of line lineIndex 823 TextPosition lineEnd(int lineIndex) { 824 return TextPosition(lineIndex, lineLength(lineIndex)); 825 } 826 827 /// returns text position for begin of line lineIndex (if lineIndex > number of lines, returns end of last line) 828 TextPosition lineBegin(int lineIndex) { 829 if (lineIndex >= _lines.length) 830 return lineEnd(lineIndex - 1); 831 return TextPosition(lineIndex, 0); 832 } 833 834 /// returns previous character position 835 TextPosition prevCharPos(TextPosition p) { 836 if (p.line < 0) 837 return TextPosition(0, 0); 838 p.pos--; 839 for (;;) { 840 if (p.line < 0) 841 return TextPosition(0, 0); 842 if (p.pos >= 0 && p.pos < lineLength(p.line)) 843 return p; 844 p.line--; 845 p.pos = lineLength(p.line) - 1; 846 } 847 } 848 849 /// returns previous character position 850 TextPosition nextCharPos(TextPosition p) { 851 TextPosition eof = endOfFile(); 852 if (p >= eof) 853 return eof; 854 p.pos++; 855 for (;;) { 856 if (p >= eof) 857 return eof; 858 if (p.pos >= 0 && p.pos < lineLength(p.line)) 859 return p; 860 p.line++; 861 p.pos = 0; 862 } 863 } 864 865 /// returns text range for whole line lineIndex 866 TextRange lineRange(int lineIndex) { 867 return TextRange(TextPosition(lineIndex, 0), lineIndex < cast(int)_lines.length - 1 ? lineBegin(lineIndex + 1) : lineEnd(lineIndex)); 868 } 869 870 /// find nearest next tab position 871 int nextTab(int pos) { 872 return (pos + tabSize) / tabSize * tabSize; 873 } 874 875 /// to return information about line space positions 876 static struct LineWhiteSpace { 877 int firstNonSpaceIndex = -1; 878 int firstNonSpaceColumn = -1; 879 int lastNonSpaceIndex = -1; 880 int lastNonSpaceColumn = -1; 881 @property bool empty() { return firstNonSpaceColumn < 0; } 882 } 883 884 LineWhiteSpace getLineWhiteSpace(int lineIndex) { 885 LineWhiteSpace res; 886 if (lineIndex < 0 || lineIndex >= _lines.length) 887 return res; 888 dstring s = _lines[lineIndex]; 889 int x = 0; 890 for (int i = 0; i < s.length; i++) { 891 dchar ch = s[i]; 892 if (ch == '\t') { 893 x = (x + _tabSize) / _tabSize * _tabSize; 894 } else if (ch == ' ') { 895 x++; 896 } else { 897 if (res.firstNonSpaceIndex < 0) { 898 res.firstNonSpaceIndex = i; 899 res.firstNonSpaceColumn = x; 900 } 901 res.lastNonSpaceIndex = i; 902 res.lastNonSpaceColumn = x; 903 x++; 904 } 905 } 906 return res; 907 } 908 909 /// returns spaces/tabs for filling from the beginning of line to specified position 910 dstring fillSpace(int pos) { 911 dchar[] buf; 912 int x = 0; 913 while (x + tabSize <= pos) { 914 if (useSpacesForTabs) { 915 foreach(i; 0 .. tabSize) 916 buf ~= ' '; 917 } else { 918 buf ~= '\t'; 919 } 920 x += tabSize; 921 } 922 while (x < pos) { 923 buf ~= ' '; 924 x++; 925 } 926 return cast(dstring)buf; 927 } 928 929 /// measures line non-space start and end positions 930 TextLineMeasure measureLine(int lineIndex) { 931 TextLineMeasure res; 932 dstring s = _lines[lineIndex]; 933 res.len = cast(int)s.length; 934 if (lineIndex < 0 || lineIndex >= _lines.length) 935 return res; 936 int x = 0; 937 for (int i = 0; i < s.length; i++) { 938 dchar ch = s[i]; 939 if (ch == ' ') { 940 x++; 941 } else if (ch == '\t') { 942 x = (x + _tabSize) / _tabSize * _tabSize; 943 } else { 944 if (res.firstNonSpace < 0) { 945 res.firstNonSpace = i; 946 res.firstNonSpaceX = x; 947 } 948 res.lastNonSpace = i; 949 res.lastNonSpaceX = x; 950 x++; 951 } 952 } 953 return res; 954 } 955 956 /// return true if line with index lineIndex is empty (has length 0 or consists only of spaces and tabs) 957 bool lineIsEmpty(int lineIndex) { 958 if (lineIndex < 0 || lineIndex >= _lines.length) 959 return true; 960 dstring s = _lines[lineIndex]; 961 foreach(ch; s) 962 if (ch != ' ' && ch != '\t') 963 return false; 964 return true; 965 } 966 967 /// corrent range to cover full lines 968 TextRange fullLinesRange(TextRange r) { 969 r.start.pos = 0; 970 if (r.end.pos > 0 || r.start.line == r.end.line) 971 r.end = lineBegin(r.end.line + 1); 972 return r; 973 } 974 975 /// returns position before first non-space character of line, returns 0 position if no non-space chars 976 TextPosition firstNonSpace(int lineIndex) { 977 dstring s = line(lineIndex); 978 for (int i = 0; i < s.length; i++) 979 if (s[i] != ' ' && s[i] != '\t') 980 return TextPosition(lineIndex, i); 981 return TextPosition(lineIndex, 0); 982 } 983 984 /// returns position after last non-space character of line, returns 0 position if no non-space chars on line 985 TextPosition lastNonSpace(int lineIndex) { 986 dstring s = line(lineIndex); 987 for (int i = cast(int)s.length - 1; i >= 0; i--) 988 if (s[i] != ' ' && s[i] != '\t') 989 return TextPosition(lineIndex, i + 1); 990 return TextPosition(lineIndex, 0); 991 } 992 993 /// returns text position for end of line lineIndex 994 int lineLength(int lineIndex) { 995 return lineIndex >= 0 && lineIndex < _lines.length ? cast(int)_lines[lineIndex].length : 0; 996 } 997 998 /// returns maximum length of line 999 int maxLineLength() { 1000 int m = 0; 1001 foreach(s; _lines) 1002 if (m < s.length) 1003 m = cast(int)s.length; 1004 return m; 1005 } 1006 1007 void handleContentChange(EditOperation op, ref TextRange rangeBefore, ref TextRange rangeAfter, Object source) { 1008 // update highlight if necessary 1009 updateTokenProps(rangeAfter.start.line, rangeAfter.end.line + 1); 1010 LineIcon[] moved; 1011 LineIcon[] removed; 1012 if (_lineIcons.updateLinePositions(rangeBefore, rangeAfter, moved, removed)) { 1013 if (marksChanged.assigned) 1014 marksChanged(this, moved, removed); 1015 } 1016 // call listeners 1017 if (contentChanged.assigned) 1018 contentChanged(this, op, rangeBefore, rangeAfter, source); 1019 } 1020 1021 /// return edit marks for specified range 1022 EditStateMark[] rangeMarks(TextRange range) { 1023 EditStateMark[] res; 1024 if (range.empty) { 1025 res ~= EditStateMark.unchanged; 1026 return res; 1027 } 1028 for (int lineIndex = range.start.line; lineIndex <= range.end.line; lineIndex++) { 1029 res ~= _editMarks[lineIndex]; 1030 } 1031 return res; 1032 } 1033 1034 /// return text for specified range 1035 dstring[] rangeText(TextRange range) { 1036 dstring[] res; 1037 if (range.empty) { 1038 res ~= ""d; 1039 return res; 1040 } 1041 for (int lineIndex = range.start.line; lineIndex <= range.end.line; lineIndex++) { 1042 dstring lineText = line(lineIndex); 1043 dstring lineFragment = lineText; 1044 int startchar = (lineIndex == range.start.line) ? range.start.pos : 0; 1045 int endchar = (lineIndex == range.end.line) ? range.end.pos : cast(int)lineText.length; 1046 if (endchar > lineText.length) 1047 endchar = cast(int)lineText.length; 1048 if (endchar <= startchar) 1049 lineFragment = ""d; 1050 else if (startchar != 0 || endchar != lineText.length) 1051 lineFragment = lineText[startchar .. endchar].dup; 1052 res ~= lineFragment; 1053 } 1054 return res; 1055 } 1056 1057 /// when position is out of content bounds, fix it to nearest valid position 1058 void correctPosition(ref TextPosition position) { 1059 if (position.line >= length) { 1060 position.line = length - 1; 1061 position.pos = lineLength(position.line); 1062 } 1063 if (position.line < 0) { 1064 position.line = 0; 1065 position.pos = 0; 1066 } 1067 int currentLineLength = lineLength(position.line); 1068 if (position.pos > currentLineLength) 1069 position.pos = currentLineLength; 1070 if (position.pos < 0) 1071 position.pos = 0; 1072 } 1073 1074 /// when range positions is out of content bounds, fix it to nearest valid position 1075 void correctRange(ref TextRange range) { 1076 correctPosition(range.start); 1077 correctPosition(range.end); 1078 } 1079 1080 /// removes removedCount lines starting from start 1081 protected void removeLines(int start, int removedCount) { 1082 int end = start + removedCount; 1083 assert(removedCount > 0 && start >= 0 && end > 0 && start < _lines.length && end <= _lines.length); 1084 for (int i = start; i < _lines.length - removedCount; i++) { 1085 _lines[i] = _lines[i + removedCount]; 1086 _tokenProps[i] = _tokenProps[i + removedCount]; 1087 _editMarks[i] = _editMarks[i + removedCount]; 1088 } 1089 for (int i = cast(int)_lines.length - removedCount; i < _lines.length; i++) { 1090 _lines[i] = null; // free unused line references 1091 _tokenProps[i] = null; // free unused line references 1092 _editMarks[i] = EditStateMark.unchanged; // free unused line references 1093 } 1094 _lines.length -= removedCount; 1095 _tokenProps.length = _lines.length; 1096 _editMarks.length = _lines.length; 1097 } 1098 1099 /// inserts count empty lines at specified position 1100 protected void insertLines(int start, int count) 1101 in { assert(count > 0); } 1102 body { 1103 _lines.length += count; 1104 _tokenProps.length = _lines.length; 1105 _editMarks.length = _lines.length; 1106 for (int i = cast(int)_lines.length - 1; i >= start + count; i--) { 1107 _lines[i] = _lines[i - count]; 1108 _tokenProps[i] = _tokenProps[i - count]; 1109 _editMarks[i] = _editMarks[i - count]; 1110 } 1111 foreach(i; start .. start + count) { 1112 _lines[i] = ""d; 1113 _tokenProps[i] = null; 1114 _editMarks[i] = EditStateMark.changed; 1115 } 1116 } 1117 1118 /// inserts or removes lines, removes text in range 1119 protected void replaceRange(TextRange before, TextRange after, dstring[] newContent, EditStateMark[] marks = null) { 1120 dstring firstLineBefore = line(before.start.line); 1121 dstring lastLineBefore = before.singleLine ? firstLineBefore : line(before.end.line); 1122 dstring firstLineHead = before.start.pos > 0 && before.start.pos <= firstLineBefore.length ? firstLineBefore[0..before.start.pos] : ""d; 1123 dstring lastLineTail = before.end.pos >= 0 && before.end.pos < lastLineBefore.length ? lastLineBefore[before.end.pos .. $] : ""d; 1124 1125 int linesBefore = before.lines; 1126 int linesAfter = after.lines; 1127 if (linesBefore < linesAfter) { 1128 // add more lines 1129 insertLines(before.start.line + 1, linesAfter - linesBefore); 1130 } else if (linesBefore > linesAfter) { 1131 // remove extra lines 1132 removeLines(before.start.line + 1, linesBefore - linesAfter); 1133 } 1134 foreach(int i; after.start.line .. after.end.line + 1) { 1135 if (marks) { 1136 //if (i - after.start.line < marks.length) 1137 _editMarks[i] = marks[i - after.start.line]; 1138 } 1139 dstring newline = newContent[i - after.start.line]; 1140 if (i == after.start.line && i == after.end.line) { 1141 dchar[] buf; 1142 buf ~= firstLineHead; 1143 buf ~= newline; 1144 buf ~= lastLineTail; 1145 //Log.d("merging lines ", firstLineHead, " ", newline, " ", lastLineTail); 1146 _lines[i] = cast(dstring)buf; 1147 clearTokenProps(i, i + 1); 1148 if (!marks) 1149 markChangedLines(i, i + 1); 1150 //Log.d("merge result: ", _lines[i]); 1151 } else if (i == after.start.line) { 1152 dchar[] buf; 1153 buf ~= firstLineHead; 1154 buf ~= newline; 1155 _lines[i] = cast(dstring)buf; 1156 clearTokenProps(i, i + 1); 1157 if (!marks) 1158 markChangedLines(i, i + 1); 1159 } else if (i == after.end.line) { 1160 dchar[] buf; 1161 buf ~= newline; 1162 buf ~= lastLineTail; 1163 _lines[i] = cast(dstring)buf; 1164 clearTokenProps(i, i + 1); 1165 if (!marks) 1166 markChangedLines(i, i + 1); 1167 } else { 1168 _lines[i] = newline; // no dup needed 1169 clearTokenProps(i, i + 1); 1170 if (!marks) 1171 markChangedLines(i, i + 1); 1172 } 1173 } 1174 } 1175 1176 1177 static alias isDigit = std.uni.isNumber; 1178 static bool isAlpha(dchar ch) pure nothrow { 1179 static import std.uni; 1180 return std.uni.isAlpha(ch) || ch == '_'; 1181 } 1182 static bool isAlNum(dchar ch) pure nothrow { 1183 static import std.uni; 1184 return isDigit(ch) || isAlpha(ch); 1185 } 1186 static bool isLowerAlpha(dchar ch) pure nothrow { 1187 static import std.uni; 1188 return std.uni.isLower(ch) || ch == '_'; 1189 } 1190 static alias isUpperAlpha = std.uni.isUpper; 1191 static bool isPunct(dchar ch) pure nothrow { 1192 switch(ch) { 1193 case '.': 1194 case ',': 1195 case ';': 1196 case '?': 1197 case '!': 1198 return true; 1199 default: 1200 return false; 1201 } 1202 } 1203 static bool isBracket(dchar ch) pure nothrow { 1204 switch(ch) { 1205 case '(': 1206 case ')': 1207 case '[': 1208 case ']': 1209 case '{': 1210 case '}': 1211 return true; 1212 default: 1213 return false; 1214 } 1215 } 1216 1217 static bool isWordBound(dchar thischar, dchar nextchar) { 1218 return (isAlNum(thischar) && !isAlNum(nextchar)) 1219 || (isPunct(thischar) && !isPunct(nextchar)) 1220 || (isBracket(thischar) && !isBracket(nextchar)) 1221 || (thischar != ' ' && nextchar == ' '); 1222 } 1223 1224 /// change text position to nearest word bound (direction < 0 - back, > 0 - forward) 1225 TextPosition moveByWord(TextPosition p, int direction, bool camelCasePartsAsWords) { 1226 correctPosition(p); 1227 TextPosition firstns = firstNonSpace(p.line); // before first non space 1228 TextPosition lastns = lastNonSpace(p.line); // after last non space 1229 int linelen = lineLength(p.line); // line length 1230 if (direction < 0) { 1231 // back 1232 if (p.pos <= 0) { 1233 // beginning of line - move to prev line 1234 if (p.line > 0) 1235 p = lastNonSpace(p.line - 1); 1236 } else if (p.pos <= firstns.pos) { // before first nonspace 1237 // to beginning of line 1238 p.pos = 0; 1239 } else { 1240 dstring txt = line(p.line); 1241 int found = -1; 1242 for (int i = p.pos - 1; i > 0; i--) { 1243 // check if position i + 1 is after word end 1244 dchar thischar = i >= 0 && i < linelen ? txt[i] : ' '; 1245 if (thischar == '\t') 1246 thischar = ' '; 1247 dchar nextchar = i - 1 >= 0 && i - 1 < linelen ? txt[i - 1] : ' '; 1248 if (nextchar == '\t') 1249 nextchar = ' '; 1250 if (isWordBound(thischar, nextchar) 1251 || (camelCasePartsAsWords && isUpperAlpha(thischar) && isLowerAlpha(nextchar))) { 1252 found = i; 1253 break; 1254 } 1255 } 1256 if (found >= 0) 1257 p.pos = found; 1258 else 1259 p.pos = 0; 1260 } 1261 } else if (direction > 0) { 1262 // forward 1263 if (p.pos >= linelen) { 1264 // last position of line 1265 if (p.line < length - 1) 1266 p = firstNonSpace(p.line + 1); 1267 } else if (p.pos >= lastns.pos) { // before first nonspace 1268 // to beginning of line 1269 p.pos = linelen; 1270 } else { 1271 dstring txt = line(p.line); 1272 int found = -1; 1273 for (int i = p.pos; i < linelen; i++) { 1274 // check if position i + 1 is after word end 1275 dchar thischar = txt[i]; 1276 if (thischar == '\t') 1277 thischar = ' '; 1278 dchar nextchar = i < linelen - 1 ? txt[i + 1] : ' '; 1279 if (nextchar == '\t') 1280 nextchar = ' '; 1281 if (isWordBound(thischar, nextchar) 1282 || (camelCasePartsAsWords && isLowerAlpha(thischar) && isUpperAlpha(nextchar))) { 1283 found = i + 1; 1284 break; 1285 } 1286 } 1287 if (found >= 0) 1288 p.pos = found; 1289 else 1290 p.pos = linelen; 1291 } 1292 } 1293 return p; 1294 } 1295 1296 /// edit content 1297 bool performOperation(EditOperation op, Object source) { 1298 if (_readOnly) 1299 throw new Exception("content is readonly"); 1300 if (op.action == EditAction.Replace) { 1301 TextRange rangeBefore = op.range; 1302 assert(rangeBefore.start <= rangeBefore.end); 1303 //correctRange(rangeBefore); 1304 dstring[] oldcontent = rangeText(rangeBefore); 1305 EditStateMark[] oldmarks = rangeMarks(rangeBefore); 1306 dstring[] newcontent = op.content; 1307 if (newcontent.length == 0) 1308 newcontent ~= ""d; 1309 TextRange rangeAfter = op.range; 1310 rangeAfter.end = rangeAfter.start; 1311 if (newcontent.length > 1) { 1312 // different lines 1313 rangeAfter.end.line = rangeAfter.start.line + cast(int)newcontent.length - 1; 1314 rangeAfter.end.pos = cast(int)newcontent[$ - 1].length; 1315 } else { 1316 // same line 1317 rangeAfter.end.pos = rangeAfter.start.pos + cast(int)newcontent[0].length; 1318 } 1319 assert(rangeAfter.start <= rangeAfter.end); 1320 op.newRange = rangeAfter; 1321 op.oldContent = oldcontent; 1322 op.oldEditMarks = oldmarks; 1323 replaceRange(rangeBefore, rangeAfter, newcontent); 1324 _undoBuffer.saveForUndo(op); 1325 handleContentChange(op, rangeBefore, rangeAfter, source); 1326 return true; 1327 } 1328 return false; 1329 } 1330 1331 /// return true if there is at least one operation in undo buffer 1332 @property bool hasUndo() { 1333 return _undoBuffer.hasUndo; 1334 } 1335 /// return true if there is at least one operation in redo buffer 1336 @property bool hasRedo() { 1337 return _undoBuffer.hasRedo; 1338 } 1339 /// undoes last change 1340 bool undo(Object source) { 1341 if (!hasUndo) 1342 return false; 1343 if (_readOnly) 1344 throw new Exception("content is readonly"); 1345 EditOperation op = _undoBuffer.undo(); 1346 TextRange rangeBefore = op.newRange; 1347 dstring[] oldcontent = op.content; 1348 dstring[] newcontent = op.oldContent; 1349 EditStateMark[] newmarks = op.oldEditMarks; //_undoBuffer.savedInUndo() ? : null; 1350 TextRange rangeAfter = op.range; 1351 //Log.d("Undoing op rangeBefore=", rangeBefore, " contentBefore=`", oldcontent, "` rangeAfter=", rangeAfter, " contentAfter=`", newcontent, "`"); 1352 replaceRange(rangeBefore, rangeAfter, newcontent, newmarks); 1353 handleContentChange(op, rangeBefore, rangeAfter, source ? source : this); 1354 return true; 1355 } 1356 1357 /// redoes last undone change 1358 bool redo(Object source) { 1359 if (!hasRedo) 1360 return false; 1361 if (_readOnly) 1362 throw new Exception("content is readonly"); 1363 EditOperation op = _undoBuffer.redo(); 1364 TextRange rangeBefore = op.range; 1365 dstring[] oldcontent = op.oldContent; 1366 dstring[] newcontent = op.content; 1367 TextRange rangeAfter = op.newRange; 1368 //Log.d("Redoing op rangeBefore=", rangeBefore, " contentBefore=`", oldcontent, "` rangeAfter=", rangeAfter, " contentAfter=`", newcontent, "`"); 1369 replaceRange(rangeBefore, rangeAfter, newcontent); 1370 handleContentChange(op, rangeBefore, rangeAfter, source ? source : this); 1371 return true; 1372 } 1373 /// clear undo/redp history 1374 void clearUndo() { 1375 _undoBuffer.clear(); 1376 } 1377 1378 protected string _filename; 1379 protected TextFileFormat _format; 1380 1381 /// file used to load editor content 1382 @property string filename() { 1383 return _filename; 1384 } 1385 1386 1387 /// load content form input stream 1388 bool load(InputStream f, string fname = null) { 1389 import dlangui.core.linestream; 1390 clear(); 1391 _filename = fname; 1392 _format = TextFileFormat.init; 1393 try { 1394 LineStream lines = LineStream.create(f, fname); 1395 for (;;) { 1396 dchar[] s = lines.readLine(); 1397 if (s is null) 1398 break; 1399 int pos = cast(int)(_lines.length++); 1400 _tokenProps.length = _lines.length; 1401 _lines[pos] = s.dup; 1402 clearTokenProps(pos, pos + 1); 1403 } 1404 if (lines.errorCode != 0) { 1405 clear(); 1406 Log.e("Error ", lines.errorCode, " ", lines.errorMessage, " -- at line ", lines.errorLine, " position ", lines.errorPos); 1407 notifyContentReplaced(); 1408 return false; 1409 } 1410 // EOF 1411 _format = lines.textFormat; 1412 _undoBuffer.clear(); 1413 debug(FileFormats)Log.d("loaded file:", filename, " format detected:", _format); 1414 notifyContentReplaced(); 1415 return true; 1416 } catch (Exception e) { 1417 Log.e("Exception while trying to read file ", fname, " ", e.toString); 1418 clear(); 1419 notifyContentReplaced(); 1420 return false; 1421 } 1422 } 1423 /// load content from file 1424 bool load(string filename) { 1425 clear(); 1426 try { 1427 InputStream f; 1428 f = new FileInputStream(filename); 1429 scope(exit) { f.close(); } 1430 bool res = load(f, filename); 1431 return res; 1432 } catch (Exception e) { 1433 Log.e("Exception while trying to read file ", filename, " ", e.toString); 1434 clear(); 1435 return false; 1436 } 1437 } 1438 /// save to output stream in specified format 1439 bool save(OutputStream stream, string filename, TextFileFormat format) { 1440 if (!filename) 1441 filename = _filename; 1442 _format = format; 1443 import dlangui.core.linestream; 1444 try { 1445 debug(FileFormats)Log.d("creating output stream, file=", filename, " format=", format); 1446 OutputLineStream writer = new OutputLineStream(stream, filename, format); 1447 scope(exit) { writer.close(); } 1448 for (int i = 0; i < _lines.length; i++) { 1449 writer.writeLine(_lines[i]); 1450 } 1451 _undoBuffer.saved(); 1452 notifyContentSaved(); 1453 return true; 1454 } catch (Exception e) { 1455 Log.e("Exception while trying to write file ", filename, " ", e.toString); 1456 return false; 1457 } 1458 } 1459 /// save to output stream in current format 1460 bool save(OutputStream stream, string filename) { 1461 return save(stream, filename, _format); 1462 } 1463 /// save to file in specified format 1464 bool save(string filename, TextFileFormat format) { 1465 if (!filename) 1466 filename = _filename; 1467 try { 1468 OutputStream f = new FileOutputStream(filename); 1469 scope(exit) { f.close(); } 1470 return save(f, filename, format); 1471 } catch (Exception e) { 1472 Log.e("Exception while trying to save file ", filename, " ", e.toString); 1473 return false; 1474 } 1475 } 1476 /// save to file in current format 1477 bool save(string filename = null) { 1478 return save(filename, _format); 1479 } 1480 } 1481 1482 /// types of text editor line icon marks (bookmark / breakpoint / error / ...) 1483 enum LineIconType : int { 1484 /// bookmark 1485 bookmark, 1486 /// breakpoint mark 1487 breakpoint, 1488 /// error mark 1489 error, 1490 } 1491 1492 /// text editor line icon 1493 class LineIcon { 1494 /// mark type 1495 LineIconType type; 1496 /// line number 1497 int line; 1498 /// arbitrary parameter 1499 Object objectParam; 1500 /// empty 1501 this() { 1502 } 1503 this(LineIconType type, int line, Object obj = null) { 1504 this.type = type; 1505 this.line = line; 1506 this.objectParam = obj; 1507 } 1508 } 1509 1510 /// text editor line icon list 1511 struct LineIcons { 1512 private LineIcon[] _items; 1513 private int _len; 1514 1515 /// returns count of items 1516 @property int length() { return _len; } 1517 /// returns item by index, or null if index out of bounds 1518 LineIcon opIndex(int index) { 1519 if (index < 0 || index >= _len) 1520 return null; 1521 return _items[index]; 1522 } 1523 private void insert(LineIcon icon, int index) { 1524 if (index < 0) 1525 index = 0; 1526 if (index > _len) 1527 index = _len; 1528 if (_items.length <= index) 1529 _items.length = index + 16; 1530 if (index < _len) { 1531 for (size_t i = _len; i > index; i--) 1532 _items[i] = _items[i - 1]; 1533 } 1534 _items[index] = icon; 1535 _len++; 1536 } 1537 private int findSortedIndex(int line, LineIconType type) { 1538 // TODO: use binary search 1539 for (int i = 0; i < _len; i++) { 1540 if (_items[i].line > line || _items[i].type > type) { 1541 return i; 1542 } 1543 } 1544 return _len; 1545 } 1546 /// add icon mark 1547 void add(LineIcon icon) { 1548 int index = findSortedIndex(icon.line, icon.type); 1549 insert(icon, index); 1550 } 1551 /// add all icons from list 1552 void addAll(LineIcon[] list) { 1553 foreach(item; list) 1554 add(item); 1555 } 1556 /// remove icon mark by index 1557 LineIcon remove(int index) { 1558 if (index < 0 || index >= _len) 1559 return null; 1560 LineIcon res = _items[index]; 1561 for (int i = index; i < _len - 1; i++) 1562 _items[i] = _items[i + 1]; 1563 _items[_len] = null; 1564 _len--; 1565 return res; 1566 } 1567 1568 /// remove icon mark 1569 LineIcon remove(LineIcon icon) { 1570 // same object 1571 for (int i = 0; i < _len; i++) { 1572 if (_items[i] is icon) 1573 return remove(i); 1574 } 1575 // has the same objectParam 1576 for (int i = 0; i < _len; i++) { 1577 if (_items[i].objectParam !is null && icon.objectParam !is null && _items[i].objectParam is icon.objectParam) 1578 return remove(i); 1579 } 1580 // has same line and type 1581 for (int i = 0; i < _len; i++) { 1582 if (_items[i].line == icon.line && _items[i].type == icon.type) 1583 return remove(i); 1584 } 1585 return null; 1586 } 1587 1588 /// remove all icon marks of specified type, return true if any of items removed 1589 bool removeByType(LineIconType type) { 1590 bool res = false; 1591 for (int i = _len - 1; i >= 0; i--) { 1592 if (_items[i].type == type) { 1593 remove(i); 1594 res = true; 1595 } 1596 } 1597 return res; 1598 } 1599 /// get array of icons of specified type 1600 LineIcon[] findByType(LineIconType type) { 1601 LineIcon[] res; 1602 for (int i = 0; i < _len; i++) { 1603 if (_items[i].type == type) 1604 res ~= _items[i]; 1605 } 1606 return res; 1607 } 1608 /// get array of icons of specified type 1609 LineIcon findByLineAndType(int line, LineIconType type) { 1610 for (int i = 0; i < _len; i++) { 1611 if (_items[i].type == type && _items[i].line == line) 1612 return _items[i]; 1613 } 1614 return null; 1615 } 1616 /// update mark position lines after text change, returns true if any of marks were moved or removed 1617 bool updateLinePositions(TextRange rangeBefore, TextRange rangeAfter, ref LineIcon[] moved, ref LineIcon[] removed) { 1618 moved = null; 1619 removed = null; 1620 bool res = false; 1621 for (int i = _len - 1; i >= 0; i--) { 1622 LineIcon item = _items[i]; 1623 if (rangeBefore.start.line > item.line && rangeAfter.start.line > item.line) 1624 continue; // line is before ranges 1625 else if (rangeBefore.start.line < item.line || rangeAfter.start.line < item.line) { 1626 // line is fully after change 1627 int deltaLines = rangeAfter.end.line - rangeBefore.end.line; 1628 if (!deltaLines) 1629 continue; 1630 if (deltaLines < 0 && rangeBefore.end.line >= item.line && rangeAfter.end.line < item.line) { 1631 // remove 1632 removed ~= item; 1633 remove(i); 1634 res = true; 1635 } else { 1636 // move 1637 item.line += deltaLines; 1638 moved ~= item; 1639 res = true; 1640 } 1641 } 1642 } 1643 return res; 1644 } 1645 1646 LineIcon findNext(LineIconType type, int line, int direction) { 1647 LineIcon firstBefore; 1648 LineIcon firstAfter; 1649 if (direction < 0) { 1650 // backward 1651 for (int i = _len - 1; i >= 0; i--) { 1652 LineIcon item = _items[i]; 1653 if (item.type != type) 1654 continue; 1655 if (!firstBefore && item.line >= line) 1656 firstBefore = item; 1657 else if (!firstAfter && item.line < line) 1658 firstAfter = item; 1659 } 1660 } else { 1661 // forward 1662 for (int i = 0; i < _len; i++) { 1663 LineIcon item = _items[i]; 1664 if (item.type != type) 1665 continue; 1666 if (!firstBefore && item.line <= line) 1667 firstBefore = item; 1668 else if (!firstAfter && item.line > line) 1669 firstAfter = item; 1670 } 1671 } 1672 if (firstAfter) 1673 return firstAfter; 1674 return firstBefore; 1675 } 1676 1677 @property bool hasBookmarks() { 1678 for (int i = 0; i < _len; i++) { 1679 if (_items[i].type == LineIconType.bookmark) 1680 return true; 1681 } 1682 return false; 1683 } 1684 1685 void toggleBookmark(int line) { 1686 LineIcon existing = findByLineAndType(line, LineIconType.bookmark); 1687 if (existing) 1688 remove(existing); 1689 else 1690 add(new LineIcon(LineIconType.bookmark, line)); 1691 } 1692 } 1693