diff --git a/internal/lsp/source/completion.go b/internal/lsp/source/completion.go index 4d96d89043..a40e549822 100644 --- a/internal/lsp/source/completion.go +++ b/internal/lsp/source/completion.go @@ -515,6 +515,9 @@ func Completion(ctx context.Context, view View, f File, pos protocol.Position, o if err := c.lexical(); err != nil { return nil, nil, err } + if err := c.keyword(); err != nil { + return nil, nil, err + } // The function name hasn't been typed yet, but the parens are there: // recv.‸(arg) diff --git a/internal/lsp/source/completion_keywords.go b/internal/lsp/source/completion_keywords.go new file mode 100644 index 0000000000..ef787b750e --- /dev/null +++ b/internal/lsp/source/completion_keywords.go @@ -0,0 +1,105 @@ +package source + +import ( + "go/ast" + + "golang.org/x/tools/internal/lsp/protocol" + + errors "golang.org/x/xerrors" +) + +const ( + BREAK = "break" + CASE = "case" + CHAN = "chan" + CONST = "const" + CONTINUE = "continue" + DEFAULT = "default" + DEFER = "defer" + ELSE = "else" + FALLTHROUGH = "fallthrough" + FOR = "for" + FUNC = "func" + GO = "go" + GOTO = "goto" + IF = "if" + IMPORT = "import" + INTERFACE = "interface" + MAP = "map" + PACKAGE = "package" + RANGE = "range" + RETURN = "return" + SELECT = "select" + STRUCT = "struct" + SWITCH = "switch" + TYPE = "type" + VAR = "var" +) + +// keyword looks at the current scope of an *ast.Ident and recommends keywords +func (c *completer) keyword() error { + if _, ok := c.path[0].(*ast.Ident); !ok { + // TODO(golang/go#34009): Support keyword completion in any context + return errors.Errorf("keywords are currently only recommended for identifiers") + } + // Track which keywords we've already determined are in a valid scope + // Use score to order keywords by how close we are to where they are useful + valid := make(map[string]float64) + + // only suggest keywords at the begnning of a statement + switch c.path[1].(type) { + case *ast.BlockStmt, *ast.CommClause, *ast.CaseClause, *ast.ExprStmt: + default: + return nil + } + + // Filter out keywords depending on scope + // Skip the first one because we want to look at the enclosing scopes + for _, n := range c.path[1:] { + switch node := n.(type) { + case *ast.CaseClause: + // only recommend "fallthrough" and "break" within the bodies of a case clause + if c.pos > node.Colon { + valid[BREAK] = stdScore + // TODO: "fallthrough" is only valid in switch statements + valid[FALLTHROUGH] = stdScore + } + case *ast.CommClause: + if c.pos > node.Colon { + valid[BREAK] = stdScore + } + case *ast.TypeSwitchStmt, *ast.SelectStmt, *ast.SwitchStmt: + valid[CASE] = stdScore + lowScore + valid[DEFAULT] = stdScore + lowScore + case *ast.ForStmt: + valid[BREAK] = stdScore + valid[CONTINUE] = stdScore + // This is a bit weak, functions allow for many keywords + case *ast.FuncDecl: + if node.Body != nil && c.pos > node.Body.Lbrace { + valid[DEFER] = stdScore - lowScore + valid[RETURN] = stdScore - lowScore + valid[FOR] = stdScore - lowScore + valid[GO] = stdScore - lowScore + valid[SWITCH] = stdScore - lowScore + valid[SELECT] = stdScore - lowScore + valid[IF] = stdScore - lowScore + valid[ELSE] = stdScore - lowScore + valid[VAR] = stdScore - lowScore + valid[CONST] = stdScore - lowScore + } + } + } + + for ident, score := range valid { + if c.matcher.Score(ident) > 0 { + c.items = append(c.items, CompletionItem{ + Label: ident, + Kind: protocol.KeywordCompletion, + InsertText: ident, + Score: score, + }) + } + } + return nil +} diff --git a/internal/lsp/testdata/keywords/keywords.go b/internal/lsp/testdata/keywords/keywords.go new file mode 100644 index 0000000000..ce3bfdcef7 --- /dev/null +++ b/internal/lsp/testdata/keywords/keywords.go @@ -0,0 +1,74 @@ +package keywords + +func _() { + var test int + var tChan chan int + switch test { + case 1: // TODO: trying to complete case here will break because the parser wont return *ast.Ident + b //@complete(" //", break) + case 2: + f //@complete(" //", fallthrough, for) + r //@complete(" //", return) + d //@complete(" //", default, defer) + c //@complete(" //", case, const) + } + + switch test.(type) { + case int: + b //@complete(" //", break) + case int32: + f //@complete(" //", fallthrough, for) + d //@complete(" //", default, defer) + r //@complete(" //", return) + c //@complete(" //", case, const) + } + + select { + case <-tChan: + b //@complete(" //", break) + c //@complete(" //", case, const) + } + + for index := 0; index < test; index++ { + c //@complete(" //", continue, const) + b //@complete(" //", break) + } + + // Test function level keywords + + //Using 2 characters to test because map output order is random + sw //@complete(" //", switch) + se //@complete(" //", select) + + f //@complete(" //", for) + d //@complete(" //", defer) + g //@complete(" //", go) + r //@complete(" //", return) + i //@complete(" //", if) + e //@complete(" //", else) + v //@complete(" //", var) + c //@complete(" //", const) + +} + +/* package */ //@item(package, "package", "", "keyword") +/* import */ //@item(import, "import", "", "keyword") +/* func */ //@item(func, "func", "", "keyword") +/* type */ //@item(type, "type", "", "keyword") +/* var */ //@item(var, "var", "", "keyword") +/* const */ //@item(const, "const", "", "keyword") +/* break */ //@item(break, "break", "", "keyword") +/* default */ //@item(default, "default", "", "keyword") +/* case */ //@item(case, "case", "", "keyword") +/* defer */ //@item(defer, "defer", "", "keyword") +/* go */ //@item(go, "go", "", "keyword") +/* for */ //@item(for, "for", "", "keyword") +/* if */ //@item(if, "if", "", "keyword") +/* else */ //@item(else, "else", "", "keyword") +/* switch */ //@item(switch, "switch", "", "keyword") +/* select */ //@item(select, "select", "", "keyword") +/* fallthrough */ //@item(fallthrough, "fallthrough", "", "keyword") +/* continue */ //@item(continue, "continue", "", "keyword") +/* return */ //@item(return, "return", "", "keyword") +/* var */ //@item(var, "var", "", "keyword") +/* const */ //@item(const, "const", "", "keyword") diff --git a/internal/lsp/testdata/rank/type_switch_rank.go.in b/internal/lsp/testdata/rank/type_switch_rank.go.in index 457c64b82e..6cec59742d 100644 --- a/internal/lsp/testdata/rank/type_switch_rank.go.in +++ b/internal/lsp/testdata/rank/type_switch_rank.go.in @@ -6,6 +6,6 @@ func _() { switch interface{}(pear).(type) { case b: //@complete(":", basket, banana) - b //@complete(" //", banana, basket) + b //@complete(" //", banana, basket, break) } } diff --git a/internal/lsp/testdata/summary.txt.golden b/internal/lsp/testdata/summary.txt.golden index 75f2e36147..9d4910b79d 100644 --- a/internal/lsp/testdata/summary.txt.golden +++ b/internal/lsp/testdata/summary.txt.golden @@ -1,5 +1,5 @@ -- summary -- -CompletionsCount = 185 +CompletionsCount = 209 CompletionSnippetCount = 39 UnimportedCompletionsCount = 1 DeepCompletionsCount = 5