diff --git a/src/dlangui/core/editable.d b/src/dlangui/core/editable.d index 56640c38..50ae3518 100644 --- a/src/dlangui/core/editable.d +++ b/src/dlangui/core/editable.d @@ -402,20 +402,42 @@ alias TokenPropString = ubyte[]; /// interface for custom syntax highlight interface SyntaxHighlighter { + + /// returns editable content + @property EditableContent content(); + /// set editable content + @property SyntaxHighlighter content(EditableContent content); + /// categorize characters in content by token types void updateHighlight(dstring[] lines, TokenPropString[] props, int changeStartLine, int changeEndLine); /// return true if toggle line comment is supported for file type @property bool supportsToggleLineComment(); /// return true if can toggle line comments for specified text range - bool canToggleLineComment(EditableContent content, TextRange range); + bool canToggleLineComment(TextRange range); /// toggle line comments for specified text range - void toggleLineComment(EditableContent content, TextRange range, Object source); + void toggleLineComment(TextRange range, Object source); /// return true if toggle block comment is supported for file type @property bool supportsToggleBlockComment(); /// return true if can toggle block comments for specified text range - bool canToggleBlockComment(EditableContent content, TextRange range); + bool canToggleBlockComment(TextRange range); /// toggle block comments for specified text range - void toggleBlockComment(EditableContent content, TextRange range, Object source); + void toggleBlockComment(TextRange range, Object source); +} + +/// measure line text (tabs, spaces, and nonspace positions) +struct TextLineMeasure { + /// line length + int len; + /// first non-space index in line + int firstNonSpace = -1; + /// first non-space position according to tab size + int firstNonSpaceX; + /// last non-space character index in line + int lastNonSpace = -1; + /// last non-space position based on tab size + int lastNonSpaceX; + /// true if line has zero length or consists of spaces and tabs only + @property bool empty() { return len == 0 || firstNonSpace < 0; } } /// editable plain text (singleline/multiline) @@ -442,6 +464,7 @@ class EditableContent { @property EditableContent syntaxHighlighter(SyntaxHighlighter syntaxHighlighter) { _syntaxHighlighter = syntaxHighlighter; + _syntaxHighlighter.content = this; updateTokenProps(0, cast(int)_lines.length); return this; } @@ -461,6 +484,31 @@ class EditableContent { _readOnly = readOnly; } + protected int _tabSize = 4; + protected bool _useSpacesForTabs = true; + /// returns tab size (in number of spaces) + @property int tabSize() { + return _tabSize; + } + /// sets tab size (in number of spaces) + @property EditableContent tabSize(int newTabSize) { + if (newTabSize < 1) + newTabSize = 1; + else if (newTabSize > 16) + newTabSize = 16; + _tabSize = newTabSize; + return this; + } + /// when true, spaces will be inserted instead of tabs + @property bool useSpacesForTabs() { + return _useSpacesForTabs; + } + /// set new Tab key behavior flag: when true, spaces will be inserted instead of tabs + @property EditableContent useSpacesForTabs(bool useSpacesForTabs) { + _useSpacesForTabs = useSpacesForTabs; + return this; + } + /// listeners for edit operations Signal!EditableContentListener contentChangeListeners; @@ -622,6 +670,52 @@ class EditableContent { return TextRange(TextPosition(lineIndex, 0), lineIndex < _lines.length - 1 ? lineBegin(lineIndex + 1) : lineEnd(lineIndex)); } + /// measures line non-space start and end positions + TextLineMeasure measureLine(int lineIndex) { + TextLineMeasure res; + dstring s = _lines[lineIndex]; + res.len = cast(int)s.length; + if (lineIndex < 0 || lineIndex >= _lines.length) + return res; + int x = 0; + for (int i = 0; i < s.length; i++) { + dchar ch = s[i]; + if (ch == ' ') { + x++; + } else if (ch == '\t') { + x = (x + _tabSize) % _tabSize; + } else { + if (res.firstNonSpace < 0) { + res.firstNonSpace = i; + res.firstNonSpaceX = x; + } + res.lastNonSpace = i; + res.lastNonSpaceX = x; + x++; + } + } + return res; + } + + /// return true if line with index lineIndex is empty (has length 0 or consists only of spaces and tabs) + bool lineIsEmpty(int lineIndex) { + if (lineIndex < 0 || lineIndex >= _lines.length) + return true; + dstring s = _lines[lineIndex]; + foreach(ch; s) + if (ch != ' ' && ch != '\t') + return false; + return true; + } + + /// corrent range to cover full lines + TextRange fullLinesRange(TextRange r) { + r.start.pos = 0; + if (r.end.pos > 0) + r.end = lineBegin(r.end.line + 1); + return r; + } + /// 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); diff --git a/src/dlangui/widgets/editors.d b/src/dlangui/widgets/editors.d index 684e5bf0..824217cb 100644 --- a/src/dlangui/widgets/editors.d +++ b/src/dlangui/widgets/editors.d @@ -180,14 +180,12 @@ class EditWidgetBase : ScrollWidgetBase, EditableContentListener, MenuItemAction protected Point _scrollPos; protected bool _fixedFont; protected int _spaceWidth; - protected int _tabSize = 4; protected int _leftPaneWidth; // left pane - can be used to show line numbers, collapse controls, bookmarks, breakpoints, custom icons protected int _minFontSize = -1; // disable zooming protected int _maxFontSize = -1; // disable zooming protected bool _wantTabs = true; - protected bool _useSpacesForTabs = false; protected bool _showLineNumbers = false; // show line numbers in left pane protected bool _showModificationMarks = false; // show modification marks in left pane protected bool _showIcons = false; // show icons in left pane @@ -564,18 +562,18 @@ class EditWidgetBase : ScrollWidgetBase, EditableContentListener, MenuItemAction /// when true, spaces will be inserted instead of tabs @property bool useSpacesForTabs() { - return _useSpacesForTabs; + return _content.useSpacesForTabs; } /// set new Tab key behavior flag: when true, spaces will be inserted instead of tabs @property EditWidgetBase useSpacesForTabs(bool useSpacesForTabs) { - _useSpacesForTabs = useSpacesForTabs; + _content.useSpacesForTabs = useSpacesForTabs; return this; } /// returns tab size (in number of spaces) @property int tabSize() { - return _tabSize; + return _content.tabSize; } /// sets tab size (in number of spaces) @@ -584,8 +582,8 @@ class EditWidgetBase : ScrollWidgetBase, EditableContentListener, MenuItemAction newTabSize = 1; else if (newTabSize > 16) newTabSize = 16; - if (newTabSize != _tabSize) { - _tabSize = newTabSize; + if (newTabSize != tabSize) { + _content.tabSize = newTabSize; requestLayout(); } return this; @@ -807,7 +805,7 @@ class EditWidgetBase : ScrollWidgetBase, EditableContentListener, MenuItemAction protected int calcLineWidth(dstring s) { int w = 0; if (_fixedFont) { - int tabw = _tabSize * _spaceWidth; + int tabw = tabSize * _spaceWidth; // version optimized for fixed font for (int i = 0; i < s.length; i++) { if (s[i] == '\t') { @@ -910,7 +908,7 @@ class EditWidgetBase : ScrollWidgetBase, EditableContentListener, MenuItemAction case EditorActions.ToggleBlockComment: if (!_content.syntaxHighlighter || !_content.syntaxHighlighter.supportsToggleBlockComment) a.state = ACTION_STATE_INVISIBLE; - else if (_content.syntaxHighlighter.canToggleBlockComment(_content, _selectionRange)) + else if (_content.syntaxHighlighter.canToggleBlockComment(_selectionRange)) a.state = ACTION_STATE_ENABLED; else a.state = ACTION_STATE_DISABLE; @@ -918,7 +916,7 @@ class EditWidgetBase : ScrollWidgetBase, EditableContentListener, MenuItemAction case EditorActions.ToggleLineComment: if (!_content.syntaxHighlighter || !_content.syntaxHighlighter.supportsToggleLineComment) a.state = ACTION_STATE_INVISIBLE; - else if (_content.syntaxHighlighter.canToggleLineComment(_content, _selectionRange)) + else if (_content.syntaxHighlighter.canToggleLineComment(_selectionRange)) a.state = ACTION_STATE_ENABLED; else a.state = ACTION_STATE_DISABLE; @@ -1142,7 +1140,7 @@ class EditWidgetBase : ScrollWidgetBase, EditableContentListener, MenuItemAction if (readOnly) return true; if (_selectionRange.empty) { - if (_useSpacesForTabs) { + if (useSpacesForTabs) { // insert one or more spaces to EditOperation op = new EditOperation(EditAction.Replace, TextRange(_caretPos, _caretPos), [spacesForTab(_caretPos.pos)]); _content.performOperation(op, this); @@ -1157,7 +1155,7 @@ class EditWidgetBase : ScrollWidgetBase, EditableContentListener, MenuItemAction return handleAction(new Action(EditorActions.Indent)); } else { // insert tab - if (_useSpacesForTabs) { + if (useSpacesForTabs) { // insert one or more spaces to EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [spacesForTab(_selectionRange.start.pos)]); _content.performOperation(op, this); @@ -1286,7 +1284,7 @@ class EditWidgetBase : ScrollWidgetBase, EditableContentListener, MenuItemAction return src[unindentPos .. $].dup; } else { // indent - if (_useSpacesForTabs) { + if (useSpacesForTabs) { if (cursor > 0) cursorPos.pos += tabSize; return spacesForTab(0) ~ src; @@ -2039,12 +2037,12 @@ class EditBox : EditWidgetBase { } return true; case EditorActions.ToggleBlockComment: - if (_content.syntaxHighlighter && _content.syntaxHighlighter.supportsToggleBlockComment && _content.syntaxHighlighter.canToggleBlockComment(_content, _selectionRange)) - _content.syntaxHighlighter.toggleBlockComment(_content, _selectionRange, this); + if (_content.syntaxHighlighter && _content.syntaxHighlighter.supportsToggleBlockComment && _content.syntaxHighlighter.canToggleBlockComment(_selectionRange)) + _content.syntaxHighlighter.toggleBlockComment(_selectionRange, this); return true; case EditorActions.ToggleLineComment: - if (_content.syntaxHighlighter && _content.syntaxHighlighter.supportsToggleLineComment && _content.syntaxHighlighter.canToggleLineComment(_content, _selectionRange)) - _content.syntaxHighlighter.toggleLineComment(_content, _selectionRange, this); + if (_content.syntaxHighlighter && _content.syntaxHighlighter.supportsToggleLineComment && _content.syntaxHighlighter.canToggleLineComment(_selectionRange)) + _content.syntaxHighlighter.toggleLineComment(_selectionRange, this); return true; case EditorActions.InsertLine: {