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