Skip to content

Commit 29d4341

Browse files
authored
LSP add completion for service methods (#4147)
1 parent 8cec659 commit 29d4341

File tree

1 file changed

+141
-53
lines changed

1 file changed

+141
-53
lines changed

private/buf/buflsp/completion.go

Lines changed: 141 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -237,11 +237,12 @@ func completionItemsForDef(ctx context.Context, file *file, declPath []ast.DeclA
237237
)
238238

239239
switch def.Classify() {
240-
case ast.DefKindMessage, ast.DefKindService, ast.DefKindEnum:
241-
return nil // ignored
242-
case ast.DefKindField, ast.DefKindInvalid:
243-
// Use DefKindField and DefKindInvalid as completion starts.
244-
// An invalid field is caused from partial values, with still invalid syntax.
240+
case ast.DefKindMessage, ast.DefKindService, ast.DefKindEnum, ast.DefKindGroup:
241+
// Ignore these kinds as this will be a completion for the name of the declaration.
242+
return nil
243+
case ast.DefKindField, ast.DefKindMethod, ast.DefKindInvalid:
244+
// Use these kinds as completion starts.
245+
// An invalid kind is caused from partial values, which may be any kind.
245246
default:
246247
file.lsp.logger.DebugContext(ctx, "completion: unknown definition type", slog.String("kind", def.Classify().String()))
247248
return nil
@@ -257,56 +258,47 @@ func completionItemsForDef(ctx context.Context, file *file, declPath []ast.DeclA
257258
// Use the token stream to capture invalid declarations.
258259
beforeCount, afterCount := 0, 0
259260
hasBeforeGap, hasAfterGap := false, false
260-
hasBeforeNewline, hasAfterNewline := false, false
261-
hasFieldModifier := false
261+
hasStart := false // Start is a newline or open parenthesis for the start of a definition
262+
hasTypeModifier := false
262263
hasDeclaration := false
263264
typeSpan := extractAroundOffset(file, offset,
264265
func(tok token.Token) bool {
266+
if isTokenTypeDelimiter(tok) {
267+
hasStart = true
268+
return false
269+
}
265270
if hasBeforeGap {
266271
beforeCount += 1
267272
hasBeforeGap = false
268273
if kw := tok.Keyword(); kw != keyword.Unknown {
269274
_, isDeclaration := declarationSet[tok.Keyword()]
270275
hasDeclaration = hasDeclaration || isDeclaration
271-
_, isFieldModifier := fieldModifierSet[tok.Keyword()]
272-
hasFieldModifier = hasFieldModifier || isFieldModifier
276+
_, isFieldModifier := typeModifierSet[tok.Keyword()]
277+
hasTypeModifier = hasTypeModifier || isFieldModifier
273278
}
274279
}
275-
if isTokenNewline(tok) {
276-
hasBeforeNewline = true
277-
return false
278-
}
279280
if isTokenSpace(tok) {
280281
hasBeforeGap = true
281282
return true
282283
}
283-
return isTokenType(tok)
284+
return isTokenType(tok) || isTokenParen(tok)
284285
},
285286
func(tok token.Token) bool {
286287
if hasAfterGap {
287288
afterCount += 1
288289
hasAfterGap = false
289290
}
290-
if isTokenNewline(tok) {
291-
hasAfterNewline = true
291+
if isTokenTypeDelimiter(tok) {
292292
return false
293293
}
294294
if isTokenSpace(tok) {
295295
hasAfterGap = true
296296
return true
297297
}
298-
return isTokenType(tok)
298+
return isTokenType(tok) || isTokenParen(tok)
299299
},
300300
)
301301
typePrefix, typeSuffix := splitSpan(typeSpan, offset)
302-
if !hasBeforeNewline {
303-
file.lsp.logger.DebugContext(
304-
ctx,
305-
"completion: ignoring definition type not on newline",
306-
slog.String("kind", def.Classify().String()),
307-
)
308-
return nil
309-
}
310302
file.lsp.logger.DebugContext(
311303
ctx, "completion: definition value",
312304
slog.String("token", tokenSpan.Text()),
@@ -317,15 +309,23 @@ func completionItemsForDef(ctx context.Context, file *file, declPath []ast.DeclA
317309
slog.String("type_suffix", typeSuffix),
318310
slog.Int("before_count", beforeCount),
319311
slog.Int("after_count", afterCount),
320-
slog.Bool("has_before_newline", hasBeforeNewline),
321-
slog.Bool("has_after_newline", hasAfterNewline),
322-
slog.Bool("has_field_modifier", hasFieldModifier),
312+
slog.Bool("has_start", hasStart),
313+
slog.Bool("has_field_modifier", hasTypeModifier),
323314
slog.Bool("has_declaration", hasDeclaration),
324315
)
316+
if !hasStart {
317+
file.lsp.logger.DebugContext(
318+
ctx,
319+
"completion: ignoring definition type unable to find start",
320+
slog.String("kind", def.Classify().String()),
321+
)
322+
return nil
323+
}
325324

326325
// If at the top level, and on the first item, return top level keywords.
327326
if len(declPath) == 1 {
328-
if beforeCount == 0 {
327+
showKeywords := beforeCount == 0
328+
if showKeywords {
329329
file.lsp.logger.DebugContext(ctx, "completion: definition returning top-level keywords")
330330
return slices.Collect(keywordToCompletionItem(
331331
topLevelKeywords(),
@@ -337,6 +337,7 @@ func completionItemsForDef(ctx context.Context, file *file, declPath []ast.DeclA
337337
return nil // unknown
338338
}
339339

340+
// Parent declaration determines child completions.
340341
parent := declPath[len(declPath)-2]
341342
if parent.Kind() != ast.DeclKindDef {
342343
return nil
@@ -347,22 +348,20 @@ func completionItemsForDef(ctx context.Context, file *file, declPath []ast.DeclA
347348
slog.String("kind", parentDef.Classify().String()),
348349
)
349350

350-
if offsetInSpan(def.Options().Span(), offset) {
351+
if offsetInSpan(offset, def.Options().Span()) == 0 {
351352
// TODO: Handle option completions within options block.
352353
file.lsp.logger.DebugContext(ctx, "completion: ignoring options block completion")
353354
return nil
354355
}
355356

356-
// Limit completions based on the following heuristic:
357-
// - Show keywords for the first values
358-
// - Show types if no type declaration and at first, or second position with field modifier.
359-
showKeywords := beforeCount == 0
360-
showTypes := !hasDeclaration &&
361-
((hasFieldModifier && beforeCount == 1) || (beforeCount == 0))
362-
363357
var iters []iter.Seq[protocol.CompletionItem]
364358
switch parentDef.Classify() {
365359
case ast.DefKindMessage:
360+
// Limit completions based on the following heuristics:
361+
// - Show keywords for the first values
362+
// - Show types if no type declaration and at first, or second position with field modifier.
363+
showKeywords := beforeCount == 0
364+
showTypes := !hasDeclaration && (beforeCount == 0 || (hasTypeModifier && beforeCount == 1))
366365
if showKeywords {
367366
isProto2 := isProto2(file)
368367
iters = append(iters,
@@ -393,36 +392,89 @@ func completionItemsForDef(ctx context.Context, file *file, declPath []ast.DeclA
393392
findTypeFullName(file, parentDef),
394393
tokenSpan,
395394
offset,
395+
true, // Allow enums.
396396
),
397397
)
398398
}
399399
case ast.DefKindService:
400+
// Method types are only shown within args of the method definition.
401+
// Use the type to handle invalid defs.
402+
defMethod := def.AsMethod()
403+
isRPC := defMethod.Keyword.Keyword() == keyword.RPC ||
404+
strings.HasPrefix(typeSpan.Text(), keyword.RPC.String())
405+
406+
// Limit complitions based on the following heuristics:
407+
// - Show keywords for the first values.
408+
// - Show return keyword if an RPC method and at the third position.
409+
// - Show types if an RPC method and within the Input or Output args.
410+
showKeywords := beforeCount == 0
411+
showReturnKeyword := isRPC &&
412+
(beforeCount == 2 || offsetInSpan(offset, defMethod.Signature.Returns().Span()) == 0)
413+
showTypes := isRPC && !hasDeclaration &&
414+
(beforeCount == 0 || (hasTypeModifier && beforeCount == 1)) &&
415+
(offsetInSpan(offset, defMethod.Signature.Inputs().Span()) == 0 ||
416+
offsetInSpan(offset, defMethod.Signature.Outputs().Span()) == 0)
417+
400418
if showKeywords {
419+
// If both showKeywords and showTypes is set, we are in a services method args.
420+
if showTypes {
421+
iters = append(iters,
422+
keywordToCompletionItem(
423+
methodArgLevelKeywords(),
424+
protocol.CompletionItemKindKeyword,
425+
tokenSpan,
426+
offset,
427+
),
428+
)
429+
} else {
430+
iters = append(iters,
431+
keywordToCompletionItem(
432+
serviceLevelKeywords(),
433+
protocol.CompletionItemKindKeyword,
434+
tokenSpan,
435+
offset,
436+
),
437+
)
438+
}
439+
} else if showReturnKeyword {
401440
iters = append(iters,
402441
keywordToCompletionItem(
403-
serviceLevelKeywords(),
442+
serviceReturnKeyword(),
404443
protocol.CompletionItemKindKeyword,
405444
tokenSpan,
406445
offset,
407446
),
408447
)
409448
}
449+
if showTypes {
450+
iters = append(iters,
451+
typeReferencesToCompletionItems(
452+
file,
453+
"", // No parent type within a service declaration.
454+
tokenSpan,
455+
offset,
456+
false, // Disallow enums.
457+
),
458+
)
459+
}
410460
case ast.DefKindMethod:
461+
showKeywords := beforeCount == 0
411462
if showKeywords {
412463
iters = append(iters,
413464
keywordToCompletionItem(
414-
optionKeyword(),
465+
optionKeywords(),
415466
protocol.CompletionItemKindKeyword,
416467
tokenSpan,
417468
offset,
418469
),
419470
)
420471
}
421472
case ast.DefKindEnum:
473+
showKeywords := beforeCount == 0
422474
if showKeywords {
423475
iters = append(iters,
424476
keywordToCompletionItem(
425-
optionKeyword(),
477+
optionKeywords(),
426478
protocol.CompletionItemKindKeyword,
427479
tokenSpan,
428480
offset,
@@ -544,10 +596,13 @@ var declarationSet = func() map[keyword.Keyword]struct{} {
544596
return m
545597
}()
546598

547-
// fieldModifierSet is the set of keywords for type modifiers.
548-
var fieldModifierSet = func() map[keyword.Keyword]struct{} {
599+
// typeModifierSet is the set of keywords for type modifiers.
600+
var typeModifierSet = func() map[keyword.Keyword]struct{} {
549601
m := make(map[keyword.Keyword]struct{})
550-
for keyword := range messageLevelFieldKeywords() {
602+
for keyword := range joinSequences(
603+
messageLevelFieldKeywords(),
604+
methodArgLevelKeywords(),
605+
) {
551606
m[keyword] = struct{}{}
552607
}
553608
return m
@@ -621,8 +676,22 @@ func serviceLevelKeywords() iter.Seq[keyword.Keyword] {
621676
}
622677
}
623678

624-
// optionKeyword returns the option keywords for methods and enums.
625-
func optionKeyword() iter.Seq[keyword.Keyword] {
679+
// serviceReturnKeyword returns keyword for service "return" value.
680+
func serviceReturnKeyword() iter.Seq[keyword.Keyword] {
681+
return func(yield func(keyword.Keyword) bool) {
682+
_ = yield(keyword.Returns)
683+
}
684+
}
685+
686+
// methodArgLevelKeywords returns keyword for methods.
687+
func methodArgLevelKeywords() iter.Seq[keyword.Keyword] {
688+
return func(yield func(keyword.Keyword) bool) {
689+
_ = yield(keyword.Stream)
690+
}
691+
}
692+
693+
// optionKeywords returns the option keywords for methods and enums.
694+
func optionKeywords() iter.Seq[keyword.Keyword] {
626695
return func(yield func(keyword.Keyword) bool) {
627696
_ = yield(keyword.Option)
628697
}
@@ -663,6 +732,7 @@ func typeReferencesToCompletionItems(
663732
parentFullName ir.FullName,
664733
span report.Span,
665734
offset int,
735+
allowEnums bool,
666736
) iter.Seq[protocol.CompletionItem] {
667737
fileSymbolTypesIter := func(yield func(*file, *symbol) bool) {
668738
for _, imported := range current.workspace.PathToFile() {
@@ -713,12 +783,15 @@ func typeReferencesToCompletionItems(
713783
case ir.SymbolKindMessage:
714784
kind = protocol.CompletionItemKindClass // Messages are like classes
715785
case ir.SymbolKindEnum:
786+
if !allowEnums {
787+
continue
788+
}
716789
kind = protocol.CompletionItemKindEnum
717790
default:
718791
continue // Unsupported kind, skip it.
719792
}
720793
label := string(symbol.ir.FullName())
721-
if strings.HasPrefix(label, parentPrefix) {
794+
if len(parentFullName) > 0 && strings.HasPrefix(label, parentPrefix) {
722795
label = label[len(parentPrefix):]
723796
} else if strings.HasPrefix(label, packagePrefix) {
724797
label = label[len(packagePrefix):]
@@ -829,8 +902,17 @@ func isTokenSpace(tok token.Token) bool {
829902
return tok.Kind() == token.Space && strings.IndexByte(tok.Text(), '\n') == -1
830903
}
831904

832-
func isTokenNewline(tok token.Token) bool {
833-
return tok.Kind() == token.Space && strings.IndexByte(tok.Text(), '\n') != -1
905+
func isTokenParen(tok token.Token) bool {
906+
return tok.Kind() == token.Punct &&
907+
(strings.HasPrefix(tok.Text(), "(") ||
908+
strings.HasSuffix(tok.Text(), ")"))
909+
}
910+
911+
func isTokenTypeDelimiter(tok token.Token) bool {
912+
kind := tok.Kind()
913+
return (kind == token.Unrecognized && tok.IsZero()) ||
914+
(kind == token.Space && strings.IndexByte(tok.Text(), '\n') != -1) ||
915+
(kind == token.Comment)
834916
}
835917

836918
// extractAroundOffset extracts the value around the offset by querying the token stream.
@@ -852,7 +934,7 @@ func extractAroundOffset(file *file, offset int, isTokenBefore, isTokenAfter fun
852934
Start: offset,
853935
End: offset,
854936
}
855-
if !before.IsZero() {
937+
if !before.IsZero() && isTokenBefore != nil {
856938
var firstToken token.Token
857939
for cursor := token.NewCursorAt(before); isTokenBefore(before); before = cursor.PrevSkippable() {
858940
firstToken = before
@@ -861,7 +943,7 @@ func extractAroundOffset(file *file, offset int, isTokenBefore, isTokenAfter fun
861943
span.Start = firstToken.Span().Start
862944
}
863945
}
864-
if !after.IsZero() {
946+
if !after.IsZero() && isTokenAfter != nil {
865947
var lastToken token.Token
866948
for cursor := token.NewCursorAt(after); isTokenAfter(after); after = cursor.NextSkippable() {
867949
lastToken = after
@@ -874,16 +956,22 @@ func extractAroundOffset(file *file, offset int, isTokenBefore, isTokenAfter fun
874956
}
875957

876958
func splitSpan(span report.Span, offset int) (prefix string, suffix string) {
877-
if !offsetInSpan(span, offset) {
959+
if offsetInSpan(offset, span) != 0 {
878960
return "", ""
879961
}
880962
index := offset - span.Start
881963
text := span.Text()
882964
return text[:index], text[index:]
883965
}
884966

885-
func offsetInSpan(span report.Span, offset int) bool {
886-
return span.Start <= offset && offset <= span.End // End is inclusive for completions_
967+
func offsetInSpan(offset int, span report.Span) int {
968+
if offset < span.Start {
969+
return -1
970+
} else if offset > span.End {
971+
// End is inclusive for completions _
972+
return 1
973+
}
974+
return 0
887975
}
888976

889977
// isProto2 returns true if the file has a syntax declaration of proto2.

0 commit comments

Comments
 (0)