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