|  | // Copyright 2018 The Go Authors. All rights reserved. | 
|  | // Use of this source code is governed by a BSD-style | 
|  | // license that can be found in the LICENSE file. | 
|  |  | 
|  | package lsp | 
|  |  | 
|  | import ( | 
|  | "bytes" | 
|  | "context" | 
|  | "fmt" | 
|  | "strings" | 
|  |  | 
|  | "golang.org/x/tools/internal/event" | 
|  | "golang.org/x/tools/internal/lsp/debug/tag" | 
|  | "golang.org/x/tools/internal/lsp/protocol" | 
|  | "golang.org/x/tools/internal/lsp/source" | 
|  | "golang.org/x/tools/internal/lsp/source/completion" | 
|  | "golang.org/x/tools/internal/lsp/template" | 
|  | "golang.org/x/tools/internal/span" | 
|  | ) | 
|  |  | 
|  | func (s *Server) completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) { | 
|  | snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind) | 
|  | defer release() | 
|  | if !ok { | 
|  | return nil, err | 
|  | } | 
|  | var candidates []completion.CompletionItem | 
|  | var surrounding *completion.Selection | 
|  | switch fh.Kind() { | 
|  | case source.Go: | 
|  | candidates, surrounding, err = completion.Completion(ctx, snapshot, fh, params.Position, params.Context) | 
|  | case source.Mod: | 
|  | candidates, surrounding = nil, nil | 
|  | case source.Tmpl: | 
|  | var cl *protocol.CompletionList | 
|  | cl, err = template.Completion(ctx, snapshot, fh, params.Position, params.Context) | 
|  | if err != nil { | 
|  | break // use common error handling, candidates==nil | 
|  | } | 
|  | return cl, nil | 
|  | } | 
|  | if err != nil { | 
|  | event.Error(ctx, "no completions found", err, tag.Position.Of(params.Position)) | 
|  | } | 
|  | if candidates == nil { | 
|  | return &protocol.CompletionList{ | 
|  | IsIncomplete: true, | 
|  | Items:        []protocol.CompletionItem{}, | 
|  | }, nil | 
|  | } | 
|  | // We might need to adjust the position to account for the prefix. | 
|  | rng, err := surrounding.Range() | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  |  | 
|  | // internal/span treats end of file as the beginning of the next line, even | 
|  | // when it's not newline-terminated. We correct for that behaviour here if | 
|  | // end of file is not newline-terminated. See golang/go#41029. | 
|  | src, err := fh.Read() | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | numLines := len(bytes.Split(src, []byte("\n"))) | 
|  | tok := snapshot.FileSet().File(surrounding.Start()) | 
|  | eof := tok.Pos(tok.Size()) | 
|  |  | 
|  | // For newline-terminated files, the line count reported by go/token should | 
|  | // be lower than the actual number of lines we see when splitting by \n. If | 
|  | // they're the same, the file isn't newline-terminated. | 
|  | if tok.Size() > 0 && tok.LineCount() == numLines { | 
|  | // Get the span for the last character in the file-1. This is | 
|  | // technically incorrect, but will get span to point to the previous | 
|  | // line. | 
|  | spn, err := span.NewRange(snapshot.FileSet(), eof-1, eof-1).Span() | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | m := &protocol.ColumnMapper{ | 
|  | URI:       fh.URI(), | 
|  | Converter: span.NewContentConverter(fh.URI().Filename(), src), | 
|  | Content:   src, | 
|  | } | 
|  | eofRng, err := m.Range(spn) | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | // Instead of using the computed range, correct for our earlier | 
|  | // position adjustment by adding 1 to the column, not the line number. | 
|  | pos := protocol.Position{ | 
|  | Line:      eofRng.Start.Line, | 
|  | Character: eofRng.Start.Character + 1, | 
|  | } | 
|  | if surrounding.Start() >= eof { | 
|  | rng.Start = pos | 
|  | } | 
|  | if surrounding.End() >= eof { | 
|  | rng.End = pos | 
|  | } | 
|  | } | 
|  |  | 
|  | // When using deep completions/fuzzy matching, report results as incomplete so | 
|  | // client fetches updated completions after every key stroke. | 
|  | options := snapshot.View().Options() | 
|  | incompleteResults := options.DeepCompletion || options.Matcher == source.Fuzzy | 
|  |  | 
|  | items := toProtocolCompletionItems(candidates, rng, options) | 
|  |  | 
|  | return &protocol.CompletionList{ | 
|  | IsIncomplete: incompleteResults, | 
|  | Items:        items, | 
|  | }, nil | 
|  | } | 
|  |  | 
|  | func toProtocolCompletionItems(candidates []completion.CompletionItem, rng protocol.Range, options *source.Options) []protocol.CompletionItem { | 
|  | var ( | 
|  | items                  = make([]protocol.CompletionItem, 0, len(candidates)) | 
|  | numDeepCompletionsSeen int | 
|  | ) | 
|  | for i, candidate := range candidates { | 
|  | // Limit the number of deep completions to not overwhelm the user in cases | 
|  | // with dozens of deep completion matches. | 
|  | if candidate.Depth > 0 { | 
|  | if !options.DeepCompletion { | 
|  | continue | 
|  | } | 
|  | if numDeepCompletionsSeen >= completion.MaxDeepCompletions { | 
|  | continue | 
|  | } | 
|  | numDeepCompletionsSeen++ | 
|  | } | 
|  | insertText := candidate.InsertText | 
|  | if options.InsertTextFormat == protocol.SnippetTextFormat { | 
|  | insertText = candidate.Snippet() | 
|  | } | 
|  |  | 
|  | // This can happen if the client has snippets disabled but the | 
|  | // candidate only supports snippet insertion. | 
|  | if insertText == "" { | 
|  | continue | 
|  | } | 
|  |  | 
|  | item := protocol.CompletionItem{ | 
|  | Label:  candidate.Label, | 
|  | Detail: candidate.Detail, | 
|  | Kind:   candidate.Kind, | 
|  | TextEdit: &protocol.TextEdit{ | 
|  | NewText: insertText, | 
|  | Range:   rng, | 
|  | }, | 
|  | InsertTextFormat:    options.InsertTextFormat, | 
|  | AdditionalTextEdits: candidate.AdditionalTextEdits, | 
|  | // This is a hack so that the client sorts completion results in the order | 
|  | // according to their score. This can be removed upon the resolution of | 
|  | // https://github.com/Microsoft/language-server-protocol/issues/348. | 
|  | SortText: fmt.Sprintf("%05d", i), | 
|  |  | 
|  | // Trim operators (VSCode doesn't like weird characters in | 
|  | // filterText). | 
|  | FilterText: strings.TrimLeft(candidate.InsertText, "&*"), | 
|  |  | 
|  | Preselect:     i == 0, | 
|  | Documentation: candidate.Documentation, | 
|  | Tags:          candidate.Tags, | 
|  | Deprecated:    candidate.Deprecated, | 
|  | } | 
|  | items = append(items, item) | 
|  | } | 
|  | return items | 
|  | } |