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