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  }