@@ -282,6 +282,7 @@ func completionItemsForDef(ctx context.Context, file *file, declPath []ast.DeclA
282282 hasStart := false // Start is a newline or open parenthesis for the start of a definition
283283 hasTypeModifier := false
284284 hasDeclaration := false
285+ hasVisibilityModifier := false
285286 typeSpan := extractAroundOffset (file , offset ,
286287 func (tok token.Token ) bool {
287288 if isTokenTypeDelimiter (tok ) {
@@ -296,6 +297,8 @@ func completionItemsForDef(ctx context.Context, file *file, declPath []ast.DeclA
296297 hasDeclaration = hasDeclaration || isDeclaration
297298 _ , isFieldModifier := typeModifierSet [tok .Keyword ()]
298299 hasTypeModifier = hasTypeModifier || isFieldModifier
300+ _ , isVisibilityMod := visibilityModifierSet [tok .Keyword ()]
301+ hasVisibilityModifier = hasVisibilityModifier || isVisibilityMod
299302 }
300303 }
301304 if isTokenSpace (tok ) {
@@ -340,6 +343,7 @@ func completionItemsForDef(ctx context.Context, file *file, declPath []ast.DeclA
340343 slog .Bool ("has_start" , hasStart ),
341344 slog .Bool ("has_field_modifier" , hasTypeModifier ),
342345 slog .Bool ("has_declaration" , hasDeclaration ),
346+ slog .Bool ("has_visibility_modifier" , hasVisibilityModifier ),
343347 slog .Bool ("inside_map_type" , insideMapType ),
344348 slog .Bool ("is_map_key_position" , isMapKeyPosition ),
345349 )
@@ -400,13 +404,29 @@ func completionItemsForDef(ctx context.Context, file *file, declPath []ast.DeclA
400404 return completionItemsForOptions (ctx , file , parentDef , def , offset )
401405 }
402406
403- // If at the top level, and on the first item, return top level keywords.
407+ // If at the top level, return top level keywords.
404408 if parentDef .IsZero () {
405- showKeywords := beforeCount == 0
406- if showKeywords {
409+ editions := isEditions (file )
410+ switch {
411+ case beforeCount == 0 :
412+ // At the start of a definition: show all top-level keywords, plus
413+ // visibility modifiers in edition 2024+ files.
407414 file .lsp .logger .DebugContext (ctx , "completion: definition returning top-level keywords" )
415+ kws := topLevelKeywords ()
416+ if editions {
417+ kws = joinSequences (kws , visibilityModifierKeywords ())
418+ }
419+ return slices .Collect (keywordToCompletionItem (
420+ kws ,
421+ protocol .CompletionItemKindKeyword ,
422+ tokenSpan ,
423+ offset ,
424+ ))
425+ case editions && beforeCount == 1 && hasVisibilityModifier :
426+ // After export/local, only type declaration keywords are valid.
427+ file .lsp .logger .DebugContext (ctx , "completion: definition returning top-level type declaration keywords after visibility modifier" )
408428 return slices .Collect (keywordToCompletionItem (
409- topLevelKeywords (),
429+ topLevelTypeDeclarationKeywords (),
410430 protocol .CompletionItemKindKeyword ,
411431 tokenSpan ,
412432 offset ,
@@ -422,13 +442,13 @@ func completionItemsForDef(ctx context.Context, file *file, declPath []ast.DeclA
422442 // - Show keywords for the first values (but not when inside map key position)
423443 // - Show types if no type declaration and at first, or second position with field modifier.
424444 // - Always show types if cursor is inside map<...> angle brackets
445+ editions := isEditions (file )
425446 showKeywords := beforeCount == 0 && ! (insideMapType && isMapKeyPosition )
426447 showTypes := insideMapType || (! hasDeclaration && (beforeCount == 0 || (hasTypeModifier && beforeCount == 1 )))
427448 if showKeywords {
428- isProto2 := isProto2 (file )
429449 iters = append (iters ,
430450 keywordToCompletionItem (
431- messageLevelKeywords (isProto2 ),
451+ messageLevelKeywords (isProto2 ( file ) ),
432452 protocol .CompletionItemKindKeyword ,
433453 tokenSpan ,
434454 offset ,
@@ -440,6 +460,22 @@ func completionItemsForDef(ctx context.Context, file *file, declPath []ast.DeclA
440460 offset ,
441461 ),
442462 )
463+ if editions {
464+ iters = append (iters , keywordToCompletionItem (
465+ visibilityModifierKeywords (),
466+ protocol .CompletionItemKindKeyword ,
467+ tokenSpan ,
468+ offset ,
469+ ))
470+ }
471+ } else if editions && beforeCount == 1 && hasVisibilityModifier {
472+ // After export/local inside a message, only nested type declarations are valid.
473+ iters = append (iters , keywordToCompletionItem (
474+ messageLevelTypeDeclarationKeywords (),
475+ protocol .CompletionItemKindKeyword ,
476+ tokenSpan ,
477+ offset ,
478+ ))
443479 }
444480 if showTypes {
445481 // When inside map angle brackets, use only the prefix of the tokenSpan for filtering
@@ -818,6 +854,15 @@ var typeModifierSet = func() map[keyword.Keyword]struct{} {
818854 return m
819855}()
820856
857+ // visibilityModifierSet is the set of edition 2024+ visibility modifier keywords.
858+ var visibilityModifierSet = func () map [keyword.Keyword ]struct {} {
859+ m := make (map [keyword.Keyword ]struct {})
860+ for kw := range visibilityModifierKeywords () {
861+ m [kw ] = struct {}{}
862+ }
863+ return m
864+ }()
865+
821866// topLevelKeywords returns keywords for the top-level.
822867func topLevelKeywords () iter.Seq [keyword.Keyword ] {
823868 return func (yield func (keyword.Keyword ) bool ) {
@@ -833,6 +878,33 @@ func topLevelKeywords() iter.Seq[keyword.Keyword] {
833878 }
834879}
835880
881+ // topLevelTypeDeclarationKeywords returns the type declaration keywords that can
882+ // follow a visibility modifier (export/local) at the top level in edition 2024+.
883+ func topLevelTypeDeclarationKeywords () iter.Seq [keyword.Keyword ] {
884+ return func (yield func (keyword.Keyword ) bool ) {
885+ _ = yield (keyword .Message ) &&
886+ yield (keyword .Enum ) &&
887+ yield (keyword .Service )
888+ }
889+ }
890+
891+ // messageLevelTypeDeclarationKeywords returns the type declaration keywords that can
892+ // follow a visibility modifier (export/local) inside a message in edition 2024+.
893+ func messageLevelTypeDeclarationKeywords () iter.Seq [keyword.Keyword ] {
894+ return func (yield func (keyword.Keyword ) bool ) {
895+ _ = yield (keyword .Message ) &&
896+ yield (keyword .Enum )
897+ }
898+ }
899+
900+ // visibilityModifierKeywords returns the visibility modifier keywords for edition 2024+.
901+ func visibilityModifierKeywords () iter.Seq [keyword.Keyword ] {
902+ return func (yield func (keyword.Keyword ) bool ) {
903+ _ = yield (keyword .Export ) &&
904+ yield (keyword .Local )
905+ }
906+ }
907+
836908// messageLevelKeywords returns keywords for messages.
837909func messageLevelKeywords (isProto2 bool ) iter.Seq [keyword.Keyword ] {
838910 return func (yield func (keyword.Keyword ) bool ) {
@@ -1782,6 +1854,11 @@ func isProto2(file *file) bool {
17821854 return file .ir .Syntax () == syntax .Proto2
17831855}
17841856
1857+ // isEditions returns true if the file uses editions syntax.
1858+ func isEditions (file * file ) bool {
1859+ return file .ir .Syntax ().IsEdition ()
1860+ }
1861+
17851862// findTypeBySpan returns the IR Type that corresponds to the given AST span.
17861863// Returns a zero Type if no matching type is found.
17871864func findTypeBySpan (file * file , span source.Span ) ir.Type {
0 commit comments