@@ -27,6 +27,7 @@ import (
2727 "github.com/bufbuild/buf/private/pkg/normalpath"
2828 "github.com/bufbuild/protocompile/experimental/ast"
2929 "github.com/bufbuild/protocompile/experimental/ast/syntax"
30+ "github.com/bufbuild/protocompile/experimental/ir"
3031 "github.com/bufbuild/protocompile/experimental/report"
3132 "github.com/bufbuild/protocompile/experimental/seq"
3233 "github.com/bufbuild/protocompile/experimental/token"
@@ -197,7 +198,7 @@ func completionItemsForDef(ctx context.Context, file *file, declPath []ast.DeclA
197198 case ast .DefKindEnum :
198199 return nil
199200 case ast .DefKindField :
200- return completionItemsForField (ctx , file , declPath , def . AsField () , position )
201+ return completionItemsForField (ctx , file , declPath , def , position )
201202 case ast .DefKindInvalid :
202203 return completionItemsForKeyword (ctx , file , declPath , def , position )
203204 default :
@@ -210,11 +211,13 @@ func completionItemsForDef(ctx context.Context, file *file, declPath []ast.DeclA
210211func completionItemsForKeyword (ctx context.Context , file * file , declPath []ast.DeclAny , def ast.DeclDef , position protocol.Position ) []protocol.CompletionItem {
211212 positionLocation := file .file .InverseLocation (int (position .Line )+ 1 , int (position .Character )+ 1 , positionalEncoding )
212213 offset := positionLocation .Offset
213-
214214 span := def .Span ()
215215
216216 // Check if at newline or end of span. Keywords are restricted to the first identifier.
217217 if ! isNewlineOrEndOfSpan (span , offset ) {
218+ if len (declPath ) > 1 {
219+ return completionItemsForField (ctx , file , declPath , def , position )
220+ }
218221 file .lsp .logger .Debug ("completion: keyword skip on span bounds" , slog .String ("span" , span .Text ()))
219222 return nil
220223 }
@@ -247,7 +250,7 @@ func completionItemsForKeyword(ctx context.Context, file *file, declPath []ast.D
247250 switch parentDef .Classify () {
248251 case ast .DefKindMessage :
249252 isProto2 := isProto2 (file )
250- items = joinCompletionItems (
253+ items = joinSequences (
251254 keywordToCompletionItem (
252255 messageLevelKeywords (isProto2 ),
253256 protocol .CompletionItemKindKeyword ,
@@ -266,7 +269,11 @@ func completionItemsForKeyword(ctx context.Context, file *file, declPath []ast.D
266269 tokenSpan ,
267270 offset ,
268271 ),
269- // TODO: add custom types.
272+ typeReferencesToCompletionItems (
273+ file ,
274+ tokenSpan ,
275+ offset ,
276+ ),
270277 )
271278 case ast .DefKindService :
272279 items = keywordToCompletionItem (
@@ -289,10 +296,10 @@ func completionItemsForKeyword(ctx context.Context, file *file, declPath []ast.D
289296}
290297
291298// completionItemsForField returns completion items for a field.
292- func completionItemsForField (ctx context.Context , file * file , declPath []ast.DeclAny , defField ast.DefField , position protocol.Position ) []protocol.CompletionItem {
299+ func completionItemsForField (ctx context.Context , file * file , declPath []ast.DeclAny , def ast.DeclDef , position protocol.Position ) []protocol.CompletionItem {
293300 if len (declPath ) == 1 {
294301 file .lsp .logger .DebugContext (ctx , "completion: field top level, going to keywords" )
295- return completionItemsForKeyword (ctx , file , declPath , defField . Decl , position )
302+ return completionItemsForKeyword (ctx , file , declPath , def , position )
296303 }
297304
298305 parent := declPath [len (declPath )- 2 ]
@@ -308,33 +315,89 @@ func completionItemsForField(ctx context.Context, file *file, declPath []ast.Dec
308315 offset := positionLocation .Offset
309316
310317 // If on a newline, before the current field or at the end of the span return keywords.
311- if isNewlineOrEndOfSpan (defField .Span (), offset ) {
318+ if isNewlineOrEndOfSpan (def .Span (), offset ) {
312319 file .lsp .logger .DebugContext (ctx , "completion: field on newline, return keywords" )
313- return completionItemsForKeyword (ctx , file , declPath , defField . Decl , position )
320+ return completionItemsForKeyword (ctx , file , declPath , def , position )
314321 }
315322
316- typeSpan := defField .Type .Span ()
317- start , end := typeSpan .Start , typeSpan .End
318-
319- if start > offset || offset > end {
323+ typeSpan := def .Type ().Span ()
324+ if offsetInSpan (typeSpan , offset ) {
320325 file .lsp .logger .DebugContext (
321326 ctx , "completion: field outside definition" ,
322327 slog .String ("kind" , parent .Kind ().String ()),
323328 )
324329 return nil
325330 }
326331
327- def := parent .AsDef ()
328- switch def .Classify () {
332+ // Resolve the token the cursor is under. We don't use the ast.FieldStruct as this doesn't
333+ // work with invalid path types. Instead resolve the token from the stream.
334+ tokenSpan := extractAroundToken (file , offset )
335+ tokenPrefix , tokenSuffix := splitSpan (tokenSpan , offset )
336+ typePrefix , typeSuffix := splitSpan (typeSpan , offset )
337+ prefixCount := 0
338+ for range strings .FieldsSeq (strings .TrimSuffix (typePrefix , tokenPrefix )) {
339+ prefixCount ++
340+ }
341+ suffixCount := 0
342+ for range strings .FieldsSeq (strings .TrimPrefix (typeSuffix , tokenSuffix )) {
343+ suffixCount ++
344+ }
345+ // Limit completions based on the following heuristic:
346+ // - Show modifiers for the first two types
347+ // - Only show types on the final type
348+ showModifiers := prefixCount <= 2
349+ showTypes := suffixCount == 0
350+
351+ file .lsp .logger .DebugContext (
352+ ctx , "completion: got types" ,
353+ slog .String ("kind" , parent .Kind ().String ()),
354+ slog .String ("type" , typeSpan .Text ()),
355+ slog .String ("type_kind" , def .Type ().Kind ().String ()),
356+ slog .Int ("start" , typeSpan .Start ),
357+ slog .Int ("end" , typeSpan .End ),
358+ slog .Bool ("show_modifiers" , showModifiers ),
359+ slog .Bool ("show_types" , showTypes ),
360+ slog .String ("token" , tokenSpan .Text ()),
361+ )
362+
363+ var items iter.Seq [protocol.CompletionItem ]
364+ parentDef := parent .AsDef ()
365+ switch parentDef .Classify () {
329366 case ast .DefKindMessage :
330- return nil
331- case ast .DefKindService :
332- return nil
333- case ast .DefKindEnum :
334- return nil
367+ var iters []iter.Seq [protocol.CompletionItem ]
368+ if showModifiers {
369+ iters = append (iters ,
370+ keywordToCompletionItem (
371+ messageLevelFieldKeywords (),
372+ protocol .CompletionItemKindKeyword ,
373+ tokenSpan ,
374+ offset ,
375+ ),
376+ )
377+ }
378+ if showTypes {
379+ iters = append (iters ,
380+ keywordToCompletionItem (
381+ predeclaredTypeKeywords (),
382+ protocol .CompletionItemKindClass ,
383+ tokenSpan ,
384+ offset ,
385+ ),
386+ typeReferencesToCompletionItems (
387+ file ,
388+ tokenSpan ,
389+ offset ,
390+ ),
391+ )
392+ }
393+ if len (iters ) == 0 {
394+ return nil
395+ }
396+ items = joinSequences (iters ... )
335397 default :
336398 return nil
337399 }
400+ return slices .Collect (items )
338401}
339402
340403// completionItemsForImport returns completion items for import declarations.
@@ -504,9 +567,80 @@ func keywordToCompletionItem(
504567 }
505568}
506569
507- // joinCompletionItems returns a sequence of sequences.
508- func joinCompletionItems (itemIters ... iter.Seq [protocol.CompletionItem ]) iter.Seq [protocol.CompletionItem ] {
570+ // typeReferencesToCompletionItems returns completion items for user-defined types (messages, enums, etc).
571+ func typeReferencesToCompletionItems (
572+ current * file ,
573+ span report.Span ,
574+ offset int ,
575+ ) iter.Seq [protocol.CompletionItem ] {
576+ fileSymbolTypesIter := func (yield func (* file , * symbol ) bool ) {
577+ for _ , imported := range current .importToFile {
578+ for _ , symbol := range imported .referenceableSymbols {
579+ if ! yield (imported , symbol ) {
580+ return
581+ }
582+ }
583+ }
584+ }
509585 return func (yield func (protocol.CompletionItem ) bool ) {
586+ editRange := reportSpanToProtocolRange (span )
587+ prefix , suffix := splitSpan (span , offset )
588+ for file , symbol := range fileSymbolTypesIter {
589+ if ! symbol .ir .Kind ().IsType () {
590+ continue
591+ }
592+ var (
593+ label string
594+ kind protocol.CompletionItemKind
595+ )
596+ if file .ir .Package () == current .ir .Package () {
597+ label = symbol .ir .FullName ().Name ()
598+ } else {
599+ label = string (symbol .ir .FullName ())
600+ }
601+ if ! strings .HasPrefix (label , prefix ) || ! strings .HasSuffix (label , suffix ) {
602+ continue
603+ }
604+ switch symbol .ir .Kind () {
605+ case ir .SymbolKindMessage :
606+ kind = protocol .CompletionItemKindStruct
607+ case ir .SymbolKindEnum :
608+ kind = protocol .CompletionItemKindEnum
609+ case ir .SymbolKindService :
610+ kind = protocol .CompletionItemKindInterface
611+ case ir .SymbolKindScalar :
612+ kind = protocol .CompletionItemKindClass
613+ case ir .SymbolKindPackage :
614+ kind = protocol .CompletionItemKindModule
615+ case ir .SymbolKindField , ir .SymbolKindEnumValue , ir .SymbolKindExtension , ir .SymbolKindOneof , ir .SymbolKindMethod :
616+ // These should be skipped by IsType() filter.
617+ }
618+ if kind == 0 {
619+ continue // Unsupported kind, skip it.
620+ }
621+ var isDeprecated bool
622+ if _ , ok := symbol .ir .Deprecated ().AsBool (); ok {
623+ isDeprecated = true
624+ }
625+ if ! yield (protocol.CompletionItem {
626+ Label : label ,
627+ Kind : kind ,
628+ TextEdit : & protocol.TextEdit {
629+ Range : editRange ,
630+ NewText : label ,
631+ },
632+ Deprecated : isDeprecated ,
633+ // TODO: If this type's file is not currently imported add an additional edit.
634+ }) {
635+ break
636+ }
637+ }
638+ }
639+ }
640+
641+ // joinSequences returns a sequence of sequences.
642+ func joinSequences [T any ](itemIters ... iter.Seq [T ]) iter.Seq [T ] {
643+ return func (yield func (T ) bool ) {
510644 for _ , items := range itemIters {
511645 for item := range items {
512646 if ! yield (item ) {
@@ -614,14 +748,18 @@ func extractAroundToken(file *file, offset int) report.Span {
614748}
615749
616750func splitSpan (span report.Span , offset int ) (prefix string , suffix string ) {
617- if span . Start > offset || offset > span . End {
751+ if ! offsetInSpan ( span , offset ) {
618752 return "" , ""
619753 }
620754 index := offset - span .Start
621755 text := span .Text ()
622756 return text [:index ], text [index :]
623757}
624758
759+ func offsetInSpan (span report.Span , offset int ) bool {
760+ return span .Start > offset || offset > span .End
761+ }
762+
625763// isNewlineOrEndOfSpan returns true if this offset is separated be a newline or at the end of the span.
626764// This most likely means we are at the start of a new declaration.
627765func isNewlineOrEndOfSpan (span report.Span , offset int ) bool {
0 commit comments