From db4e71a5c160c895bf7735ce9258653912ab6ee6 Mon Sep 17 00:00:00 2001 From: mohammadkhan Date: Sat, 24 Aug 2024 13:56:26 +0530 Subject: [PATCH 1/5] wip: rename workspace --- sample/simple.proto | 1 + src/lsp.rs | 48 +++++++++--- src/nodekind.rs | 4 + src/parser/definition.rs | 57 +++++++------- src/parser/hover.rs | 48 ++++++------ src/parser/rename.rs | 76 +++++++------------ ...parser__rename__test__rename_fields-2.snap | 12 +++ ...parser__rename__test__rename_fields-3.snap | 5 ++ ...__parser__rename__test__rename_fields.snap | 28 +++++++ src/parser/tree.rs | 29 +++++-- src/state.rs | 9 +++ src/workspace/mod.rs | 1 + src/workspace/rename.rs | 61 +++++++++++++++ 13 files changed, 262 insertions(+), 117 deletions(-) create mode 100644 src/parser/snapshots/protols__parser__rename__test__rename_fields-2.snap create mode 100644 src/parser/snapshots/protols__parser__rename__test__rename_fields-3.snap create mode 100644 src/parser/snapshots/protols__parser__rename__test__rename_fields.snap create mode 100644 src/workspace/rename.rs diff --git a/sample/simple.proto b/sample/simple.proto index ee51325..9cc8717 100644 --- a/sample/simple.proto +++ b/sample/simple.proto @@ -2,6 +2,7 @@ syntax = "proto3"; package com.book; +// This is a book represeted by some comments that we like to address in the review message Book { // This is a multi line comment on the field name // Of a message called Book diff --git a/src/lsp.rs b/src/lsp.rs index 36be1b6..0b28efb 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::fs::read_to_string; use std::ops::ControlFlow; use std::sync::mpsc; @@ -11,11 +12,10 @@ use async_lsp::lsp_types::{ DocumentSymbolResponse, FileOperationFilter, FileOperationPattern, FileOperationPatternKind, FileOperationRegistrationOptions, GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams, HoverProviderCapability, InitializeParams, InitializeResult, OneOf, - PrepareRenameResponse, ProgressParams, RenameFilesParams, RenameOptions, - RenameParams, ServerCapabilities, ServerInfo, TextDocumentPositionParams, - TextDocumentSyncCapability, TextDocumentSyncKind, Url, WorkspaceEdit, - WorkspaceFileOperationsServerCapabilities, WorkspaceFoldersServerCapabilities, - WorkspaceServerCapabilities, + PrepareRenameResponse, ProgressParams, RenameFilesParams, RenameOptions, RenameParams, + ServerCapabilities, ServerInfo, TextDocumentPositionParams, TextDocumentSyncCapability, + TextDocumentSyncKind, TextEdit, Url, WorkspaceEdit, WorkspaceFileOperationsServerCapabilities, + WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities, }; use async_lsp::{LanguageClient, LanguageServer, ResponseError}; use futures::future::BoxFuture; @@ -187,7 +187,7 @@ impl LanguageServer for ProtoLanguageServer { "enum", "oneof", "repeated", "reserved", "to", ]; - let mut keywords: Vec = keywords + let mut completions: Vec = keywords .into_iter() .map(|w| CompletionItem { label: w.to_string(), @@ -199,10 +199,10 @@ impl LanguageServer for ProtoLanguageServer { if let Some(tree) = self.state.get_tree(&uri) { let content = self.state.get_content(&uri); if let Some(package_name) = tree.get_package_name(content.as_bytes()) { - keywords.extend(self.state.completion_items(package_name)); + completions.extend(self.state.completion_items(package_name)); } } - Box::pin(async move { Ok(Some(CompletionResponse::Array(keywords))) }) + Box::pin(async move { Ok(Some(CompletionResponse::Array(completions))) }) } fn prepare_rename( @@ -238,12 +238,36 @@ impl LanguageServer for ProtoLanguageServer { let content = self.state.get_content(&uri); - let response = if tree.can_rename(&pos).is_some() { - tree.rename(&pos, &new_name, content) - } else { - None + let Some(identifier) = tree.get_full_node_text_at_position(&pos, content.as_bytes()) else { + error!(uri=%uri, "failed to get full identifier"); + return Box::pin(async move { Ok(None) }); + }; + + let Some(current_package) = tree.get_package_name(content.as_bytes()) else { + error!(uri=%uri, "failed to get package name"); + return Box::pin(async move { Ok(None) }); + }; + + let Some(rename_range) = tree.can_rename(&pos) else { + return Box::pin(async move { Ok(None) }); }; + let mut h = HashMap::new(); + h.insert( + tree.uri.clone(), + vec![TextEdit { + new_text: new_name.clone(), + range: rename_range, + }], + ); + + h.extend(self.state.rename(current_package, &identifier, &new_name)); + + let response = Some(WorkspaceEdit { + changes: Some(h), + ..Default::default() + }); + Box::pin(async move { Ok(response) }) } diff --git a/src/nodekind.rs b/src/nodekind.rs index 83a7d94..0095137 100644 --- a/src/nodekind.rs +++ b/src/nodekind.rs @@ -47,6 +47,10 @@ impl NodeKind { n.kind() == Self::MessageName.as_str() } + pub fn is_field_name(n: &Node) -> bool { + n.kind() == Self::FieldName.as_str() + } + pub fn is_userdefined(n: &Node) -> bool { n.kind() == Self::EnumName.as_str() || n.kind() == Self::MessageName.as_str() } diff --git a/src/parser/definition.rs b/src/parser/definition.rs index e1f6749..97027c5 100644 --- a/src/parser/definition.rs +++ b/src/parser/definition.rs @@ -22,34 +22,39 @@ impl ParsedTree { return; } - if !identifier.contains('.') { - let locations: Vec = self - .filter_nodes_from(n, NodeKind::is_userdefined) - .into_iter() - .filter(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error") == identifier) - .map(|n| Location { - uri: self.uri.clone(), - range: Range { - start: ts_to_lsp_position(&n.start_position()), - end: ts_to_lsp_position(&n.end_position()), - }, - }) - .collect(); + match identifier.split_once('.') { + Some((parent_identifier, remaining)) => { + let child_node = self + .filter_nodes_from(n, NodeKind::is_userdefined) + .into_iter() + .find(|n| { + n.utf8_text(content.as_ref()).expect("utf8-parse error") + == parent_identifier + }) + .and_then(|n| n.parent()); - v.extend(locations); - return; - } - - // Safety: identifier contains a . - let (parent_identifier, remaining) = identifier.split_once('.').unwrap(); - let child_node = self - .filter_nodes_from(n, NodeKind::is_userdefined) - .into_iter() - .find(|n| n.utf8_text(content.as_ref()).expect("utf8-parse error") == parent_identifier) - .and_then(|n| n.parent()); + if let Some(inner) = child_node { + self.definition_impl(remaining, inner, v, content); + } + } + None => { + let locations: Vec = self + .filter_nodes_from(n, NodeKind::is_userdefined) + .into_iter() + .filter(|n| { + n.utf8_text(content.as_ref()).expect("utf-8 parse error") == identifier + }) + .map(|n| Location { + uri: self.uri.clone(), + range: Range { + start: ts_to_lsp_position(&n.start_position()), + end: ts_to_lsp_position(&n.end_position()), + }, + }) + .collect(); - if let Some(inner) = child_node { - self.definition_impl(remaining, inner, v, content); + v.extend(locations); + } } } } diff --git a/src/parser/hover.rs b/src/parser/hover.rs index 738abaa..110b7f5 100644 --- a/src/parser/hover.rs +++ b/src/parser/hover.rs @@ -64,29 +64,31 @@ impl ParsedTree { return; } - if !identifier.contains('.') { - let comments: Vec = self - .filter_nodes_from(n, NodeKind::is_userdefined) - .into_iter() - .filter(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error") == identifier) - .filter_map(|n| self.find_preceding_comments(n.id(), content.as_ref())) - .map(MarkedString::String) - .collect(); - - v.extend(comments); - return; - } - - // Safety: identifier contains a . - let (parent_identifier, remaining) = identifier.split_once('.').unwrap(); - let child_node = self - .filter_nodes_from(n, NodeKind::is_userdefined) - .into_iter() - .find(|n| n.utf8_text(content.as_ref()).expect("utf8-parse error") == parent_identifier) - .and_then(|n| n.parent()); - - if let Some(inner) = child_node { - self.hover_impl(remaining, inner, v, content); + match identifier.split_once('.') { + Some((parent, child)) => { + let child_node = self + .filter_nodes_from(n, NodeKind::is_userdefined) + .into_iter() + .find(|n| n.utf8_text(content.as_ref()).expect("utf8-parse error") == parent) + .and_then(|n| n.parent()); + + if let Some(inner) = child_node { + self.hover_impl(child, inner, v, content); + } + } + None => { + let comments: Vec = self + .filter_nodes_from(n, NodeKind::is_userdefined) + .into_iter() + .filter(|n| { + n.utf8_text(content.as_ref()).expect("utf-8 parse error") == identifier + }) + .filter_map(|n| self.find_preceding_comments(n.id(), content.as_ref())) + .map(MarkedString::String) + .collect(); + + v.extend(comments); + } } } } diff --git a/src/parser/rename.rs b/src/parser/rename.rs index e61f231..ffbad44 100644 --- a/src/parser/rename.rs +++ b/src/parser/rename.rs @@ -1,6 +1,4 @@ -use std::collections::HashMap; - -use async_lsp::lsp_types::{Position, Range, TextEdit, WorkspaceEdit}; +use async_lsp::lsp_types::{Position, Range, TextEdit}; use crate::{nodekind::NodeKind, utils::ts_to_lsp_position}; @@ -22,41 +20,33 @@ impl ParsedTree { }) } - pub fn rename( + pub fn rename_fields( &self, - pos: &Position, - new_text: &str, + field_name: &str, + new_identifier: &str, content: impl AsRef<[u8]>, - ) -> Option { - let old_text = self - .get_node_text_at_position(pos, content.as_ref()) - .unwrap_or_default(); - - let mut changes = HashMap::new(); + ) -> Vec { + let renaming_field = field_name.split('.').last().unwrap_or(field_name); + let new_field_name = field_name.replace(renaming_field, new_identifier); - let diff: Vec<_> = self - .filter_nodes(NodeKind::is_identifier) + self.filter_nodes(NodeKind::is_field_name) .into_iter() - .filter(|n| n.utf8_text(content.as_ref()).unwrap() == old_text) - .map(|n| TextEdit { - new_text: new_text.to_string(), - range: Range { - start: ts_to_lsp_position(&n.start_position()), - end: ts_to_lsp_position(&n.end_position()), - }, + .filter(|n| { + n.utf8_text(content.as_ref()) + .expect("utf-8 parse error") + .starts_with(field_name) }) - .collect(); - - if diff.is_empty() { - return None; - } - - changes.insert(self.uri.clone(), diff); - - Some(WorkspaceEdit { - changes: Some(changes), - ..Default::default() - }) + .map(|n| { + let old_text = n.utf8_text(content.as_ref()).expect("utf-8 parse error"); + TextEdit { + new_text: old_text.replace(field_name, &new_field_name), + range: Range { + start: ts_to_lsp_position(&n.start_position()), + end: ts_to_lsp_position(&n.end_position()), + }, + } + }) + .collect() } } @@ -68,29 +58,17 @@ mod test { use crate::parser::ProtoParser; #[test] - fn test_rename() { + fn test_rename_fields() { let uri: Url = "file://foo/bar.proto".parse().unwrap(); - let pos_book_rename = Position { - line: 5, - character: 9, - }; - let pos_author_rename = Position { - line: 21, - character: 10, - }; - let pos_non_renamble = Position { - line: 24, - character: 4, - }; let contents = include_str!("input/test_rename.proto"); let parsed = ProtoParser::new().parse(uri.clone(), contents); assert!(parsed.is_some()); let tree = parsed.unwrap(); - assert_yaml_snapshot!(tree.rename(&pos_book_rename, "Kitab", contents)); - assert_yaml_snapshot!(tree.rename(&pos_author_rename, "Writer", contents)); - assert_yaml_snapshot!(tree.rename(&pos_non_renamble, "Doesn't matter", contents)); + assert_yaml_snapshot!(tree.rename_fields("Book", "Kitab", contents)); + assert_yaml_snapshot!(tree.rename_fields("Book.Author", "Writer", contents)); + assert_yaml_snapshot!(tree.rename_fields("xyz.abc", "Doesn't matter", contents)); } #[test] diff --git a/src/parser/snapshots/protols__parser__rename__test__rename_fields-2.snap b/src/parser/snapshots/protols__parser__rename__test__rename_fields-2.snap new file mode 100644 index 0000000..3cf596e --- /dev/null +++ b/src/parser/snapshots/protols__parser__rename__test__rename_fields-2.snap @@ -0,0 +1,12 @@ +--- +source: src/parser/rename.rs +expression: "tree.rename_fields(\"Book.Author\", \"Writer\", contents)" +--- +- range: + start: + line: 21 + character: 4 + end: + line: 21 + character: 15 + newText: Book.Writer diff --git a/src/parser/snapshots/protols__parser__rename__test__rename_fields-3.snap b/src/parser/snapshots/protols__parser__rename__test__rename_fields-3.snap new file mode 100644 index 0000000..02ded32 --- /dev/null +++ b/src/parser/snapshots/protols__parser__rename__test__rename_fields-3.snap @@ -0,0 +1,5 @@ +--- +source: src/parser/rename.rs +expression: "tree.rename_fields(\"xyz.abc\", \"Doesn't matter\", contents)" +--- +[] diff --git a/src/parser/snapshots/protols__parser__rename__test__rename_fields.snap b/src/parser/snapshots/protols__parser__rename__test__rename_fields.snap new file mode 100644 index 0000000..0d62b54 --- /dev/null +++ b/src/parser/snapshots/protols__parser__rename__test__rename_fields.snap @@ -0,0 +1,28 @@ +--- +source: src/parser/rename.rs +expression: "tree.rename_fields(\"Book\", \"Kitab\", contents)" +--- +- range: + start: + line: 20 + character: 13 + end: + line: 20 + character: 17 + newText: Kitab +- range: + start: + line: 21 + character: 4 + end: + line: 21 + character: 15 + newText: Kitab.Author +- range: + start: + line: 25 + character: 32 + end: + line: 25 + character: 36 + newText: Kitab diff --git a/src/parser/tree.rs b/src/parser/tree.rs index fc82d40..7ecf5a7 100644 --- a/src/parser/tree.rs +++ b/src/parser/tree.rs @@ -72,6 +72,26 @@ impl ParsedTree { .map(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error")) } + pub fn get_full_node_text_at_position<'a>( + &'a self, + pos: &Position, + content: &'a [u8], + ) -> Option { + let Some(n) = self.get_actionable_node_at_position(pos) else { + return None; + }; + + let ntext = n.utf8_text(content.as_ref()).expect("utf-8 parse error"); + let mut result = format!("{ntext}"); + while let Some(p) = n.parent() { + if NodeKind::is_message_name(&n) { + let ptext = p.utf8_text(content.as_ref()).expect("utf-8 parse error"); + result = format!("{ptext}.{result}"); + } + } + Some(result) + } + pub fn get_actionable_node_at_position<'a>(&'a self, pos: &Position) -> Option> { self.get_node_at_position(pos) .map(|n| { @@ -118,13 +138,8 @@ impl ParsedTree { mod test { use async_lsp::lsp_types::Url; use insta::assert_yaml_snapshot; - use tree_sitter::Node; - use crate::parser::ProtoParser; - - fn is_message(n: &Node) -> bool { - n.kind() == "message_name" - } + use crate::{nodekind::NodeKind, parser::ProtoParser}; #[test] fn test_filter() { @@ -134,7 +149,7 @@ mod test { assert!(parsed.is_some()); let tree = parsed.unwrap(); - let nodes = tree.filter_nodes(is_message); + let nodes = tree.filter_nodes(NodeKind::is_message_name); assert_eq!(nodes.len(), 2); diff --git a/src/state.rs b/src/state.rs index 983eec2..fc20510 100644 --- a/src/state.rs +++ b/src/state.rs @@ -47,6 +47,15 @@ impl ProtoLanguageState { self.trees.read().expect("poison").get(uri).cloned() } + pub fn get_trees(&self) -> Vec { + self.trees + .read() + .expect("poison") + .values() + .map(ToOwned::to_owned) + .collect() + } + pub fn get_trees_for_package(&self, package: &str) -> Vec { self.trees .read() diff --git a/src/workspace/mod.rs b/src/workspace/mod.rs index 9a26930..cca1e38 100644 --- a/src/workspace/mod.rs +++ b/src/workspace/mod.rs @@ -1,2 +1,3 @@ mod definition; mod hover; +mod rename; diff --git a/src/workspace/rename.rs b/src/workspace/rename.rs new file mode 100644 index 0000000..15064a1 --- /dev/null +++ b/src/workspace/rename.rs @@ -0,0 +1,61 @@ +use crate::utils::split_identifier_package; +use std::collections::HashMap; + +use async_lsp::lsp_types::{TextEdit, Url}; + +use crate::state::ProtoLanguageState; + +impl ProtoLanguageState { + pub fn rename( + &self, + current_package: &str, + identifier: &str, + new_text: &str, + ) -> HashMap> { + let (_, identifier) = split_identifier_package(identifier); + self.get_trees() + .into_iter() + .fold(HashMap::new(), |mut h, tree| { + let content = self.get_content(&tree.uri); + let package = tree.get_package_name(content.as_ref()).unwrap_or_default(); + let target = if current_package != package { + format!("{current_package}.{identifier}") + } else { + identifier.to_owned() + }; + let v = tree.rename_fields(target.as_str(), new_text, content.as_str()); + if !v.is_empty() { + h.insert(tree.uri.clone(), v); + } + h + }) + } +} + +#[cfg(test)] +mod test { + use insta::assert_yaml_snapshot; + + use crate::state::ProtoLanguageState; + + #[test] + fn test_rename() { + let a_uri = "file://input/a.proto".parse().unwrap(); + let b_uri = "file://input/b.proto".parse().unwrap(); + let c_uri = "file://input/c.proto".parse().unwrap(); + + let a = include_str!("input/a.proto"); + let b = include_str!("input/b.proto"); + let c = include_str!("input/c.proto"); + + let mut state = ProtoLanguageState::new(); + state.upsert_file(&a_uri, a.to_owned()); + state.upsert_file(&b_uri, b.to_owned()); + state.upsert_file(&c_uri, c.to_owned()); + + assert_yaml_snapshot!(state.rename("com.workspace", "Author", "Writer")); + assert_yaml_snapshot!(state.rename("com.workspace", "Author.Address", "Location")); + assert_yaml_snapshot!(state.rename("com.workspace", "com.utility.Foobar.Baz", "Baaz")); + assert_yaml_snapshot!(state.rename("com.utility", "Baz", "Baaz")); + } +} From 4c8099f91487320ed371fc66b49e1426c681d69a Mon Sep 17 00:00:00 2001 From: coder3101 Date: Sat, 24 Aug 2024 23:31:58 +0530 Subject: [PATCH 2/5] feat: passing test for rename across workspace --- src/lsp.rs | 22 ++-- src/nodekind.rs | 6 ++ src/parser/input/test_rename.proto | 1 + src/parser/rename.rs | 101 +++++++++++++++--- ...otols__parser__rename__test__rename-2.snap | 60 ++++++----- ...otols__parser__rename__test__rename-3.snap | 4 +- ...protols__parser__rename__test__rename.snap | 76 +++++++------ src/parser/tree.rs | 35 +++--- src/workspace/input/a.proto | 2 +- src/workspace/rename.rs | 22 ++-- ...ls__workspace__rename__test__rename-2.snap | 13 +++ ...ls__workspace__rename__test__rename-3.snap | 13 +++ ...tols__workspace__rename__test__rename.snap | 21 ++++ 13 files changed, 250 insertions(+), 126 deletions(-) create mode 100644 src/workspace/snapshots/protols__workspace__rename__test__rename-2.snap create mode 100644 src/workspace/snapshots/protols__workspace__rename__test__rename-3.snap create mode 100644 src/workspace/snapshots/protols__workspace__rename__test__rename.snap diff --git a/src/lsp.rs b/src/lsp.rs index 0b28efb..e96d43d 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -14,7 +14,7 @@ use async_lsp::lsp_types::{ HoverContents, HoverParams, HoverProviderCapability, InitializeParams, InitializeResult, OneOf, PrepareRenameResponse, ProgressParams, RenameFilesParams, RenameOptions, RenameParams, ServerCapabilities, ServerInfo, TextDocumentPositionParams, TextDocumentSyncCapability, - TextDocumentSyncKind, TextEdit, Url, WorkspaceEdit, WorkspaceFileOperationsServerCapabilities, + TextDocumentSyncKind, Url, WorkspaceEdit, WorkspaceFileOperationsServerCapabilities, WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities, }; use async_lsp::{LanguageClient, LanguageServer, ResponseError}; @@ -238,30 +238,20 @@ impl LanguageServer for ProtoLanguageServer { let content = self.state.get_content(&uri); - let Some(identifier) = tree.get_full_node_text_at_position(&pos, content.as_bytes()) else { - error!(uri=%uri, "failed to get full identifier"); - return Box::pin(async move { Ok(None) }); - }; - let Some(current_package) = tree.get_package_name(content.as_bytes()) else { error!(uri=%uri, "failed to get package name"); return Box::pin(async move { Ok(None) }); }; - let Some(rename_range) = tree.can_rename(&pos) else { + 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) }); }; let mut h = HashMap::new(); - h.insert( - tree.uri.clone(), - vec![TextEdit { - new_text: new_name.clone(), - range: rename_range, - }], - ); - - h.extend(self.state.rename(current_package, &identifier, &new_name)); + h.insert(tree.uri.clone(), edit); + h.extend(self.state.rename_fields(current_package, &otext, &ntext)); let response = Some(WorkspaceEdit { changes: Some(h), diff --git a/src/nodekind.rs b/src/nodekind.rs index 0095137..0205ff0 100644 --- a/src/nodekind.rs +++ b/src/nodekind.rs @@ -5,6 +5,7 @@ pub enum NodeKind { Identifier, Error, MessageName, + Message, EnumName, FieldName, ServiceName, @@ -19,6 +20,7 @@ impl NodeKind { NodeKind::Identifier => "identifier", NodeKind::Error => "ERROR", NodeKind::MessageName => "message_name", + NodeKind::Message => "message", NodeKind::EnumName => "enum_name", NodeKind::FieldName => "message_or_enum_type", NodeKind::ServiceName => "service_name", @@ -47,6 +49,10 @@ impl NodeKind { n.kind() == Self::MessageName.as_str() } + pub fn is_message(n: &Node) -> bool { + n.kind() == Self::Message.as_str() + } + pub fn is_field_name(n: &Node) -> bool { n.kind() == Self::FieldName.as_str() } diff --git a/src/parser/input/test_rename.proto b/src/parser/input/test_rename.proto index a10bef9..d7e5b16 100644 --- a/src/parser/input/test_rename.proto +++ b/src/parser/input/test_rename.proto @@ -24,4 +24,5 @@ message Library { service Myservice { rpc GetBook(Empty) returns (Book); + rpc GetAuthor(Empty) returns (Book.Author) } diff --git a/src/parser/rename.rs b/src/parser/rename.rs index ffbad44..1f988d1 100644 --- a/src/parser/rename.rs +++ b/src/parser/rename.rs @@ -1,4 +1,5 @@ use async_lsp::lsp_types::{Position, Range, TextEdit}; +use tree_sitter::Node; use crate::{nodekind::NodeKind, utils::ts_to_lsp_position}; @@ -20,26 +21,80 @@ impl ParsedTree { }) } - pub fn rename_fields( + fn rename_within<'a>( &self, - field_name: &str, + n: Node<'a>, + identifier: &str, new_identifier: &str, content: impl AsRef<[u8]>, - ) -> Vec { - let renaming_field = field_name.split('.').last().unwrap_or(field_name); - let new_field_name = field_name.replace(renaming_field, new_identifier); + ) -> Option> { + n.parent().map(|p| { + self.filter_nodes_from(p, NodeKind::is_field_name) + .into_iter() + .filter(|i| i.utf8_text(content.as_ref()).expect("utf-8 parse error") == identifier) + .map(|i| TextEdit { + range: Range { + start: ts_to_lsp_position(&i.start_position()), + end: ts_to_lsp_position(&i.end_position()), + }, + new_text: new_identifier.to_string(), + }) + .collect() + }) + } + + pub fn rename_tree( + &self, + pos: &Position, + new_name: &str, + content: impl AsRef<[u8]>, + ) -> Option<(Vec, String, String)> { + let rename_range = self.can_rename(pos)?; + + let mut v = vec![TextEdit { + range: rename_range, + new_text: new_name.to_owned(), + }]; + let nodes = self.get_ancestor_nodes_at_position(pos); + + let mut i = 1; + let mut otext = nodes.get(0)?.utf8_text(content.as_ref()).ok()?.to_owned(); + let mut ntext = new_name.to_owned(); + + while nodes.len() > i { + let id = nodes[i].utf8_text(content.as_ref()).ok()?; + + if let Some(edit) = self.rename_within(nodes[i], &otext, &ntext, content.as_ref()) { + v.extend(edit); + } + + otext = format!("{id}.{otext}"); + ntext = format!("{id}.{ntext}"); + + i += 1 + } + + return Some((v, otext, ntext)); + } + + pub fn rename_field( + &self, + old_identifier: &str, + new_identifier: &str, + content: impl AsRef<[u8]>, + ) -> Vec { self.filter_nodes(NodeKind::is_field_name) .into_iter() .filter(|n| { n.utf8_text(content.as_ref()) .expect("utf-8 parse error") - .starts_with(field_name) + .starts_with(old_identifier) }) .map(|n| { - let old_text = n.utf8_text(content.as_ref()).expect("utf-8 parse error"); + let text = n.utf8_text(content.as_ref()).expect("utf-8 parse error"); TextEdit { - new_text: old_text.replace(field_name, &new_field_name), + new_text: text.replace(old_identifier, new_identifier), range: Range { start: ts_to_lsp_position(&n.start_position()), end: ts_to_lsp_position(&n.end_position()), @@ -58,17 +113,39 @@ mod test { use crate::parser::ProtoParser; #[test] - fn test_rename_fields() { + fn test_rename() { let uri: Url = "file://foo/bar.proto".parse().unwrap(); + let pos_book = Position { + line: 5, + character: 9, + }; + let pos_author = Position { + line: 11, + character: 14, + }; + let pos_non_rename = Position { + line: 21, + character: 5, + }; let contents = include_str!("input/test_rename.proto"); let parsed = ProtoParser::new().parse(uri.clone(), contents); assert!(parsed.is_some()); let tree = parsed.unwrap(); - assert_yaml_snapshot!(tree.rename_fields("Book", "Kitab", contents)); - assert_yaml_snapshot!(tree.rename_fields("Book.Author", "Writer", contents)); - assert_yaml_snapshot!(tree.rename_fields("xyz.abc", "Doesn't matter", contents)); + let rename_fn = |nt: &str, pos: &Position| { + if let Some(k) = tree.rename_tree(pos, nt, contents) { + let mut v = tree.rename_field(&k.1, &k.2, contents); + v.extend(k.0); + v + } else { + vec![] + } + }; + + assert_yaml_snapshot!(rename_fn("Kitab", &pos_book)); + assert_yaml_snapshot!(rename_fn("Writer", &pos_author)); + assert_yaml_snapshot!(rename_fn("xyx", &pos_non_rename)); } #[test] diff --git a/src/parser/snapshots/protols__parser__rename__test__rename-2.snap b/src/parser/snapshots/protols__parser__rename__test__rename-2.snap index a0eebe6..bce7327 100644 --- a/src/parser/snapshots/protols__parser__rename__test__rename-2.snap +++ b/src/parser/snapshots/protols__parser__rename__test__rename-2.snap @@ -1,30 +1,36 @@ --- source: src/parser/rename.rs -expression: "tree.rename(&pos_author_rename, \"Writer\", contents)" +expression: "rename_fn(\"Writer\", &pos_author)" --- -changes: - "file://foo/bar.proto": - - range: - start: - line: 11 - character: 12 - end: - line: 11 - character: 18 - newText: Writer - - range: - start: - line: 15 - character: 4 - end: - line: 15 - character: 10 - newText: Writer - - range: - start: - line: 21 - character: 9 - end: - line: 21 - character: 15 - newText: Writer +- range: + start: + line: 21 + character: 4 + end: + line: 21 + character: 15 + newText: Book.Writer +- range: + start: + line: 26 + character: 34 + end: + line: 26 + character: 45 + newText: Book.Writer +- range: + start: + line: 11 + character: 12 + end: + line: 11 + character: 18 + newText: Writer +- range: + start: + line: 15 + character: 4 + end: + line: 15 + character: 10 + newText: Writer diff --git a/src/parser/snapshots/protols__parser__rename__test__rename-3.snap b/src/parser/snapshots/protols__parser__rename__test__rename-3.snap index 9fe1587..50e431e 100644 --- a/src/parser/snapshots/protols__parser__rename__test__rename-3.snap +++ b/src/parser/snapshots/protols__parser__rename__test__rename-3.snap @@ -1,5 +1,5 @@ --- source: src/parser/rename.rs -expression: "tree.rename(&pos_non_renamble, \"Doesn't matter\", contents)" +expression: "rename_fn(\"xyx\", &pos_non_rename)" --- -~ +[] diff --git a/src/parser/snapshots/protols__parser__rename__test__rename.snap b/src/parser/snapshots/protols__parser__rename__test__rename.snap index 91465d7..1302a83 100644 --- a/src/parser/snapshots/protols__parser__rename__test__rename.snap +++ b/src/parser/snapshots/protols__parser__rename__test__rename.snap @@ -1,38 +1,44 @@ --- source: src/parser/rename.rs -expression: "tree.rename(&pos_book_rename, \"Kitab\", contents)" +expression: "rename_fn(\"Kitab\", &pos_book)" --- -changes: - "file://foo/bar.proto": - - range: - start: - line: 5 - character: 8 - end: - line: 5 - character: 12 - newText: Kitab - - range: - start: - line: 20 - character: 13 - end: - line: 20 - character: 17 - newText: Kitab - - range: - start: - line: 21 - character: 4 - end: - line: 21 - character: 8 - newText: Kitab - - range: - start: - line: 25 - character: 32 - end: - line: 25 - character: 36 - newText: Kitab +- range: + start: + line: 20 + character: 13 + end: + line: 20 + character: 17 + newText: Kitab +- range: + start: + line: 21 + character: 4 + end: + line: 21 + character: 15 + newText: Kitab.Author +- range: + start: + line: 25 + character: 32 + end: + line: 25 + character: 36 + newText: Kitab +- range: + start: + line: 26 + character: 34 + end: + line: 26 + character: 45 + newText: Kitab.Author +- range: + start: + line: 5 + character: 8 + end: + line: 5 + character: 12 + newText: Kitab diff --git a/src/parser/tree.rs b/src/parser/tree.rs index 7ecf5a7..68cffc7 100644 --- a/src/parser/tree.rs +++ b/src/parser/tree.rs @@ -54,15 +54,6 @@ impl ParsedTree { } } - pub fn get_node_text_at_position<'a>( - &'a self, - pos: &Position, - content: &'a [u8], - ) -> Option<&'a str> { - self.get_node_at_position(pos) - .map(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error")) - } - pub fn get_actionable_node_text_at_position<'a>( &'a self, pos: &Position, @@ -72,24 +63,24 @@ impl ParsedTree { .map(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error")) } - pub fn get_full_node_text_at_position<'a>( - &'a self, - pos: &Position, - content: &'a [u8], - ) -> Option { - let Some(n) = self.get_actionable_node_at_position(pos) else { - return None; + pub fn get_ancestor_nodes_at_position<'a>(&'a self, pos: &Position) -> Vec> { + let Some(mut n) = self.get_actionable_node_at_position(pos) else { + return vec![]; }; - let ntext = n.utf8_text(content.as_ref()).expect("utf-8 parse error"); - let mut result = format!("{ntext}"); + let mut nodes = vec![]; while let Some(p) = n.parent() { - if NodeKind::is_message_name(&n) { - let ptext = p.utf8_text(content.as_ref()).expect("utf-8 parse error"); - result = format!("{ptext}.{result}"); + if NodeKind::is_message(&p) { + for i in 0..p.child_count() { + let t = p.child(i).unwrap(); + if NodeKind::is_message_name(&t) { + nodes.push(t); + } + } } + n = p; } - Some(result) + nodes } pub fn get_actionable_node_at_position<'a>(&'a self, pos: &Position) -> Option> { diff --git a/src/workspace/input/a.proto b/src/workspace/input/a.proto index a982e4a..49df280 100644 --- a/src/workspace/input/a.proto +++ b/src/workspace/input/a.proto @@ -8,6 +8,6 @@ import "c.proto"; message Book { Author author = 1; Author.Address foo = 2; - com.utility.FooBar.Baz z = 3; + com.utility.Foobar.Baz z = 3; } diff --git a/src/workspace/rename.rs b/src/workspace/rename.rs index 15064a1..717d2ee 100644 --- a/src/workspace/rename.rs +++ b/src/workspace/rename.rs @@ -6,7 +6,7 @@ use async_lsp::lsp_types::{TextEdit, Url}; use crate::state::ProtoLanguageState; impl ProtoLanguageState { - pub fn rename( + pub fn rename_fields( &self, current_package: &str, identifier: &str, @@ -18,12 +18,13 @@ impl ProtoLanguageState { .fold(HashMap::new(), |mut h, tree| { let content = self.get_content(&tree.uri); let package = tree.get_package_name(content.as_ref()).unwrap_or_default(); - let target = if current_package != package { - format!("{current_package}.{identifier}") - } else { - identifier.to_owned() - }; - let v = tree.rename_fields(target.as_str(), new_text, content.as_str()); + let mut old = identifier.to_string(); + let mut new = new_text.to_string(); + if current_package != package { + old = format!("{current_package}.{old}"); + new = format!("{current_package}.{new}"); + } + let v = tree.rename_field(&old, &new, content.as_str()); if !v.is_empty() { h.insert(tree.uri.clone(), v); } @@ -53,9 +54,8 @@ mod test { state.upsert_file(&b_uri, b.to_owned()); state.upsert_file(&c_uri, c.to_owned()); - assert_yaml_snapshot!(state.rename("com.workspace", "Author", "Writer")); - assert_yaml_snapshot!(state.rename("com.workspace", "Author.Address", "Location")); - assert_yaml_snapshot!(state.rename("com.workspace", "com.utility.Foobar.Baz", "Baaz")); - assert_yaml_snapshot!(state.rename("com.utility", "Baz", "Baaz")); + assert_yaml_snapshot!(state.rename_fields("com.workspace", "Author", "Writer")); + assert_yaml_snapshot!(state.rename_fields("com.workspace", "Author.Address", "Author.Location")); + assert_yaml_snapshot!(state.rename_fields("com.utility", "Foobar.Baz", "Foobar.Baaz")); } } diff --git a/src/workspace/snapshots/protols__workspace__rename__test__rename-2.snap b/src/workspace/snapshots/protols__workspace__rename__test__rename-2.snap new file mode 100644 index 0000000..68638ac --- /dev/null +++ b/src/workspace/snapshots/protols__workspace__rename__test__rename-2.snap @@ -0,0 +1,13 @@ +--- +source: src/workspace/rename.rs +expression: "state.rename_fields(\"com.workspace\", \"Author.Address\", \"Author.Location\")" +--- +"file://input/a.proto": + - range: + start: + line: 9 + character: 3 + end: + line: 9 + character: 17 + newText: Author.Location diff --git a/src/workspace/snapshots/protols__workspace__rename__test__rename-3.snap b/src/workspace/snapshots/protols__workspace__rename__test__rename-3.snap new file mode 100644 index 0000000..35166b1 --- /dev/null +++ b/src/workspace/snapshots/protols__workspace__rename__test__rename-3.snap @@ -0,0 +1,13 @@ +--- +source: src/workspace/rename.rs +expression: "state.rename_fields(\"com.utility\", \"Foobar.Baz\", \"Foobar.Baaz\")" +--- +"file://input/a.proto": + - range: + start: + line: 10 + character: 3 + end: + line: 10 + character: 25 + newText: com.utility.Foobar.Baaz diff --git a/src/workspace/snapshots/protols__workspace__rename__test__rename.snap b/src/workspace/snapshots/protols__workspace__rename__test__rename.snap new file mode 100644 index 0000000..4caedd6 --- /dev/null +++ b/src/workspace/snapshots/protols__workspace__rename__test__rename.snap @@ -0,0 +1,21 @@ +--- +source: src/workspace/rename.rs +expression: "state.rename_fields(\"com.workspace\", \"Author\", \"Writer\")" +--- +"file://input/a.proto": + - range: + start: + line: 8 + character: 3 + end: + line: 8 + character: 9 + newText: Writer + - range: + start: + line: 9 + character: 3 + end: + line: 9 + character: 17 + newText: Writer.Address From fd53a4ca5bf2d4a14ece4aea28ae9a2a8be3ed57 Mon Sep 17 00:00:00 2001 From: coder3101 Date: Sat, 24 Aug 2024 23:34:31 +0530 Subject: [PATCH 3/5] bump: version to 0.5.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4c8f272..2e4d717 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "protols" description = "Language server for proto3 files" -version = "0.4.0" +version = "0.5.0" edition = "2021" license = "MIT" homepage = "https://github.com/coder3101/protols" From 91308aca4a89cddce6ba7be6834a3a87ecbe45e0 Mon Sep 17 00:00:00 2001 From: coder3101 Date: Sat, 24 Aug 2024 23:35:59 +0530 Subject: [PATCH 4/5] format code --- Cargo.lock | 2 +- src/workspace/rename.rs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 077c67e..297ca84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -472,7 +472,7 @@ dependencies = [ [[package]] name = "protols" -version = "0.4.0" +version = "0.5.0" dependencies = [ "async-lsp", "futures", diff --git a/src/workspace/rename.rs b/src/workspace/rename.rs index 717d2ee..ab49f59 100644 --- a/src/workspace/rename.rs +++ b/src/workspace/rename.rs @@ -55,7 +55,11 @@ mod test { state.upsert_file(&c_uri, c.to_owned()); assert_yaml_snapshot!(state.rename_fields("com.workspace", "Author", "Writer")); - assert_yaml_snapshot!(state.rename_fields("com.workspace", "Author.Address", "Author.Location")); + assert_yaml_snapshot!(state.rename_fields( + "com.workspace", + "Author.Address", + "Author.Location" + )); assert_yaml_snapshot!(state.rename_fields("com.utility", "Foobar.Baz", "Foobar.Baaz")); } } From 65c22d1e12dd72d3b965f1b7abca254ca62aadc4 Mon Sep 17 00:00:00 2001 From: coder3101 Date: Sat, 24 Aug 2024 23:36:40 +0530 Subject: [PATCH 5/5] lint: clippy --- src/parser/rename.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/parser/rename.rs b/src/parser/rename.rs index 1f988d1..c206dd2 100644 --- a/src/parser/rename.rs +++ b/src/parser/rename.rs @@ -21,9 +21,9 @@ impl ParsedTree { }) } - fn rename_within<'a>( + fn rename_within( &self, - n: Node<'a>, + n: Node<'_>, identifier: &str, new_identifier: &str, content: impl AsRef<[u8]>, @@ -59,7 +59,7 @@ impl ParsedTree { let nodes = self.get_ancestor_nodes_at_position(pos); let mut i = 1; - let mut otext = nodes.get(0)?.utf8_text(content.as_ref()).ok()?.to_owned(); + let mut otext = nodes.first()?.utf8_text(content.as_ref()).ok()?.to_owned(); let mut ntext = new_name.to_owned(); while nodes.len() > i { @@ -75,7 +75,7 @@ impl ParsedTree { i += 1 } - return Some((v, otext, ntext)); + Some((v, otext, ntext)) } pub fn rename_field(