Skip to content

Commit 85e8b3c

Browse files
authored
LSP handle empty nested completions (#4160)
1 parent ef0782b commit 85e8b3c

File tree

3 files changed

+35
-54
lines changed

3 files changed

+35
-54
lines changed

private/buf/buflsp/completion.go

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ func getCompletionItems(
5656

5757
// This grabs the contents of the file as the top-level [ast.DeclBody], see [ast.File].Decls()
5858
// for reference.
59-
declPath := getDeclForPosition(id.Wrap(file.ir.AST(), id.ID[ast.DeclBody](1)), position)
59+
offset := positionToOffset(file, position)
60+
declPath := getDeclForOffset(id.Wrap(file.ir.AST(), id.ID[ast.DeclBody](1)), offset)
6061
if len(declPath) > 0 {
6162
decl := declPath[len(declPath)-1]
6263
file.lsp.logger.DebugContext(
@@ -112,9 +113,7 @@ func completionItemsForDeclPath(ctx context.Context, file *file, declPath []ast.
112113
func completionItemsForSyntax(ctx context.Context, file *file, syntaxDecl ast.DeclSyntax, position protocol.Position) []protocol.CompletionItem {
113114
file.lsp.logger.DebugContext(ctx, "completion: syntax declaration", slog.Bool("is_edition", syntaxDecl.IsEdition()))
114115

115-
positionLocation := file.file.InverseLocation(int(position.Line)+1, int(position.Character)+1, positionalEncoding)
116-
offset := positionLocation.Offset
117-
116+
offset := positionToOffset(file, position)
118117
valueSpan := syntaxDecl.Value().Span()
119118
start, end := valueSpan.Start, valueSpan.End
120119
valueText := valueSpan.Text()
@@ -227,12 +226,27 @@ func completionItemsForPackage(ctx context.Context, file *file, syntaxPackage as
227226

228227
// completionItemsForDef returns completion items for definition declarations (message, enum, service, etc.).
229228
func completionItemsForDef(ctx context.Context, file *file, declPath []ast.DeclAny, def ast.DeclDef, position protocol.Position) []protocol.CompletionItem {
229+
offset := positionToOffset(file, position)
230+
inBody := offsetInSpan(offset, def.Body().Span()) == 0
231+
232+
// Parent declaration determines child completions.
233+
var parentDef ast.DeclDef
234+
if len(declPath) >= 2 {
235+
parentDef = declPath[len(declPath)-2].AsDef()
236+
}
237+
if inBody {
238+
parentDef = def
239+
def = ast.DeclDef{} // Mark current def as invalid.
240+
}
241+
230242
file.lsp.logger.DebugContext(
231243
ctx,
232244
"completion: definition",
233245
slog.String("type", def.Type().Span().String()),
234246
slog.String("name", def.Name().Span().String()),
235247
slog.String("kind", def.Classify().String()),
248+
slog.String("parent_kind", parentDef.Classify().String()),
249+
slog.Bool("in_body", inBody),
236250
slog.Int("path_depth", len(declPath)),
237251
)
238252

@@ -248,9 +262,6 @@ func completionItemsForDef(ctx context.Context, file *file, declPath []ast.DeclA
248262
return nil
249263
}
250264

251-
positionLocation := file.file.InverseLocation(int(position.Line)+1, int(position.Character)+1, positionalEncoding)
252-
offset := positionLocation.Offset
253-
254265
tokenSpan := extractAroundOffset(file, offset, isTokenType, isTokenType)
255266
tokenPrefix, tokenSuffix := splitSpan(tokenSpan, offset)
256267

@@ -323,7 +334,7 @@ func completionItemsForDef(ctx context.Context, file *file, declPath []ast.DeclA
323334
}
324335

325336
// If at the top level, and on the first item, return top level keywords.
326-
if len(declPath) == 1 {
337+
if parentDef.IsZero() {
327338
showKeywords := beforeCount == 0
328339
if showKeywords {
329340
file.lsp.logger.DebugContext(ctx, "completion: definition returning top-level keywords")
@@ -337,17 +348,6 @@ func completionItemsForDef(ctx context.Context, file *file, declPath []ast.DeclA
337348
return nil // unknown
338349
}
339350

340-
// Parent declaration determines child completions.
341-
parent := declPath[len(declPath)-2]
342-
if parent.Kind() != ast.DeclKindDef {
343-
return nil
344-
}
345-
parentDef := parent.AsDef()
346-
file.lsp.logger.DebugContext(
347-
ctx, "completion: definition nested declaration",
348-
slog.String("kind", parentDef.Classify().String()),
349-
)
350-
351351
if offsetInSpan(offset, def.Options().Span()) == 0 {
352352
// TODO: Handle option completions within options block.
353353
file.lsp.logger.DebugContext(ctx, "completion: ignoring options block completion")
@@ -496,9 +496,7 @@ func completionItemsForDef(ctx context.Context, file *file, declPath []ast.DeclA
496496
func completionItemsForImport(ctx context.Context, file *file, declImport ast.DeclImport, position protocol.Position) []protocol.CompletionItem {
497497
file.lsp.logger.DebugContext(ctx, "completion: import declaration", slog.Int("importable_count", len(file.workspace.PathToFile())))
498498

499-
positionLocation := file.file.InverseLocation(int(position.Line)+1, int(position.Character)+1, positionalEncoding)
500-
offset := positionLocation.Offset
501-
499+
offset := positionToOffset(file, position)
502500
importPathSpan := declImport.ImportPath().Span()
503501
start, end := importPathSpan.Start, importPathSpan.End
504502
importPathText := importPathSpan.Text()
@@ -845,15 +843,15 @@ func joinSequences[T any](itemIters ...iter.Seq[T]) iter.Seq[T] {
845843
}
846844
}
847845

848-
// getDeclForPosition finds the path of AST declarations from parent to smallest that contains the given protocol position.
846+
// getDeclForOffset finds the path of AST declarations from parent to smallest that contains the given offset.
849847
// Returns a slice where [0] is the top-level declaration and [len-1] is the smallest/innermost declaration.
850-
// Returns nil if no declaration contains the position.
851-
func getDeclForPosition(body ast.DeclBody, position protocol.Position) []ast.DeclAny {
852-
return getDeclForPositionHelper(body, position, nil)
848+
// Returns nil if no declaration contains the offset.
849+
func getDeclForOffset(body ast.DeclBody, offset int) []ast.DeclAny {
850+
return getDeclForOffsetHelper(body, offset, nil)
853851
}
854852

855-
// getDeclForPositionHelper is the recursive helper for getDeclForPosition.
856-
func getDeclForPositionHelper(body ast.DeclBody, position protocol.Position, path []ast.DeclAny) []ast.DeclAny {
853+
// getDeclForOffsetHelper is the recursive helper for getDeclForOffset.
854+
func getDeclForOffsetHelper(body ast.DeclBody, offset int, path []ast.DeclAny) []ast.DeclAny {
857855
smallestSize := -1
858856
var bestPath []ast.DeclAny
859857

@@ -865,10 +863,8 @@ func getDeclForPositionHelper(body ast.DeclBody, position protocol.Position, pat
865863
if span.IsZero() {
866864
continue
867865
}
868-
869866
// Check if the position is within this declaration's span.
870-
within := reportSpanToProtocolRange(span)
871-
if positionInRange(position, within) == 0 {
867+
if offsetInSpan(offset, span) == 0 {
872868
// Build the new path including this declaration.
873869
newPath := append(append([]ast.DeclAny(nil), path...), decl)
874870
size := span.End - span.Start
@@ -879,7 +875,7 @@ func getDeclForPositionHelper(body ast.DeclBody, position protocol.Position, pat
879875

880876
// If this is a definition with a body, search recursively.
881877
if decl.Kind() == ast.DeclKindDef && !decl.AsDef().Body().IsZero() {
882-
childPath := getDeclForPositionHelper(decl.AsDef().Body(), position, newPath)
878+
childPath := getDeclForOffsetHelper(decl.AsDef().Body(), offset, newPath)
883879
if len(childPath) > 0 {
884880
childSize := childPath[len(childPath)-1].Span().End - childPath[len(childPath)-1].Span().Start
885881
if childSize < smallestSize {
@@ -974,6 +970,12 @@ func offsetInSpan(offset int, span report.Span) int {
974970
return 0
975971
}
976972

973+
// positionToOffset returns the offset from the protocol position.
974+
func positionToOffset(file *file, position protocol.Position) int {
975+
positionLocation := file.file.InverseLocation(int(position.Line)+1, int(position.Character)+1, positionalEncoding)
976+
return positionLocation.Offset
977+
}
978+
977979
// isProto2 returns true if the file has a syntax declaration of proto2.
978980
func isProto2(file *file) bool {
979981
return file.ir.Syntax() == syntax.Proto2

private/buf/buflsp/file.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -740,8 +740,7 @@ func (f *file) messageToSymbolsHelper(msg ir.MessageValue, index int, parents []
740740
//
741741
// Returns nil if no symbol is found.
742742
func (f *file) SymbolAt(ctx context.Context, cursor protocol.Position) *symbol {
743-
cursorLocation := f.file.InverseLocation(int(cursor.Line)+1, int(cursor.Character)+1, positionalEncoding)
744-
offset := cursorLocation.Offset
743+
offset := positionToOffset(f, cursor)
745744

746745
// Binary search for insertion point based on Start.
747746
idx, _ := slices.BinarySearchFunc(f.symbols, offset, func(sym *symbol, offset int) int {

private/buf/buflsp/symbol.go

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -465,26 +465,6 @@ func commentToMarkdown(comment string) string {
465465
return strings.TrimSuffix(strings.TrimPrefix(comment, "/*"), "*/")
466466
}
467467

468-
// comparePositions compares two positions for lexicographic ordering.
469-
func comparePositions(a, b protocol.Position) int {
470-
diff := int(a.Line) - int(b.Line)
471-
if diff == 0 {
472-
return int(a.Character) - int(b.Character)
473-
}
474-
return diff
475-
}
476-
477-
// positionInRange returns 0 if a position is within the range, else returns -1 before or 1 after.
478-
func positionInRange(position protocol.Position, within protocol.Range) int {
479-
if comparePositions(position, within.Start) < 0 {
480-
return -1
481-
}
482-
if comparePositions(position, within.End) > 0 {
483-
return 1
484-
}
485-
return 0
486-
}
487-
488468
func reportSpanToProtocolRange(span report.Span) protocol.Range {
489469
startLocation := span.File.Location(span.Start, positionalEncoding)
490470
endLocation := span.File.Location(span.End, positionalEncoding)

0 commit comments

Comments
 (0)