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