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