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