Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## [Unreleased]

- No changes yet.
- Add `textDocument/rename` and `textDocument/prepareRename` support for `buf lsp serve`.

## [v1.61.0] - 2025-11-25

Expand Down
8 changes: 6 additions & 2 deletions private/buf/buflsp/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -588,10 +588,12 @@ func (f *file) irToSymbols(irSymbol ir.Symbol) ([]*symbol, []*symbol) {
resolved = append(resolved, method)

input, _ := irSymbol.AsMethod().Input()
// Method input must be a single message type.
inputAST := irSymbol.AsMethod().AST().AsMethod().Signature.Inputs().At(0)
inputSym := &symbol{
ir: irSymbol,
file: f,
span: irSymbol.AsMethod().AST().AsMethod().Signature.Inputs().Span(),
span: inputAST.RemovePrefixes().Span(), // We always strip prefixes in case of streaming.
kind: &reference{
def: input.AST(), // Only messages can be method inputs and outputs
fullName: input.FullName(),
Expand All @@ -600,10 +602,12 @@ func (f *file) irToSymbols(irSymbol ir.Symbol) ([]*symbol, []*symbol) {
unresolved = append(unresolved, inputSym)

output, _ := irSymbol.AsMethod().Output()
// Method output must be a single message type.
outputAST := irSymbol.AsMethod().AST().AsMethod().Signature.Outputs().At(0)
outputSym := &symbol{
ir: irSymbol,
file: f,
span: irSymbol.AsMethod().AST().AsMethod().Signature.Outputs().Span(),
span: outputAST.RemovePrefixes().Span(), // We always strip prefixes in case of streaming.
kind: &reference{
def: output.AST(), // Only messages can be method inputs and outputs
fullName: output.FullName(),
Expand Down
35 changes: 35 additions & 0 deletions private/buf/buflsp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ func (s *server) Initialize(
DocumentFormattingProvider: true,
HoverProvider: true,
ReferencesProvider: &protocol.ReferenceOptions{},
RenameProvider: &protocol.RenameOptions{
PrepareProvider: true,
},
SemanticTokensProvider: &SemanticTokensOptions{
Legend: SemanticTokensLegend{
TokenTypes: semanticTypeLegend,
Expand Down Expand Up @@ -559,6 +562,38 @@ func (s *server) DocumentSymbol(ctx context.Context, params *protocol.DocumentSy
return anyResults, nil
}

// PrepareRename is the entry point for checking workspace wide renaming of a symbol.
//
// If a symbol can be renamed, PrepareRename will return the range for the rename. Returning
// an empty range indicates that the requested position cannot be renamed and the client
// will handle providing feedback to the user.
//
// Supported symbol types for renaming are [referenceable], [static], and [reference].
func (s *server) PrepareRename(ctx context.Context, params *protocol.PrepareRenameParams) (*protocol.Range, error) {
symbol := s.getSymbol(ctx, params.TextDocument.URI, params.Position)
if symbol == nil {
return nil, nil
}
switch symbol.kind.(type) {
case *referenceable, *static, *reference:
rnge := reportSpanToProtocolRange(symbol.span)
return &rnge, nil
}
return nil, nil
}

// Rename is the entry point for workspace wide renaming of a symbol.
func (s *server) Rename(
ctx context.Context,
params *protocol.RenameParams,
) (*protocol.WorkspaceEdit, error) {
symbol := s.getSymbol(ctx, params.TextDocument.URI, params.Position)
if symbol == nil {
return nil, nil
}
return symbol.Rename(params.NewName)
}

// getSymbol is a helper function that gets the *[symbol] for the given [protocol.URI] and
// [protocol.Position].
func (s *server) getSymbol(
Expand Down
55 changes: 55 additions & 0 deletions private/buf/buflsp/symbol.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,61 @@ func (s *symbol) GetSymbolInformation() protocol.SymbolInformation {
}
}

// Rename returns the [protocol.WorkspaceEdit] for renaming the symbol.
func (s *symbol) Rename(newName string) (*protocol.WorkspaceEdit, error) {
var edits protocol.WorkspaceEdit
switch s.kind.(type) {
case *referenceable:
changes, err := renameChangesForReferenceableSymbol(s, newName)
if err != nil {
return nil, err
}
edits.Changes = changes
case *static:
edits.Changes = map[protocol.DocumentURI][]protocol.TextEdit{
s.file.uri: {{
Range: reportSpanToProtocolRange(s.span),
NewText: newName,
}},
}
case *reference:
// For references, we attempt to rename the definition symbol, if resolved. This would
// include this reference symbol.
if s.def != nil {
changes, err := renameChangesForReferenceableSymbol(s.def, newName)
if err != nil {
return nil, err
}
edits.Changes = changes
}
}
// All other symbol types (options, imports, built-ins, and tags) cannot be renamed.
return &edits, nil
}

// 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) {
// At minimum, we would rename the symbol itself.
changes := map[protocol.DocumentURI][]protocol.TextEdit{
s.file.uri: {{
Range: reportSpanToProtocolRange(s.span),
NewText: newName,
}},
}
if def, ok := s.def.kind.(*referenceable); ok {
for _, reference := range def.references {
changes[reference.file.uri] = append(changes[reference.file.uri], protocol.TextEdit{
Range: reportSpanToProtocolRange(reference.span),
NewText: newName,
})
}
} else {
return nil, fmt.Errorf("attempting to rename a non-referenceble symbol as a referenceable symbol: %v", s)
}
return changes, nil
}

func protowireTypeForPredeclared(name predeclared.Name) protowire.Type {
switch name {
case predeclared.Bool, predeclared.Int32, predeclared.Int64, predeclared.UInt32,
Expand Down