mirror of https://github.com/buggins/dlangui.git
refactoring: move EditableContent into separate module
This commit is contained in:
parent
d74e995d02
commit
7dca400e4c
|
@ -89,7 +89,6 @@
|
|||
<resfile />
|
||||
<exefile>$(OutDir)\$(ProjectName).lib</exefile>
|
||||
<useStdLibPath>1</useStdLibPath>
|
||||
<cRuntime>2</cRuntime>
|
||||
<additionalOptions />
|
||||
<preBuildCommand />
|
||||
<postBuildCommand />
|
||||
|
@ -184,7 +183,6 @@
|
|||
<resfile />
|
||||
<exefile>$(OutDir)\$(ProjectName).lib</exefile>
|
||||
<useStdLibPath>1</useStdLibPath>
|
||||
<cRuntime>1</cRuntime>
|
||||
<additionalOptions />
|
||||
<preBuildCommand />
|
||||
<postBuildCommand />
|
||||
|
@ -337,6 +335,7 @@
|
|||
<Folder name="dlangui">
|
||||
<Folder name="core">
|
||||
<File path="src\dlangui\core\collections.d" />
|
||||
<File path="src\dlangui\core\editable.d" />
|
||||
<File path="src\dlangui\core\events.d" />
|
||||
<File path="src\dlangui\core\files.d" />
|
||||
<File path="src\dlangui\core\i18n.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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue