diff --git a/dlanguilib.visualdproj b/dlanguilib.visualdproj
index 128d1fb1..9cc07065 100644
--- a/dlanguilib.visualdproj
+++ b/dlanguilib.visualdproj
@@ -89,7 +89,6 @@
$(OutDir)\$(ProjectName).lib
1
- 2
@@ -184,7 +183,6 @@
$(OutDir)\$(ProjectName).lib
1
- 1
@@ -337,6 +335,7 @@
+
diff --git a/src/dlangui/core/editable.d b/src/dlangui/core/editable.d
new file mode 100644
index 00000000..a5f6fb29
--- /dev/null
+++ b/src/dlangui/core/editable.d
@@ -0,0 +1,978 @@
+// Written in the D programming language.
+
+/**
+This module contains implementation of editable text content.
+
+
+Synopsis:
+
+----
+import dlangui.core.editable;
+
+----
+
+Copyright: Vadim Lopatin, 2014
+License: Boost License 1.0
+Authors: Vadim Lopatin, coolreader.org@gmail.com
+*/
+module dlangui.core.editable;
+
+import dlangui.core.logger;
+import dlangui.core.signals;
+import dlangui.core.collections;
+import dlangui.core.linestream;
+import std.algorithm;
+import std.stream;
+
+
+immutable dchar EOL = '\n';
+
+const ubyte TOKEN_CATEGORY_SHIFT = 4;
+const ubyte TOKEN_CATEGORY_MASK = 0xF0; // token category 0..15
+const ubyte TOKEN_SUBCATEGORY_MASK = 0x0F; // token subcategory 0..15
+const ubyte TOKEN_UNKNOWN = 0;
+
+/*
+Bit mask:
+7654 3210
+cccc ssss
+| |
+| \ ssss = token subcategory
+|
+\ cccc = token category
+
+*/
+/// token category for syntax highlight
+enum TokenCategory : ubyte {
+ WhiteSpace = (0 << TOKEN_CATEGORY_SHIFT),
+ WhiteSpace_Space = (0 << TOKEN_CATEGORY_SHIFT) | 1,
+ WhiteSpace_Tab = (0 << TOKEN_CATEGORY_SHIFT) | 2,
+
+ Comment = (1 << TOKEN_CATEGORY_SHIFT),
+ Comment_SingleLine = (1 << TOKEN_CATEGORY_SHIFT) | 1, // single line comment
+ Comment_SingleLineDoc = (1 << TOKEN_CATEGORY_SHIFT) | 2,// documentation in single line comment
+ Comment_MultyLine = (1 << TOKEN_CATEGORY_SHIFT) | 3, // multiline coment
+ Comment_MultyLineDoc = (1 << TOKEN_CATEGORY_SHIFT) | 4, // documentation in multiline comment
+ Comment_Documentation = (1 << TOKEN_CATEGORY_SHIFT) | 5,// documentation comment
+
+ Identifier = (2 << TOKEN_CATEGORY_SHIFT), // identifier (exact subcategory is unknown)
+ Identifier_Class = (2 << TOKEN_CATEGORY_SHIFT) | 1, // class name
+ Identifier_Struct = (2 << TOKEN_CATEGORY_SHIFT) | 2, // struct name
+ Identifier_Local = (2 << TOKEN_CATEGORY_SHIFT) | 3, // local variable
+ Identifier_Member = (2 << TOKEN_CATEGORY_SHIFT) | 4, // struct or class member
+ Identifier_Deprecated = (2 << TOKEN_CATEGORY_SHIFT) | 15, // usage of this identifier is deprecated
+ /// string literal
+ String = (3 << TOKEN_CATEGORY_SHIFT),
+ /// character literal
+ Character = (4 << TOKEN_CATEGORY_SHIFT),
+ /// integer literal
+ Integer = (5 << TOKEN_CATEGORY_SHIFT),
+ /// floating point number literal
+ Float = (6 << TOKEN_CATEGORY_SHIFT),
+ /// keyword
+ Keyword = (7 << TOKEN_CATEGORY_SHIFT),
+ /// operator
+ Op = (8 << TOKEN_CATEGORY_SHIFT),
+ // add more here
+ //....
+ /// error - unparsed character sequence
+ Error = (15 << TOKEN_CATEGORY_SHIFT),
+ /// invalid token - generic
+ Error_InvalidToken = (15 << TOKEN_CATEGORY_SHIFT) | 1,
+ /// invalid number token - error occured while parsing number
+ Error_InvalidNumber = (15 << TOKEN_CATEGORY_SHIFT) | 2,
+ /// invalid string token - error occured while parsing string
+ Error_InvalidString = (15 << TOKEN_CATEGORY_SHIFT) | 3,
+ /// invalid identifier token - error occured while parsing identifier
+ Error_InvalidIdentifier = (15 << TOKEN_CATEGORY_SHIFT) | 4,
+ /// invalid comment token - error occured while parsing comment
+ Error_InvalidComment = (15 << TOKEN_CATEGORY_SHIFT) | 4,
+}
+
+
+/// split dstring by delimiters
+dstring[] splitDString(dstring source, dchar delimiter = EOL) {
+ int start = 0;
+ dstring[] res;
+ dchar lastchar;
+ for (int i = 0; i <= source.length; i++) {
+ if (i == source.length || source[i] == delimiter) {
+ if (i >= start) {
+ dchar prevchar = i > 1 && i > start + 1 ? source[i - 1] : 0;
+ int end = i;
+ if (delimiter == EOL && prevchar == '\r') // windows CR/LF
+ end--;
+ dstring line = i > start ? cast(dstring)(source[start .. end].dup) : ""d;
+ res ~= line;
+ }
+ start = i + 1;
+ }
+ }
+ return res;
+}
+
+version (Windows) {
+ immutable dstring SYSTEM_DEFAULT_EOL = "\r\n";
+} else {
+ immutable dstring SYSTEM_DEFAULT_EOL = "\n";
+}
+
+/// concat strings from array using delimiter
+dstring concatDStrings(dstring[] lines, dstring delimiter = SYSTEM_DEFAULT_EOL) {
+ dchar[] buf;
+ foreach(line; lines) {
+ if (buf.length)
+ buf ~= delimiter;
+ buf ~= line;
+ }
+ return cast(dstring)buf;
+}
+
+/// replace end of lines with spaces
+dstring replaceEolsWithSpaces(dstring source) {
+ dchar[] buf;
+ dchar lastch;
+ foreach(ch; source) {
+ if (ch == '\r') {
+ buf ~= ' ';
+ } else if (ch == '\n') {
+ if (lastch != '\r')
+ buf ~= ' ';
+ } else {
+ buf ~= ch;
+ }
+ lastch = ch;
+ }
+ return cast(dstring)buf;
+}
+
+/// text content position
+struct TextPosition {
+ /// line number, zero based
+ int line;
+ /// character position in line (0 == before first character)
+ int pos;
+ /// compares two positions
+ int opCmp(ref const TextPosition v) const {
+ if (line < v.line)
+ return -1;
+ if (line > v.line)
+ return 1;
+ if (pos < v.pos)
+ return -1;
+ if (pos > v.pos)
+ return 1;
+ return 0;
+ }
+}
+
+/// text content range
+struct TextRange {
+ TextPosition start;
+ TextPosition end;
+ /// returns true if range is empty
+ @property bool empty() const {
+ return end <= start;
+ }
+ /// returns true if start and end located at the same line
+ @property bool singleLine() const {
+ return end.line == start.line;
+ }
+ /// returns count of lines in range
+ @property int lines() const {
+ return end.line - start.line + 1;
+ }
+}
+
+/// action performed with editable contents
+enum EditAction {
+ /// insert content into specified position (range.start)
+ //Insert,
+ /// delete content in range
+ //Delete,
+ /// replace range content with new content
+ Replace,
+
+ /// replace whole content
+ ReplaceContent,
+ /// saved content
+ SaveContent,
+}
+
+/// edit operation details for EditableContent
+class EditOperation {
+ protected EditAction _action;
+ /// action performed
+ @property EditAction action() { return _action; }
+ protected TextRange _range;
+
+ /// source range to replace with new content
+ @property ref TextRange range() { return _range; }
+ protected TextRange _newRange;
+
+ /// new range after operation applied
+ @property ref TextRange newRange() { return _newRange; }
+ @property void newRange(TextRange range) { _newRange = range; }
+
+ /// new content for range (if required for this action)
+ protected dstring[] _content;
+ @property ref dstring[] content() { return _content; }
+
+ /// old content for range
+ protected dstring[] _oldContent;
+ @property ref dstring[] oldContent() { return _oldContent; }
+ @property void oldContent(dstring[] content) { _oldContent = content; }
+
+ this(EditAction action) {
+ _action = action;
+ }
+ this(EditAction action, TextPosition pos, dstring text) {
+ this(action, TextRange(pos, pos), text);
+ }
+ this(EditAction action, TextRange range, dstring text) {
+ _action = action;
+ _range = range;
+ _content.length = 1;
+ _content[0] = text.dup;
+ }
+ this(EditAction action, TextRange range, dstring[] text) {
+ _action = action;
+ _range = range;
+ _content.length = text.length;
+ for(int i = 0; i < text.length; i++)
+ _content[i] = text[i].dup;
+ //_content = text;
+ }
+ /// try to merge two operations (simple entering of characters in the same line), return true if succeded
+ bool merge(EditOperation op) {
+ if (_range.start.line != op._range.start.line) // both ops whould be on the same line
+ return false;
+ if (_content.length != 1 || op._content.length != 1) // both ops should operate the same line
+ return false;
+ // appending of single character
+ if (_range.empty && op._range.empty && op._content[0].length == 1 && _newRange.end.pos == op._range.start.pos) {
+ _content[0] ~= op._content[0];
+ _newRange.end.pos++;
+ return true;
+ }
+ // removing single character
+ if (_newRange.empty && op._newRange.empty && op._oldContent[0].length == 1) {
+ if (_newRange.end.pos == op.range.end.pos) {
+ // removed char before
+ _range.start.pos--;
+ _newRange.start.pos--;
+ _newRange.end.pos--;
+ _oldContent[0] = (op._oldContent[0].dup ~ _oldContent[0].dup).dup;
+ return true;
+ } else if (_newRange.end.pos == op._range.start.pos) {
+ // removed char after
+ _range.end.pos++;
+ _oldContent[0] = (_oldContent[0].dup ~ op._oldContent[0].dup).dup;
+ return true;
+ }
+ }
+ return false;
+ }
+}
+
+/// Undo/Redo buffer
+class UndoBuffer {
+ protected Collection!EditOperation _undoList;
+ protected Collection!EditOperation _redoList;
+
+ /// returns true if buffer contains any undo items
+ @property bool hasUndo() {
+ return !_undoList.empty;
+ }
+
+ /// returns true if buffer contains any redo items
+ @property bool hasRedo() {
+ return !_redoList.empty;
+ }
+
+ /// adds undo operation
+ void saveForUndo(EditOperation op) {
+ _redoList.clear();
+ if (!_undoList.empty) {
+ if (_undoList.back.merge(op)) {
+ return; // merged - no need to add new operation
+ }
+ }
+ _undoList.pushBack(op);
+ }
+
+ /// returns operation to be undone (put it to redo), null if no undo ops available
+ EditOperation undo() {
+ if (!hasUndo)
+ return null; // no undo operations
+ EditOperation res = _undoList.popBack();
+ _redoList.pushBack(res);
+ return res;
+ }
+
+ /// returns operation to be redone (put it to undo), null if no undo ops available
+ EditOperation redo() {
+ if (!hasRedo)
+ return null; // no undo operations
+ EditOperation res = _redoList.popBack();
+ _undoList.pushBack(res);
+ return res;
+ }
+
+ /// clears both undo and redo buffers
+ void clear() {
+ _undoList.clear();
+ _redoList.clear();
+ _savedState = null;
+ }
+
+ protected EditOperation _savedState;
+
+ /// current state is saved
+ void saved() {
+ _savedState = _undoList.peekBack;
+ }
+ /// returns true if content has been changed since last saved() or clear() call
+ @property bool modified() {
+ return _savedState !is _undoList.peekBack;
+ }
+}
+
+/// Editable Content change listener
+interface EditableContentListener {
+ void onContentChange(EditableContent content, EditOperation operation, ref TextRange rangeBefore, ref TextRange rangeAfter, Object source);
+}
+
+alias TokenPropString = ubyte[];
+
+/// interface for custom syntax highlight
+interface SyntaxHighlighter {
+ /// categorize characters in content by token types
+ void updateHighlight(dstring[] lines, TokenPropString[] props, int changeStartLine, int changeEndLine);
+}
+
+/// editable plain text (singleline/multiline)
+class EditableContent {
+
+ this(bool multiline) {
+ _multiline = multiline;
+ _lines.length = 1; // initial state: single empty line
+ _undoBuffer = new UndoBuffer();
+ }
+
+ @property bool modified() {
+ return _undoBuffer.modified;
+ }
+
+ protected UndoBuffer _undoBuffer;
+
+ protected SyntaxHighlighter _syntaxHighlighter;
+
+ @property SyntaxHighlighter syntaxHighlighter() {
+ return _syntaxHighlighter;
+ }
+
+ @property EditableContent syntaxHighlighter(SyntaxHighlighter syntaxHighlighter) {
+ _syntaxHighlighter = syntaxHighlighter;
+ updateTokenProps(0, cast(int)_lines.length);
+ return this;
+ }
+
+ /// returns true if content has syntax highlight handler set
+ @property bool hasSyntaxHighlight() {
+ return _syntaxHighlighter !is null;
+ }
+
+ protected bool _readOnly;
+
+ @property bool readOnly() {
+ return _readOnly;
+ }
+
+ @property void readOnly(bool readOnly) {
+ _readOnly = readOnly;
+ }
+
+ /// listeners for edit operations
+ Signal!EditableContentListener contentChangeListeners;
+
+ protected bool _multiline;
+ /// returns true if miltyline content is supported
+ @property bool multiline() { return _multiline; }
+
+ protected dstring[] _lines;
+ protected TokenPropString[] _tokenProps;
+
+ /// returns all lines concatenated delimited by '\n'
+ @property dstring text() {
+ if (_lines.length == 0)
+ return "";
+ if (_lines.length == 1)
+ return _lines[0];
+ // concat lines
+ dchar[] buf;
+ foreach(item;_lines) {
+ if (buf.length)
+ buf ~= EOL;
+ buf ~= item;
+ }
+ return cast(dstring)buf;
+ }
+
+ /// append one or more lines at end
+ void appendLines(dstring[] lines...) {
+ TextRange rangeBefore;
+ rangeBefore.start = rangeBefore.end = lineEnd(_lines.length ? cast(int)_lines.length - 1 : 0);
+ EditOperation op = new EditOperation(EditAction.Replace, rangeBefore, lines);
+ performOperation(op, this);
+ }
+
+ /// call listener to say that whole content is replaced e.g. by loading from file
+ void notifyContentReplaced() {
+ TextRange rangeBefore;
+ TextRange rangeAfter;
+ // notify about content change
+ handleContentChange(new EditOperation(EditAction.ReplaceContent), rangeBefore, rangeAfter, this);
+ }
+
+ /// call listener to say that content is saved
+ void notifyContentSaved() {
+ TextRange rangeBefore;
+ TextRange rangeAfter;
+ // notify about content change
+ handleContentChange(new EditOperation(EditAction.SaveContent), rangeBefore, rangeAfter, this);
+ }
+
+ protected void updateTokenProps(int startLine, int endLine) {
+ clearTokenProps(startLine, endLine);
+ if (_syntaxHighlighter) {
+ _syntaxHighlighter.updateHighlight(_lines, _tokenProps, startLine, endLine);
+ }
+ }
+
+ /// set props arrays size equal to text line sizes, bit fill with unknown token
+ protected void clearTokenProps(int startLine, int endLine) {
+ for (int i = startLine; i < endLine; i++) {
+ if (hasSyntaxHighlight) {
+ int len = cast(int)_lines[i].length;
+ _tokenProps[i].length = len;
+ for (int j = 0; j < len; j++)
+ _tokenProps[i][j] = TOKEN_UNKNOWN;
+ } else {
+ _tokenProps[i] = null; // no token props
+ }
+ }
+ }
+
+ /// replace whole text with another content
+ @property EditableContent text(dstring newContent) {
+ clearUndo();
+ _lines.length = 0;
+ if (_multiline) {
+ _lines = splitDString(newContent);
+ _tokenProps.length = _lines.length;
+ updateTokenProps(0, cast(int)_lines.length);
+ } else {
+ _lines.length = 1;
+ _lines[0] = replaceEolsWithSpaces(newContent);
+ _tokenProps.length = 1;
+ updateTokenProps(0, cast(int)_lines.length);
+ }
+ notifyContentReplaced();
+ return this;
+ }
+
+ /// clear content
+ void clear() {
+ clearUndo();
+ _lines.length = 0;
+ }
+
+
+ /// returns line text
+ @property int length() { return cast(int)_lines.length; }
+ dstring opIndex(int index) {
+ return line(index);
+ }
+
+ /// returns line text by index, "" if index is out of bounds
+ dstring line(int index) {
+ return index >= 0 && index < _lines.length ? _lines[index] : ""d;
+ }
+
+ /// returns line token properties one item per character
+ TokenPropString lineTokenProps(int index) {
+ return index >= 0 && index < _tokenProps.length ? _tokenProps[index] : null;
+ }
+
+ /// returns text position for end of line lineIndex
+ TextPosition lineEnd(int lineIndex) {
+ return TextPosition(lineIndex, lineLength(lineIndex));
+ }
+
+ /// returns position before first non-space character of line, returns 0 position if no non-space chars
+ TextPosition firstNonSpace(int lineIndex) {
+ dstring s = line(lineIndex);
+ for (int i = 0; i < s.length; i++)
+ if (s[i] != ' ' && s[i] != '\t')
+ return TextPosition(lineIndex, i);
+ return TextPosition(lineIndex, 0);
+ }
+
+ /// returns position after last non-space character of line, returns 0 position if no non-space chars on line
+ TextPosition lastNonSpace(int lineIndex) {
+ dstring s = line(lineIndex);
+ for (int i = cast(int)s.length - 1; i >= 0; i--)
+ if (s[i] != ' ' && s[i] != '\t')
+ return TextPosition(lineIndex, i + 1);
+ return TextPosition(lineIndex, 0);
+ }
+
+ /// returns text position for end of line lineIndex
+ int lineLength(int lineIndex) {
+ return lineIndex >= 0 && lineIndex < _lines.length ? cast(int)_lines[lineIndex].length : 0;
+ }
+
+ /// returns maximum length of line
+ int maxLineLength() {
+ int m = 0;
+ foreach(s; _lines)
+ if (m < s.length)
+ m = cast(int)s.length;
+ return m;
+ }
+
+ void handleContentChange(EditOperation op, ref TextRange rangeBefore, ref TextRange rangeAfter, Object source) {
+ // update highlight if necessary
+ updateTokenProps(rangeAfter.start.line, rangeAfter.end.line + 1);
+ // call listeners
+ if (contentChangeListeners.assigned)
+ contentChangeListeners(this, op, rangeBefore, rangeAfter, source);
+ }
+
+ /// return text for specified range
+ dstring[] rangeText(TextRange range) {
+ dstring[] res;
+ if (range.empty) {
+ res ~= ""d;
+ return res;
+ }
+ for (int lineIndex = range.start.line; lineIndex <= range.end.line; lineIndex++) {
+ dstring lineText = line(lineIndex);
+ dstring lineFragment = lineText;
+ int startchar = 0;
+ int endchar = cast(int)lineText.length;
+ if (lineIndex == range.start.line)
+ startchar = range.start.pos;
+ if (lineIndex == range.end.line)
+ endchar = range.end.pos;
+ if (endchar > lineText.length)
+ endchar = cast(int)lineText.length;
+ if (endchar <= startchar)
+ lineFragment = ""d;
+ else if (startchar != 0 || endchar != lineText.length)
+ lineFragment = lineText[startchar .. endchar].dup;
+ res ~= lineFragment;
+ }
+ return res;
+ }
+
+ /// when position is out of content bounds, fix it to nearest valid position
+ void correctPosition(ref TextPosition position) {
+ if (position.line >= length) {
+ position.line = length - 1;
+ position.pos = lineLength(position.line);
+ }
+ if (position.line < 0) {
+ position.line = 0;
+ position.pos = 0;
+ }
+ int currentLineLength = lineLength(position.line);
+ if (position.pos > currentLineLength)
+ position.pos = currentLineLength;
+ if (position.pos < 0)
+ position.pos = 0;
+ }
+
+ /// when range positions is out of content bounds, fix it to nearest valid position
+ void correctRange(ref TextRange range) {
+ correctPosition(range.start);
+ correctPosition(range.end);
+ }
+
+ /// removes removedCount lines starting from start
+ protected void removeLines(int start, int removedCount) {
+ int end = start + removedCount;
+ assert(removedCount > 0 && start >= 0 && end > 0 && start < _lines.length && end <= _lines.length);
+ for (int i = start; i < _lines.length - removedCount; i++) {
+ _lines[i] = _lines[i + removedCount];
+ _tokenProps[i] = _tokenProps[i + removedCount];
+ }
+ for (int i = cast(int)_lines.length - removedCount; i < _lines.length; i++) {
+ _lines[i] = null; // free unused line references
+ _tokenProps[i] = null; // free unused line references
+ }
+ _lines.length -= removedCount;
+ _tokenProps.length = _lines.length;
+ }
+
+ /// inserts count empty lines at specified position
+ protected void insertLines(int start, int count) {
+ assert(count > 0);
+ _lines.length += count;
+ _tokenProps.length = _lines.length;
+ for (int i = cast(int)_lines.length - 1; i >= start + count; i--) {
+ _lines[i] = _lines[i - count];
+ _tokenProps[i] = _tokenProps[i - count];
+ }
+ for (int i = start; i < start + count; i++) {
+ _lines[i] = ""d;
+ _tokenProps[i] = null;
+ }
+ }
+
+ /// inserts or removes lines, removes text in range
+ protected void replaceRange(TextRange before, TextRange after, dstring[] newContent) {
+ dstring firstLineBefore = line(before.start.line);
+ dstring lastLineBefore = before.singleLine ? firstLineBefore : line(before.end.line);
+ dstring firstLineHead = before.start.pos > 0 && before.start.pos <= firstLineBefore.length ? firstLineBefore[0..before.start.pos] : ""d;
+ dstring lastLineTail = before.end.pos >= 0 && before.end.pos < lastLineBefore.length ? lastLineBefore[before.end.pos .. $] : ""d;
+
+ int linesBefore = before.lines;
+ int linesAfter = after.lines;
+ if (linesBefore < linesAfter) {
+ // add more lines
+ insertLines(before.start.line + 1, linesAfter - linesBefore);
+ } else if (linesBefore > linesAfter) {
+ // remove extra lines
+ removeLines(before.start.line + 1, linesBefore - linesAfter);
+ }
+ for (int i = after.start.line; i <= after.end.line; i++) {
+ dstring newline = newContent[i - after.start.line];
+ if (i == after.start.line && i == after.end.line) {
+ dchar[] buf;
+ buf ~= firstLineHead;
+ buf ~= newline;
+ buf ~= lastLineTail;
+ //Log.d("merging lines ", firstLineHead, " ", newline, " ", lastLineTail);
+ _lines[i] = cast(dstring)buf;
+ clearTokenProps(i, i + 1);
+ //Log.d("merge result: ", _lines[i]);
+ } else if (i == after.start.line) {
+ dchar[] buf;
+ buf ~= firstLineHead;
+ buf ~= newline;
+ _lines[i] = cast(dstring)buf;
+ clearTokenProps(i, i + 1);
+ } else if (i == after.end.line) {
+ dchar[] buf;
+ buf ~= newline;
+ buf ~= lastLineTail;
+ _lines[i] = cast(dstring)buf;
+ clearTokenProps(i, i + 1);
+ } else {
+ _lines[i] = newline; // no dup needed
+ clearTokenProps(i, i + 1);
+ }
+ }
+ }
+
+ static bool isDigit(dchar ch) pure nothrow {
+ return ch >= '0' && ch <= '9';
+ }
+ static bool isAlpha(dchar ch) pure nothrow {
+ return isLowerAlpha(ch) || isUpperAlpha(ch);
+ }
+ static bool isAlNum(dchar ch) pure nothrow {
+ return isDigit(ch) || isAlpha(ch);
+ }
+ static bool isLowerAlpha(dchar ch) pure nothrow {
+ return (ch >= 'a' && ch <= 'z') || (ch == '_');
+ }
+ static bool isUpperAlpha(dchar ch) pure nothrow {
+ return (ch >= 'A' && ch <= 'Z');
+ }
+ static bool isPunct(dchar ch) pure nothrow {
+ switch(ch) {
+ case '.':
+ case ',':
+ case ';':
+ case '?':
+ case '!':
+ return true;
+ default:
+ return false;
+ }
+ }
+ static bool isBracket(dchar ch) pure nothrow {
+ switch(ch) {
+ case '(':
+ case ')':
+ case '[':
+ case ']':
+ case '{':
+ case '}':
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ static bool isWordBound(dchar thischar, dchar nextchar) {
+ return (isAlNum(thischar) && !isAlNum(nextchar))
+ || (isPunct(thischar) && !isPunct(nextchar))
+ || (isBracket(thischar) && !isBracket(nextchar))
+ || (thischar != ' ' && nextchar == ' ');
+ }
+
+ /// change text position to nearest word bound (direction < 0 - back, > 0 - forward)
+ TextPosition moveByWord(TextPosition p, int direction, bool camelCasePartsAsWords) {
+ correctPosition(p);
+ TextPosition firstns = firstNonSpace(p.line); // before first non space
+ TextPosition lastns = lastNonSpace(p.line); // after last non space
+ int linelen = lineLength(p.line); // line length
+ if (direction < 0) {
+ // back
+ if (p.pos <= 0) {
+ // beginning of line - move to prev line
+ if (p.line > 0)
+ p = lastNonSpace(p.line - 1);
+ } else if (p.pos <= firstns.pos) { // before first nonspace
+ // to beginning of line
+ p.pos = 0;
+ } else {
+ dstring txt = line(p.line);
+ int found = -1;
+ for (int i = p.pos - 1; i > 0; i--) {
+ // check if position i + 1 is after word end
+ dchar thischar = i >= 0 && i < linelen ? txt[i] : ' ';
+ if (thischar == '\t')
+ thischar = ' ';
+ dchar nextchar = i - 1 >= 0 && i - 1 < linelen ? txt[i - 1] : ' ';
+ if (nextchar == '\t')
+ nextchar = ' ';
+ if (isWordBound(thischar, nextchar)
+ || (camelCasePartsAsWords && isUpperAlpha(thischar) && isLowerAlpha(nextchar))) {
+ found = i;
+ break;
+ }
+ }
+ if (found >= 0)
+ p.pos = found;
+ else
+ p.pos = 0;
+ }
+ } else if (direction > 0) {
+ // forward
+ if (p.pos >= linelen) {
+ // last position of line
+ if (p.line < length - 1)
+ p = firstNonSpace(p.line + 1);
+ } else if (p.pos >= lastns.pos) { // before first nonspace
+ // to beginning of line
+ p.pos = linelen;
+ } else {
+ dstring txt = line(p.line);
+ int found = -1;
+ for (int i = p.pos; i < linelen; i++) {
+ // check if position i + 1 is after word end
+ dchar thischar = txt[i];
+ if (thischar == '\t')
+ thischar = ' ';
+ dchar nextchar = i < linelen - 1 ? txt[i + 1] : ' ';
+ if (nextchar == '\t')
+ nextchar = ' ';
+ if (isWordBound(thischar, nextchar)
+ || (camelCasePartsAsWords && isLowerAlpha(thischar) && isUpperAlpha(nextchar))) {
+ found = i + 1;
+ break;
+ }
+ }
+ if (found >= 0)
+ p.pos = found;
+ else
+ p.pos = linelen;
+ }
+ }
+ return p;
+ }
+
+ /// edit content
+ bool performOperation(EditOperation op, Object source) {
+ if (_readOnly)
+ throw new Exception("content is readonly");
+ if (op.action == EditAction.Replace) {
+ TextRange rangeBefore = op.range;
+ assert(rangeBefore.start <= rangeBefore.end);
+ //correctRange(rangeBefore);
+ dstring[] oldcontent = rangeText(rangeBefore);
+ dstring[] newcontent = op.content;
+ if (newcontent.length == 0)
+ newcontent ~= ""d;
+ TextRange rangeAfter = op.range;
+ rangeAfter.end = rangeAfter.start;
+ if (newcontent.length > 1) {
+ // different lines
+ rangeAfter.end.line = rangeAfter.start.line + cast(int)newcontent.length - 1;
+ rangeAfter.end.pos = cast(int)newcontent[$ - 1].length;
+ } else {
+ // same line
+ rangeAfter.end.pos = rangeAfter.start.pos + cast(int)newcontent[0].length;
+ }
+ assert(rangeAfter.start <= rangeAfter.end);
+ op.newRange = rangeAfter;
+ op.oldContent = oldcontent;
+ replaceRange(rangeBefore, rangeAfter, newcontent);
+ _undoBuffer.saveForUndo(op);
+ handleContentChange(op, rangeBefore, rangeAfter, source);
+ return true;
+ }
+ return false;
+ }
+
+ /// return true if there is at least one operation in undo buffer
+ @property bool hasUndo() {
+ return _undoBuffer.hasUndo;
+ }
+ /// return true if there is at least one operation in redo buffer
+ @property bool hasRedo() {
+ return _undoBuffer.hasRedo;
+ }
+ /// undoes last change
+ bool undo() {
+ if (!hasUndo)
+ return false;
+ if (_readOnly)
+ throw new Exception("content is readonly");
+ EditOperation op = _undoBuffer.undo();
+ TextRange rangeBefore = op.newRange;
+ dstring[] oldcontent = op.content;
+ dstring[] newcontent = op.oldContent;
+ TextRange rangeAfter = op.range;
+ //Log.d("Undoing op rangeBefore=", rangeBefore, " contentBefore=`", oldcontent, "` rangeAfter=", rangeAfter, " contentAfter=`", newcontent, "`");
+ replaceRange(rangeBefore, rangeAfter, newcontent);
+ handleContentChange(op, rangeBefore, rangeAfter, this);
+ return true;
+ }
+
+ /// redoes last undone change
+ bool redo() {
+ if (!hasUndo)
+ return false;
+ if (_readOnly)
+ throw new Exception("content is readonly");
+ EditOperation op = _undoBuffer.redo();
+ TextRange rangeBefore = op.range;
+ dstring[] oldcontent = op.oldContent;
+ dstring[] newcontent = op.content;
+ TextRange rangeAfter = op.newRange;
+ //Log.d("Redoing op rangeBefore=", rangeBefore, " contentBefore=`", oldcontent, "` rangeAfter=", rangeAfter, " contentAfter=`", newcontent, "`");
+ replaceRange(rangeBefore, rangeAfter, newcontent);
+ handleContentChange(op, rangeBefore, rangeAfter, this);
+ return true;
+ }
+ /// clear undo/redp history
+ void clearUndo() {
+ _undoBuffer.clear();
+ }
+
+ protected string _filename;
+ protected TextFileFormat _format;
+
+ /// file used to load editor content
+ @property string filename() {
+ return _filename;
+ }
+
+
+ /// load content form input stream
+ bool load(InputStream f, string fname = null) {
+ import dlangui.core.linestream;
+ clear();
+ _filename = fname;
+ _format = TextFileFormat.init;
+ try {
+ LineStream lines = LineStream.create(f, fname);
+ for (;;) {
+ dchar[] s = lines.readLine();
+ if (s is null)
+ break;
+ int pos = cast(int)(_lines.length++);
+ _tokenProps.length = _lines.length;
+ _lines[pos] = s.dup;
+ clearTokenProps(pos, pos + 1);
+ }
+ if (lines.errorCode != 0) {
+ clear();
+ Log.e("Error ", lines.errorCode, " ", lines.errorMessage, " -- at line ", lines.errorLine, " position ", lines.errorPos);
+ notifyContentReplaced();
+ return false;
+ }
+ // EOF
+ _format = lines.textFormat;
+ _undoBuffer.clear();
+ notifyContentReplaced();
+ return true;
+ } catch (Exception e) {
+ Log.e("Exception while trying to read file ", fname, " ", e.toString);
+ clear();
+ notifyContentReplaced();
+ return false;
+ }
+ }
+ /// load content from file
+ bool load(string filename) {
+ clear();
+ try {
+ std.stream.File f = new std.stream.File(filename);
+ scope(exit) { f.close(); }
+ return load(f, filename);
+ } catch (Exception e) {
+ Log.e("Exception while trying to read file ", filename, " ", e.toString);
+ clear();
+ return false;
+ }
+ }
+ /// save to output stream in specified format
+ bool save(OutputStream stream, string filename, TextFileFormat format) {
+ if (!filename)
+ filename = _filename;
+ _format = format;
+ import dlangui.core.linestream;
+ try {
+ OutputLineStream writer = new OutputLineStream(stream, filename, format);
+ scope(exit) { writer.close(); }
+ for (int i = 0; i < _lines.length; i++) {
+ writer.writeLine(_lines[i]);
+ }
+ _undoBuffer.saved();
+ notifyContentSaved();
+ return true;
+ } catch (Exception e) {
+ Log.e("Exception while trying to write file ", filename, " ", e.toString);
+ return false;
+ }
+ }
+ /// save to output stream in current format
+ bool save(OutputStream stream, string filename) {
+ return save(stream, filename, _format);
+ }
+ /// save to file in specified format
+ bool save(string filename, TextFileFormat format) {
+ if (!filename)
+ filename = _filename;
+ try {
+ std.stream.File f = new std.stream.File(filename, FileMode.OutNew);
+ scope(exit) { f.close(); }
+ return save(f, filename, format);
+ } catch (Exception e) {
+ Log.e("Exception while trying to save file ", filename, " ", e.toString);
+ return false;
+ }
+ }
+ /// save to file in current format
+ bool save(string filename = null) {
+ return save(filename, _format);
+ }
+}
+
diff --git a/src/dlangui/widgets/editors.d b/src/dlangui/widgets/editors.d
index 2f15857b..7ab411d3 100644
--- a/src/dlangui/widgets/editors.d
+++ b/src/dlangui/widgets/editors.d
@@ -32,73 +32,15 @@ import dlangui.core.linestream;
import dlangui.platforms.common.platform;
import dlangui.widgets.menu;
import dlangui.widgets.popup;
-private import dlangui.graphics.colors;
+import dlangui.graphics.colors;
+public import dlangui.core.editable;
import std.algorithm;
import std.stream;
-immutable dchar EOL = '\n';
-
-const ubyte TOKEN_CATEGORY_SHIFT = 4;
-const ubyte TOKEN_CATEGORY_MASK = 0xF0; // token category 0..15
-const ubyte TOKEN_SUBCATEGORY_MASK = 0x0F; // token subcategory 0..15
-const ubyte TOKEN_UNKNOWN = 0;
-
-/*
- Bit mask:
- 7654 3210
- cccc ssss
- | |
- | \ ssss = token subcategory
- |
- \ cccc = token category
-
- */
-/// token category for syntax highlight
-enum TokenCategory : ubyte {
- WhiteSpace = (0 << TOKEN_CATEGORY_SHIFT),
- WhiteSpace_Space = (0 << TOKEN_CATEGORY_SHIFT) | 1,
- WhiteSpace_Tab = (0 << TOKEN_CATEGORY_SHIFT) | 2,
-
- Comment = (1 << TOKEN_CATEGORY_SHIFT),
- Comment_SingleLine = (1 << TOKEN_CATEGORY_SHIFT) | 1, // single line comment
- Comment_SingleLineDoc = (1 << TOKEN_CATEGORY_SHIFT) | 2,// documentation in single line comment
- Comment_MultyLine = (1 << TOKEN_CATEGORY_SHIFT) | 3, // multiline coment
- Comment_MultyLineDoc = (1 << TOKEN_CATEGORY_SHIFT) | 4, // documentation in multiline comment
- Comment_Documentation = (1 << TOKEN_CATEGORY_SHIFT) | 5,// documentation comment
-
- Identifier = (2 << TOKEN_CATEGORY_SHIFT), // identifier (exact subcategory is unknown)
- Identifier_Class = (2 << TOKEN_CATEGORY_SHIFT) | 1, // class name
- Identifier_Struct = (2 << TOKEN_CATEGORY_SHIFT) | 2, // struct name
- Identifier_Local = (2 << TOKEN_CATEGORY_SHIFT) | 3, // local variable
- Identifier_Member = (2 << TOKEN_CATEGORY_SHIFT) | 4, // struct or class member
- Identifier_Deprecated = (2 << TOKEN_CATEGORY_SHIFT) | 15, // usage of this identifier is deprecated
- /// string literal
- String = (3 << TOKEN_CATEGORY_SHIFT),
- /// character literal
- Character = (4 << TOKEN_CATEGORY_SHIFT),
- /// integer literal
- Integer = (5 << TOKEN_CATEGORY_SHIFT),
- /// floating point number literal
- Float = (6 << TOKEN_CATEGORY_SHIFT),
- /// keyword
- Keyword = (7 << TOKEN_CATEGORY_SHIFT),
- /// operator
- Op = (8 << TOKEN_CATEGORY_SHIFT),
- // add more here
- //....
- /// error - unparsed character sequence
- Error = (15 << TOKEN_CATEGORY_SHIFT),
- /// invalid token - generic
- Error_InvalidToken = (15 << TOKEN_CATEGORY_SHIFT) | 1,
- /// invalid number token - error occured while parsing number
- Error_InvalidNumber = (15 << TOKEN_CATEGORY_SHIFT) | 2,
- /// invalid string token - error occured while parsing string
- Error_InvalidString = (15 << TOKEN_CATEGORY_SHIFT) | 3,
- /// invalid identifier token - error occured while parsing identifier
- Error_InvalidIdentifier = (15 << TOKEN_CATEGORY_SHIFT) | 4,
- /// invalid comment token - error occured while parsing comment
- Error_InvalidComment = (15 << TOKEN_CATEGORY_SHIFT) | 4,
+/// Modified state change listener
+interface ModifiedStateListener {
+ void onModifiedStateChange(Widget source, bool modified);
}
/// Editor action codes
@@ -218,899 +160,6 @@ enum EditorActions : int {
}
-
-
-/// split dstring by delimiters
-dstring[] splitDString(dstring source, dchar delimiter = EOL) {
- int start = 0;
- dstring[] res;
- dchar lastchar;
- for (int i = 0; i <= source.length; i++) {
- if (i == source.length || source[i] == delimiter) {
- if (i >= start) {
- dchar prevchar = i > 1 && i > start + 1 ? source[i - 1] : 0;
- int end = i;
- if (delimiter == EOL && prevchar == '\r') // windows CR/LF
- end--;
- dstring line = i > start ? cast(dstring)(source[start .. end].dup) : ""d;
- res ~= line;
- }
- start = i + 1;
- }
- }
- return res;
-}
-
-version (Windows) {
- immutable dstring SYSTEM_DEFAULT_EOL = "\r\n";
-} else {
- immutable dstring SYSTEM_DEFAULT_EOL = "\n";
-}
-
-/// concat strings from array using delimiter
-dstring concatDStrings(dstring[] lines, dstring delimiter = SYSTEM_DEFAULT_EOL) {
- dchar[] buf;
- foreach(line; lines) {
- if (buf.length)
- buf ~= delimiter;
- buf ~= line;
- }
- return cast(dstring)buf;
-}
-
-/// replace end of lines with spaces
-dstring replaceEolsWithSpaces(dstring source) {
- dchar[] buf;
- dchar lastch;
- foreach(ch; source) {
- if (ch == '\r') {
- buf ~= ' ';
- } else if (ch == '\n') {
- if (lastch != '\r')
- buf ~= ' ';
- } else {
- buf ~= ch;
- }
- lastch = ch;
- }
- return cast(dstring)buf;
-}
-
-/// text content position
-struct TextPosition {
- /// line number, zero based
- int line;
- /// character position in line (0 == before first character)
- int pos;
- /// compares two positions
- int opCmp(ref const TextPosition v) const {
- if (line < v.line)
- return -1;
- if (line > v.line)
- return 1;
- if (pos < v.pos)
- return -1;
- if (pos > v.pos)
- return 1;
- return 0;
- }
-}
-
-/// text content range
-struct TextRange {
- TextPosition start;
- TextPosition end;
- /// returns true if range is empty
- @property bool empty() const {
- return end <= start;
- }
- /// returns true if start and end located at the same line
- @property bool singleLine() const {
- return end.line == start.line;
- }
- /// returns count of lines in range
- @property int lines() const {
- return end.line - start.line + 1;
- }
-}
-
-/// action performed with editable contents
-enum EditAction {
- /// insert content into specified position (range.start)
- //Insert,
- /// delete content in range
- //Delete,
- /// replace range content with new content
- Replace,
-
- /// replace whole content
- ReplaceContent,
- /// saved content
- SaveContent,
-}
-
-/// edit operation details for EditableContent
-class EditOperation {
- protected EditAction _action;
- /// action performed
- @property EditAction action() { return _action; }
- protected TextRange _range;
-
- /// source range to replace with new content
- @property ref TextRange range() { return _range; }
- protected TextRange _newRange;
-
- /// new range after operation applied
- @property ref TextRange newRange() { return _newRange; }
- @property void newRange(TextRange range) { _newRange = range; }
-
- /// new content for range (if required for this action)
- protected dstring[] _content;
- @property ref dstring[] content() { return _content; }
-
- /// old content for range
- protected dstring[] _oldContent;
- @property ref dstring[] oldContent() { return _oldContent; }
- @property void oldContent(dstring[] content) { _oldContent = content; }
-
- this(EditAction action) {
- _action = action;
- }
- this(EditAction action, TextPosition pos, dstring text) {
- this(action, TextRange(pos, pos), text);
- }
- this(EditAction action, TextRange range, dstring text) {
- _action = action;
- _range = range;
- _content.length = 1;
- _content[0] = text.dup;
- }
- this(EditAction action, TextRange range, dstring[] text) {
- _action = action;
- _range = range;
- _content.length = text.length;
- for(int i = 0; i < text.length; i++)
- _content[i] = text[i].dup;
- //_content = text;
- }
- /// try to merge two operations (simple entering of characters in the same line), return true if succeded
- bool merge(EditOperation op) {
- if (_range.start.line != op._range.start.line) // both ops whould be on the same line
- return false;
- if (_content.length != 1 || op._content.length != 1) // both ops should operate the same line
- return false;
- // appending of single character
- if (_range.empty && op._range.empty && op._content[0].length == 1 && _newRange.end.pos == op._range.start.pos) {
- _content[0] ~= op._content[0];
- _newRange.end.pos++;
- return true;
- }
- // removing single character
- if (_newRange.empty && op._newRange.empty && op._oldContent[0].length == 1) {
- if (_newRange.end.pos == op.range.end.pos) {
- // removed char before
- _range.start.pos--;
- _newRange.start.pos--;
- _newRange.end.pos--;
- _oldContent[0] = (op._oldContent[0].dup ~ _oldContent[0].dup).dup;
- return true;
- } else if (_newRange.end.pos == op._range.start.pos) {
- // removed char after
- _range.end.pos++;
- _oldContent[0] = (_oldContent[0].dup ~ op._oldContent[0].dup).dup;
- return true;
- }
- }
- return false;
- }
-}
-
-/// Undo/Redo buffer
-class UndoBuffer {
- protected Collection!EditOperation _undoList;
- protected Collection!EditOperation _redoList;
-
- /// returns true if buffer contains any undo items
- @property bool hasUndo() {
- return !_undoList.empty;
- }
-
- /// returns true if buffer contains any redo items
- @property bool hasRedo() {
- return !_redoList.empty;
- }
-
- /// adds undo operation
- void saveForUndo(EditOperation op) {
- _redoList.clear();
- if (!_undoList.empty) {
- if (_undoList.back.merge(op)) {
- return; // merged - no need to add new operation
- }
- }
- _undoList.pushBack(op);
- }
-
- /// returns operation to be undone (put it to redo), null if no undo ops available
- EditOperation undo() {
- if (!hasUndo)
- return null; // no undo operations
- EditOperation res = _undoList.popBack();
- _redoList.pushBack(res);
- return res;
- }
-
- /// returns operation to be redone (put it to undo), null if no undo ops available
- EditOperation redo() {
- if (!hasRedo)
- return null; // no undo operations
- EditOperation res = _redoList.popBack();
- _undoList.pushBack(res);
- return res;
- }
-
- /// clears both undo and redo buffers
- void clear() {
- _undoList.clear();
- _redoList.clear();
- _savedState = null;
- }
-
- protected EditOperation _savedState;
-
- /// current state is saved
- void saved() {
- _savedState = _undoList.peekBack;
- }
- /// returns true if content has been changed since last saved() or clear() call
- @property bool modified() {
- return _savedState !is _undoList.peekBack;
- }
-}
-
-/// Editable Content change listener
-interface EditableContentListener {
- void onContentChange(EditableContent content, EditOperation operation, ref TextRange rangeBefore, ref TextRange rangeAfter, Object source);
-}
-
-/// Modified state change listener
-interface ModifiedStateListener {
- void onModifiedStateChange(Widget source, bool modified);
-}
-
-alias TokenPropString = ubyte[];
-
-/// interface for custom syntax highlight
-interface SyntaxHighlighter {
- /// categorize characters in content by token types
- void updateHighlight(dstring[] lines, TokenPropString[] props, int changeStartLine, int changeEndLine);
-}
-
-/// editable plain text (singleline/multiline)
-class EditableContent {
-
- this(bool multiline) {
- _multiline = multiline;
- _lines.length = 1; // initial state: single empty line
- _undoBuffer = new UndoBuffer();
- }
-
- @property bool modified() {
- return _undoBuffer.modified;
- }
-
- protected UndoBuffer _undoBuffer;
-
- protected SyntaxHighlighter _syntaxHighlighter;
-
- @property SyntaxHighlighter syntaxHighlighter() {
- return _syntaxHighlighter;
- }
-
- @property EditableContent syntaxHighlighter(SyntaxHighlighter syntaxHighlighter) {
- _syntaxHighlighter = syntaxHighlighter;
- updateTokenProps(0, cast(int)_lines.length);
- return this;
- }
-
- /// returns true if content has syntax highlight handler set
- @property bool hasSyntaxHighlight() {
- return _syntaxHighlighter !is null;
- }
-
- protected bool _readOnly;
-
- @property bool readOnly() {
- return _readOnly;
- }
-
- @property void readOnly(bool readOnly) {
- _readOnly = readOnly;
- }
-
- /// listeners for edit operations
- Signal!EditableContentListener contentChangeListeners;
-
- protected bool _multiline;
- /// returns true if miltyline content is supported
- @property bool multiline() { return _multiline; }
-
- protected dstring[] _lines;
- protected TokenPropString[] _tokenProps;
-
- /// returns all lines concatenated delimited by '\n'
- @property dstring text() {
- if (_lines.length == 0)
- return "";
- if (_lines.length == 1)
- return _lines[0];
- // concat lines
- dchar[] buf;
- foreach(item;_lines) {
- if (buf.length)
- buf ~= EOL;
- buf ~= item;
- }
- return cast(dstring)buf;
- }
-
- /// append one or more lines at end
- void appendLines(dstring[] lines...) {
- TextRange rangeBefore;
- rangeBefore.start = rangeBefore.end = lineEnd(_lines.length ? cast(int)_lines.length - 1 : 0);
- EditOperation op = new EditOperation(EditAction.Replace, rangeBefore, lines);
- performOperation(op, this);
- }
-
- /// call listener to say that whole content is replaced e.g. by loading from file
- void notifyContentReplaced() {
- TextRange rangeBefore;
- TextRange rangeAfter;
- // notify about content change
- handleContentChange(new EditOperation(EditAction.ReplaceContent), rangeBefore, rangeAfter, this);
- }
-
- /// call listener to say that content is saved
- void notifyContentSaved() {
- TextRange rangeBefore;
- TextRange rangeAfter;
- // notify about content change
- handleContentChange(new EditOperation(EditAction.SaveContent), rangeBefore, rangeAfter, this);
- }
-
- protected void updateTokenProps(int startLine, int endLine) {
- clearTokenProps(startLine, endLine);
- if (_syntaxHighlighter) {
- _syntaxHighlighter.updateHighlight(_lines, _tokenProps, startLine, endLine);
- }
- }
-
- /// set props arrays size equal to text line sizes, bit fill with unknown token
- protected void clearTokenProps(int startLine, int endLine) {
- for (int i = startLine; i < endLine; i++) {
- if (hasSyntaxHighlight) {
- int len = cast(int)_lines[i].length;
- _tokenProps[i].length = len;
- for (int j = 0; j < len; j++)
- _tokenProps[i][j] = TOKEN_UNKNOWN;
- } else {
- _tokenProps[i] = null; // no token props
- }
- }
- }
-
- /// replace whole text with another content
- @property EditableContent text(dstring newContent) {
- clearUndo();
- _lines.length = 0;
- if (_multiline) {
- _lines = splitDString(newContent);
- _tokenProps.length = _lines.length;
- updateTokenProps(0, cast(int)_lines.length);
- } else {
- _lines.length = 1;
- _lines[0] = replaceEolsWithSpaces(newContent);
- _tokenProps.length = 1;
- updateTokenProps(0, cast(int)_lines.length);
- }
- notifyContentReplaced();
- return this;
- }
-
- /// clear content
- void clear() {
- clearUndo();
- _lines.length = 0;
- }
-
-
- /// returns line text
- @property int length() { return cast(int)_lines.length; }
- dstring opIndex(int index) {
- return line(index);
- }
-
- /// returns line text by index, "" if index is out of bounds
- dstring line(int index) {
- return index >= 0 && index < _lines.length ? _lines[index] : ""d;
- }
-
- /// returns line token properties one item per character
- TokenPropString lineTokenProps(int index) {
- return index >= 0 && index < _tokenProps.length ? _tokenProps[index] : null;
- }
-
- /// returns text position for end of line lineIndex
- TextPosition lineEnd(int lineIndex) {
- return TextPosition(lineIndex, lineLength(lineIndex));
- }
-
- /// returns position before first non-space character of line, returns 0 position if no non-space chars
- TextPosition firstNonSpace(int lineIndex) {
- dstring s = line(lineIndex);
- for (int i = 0; i < s.length; i++)
- if (s[i] != ' ' && s[i] != '\t')
- return TextPosition(lineIndex, i);
- return TextPosition(lineIndex, 0);
- }
-
- /// returns position after last non-space character of line, returns 0 position if no non-space chars on line
- TextPosition lastNonSpace(int lineIndex) {
- dstring s = line(lineIndex);
- for (int i = cast(int)s.length - 1; i >= 0; i--)
- if (s[i] != ' ' && s[i] != '\t')
- return TextPosition(lineIndex, i + 1);
- return TextPosition(lineIndex, 0);
- }
-
- /// returns text position for end of line lineIndex
- int lineLength(int lineIndex) {
- return lineIndex >= 0 && lineIndex < _lines.length ? cast(int)_lines[lineIndex].length : 0;
- }
-
- /// returns maximum length of line
- int maxLineLength() {
- int m = 0;
- foreach(s; _lines)
- if (m < s.length)
- m = cast(int)s.length;
- return m;
- }
-
- void handleContentChange(EditOperation op, ref TextRange rangeBefore, ref TextRange rangeAfter, Object source) {
- // update highlight if necessary
- updateTokenProps(rangeAfter.start.line, rangeAfter.end.line + 1);
- // call listeners
- if (contentChangeListeners.assigned)
- contentChangeListeners(this, op, rangeBefore, rangeAfter, source);
- }
-
- /// return text for specified range
- dstring[] rangeText(TextRange range) {
- dstring[] res;
- if (range.empty) {
- res ~= ""d;
- return res;
- }
- for (int lineIndex = range.start.line; lineIndex <= range.end.line; lineIndex++) {
- dstring lineText = line(lineIndex);
- dstring lineFragment = lineText;
- int startchar = 0;
- int endchar = cast(int)lineText.length;
- if (lineIndex == range.start.line)
- startchar = range.start.pos;
- if (lineIndex == range.end.line)
- endchar = range.end.pos;
- if (endchar > lineText.length)
- endchar = cast(int)lineText.length;
- if (endchar <= startchar)
- lineFragment = ""d;
- else if (startchar != 0 || endchar != lineText.length)
- lineFragment = lineText[startchar .. endchar].dup;
- res ~= lineFragment;
- }
- return res;
- }
-
- /// when position is out of content bounds, fix it to nearest valid position
- void correctPosition(ref TextPosition position) {
- if (position.line >= length) {
- position.line = length - 1;
- position.pos = lineLength(position.line);
- }
- if (position.line < 0) {
- position.line = 0;
- position.pos = 0;
- }
- int currentLineLength = lineLength(position.line);
- if (position.pos > currentLineLength)
- position.pos = currentLineLength;
- if (position.pos < 0)
- position.pos = 0;
- }
-
- /// when range positions is out of content bounds, fix it to nearest valid position
- void correctRange(ref TextRange range) {
- correctPosition(range.start);
- correctPosition(range.end);
- }
-
- /// removes removedCount lines starting from start
- protected void removeLines(int start, int removedCount) {
- int end = start + removedCount;
- assert(removedCount > 0 && start >= 0 && end > 0 && start < _lines.length && end <= _lines.length);
- for (int i = start; i < _lines.length - removedCount; i++) {
- _lines[i] = _lines[i + removedCount];
- _tokenProps[i] = _tokenProps[i + removedCount];
- }
- for (int i = cast(int)_lines.length - removedCount; i < _lines.length; i++) {
- _lines[i] = null; // free unused line references
- _tokenProps[i] = null; // free unused line references
- }
- _lines.length -= removedCount;
- _tokenProps.length = _lines.length;
- }
-
- /// inserts count empty lines at specified position
- protected void insertLines(int start, int count) {
- assert(count > 0);
- _lines.length += count;
- _tokenProps.length = _lines.length;
- for (int i = cast(int)_lines.length - 1; i >= start + count; i--) {
- _lines[i] = _lines[i - count];
- _tokenProps[i] = _tokenProps[i - count];
- }
- for (int i = start; i < start + count; i++) {
- _lines[i] = ""d;
- _tokenProps[i] = null;
- }
- }
-
- /// inserts or removes lines, removes text in range
- protected void replaceRange(TextRange before, TextRange after, dstring[] newContent) {
- dstring firstLineBefore = line(before.start.line);
- dstring lastLineBefore = before.singleLine ? firstLineBefore : line(before.end.line);
- dstring firstLineHead = before.start.pos > 0 && before.start.pos <= firstLineBefore.length ? firstLineBefore[0..before.start.pos] : ""d;
- dstring lastLineTail = before.end.pos >= 0 && before.end.pos < lastLineBefore.length ? lastLineBefore[before.end.pos .. $] : ""d;
-
- int linesBefore = before.lines;
- int linesAfter = after.lines;
- if (linesBefore < linesAfter) {
- // add more lines
- insertLines(before.start.line + 1, linesAfter - linesBefore);
- } else if (linesBefore > linesAfter) {
- // remove extra lines
- removeLines(before.start.line + 1, linesBefore - linesAfter);
- }
- for (int i = after.start.line; i <= after.end.line; i++) {
- dstring newline = newContent[i - after.start.line];
- if (i == after.start.line && i == after.end.line) {
- dchar[] buf;
- buf ~= firstLineHead;
- buf ~= newline;
- buf ~= lastLineTail;
- //Log.d("merging lines ", firstLineHead, " ", newline, " ", lastLineTail);
- _lines[i] = cast(dstring)buf;
- clearTokenProps(i, i + 1);
- //Log.d("merge result: ", _lines[i]);
- } else if (i == after.start.line) {
- dchar[] buf;
- buf ~= firstLineHead;
- buf ~= newline;
- _lines[i] = cast(dstring)buf;
- clearTokenProps(i, i + 1);
- } else if (i == after.end.line) {
- dchar[] buf;
- buf ~= newline;
- buf ~= lastLineTail;
- _lines[i] = cast(dstring)buf;
- clearTokenProps(i, i + 1);
- } else {
- _lines[i] = newline; // no dup needed
- clearTokenProps(i, i + 1);
- }
- }
- }
-
- static bool isDigit(dchar ch) pure nothrow {
- return ch >= '0' && ch <= '9';
- }
- static bool isAlpha(dchar ch) pure nothrow {
- return isLowerAlpha(ch) || isUpperAlpha(ch);
- }
- static bool isAlNum(dchar ch) pure nothrow {
- return isDigit(ch) || isAlpha(ch);
- }
- static bool isLowerAlpha(dchar ch) pure nothrow {
- return (ch >= 'a' && ch <= 'z') || (ch == '_');
- }
- static bool isUpperAlpha(dchar ch) pure nothrow {
- return (ch >= 'A' && ch <= 'Z');
- }
- static bool isPunct(dchar ch) pure nothrow {
- switch(ch) {
- case '.':
- case ',':
- case ';':
- case '?':
- case '!':
- return true;
- default:
- return false;
- }
- }
- static bool isBracket(dchar ch) pure nothrow {
- switch(ch) {
- case '(':
- case ')':
- case '[':
- case ']':
- case '{':
- case '}':
- return true;
- default:
- return false;
- }
- }
-
- static bool isWordBound(dchar thischar, dchar nextchar) {
- return (isAlNum(thischar) && !isAlNum(nextchar))
- || (isPunct(thischar) && !isPunct(nextchar))
- || (isBracket(thischar) && !isBracket(nextchar))
- || (thischar != ' ' && nextchar == ' ');
- }
-
- /// change text position to nearest word bound (direction < 0 - back, > 0 - forward)
- TextPosition moveByWord(TextPosition p, int direction, bool camelCasePartsAsWords) {
- correctPosition(p);
- TextPosition firstns = firstNonSpace(p.line); // before first non space
- TextPosition lastns = lastNonSpace(p.line); // after last non space
- int linelen = lineLength(p.line); // line length
- if (direction < 0) {
- // back
- if (p.pos <= 0) {
- // beginning of line - move to prev line
- if (p.line > 0)
- p = lastNonSpace(p.line - 1);
- } else if (p.pos <= firstns.pos) { // before first nonspace
- // to beginning of line
- p.pos = 0;
- } else {
- dstring txt = line(p.line);
- int found = -1;
- for (int i = p.pos - 1; i > 0; i--) {
- // check if position i + 1 is after word end
- dchar thischar = i >= 0 && i < linelen ? txt[i] : ' ';
- if (thischar == '\t')
- thischar = ' ';
- dchar nextchar = i - 1 >= 0 && i - 1 < linelen ? txt[i - 1] : ' ';
- if (nextchar == '\t')
- nextchar = ' ';
- if (isWordBound(thischar, nextchar)
- || (camelCasePartsAsWords && isUpperAlpha(thischar) && isLowerAlpha(nextchar))) {
- found = i;
- break;
- }
- }
- if (found >= 0)
- p.pos = found;
- else
- p.pos = 0;
- }
- } else if (direction > 0) {
- // forward
- if (p.pos >= linelen) {
- // last position of line
- if (p.line < length - 1)
- p = firstNonSpace(p.line + 1);
- } else if (p.pos >= lastns.pos) { // before first nonspace
- // to beginning of line
- p.pos = linelen;
- } else {
- dstring txt = line(p.line);
- int found = -1;
- for (int i = p.pos; i < linelen; i++) {
- // check if position i + 1 is after word end
- dchar thischar = txt[i];
- if (thischar == '\t')
- thischar = ' ';
- dchar nextchar = i < linelen - 1 ? txt[i + 1] : ' ';
- if (nextchar == '\t')
- nextchar = ' ';
- if (isWordBound(thischar, nextchar)
- || (camelCasePartsAsWords && isLowerAlpha(thischar) && isUpperAlpha(nextchar))) {
- found = i + 1;
- break;
- }
- }
- if (found >= 0)
- p.pos = found;
- else
- p.pos = linelen;
- }
- }
- return p;
- }
-
- /// edit content
- bool performOperation(EditOperation op, Object source) {
- if (_readOnly)
- throw new Exception("content is readonly");
- if (op.action == EditAction.Replace) {
- TextRange rangeBefore = op.range;
- assert(rangeBefore.start <= rangeBefore.end);
- //correctRange(rangeBefore);
- dstring[] oldcontent = rangeText(rangeBefore);
- dstring[] newcontent = op.content;
- if (newcontent.length == 0)
- newcontent ~= ""d;
- TextRange rangeAfter = op.range;
- rangeAfter.end = rangeAfter.start;
- if (newcontent.length > 1) {
- // different lines
- rangeAfter.end.line = rangeAfter.start.line + cast(int)newcontent.length - 1;
- rangeAfter.end.pos = cast(int)newcontent[$ - 1].length;
- } else {
- // same line
- rangeAfter.end.pos = rangeAfter.start.pos + cast(int)newcontent[0].length;
- }
- assert(rangeAfter.start <= rangeAfter.end);
- op.newRange = rangeAfter;
- op.oldContent = oldcontent;
- replaceRange(rangeBefore, rangeAfter, newcontent);
- _undoBuffer.saveForUndo(op);
- handleContentChange(op, rangeBefore, rangeAfter, source);
- return true;
- }
- return false;
- }
-
- /// return true if there is at least one operation in undo buffer
- @property bool hasUndo() {
- return _undoBuffer.hasUndo;
- }
- /// return true if there is at least one operation in redo buffer
- @property bool hasRedo() {
- return _undoBuffer.hasRedo;
- }
- /// undoes last change
- bool undo() {
- if (!hasUndo)
- return false;
- if (_readOnly)
- throw new Exception("content is readonly");
- EditOperation op = _undoBuffer.undo();
- TextRange rangeBefore = op.newRange;
- dstring[] oldcontent = op.content;
- dstring[] newcontent = op.oldContent;
- TextRange rangeAfter = op.range;
- //Log.d("Undoing op rangeBefore=", rangeBefore, " contentBefore=`", oldcontent, "` rangeAfter=", rangeAfter, " contentAfter=`", newcontent, "`");
- replaceRange(rangeBefore, rangeAfter, newcontent);
- handleContentChange(op, rangeBefore, rangeAfter, this);
- return true;
- }
-
- /// redoes last undone change
- bool redo() {
- if (!hasUndo)
- return false;
- if (_readOnly)
- throw new Exception("content is readonly");
- EditOperation op = _undoBuffer.redo();
- TextRange rangeBefore = op.range;
- dstring[] oldcontent = op.oldContent;
- dstring[] newcontent = op.content;
- TextRange rangeAfter = op.newRange;
- //Log.d("Redoing op rangeBefore=", rangeBefore, " contentBefore=`", oldcontent, "` rangeAfter=", rangeAfter, " contentAfter=`", newcontent, "`");
- replaceRange(rangeBefore, rangeAfter, newcontent);
- handleContentChange(op, rangeBefore, rangeAfter, this);
- return true;
- }
- /// clear undo/redp history
- void clearUndo() {
- _undoBuffer.clear();
- }
-
- protected string _filename;
- protected TextFileFormat _format;
-
- /// file used to load editor content
- @property string filename() {
- return _filename;
- }
-
-
- /// load content form input stream
- bool load(InputStream f, string fname = null) {
- import dlangui.core.linestream;
- clear();
- _filename = fname;
- _format = TextFileFormat.init;
- try {
- LineStream lines = LineStream.create(f, fname);
- for (;;) {
- dchar[] s = lines.readLine();
- if (s is null)
- break;
- int pos = cast(int)(_lines.length++);
- _tokenProps.length = _lines.length;
- _lines[pos] = s.dup;
- clearTokenProps(pos, pos + 1);
- }
- if (lines.errorCode != 0) {
- clear();
- Log.e("Error ", lines.errorCode, " ", lines.errorMessage, " -- at line ", lines.errorLine, " position ", lines.errorPos);
- notifyContentReplaced();
- return false;
- }
- // EOF
- _format = lines.textFormat;
- _undoBuffer.clear();
- notifyContentReplaced();
- return true;
- } catch (Exception e) {
- Log.e("Exception while trying to read file ", fname, " ", e.toString);
- clear();
- notifyContentReplaced();
- return false;
- }
- }
- /// load content from file
- bool load(string filename) {
- clear();
- try {
- std.stream.File f = new std.stream.File(filename);
- scope(exit) { f.close(); }
- return load(f, filename);
- } catch (Exception e) {
- Log.e("Exception while trying to read file ", filename, " ", e.toString);
- clear();
- return false;
- }
- }
- /// save to output stream in specified format
- bool save(OutputStream stream, string filename, TextFileFormat format) {
- if (!filename)
- filename = _filename;
- _format = format;
- import dlangui.core.linestream;
- try {
- OutputLineStream writer = new OutputLineStream(stream, filename, format);
- scope(exit) { writer.close(); }
- for (int i = 0; i < _lines.length; i++) {
- writer.writeLine(_lines[i]);
- }
- _undoBuffer.saved();
- notifyContentSaved();
- return true;
- } catch (Exception e) {
- Log.e("Exception while trying to write file ", filename, " ", e.toString);
- return false;
- }
- }
- /// save to output stream in current format
- bool save(OutputStream stream, string filename) {
- return save(stream, filename, _format);
- }
- /// save to file in specified format
- bool save(string filename, TextFileFormat format) {
- if (!filename)
- filename = _filename;
- try {
- std.stream.File f = new std.stream.File(filename, FileMode.OutNew);
- scope(exit) { f.close(); }
- return save(f, filename, format);
- } catch (Exception e) {
- Log.e("Exception while trying to save file ", filename, " ", e.toString);
- return false;
- }
- }
- /// save to file in current format
- bool save(string filename = null) {
- return save(filename, _format);
- }
-}
-
/// base for all editor widgets
class EditWidgetBase : ScrollWidgetBase, EditableContentListener, MenuItemActionHandler {
protected EditableContent _content;