diff --git a/main.go b/main.go index c75c417..31be570 100644 --- a/main.go +++ b/main.go @@ -172,6 +172,7 @@ func main() { http.HandleFunc(conf.Wide.Context+"/playground/build", handlerWrapper(playground.BuildHandler)) http.HandleFunc(conf.Wide.Context+"/playground/run", handlerWrapper(playground.RunHandler)) http.HandleFunc(conf.Wide.Context+"/playground/stop", handlerWrapper(playground.StopHandler)) + http.HandleFunc(conf.Wide.Context+"/playground/autocomplete", handlerWrapper(playground.AutocompleteHandler)) logger.Infof("Wide is running [%s]", conf.Wide.Server+conf.Wide.Context) diff --git a/playground/autocomplete.go b/playground/autocomplete.go new file mode 100644 index 0000000..2734250 --- /dev/null +++ b/playground/autocomplete.go @@ -0,0 +1,97 @@ +// Copyright (c) 2014-2015, b3log.org +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package playground + +import ( + "bytes" + "encoding/json" + "net/http" + "os/exec" + "strconv" + "strings" + + "github.com/b3log/wide/session" + "github.com/b3log/wide/util" +) + +// AutocompleteHandler handles request of code autocompletion. +func AutocompleteHandler(w http.ResponseWriter, r *http.Request) { + var args map[string]interface{} + + if err := json.NewDecoder(r.Body).Decode(&args); err != nil { + logger.Error(err) + http.Error(w, err.Error(), 500) + + return + } + + session, _ := session.HTTPSession.Get(r, "wide-session") + if session.IsNew { + http.Error(w, "Forbidden", http.StatusForbidden) + + return + } + + code := args["code"].(string) + line := int(args["cursorLine"].(float64)) + ch := int(args["cursorCh"].(float64)) + + offset := getCursorOffset(code, line, ch) + + argv := []string{"-f=json", "autocomplete", strconv.Itoa(offset)} + gocode := util.Go.GetExecutableInGOBIN("gocode") + cmd := exec.Command(gocode, argv...) + + stdin, _ := cmd.StdinPipe() + stdin.Write([]byte(code)) + stdin.Close() + + output, err := cmd.CombinedOutput() + if nil != err { + logger.Error(err) + http.Error(w, err.Error(), 500) + + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write(output) +} + +// getCursorOffset calculates the cursor offset. +// +// line is the line number, starts with 0 that means the first line +// ch is the column number, starts with 0 that means the first column +func getCursorOffset(code string, line, ch int) (offset int) { + lines := strings.Split(code, "\n") + + // calculate sum length of lines before + for i := 0; i < line; i++ { + offset += len(lines[i]) + } + + // calculate length of the current line and column + curLine := lines[line] + var buffer bytes.Buffer + r := []rune(curLine) + for i := 0; i < ch; i++ { + buffer.WriteString(string(r[i])) + } + + offset += len(buffer.String()) // append length of current line + offset += line // append number of '\n' + + return offset +} diff --git a/static/js/playground.js b/static/js/playground.js index 1a12e9c..0d2d034 100644 --- a/static/js/playground.js +++ b/static/js/playground.js @@ -58,6 +58,109 @@ var playground = { }); }, init: function () { + CodeMirror.registerHelper("hint", "go", function (editor) { + var word = /[\w$]+/; + + var cur = editor.getCursor(), curLine = editor.getLine(cur.line); + + var start = cur.ch, end = start; + while (end < curLine.length && word.test(curLine.charAt(end))) { + ++end; + } + while (start && word.test(curLine.charAt(start - 1))) { + --start; + } + + var request = newWideRequest(); + request.code = editor.getValue(); + request.cursorLine = cur.line; + request.cursorCh = cur.ch; + + var autocompleteHints = []; + + $.ajax({ + async: false, // 同步执行 + type: 'POST', + url: config.context + '/playground/autocomplete', + data: JSON.stringify(request), + dataType: "json", + success: function (data) { + var autocompleteArray = data[1]; + + if (autocompleteArray) { + for (var i = 0; i < autocompleteArray.length; i++) { + var displayText = '', + text = autocompleteArray[i].name; + + switch (autocompleteArray[i].class) { + case "type": + displayText = ''// + autocompleteArray[i].class + + '' + autocompleteArray[i].name + ' ' + + autocompleteArray[i].type + ''; + break; + case "const": + displayText = ''// + autocompleteArray[i].class + + '' + autocompleteArray[i].name + ' ' + + autocompleteArray[i].type + ''; + break; + case "var": + displayText = ''// + autocompleteArray[i].class + + '' + autocompleteArray[i].name + ' ' + + autocompleteArray[i].type + ''; + break; + case "package": + displayText = ''// + autocompleteArray[i].class + + '' + autocompleteArray[i].name + ' ' + + autocompleteArray[i].type + ''; + break; + case "func": + displayText = ''// + autocompleteArray[i].class + + '' + autocompleteArray[i].name + '' + + autocompleteArray[i].type.substring(4) + ''; + text += '()'; + break; + default: + console.warn("Can't handle autocomplete [" + autocompleteArray[i].class + "]"); + break; + } + + autocompleteHints[i] = { + displayText: displayText, + text: text + }; + } + } + } + }); + + return {list: autocompleteHints, from: CodeMirror.Pos(cur.line, start), to: CodeMirror.Pos(cur.line, end)}; + }); + + CodeMirror.commands.autocompleteAnyWord = function (cm) { + cm.showHint({hint: CodeMirror.hint.auto}); + }; + + CodeMirror.commands.autocompleteAfterDot = function (cm) { + var mode = cm.getMode(); + if (mode && "go" !== mode.name) { + return CodeMirror.Pass; + } + + var token = cm.getTokenAt(cm.getCursor()); + + if ("comment" === token.type || "string" === token.type) { + return CodeMirror.Pass; + } + + setTimeout(function () { + if (!cm.state.completionActive) { + cm.showHint({hint: CodeMirror.hint.go, completeSingle: false}); + } + }, 50); + + return CodeMirror.Pass; + }; + playground.editor = CodeMirror.fromTextArea($("#editor")[0], { lineNumbers: true, autofocus: true, @@ -71,7 +174,11 @@ var playground = { indentUnit: 4, foldGutter: true, cursorHeight: 1, - viewportMargin: 1000 + viewportMargin: 500, + extraKeys: { + "Ctrl-\\": "autocompleteAnyWord", + ".": "autocompleteAfterDot" + } }); $("#editorDiv").show(); diff --git a/views/playground/index.html b/views/playground/index.html index 38ff958..dbc7c91 100644 --- a/views/playground/index.html +++ b/views/playground/index.html @@ -8,15 +8,18 @@ - + + + - + +