From 465e898ee4c2ac8cb37b73409527cac31ebd76ac Mon Sep 17 00:00:00 2001 From: x1unix Date: Tue, 18 Aug 2020 17:10:46 +0300 Subject: [PATCH 1/5] ui: add F5 hotkey to run app --- web/src/Header.tsx | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/web/src/Header.tsx b/web/src/Header.tsx index 89468a37..427e2186 100644 --- a/web/src/Header.tsx +++ b/web/src/Header.tsx @@ -46,6 +46,17 @@ export class Header extends React.Component { }; } + onKeyCommand = (e: KeyboardEvent) => { + switch (e.code) { + case 'F5': + e.preventDefault(); + this.props.dispatch(runFileDispatcher); + return; + default: + return; + } + } + componentDidMount(): void { const fileElement = document.createElement('input') as HTMLInputElement; fileElement.type = 'file'; @@ -59,6 +70,13 @@ export class Header extends React.Component { if (!version) return; this.setState({showUpdateBanner: version !== config.appVersion}); }).catch(err => console.warn('failed to check server API version: ', err)); + + // register keybindings + window.addEventListener('keydown', this.onKeyCommand) + } + + componentWillUnmount() { + window.removeEventListener('keydown', this.onKeyCommand); } onItemSelect() { @@ -91,7 +109,8 @@ export class Header extends React.Component { { key: 'run', text: 'Run', - ariaLabel: 'Run', + ariaLabel: 'Run program (F5)', + title: 'Run program (F5)', iconProps: {iconName: 'Play'}, disabled: this.props.loading, onClick: () => { From c06c6b130fb104e980c936d87835325c1a5e208d Mon Sep 17 00:00:00 2001 From: x1unix Date: Tue, 18 Aug 2020 17:17:18 +0300 Subject: [PATCH 2/5] editor: manually filter related snippets --- web/src/editor/provider.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/web/src/editor/provider.ts b/web/src/editor/provider.ts index a7065a13..f60c9de8 100644 --- a/web/src/editor/provider.ts +++ b/web/src/editor/provider.ts @@ -52,13 +52,16 @@ class GoCompletionItemProvider implements monaco.languages.CompletionItemProvide return Promise.resolve({suggestions: []}); } + // filter snippets by prefix. + // usually monaco does that but not always in right way + const relatedSnippets = snippets.filter(s => s.label.toString().startsWith(query.value)) try { const resp = await this.client.getSuggestions(query); - resp.suggestions = resp.suggestions ? snippets.concat(resp.suggestions) : snippets; + resp.suggestions = resp.suggestions ? relatedSnippets.concat(resp.suggestions) : relatedSnippets; return resp; } catch (err) { console.error(`Failed to get code completion from server: ${err.message}`); - return {suggestions: snippets}; + return {suggestions: relatedSnippets}; } } } From ac4f707a342e7c4a5e33b9aa19a12f7065463c9d Mon Sep 17 00:00:00 2001 From: x1unix Date: Tue, 18 Aug 2020 19:17:20 +0300 Subject: [PATCH 3/5] analyzer: format insertText as snippet for functions --- pkg/analyzer/completion_item.go | 15 +++++ pkg/analyzer/decl.go | 49 ++++++++++++-- pkg/analyzer/scanner.go | 2 +- pkg/analyzer/scanner_test.go | 69 +++++++++++++------- pkg/analyzer/testdata/src/example/example.go | 8 +++ 5 files changed, 113 insertions(+), 30 deletions(-) diff --git a/pkg/analyzer/completion_item.go b/pkg/analyzer/completion_item.go index f7702006..18ae6b8e 100644 --- a/pkg/analyzer/completion_item.go +++ b/pkg/analyzer/completion_item.go @@ -35,6 +35,18 @@ const ( Snippet ) +// CompletionItemInsertTextRule is insert text insert rule. +type CompletionItemInsertTextRule int + +const ( + // KeepWhitespace is adjust whitespace/indentation of + // multiline insert texts to match the current line indentation. + KeepWhitespace CompletionItemInsertTextRule = 1 + + // InsertAsSnippet means that `insertText` is a snippet. + InsertAsSnippet CompletionItemInsertTextRule = 4 +) + // CompletionItem is monaco-editor binding type CompletionItem struct { // Label is item label @@ -47,6 +59,9 @@ type CompletionItem struct { Documentation interface{} `json:"documentation"` // InsertText is text to be inserted InsertText string `json:"insertText"` + // InsertTextRules is a string or snippet that should be inserted in a document + // when selecting this completion. When `falsy` the label in used. + InsertTextRules CompletionItemInsertTextRule `json:"insertTextRules,omitempty"` } // MarkdownString is monaco-editor string with markdown diff --git a/pkg/analyzer/decl.go b/pkg/analyzer/decl.go index d70d1cdd..63acec7d 100644 --- a/pkg/analyzer/decl.go +++ b/pkg/analyzer/decl.go @@ -2,6 +2,7 @@ package analyzer import ( "go/ast" + "strconv" "strings" ) @@ -77,14 +78,52 @@ func funcToString(fn *ast.FuncType) string { return str } +// formatFuncInsertText returns `insertText` snippet template for monaco-editor +// from function signature. +// +// Example: +// func sum(a, b int) int +// Will output: +// "sum(${1:a}, ${2:b})" +func formatFuncInsertText(fn *ast.FuncDecl) string { + sb := strings.Builder{} + sb.WriteString(fn.Name.String()) + sb.WriteByte('(') + + if fn.Type.Params != nil && fn.Type.Params.List != nil { + pos := 1 // in monaco, first input argument should start with 0 + params := make([]string, 0, fn.Type.Params.NumFields()) + + for _, param := range fn.Type.Params.List { + if len(param.Names) == 0 { + // $pos + params = append(params, "$"+strconv.Itoa(pos)) + pos++ + continue + } + + for _, p := range param.Names { + // ${pos:paramName} + params = append(params, "${"+strconv.Itoa(pos)+":"+p.Name+"}") + pos++ + } + } + sb.WriteString(strings.Join(params, ", ")) + } + + sb.WriteRune(')') + return sb.String() +} + func funcToItem(fn *ast.FuncDecl) *CompletionItem { ci := &CompletionItem{ - Label: fn.Name.String(), - Kind: Function, - Documentation: formatDoc(fn.Doc.Text()), + Label: fn.Name.String(), + Kind: Function, + Detail: funcToString(fn.Type), + Documentation: formatDoc(fn.Doc.Text()), + InsertText: formatFuncInsertText(fn), + InsertTextRules: InsertAsSnippet, } - ci.Detail = funcToString(fn.Type) - ci.InsertText = ci.Label + "()" return ci } diff --git a/pkg/analyzer/scanner.go b/pkg/analyzer/scanner.go index d25280db..81dab842 100644 --- a/pkg/analyzer/scanner.go +++ b/pkg/analyzer/scanner.go @@ -78,7 +78,7 @@ func (p *PackageScanner) appendFunc(fn *ast.FuncDecl, dest *SymbolIndex) { } item := funcToItem(fn) - log.Debugf("found function '%s.%s'", p.name, item.InsertText) + log.Debugf("found function '%s.%s'", p.name, item.Detail) dest.Append(item) } diff --git a/pkg/analyzer/scanner_test.go b/pkg/analyzer/scanner_test.go index e282b27b..6e8f48b9 100644 --- a/pkg/analyzer/scanner_test.go +++ b/pkg/analyzer/scanner_test.go @@ -12,42 +12,63 @@ import ( var examplePackageSummary = PackageSummary{ Functions: newSymbolIndex([]*CompletionItem{ { - Label: "SomeFunc", - Kind: Function, - Detail: "func(val string) string", - InsertText: "SomeFunc()", + Label: "SomeFunc", + Kind: Function, + Detail: "func(val string) string", + InsertText: "SomeFunc(${1:val})", + InsertTextRules: InsertAsSnippet, Documentation: NewMarkdownString( "SomeFunc is test function sample\nwith doc that contains code sample:" + "\n\n```\n\ta := \"foo\"\n\tfmt.PrintLn(a)\n\n```\nend\n\n", ), }, { - Label: "ChanArrFunc", - Kind: Function, - Detail: "func(items ...string) chan string", - InsertText: "ChanArrFunc()", - Documentation: NewMarkdownString("ChanArrFunc is stub\n\n"), + Label: "ChanArrFunc", + Kind: Function, + Detail: "func(items ...string) chan string", + InsertText: "ChanArrFunc(${1:items})", + InsertTextRules: InsertAsSnippet, + Documentation: NewMarkdownString("ChanArrFunc is stub\n\n"), }, { - Label: "SomeFunc2", - Kind: Function, - Detail: "func(m map[string]interface{}, v *int) []interface{}", - InsertText: "SomeFunc2()", - Documentation: NewMarkdownString("SomeFunc2 is func stub\n\n"), + Label: "SomeFunc2", + Kind: Function, + Detail: "func(m map[string]interface{}, v *int) []interface{}", + InsertText: "SomeFunc2(${1:m}, ${2:v})", + InsertTextRules: InsertAsSnippet, + Documentation: NewMarkdownString("SomeFunc2 is func stub\n\n"), }, { - Label: "IfaceFunc", - Kind: Function, - Detail: "func() Action", - InsertText: "IfaceFunc()", - Documentation: NewMarkdownString("IfaceFunc is stub with unterminated code block\n```\n\t2 + 2\n\n```\n"), + Label: "IfaceFunc", + Kind: Function, + Detail: "func() Action", + InsertText: "IfaceFunc()", + InsertTextRules: InsertAsSnippet, + Documentation: NewMarkdownString("IfaceFunc is stub with unterminated code block\n```\n\t2 + 2\n\n```\n"), }, { - Label: "FuncReturnFuncAndIface", - Kind: Function, - Detail: "func() (func() (string, error), interface{\nf func()\n})", - InsertText: "FuncReturnFuncAndIface()", - Documentation: NewMarkdownString("FuncReturnFuncAndIface is stub\n\n"), + Label: "FuncReturnFuncAndIface", + Kind: Function, + Detail: "func() (func() (string, error), interface{\nf func()\n})", + InsertText: "FuncReturnFuncAndIface()", + InsertTextRules: InsertAsSnippet, + Documentation: NewMarkdownString("FuncReturnFuncAndIface is stub\n\n"), + }, + { + Label: "XXX", + Kind: Function, + Detail: "func(a string, b string)", + InsertText: "XXX(${1:a}, ${2:b})", + InsertTextRules: InsertAsSnippet, + Documentation: NewMarkdownString("XXX is function example\n\n"), + }, + { + Label: "FuncUnnamedParams", + Kind: Function, + Detail: "func(string)", + InsertText: "FuncUnnamedParams($1)", + InsertTextRules: InsertAsSnippet, + Documentation: NewMarkdownString("FuncUnnamedParams is function with unnamed params\n\n"), }, }), Values: newSymbolIndex([]*CompletionItem{ diff --git a/pkg/analyzer/testdata/src/example/example.go b/pkg/analyzer/testdata/src/example/example.go index f88a7947..377c0d1a 100644 --- a/pkg/analyzer/testdata/src/example/example.go +++ b/pkg/analyzer/testdata/src/example/example.go @@ -77,3 +77,11 @@ func IfaceFunc() Action { func FuncReturnFuncAndIface() (func() (string, error), interface{ f() }) { return nil, nil } + +// XXX is function example +func XXX(a, b string) { + +} + +// FuncUnnamedParams is function with unnamed params +func FuncUnnamedParams(string) {} From 16b292fbe2d385618bfbe988496b3691da2ec698 Mon Sep 17 00:00:00 2001 From: x1unix Date: Tue, 18 Aug 2020 19:19:06 +0300 Subject: [PATCH 4/5] editor: add fix snippets indentation --- web/src/editor/provider.ts | 26 ++++++++++++++++++++++---- web/src/editor/snippets.ts | 9 ++------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/web/src/editor/provider.ts b/web/src/editor/provider.ts index f60c9de8..19152ca9 100644 --- a/web/src/editor/provider.ts +++ b/web/src/editor/provider.ts @@ -52,13 +52,31 @@ class GoCompletionItemProvider implements monaco.languages.CompletionItemProvide return Promise.resolve({suggestions: []}); } + let word = model.getWordUntilPosition(position); + let range = { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: word.startColumn, + endColumn: word.endColumn + }; + // filter snippets by prefix. // usually monaco does that but not always in right way - const relatedSnippets = snippets.filter(s => s.label.toString().startsWith(query.value)) + const relatedSnippets = snippets + .filter(s => s.label.toString().startsWith(query.value)) + .map(s => ({...s, range})); + try { - const resp = await this.client.getSuggestions(query); - resp.suggestions = resp.suggestions ? relatedSnippets.concat(resp.suggestions) : relatedSnippets; - return resp; + const {suggestions} = await this.client.getSuggestions(query); + if (!suggestions) { + return { + suggestions: relatedSnippets + } + } + + return { + suggestions: relatedSnippets.concat(suggestions.map(s => ({...s, range}))) + } } catch (err) { console.error(`Failed to get code completion from server: ${err.message}`); return {suggestions: relatedSnippets}; diff --git a/web/src/editor/snippets.ts b/web/src/editor/snippets.ts index 7c7ee405..cffa732d 100644 --- a/web/src/editor/snippets.ts +++ b/web/src/editor/snippets.ts @@ -2,12 +2,7 @@ import * as monaco from 'monaco-editor'; /* eslint-disable no-template-curly-in-string */ -// Import aliases -type CompletionList = monaco.languages.CompletionList; -type CompletionContext = monaco.languages.CompletionContext; -type ITextModel = monaco.editor.ITextModel; -type Position = monaco.Position; - +const Rule = monaco.languages.CompletionItemInsertTextRule; /** * List of snippets for editor */ @@ -121,7 +116,7 @@ const snippets = [ ].map(s => ({ kind: monaco.languages.CompletionItemKind.Snippet, - insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + insertTextRules: Rule.InsertAsSnippet, ...s, })); From 73e185f7bc1974a6094fa5526e6d59797627eee3 Mon Sep 17 00:00:00 2001 From: x1unix Date: Tue, 18 Aug 2020 19:39:58 +0300 Subject: [PATCH 5/5] update changelog for 1.6 --- web/src/ChangeLogModal.tsx | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/web/src/ChangeLogModal.tsx b/web/src/ChangeLogModal.tsx index 96c2c214..2f06a492 100644 --- a/web/src/ChangeLogModal.tsx +++ b/web/src/ChangeLogModal.tsx @@ -39,31 +39,14 @@ export default function ChangeLogModal(props: ChangeLogModalProps) {

Interface - Global

    -
  • Added list of snippets with templates and tutorials near Open menu item
  • -
  • Moved Settings menu button from drop-down to main section
  • -
-

-

- Interface - Settings -

    -
  • Added editor fonts selector
  • -
  • Added support of font code ligatures
  • -
  • Fixed fallback font issue that might cause issues on Linux
  • +
  • Add F5 hotkey for Run action

Interface - Editor

    -
  • - Added code snippets to make code input faster: -
      -
    • iferr - Error handling snippet
    • -
    • switch - Quick switch declaration
    • -
    • typestruct - Quickly declare struct
    • -
    • fmtprintf - fmt.Printf shorthand
    • -
    • and other (see release notes)
    • -
    -
  • +
  • Suggest function arguments completion
  • +
  • Fix snippets suggestion issue