Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions pkg/analyzer/completion_item.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
49 changes: 44 additions & 5 deletions pkg/analyzer/decl.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package analyzer

import (
"go/ast"
"strconv"
"strings"
)

Expand Down Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion pkg/analyzer/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
69 changes: 45 additions & 24 deletions pkg/analyzer/scanner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
8 changes: 8 additions & 0 deletions pkg/analyzer/testdata/src/example/example.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}
23 changes: 3 additions & 20 deletions web/src/ChangeLogModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,31 +39,14 @@ export default function ChangeLogModal(props: ChangeLogModalProps) {
<p>
<b>Interface - Global</b>
<ul>
<li>Added list of snippets with <u>templates and tutorials</u> near <b>Open</b> menu item</li>
<li>Moved <b>Settings</b> menu button from drop-down to main section</li>
</ul>
</p>
<p>
<b>Interface - Settings</b>
<ul>
<li>Added editor fonts selector</li>
<li>Added support of font code ligatures</li>
<li>Fixed fallback font issue that might cause issues on Linux</li>
<li>Add <kbd>F5</kbd> hotkey for <b>Run</b> action</li>
</ul>
</p>
<p>
<b>Interface - Editor</b>
<ul>
<li>
Added code snippets to make code input faster:
<ul>
<li><code>iferr</code> - Error handling snippet</li>
<li><code>switch</code> - Quick switch declaration</li>
<li><code>typestruct</code> - Quickly declare struct</li>
<li><code>fmtprintf</code> - fmt.Printf shorthand</li>
<li>and other (see release notes)</li>
</ul>
</li>
<li>Suggest function arguments completion</li>
<li>Fix snippets suggestion issue</li>
</ul>
</p>
<p>
Expand Down
21 changes: 20 additions & 1 deletion web/src/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ export class Header extends React.Component<any, HeaderState> {
};
}

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';
Expand All @@ -59,6 +70,13 @@ export class Header extends React.Component<any, HeaderState> {
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() {
Expand Down Expand Up @@ -91,7 +109,8 @@ export class Header extends React.Component<any, HeaderState> {
{
key: 'run',
text: 'Run',
ariaLabel: 'Run',
ariaLabel: 'Run program (F5)',
title: 'Run program (F5)',
iconProps: {iconName: 'Play'},
disabled: this.props.loading,
onClick: () => {
Expand Down
29 changes: 25 additions & 4 deletions web/src/editor/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,34 @@ 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))
.map(s => ({...s, range}));

try {
const resp = await this.client.getSuggestions(query);
resp.suggestions = resp.suggestions ? snippets.concat(resp.suggestions) : snippets;
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: snippets};
return {suggestions: relatedSnippets};
}
}
}
Expand Down
9 changes: 2 additions & 7 deletions web/src/editor/snippets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -121,7 +116,7 @@ const snippets = [

].map(s => ({
kind: monaco.languages.CompletionItemKind.Snippet,
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
insertTextRules: Rule.InsertAsSnippet,
...s,
}));

Expand Down