Skip to content
Open
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,9 @@ Hover over any symbol or imports to get detailed documentation and comments asso

### Rename Symbols

Rename symbols like messages or enums, and Propagate the changes throughout the codebase. Currently, field renaming within symbols is not supported.
Rename symbols like messages, enums, services and RPC methods, and propagate the changes throughout the codebase. Rename also works when invoked on a type reference (e.g. the request or response type of an `rpc`) — the LSP pivots to the declaration and applies the rename from there. Field names, oneof names, and enum values can also be renamed at their declaration site (single-site rename, since they aren't referenced as types from other `.proto` files).

When an `rpc` follows the `rpc <Name>(<Name>Request) returns (<Name>Response)` convention from the [Google API design guide](https://google.aip.dev/) (AIPs 131–136), renaming any one of the three triggers a chained rename of the other two — but only when (a) the matching message name follows the convention exactly, (b) the request/response is used by exactly one rpc in the workspace, and (c) the user's new name preserves the convention. If any check fails, only the symbol the user invoked rename on is renamed.

### Find References

Expand Down
72 changes: 47 additions & 25 deletions src/lsp.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::ops::ControlFlow;
use std::{collections::HashMap, fs::read_to_string, path::PathBuf};
use std::{fs::read_to_string, path::PathBuf};
use tracing::{error, info, warn};

use async_lsp::lsp_types::{
Expand All @@ -20,6 +20,7 @@ use async_lsp::{Error, LanguageClient, ResponseError};
use futures::future::BoxFuture;
use serde_json::Value;

use crate::context::jumpable::Jumpable;
use crate::formatter::ProtoFormatter;
use crate::server::ProtoLanguageServer;
use crate::{docs, log};
Expand Down Expand Up @@ -244,7 +245,6 @@ impl ProtoLanguageServer {
) -> BoxFuture<'static, Result<Option<WorkspaceEdit>, ResponseError>> {
let uri = params.text_document_position.text_document.uri;
let pos = params.text_document_position.position;

let new_name = params.new_name;

let Some(tree) = self.state.get_tree(&uri) else {
Expand All @@ -253,38 +253,60 @@ impl ProtoLanguageServer {
};

let content = self.state.get_content(&uri);

let current_package = tree.get_package_name(content.as_bytes()).unwrap_or(".");
let ipath = self.configs.get_include_paths(&uri).unwrap_or_default();

let Some((edit, otext, ntext)) = tree.rename_tree(&pos, &new_name, content.as_bytes())
else {
error!(uri=%uri, "failed to rename in a tree");
return Box::pin(async move { Ok(None) });
// If the cursor is on a type reference (inside a message_or_enum_type
// node), pivot to the declaration and rename from there. The workspace
// pass then handles all references — including the one the user is
// standing on.
let (decl_uri, decl_pos) = match tree.rename_pivot_identifier(&pos, content.as_bytes()) {
Some(decl_path) => {
let locations =
self.state
.definition(&ipath, current_package, Jumpable::Identifier(decl_path));
let Some(decl) = locations.into_iter().next() else {
error!(uri=%uri, "failed to resolve declaration for reference-site rename");
return Box::pin(async move { Ok(None) });
};
(decl.uri, decl.range.start)
}
None => (uri.clone(), pos),
};

let Some(workspace) = self.configs.get_workspace_for_uri(&uri) else {
error!(uri=%uri, "failed to get workspace");
let Some(workspace) = self.configs.get_workspace_for_uri(&decl_uri) else {
error!(uri=%decl_uri, "failed to get workspace");
return Box::pin(async move { Ok(None) });
};
let Ok(workspace_path) = workspace.to_file_path() else {
error!(uri=%workspace, "workspace url is not a file path");
return Box::pin(async move { Ok(None) });
};

let work_done_token = params.work_done_progress_params.work_done_token;
let progress_sender = work_done_token.map(|token| self.with_report_progress(token));
let progress_sender = params
.work_done_progress_params
.work_done_token
.map(|token| self.with_report_progress(token));

let mut h = HashMap::new();
h.extend(self.state.rename_fields(
current_package,
&otext,
&ntext,
workspace.to_file_path().unwrap(),
progress_sender,
));

h.entry(tree.uri).or_insert(edit.clone()).extend(edit);
let ops = self
.state
.compute_rename_ops(&decl_uri, decl_pos, &new_name, &ipath);
let Some(all_edits) = self
.state
.apply_rename_ops(&ops, workspace_path, progress_sender)
else {
error!(uri=%decl_uri, "failed to apply primary rename");
return Box::pin(async move { Ok(None) });
};

let response = Some(WorkspaceEdit {
changes: Some(h),
..Default::default()
});
let response = if all_edits.is_empty() {
None
} else {
Some(WorkspaceEdit {
changes: Some(all_edits),
..Default::default()
})
};

Box::pin(async move { Ok(response) })
}
Expand Down
25 changes: 25 additions & 0 deletions src/nodekind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,35 @@ impl NodeKind {
n.kind() == Self::FieldName.as_str()
}

pub fn is_rpc_name(n: &Node) -> bool {
n.kind() == Self::RpcName.as_str()
}

pub fn is_userdefined(n: &Node) -> bool {
n.kind() == Self::EnumName.as_str() || n.kind() == Self::MessageName.as_str()
}

pub fn is_renameable(n: &Node) -> bool {
Self::is_userdefined(n)
|| n.kind() == Self::ServiceName.as_str()
|| n.kind() == Self::RpcName.as_str()
|| n.kind() == Self::FieldName.as_str()
|| Self::is_field_decl_parent(n)
}

/// Kinds whose direct identifier child is the *name* of a field-like
/// declaration: regular fields, map fields, oneof fields, the oneof itself,
/// and enum values. For `string title = 1;`, the identifier `title` has
/// parent `field` — that's what we match here. The type identifier (e.g.
/// `Author` in `Author author = 2;`) is nested deeper under
/// `message_or_enum_type`, so it isn't caught by this predicate.
pub fn is_field_decl_parent(n: &Node) -> bool {
matches!(
n.kind(),
"field" | "map_field" | "oneof_field" | "oneof" | "enum_field"
)
}

pub fn is_actionable(n: &Node) -> bool {
n.kind() == Self::MessageName.as_str()
|| n.kind() == Self::EnumName.as_str()
Expand Down
22 changes: 22 additions & 0 deletions src/parser/input/test_rename_field.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
syntax = "proto3";

package com.parser;

enum Color {
RED = 0;
GREEN = 1;
}

message Author {
string name = 1;
}

message Book {
string title = 1;
Author author = 2;
map<string, int32> counts = 3;
oneof body {
string text = 4;
bytes blob = 5;
}
}
14 changes: 14 additions & 0 deletions src/parser/input/test_rename_service.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
syntax = "proto3";

package com.parser;

message Empty {}

message Book {
string title = 1;
}

service Library {
rpc GetBook(Empty) returns (Book);
rpc ListBooks(Empty) returns (Book);
}
Loading