From 9258568c99c1542c1f916cce94322b3cf4408437 Mon Sep 17 00:00:00 2001 From: coder3101 Date: Mon, 22 Jul 2024 01:00:52 +0530 Subject: [PATCH 1/2] wip: feat: add rename capability --- README.md | 2 + src/lsp.rs | 49 +++++++++++++++++- src/parser.rs | 136 ++++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 174 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index cd1107e..acf9f18 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ A Language Server for **proto3** files. It uses tree-sitter parser for all opera - [x] Go to definition - [x] Diagnostics - [x] Document Symbols for message and enums +- [x] Rename message, enum and rpc +- [x] Completion for proto3 keywords ## Installation diff --git a/src/lsp.rs b/src/lsp.rs index 975a9fb..b0f57f6 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -6,8 +6,10 @@ use async_lsp::lsp_types::{ DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, DocumentSymbolParams, DocumentSymbolResponse, GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams, HoverProviderCapability, - InitializeParams, InitializeResult, OneOf, ServerCapabilities, ServerInfo, - TextDocumentSyncCapability, TextDocumentSyncKind, + InitializeParams, InitializeResult, OneOf, PrepareRenameResponse, + RegularExpressionsClientCapabilities, RenameOptions, RenameParams, ServerCapabilities, + ServerInfo, TextDocumentPositionParams, TextDocumentSyncCapability, TextDocumentSyncKind, + WorkspaceEdit, }; use async_lsp::{LanguageClient, LanguageServer, ResponseError}; use futures::future::BoxFuture; @@ -42,6 +44,7 @@ impl LanguageServer for ServerState { hover_provider: Some(HoverProviderCapability::Simple(true)), document_symbol_provider: Some(OneOf::Left(true)), completion_provider: Some(CompletionOptions::default()), + rename_provider: Some(OneOf::Left(true)), ..ServerCapabilities::default() }, server_info: Some(ServerInfo { @@ -102,6 +105,48 @@ impl LanguageServer for ServerState { Box::pin(async move { Ok(Some(CompletionResponse::Array(keywords))) }) } + fn prepare_rename( + &mut self, + params: TextDocumentPositionParams, + ) -> BoxFuture<'static, Result, Self::Error>> { + let uri = params.text_document.uri; + let pos = params.position; + + match self.get_parsed_tree_and_content(&uri) { + Err(e) => Box::pin(async move { Err(e) }), + Ok((tree, _)) => { + let response = tree + .can_rename(&pos) + .map(|(r, _)| PrepareRenameResponse::Range(r)); + + Box::pin(async move { Ok(response) }) + } + } + } + + fn rename( + &mut self, + params: RenameParams, + ) -> BoxFuture<'static, Result, Self::Error>> { + let uri = params.text_document_position.text_document.uri; + let pos = params.text_document_position.position; + + let new_name = params.new_name; + + match self.get_parsed_tree_and_content(&uri) { + Err(e) => Box::pin(async move { Err(e) }), + Ok((tree, content)) => { + let response = if let Some((_, kind)) = tree.can_rename(&pos) { + tree.rename_kind(&uri, &pos, kind, &new_name, content) + } else { + None + }; + + Box::pin(async move { Ok(response) }) + } + } + } + fn definition( &mut self, param: GotoDefinitionParams, diff --git a/src/parser.rs b/src/parser.rs index 270d9bc..ffff6d1 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,8 +1,8 @@ -use std::unreachable; +use std::{collections::HashMap, unreachable}; use async_lsp::lsp_types::{ Diagnostic, DiagnosticSeverity, DocumentSymbol, Location, MarkedString, Position, - PublishDiagnosticsParams, Range, SymbolKind, Url, + PublishDiagnosticsParams, Range, SymbolKind, TextEdit, Url, WorkspaceEdit, }; use tracing::info; use tree_sitter::{Node, Tree, TreeCursor}; @@ -17,6 +17,9 @@ pub struct ParsedTree { tree: Tree, } +const USER_DEFINED_KINDS: &[&str] = &["message_name", "enum_name"]; +const ACTIONABLE_KINDS: &[&str] = &["message_name", "enum_name", "rpc_name", "service_name"]; + #[derive(Default)] struct DocumentSymbolTreeBuilder { // The stack are things we're still in the process of building/parsing. @@ -149,13 +152,15 @@ impl ParsedTree { pos: &Position, content: &'a [u8], ) -> Option<&'a str> { - let pos = lsp_to_ts_point(pos); - self.tree - .root_node() - .descendant_for_point_range(pos, pos) + self.get_node_at_position(pos) .map(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error")) } + pub fn get_node_at_position<'a>(&'a self, pos: &Position) -> Option> { + let pos = lsp_to_ts_point(pos); + self.tree.root_node().descendant_for_point_range(pos, pos) + } + pub fn find_childrens_by_kinds(&self, kinds: &[&str]) -> Vec { let mut cursor = self.tree.root_node().walk(); Self::walk_and_collect_kinds(&mut cursor, kinds) @@ -177,11 +182,10 @@ impl ParsedTree { cursor: &'_ mut TreeCursor, content: &[u8], ) { - let kinds = &["message_name", "enum_name"]; loop { let node = cursor.node(); - if kinds.contains(&node.kind()) { + if USER_DEFINED_KINDS.contains(&node.kind()) { let name = node.utf8_text(content).unwrap(); let kind = match node.kind() { "message_name" => SymbolKind::STRUCT, @@ -236,7 +240,7 @@ impl ParsedTree { match text { Some(text) => self - .find_childrens_by_kinds(&["message_name", "enum_name"]) + .find_childrens_by_kinds(USER_DEFINED_KINDS) .into_iter() .filter(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error") == text) .map(|n| Location { @@ -257,7 +261,7 @@ impl ParsedTree { match text { Some(text) => self - .find_childrens_by_kinds(&["message_name", "enum_name", "service_name", "rpc_name"]) + .find_childrens_by_kinds(ACTIONABLE_KINDS) .into_iter() .filter(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error") == text) .filter_map(|n| self.find_preceding_comments(n.id(), content.as_ref())) @@ -267,6 +271,57 @@ impl ParsedTree { } } + pub fn can_rename(&self, pos: &Position) -> Option<(Range, &str)> { + self.get_node_at_position(pos) + .filter(|n| n.kind() == "identifier") + .map(|n| n.parent().unwrap()) // Safety: Identifier must have a parent node + .filter(|n| ACTIONABLE_KINDS.contains(&n.kind())) + .map(|n| { + ( + Range { + start: ts_to_lsp_position(&n.start_position()), + end: ts_to_lsp_position(&n.end_position()), + }, + n.kind(), + ) + }) + } + + pub fn rename_kind( + &self, + uri: &Url, + pos: &Position, + kind: &str, + new_text: &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(); + + let mut cursor = self.tree.root_node().walk(); + let diff = Self::walk_and_collect_kinds(&mut cursor, &[kind]) + .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()), + }, + }) + .collect(); + + changes.insert(uri.clone(), diff); + + Some(WorkspaceEdit { + changes: Some(changes), + ..Default::default() + }) + } + pub fn collect_parse_errors(&self, uri: &Url) -> PublishDiagnosticsParams { let diagnostics = self .find_childrens_by_kinds(&["ERROR"]) @@ -305,7 +360,7 @@ mod test { package com.book; message Book { - + message Author { string name = 1; string country = 2; @@ -390,6 +445,65 @@ message Book { ); } + #[test] + fn test_rename_kind() { + todo!("implement me") + } + + #[test] + fn test_can_rename() { + let pos_rename = Position { + line: 5, + character: 9, + }; + let pos_non_rename = Position { + line: 2, + character: 2, + }; + let contents = r#"syntax = "proto3"; + +package com.book; + +// A Book is book +message Book { + + // This is represents author + // A author is a someone who writes books + // + // Author has a name and a country where they were born + message Author { + string name = 1; + string country = 2; + }; +} +"#; + let parsed = ProtoParser::new().parse(contents); + assert!(parsed.is_some()); + let tree = parsed.unwrap(); + let res = tree.can_rename(&pos_rename); + + assert!(res.is_some()); + assert_eq!( + res.unwrap(), + ( + Range { + start: Position { + line: 5, + character: 8 + }, + end: Position { + line: 5, + character: 12 + }, + }, + "message_name" + ) + ); + + let res = tree.can_rename(&pos_non_rename); + assert!(res.is_none()); + } + #[test] fn test_hover() { let posbook = Position { From 21317d3d08eb1874df91da02b15833c23b663f78 Mon Sep 17 00:00:00 2001 From: mohammadkhan Date: Mon, 22 Jul 2024 20:33:48 +0530 Subject: [PATCH 2/2] allow renaming all identifiers --- src/lsp.rs | 15 ++-- src/parser.rs | 231 +++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 207 insertions(+), 39 deletions(-) diff --git a/src/lsp.rs b/src/lsp.rs index b0f57f6..0a33c66 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -6,10 +6,9 @@ use async_lsp::lsp_types::{ DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, DocumentSymbolParams, DocumentSymbolResponse, GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams, HoverProviderCapability, - InitializeParams, InitializeResult, OneOf, PrepareRenameResponse, - RegularExpressionsClientCapabilities, RenameOptions, RenameParams, ServerCapabilities, - ServerInfo, TextDocumentPositionParams, TextDocumentSyncCapability, TextDocumentSyncKind, - WorkspaceEdit, + InitializeParams, InitializeResult, OneOf, PrepareRenameResponse, RenameParams, + ServerCapabilities, ServerInfo, TextDocumentPositionParams, TextDocumentSyncCapability, + TextDocumentSyncKind, WorkspaceEdit, }; use async_lsp::{LanguageClient, LanguageServer, ResponseError}; use futures::future::BoxFuture; @@ -115,9 +114,7 @@ impl LanguageServer for ServerState { match self.get_parsed_tree_and_content(&uri) { Err(e) => Box::pin(async move { Err(e) }), Ok((tree, _)) => { - let response = tree - .can_rename(&pos) - .map(|(r, _)| PrepareRenameResponse::Range(r)); + let response = tree.can_rename(&pos).map(PrepareRenameResponse::Range); Box::pin(async move { Ok(response) }) } @@ -136,8 +133,8 @@ impl LanguageServer for ServerState { match self.get_parsed_tree_and_content(&uri) { Err(e) => Box::pin(async move { Err(e) }), Ok((tree, content)) => { - let response = if let Some((_, kind)) = tree.can_rename(&pos) { - tree.rename_kind(&uri, &pos, kind, &new_name, content) + let response = if tree.can_rename(&pos).is_some() { + tree.rename(&uri, &pos, &new_name, content) } else { None }; diff --git a/src/parser.rs b/src/parser.rs index ffff6d1..4af2925 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -17,8 +17,16 @@ pub struct ParsedTree { tree: Tree, } +// Adding any new kind to USER_DEFINED_KINDS must be accompanied with a change in DocumentSymbol +// handler match statement. const USER_DEFINED_KINDS: &[&str] = &["message_name", "enum_name"]; -const ACTIONABLE_KINDS: &[&str] = &["message_name", "enum_name", "rpc_name", "service_name"]; +const ACTIONABLE_KINDS: &[&str] = &[ + "message_name", + "enum_name", + "message_or_enum_type", + "rpc_name", + "service_name", +]; #[derive(Default)] struct DocumentSymbolTreeBuilder { @@ -271,40 +279,34 @@ impl ParsedTree { } } - pub fn can_rename(&self, pos: &Position) -> Option<(Range, &str)> { + pub fn can_rename(&self, pos: &Position) -> Option { self.get_node_at_position(pos) .filter(|n| n.kind() == "identifier") .map(|n| n.parent().unwrap()) // Safety: Identifier must have a parent node .filter(|n| ACTIONABLE_KINDS.contains(&n.kind())) - .map(|n| { - ( - Range { - start: ts_to_lsp_position(&n.start_position()), - end: ts_to_lsp_position(&n.end_position()), - }, - n.kind(), - ) + .map(|n| Range { + start: ts_to_lsp_position(&n.start_position()), + end: ts_to_lsp_position(&n.end_position()), }) } - pub fn rename_kind( + pub fn rename( &self, uri: &Url, pos: &Position, - kind: &str, new_text: &str, content: impl AsRef<[u8]>, ) -> Option { let old_text = self - .get_node_text_at_position(pos, &content.as_ref()) + .get_node_text_at_position(pos, content.as_ref()) .unwrap_or_default(); let mut changes = HashMap::new(); let mut cursor = self.tree.root_node().walk(); - let diff = Self::walk_and_collect_kinds(&mut cursor, &[kind]) + let diff: Vec<_> = Self::walk_and_collect_kinds(&mut cursor, &["identifier"]) .into_iter() - .filter(|n| n.utf8_text(&content.as_ref()).unwrap() == old_text) + .filter(|n| n.utf8_text(content.as_ref()).unwrap() == old_text) .map(|n| TextEdit { new_text: new_text.to_string(), range: Range { @@ -314,6 +316,10 @@ impl ParsedTree { }) .collect(); + if diff.is_empty() { + return None; + } + changes.insert(uri.clone(), diff); Some(WorkspaceEdit { @@ -347,8 +353,10 @@ impl ParsedTree { #[cfg(test)] mod test { + use async_lsp::lsp_types::{ - DiagnosticSeverity, DocumentSymbol, MarkedString, Position, Range, SymbolKind, Url, + DiagnosticSeverity, DocumentSymbol, MarkedString, Position, Range, SymbolKind, TextEdit, + Url, }; use super::ProtoParser; @@ -446,8 +454,174 @@ message Book { } #[test] - fn test_rename_kind() { - todo!("implement me") + fn test_rename() { + let uri = "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 = r#"syntax = "proto3"; + +package com.book; + +// A Book is book +message Book { + + // This is represents author + // A author is a someone who writes books + // + // Author has a name and a country where they were born + message Author { + string name = 1; + string country = 2; + }; + Author author = 1; + int price_usd = 2; +} + +message Library { + repeated Book books = 1; + Book.Author collection = 2; +} + +service Myservice { + rpc GetBook(Empty) returns (Book); +} +"#; + + let parsed = ProtoParser::new().parse(contents); + assert!(parsed.is_some()); + let tree = parsed.unwrap(); + + let res = tree.rename(&uri, &pos_book_rename, "Kitab", contents); + assert!(res.is_some()); + let changes = res.unwrap().changes; + assert!(changes.is_some()); + let changes = changes.unwrap(); + assert!(changes.contains_key(&uri)); + let edits = changes.get(&uri).unwrap(); + + assert_eq!( + *edits, + vec![ + TextEdit { + range: Range { + start: Position { + line: 5, + character: 8, + }, + end: Position { + line: 5, + character: 12, + }, + }, + new_text: "Kitab".to_string(), + }, + TextEdit { + range: Range { + start: Position { + line: 20, + character: 13, + }, + end: Position { + line: 20, + character: 17, + }, + }, + new_text: "Kitab".to_string(), + }, + TextEdit { + range: Range { + start: Position { + line: 21, + character: 4, + }, + end: Position { + line: 21, + character: 8, + }, + }, + new_text: "Kitab".to_string(), + }, + TextEdit { + range: Range { + start: Position { + line: 25, + character: 32, + }, + end: Position { + line: 25, + character: 36, + }, + }, + new_text: "Kitab".to_string(), + }, + ], + ); + + let res = tree.rename(&uri, &pos_author_rename, "Writer", contents); + assert!(res.is_some()); + let changes = res.unwrap().changes; + assert!(changes.is_some()); + let changes = changes.unwrap(); + assert!(changes.contains_key(&uri)); + let edits = changes.get(&uri).unwrap(); + + assert_eq!( + *edits, + vec![ + TextEdit { + range: Range { + start: Position { + line: 11, + character: 12, + }, + end: Position { + line: 11, + character: 18, + }, + }, + new_text: "Writer".to_string(), + }, + TextEdit { + range: Range { + start: Position { + line: 15, + character: 4, + }, + end: Position { + line: 15, + character: 10, + }, + }, + new_text: "Writer".to_string(), + }, + TextEdit { + range: Range { + start: Position { + line: 21, + character: 9, + }, + end: Position { + line: 21, + character: 15, + }, + }, + new_text: "Writer".to_string(), + }, + ], + ); + + let res = tree.rename(&uri, &pos_non_renamble, "Doesn't matter", contents); + assert!(res.is_none()); } #[test] @@ -485,19 +659,16 @@ message Book { assert!(res.is_some()); assert_eq!( res.unwrap(), - ( - Range { - start: Position { - line: 5, - character: 8 - }, - end: Position { - line: 5, - character: 12 - }, + Range { + start: Position { + line: 5, + character: 8 }, - "message_name" - ) + end: Position { + line: 5, + character: 12 + }, + }, ); let res = tree.can_rename(&pos_non_rename);