github.com/jhump/golang-x-tools@v0.0.0-20220218190644-4958d6d39439/internal/lsp/completion.go (about) 1 // Copyright 2018 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package lsp 6 7 import ( 8 "bytes" 9 "context" 10 "fmt" 11 "strings" 12 13 "github.com/jhump/golang-x-tools/internal/event" 14 "github.com/jhump/golang-x-tools/internal/lsp/debug/tag" 15 "github.com/jhump/golang-x-tools/internal/lsp/protocol" 16 "github.com/jhump/golang-x-tools/internal/lsp/source" 17 "github.com/jhump/golang-x-tools/internal/lsp/source/completion" 18 "github.com/jhump/golang-x-tools/internal/lsp/template" 19 "github.com/jhump/golang-x-tools/internal/span" 20 ) 21 22 func (s *Server) completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) { 23 snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind) 24 defer release() 25 if !ok { 26 return nil, err 27 } 28 var candidates []completion.CompletionItem 29 var surrounding *completion.Selection 30 switch snapshot.View().FileKind(fh) { 31 case source.Go: 32 candidates, surrounding, err = completion.Completion(ctx, snapshot, fh, params.Position, params.Context) 33 case source.Mod: 34 candidates, surrounding = nil, nil 35 case source.Tmpl: 36 var cl *protocol.CompletionList 37 cl, err = template.Completion(ctx, snapshot, fh, params.Position, params.Context) 38 if err != nil { 39 break // use common error handling, candidates==nil 40 } 41 return cl, nil 42 } 43 if err != nil { 44 event.Error(ctx, "no completions found", err, tag.Position.Of(params.Position)) 45 } 46 if candidates == nil { 47 return &protocol.CompletionList{ 48 IsIncomplete: true, 49 Items: []protocol.CompletionItem{}, 50 }, nil 51 } 52 // We might need to adjust the position to account for the prefix. 53 rng, err := surrounding.Range() 54 if err != nil { 55 return nil, err 56 } 57 58 // internal/span treats end of file as the beginning of the next line, even 59 // when it's not newline-terminated. We correct for that behaviour here if 60 // end of file is not newline-terminated. See golang/go#41029. 61 src, err := fh.Read() 62 if err != nil { 63 return nil, err 64 } 65 numLines := len(bytes.Split(src, []byte("\n"))) 66 tok := snapshot.FileSet().File(surrounding.Start()) 67 eof := tok.Pos(tok.Size()) 68 69 // For newline-terminated files, the line count reported by go/token should 70 // be lower than the actual number of lines we see when splitting by \n. If 71 // they're the same, the file isn't newline-terminated. 72 if tok.Size() > 0 && tok.LineCount() == numLines { 73 // Get the span for the last character in the file-1. This is 74 // technically incorrect, but will get span to point to the previous 75 // line. 76 spn, err := span.NewRange(snapshot.FileSet(), eof-1, eof-1).Span() 77 if err != nil { 78 return nil, err 79 } 80 m := &protocol.ColumnMapper{ 81 URI: fh.URI(), 82 Converter: span.NewContentConverter(fh.URI().Filename(), src), 83 Content: src, 84 } 85 eofRng, err := m.Range(spn) 86 if err != nil { 87 return nil, err 88 } 89 // Instead of using the computed range, correct for our earlier 90 // position adjustment by adding 1 to the column, not the line number. 91 pos := protocol.Position{ 92 Line: eofRng.Start.Line, 93 Character: eofRng.Start.Character + 1, 94 } 95 if surrounding.Start() >= eof { 96 rng.Start = pos 97 } 98 if surrounding.End() >= eof { 99 rng.End = pos 100 } 101 } 102 103 // When using deep completions/fuzzy matching, report results as incomplete so 104 // client fetches updated completions after every key stroke. 105 options := snapshot.View().Options() 106 incompleteResults := options.DeepCompletion || options.Matcher == source.Fuzzy 107 108 items := toProtocolCompletionItems(candidates, rng, options) 109 110 return &protocol.CompletionList{ 111 IsIncomplete: incompleteResults, 112 Items: items, 113 }, nil 114 } 115 116 func toProtocolCompletionItems(candidates []completion.CompletionItem, rng protocol.Range, options *source.Options) []protocol.CompletionItem { 117 var ( 118 items = make([]protocol.CompletionItem, 0, len(candidates)) 119 numDeepCompletionsSeen int 120 ) 121 for i, candidate := range candidates { 122 // Limit the number of deep completions to not overwhelm the user in cases 123 // with dozens of deep completion matches. 124 if candidate.Depth > 0 { 125 if !options.DeepCompletion { 126 continue 127 } 128 if numDeepCompletionsSeen >= completion.MaxDeepCompletions { 129 continue 130 } 131 numDeepCompletionsSeen++ 132 } 133 insertText := candidate.InsertText 134 if options.InsertTextFormat == protocol.SnippetTextFormat { 135 insertText = candidate.Snippet() 136 } 137 138 // This can happen if the client has snippets disabled but the 139 // candidate only supports snippet insertion. 140 if insertText == "" { 141 continue 142 } 143 144 item := protocol.CompletionItem{ 145 Label: candidate.Label, 146 Detail: candidate.Detail, 147 Kind: candidate.Kind, 148 TextEdit: &protocol.TextEdit{ 149 NewText: insertText, 150 Range: rng, 151 }, 152 InsertTextFormat: options.InsertTextFormat, 153 AdditionalTextEdits: candidate.AdditionalTextEdits, 154 // This is a hack so that the client sorts completion results in the order 155 // according to their score. This can be removed upon the resolution of 156 // https://github.com/Microsoft/language-server-protocol/issues/348. 157 SortText: fmt.Sprintf("%05d", i), 158 159 // Trim operators (VSCode doesn't like weird characters in 160 // filterText). 161 FilterText: strings.TrimLeft(candidate.InsertText, "&*"), 162 163 Preselect: i == 0, 164 Documentation: candidate.Documentation, 165 Tags: candidate.Tags, 166 Deprecated: candidate.Deprecated, 167 } 168 items = append(items, item) 169 } 170 return items 171 }