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