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