From 17b95acf9ab51565744ec981bb8368a9dbb37e93 Mon Sep 17 00:00:00 2001 From: Edward McFarlane Date: Wed, 19 Aug 2026 21:01:12 +0100 Subject: [PATCH 1/4] Fix LSP references on dependencies --- private/buf/buflsp/buflsp.go | 2 + private/buf/buflsp/file.go | 114 +++--------- private/buf/buflsp/reference_index.go | 140 +++++++++++++++ private/buf/buflsp/references_test.go | 162 ++++++++++++++++++ private/buf/buflsp/server.go | 12 +- private/buf/buflsp/symbol.go | 84 +++++---- .../testdata/references_dependency/buf.yaml | 9 + .../references_dependency/first.proto | 12 ++ .../references_dependency/second.proto | 11 ++ .../references_dependency/shared.proto | 7 + 10 files changed, 421 insertions(+), 132 deletions(-) create mode 100644 private/buf/buflsp/reference_index.go create mode 100644 private/buf/buflsp/testdata/references_dependency/buf.yaml create mode 100644 private/buf/buflsp/testdata/references_dependency/first.proto create mode 100644 private/buf/buflsp/testdata/references_dependency/second.proto create mode 100644 private/buf/buflsp/testdata/references_dependency/shared.proto diff --git a/private/buf/buflsp/buflsp.go b/private/buf/buflsp/buflsp.go index 2f05480017..28c8df83de 100644 --- a/private/buf/buflsp/buflsp.go +++ b/private/buf/buflsp/buflsp.go @@ -100,6 +100,7 @@ func Serve( } lsp.fileManager = newFileManager(lsp) lsp.workspaceManager = newWorkspaceManager(lsp) + lsp.referenceIndex = newReferenceIndex() lsp.bufYAMLManager = newBufYAMLManager(lsp) lsp.bufGenYAMLManager = newBufGenYAMLManager(lsp) lsp.bufPolicyYAMLManager = newBufPolicyYAMLManager() @@ -137,6 +138,7 @@ type lsp struct { wasmRuntime wasm.Runtime fileManager *fileManager workspaceManager *workspaceManager + referenceIndex *referenceIndex bufYAMLManager *bufYAMLManager bufGenYAMLManager *bufGenYAMLManager bufPolicyYAMLManager *bufPolicyYAMLManager diff --git a/private/buf/buflsp/file.go b/private/buf/buflsp/file.go index 3ee402f47f..0fb2e18d19 100644 --- a/private/buf/buflsp/file.go +++ b/private/buf/buflsp/file.go @@ -63,7 +63,6 @@ type file struct { ir *ir.File referenceableSymbols map[ir.FullName]*symbol - referenceSymbols []*symbol symbols []*symbol irReport *report.Report // IR diagnostic report for code actions diagnostics []protocol.Diagnostic // Converted LSP diagnostics @@ -98,6 +97,9 @@ func (f *file) Reset(ctx context.Context) { f.workspace.Release() f.workspace = nil } + // Drop this file's references. The index holds symbols pointing back at this file, which + // is zeroed below, so entries left behind would resolve to an empty URI. + f.lsp.referenceIndex.RemoveFile(f.uri) // Evict the query key if there is a query cached on the file. We cache the [queries.File] // query since this allows the executor to evict all dependent queries, e.g. AST and IR. f.lsp.queryExecutor.Evict(f.queryFileKeys()...) @@ -354,7 +356,6 @@ func (f *file) IndexSymbols(ctx context.Context) { // Throw away all the old symbols and rebuild symbols unconditionally. This is because if // this file depends on a file that has since been modified, we may need to update references. f.symbols = nil - f.referenceSymbols = nil f.referenceableSymbols = make(map[ir.FullName]*symbol) // Process all imports as symbols @@ -365,7 +366,6 @@ func (f *file) IndexSymbols(ctx context.Context) { resolved, unresolved := f.indexSymbols() f.symbols = append(f.symbols, resolved...) f.symbols = append(f.symbols, unresolved...) - f.referenceSymbols = append(f.referenceSymbols, unresolved...) // Index all referenceable symbols for _, sym := range resolved { @@ -376,47 +376,30 @@ func (f *file) IndexSymbols(ctx context.Context) { f.referenceableSymbols[sym.ir.FullName()] = sym } - // TODO: this could use a refactor, probably. + // Resolve all unresolved symbols from this file, and record the references they make in + // the reference index. // - // Resolve all unresolved symbols from this file + // A reference is recorded by definition site rather than by resolved symbol, so it is + // indexed whether or not the declaring file has been indexed yet. Resolution below only + // populates def and typeDef, which go-to-definition and hover need; find-references does + // not depend on it, and so no longer depends on the order files happen to be indexed in. + references := make(map[referenceKey][]*symbol) + addReference := func(def ast.DeclDef, fullName ir.FullName, sym *symbol) { + key, ok := newReferenceKey(def, fullName) + if !ok { + return + } + references[key] = append(references[key], sym) + } for _, sym := range unresolved { switch kind := sym.kind.(type) { case *reference: - def := f.resolveASTDefinition(kind.def, kind.fullName) - sym.def = def - if def == nil { - // In the case where the symbol is not resolved, we continue - continue - } - referenceable, ok := def.kind.(*referenceable) - if !ok { - // This shouldn't happen, logging a warning - f.lsp.logger.Warn( - "found non-referenceable symbol in index", - slog.String("file", f.uri.Filename()), - slog.Any("symbol", def), - ) - continue - } - referenceable.references = append(referenceable.references, sym) + sym.def = f.resolveASTDefinition(kind.def, kind.fullName) + addReference(kind.def, kind.fullName, sym) case *option: - def := f.resolveASTDefinition(kind.def, kind.defFullName) - sym.def = def - if def != nil { - referenceable, ok := def.kind.(*referenceable) - if !ok { - // This shouldn't happen, logging a warning - f.lsp.logger.Warn( - "found non-referenceable symbol in index", - slog.String("file", f.uri.Filename()), - slog.Any("symbol", def), - ) - } else { - referenceable.references = append(referenceable.references, sym) - } - } - typeDef := f.resolveASTDefinition(kind.typeDef, kind.typeDefFullName) - sym.typeDef = typeDef + sym.def = f.resolveASTDefinition(kind.def, kind.defFullName) + addReference(kind.def, kind.defFullName, sym) + sym.typeDef = f.resolveASTDefinition(kind.typeDef, kind.typeDefFullName) default: // This shouldn't happen, logging a warning f.lsp.logger.Warn( @@ -426,58 +409,7 @@ func (f *file) IndexSymbols(ctx context.Context) { ) } } - - // Resolve all references outside of this file to symbols in this file - for _, file := range f.workspace.PathToFile() { - if f == file { - continue // ignore self - } - for _, sym := range file.referenceSymbols { - var fullName ir.FullName - switch kind := sym.kind.(type) { - case *reference: - if kind.def.Span().Path() != f.objectInfo.LocalPath() { - continue - } - fullName = kind.fullName - case *option: - if kind.def.Span().Path() != f.objectInfo.LocalPath() { - continue - } - fullName = kind.defFullName - default: - // This shouldn't happen, logging a warning - f.lsp.logger.Warn( - "found unresolved non-reference and non-option symbol", - slog.String("file", f.uri.Filename()), - slog.Any("symbol", sym), - ) - continue - } - def, ok := f.referenceableSymbols[fullName] - if !ok { - // This shouldn't happen, if a symbol is pointing at this file, all definitions - // should be resolved, logging a warning - f.lsp.logger.Warn( - "found reference to unknown symbol", - slog.String("file", f.uri.Filename()), - slog.Any("reference", sym), - ) - continue - } - referenceable, ok := def.kind.(*referenceable) - if !ok { - // This shouldn't happen, logging a warning - f.lsp.logger.Warn( - "found non-referenceable symbol in index", - slog.String("file", f.uri.Filename()), - slog.Any("symbol", def), - ) - continue - } - referenceable.references = append(referenceable.references, sym) - } - } + f.lsp.referenceIndex.SetFile(f.uri, references) // Finally, sort the symbols in position order, with shorter symbols sorting smaller. slices.SortFunc(f.symbols, func(s1, s2 *symbol) int { diff --git a/private/buf/buflsp/reference_index.go b/private/buf/buflsp/reference_index.go new file mode 100644 index 0000000000..7b7213dfbb --- /dev/null +++ b/private/buf/buflsp/reference_index.go @@ -0,0 +1,140 @@ +// Copyright 2020-2026 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This file defines the reverse index from symbol definitions to their references. + +package buflsp + +import ( + "cmp" + "slices" + + "github.com/bufbuild/protocompile/experimental/ast" + "github.com/bufbuild/protocompile/experimental/ir" + "go.lsp.dev/protocol" +) + +// referenceKey identifies a symbol definition site. +// +// The key is the absolute local path of the file declaring the symbol plus the symbol's +// fully-qualified name. Keying on the definition's path, rather than on a resolved +// [symbol] pointer, means a reference can be recorded before the declaring file has been +// indexed. Including the path, rather than keying on the full name alone, keeps two +// workspaces that legitimately declare the same fully-qualified name from cross-linking. +type referenceKey struct { + path string + fullName ir.FullName +} + +// newReferenceKey builds the key for the definition an unresolved symbol names. +// +// Returns false if the definition has no span to take a path from, which happens for a +// zero [ast.DeclDef]. +func newReferenceKey(def ast.DeclDef, fullName ir.FullName) (referenceKey, bool) { + path := def.Span().Path() + if path == "" || fullName == "" { + return referenceKey{}, false + } + return referenceKey{path: path, fullName: fullName}, true +} + +// referenceIndex is a reverse index from a symbol definition to the symbols referencing it. +// +// The index is owned by the [lsp] rather than by a file or a workspace, so references +// resolve across every workspace the server has indexed. This matters for shared +// dependencies: a well-known type is reachable from many workspaces, but a file belongs to +// at most one, so a per-workspace index only ever sees a fraction of its references. +// +// References are grouped by the file containing them so that re-indexing a file replaces +// only that file's contribution, leaving every other file's references untouched. Indexing +// a file therefore costs time proportional to the references in that file alone. +// +// The index is not safe for concurrent use. It is protected by the [lsp] lock, which +// already serializes all request handling. +type referenceIndex struct { + // keyToFileRefs maps a definition to the referencing symbols, grouped by containing file. + keyToFileRefs map[referenceKey]map[protocol.URI][]*symbol + // uriToKeys records which definitions each file contributes references to, so that a + // file's contribution can be removed without scanning the whole index. + uriToKeys map[protocol.URI][]referenceKey +} + +// newReferenceIndex creates a new reference index. +func newReferenceIndex() *referenceIndex { + return &referenceIndex{ + keyToFileRefs: make(map[referenceKey]map[protocol.URI][]*symbol), + uriToKeys: make(map[protocol.URI][]referenceKey), + } +} + +// SetFile replaces all references contributed by the file at the given URI. +func (i *referenceIndex) SetFile(uri protocol.URI, references map[referenceKey][]*symbol) { + i.RemoveFile(uri) + if len(references) == 0 { + return + } + keys := make([]referenceKey, 0, len(references)) + for key, symbols := range references { + fileRefs, ok := i.keyToFileRefs[key] + if !ok { + fileRefs = make(map[protocol.URI][]*symbol, 1) + i.keyToFileRefs[key] = fileRefs + } + fileRefs[uri] = symbols + keys = append(keys, key) + } + i.uriToKeys[uri] = keys +} + +// RemoveFile drops all references contributed by the file at the given URI. +// +// This must be called when a file is evicted. The index holds [symbol] values that point +// back at their file, and an evicted file is zeroed, so entries left behind would resolve +// to locations with an empty URI. +func (i *referenceIndex) RemoveFile(uri protocol.URI) { + for _, key := range i.uriToKeys[uri] { + fileRefs, ok := i.keyToFileRefs[key] + if !ok { + continue + } + delete(fileRefs, uri) + if len(fileRefs) == 0 { + delete(i.keyToFileRefs, key) + } + } + delete(i.uriToKeys, uri) +} + +// References returns all symbols referencing the given definition. +// +// The result is ordered by file URI and then by position, so that results are stable +// across calls. The index is built from maps, whose iteration order is not. +func (i *referenceIndex) References(key referenceKey) []*symbol { + fileRefs := i.keyToFileRefs[key] + if len(fileRefs) == 0 { + return nil + } + var symbols []*symbol + for _, fileSymbols := range fileRefs { + symbols = append(symbols, fileSymbols...) + } + slices.SortFunc(symbols, func(symbol1, symbol2 *symbol) int { + return cmp.Or( + cmp.Compare(symbol1.file.uri, symbol2.file.uri), + cmp.Compare(symbol1.span.Start, symbol2.span.Start), + cmp.Compare(symbol1.span.End, symbol2.span.End), + ) + }) + return symbols +} diff --git a/private/buf/buflsp/references_test.go b/private/buf/buflsp/references_test.go index 55a0a58a3a..13dc82eb4e 100644 --- a/private/buf/buflsp/references_test.go +++ b/private/buf/buflsp/references_test.go @@ -15,6 +15,8 @@ package buflsp_test import ( + "context" + "os" "path/filepath" "slices" "testing" @@ -22,6 +24,7 @@ import ( "github.com/bufbuild/buf/private/buf/buflsp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.lsp.dev/jsonrpc2" "go.lsp.dev/protocol" ) @@ -168,3 +171,162 @@ func TestReferences(t *testing.T) { }) } } + +// TestReferencesToDependency verifies that references to a symbol declared in a dependency +// file are found across every indexed file, not only the file open in the editor. +// +// Regression test. References used to be recorded only when the referencing file could +// resolve the declaration, which required that file to own a workspace. Only the file open +// in the editor does, so references from every other file were silently dropped. Re-indexing +// the declaring file then discarded whatever had accumulated, leaving a subset that varied +// with map iteration order. +func TestReferencesToDependency(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + firstProtoPath, err := filepath.Abs("testdata/references_dependency/first.proto") + require.NoError(t, err) + + secondProtoPath, err := filepath.Abs("testdata/references_dependency/second.proto") + require.NoError(t, err) + + sharedProtoPath, err := filepath.Abs("testdata/references_dependency/shared.proto") + require.NoError(t, err) + + // Only first.proto is opened. second.proto and shared.proto are indexed as part of the + // workspace and must still contribute and receive references. + clientJSONConn, firstURI := setupLSPServer(t, firstProtoPath) + secondURI := buflsp.FilePathToURI(secondProtoPath) + sharedURI := buflsp.FilePathToURI(sharedProtoPath) + + type refLocation struct { + uri protocol.URI + line uint32 + } + tests := []struct { + name string + line uint32 + character uint32 + includeDeclaration bool + expectedReferences []refLocation + }{ + { + name: "local_dependency_across_files", + line: 8, // Shared shared = 1; + character: 3, + includeDeclaration: false, + expectedReferences: []refLocation{ + {uri: firstURI, line: 8}, // Shared shared + {uri: firstURI, line: 10}, // repeated Shared others + {uri: secondURI, line: 8}, // Shared shared, in the unopened file + }, + }, + { + name: "local_dependency_with_declaration", + line: 8, + character: 3, + includeDeclaration: true, + expectedReferences: []refLocation{ + {uri: firstURI, line: 8}, + {uri: firstURI, line: 10}, + {uri: secondURI, line: 8}, + {uri: sharedURI, line: 4}, // message Shared + }, + }, + { + // The reported case: a well-known type is a dependency shared by every workspace, + // so it is the symbol most likely to lose references. + name: "wellknown_type_across_files", + line: 9, // google.protobuf.Timestamp created_at = 2; + character: 20, + includeDeclaration: false, + expectedReferences: []refLocation{ + {uri: firstURI, line: 9}, + {uri: secondURI, line: 9}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + locations := requestReferences(ctx, t, clientJSONConn, firstURI, tt.line, tt.character, tt.includeDeclaration) + + require.Len(t, locations, len(tt.expectedReferences)) + for _, expectedRef := range tt.expectedReferences { + idx := slices.IndexFunc(locations, func(loc protocol.Location) bool { + return loc.URI == expectedRef.uri && loc.Range.Start.Line == expectedRef.line + }) + assert.NotEqual(t, -1, idx, "expected reference at %s:%d not found", expectedRef.uri, expectedRef.line) + } + }) + } +} + +// TestReferencesStableAcrossReindex verifies that re-indexing does not drop or duplicate +// references. +// +// This guards the invariant that made the deduplication in server.References unnecessary: +// each file's references are replaced wholesale on re-index, so they can neither accumulate +// nor be dropped. Unlike TestReferencesToDependency, this does not fail against the previous +// implementation, which was stable for a fixture this small. +func TestReferencesStableAcrossReindex(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + firstProtoPath, err := filepath.Abs("testdata/references_dependency/first.proto") + require.NoError(t, err) + + clientJSONConn, firstURI := setupLSPServer(t, firstProtoPath) + + firstProtoContent, err := os.ReadFile(firstProtoPath) + require.NoError(t, err) + + // Position of the google.protobuf.Timestamp reference in first.proto. + const timestampLine, timestampCharacter = 9, 20 + + before := requestReferences(ctx, t, clientJSONConn, firstURI, timestampLine, timestampCharacter, true) + require.NotEmpty(t, before) + + // Re-send the file unchanged. This forces a full re-index without moving any position, so + // the reference set must come back identical. + require.NoError(t, clientJSONConn.Notify(ctx, protocol.MethodTextDocumentDidChange, &protocol.DidChangeTextDocumentParams{ + TextDocument: protocol.VersionedTextDocumentIdentifier{ + TextDocumentIdentifier: protocol.TextDocumentIdentifier{URI: firstURI}, + Version: 2, + }, + ContentChanges: []protocol.TextDocumentContentChangeEvent{{Text: string(firstProtoContent)}}, + })) + + after := requestReferences(ctx, t, clientJSONConn, firstURI, timestampLine, timestampCharacter, true) + + assert.ElementsMatch(t, before, after, "reference set changed after re-indexing") + assert.Len(t, slices.Compact(slices.Clone(after)), len(after), "re-indexing introduced duplicate references") +} + +// requestReferences sends a textDocument/references request and returns the locations. +func requestReferences( + ctx context.Context, + t *testing.T, + clientJSONConn jsonrpc2.Conn, + uri protocol.URI, + line uint32, + character uint32, + includeDeclaration bool, +) []protocol.Location { + t.Helper() + + var locations []protocol.Location + _, err := clientJSONConn.Call(ctx, protocol.MethodTextDocumentReferences, protocol.ReferenceParams{ + TextDocumentPositionParams: protocol.TextDocumentPositionParams{ + TextDocument: protocol.TextDocumentIdentifier{URI: uri}, + Position: protocol.Position{Line: line, Character: character}, + }, + Context: protocol.ReferenceContext{IncludeDeclaration: includeDeclaration}, + }, &locations) + require.NoError(t, err) + return locations +} diff --git a/private/buf/buflsp/server.go b/private/buf/buflsp/server.go index b17599c22f..b686ff5eec 100644 --- a/private/buf/buflsp/server.go +++ b/private/buf/buflsp/server.go @@ -517,14 +517,10 @@ func (s *server) References( if symbol == nil { return nil, nil } - // We deduplicate the references here in the case where a file's symbols have not yet - // been refreshed, but a new file with references to symbols in said file is opened. This - // can cause duplicate references to be appended and not all clients deduplicate the - // returned references. - // - // We also do not want to refresh all symbols in the workspace when a single file is - // interacted with, since that could be detrimental to performance. - return xslices.Deduplicate(symbol.References(params.Context.IncludeDeclaration)), nil + // The reference index stores each file's references separately and replaces them wholesale + // when the file is re-indexed, so references cannot accumulate duplicates and need no + // deduplication here. + return symbol.References(params.Context.IncludeDeclaration), nil } // Completion is the entry point for code completion. diff --git a/private/buf/buflsp/symbol.go b/private/buf/buflsp/symbol.go index c4e02eeb0d..d630c8c32c 100644 --- a/private/buf/buflsp/symbol.go +++ b/private/buf/buflsp/symbol.go @@ -63,8 +63,7 @@ type kind interface { } type referenceable struct { - ast ast.DeclDef - references []*symbol + ast ast.DeclDef } type reference struct { @@ -174,14 +173,9 @@ func (s *symbol) TypeDefinition() protocol.Location { // of the symbol is included as a reference. func (s *symbol) References(includeDeclaration bool) []protocol.Location { var references []protocol.Location - referenceableKind, ok := s.kind.(*referenceable) - if !ok && s.def != nil { - // If the symbol isn't referenceable itself, but has a referenceable definition, use the - // definition for the references. - referenceableKind, ok = s.def.kind.(*referenceable) - } + declaration, referenceSymbols, ok := s.referenceSymbols() if ok { - for _, reference := range referenceableKind.references { + for _, reference := range referenceSymbols { references = append(references, protocol.Location{ URI: reference.file.uri, Range: reference.Range(), @@ -193,19 +187,53 @@ func (s *symbol) References(includeDeclaration bool) []protocol.Location { URI: s.file.uri, Range: s.Range(), }) - } - if includeDeclaration { - // Add the definition of the symbol to the list of references. - if s.def != nil { - references = append(references, protocol.Location{ - URI: s.def.file.uri, - Range: s.def.Range(), - }) + // Declaration symbols are self-referential, e.g. service.def == service, so only take + // the definition when it is a distinct symbol. Otherwise it was just added above. + if s.def != s { + declaration = s.def } } + if includeDeclaration && declaration != nil { + // Add the declaration of the symbol to the list of references. The declaration is the + // symbol itself when it is the referenceable one, so requesting references on a + // declaration includes it. + references = append(references, protocol.Location{ + URI: declaration.file.uri, + Range: declaration.Range(), + }) + } return references } +// referenceSymbols returns the symbol declaring what this symbol names, together with every +// symbol referencing that declaration across all indexed workspaces. +// +// The declaration is this symbol when it is itself referenceable, and its resolved definition +// otherwise. Returns false if neither is referenceable, meaning the symbol cannot be +// referenced. +func (s *symbol) referenceSymbols() (*symbol, []*symbol, bool) { + declaration := s + if _, ok := s.kind.(*referenceable); !ok { + // If the symbol isn't referenceable itself, but has a referenceable definition, use the + // definition for the references. + if s.def == nil { + return nil, nil, false + } + if _, ok := s.def.kind.(*referenceable); !ok { + return nil, nil, false + } + declaration = s.def + } + if declaration.file == nil || declaration.ir.IsZero() { + return declaration, nil, true + } + key := referenceKey{ + path: declaration.file.uri.Filename(), + fullName: declaration.ir.FullName(), + } + return declaration, declaration.file.lsp.referenceIndex.References(key), true +} + // DocumentHighlights returns document highlights for the symbol within the current file. // This includes the definition (if in the same file) and all references in the same file. // All highlights use the [protocol.DocumentHighlightKindText] kind. @@ -215,13 +243,8 @@ func (s *symbol) DocumentHighlights() []protocol.DocumentHighlight { return nil } - // Get the referenceable kind to find all references - referenceableKind, ok := s.kind.(*referenceable) - if !ok && s.def != nil { - // If the symbol isn't referenceable itself, but has a referenceable definition, use the - // definition for the references. - referenceableKind, ok = s.def.kind.(*referenceable) - } + // Get the referenceable declaration to find all references + _, referenceSymbols, ok := s.referenceSymbols() if !ok { return nil } @@ -235,7 +258,7 @@ func (s *symbol) DocumentHighlights() []protocol.DocumentHighlight { var highlights []protocol.DocumentHighlight // Add all references in the same file - for _, reference := range referenceableKind.references { + for _, reference := range referenceSymbols { if reference.file.uri == s.file.uri { highlights = append(highlights, protocol.DocumentHighlight{ Range: reference.Range(), @@ -458,15 +481,10 @@ func renameChangesForReferenceableSymbol(s *symbol, newName string) (map[protoco NewText: newName, }}, } - // Get the referenceable kind to find all references - referenceableKind, ok := s.kind.(*referenceable) - if !ok && s.def != nil { - // If the symbol isn't referenceable itself, but has a referenceable definition, use the - // definition for the references. - referenceableKind, ok = s.def.kind.(*referenceable) - } + // Get the referenceable declaration to find all references + _, referenceSymbols, ok := s.referenceSymbols() if ok { - for _, reference := range referenceableKind.references { + for _, reference := range referenceSymbols { newText := newName // For option references (extension usages), preserve package qualification and parentheses. // e.g., if renaming "(subpkg.testing)" to "validated", result should be "(subpkg.validated)" diff --git a/private/buf/buflsp/testdata/references_dependency/buf.yaml b/private/buf/buflsp/testdata/references_dependency/buf.yaml new file mode 100644 index 0000000000..f74da98a3c --- /dev/null +++ b/private/buf/buflsp/testdata/references_dependency/buf.yaml @@ -0,0 +1,9 @@ +version: v2 +modules: + - path: . +lint: + use: + - STANDARD +breaking: + use: + - FILE diff --git a/private/buf/buflsp/testdata/references_dependency/first.proto b/private/buf/buflsp/testdata/references_dependency/first.proto new file mode 100644 index 0000000000..cb18a1f6fc --- /dev/null +++ b/private/buf/buflsp/testdata/references_dependency/first.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package referencesdep.v1; + +import "google/protobuf/timestamp.proto"; +import "shared.proto"; + +message First { + Shared shared = 1; + google.protobuf.Timestamp created_at = 2; + repeated Shared others = 3; +} diff --git a/private/buf/buflsp/testdata/references_dependency/second.proto b/private/buf/buflsp/testdata/references_dependency/second.proto new file mode 100644 index 0000000000..a3ff99d86c --- /dev/null +++ b/private/buf/buflsp/testdata/references_dependency/second.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package referencesdep.v1; + +import "google/protobuf/timestamp.proto"; +import "shared.proto"; + +message Second { + Shared shared = 1; + google.protobuf.Timestamp updated_at = 2; +} diff --git a/private/buf/buflsp/testdata/references_dependency/shared.proto b/private/buf/buflsp/testdata/references_dependency/shared.proto new file mode 100644 index 0000000000..592b8b9365 --- /dev/null +++ b/private/buf/buflsp/testdata/references_dependency/shared.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +package referencesdep.v1; + +message Shared { + string id = 1; +} From fbdfd93a9111cc531879377bb755ff85c0f2932c Mon Sep 17 00:00:00 2001 From: Edward McFarlane Date: Wed, 19 Aug 2026 21:33:31 +0100 Subject: [PATCH 2/4] Simplify --- private/buf/buflsp/file.go | 16 ++-- private/buf/buflsp/reference_index.go | 64 ++++++++-------- private/buf/buflsp/references_test.go | 31 +------- private/buf/buflsp/symbol.go | 102 +++++++++++--------------- 4 files changed, 85 insertions(+), 128 deletions(-) diff --git a/private/buf/buflsp/file.go b/private/buf/buflsp/file.go index 0fb2e18d19..84ac30e474 100644 --- a/private/buf/buflsp/file.go +++ b/private/buf/buflsp/file.go @@ -97,8 +97,7 @@ func (f *file) Reset(ctx context.Context) { f.workspace.Release() f.workspace = nil } - // Drop this file's references. The index holds symbols pointing back at this file, which - // is zeroed below, so entries left behind would resolve to an empty URI. + // Drop this file's references from the index before the file is zeroed. f.lsp.referenceIndex.RemoveFile(f.uri) // Evict the query key if there is a query cached on the file. We cache the [queries.File] // query since this allows the executor to evict all dependent queries, e.g. AST and IR. @@ -377,18 +376,17 @@ func (f *file) IndexSymbols(ctx context.Context) { } // Resolve all unresolved symbols from this file, and record the references they make in - // the reference index. - // - // A reference is recorded by definition site rather than by resolved symbol, so it is - // indexed whether or not the declaring file has been indexed yet. Resolution below only - // populates def and typeDef, which go-to-definition and hover need; find-references does - // not depend on it, and so no longer depends on the order files happen to be indexed in. - references := make(map[referenceKey][]*symbol) + // the reference index. References are keyed by definition site rather than by resolved + // symbol, so recording does not depend on the order files are indexed in. + var references map[referenceKey][]*symbol addReference := func(def ast.DeclDef, fullName ir.FullName, sym *symbol) { key, ok := newReferenceKey(def, fullName) if !ok { return } + if references == nil { + references = make(map[referenceKey][]*symbol) + } references[key] = append(references[key], sym) } for _, sym := range unresolved { diff --git a/private/buf/buflsp/reference_index.go b/private/buf/buflsp/reference_index.go index 7b7213dfbb..6ae036e1db 100644 --- a/private/buf/buflsp/reference_index.go +++ b/private/buf/buflsp/reference_index.go @@ -25,22 +25,15 @@ import ( "go.lsp.dev/protocol" ) -// referenceKey identifies a symbol definition site. -// -// The key is the absolute local path of the file declaring the symbol plus the symbol's -// fully-qualified name. Keying on the definition's path, rather than on a resolved -// [symbol] pointer, means a reference can be recorded before the declaring file has been -// indexed. Including the path, rather than keying on the full name alone, keeps two -// workspaces that legitimately declare the same fully-qualified name from cross-linking. +// referenceKey identifies a symbol definition site: the path of the declaring file as it +// appears in source spans, plus the symbol's fully-qualified name. type referenceKey struct { path string fullName ir.FullName } -// newReferenceKey builds the key for the definition an unresolved symbol names. -// -// Returns false if the definition has no span to take a path from, which happens for a -// zero [ast.DeclDef]. +// newReferenceKey builds the key for the definition an unresolved symbol names. Returns +// false if the definition has no span to take a path from, such as a zero [ast.DeclDef]. func newReferenceKey(def ast.DeclDef, fullName ir.FullName) (referenceKey, bool) { path := def.Span().Path() if path == "" || fullName == "" { @@ -49,21 +42,25 @@ func newReferenceKey(def ast.DeclDef, fullName ir.FullName) (referenceKey, bool) return referenceKey{path: path, fullName: fullName}, true } -// referenceIndex is a reverse index from a symbol definition to the symbols referencing it. -// -// The index is owned by the [lsp] rather than by a file or a workspace, so references -// resolve across every workspace the server has indexed. This matters for shared -// dependencies: a well-known type is reachable from many workspaces, but a file belongs to -// at most one, so a per-workspace index only ever sees a fraction of its references. -// -// References are grouped by the file containing them so that re-indexing a file replaces -// only that file's contribution, leaving every other file's references untouched. Indexing -// a file therefore costs time proportional to the references in that file alone. +// newReferenceKeyForDeclaration builds the key for a declaration symbol. This is the only +// other way to construct a key, and derives the path from the same source as spans do, so +// lookups cannot diverge from what [newReferenceKey] indexed. +func newReferenceKeyForDeclaration(declaration *symbol) (referenceKey, bool) { + if declaration.file == nil || declaration.file.file == nil || declaration.ir.IsZero() { + return referenceKey{}, false + } + return referenceKey{ + path: declaration.file.file.Path(), + fullName: declaration.ir.FullName(), + }, true +} + +// referenceIndex is a reverse index from a symbol definition to the symbols referencing it, +// across all workspaces. References are grouped by the file containing them, so re-indexing +// a file replaces only that file's contribution. // -// The index is not safe for concurrent use. It is protected by the [lsp] lock, which -// already serializes all request handling. +// The index is not safe for concurrent use; it is protected by the [lsp] lock. type referenceIndex struct { - // keyToFileRefs maps a definition to the referencing symbols, grouped by containing file. keyToFileRefs map[referenceKey]map[protocol.URI][]*symbol // uriToKeys records which definitions each file contributes references to, so that a // file's contribution can be removed without scanning the whole index. @@ -99,9 +96,9 @@ func (i *referenceIndex) SetFile(uri protocol.URI, references map[referenceKey][ // RemoveFile drops all references contributed by the file at the given URI. // -// This must be called when a file is evicted. The index holds [symbol] values that point -// back at their file, and an evicted file is zeroed, so entries left behind would resolve -// to locations with an empty URI. +// This must be called when a file is evicted. The index holds symbols pointing back at +// their file, and an evicted file is zeroed, so entries left behind would resolve to +// locations with an empty URI. func (i *referenceIndex) RemoveFile(uri protocol.URI) { for _, key := range i.uriToKeys[uri] { fileRefs, ok := i.keyToFileRefs[key] @@ -116,10 +113,8 @@ func (i *referenceIndex) RemoveFile(uri protocol.URI) { delete(i.uriToKeys, uri) } -// References returns all symbols referencing the given definition. -// -// The result is ordered by file URI and then by position, so that results are stable -// across calls. The index is built from maps, whose iteration order is not. +// References returns all symbols referencing the given definition, ordered by file URI and +// then by position so that results are stable across calls. func (i *referenceIndex) References(key referenceKey) []*symbol { fileRefs := i.keyToFileRefs[key] if len(fileRefs) == 0 { @@ -138,3 +133,10 @@ func (i *referenceIndex) References(key referenceKey) []*symbol { }) return symbols } + +// FileReferences returns the symbols in the file at the given URI that reference the given +// definition. Unlike [referenceIndex.References], this needs no cross-file merge or sort, +// so it is cheap enough for per-cursor-move requests. +func (i *referenceIndex) FileReferences(key referenceKey, uri protocol.URI) []*symbol { + return i.keyToFileRefs[key][uri] +} diff --git a/private/buf/buflsp/references_test.go b/private/buf/buflsp/references_test.go index 13dc82eb4e..eb96e2f042 100644 --- a/private/buf/buflsp/references_test.go +++ b/private/buf/buflsp/references_test.go @@ -143,22 +143,7 @@ func TestReferences(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - var locations []protocol.Location - _, refErr := clientJSONConn.Call(ctx, protocol.MethodTextDocumentReferences, protocol.ReferenceParams{ - TextDocumentPositionParams: protocol.TextDocumentPositionParams{ - TextDocument: protocol.TextDocumentIdentifier{ - URI: tt.targetURI, - }, - Position: protocol.Position{ - Line: tt.line, - Character: tt.character, - }, - }, - Context: protocol.ReferenceContext{ - IncludeDeclaration: tt.includeDeclaration, - }, - }, &locations) - require.NoError(t, refErr) + locations := requestReferences(ctx, t, clientJSONConn, tt.targetURI, tt.line, tt.character, tt.includeDeclaration) require.Len(t, locations, len(tt.expectedReferences)) @@ -174,12 +159,6 @@ func TestReferences(t *testing.T) { // TestReferencesToDependency verifies that references to a symbol declared in a dependency // file are found across every indexed file, not only the file open in the editor. -// -// Regression test. References used to be recorded only when the referencing file could -// resolve the declaration, which required that file to own a workspace. Only the file open -// in the editor does, so references from every other file were silently dropped. Re-indexing -// the declaring file then discarded whatever had accumulated, leaving a subset that varied -// with map iteration order. func TestReferencesToDependency(t *testing.T) { t.Parallel() @@ -266,12 +245,8 @@ func TestReferencesToDependency(t *testing.T) { } // TestReferencesStableAcrossReindex verifies that re-indexing does not drop or duplicate -// references. -// -// This guards the invariant that made the deduplication in server.References unnecessary: -// each file's references are replaced wholesale on re-index, so they can neither accumulate -// nor be dropped. Unlike TestReferencesToDependency, this does not fail against the previous -// implementation, which was stable for a fixture this small. +// references. This guards the invariant that makes deduplication in server.References +// unnecessary. func TestReferencesStableAcrossReindex(t *testing.T) { t.Parallel() diff --git a/private/buf/buflsp/symbol.go b/private/buf/buflsp/symbol.go index d630c8c32c..c1a151f133 100644 --- a/private/buf/buflsp/symbol.go +++ b/private/buf/buflsp/symbol.go @@ -172,31 +172,22 @@ func (s *symbol) TypeDefinition() protocol.Location { // It also accepts the includeDeclaration param from the client - if true, the declaration // of the symbol is included as a reference. func (s *symbol) References(includeDeclaration bool) []protocol.Location { - var references []protocol.Location - declaration, referenceSymbols, ok := s.referenceSymbols() - if ok { - for _, reference := range referenceSymbols { - references = append(references, protocol.Location{ - URI: reference.file.uri, - Range: reference.Range(), - }) - } - } else { - // No referenceable kind; add the location of the symbol itself. - references = append(references, protocol.Location{ + declaration, ok := s.referenceDeclaration() + if !ok { + // No referenceable declaration; the symbol's own location is the only result. + return []protocol.Location{{ URI: s.file.uri, Range: s.Range(), + }} + } + var references []protocol.Location + for _, reference := range declaration.referenceSymbols() { + references = append(references, protocol.Location{ + URI: reference.file.uri, + Range: reference.Range(), }) - // Declaration symbols are self-referential, e.g. service.def == service, so only take - // the definition when it is a distinct symbol. Otherwise it was just added above. - if s.def != s { - declaration = s.def - } } - if includeDeclaration && declaration != nil { - // Add the declaration of the symbol to the list of references. The declaration is the - // symbol itself when it is the referenceable one, so requesting references on a - // declaration includes it. + if includeDeclaration { references = append(references, protocol.Location{ URI: declaration.file.uri, Range: declaration.Range(), @@ -205,33 +196,29 @@ func (s *symbol) References(includeDeclaration bool) []protocol.Location { return references } -// referenceSymbols returns the symbol declaring what this symbol names, together with every -// symbol referencing that declaration across all indexed workspaces. -// -// The declaration is this symbol when it is itself referenceable, and its resolved definition -// otherwise. Returns false if neither is referenceable, meaning the symbol cannot be -// referenced. -func (s *symbol) referenceSymbols() (*symbol, []*symbol, bool) { - declaration := s - if _, ok := s.kind.(*referenceable); !ok { - // If the symbol isn't referenceable itself, but has a referenceable definition, use the - // definition for the references. - if s.def == nil { - return nil, nil, false - } - if _, ok := s.def.kind.(*referenceable); !ok { - return nil, nil, false - } - declaration = s.def +// referenceDeclaration returns the referenceable symbol declaring what this symbol names: +// the symbol itself if it is referenceable, otherwise its resolved definition. Returns +// false if neither is, meaning the symbol cannot be referenced. +func (s *symbol) referenceDeclaration() (*symbol, bool) { + if _, ok := s.kind.(*referenceable); ok { + return s, true } - if declaration.file == nil || declaration.ir.IsZero() { - return declaration, nil, true + if s.def != nil { + if _, ok := s.def.kind.(*referenceable); ok { + return s.def, true + } } - key := referenceKey{ - path: declaration.file.uri.Filename(), - fullName: declaration.ir.FullName(), + return nil, false +} + +// referenceSymbols returns every symbol referencing this declaration symbol, across all +// indexed workspaces. +func (s *symbol) referenceSymbols() []*symbol { + key, ok := newReferenceKeyForDeclaration(s) + if !ok { + return nil } - return declaration, declaration.file.lsp.referenceIndex.References(key), true + return s.file.lsp.referenceIndex.References(key) } // DocumentHighlights returns document highlights for the symbol within the current file. @@ -244,7 +231,7 @@ func (s *symbol) DocumentHighlights() []protocol.DocumentHighlight { } // Get the referenceable declaration to find all references - _, referenceSymbols, ok := s.referenceSymbols() + declaration, ok := s.referenceDeclaration() if !ok { return nil } @@ -257,9 +244,10 @@ func (s *symbol) DocumentHighlights() []protocol.DocumentHighlight { } var highlights []protocol.DocumentHighlight - // Add all references in the same file - for _, reference := range referenceSymbols { - if reference.file.uri == s.file.uri { + // Add all references in the same file. Only this file's entries are needed, so avoid the + // cross-file merge and sort of References. + if key, ok := newReferenceKeyForDeclaration(declaration); ok { + for _, reference := range s.file.lsp.referenceIndex.FileReferences(key, s.file.uri) { highlights = append(highlights, protocol.DocumentHighlight{ Range: reference.Range(), Kind: protocol.DocumentHighlightKindText, @@ -267,16 +255,10 @@ func (s *symbol) DocumentHighlights() []protocol.DocumentHighlight { } } - // Add the definition if it's in the same file - if s.def != nil && s.def.file.uri == s.file.uri { - highlights = append(highlights, protocol.DocumentHighlight{ - Range: s.def.Range(), - Kind: protocol.DocumentHighlightKindText, - }) - } else if s.def == nil { - // If there's no separate definition, the symbol itself is the definition + // Add the declaration if it's in the same file + if declaration.file.uri == s.file.uri { highlights = append(highlights, protocol.DocumentHighlight{ - Range: s.Range(), + Range: declaration.Range(), Kind: protocol.DocumentHighlightKindText, }) } @@ -482,9 +464,9 @@ func renameChangesForReferenceableSymbol(s *symbol, newName string) (map[protoco }}, } // Get the referenceable declaration to find all references - _, referenceSymbols, ok := s.referenceSymbols() + declaration, ok := s.referenceDeclaration() if ok { - for _, reference := range referenceSymbols { + for _, reference := range declaration.referenceSymbols() { newText := newName // For option references (extension usages), preserve package qualification and parentheses. // e.g., if renaming "(subpkg.testing)" to "validated", result should be "(subpkg.validated)" From 9d44107aa7ccb344140d8b8eed7602eb6cd083a0 Mon Sep 17 00:00:00 2001 From: Edward McFarlane Date: Wed, 19 Aug 2026 21:33:38 +0100 Subject: [PATCH 3/4] Add CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d12f5bb901..97a3440d49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - Fix `buf format` dropping comments next to commas or semicolons in message literals. - Fix compilation failing to resolve symbols re-exported through `import public` when the re-exporting file also reaches those symbols through a non-public import. +- Fix LSP finding only a subset of references to symbols declared in + dependencies, including well-known types. ## [v1.72.0] - 2026-07-17 From 977b2675bd21694d405c5b7b00d6dc6c3b95a7cd Mon Sep 17 00:00:00 2001 From: Edward McFarlane Date: Wed, 19 Aug 2026 21:40:17 +0100 Subject: [PATCH 4/4] Cleanup --- private/buf/buflsp/file.go | 4 +- private/buf/buflsp/reference_index.go | 3 +- private/buf/buflsp/references_test.go | 19 +++------- private/buf/buflsp/server.go | 3 -- private/buf/buflsp/symbol.go | 53 +++++++++++++-------------- 5 files changed, 35 insertions(+), 47 deletions(-) diff --git a/private/buf/buflsp/file.go b/private/buf/buflsp/file.go index 84ac30e474..77a626e11e 100644 --- a/private/buf/buflsp/file.go +++ b/private/buf/buflsp/file.go @@ -376,8 +376,8 @@ func (f *file) IndexSymbols(ctx context.Context) { } // Resolve all unresolved symbols from this file, and record the references they make in - // the reference index. References are keyed by definition site rather than by resolved - // symbol, so recording does not depend on the order files are indexed in. + // the reference index. References are keyed by definition site, so recording does not + // depend on the order files are indexed in. var references map[referenceKey][]*symbol addReference := func(def ast.DeclDef, fullName ir.FullName, sym *symbol) { key, ok := newReferenceKey(def, fullName) diff --git a/private/buf/buflsp/reference_index.go b/private/buf/buflsp/reference_index.go index 6ae036e1db..1037f51b0f 100644 --- a/private/buf/buflsp/reference_index.go +++ b/private/buf/buflsp/reference_index.go @@ -135,8 +135,7 @@ func (i *referenceIndex) References(key referenceKey) []*symbol { } // FileReferences returns the symbols in the file at the given URI that reference the given -// definition. Unlike [referenceIndex.References], this needs no cross-file merge or sort, -// so it is cheap enough for per-cursor-move requests. +// definition. This is a direct lookup, cheap enough for per-cursor-move requests. func (i *referenceIndex) FileReferences(key referenceKey, uri protocol.URI) []*symbol { return i.keyToFileRefs[key][uri] } diff --git a/private/buf/buflsp/references_test.go b/private/buf/buflsp/references_test.go index eb96e2f042..b3bc65aaed 100644 --- a/private/buf/buflsp/references_test.go +++ b/private/buf/buflsp/references_test.go @@ -143,7 +143,7 @@ func TestReferences(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - locations := requestReferences(ctx, t, clientJSONConn, tt.targetURI, tt.line, tt.character, tt.includeDeclaration) + locations := testRequestReferences(ctx, t, clientJSONConn, tt.targetURI, tt.line, tt.character, tt.includeDeclaration) require.Len(t, locations, len(tt.expectedReferences)) @@ -157,8 +157,6 @@ func TestReferences(t *testing.T) { } } -// TestReferencesToDependency verifies that references to a symbol declared in a dependency -// file are found across every indexed file, not only the file open in the editor. func TestReferencesToDependency(t *testing.T) { t.Parallel() @@ -214,8 +212,6 @@ func TestReferencesToDependency(t *testing.T) { }, }, { - // The reported case: a well-known type is a dependency shared by every workspace, - // so it is the symbol most likely to lose references. name: "wellknown_type_across_files", line: 9, // google.protobuf.Timestamp created_at = 2; character: 20, @@ -231,7 +227,7 @@ func TestReferencesToDependency(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - locations := requestReferences(ctx, t, clientJSONConn, firstURI, tt.line, tt.character, tt.includeDeclaration) + locations := testRequestReferences(ctx, t, clientJSONConn, firstURI, tt.line, tt.character, tt.includeDeclaration) require.Len(t, locations, len(tt.expectedReferences)) for _, expectedRef := range tt.expectedReferences { @@ -244,9 +240,6 @@ func TestReferencesToDependency(t *testing.T) { } } -// TestReferencesStableAcrossReindex verifies that re-indexing does not drop or duplicate -// references. This guards the invariant that makes deduplication in server.References -// unnecessary. func TestReferencesStableAcrossReindex(t *testing.T) { t.Parallel() @@ -263,7 +256,7 @@ func TestReferencesStableAcrossReindex(t *testing.T) { // Position of the google.protobuf.Timestamp reference in first.proto. const timestampLine, timestampCharacter = 9, 20 - before := requestReferences(ctx, t, clientJSONConn, firstURI, timestampLine, timestampCharacter, true) + before := testRequestReferences(ctx, t, clientJSONConn, firstURI, timestampLine, timestampCharacter, true) require.NotEmpty(t, before) // Re-send the file unchanged. This forces a full re-index without moving any position, so @@ -276,14 +269,14 @@ func TestReferencesStableAcrossReindex(t *testing.T) { ContentChanges: []protocol.TextDocumentContentChangeEvent{{Text: string(firstProtoContent)}}, })) - after := requestReferences(ctx, t, clientJSONConn, firstURI, timestampLine, timestampCharacter, true) + after := testRequestReferences(ctx, t, clientJSONConn, firstURI, timestampLine, timestampCharacter, true) assert.ElementsMatch(t, before, after, "reference set changed after re-indexing") assert.Len(t, slices.Compact(slices.Clone(after)), len(after), "re-indexing introduced duplicate references") } -// requestReferences sends a textDocument/references request and returns the locations. -func requestReferences( +// testRequestReferences sends a textDocument/references request and returns the locations. +func testRequestReferences( ctx context.Context, t *testing.T, clientJSONConn jsonrpc2.Conn, diff --git a/private/buf/buflsp/server.go b/private/buf/buflsp/server.go index b686ff5eec..17a744fce4 100644 --- a/private/buf/buflsp/server.go +++ b/private/buf/buflsp/server.go @@ -517,9 +517,6 @@ func (s *server) References( if symbol == nil { return nil, nil } - // The reference index stores each file's references separately and replaces them wholesale - // when the file is re-indexed, so references cannot accumulate duplicates and need no - // deduplication here. return symbol.References(params.Context.IncludeDeclaration), nil } diff --git a/private/buf/buflsp/symbol.go b/private/buf/buflsp/symbol.go index c1a151f133..ed7aac2fdc 100644 --- a/private/buf/buflsp/symbol.go +++ b/private/buf/buflsp/symbol.go @@ -196,31 +196,6 @@ func (s *symbol) References(includeDeclaration bool) []protocol.Location { return references } -// referenceDeclaration returns the referenceable symbol declaring what this symbol names: -// the symbol itself if it is referenceable, otherwise its resolved definition. Returns -// false if neither is, meaning the symbol cannot be referenced. -func (s *symbol) referenceDeclaration() (*symbol, bool) { - if _, ok := s.kind.(*referenceable); ok { - return s, true - } - if s.def != nil { - if _, ok := s.def.kind.(*referenceable); ok { - return s.def, true - } - } - return nil, false -} - -// referenceSymbols returns every symbol referencing this declaration symbol, across all -// indexed workspaces. -func (s *symbol) referenceSymbols() []*symbol { - key, ok := newReferenceKeyForDeclaration(s) - if !ok { - return nil - } - return s.file.lsp.referenceIndex.References(key) -} - // DocumentHighlights returns document highlights for the symbol within the current file. // This includes the definition (if in the same file) and all references in the same file. // All highlights use the [protocol.DocumentHighlightKindText] kind. @@ -244,8 +219,7 @@ func (s *symbol) DocumentHighlights() []protocol.DocumentHighlight { } var highlights []protocol.DocumentHighlight - // Add all references in the same file. Only this file's entries are needed, so avoid the - // cross-file merge and sort of References. + // Add all references in the same file. if key, ok := newReferenceKeyForDeclaration(declaration); ok { for _, reference := range s.file.lsp.referenceIndex.FileReferences(key, s.file.uri) { highlights = append(highlights, protocol.DocumentHighlight{ @@ -453,6 +427,31 @@ func (s *symbol) Rename(newName string) (*protocol.WorkspaceEdit, error) { return &edits, nil } +// referenceDeclaration returns the referenceable symbol declaring what this symbol names: +// the symbol itself if it is referenceable, otherwise its resolved definition. Returns +// false if neither is, meaning the symbol cannot be referenced. +func (s *symbol) referenceDeclaration() (*symbol, bool) { + if _, ok := s.kind.(*referenceable); ok { + return s, true + } + if s.def != nil { + if _, ok := s.def.kind.(*referenceable); ok { + return s.def, true + } + } + return nil, false +} + +// referenceSymbols returns every symbol referencing this declaration symbol, across all +// indexed workspaces. +func (s *symbol) referenceSymbols() []*symbol { + key, ok := newReferenceKeyForDeclaration(s) + if !ok { + return nil + } + return s.file.lsp.referenceIndex.References(key) +} + // renameChangesForReferenceableSymbol is a helper for getting all rename changes for the // given referenceable symbol. func renameChangesForReferenceableSymbol(s *symbol, newName string) (map[protocol.DocumentURI][]protocol.TextEdit, error) {