// 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/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 } if err != nil { event.Error(ctx, "no completions found", err, tag.Position.Of(params.Position)) } if candidates == nil { return &protocol.CompletionList{ 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, } items = append(items, item) } return items }