diff --git a/dlangide.visualdproj b/dlangide.visualdproj index 2277468..08a26d3 100644 --- a/dlangide.visualdproj +++ b/dlangide.visualdproj @@ -209,6 +209,14 @@ + + + + + + + + diff --git a/src/dlangide/builders/extprocess.d b/src/dlangide/builders/extprocess.d index 021d5f9..69b64a1 100644 --- a/src/dlangide/builders/extprocess.d +++ b/src/dlangide/builders/extprocess.d @@ -297,7 +297,7 @@ class ExternalProcess { params ~= _program; params ~= _args; if (!_stderr) - redirect = Redirect.stdout | Redirect.stderrToStdout; //Redirect.stdin | + redirect = Redirect.stdout | Redirect.stderrToStdout | Redirect.stdin; else redirect = Redirect.all; Log.i("Trying to run program ", _program, " with args ", _args); @@ -393,4 +393,14 @@ class ExternalProcess { } return _state; } + + void write(dstring data) { + if(_state == ExternalProcessState.Error || _state == ExternalProcessState.None || _state == ExternalProcessState.Stopped) { + return; + } + else { + _pipes.stdin.write(data); + _pipes.stdin.close(); + } + } } diff --git a/src/dlangide/tools/d/dcdinterface.d b/src/dlangide/tools/d/dcdinterface.d new file mode 100644 index 0000000..f11b7d0 --- /dev/null +++ b/src/dlangide/tools/d/dcdinterface.d @@ -0,0 +1,128 @@ +module dlangide.tools.d.dcdinterface; + +import dlangui.core.logger; + +import dlangide.builders.extprocess; + +import std.typecons; +import std.conv; +import std.string; + +enum DCDResult : int { + DCD_NOT_RUNNING = 0, + SUCCESS, + NO_RESULT, + FAIL, +} +alias ResultSet = Tuple!(DCDResult, "result", dstring[], "output"); + +//Interface to DCD +//TODO: Check if server is running, start server if needed etc. +class DCDInterface { + ExternalProcess dcdProcess; + ProtectedTextStorage stdoutTarget; + this() { + dcdProcess = new ExternalProcess(); + stdoutTarget = new ProtectedTextStorage(); + } + + ResultSet goToDefinition(in dstring content, int index) { + ExternalProcess dcdProcess = new ExternalProcess(); + + ResultSet result; + if(dcdProcess.state != ExternalProcessState.None) { + result.result = DCDResult.FAIL; + return result; + } + + char[][] arguments = ["-l".dup, "-c".dup]; + arguments ~= [to!(char[])(index)]; + ProtectedTextStorage stdoutTarget = new ProtectedTextStorage(); + + dcdProcess.run("dcd-client".dup, arguments, "/usr/bin".dup, stdoutTarget); + dcdProcess.write(content); + dcdProcess.wait(); + + dstring[] output = stdoutTarget.readText.splitLines(); + + if(dcdProcess.poll() == ExternalProcessState.Stopped) { + result.result = DCDResult.SUCCESS; + } + else { + result.result = DCDResult.FAIL; + return result; + } + + if(output.length > 0) { + if(output[0].indexOf("Not Found".dup) == 0) { + result.result = DCDResult.NO_RESULT; + return result; + } + } + + auto split = output[0].indexOf("\t"); + if(split == -1) { + Log.d("DCD output format error."); + result.result = DCDResult.FAIL; + return result; + } + + result.output ~= output[0][0 .. split]; + result.output ~= output[0][split+1 .. $]; + return result; + } + + ResultSet getCompletions(in dstring content, int index) { + ExternalProcess dcdProcess = new ExternalProcess(); + + ResultSet result; + if(dcdProcess.state != ExternalProcessState.None) { + result.result = DCDResult.FAIL; + return result; + } + + char[][] arguments = ["-c".dup]; + arguments ~= [to!(char[])(index)]; + ProtectedTextStorage stdoutTarget = new ProtectedTextStorage(); + + dcdProcess.run("dcd-client".dup, arguments, "/usr/bin".dup, stdoutTarget); + dcdProcess.write(content); + dcdProcess.wait(); + + dstring[] output = stdoutTarget.readText.splitLines(); + + if(dcdProcess.poll() == ExternalProcessState.Stopped) { + result.result = DCDResult.SUCCESS; + } + else { + result.result = DCDResult.FAIL; + return result; + } + + if(output.length == 0) { + result.result = DCDResult.NO_RESULT; + return result; + } + + enum State : int {None = 0, Identifiers, Calltips} + State state = State.None; + foreach(dstring outputLine ; output) { + if(outputLine == "identifiers") { + state = State.Identifiers; + } + else if(outputLine == "calltips") { + state = State.Calltips; + } + else { + auto split = outputLine.indexOf("\t"); + if(split < 0) { + break; + } + if(state == State.Identifiers) { + result.output ~= outputLine[0 .. split]; + } + } + } + return result; + } +} diff --git a/src/dlangide/tools/d/deditortool.d b/src/dlangide/tools/d/deditortool.d new file mode 100644 index 0000000..2a14d6d --- /dev/null +++ b/src/dlangide/tools/d/deditortool.d @@ -0,0 +1,114 @@ +module dlangide.tools.d.deditorTool; + +import dlangide.tools.editorTool; +import dlangide.tools.d.dcdinterface; +import dlangide.ui.dsourceedit; +import dlangui.widgets.editors; +import dlangide.ui.frame; +import std.stdio; +import std.string; +import dlangui.core.logger; + +import std.conv; + +class DEditorTool : EditorTool +{ + + + this(IDEFrame frame) { + _dcd = new DCDInterface(); + super(frame); + } + + override bool goToDefinition(DSourceEdit editor, TextPosition caretPosition) { + + auto byteOffset = caretPositionToByteOffset(editor.text, caretPosition); + ResultSet output = _dcd.goToDefinition(editor.text, byteOffset); + + + switch(output.result) { + //TODO: Show dialog + case DCDResult.FAIL: + case DCDResult.DCD_NOT_RUNNING: + case DCDResult.NO_RESULT: + return false; + case DCDResult.SUCCESS: + auto target = to!int(output.output[1]); + if(output.output[0].indexOf("stdin".dup) != -1) { + Log.d("Declaration is in current file. Jumping to it."); + auto destPos = byteOffsetToCaret(editor.text, target); + editor.setCaretPos(destPos.line,destPos.pos); + } + else { + //Must open file first to get the content for finding the correct caret position. + _frame.openSourceFile(to!string(output.output[0])); + auto destPos = byteOffsetToCaret(_frame.currentEditor.text(), target); + _frame.currentEditor.setCaretPos(destPos.line,destPos.pos); + } + return true; + default: + return false; + } + } + + override dstring[] getCompletions(DSourceEdit editor, TextPosition caretPosition) { + + auto byteOffset = caretPositionToByteOffset(editor.text, caretPosition); + ResultSet output = _dcd.getCompletions(editor.text, byteOffset); + switch(output.result) { + //TODO: Show dialog + case DCDResult.FAIL: + case DCDResult.DCD_NOT_RUNNING: + case DCDResult.NO_RESULT: + case DCDResult.SUCCESS: + default: + return output.output; + } + } + +private: + DCDInterface _dcd; + + int caretPositionToByteOffset(dstring content, TextPosition caretPosition) { + auto line = 0; + auto pos = 0; + auto bytes = 0; + foreach(c; content) { + bytes++; + if(c == '\n') { + line++; + } + if(line == caretPosition.line) { + if(pos == caretPosition.pos) + break; + pos++; + } + } + return bytes; + } + + TextPosition byteOffsetToCaret(dstring content, int byteOffset) { + int bytes = 0; + int line = 0; + int pos = 0; + TextPosition textPos; + foreach(c; content) { + if(bytes == byteOffset) { + //We all good. + textPos.line = line; + textPos.pos = pos; + return textPos; + } + bytes++; + if(c == '\n') + { + line++; + pos = 0; + } + else { + pos++; + } + } + return textPos; + } +} diff --git a/src/dlangide/tools/d/simpledsyntaxhighlighter.d b/src/dlangide/tools/d/simpledsyntaxhighlighter.d new file mode 100644 index 0000000..635c76f --- /dev/null +++ b/src/dlangide/tools/d/simpledsyntaxhighlighter.d @@ -0,0 +1,641 @@ +module dlangide.tools.d.simpledsyntaxhighlighter; + +import dlangui.core.logger; +import dlangui.widgets.editors; +import dlangui.widgets.srcedit; + +import ddc.lexer.textsource; +import ddc.lexer.exceptions; +import ddc.lexer.tokenizer; + +class SimpleDSyntaxHighlighter : SyntaxHighlighter { + + EditableContent _content; + SourceFile _file; + ArraySourceLines _lines; + Tokenizer _tokenizer; + this (string filename) { + _file = new SourceFile(filename); + _lines = new ArraySourceLines(); + _tokenizer = new Tokenizer(_lines); + _tokenizer.errorTolerant = true; + } + + TokenPropString[] _props; + + /// returns editable content + @property EditableContent content() { return _content; } + /// set editable content + @property SyntaxHighlighter 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 TextPosition _lastTokenStart; + protected Token _lastToken; + protected bool initTokenizer(TextPosition startPos) { + const dstring[] lines = content.lines; + _lines.init(cast(dstring[])(lines[startPos.line .. $]), _file, startPos.line); + _tokenizer.init(_lines, startPos.pos); + _lastTokenStart = startPos; + _lastToken = null; + nextToken(); + return true; + } + + protected TokenWithRange nextToken() { + TokenWithRange res; + if (_lastToken && _lastToken.type == TokenType.EOF) { + // end of file + res.range.start = _lastTokenStart; + res.range.end = content.endOfFile(); + res.token = null; + return res; + } + res.range.start = _lastTokenStart; + res.token = _lastToken; + _lastToken = _tokenizer.nextToken(); + if (_lastToken) + _lastToken = _lastToken.clone(); + _lastTokenStart = _lastToken ? TextPosition(_lastToken.line - 1, _lastToken.pos - 1) : content.endOfFile(); + res.range.end = _lastTokenStart; + return res; + } + + protected TokenWithRange getPositionToken(TextPosition pos) { + //Log.d("getPositionToken for ", pos); + TextPosition start = tokenStart(pos); + //Log.d("token start found: ", start); + initTokenizer(start); + for (;;) { + TokenWithRange tokenRange = nextToken(); + //Log.d("read token: ", tokenRange); + if (!tokenRange.token) { + //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; + //Log.d("getPositionToken for ", pos); + TextPosition start = tokenStart(range.start); + //Log.d("token start found: ", start); + initTokenizer(start); + for (;;) { + TokenWithRange tokenRange = nextToken(); + //Log.d("read token: ", tokenRange); + if (!tokenRange.token) { + //Log.d("end of file"); + 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 && 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.token && endToken.token && 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.token && endToken.token && 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) { + //Log.d("updateHighlight"); + long ms0 = currentTimeMillis(); + _props = props; + changeStartLine = 0; + changeEndLine = cast(int)lines.length; + _lines.init(lines[changeStartLine..$], _file, changeStartLine); + _tokenizer.init(_lines); + int tokenPos = 0; + int tokenLine = 0; + ubyte category = 0; + try { + for (;;) { + Token token = _tokenizer.nextToken(); + if (token is null) { + //Log.d("Null token returned"); + break; + } + uint newPos = token.pos - 1; + uint newLine = token.line - 1; + + //Log.d("", tokenLine + 1, ":", tokenPos + 1, " \t", token.line, ":", token.pos, "\t", token.toString); + if (token.type == TokenType.EOF) { + //Log.d("EOF token"); + } + + // 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.type) { + case TokenType.COMMENT: + category = token.isDocumentationComment ? TokenCategory.Comment_Documentation : TokenCategory.Comment; + break; + case TokenType.KEYWORD: + category = TokenCategory.Keyword; + break; + case TokenType.IDENTIFIER: + category = TokenCategory.Identifier; + break; + case TokenType.STRING: + category = TokenCategory.String; + break; + case TokenType.CHARACTER: + category = TokenCategory.Character; + break; + case TokenType.INTEGER: + category = TokenCategory.Integer; + break; + case TokenType.FLOAT: + category = TokenCategory.Float; + break; + case TokenType.OP: + category = TokenCategory.Op; + break; + case TokenType.INVALID: + switch (token.invalidTokenType) { + case TokenType.IDENTIFIER: + category = TokenCategory.Error_InvalidIdentifier; + break; + case TokenType.STRING: + category = TokenCategory.Error_InvalidString; + break; + case TokenType.COMMENT: + category = TokenCategory.Error_InvalidComment; + break; + case TokenType.OP: + category = TokenCategory.Error_InvalidOp; + break; + case TokenType.FLOAT: + case TokenType.INTEGER: + category = TokenCategory.Error_InvalidNumber; + break; + default: + category = TokenCategory.Error; + break; + } + break; + default: + category = 0; + break; + } + tokenPos = newPos; + tokenLine= newLine; + + if (token.type == TokenType.EOF) { + //Log.d("EOF token"); + break; + } + } + } catch (Exception e) { + Log.e("exception while trying to parse D source", e); + } + _lines.close(); + _props = null; + long elapsed = currentTimeMillis() - ms0; + if (elapsed > 20) + Log.d("updateHighlight took ", elapsed, "ms"); + } +} + diff --git a/src/dlangide/tools/editortool.d b/src/dlangide/tools/editortool.d new file mode 100644 index 0000000..c9fa8db --- /dev/null +++ b/src/dlangide/tools/editortool.d @@ -0,0 +1,23 @@ +module dlangide.tools.editorTool; + + + +import dlangui.widgets.editors; +import dlangui.core.types; +import dlangide.ui.frame; +import dlangide.ui.dsourceedit; + +public import dlangide.tools.d.deditorTool; + +class EditorTool +{ + this(IDEFrame frame) { + _frame = frame; + } + //Since files might be unsaved, we must send all the text content. + abstract bool goToDefinition(DSourceEdit editor, TextPosition caretPosition); + abstract dstring[] getCompletions(DSourceEdit editor, TextPosition caretPosition); + + protected IDEFrame _frame; + +} diff --git a/src/dlangide/ui/commands.d b/src/dlangide/ui/commands.d index bbe2971..b0b855f 100644 --- a/src/dlangide/ui/commands.d +++ b/src/dlangide/ui/commands.d @@ -40,6 +40,9 @@ enum IDEActions : int { ProjectFolderRemoveItem, ProjectFolderOpenItem, ProjectFolderRenameItem, + GoToDefinition, + GetCompletionSuggestions, + InsertCompletion, } __gshared static this() { @@ -92,4 +95,5 @@ const Action ACTION_HELP_ABOUT = new Action(IDEActions.HelpAbout, "MENU_HELP_ABO const Action ACTION_WINDOW_CLOSE_ALL_DOCUMENTS = new Action(IDEActions.WindowCloseAllDocuments, "MENU_WINDOW_CLOSE_ALL_DOCUMENTS"c); const Action ACTION_CREATE_NEW_WORKSPACE = new Action(IDEActions.CreateNewWorkspace, "Create new workspace"d); const Action ACTION_ADD_TO_CURRENT_WORKSPACE = new Action(IDEActions.AddToCurrentWorkspace, "Add to current workspace"d); - +const Action ACTION_GO_TO_DEFINITION = new Action(IDEActions.GoToDefinition, "GO_TO_DEFINITION"c, ""c, KeyCode.KEY_G, KeyFlag.Control); +const Action ACTION_GET_COMPLETIONS = new Action(IDEActions.GetCompletionSuggestions, "SHOW_COMPLETIONS"c, ""c, KeyCode.KEY_G, KeyFlag.Control|KeyFlag.Shift); \ No newline at end of file diff --git a/src/dlangide/ui/dsourceedit.d b/src/dlangide/ui/dsourceedit.d index f01b33b..ce53896 100644 --- a/src/dlangide/ui/dsourceedit.d +++ b/src/dlangide/ui/dsourceedit.d @@ -4,6 +4,7 @@ import dlangui.core.logger; import dlangui.widgets.editors; import dlangui.widgets.srcedit; import dlangui.widgets.menu; +import dlangui.widgets.popup; import ddc.lexer.textsource; import ddc.lexer.exceptions; @@ -12,6 +13,7 @@ import ddc.lexer.tokenizer; import dlangide.workspace.workspace; import dlangide.workspace.project; import dlangide.ui.commands; +import dlangide.tools.d.simpledsyntaxhighlighter; import std.algorithm; @@ -30,7 +32,7 @@ class DSourceEdit : SourceEdit { setTokenHightlightColor(TokenCategory.Comment_Documentation, 0x206000); //setTokenHightlightColor(TokenCategory.Identifier, 0x206000); // no colors 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); + 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_GET_COMPLETIONS); popupMenu = editPopupItem; showIcons = true; showFolding = true; @@ -79,6 +81,11 @@ class DSourceEdit : SourceEdit { case IDEActions.FileSave: save(); return true; + case IDEActions.InsertCompletion: + EditOperation edit = new EditOperation(EditAction.Replace, getCaretPosition, a.label); + _content.performOperation(edit, this); + setFocus(); + return true; default: break; } @@ -86,6 +93,36 @@ class DSourceEdit : SourceEdit { return super.handleAction(a); } + void showCompletionPopup(dstring[] suggestions) { + + if(suggestions.length == 0) { + return; + } + + MenuItem completionPopupItems = new MenuItem(null); + //Add all the suggestions. + foreach(int i, dstring suggestion ; suggestions) { + auto action = new Action(IDEActions.InsertCompletion, suggestion); + completionPopupItems.add(action); + } + completionPopupItems.updateActionState(this); + + PopupMenu popupMenu = new PopupMenu(completionPopupItems); + popupMenu.onMenuItemActionListener = this; + popupMenu.maxHeight(400); + popupMenu.selectItem(0); + + PopupWidget popup = window.showPopup(popupMenu, this, PopupAlign.Point | PopupAlign.Right, textPosToClient(_caretPos).left + left + _leftPaneWidth, textPosToClient(_caretPos).top + top + margins.top); + popup.setFocus(); + popup.flags = PopupFlags.CloseOnClickOutside; + + Log.d("Showing popup at ", textPosToClient(_caretPos).left, " ", textPosToClient(_caretPos).top); + } + + TextPosition getCaretPosition() { + return _caretPos; + } + /// change caret position and ensure it is visible void setCaretPos(int line, int column) { @@ -94,636 +131,3 @@ class DSourceEdit : SourceEdit { ensureCaretVisible(); } } - - - -class SimpleDSyntaxHighlighter : SyntaxHighlighter { - - EditableContent _content; - SourceFile _file; - ArraySourceLines _lines; - Tokenizer _tokenizer; - this (string filename) { - _file = new SourceFile(filename); - _lines = new ArraySourceLines(); - _tokenizer = new Tokenizer(_lines); - _tokenizer.errorTolerant = true; - } - - TokenPropString[] _props; - - /// returns editable content - @property EditableContent content() { return _content; } - /// set editable content - @property SyntaxHighlighter 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 TextPosition _lastTokenStart; - protected Token _lastToken; - protected bool initTokenizer(TextPosition startPos) { - const dstring[] lines = content.lines; - _lines.init(cast(dstring[])(lines[startPos.line .. $]), _file, startPos.line); - _tokenizer.init(_lines, startPos.pos); - _lastTokenStart = startPos; - _lastToken = null; - nextToken(); - return true; - } - - protected TokenWithRange nextToken() { - TokenWithRange res; - if (_lastToken && _lastToken.type == TokenType.EOF) { - // end of file - res.range.start = _lastTokenStart; - res.range.end = content.endOfFile(); - res.token = null; - return res; - } - res.range.start = _lastTokenStart; - res.token = _lastToken; - _lastToken = _tokenizer.nextToken(); - if (_lastToken) - _lastToken = _lastToken.clone(); - _lastTokenStart = _lastToken ? TextPosition(_lastToken.line - 1, _lastToken.pos - 1) : content.endOfFile(); - res.range.end = _lastTokenStart; - return res; - } - - protected TokenWithRange getPositionToken(TextPosition pos) { - //Log.d("getPositionToken for ", pos); - TextPosition start = tokenStart(pos); - //Log.d("token start found: ", start); - initTokenizer(start); - for (;;) { - TokenWithRange tokenRange = nextToken(); - //Log.d("read token: ", tokenRange); - if (!tokenRange.token) { - //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; - //Log.d("getPositionToken for ", pos); - TextPosition start = tokenStart(range.start); - //Log.d("token start found: ", start); - initTokenizer(start); - for (;;) { - TokenWithRange tokenRange = nextToken(); - //Log.d("read token: ", tokenRange); - if (!tokenRange.token) { - //Log.d("end of file"); - 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 && 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.token && endToken.token && 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.token && endToken.token && 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) { - //Log.d("updateHighlight"); - long ms0 = currentTimeMillis(); - _props = props; - changeStartLine = 0; - changeEndLine = cast(int)lines.length; - _lines.init(lines[changeStartLine..$], _file, changeStartLine); - _tokenizer.init(_lines); - int tokenPos = 0; - int tokenLine = 0; - ubyte category = 0; - try { - for (;;) { - Token token = _tokenizer.nextToken(); - if (token is null) { - //Log.d("Null token returned"); - break; - } - uint newPos = token.pos - 1; - uint newLine = token.line - 1; - - //Log.d("", tokenLine + 1, ":", tokenPos + 1, " \t", token.line, ":", token.pos, "\t", token.toString); - if (token.type == TokenType.EOF) { - //Log.d("EOF token"); - } - - // 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.type) { - case TokenType.COMMENT: - category = token.isDocumentationComment ? TokenCategory.Comment_Documentation : TokenCategory.Comment; - break; - case TokenType.KEYWORD: - category = TokenCategory.Keyword; - break; - case TokenType.IDENTIFIER: - category = TokenCategory.Identifier; - break; - case TokenType.STRING: - category = TokenCategory.String; - break; - case TokenType.CHARACTER: - category = TokenCategory.Character; - break; - case TokenType.INTEGER: - category = TokenCategory.Integer; - break; - case TokenType.FLOAT: - category = TokenCategory.Float; - break; - case TokenType.OP: - category = TokenCategory.Op; - break; - case TokenType.INVALID: - switch (token.invalidTokenType) { - case TokenType.IDENTIFIER: - category = TokenCategory.Error_InvalidIdentifier; - break; - case TokenType.STRING: - category = TokenCategory.Error_InvalidString; - break; - case TokenType.COMMENT: - category = TokenCategory.Error_InvalidComment; - break; - case TokenType.OP: - category = TokenCategory.Error_InvalidOp; - break; - case TokenType.FLOAT: - case TokenType.INTEGER: - category = TokenCategory.Error_InvalidNumber; - break; - default: - category = TokenCategory.Error; - break; - } - break; - default: - category = 0; - break; - } - tokenPos = newPos; - tokenLine= newLine; - - if (token.type == TokenType.EOF) { - //Log.d("EOF token"); - break; - } - } - } catch (Exception e) { - Log.e("exception while trying to parse D source", e); - } - _lines.close(); - _props = null; - long elapsed = currentTimeMillis() - ms0; - if (elapsed > 20) - Log.d("updateHighlight took ", elapsed, "ms"); - } -} diff --git a/src/dlangide/ui/frame.d b/src/dlangide/ui/frame.d index 93c7280..af2d832 100644 --- a/src/dlangide/ui/frame.d +++ b/src/dlangide/ui/frame.d @@ -10,6 +10,7 @@ import dlangui.widgets.appframe; import dlangui.widgets.docks; import dlangui.widgets.toolbars; import dlangui.widgets.combobox; +import dlangui.widgets.popup; import dlangui.dialogs.dialog; import dlangui.dialogs.filedlg; import dlangui.core.stdaction; @@ -22,6 +23,7 @@ import dlangide.ui.homescreen; import dlangide.workspace.workspace; import dlangide.workspace.project; import dlangide.builders.builder; +import dlangide.tools.editorTool; import std.conv; import std.utf; @@ -60,6 +62,7 @@ class IDEFrame : AppFrame { OutputPanel _logPanel; DockHost _dockHost; TabWidget _tabs; + EditorTool _editorTool; dstring frameWindowCaptionSuffix = "DLangIDE"d; @@ -72,6 +75,7 @@ class IDEFrame : AppFrame { override protected void init() { _appName = "dlangide"; + _editorTool = new DEditorTool(this); super.init(); } @@ -333,6 +337,9 @@ class IDEFrame : AppFrame { editItem.add(ACTION_EDIT_PREFERENCES); + MenuItem navItem = new MenuItem(new Action(21, "MENU_NAVIGATE")); + navItem.add(ACTION_GO_TO_DEFINITION, ACTION_GET_COMPLETIONS); + MenuItem projectItem = new MenuItem(new Action(21, "MENU_PROJECT")); projectItem.add(ACTION_PROJECT_SET_STARTUP, ACTION_PROJECT_REFRESH, ACTION_PROJECT_UPDATE_DEPENDENCIES, ACTION_PROJECT_SETTINGS); @@ -354,6 +361,7 @@ class IDEFrame : AppFrame { mainMenuItems.add(fileItem); mainMenuItems.add(editItem); mainMenuItems.add(projectItem); + mainMenuItems.add(navItem); mainMenuItems.add(buildItem); mainMenuItems.add(debugItem); //mainMenuItems.add(viewItem); @@ -538,6 +546,15 @@ class IDEFrame : AppFrame { }; dlg.show(); return true; + case IDEActions.GoToDefinition: + Log.d("Trying to go to definition."); + _editorTool.goToDefinition(currentEditor(), currentEditor.getCaretPosition()); + return true; + case IDEActions.GetCompletionSuggestions: + Log.d("Getting auto completion suggestions."); + auto results = _editorTool.getCompletions(currentEditor, currentEditor.getCaretPosition); + currentEditor.showCompletionPopup(results); + return true; default: return super.handleAction(a); } diff --git a/views/res/i18n/en.ini b/views/res/i18n/en.ini index a069f2f..1939fe4 100644 --- a/views/res/i18n/en.ini +++ b/views/res/i18n/en.ini @@ -55,6 +55,10 @@ MENU_WINDOW_CLOSE_ALL_DOCUMENTS=Close All Documents MENU_HELP=&HELP MENU_HELP_VIEW_HELP=&View help MENU_HELP_ABOUT=&About +GO_TO_DEFINITION=Go To Definition +SHOW_COMPLETIONS=Get Autocompletions +MENU_NAVIGATE=NAVIGATE + TAB_LONG_LIST=Long list TAB_BUTTONS=Buttons