diff --git a/dlanguilib.visualdproj b/dlanguilib.visualdproj index c903befa..e5c77654 100644 --- a/dlanguilib.visualdproj +++ b/dlanguilib.visualdproj @@ -367,6 +367,7 @@ + @@ -377,6 +378,7 @@ + diff --git a/examples/dmledit/dmledit.visualdproj b/examples/dmledit/dmledit.visualdproj index 3933df6e..8737dadb 100644 --- a/examples/dmledit/dmledit.visualdproj +++ b/examples/dmledit/dmledit.visualdproj @@ -72,7 +72,7 @@ 0 0 - Unicode USE_FREETYPE USE_OPENGL + EmbedStandardResources Unicode USE_FREETYPE USE_OPENGL 0 0 0 diff --git a/examples/dmledit/src/dmledit.d b/examples/dmledit/src/dmledit.d index 7165385b..547b7216 100644 --- a/examples/dmledit/src/dmledit.d +++ b/examples/dmledit/src/dmledit.d @@ -3,6 +3,8 @@ module dmledit; import dlangui; import dlangui.dialogs.filedlg; import dlangui.dialogs.dialog; +import dlangui.core.dmlhighlight; +import std.array : replaceFirst; mixin APP_ENTRY_POINT; @@ -37,9 +39,29 @@ const Action ACTION_EDIT_UNINDENT = (new Action(EditorActions.Unindent, "MENU_ED const Action ACTION_EDIT_TOGGLE_LINE_COMMENT = (new Action(EditorActions.ToggleLineComment, "MENU_EDIT_TOGGLE_LINE_COMMENT"c, null, KeyCode.KEY_DIVIDE, KeyFlag.Control)).disableByDefault(); const Action ACTION_EDIT_TOGGLE_BLOCK_COMMENT = (new Action(EditorActions.ToggleBlockComment, "MENU_EDIT_TOGGLE_BLOCK_COMMENT"c, null, KeyCode.KEY_DIVIDE, KeyFlag.Control|KeyFlag.Shift)).disableByDefault(); const Action ACTION_EDIT_PREFERENCES = (new Action(IDEActions.EditPreferences, "MENU_EDIT_PREFERENCES"c, null)).disableByDefault(); -const Action ACTION_DEBUG_START = new Action(IDEActions.DebugStart, "MENU_DEBUG_START_DEBUGGING"c, "debug-run"c, KeyCode.F5, 0); +const Action ACTION_DEBUG_START = new Action(IDEActions.DebugStart, "MENU_DEBUG_UPDATE_PREVIEW"c, "debug-run"c, KeyCode.F5, 0); const Action ACTION_HELP_ABOUT = new Action(IDEActions.HelpAbout, "MENU_HELP_ABOUT"c); +/// DIDE source file editor +class DMLSourceEdit : SourceEdit { + this(string ID) { + super(ID); + MenuItem editPopupItem = new MenuItem(null); + editPopupItem.add(ACTION_EDIT_COPY, ACTION_EDIT_PASTE, ACTION_EDIT_CUT, ACTION_EDIT_UNDO, ACTION_EDIT_REDO, ACTION_EDIT_INDENT, ACTION_EDIT_UNINDENT, ACTION_EDIT_TOGGLE_LINE_COMMENT, ACTION_DEBUG_START); + popupMenu = editPopupItem; + content.syntaxSupport = new DMLSyntaxSupport(""); + setTokenHightlightColor(TokenCategory.Comment, 0x008000); // green + setTokenHightlightColor(TokenCategory.Keyword, 0x0000FF); // blue + setTokenHightlightColor(TokenCategory.String, 0xa31515); // brown + setTokenHightlightColor(TokenCategory.Error, 0xFF0000); // red + + } + this() { + this("DMLEDIT"); + } +} + + class EditFrame : AppFrame { MenuItem mainMenuItems; @@ -47,7 +69,9 @@ class EditFrame : AppFrame { override protected void init() { _appName = "DMLEdit"; super.init(); + updatePreview(); } + /// create main menu override protected MainMenu createMainMenu() { mainMenuItems = new MenuItem(); @@ -58,7 +82,7 @@ class EditFrame : AppFrame { MenuItem editItem = new MenuItem(new Action(2, "MENU_EDIT")); editItem.add(ACTION_EDIT_COPY, ACTION_EDIT_PASTE, ACTION_EDIT_CUT, ACTION_EDIT_UNDO, ACTION_EDIT_REDO, - ACTION_EDIT_INDENT, ACTION_EDIT_UNINDENT, ACTION_EDIT_TOGGLE_LINE_COMMENT, ACTION_EDIT_TOGGLE_BLOCK_COMMENT); + ACTION_EDIT_INDENT, ACTION_EDIT_UNINDENT, ACTION_EDIT_TOGGLE_LINE_COMMENT, ACTION_EDIT_TOGGLE_BLOCK_COMMENT, ACTION_DEBUG_START); editItem.add(ACTION_EDIT_PREFERENCES); mainMenuItems.add(editItem); @@ -141,15 +165,25 @@ class EditFrame : AppFrame { string source = toUTF8(dsource); try { Widget w = parseML(source); - statusLine.setStatusText("No errors"d); + if (statusLine) + statusLine.setStatusText("No errors"d); _preview.contentWidget = w; } catch (ParserException e) { - statusLine.setStatusText(toUTF32("ERROR: " ~ e.msg)); - _editor.setCaretPos(e.line, e.pos); + if (statusLine) + statusLine.setStatusText(toUTF32("ERROR: " ~ e.msg)); + _editor.setCaretPos(e.line + 1, e.pos); + string msg = "\n" ~ e.msg ~ "\n"; + msg = replaceFirst(msg, " near `", "\nnear `"); + TextWidget w = new MultilineTextWidget(null, toUTF32(msg)); + w.padding = 10; + w.margins = 10; + w.maxLines = 10; + w.backgroundColor = 0xC0FF8080; + _preview.contentWidget = w; } } - protected SourceEdit _editor; + protected DMLSourceEdit _editor; protected ScrollWidget _preview; /// create app body widget override protected Widget createBody() { @@ -159,7 +193,7 @@ class EditFrame : AppFrame { HorizontalLayout hlayout = new HorizontalLayout(); hlayout.layoutWidth = FILL_PARENT; hlayout.layoutHeight = FILL_PARENT; - _editor = new SourceEdit(); + _editor = new DMLSourceEdit(); hlayout.addChild(_editor); _editor.text = q{ VerticalLayout { @@ -181,8 +215,8 @@ VerticalLayout { } CheckBox{ id: cb1; text: "Some checkbox" } HorizontalLayout { - RadioButton{ id: rb1; text: "Radio Button 1" } - RadioButton{ id: rb1; text: "Radio Button 2" } + RadioButton { id: rb1; text: "Radio Button 1" } + RadioButton { id: rb1; text: "Radio Button 2" } } } }; @@ -212,25 +246,6 @@ extern (C) int UIAppMain(string[] args) { // create some widget to show in window window.mainWidget = new EditFrame(); - /* - parseML(q{ - VerticalLayout { - id: vlayout - margins: Rect { left: 5; right: 3; top: 2; bottom: 4 } - padding: Rect { 5, 4, 3, 2 } // same as Rect { left: 5; top: 4; right: 3; bottom: 2 } - TextWidget { - id: myLabel1 - text: "Some text"; padding: 5 - enabled: false - } - TextWidget { - id: myLabel2 - text: SOME_TEXT_RESOURCE_ID; margins: 5 - enabled: true - } - } - }); - */ // show window window.show(); diff --git a/examples/dmledit/views/res/i18n/en.ini b/examples/dmledit/views/res/i18n/en.ini index 55e3afdc..576671ef 100644 --- a/examples/dmledit/views/res/i18n/en.ini +++ b/examples/dmledit/views/res/i18n/en.ini @@ -29,6 +29,7 @@ MENU_WINDOW_PREFERENCES=&Preferences MENU_HELP=&Help MENU_HELP_VIEW_HELP=&View help MENU_HELP_ABOUT=&About +MENU_DEBUG_UPDATE_PREVIEW=Update Preview TAB_LONG_LIST=Long list TAB_BUTTONS=Buttons diff --git a/src/dlangui/core/dmlhighlight.d b/src/dlangui/core/dmlhighlight.d new file mode 100644 index 00000000..23b3381d --- /dev/null +++ b/src/dlangui/core/dmlhighlight.d @@ -0,0 +1,664 @@ +module dlangui.core.dmlhighlight; + +import dlangui.core.parser; +import dlangui.core.editable; +import dlangui.core.linestream; +import dlangui.core.textsource; +import dlangui.core.logger; + +class DMLSyntaxSupport : SyntaxSupport { + + EditableContent _content; + SourceFile _file; + this (string filename) { + _file = new SourceFile(filename); + } + + TokenPropString[] _props; + + /// returns editable content + @property EditableContent content() { return _content; } + /// set editable content + @property SyntaxSupport content(EditableContent content) { + _content = content; + return this; + } + + private enum BracketMatch { + CONTINUE, + FOUND, + ERROR + } + private static struct BracketStack { + dchar[] buf; + int pos; + bool reverse; + void init(bool reverse) { + this.reverse = reverse; + pos = 0; + } + void push(dchar ch) { + if (buf.length <= pos) + buf.length = pos + 16; + buf[pos++] = ch; + } + dchar pop() { + if (pos <= 0) + return 0; + return buf[--pos]; + } + BracketMatch process(dchar ch) { + if (reverse) { + if (isCloseBracket(ch)) { + push(ch); + return BracketMatch.CONTINUE; + } else { + if (pop() != pairedBracket(ch)) + return BracketMatch.ERROR; + if (pos == 0) + return BracketMatch.FOUND; + return BracketMatch.CONTINUE; + } + } else { + if (isOpenBracket(ch)) { + push(ch); + return BracketMatch.CONTINUE; + } else { + if (pop() != pairedBracket(ch)) + return BracketMatch.ERROR; + if (pos == 0) + return BracketMatch.FOUND; + return BracketMatch.CONTINUE; + } + } + } + } + BracketStack _bracketStack; + static bool isBracket(dchar ch) { + return pairedBracket(ch) != 0; + } + static dchar pairedBracket(dchar ch) { + switch (ch) { + case '(': + return ')'; + case ')': + return '('; + case '{': + return '}'; + case '}': + return '{'; + case '[': + return ']'; + case ']': + return '['; + default: + return 0; // not a bracket + } + } + static bool isOpenBracket(dchar ch) { + switch (ch) { + case '(': + case '{': + case '[': + return true; + default: + return false; + } + } + static bool isCloseBracket(dchar ch) { + switch (ch) { + case ')': + case '}': + case ']': + return true; + default: + return false; + } + } + + protected dchar nextBracket(int dir, ref TextPosition p) { + for (;;) { + TextPosition oldpos = p; + p = dir < 0 ? _content.prevCharPos(p) : _content.nextCharPos(p); + if (p == oldpos) + return 0; + auto prop = _content.tokenProp(p); + if (tokenCategory(prop) == TokenCategory.Op) { + dchar ch = _content[p]; + if (isBracket(ch)) + return ch; + } + } + } + + /// returns paired bracket {} () [] for char at position p, returns paired char position or p if not found or not bracket + override TextPosition findPairedBracket(TextPosition p) { + if (p.line < 0 || p.line >= content.length) + return p; + dstring s = content.line(p.line); + if (p.pos < 0 || p.pos >= s.length) + return p; + dchar ch = content[p]; + dchar paired = pairedBracket(ch); + if (!paired) + return p; + TextPosition startPos = p; + int dir = isOpenBracket(ch) ? 1 : -1; + _bracketStack.init(dir < 0); + _bracketStack.process(ch); + for (;;) { + ch = nextBracket(dir, p); + if (!ch) // no more brackets + return startPos; + auto match = _bracketStack.process(ch); + if (match == BracketMatch.FOUND) + return p; + if (match == BracketMatch.ERROR) + return startPos; + // continue + } + } + + + /// return true if toggle line comment is supported for file type + override @property bool supportsToggleLineComment() { + return true; + } + + /// return true if can toggle line comments for specified text range + override bool canToggleLineComment(TextRange range) { + TextRange r = content.fullLinesRange(range); + if (isInsideBlockComment(r.start) || isInsideBlockComment(r.end)) + return false; + return true; + } + + protected bool isLineComment(dstring s) { + for (int i = 0; i < cast(int)s.length - 1; i++) { + if (s[i] == '/' && s[i + 1] == '/') + return true; + else if (s[i] != ' ' && s[i] != '\t') + return false; + } + return false; + } + + protected dstring commentLine(dstring s, int commentX) { + dchar[] res; + int x = 0; + bool commented = false; + for (int i = 0; i < s.length; i++) { + dchar ch = s[i]; + if (ch == '\t') { + int newX = (x + _content.tabSize) / _content.tabSize * _content.tabSize; + if (!commented && newX >= commentX) { + commented = true; + if (newX != commentX) { + // replace tab with space + for (; x <= commentX; x++) + res ~= ' '; + } else { + res ~= ch; + x = newX; + } + res ~= "//"d; + x += 2; + } else { + res ~= ch; + x = newX; + } + } else { + if (!commented && x == commentX) { + commented = true; + res ~= "//"d; + res ~= ch; + x += 3; + } else { + res ~= ch; + x++; + } + } + } + if (!commented) { + for (; x < commentX; x++) + res ~= ' '; + res ~= "//"d; + } + return cast(dstring)res; + } + + /// remove single line comment from beginning of line + protected dstring uncommentLine(dstring s) { + int p = -1; + for (int i = 0; i < cast(int)s.length - 1; i++) { + if (s[i] == '/' && s[i + 1] == '/') { + p = i; + break; + } + } + if (p < 0) + return s; + s = s[0..p] ~ s[p + 2 .. $]; + for (int i = 0; i < s.length; i++) { + if (s[i] != ' ' && s[i] != '\t') { + return s; + } + } + return null; + } + + /// searches for neares token start before or equal to position + protected TextPosition tokenStart(TextPosition pos) { + TextPosition p = pos; + for (;;) { + TextPosition prevPos = content.prevCharPos(p); + if (p == prevPos) + return p; // begin of file + TokenProp prop = content.tokenProp(p); + TokenProp prevProp = content.tokenProp(prevPos); + if (prop && prop != prevProp) + return p; + p = prevPos; + } + } + + static struct TokenWithRange { + Token token; + TextRange range; + @property string toString() { + return token.toString ~ range.toString; + } + } + + protected Token[] _tokens; + protected int _tokenIndex; + + protected bool initTokenizer() { + _tokens = tokenizeML(content.lines); + _tokenIndex = 0; + return true; + } + + protected TokenWithRange nextToken() { + TokenWithRange res; + if (_tokenIndex < _tokens.length) { + res.range.start = TextPosition(_tokens[_tokenIndex].line, _tokens[_tokenIndex].pos); + if (_tokenIndex + 1 < _tokens.length) + res.range.end = TextPosition(_tokens[_tokenIndex + 1].line, _tokens[_tokenIndex + 1].pos); + else + res.range.end = content.endOfFile(); + res.token = _tokens[_tokenIndex]; + _tokenIndex++; + } else { + res.range.end = res.range.start = content.endOfFile(); + } + return res; + } + + protected TokenWithRange getPositionToken(TextPosition pos) { + initTokenizer(); + for (;;) { + TokenWithRange tokenRange = nextToken(); + //Log.d("read token: ", tokenRange); + if (tokenRange.token.type == TokenType.eof) { + //Log.d("end of file"); + return tokenRange; + } + if (pos >= tokenRange.range.start && pos < tokenRange.range.end) { + //Log.d("found: ", pos, " in ", tokenRange); + return tokenRange; + } + } + } + + protected TokenWithRange[] getRangeTokens(TextRange range) { + TokenWithRange[] res; + initTokenizer(); + for (;;) { + TokenWithRange tokenRange = nextToken(); + //Log.d("read token: ", tokenRange); + if (tokenRange.token.type == TokenType.eof) { + return res; + } + if (tokenRange.range.intersects(range)) { + //Log.d("found: ", pos, " in ", tokenRange); + res ~= tokenRange; + } + } + } + + protected bool isInsideBlockComment(TextPosition pos) { + TokenWithRange tokenRange = getPositionToken(pos); + if (tokenRange.token.type == TokenType.comment && tokenRange.token.isMultilineComment) + return pos > tokenRange.range.start && pos < tokenRange.range.end; + return false; + } + + /// toggle line comments for specified text range + override void toggleLineComment(TextRange range, Object source) { + TextRange r = content.fullLinesRange(range); + if (isInsideBlockComment(r.start) || isInsideBlockComment(r.end)) + return; + int lineCount = r.end.line - r.start.line; + bool noEolAtEndOfRange = false; + if (lineCount == 0 || r.end.pos > 0) { + noEolAtEndOfRange = true; + lineCount++; + } + int minLeftX = -1; + bool hasComments = false; + bool hasNoComments = false; + bool hasNonEmpty = false; + dstring[] srctext; + dstring[] dsttext; + for (int i = 0; i < lineCount; i++) { + int lineIndex = r.start.line + i; + dstring s = content.line(lineIndex); + srctext ~= s; + TextLineMeasure m = content.measureLine(lineIndex); + if (!m.empty) { + if (minLeftX < 0 || minLeftX > m.firstNonSpaceX) + minLeftX = m.firstNonSpaceX; + hasNonEmpty = true; + if (isLineComment(s)) + hasComments = true; + else + hasNoComments = true; + } + } + if (minLeftX < 0) + minLeftX = 0; + if (hasNoComments || !hasComments) { + // comment + for (int i = 0; i < lineCount; i++) { + dsttext ~= commentLine(srctext[i], minLeftX); + } + if (!noEolAtEndOfRange) + dsttext ~= ""d; + EditOperation op = new EditOperation(EditAction.Replace, r, dsttext); + _content.performOperation(op, source); + } else { + // uncomment + for (int i = 0; i < lineCount; i++) { + dsttext ~= uncommentLine(srctext[i]); + } + if (!noEolAtEndOfRange) + dsttext ~= ""d; + EditOperation op = new EditOperation(EditAction.Replace, r, dsttext); + _content.performOperation(op, source); + } + } + + /// return true if toggle block comment is supported for file type + override @property bool supportsToggleBlockComment() { + return true; + } + /// return true if can toggle block comments for specified text range + override bool canToggleBlockComment(TextRange range) { + TokenWithRange startToken = getPositionToken(range.start); + TokenWithRange endToken = getPositionToken(range.end); + //Log.d("canToggleBlockComment: startToken=", startToken, " endToken=", endToken); + if (startToken.range == endToken.range && startToken.token.isMultilineComment) { + //Log.d("canToggleBlockComment: can uncomment"); + return true; + } + if (range.empty) + return false; + TokenWithRange[] tokens = getRangeTokens(range); + foreach(ref t; tokens) { + if (t.token.type == TokenType.comment) { + if (t.token.isMultilineComment) { + // disable until nested comments support is implemented + return false; + } else { + // single line comment + if (t.range.isInside(range.start) || t.range.isInside(range.end)) + return false; + } + } + } + return true; + } + /// toggle block comments for specified text range + override void toggleBlockComment(TextRange srcrange, Object source) { + TokenWithRange startToken = getPositionToken(srcrange.start); + TokenWithRange endToken = getPositionToken(srcrange.end); + if (startToken.range == endToken.range && startToken.token.isMultilineComment) { + TextRange range = startToken.range; + dstring[] dsttext; + for (int i = range.start.line; i <= range.end.line; i++) { + dstring s = content.line(i); + int charsRemoved = 0; + int minp = 0; + if (i == range.start.line) { + int maxp = content.lineLength(range.start.line); + if (i == range.end.line) + maxp = range.end.pos - 2; + charsRemoved = 2; + for (int j = range.start.pos + charsRemoved; j < maxp; j++) { + if (s[j] != s[j - 1]) + break; + charsRemoved++; + } + //Log.d("line before removing start of comment:", s); + s = s[range.start.pos + charsRemoved .. $]; + //Log.d("line after removing start of comment:", s); + charsRemoved += range.start.pos; + } + if (i == range.end.line) { + int endp = range.end.pos; + if (charsRemoved > 0) + endp -= charsRemoved; + int endRemoved = 2; + for (int j = endp - endRemoved; j >= 0; j--) { + if (s[j] != s[j + 1]) + break; + endRemoved++; + } + //Log.d("line before removing end of comment:", s); + s = s[0 .. endp - endRemoved]; + //Log.d("line after removing end of comment:", s); + } + dsttext ~= s; + } + EditOperation op = new EditOperation(EditAction.Replace, range, dsttext); + _content.performOperation(op, source); + return; + } else { + if (srcrange.empty) + return; + TokenWithRange[] tokens = getRangeTokens(srcrange); + foreach(ref t; tokens) { + if (t.token.type == TokenType.comment) { + if (t.token.isMultilineComment) { + // disable until nested comments support is implemented + return; + } else { + // single line comment + if (t.range.isInside(srcrange.start) || t.range.isInside(srcrange.end)) + return; + } + } + } + dstring[] dsttext; + for (int i = srcrange.start.line; i <= srcrange.end.line; i++) { + dstring s = content.line(i); + int charsAdded = 0; + if (i == srcrange.start.line) { + int p = srcrange.start.pos; + if (p < s.length) { + s = s[p .. $]; + charsAdded = -p; + } else { + charsAdded = -(cast(int)s.length); + s = null; + } + s = "/*" ~ s; + charsAdded += 2; + } + if (i == srcrange.end.line) { + int p = srcrange.end.pos + charsAdded; + s = p > 0 ? s[0..p] : null; + s ~= "*/"; + } + dsttext ~= s; + } + EditOperation op = new EditOperation(EditAction.Replace, srcrange, dsttext); + _content.performOperation(op, source); + return; + } + + } + + /// categorize characters in content by token types + void updateHighlight(dstring[] lines, TokenPropString[] props, int changeStartLine, int changeEndLine) { + + initTokenizer(); + _props = props; + changeStartLine = 0; + changeEndLine = cast(int)lines.length; + int tokenPos = 0; + int tokenLine = 0; + ubyte category = 0; + try { + for (;;) { + TokenWithRange token = nextToken(); + if (token.token.type == TokenType.eof) { + break; + } + uint newPos = token.range.start.pos; + uint newLine = token.range.start.line; + + // fill with category + for (int i = tokenLine; i <= newLine; i++) { + int start = i > tokenLine ? 0 : tokenPos; + int end = i < newLine ? cast(int)lines[i].length : newPos; + for (int j = start; j < end; j++) { + if (j < _props[i].length) { + _props[i][j] = category; + } + } + } + + // handle token - convert to category + switch(token.token.type) { + case TokenType.comment: + category = TokenCategory.Comment; + break; + case TokenType.ident: + category = TokenCategory.Identifier; + break; + case TokenType.str: + category = TokenCategory.String; + break; + case TokenType.integer: + category = TokenCategory.Integer; + break; + case TokenType.floating: + category = TokenCategory.Float; + break; + case TokenType.error: + category = TokenCategory.Error; + break; + default: + if (token.token.type >= TokenType.colon) + category = TokenCategory.Op; + else + category = 0; + break; + } + tokenPos = newPos; + tokenLine= newLine; + + } + } catch (Exception e) { + Log.e("exception while trying to parse DML source", e); + } + _props = null; + } + + + /// returns true if smart indent is supported + override bool supportsSmartIndents() { + return true; + } + + protected bool _opInProgress; + protected void applyNewLineSmartIndent(EditOperation op, Object source) { + int line = op.newRange.end.line; + if (line == 0) + return; // not for first line + int prevLine = line - 1; + dstring lineText = _content.line(line); + TextLineMeasure lineMeasurement = _content.measureLine(line); + TextLineMeasure prevLineMeasurement = _content.measureLine(prevLine); + while (prevLineMeasurement.empty && prevLine > 0) { + prevLine--; + prevLineMeasurement = _content.measureLine(prevLine); + } + if (lineMeasurement.firstNonSpaceX >= 0 && lineMeasurement.firstNonSpaceX < prevLineMeasurement.firstNonSpaceX) { + dstring prevLineText = _content.line(prevLine); + TokenPropString prevLineTokenProps = _content.lineTokenProps(prevLine); + dchar lastOpChar = 0; + for (int j = prevLineMeasurement.lastNonSpace; j >= 0; j--) { + auto cat = j < prevLineTokenProps.length ? tokenCategory(prevLineTokenProps[j]) : 0; + if (cat == TokenCategory.Op) { + lastOpChar = prevLineText[j]; + break; + } else if (cat != TokenCategory.Comment && cat != TokenCategory.WhiteSpace) { + break; + } + } + int spacex = prevLineMeasurement.firstNonSpaceX; + if (lastOpChar == '{') + spacex = _content.nextTab(spacex); + dstring txt = _content.fillSpace(spacex); + EditOperation op2 = new EditOperation(EditAction.Replace, TextRange(TextPosition(line, 0), TextPosition(line, lineMeasurement.firstNonSpace >= 0 ? lineMeasurement.firstNonSpace : 0)), [txt]); + _opInProgress = true; + _content.performOperation(op2, source); + _opInProgress = false; + } + } + + protected void applyClosingCurlySmartIndent(EditOperation op, Object source) { + int line = op.newRange.end.line; + TextPosition p2 = findPairedBracket(op.newRange.start); + if (p2 == op.newRange.start || p2.line > op.newRange.start.line) + return; + int prevLine = p2.line; + TextLineMeasure lineMeasurement = _content.measureLine(line); + TextLineMeasure prevLineMeasurement = _content.measureLine(prevLine); + if (lineMeasurement.firstNonSpace != op.newRange.start.pos) + return; // not in beginning of line + if (lineMeasurement.firstNonSpaceX >= 0 && lineMeasurement.firstNonSpaceX != prevLineMeasurement.firstNonSpaceX) { + dstring prevLineText = _content.line(prevLine); + TokenPropString prevLineTokenProps = _content.lineTokenProps(prevLine); + int spacex = prevLineMeasurement.firstNonSpaceX; + if (spacex != lineMeasurement.firstNonSpaceX) { + dstring txt = _content.fillSpace(spacex); + txt = txt ~ "}"; + EditOperation op2 = new EditOperation(EditAction.Replace, TextRange(TextPosition(line, 0), TextPosition(line, lineMeasurement.firstNonSpace >= 0 ? lineMeasurement.firstNonSpace + 1 : 0)), [txt]); + _opInProgress = true; + _content.performOperation(op2, source); + _opInProgress = false; + } + } + } + + /// apply smart indent, if supported + override void applySmartIndent(EditOperation op, Object source) { + if (_opInProgress) + return; + if (op.isInsertNewLine) { + // Enter key pressed - new line inserted or splitted + applyNewLineSmartIndent(op, source); + } else if (op.singleChar == '}') { + // } entered - probably need unindent + applyClosingCurlySmartIndent(op, source); + } else if (op.singleChar == '{') { + // { entered - probably need auto closing } + } + } + +} + diff --git a/src/dlangui/core/parser.d b/src/dlangui/core/parser.d index 5765ffa0..803c3137 100644 --- a/src/dlangui/core/parser.d +++ b/src/dlangui/core/parser.d @@ -23,6 +23,7 @@ import dlangui.widgets.metadata; import std.conv : to; import std.algorithm : equal, min, max; import std.utf : toUTF32, toUTF8; +import std.array : join; class ParserException : Exception { protected string _msg; @@ -117,11 +118,23 @@ struct Token { TokenType type; ushort line; ushort pos; + bool multiline; string text; union { int intvalue; double floatvalue; } + public @property string toString() { + if (type == TokenType.integer) + return "" ~ to!string(line) ~ ":" ~ to!string(pos) ~ " " ~ to!string(type) ~ " " ~ to!string(intvalue); + else if (type == TokenType.floating) + return "" ~ to!string(line) ~ ":" ~ to!string(pos) ~ " " ~ to!string(type) ~ " " ~ to!string(floatvalue); + else + return "" ~ to!string(line) ~ ":" ~ to!string(pos) ~ " " ~ to!string(type) ~ " \"" ~ text ~ "\""; + } + @property bool isMultilineComment() { + return type == TokenType.comment && multiline; + } } /// simple tokenizer for DlangUI ML @@ -355,6 +368,7 @@ class Tokenizer { break; } _token.type = TokenType.comment; + _token.multiline = false; return _token; } @@ -371,6 +385,7 @@ class Tokenizer { break; } _token.type = TokenType.comment; + _token.multiline = true; return _token; } @@ -800,3 +815,22 @@ public Widget parseML(string code, string filename = "", Widget context = null) scope(exit) destroy(parser); return parser.parse(); } + +/// tokenize source into array of tokens (excluding EOF) +public Token[] tokenizeML(const(dstring[]) lines) { + string code = toUTF8(join(lines, "\n")); + return tokenizeML(code); +} + +/// tokenize source into array of tokens (excluding EOF) +public Token[] tokenizeML(string code) { + Token[] res; + auto tokenizer = new Tokenizer(code, ""); + for (;;) { + auto token = tokenizer.nextToken(); + if (token.type == TokenType.eof) + break; + res ~= token; + } + return res; +} diff --git a/src/dlangui/core/textsource.d b/src/dlangui/core/textsource.d new file mode 100644 index 00000000..537099bd --- /dev/null +++ b/src/dlangui/core/textsource.d @@ -0,0 +1,115 @@ +module dlangui.core.textsource; + +private import std.utf; +private import std.array; + +/** +* Source file information. +* Even if contains only file name, it's better to use it instead of string - object reference size is twice less than array ref. +*/ +class SourceFile { + protected string _filename; + @property string filename() { return _filename; } + public this(string filename) { + _filename = filename; + } + override @property string toString() { + return _filename; + } +} + +/// source lines for tokenizer +interface SourceLines { + /// source file + @property SourceFile file(); + /// last read line + @property uint line(); + /// source encoding + //@property EncodingType encoding() { return _encoding; } + /// error code + @property int errorCode(); + /// error message + @property string errorMessage(); + /// error line + @property int errorLine(); + /// error position + @property int errorPos(); + /// end of file reached + @property bool eof(); + + /// read line, return null if EOF reached or error occured + dchar[] readLine(); +} + +const TEXT_SOURCE_ERROR_EOF = 1; + +/// Simple text source based on array +class ArraySourceLines : SourceLines { + protected SourceFile _file; + protected uint _line; + protected uint _firstLine; + protected dstring[] _lines; + static __gshared protected dchar[] _emptyLine = ""d.dup; + + this() { + } + + this(dstring[] lines, SourceFile file, uint firstLine = 0) { + init(lines, file, firstLine); + } + + this(string code, string filename) { + _lines = (toUTF32(code)).split("\n"); + _file = new SourceFile(filename); + } + + void close() { + _lines = null; + _line = 0; + _firstLine = 0; + _file = null; + } + + void init(dstring[] lines, SourceFile file, uint firstLine = 0) { + _lines = lines; + _firstLine = firstLine; + _line = 0; + _file = file; + } + + bool reset(int line) { + _line = line; + return true; + } + + /// end of file reached + override @property bool eof() { + return _line >= _lines.length; + } + /// source file + override @property SourceFile file() { return _file; } + /// last read line + override @property uint line() { return _line + _firstLine; } + /// source encoding + //@property EncodingType encoding() { return _encoding; } + /// error code + override @property int errorCode() { return 0; } + /// error message + override @property string errorMessage() { return ""; } + /// error line + override @property int errorLine() { return 0; } + /// error position + override @property int errorPos() { return 0; } + + /// read line, return null if EOF reached or error occured + override dchar[] readLine() { + if (_line < _lines.length) { + if (_lines[_line]) + return cast(dchar[])_lines[_line++]; + _line++; + return _emptyLine; + } + return null; // EOF + } +} + diff --git a/src/dlangui/graphics/resources.d b/src/dlangui/graphics/resources.d index 51c98adc..c93e1740 100644 --- a/src/dlangui/graphics/resources.d +++ b/src/dlangui/graphics/resources.d @@ -824,6 +824,7 @@ class ImageCache { _map[filename] = item; return item.get; } + /// get and cache color transformed image ref DrawBufRef get(string filename, ref ColorTransform transform) { if (transform.empty) diff --git a/src/dlangui/widgets/controls.d b/src/dlangui/widgets/controls.d index 2b69275a..987df3f6 100644 --- a/src/dlangui/widgets/controls.d +++ b/src/dlangui/widgets/controls.d @@ -1050,4 +1050,4 @@ class CanvasWidget : Widget { } import dlangui.widgets.metadata; -mixin(registerWidgets!(Widget, TextWidget, Button, ImageWidget, ImageButton, ImageTextButton, RadioButton, CheckBox, ScrollBar)()); +mixin(registerWidgets!(Widget, TextWidget, MultilineTextWidget, Button, ImageWidget, ImageButton, ImageTextButton, RadioButton, CheckBox, ScrollBar)()); diff --git a/src/dlangui/widgets/widget.d b/src/dlangui/widgets/widget.d index d508a76b..874b8b09 100644 --- a/src/dlangui/widgets/widget.d +++ b/src/dlangui/widgets/widget.d @@ -326,6 +326,12 @@ class Widget { requestLayout(); return this; } + /// set margins for widget with the same value for left, top, right, bottom - override one from style + @property Widget margins(int v) { + ownStyle.margins = Rect(v, v, v, v); + requestLayout(); + return this; + } immutable static int FOCUS_RECT_PADDING = 2; /// get padding (between background bounds and content of widget) @property Rect padding() const { @@ -355,6 +361,12 @@ class Widget { requestLayout(); return this; } + /// set padding for widget to the same value for left, top, right, bottom - override one from style + @property Widget padding(int v) { + ownStyle.padding = Rect(v, v, v, v); + requestLayout(); + return this; + } /// returns background color @property uint backgroundColor() const { return stateStyle.backgroundColor; } /// set background color for widget - override one from style