From ffa1cb39c94f7fbbd8e80e69c3e85b46843c1a99 Mon Sep 17 00:00:00 2001 From: coder3101 Date: Sun, 16 Jun 2024 14:24:27 +0530 Subject: [PATCH 1/4] add support for enum/message definition within file --- .gitignore | 1 + Cargo.lock | 139 +++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 3 + src/lsp.rs | 105 +++++++++++++++++++++++------------ src/main.rs | 7 ++- src/parser.rs | 97 +++++++++++++++++++++++++++++++++ src/server.rs | 17 ++++-- src/simple.proto | 5 ++ src/utils.rs | 16 ++++++ 9 files changed, 348 insertions(+), 42 deletions(-) create mode 100644 src/parser.rs create mode 100644 src/utils.rs diff --git a/.gitignore b/.gitignore index ea8c4bf..791af9f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +/logs diff --git a/Cargo.lock b/Cargo.lock index 1438f29..7f3b0d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "async-lsp" version = "0.2.0" @@ -88,6 +97,30 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + [[package]] name = "errno" version = "0.3.9" @@ -307,6 +340,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num_cpus" version = "1.16.0" @@ -379,6 +418,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" version = "1.0.85" @@ -398,7 +443,10 @@ dependencies = [ "tokio-util", "tower", "tracing", + "tracing-appender", "tracing-subscriber", + "tree-sitter", + "tree-sitter-proto", ] [[package]] @@ -419,6 +467,35 @@ dependencies = [ "bitflags 2.5.0", ] +[[package]] +name = "regex" +version = "1.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -576,6 +653,37 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -670,6 +778,18 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.27" @@ -716,6 +836,25 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "tree-sitter" +version = "0.19.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad726ec26496bf4c083fff0f43d4eb3a2ad1bba305323af5ff91383c0b6ecac0" +dependencies = [ + "cc", + "regex", +] + +[[package]] +name = "tree-sitter-proto" +version = "0.0.1" +source = "git+https://github.com/mitchellh/tree-sitter-proto?rev=42d82fa18f8afe59b5fc0b16c207ee4f84cb185f#42d82fa18f8afe59b5fc0b16c207ee4f84cb185f" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "unicode-bidi" version = "0.3.15" diff --git a/Cargo.toml b/Cargo.toml index d07aa26..1a7c80d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,6 @@ tokio-util = { version = "0.7.11", features = ["compat"] } tower = "0.4.13" tracing = "0.1.40" tracing-subscriber = "0.3.18" +tree-sitter-proto = { git = "https://github.com/mitchellh/tree-sitter-proto", rev = "42d82fa18f8afe59b5fc0b16c207ee4f84cb185f" } +tree-sitter = "0.19.3" +tracing-appender = "0.2.3" diff --git a/src/lsp.rs b/src/lsp.rs index 1ca126c..b47b9ac 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -1,15 +1,14 @@ -use async_lsp::LanguageClient; use std::ops::ControlFlow; use std::time::Duration; use tracing::{debug, info}; use async_lsp::lsp_types::{ - DidChangeConfigurationParams, DidChangeTextDocumentParams, DidOpenTextDocumentParams, - DidSaveTextDocumentParams, GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, - HoverParams, HoverProviderCapability, InitializeParams, InitializeResult, MarkedString, - MessageType, OneOf, ServerCapabilities, ServerInfo, ShowMessageParams, + DidChangeTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, + GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams, + InitializeParams, InitializeResult, MarkedString, OneOf, ServerCapabilities, ServerInfo, + TextDocumentSyncCapability, TextDocumentSyncKind, }; -use async_lsp::{LanguageServer, ResponseError}; +use async_lsp::{ErrorCode, LanguageServer, ResponseError}; use futures::future::BoxFuture; use crate::server::ServerState; @@ -33,31 +32,28 @@ impl LanguageServer for ServerState { info!("Connected with client {cname} {cversion}"); debug!("Initialize with {params:?}"); - Box::pin(async move { - Ok(InitializeResult { - capabilities: ServerCapabilities { - hover_provider: Some(HoverProviderCapability::Simple(true)), - ..ServerCapabilities::default() - }, - server_info: Some(ServerInfo { - name: env!("CARGO_PKG_NAME").to_string(), - version: Some(env!("CARGO_PKG_VERSION").to_string()), - }), - }) - }) + let response = InitializeResult { + capabilities: ServerCapabilities { + // todo(): We might prefer incremental sync at some later stage + text_document_sync: Some(TextDocumentSyncCapability::Kind( + TextDocumentSyncKind::FULL, + )), + definition_provider: Some(OneOf::Left(true)), + ..ServerCapabilities::default() + }, + server_info: Some(ServerInfo { + name: env!("CARGO_PKG_NAME").to_string(), + version: Some(env!("CARGO_PKG_VERSION").to_string()), + }), + }; + + Box::pin(async move { Ok(response) }) } fn hover(&mut self, _: HoverParams) -> BoxFuture<'static, Result, Self::Error>> { - let mut client = self.client.clone(); let counter = self.counter; Box::pin(async move { tokio::time::sleep(Duration::from_secs(1)).await; - client - .show_message(ShowMessageParams { - typ: MessageType::INFO, - message: "Hello LSP".into(), - }) - .unwrap(); Ok(Some(Hover { contents: HoverContents::Scalar(MarkedString::String(format!( "I am a hover text {counter}!" @@ -67,22 +63,59 @@ impl LanguageServer for ServerState { }) } - // fn definition( - // &mut self, - // _: GotoDefinitionParams, - // ) -> BoxFuture<'static, Result, ResponseError>> { - // unimplemented!("Not yet implemented!"); - // } + fn definition( + &mut self, + param: GotoDefinitionParams, + ) -> BoxFuture<'static, Result, ResponseError>> { + let uri = param.text_document_position_params.text_document.uri; + let pos = param.text_document_position_params.position; + + let Some(contents) = self.documents.get(&uri) else { + return Box::pin(async move { + Err(ResponseError::new( + ErrorCode::INVALID_REQUEST, + "uri was never opened", + )) + }); + }; + + let Some(parsed) = self.parser.parse(contents.as_bytes()) else { + return Box::pin(async move { + Err(ResponseError::new( + ErrorCode::REQUEST_FAILED, + "ts failed to parse contents", + )) + }); + }; + + let locations = parsed.definition_for(&pos, &uri, contents.as_bytes()); + info!("Found {} matching nodes in the document", locations.len()); + + let response = match locations.len() { + 0 => None, + 1 => Some(GotoDefinitionResponse::Scalar(locations[0].clone())), + 2.. => Some(GotoDefinitionResponse::Array(locations)), + }; + + Box::pin(async move { Ok(response) }) + } fn did_save(&mut self, _: DidSaveTextDocumentParams) -> Self::NotifyResult { - todo!("to implement") + ControlFlow::Continue(()) } - fn did_open(&mut self, _: DidOpenTextDocumentParams) -> Self::NotifyResult { - todo!("to implement") + fn did_open(&mut self, params: DidOpenTextDocumentParams) -> Self::NotifyResult { + let uri = params.text_document.uri; + let contents = params.text_document.text; + info!("Opened file at: {:}", uri); + self.documents.insert(uri, contents); + ControlFlow::Continue(()) } - fn did_change(&mut self, _: DidChangeTextDocumentParams) -> Self::NotifyResult { - todo!("to implement") + fn did_change(&mut self, params: DidChangeTextDocumentParams) -> Self::NotifyResult { + let uri = params.text_document.uri; + let contents = params.content_changes[0].text.clone(); + self.documents.insert(uri, contents); + ControlFlow::Continue(()) } } diff --git a/src/main.rs b/src/main.rs index edd901b..fe12496 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,8 @@ use tracing::Level; mod lsp; mod server; +mod utils; +mod parser; #[tokio::main(flavor = "current_thread")] async fn main() { @@ -37,10 +39,13 @@ async fn main() { .service(ServerState::new_router(client)) }); + let file_appender = tracing_appender::rolling::daily("/Users/ashar/Developer/protols/logs", "lsp.log"); + let (non_blocking, _gaurd) = tracing_appender::non_blocking(file_appender); + tracing_subscriber::fmt() .with_max_level(Level::INFO) .with_ansi(false) - .with_writer(std::io::stderr) + .with_writer(non_blocking) .init(); // Prefer truly asynchronous piped stdin/stdout without blocking tasks. diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..725e1b2 --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,97 @@ +use async_lsp::lsp_types::{Location, Position, Range, Url}; +use tracing::info; +use tree_sitter::{Node, Tree, TreeCursor}; + +use crate::utils::{lsp_to_ts_point, ts_to_lsp_position}; + +pub struct ProtoParser { + parser: tree_sitter::Parser, +} + +pub struct ParsedTree { + tree: Tree, +} + +impl ProtoParser { + pub fn new() -> Self { + let mut parser = tree_sitter::Parser::new(); + if let Err(e) = parser.set_language(tree_sitter_proto::language()) { + panic!("failed to set ts language parser {:?}", e); + } + Self { parser } + } + + pub fn parse(&mut self, contents: impl AsRef<[u8]>) -> Option { + self.parser + .parse(contents, None) + .map(|t| ParsedTree { tree: t }) + } +} + +impl ParsedTree { + pub fn get_node_text_at_position<'a>( + &'a self, + 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) + .map(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error")) + } + + fn walk_and_collect_kinds<'a>(&self, cursor: &mut TreeCursor<'a>, kinds: &[&str]) -> Vec> { + let mut v = vec![]; + + loop { + let node = cursor.node(); + + if kinds.contains(&node.kind()) { + v.push(node) + } + + if cursor.goto_first_child() { + v.extend(self.walk_and_collect_kinds(cursor, kinds)); + cursor.goto_parent(); + } + + if !cursor.goto_next_sibling() { + break; + } + } + + v + } + + 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) + } + + pub fn definition_for( + &self, + pos: &Position, + uri: &Url, + content: impl AsRef<[u8]>, + ) -> Vec { + let text = self.get_node_text_at_position(pos, content.as_ref()); + info!("Looking for definition of: {:?}", text); + + match text { + Some(text) => self + .find_childrens_by_kinds(&["message_name", "enum_name"]) + .into_iter() + .filter(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error") == text) + .map(|n| Location { + uri: uri.clone(), + range: Range { + start: ts_to_lsp_position(&n.start_position()), + end: ts_to_lsp_position(&n.end_position()), + }, + }) + .collect(), + None => vec![], + } + } +} diff --git a/src/server.rs b/src/server.rs index 28b76a7..252fd6e 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,22 +1,29 @@ -use async_lsp::{router::Router, ClientSocket}; -use std::ops::ControlFlow; -use tracing::info; +use async_lsp::{lsp_types::Url, router::Router, ClientSocket}; +use std::{collections::HashMap, ops::ControlFlow}; + +use crate::parser::ProtoParser; pub struct TickEvent; pub struct ServerState { pub client: ClientSocket, pub counter: i32, + pub documents: HashMap, + pub parser: ProtoParser, } impl ServerState { pub fn new_router(client: ClientSocket) -> Router { - let mut router = Router::from_language_server(Self { client, counter: 0 }); + let mut router = Router::from_language_server(Self { + client, + counter: 0, + documents: Default::default(), + parser: ProtoParser::new(), + }); router.event(Self::on_tick); router } fn on_tick(&mut self, _: TickEvent) -> ControlFlow> { - info!("tick"); self.counter += 1; ControlFlow::Continue(()) } diff --git a/src/simple.proto b/src/simple.proto index fc647b0..cdb56c9 100644 --- a/src/simple.proto +++ b/src/simple.proto @@ -12,10 +12,15 @@ message GetBookRequest { int64 isbn = 1; } +message GotoBookRequest { + bool flag = 1; +} + message GetBookViaAuthor { string author = 1; } +// It is a BookService Implementation service BookService { rpc GetBook (GetBookRequest) returns (Book) {} rpc GetBooksViaAuthor (GetBookViaAuthor) returns (stream Book) {} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..248ed5e --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,16 @@ +use async_lsp::lsp_types::Position; +use tree_sitter::{Node, Point, TreeCursor}; + +pub fn ts_to_lsp_position(p: &Point) -> Position { + Position { + line: p.row as u32, + character: p.column as u32, + } +} + +pub fn lsp_to_ts_point(p: &Position) -> Point { + Point { + row: p.line as usize, + column: p.character as usize, + } +} From 15783de433c4e2b8be26c176dca96eeb4f684466 Mon Sep 17 00:00:00 2001 From: coder3101 Date: Sun, 16 Jun 2024 20:26:21 +0530 Subject: [PATCH 2/4] add support for hover based on comments --- Cargo.lock | 6 ++-- Cargo.toml | 4 +-- src/lsp.rs | 56 ++++++++++++++++++++++++-------- src/parser.rs | 84 +++++++++++++++++++++++++++++++++++++++++++++--- src/simple.proto | 12 +++++++ 5 files changed, 140 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7f3b0d7..79ee3f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -838,9 +838,9 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.19.5" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad726ec26496bf4c083fff0f43d4eb3a2ad1bba305323af5ff91383c0b6ecac0" +checksum = "df7cc499ceadd4dcdf7ec6d4cbc34ece92c3fa07821e287aedecd4416c516dca" dependencies = [ "cc", "regex", @@ -849,7 +849,7 @@ dependencies = [ [[package]] name = "tree-sitter-proto" version = "0.0.1" -source = "git+https://github.com/mitchellh/tree-sitter-proto?rev=42d82fa18f8afe59b5fc0b16c207ee4f84cb185f#42d82fa18f8afe59b5fc0b16c207ee4f84cb185f" +source = "git+https://github.com/coder3101/tree-sitter-proto?branch=main#9f702d2b9544662bf2f9ff0035fc71c0172c6b64" dependencies = [ "cc", "tree-sitter", diff --git a/Cargo.toml b/Cargo.toml index 1a7c80d..5235f79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,6 @@ tokio-util = { version = "0.7.11", features = ["compat"] } tower = "0.4.13" tracing = "0.1.40" tracing-subscriber = "0.3.18" -tree-sitter-proto = { git = "https://github.com/mitchellh/tree-sitter-proto", rev = "42d82fa18f8afe59b5fc0b16c207ee4f84cb185f" } -tree-sitter = "0.19.3" +tree-sitter-proto = { git = "https://github.com/coder3101/tree-sitter-proto", branch = "main" } +tree-sitter = "0.22.6" tracing-appender = "0.2.3" diff --git a/src/lsp.rs b/src/lsp.rs index b47b9ac..1a1f1e2 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -5,8 +5,8 @@ use tracing::{debug, info}; use async_lsp::lsp_types::{ DidChangeTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams, - InitializeParams, InitializeResult, MarkedString, OneOf, ServerCapabilities, ServerInfo, - TextDocumentSyncCapability, TextDocumentSyncKind, + HoverProviderCapability, InitializeParams, InitializeResult, MarkedString, OneOf, + ServerCapabilities, ServerInfo, TextDocumentSyncCapability, TextDocumentSyncKind, }; use async_lsp::{ErrorCode, LanguageServer, ResponseError}; use futures::future::BoxFuture; @@ -39,6 +39,7 @@ impl LanguageServer for ServerState { TextDocumentSyncKind::FULL, )), definition_provider: Some(OneOf::Left(true)), + hover_provider: Some(HoverProviderCapability::Simple(true)), ..ServerCapabilities::default() }, server_info: Some(ServerInfo { @@ -50,17 +51,46 @@ impl LanguageServer for ServerState { Box::pin(async move { Ok(response) }) } - fn hover(&mut self, _: HoverParams) -> BoxFuture<'static, Result, Self::Error>> { - let counter = self.counter; - Box::pin(async move { - tokio::time::sleep(Duration::from_secs(1)).await; - Ok(Some(Hover { - contents: HoverContents::Scalar(MarkedString::String(format!( - "I am a hover text {counter}!" - ))), + fn hover( + &mut self, + param: HoverParams, + ) -> BoxFuture<'static, Result, Self::Error>> { + let uri = param.text_document_position_params.text_document.uri; + let pos = param.text_document_position_params.position; + + let Some(contents) = self.documents.get(&uri) else { + return Box::pin(async move { + Err(ResponseError::new( + ErrorCode::INVALID_REQUEST, + "uri was never opened", + )) + }); + }; + + let Some(parsed) = self.parser.parse(contents.as_bytes()) else { + return Box::pin(async move { + Err(ResponseError::new( + ErrorCode::REQUEST_FAILED, + "ts failed to parse contents", + )) + }); + }; + + let comments = parsed.hover(&pos, contents.as_bytes()); + info!("Found {} node comments in the document", comments.len()); + let response = match comments.len() { + 0 => None, + 1 => Some(Hover { + contents: HoverContents::Scalar(comments[0].clone()), + range: None, + }), + 2.. => Some(Hover { + contents: HoverContents::Array(comments), range: None, - })) - }) + }), + }; + + Box::pin(async move { Ok(response) }) } fn definition( @@ -88,7 +118,7 @@ impl LanguageServer for ServerState { }); }; - let locations = parsed.definition_for(&pos, &uri, contents.as_bytes()); + let locations = parsed.definition(&pos, &uri, contents.as_bytes()); info!("Found {} matching nodes in the document", locations.len()); let response = match locations.len() { diff --git a/src/parser.rs b/src/parser.rs index 725e1b2..e201ca6 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,4 +1,4 @@ -use async_lsp::lsp_types::{Location, Position, Range, Url}; +use async_lsp::lsp_types::{Location, MarkedString, Position, Range, Url}; use tracing::info; use tree_sitter::{Node, Tree, TreeCursor}; @@ -15,7 +15,7 @@ pub struct ParsedTree { impl ProtoParser { pub fn new() -> Self { let mut parser = tree_sitter::Parser::new(); - if let Err(e) = parser.set_language(tree_sitter_proto::language()) { + if let Err(e) = parser.set_language(&tree_sitter_proto::language()) { panic!("failed to set ts language parser {:?}", e); } Self { parser } @@ -41,7 +41,11 @@ impl ParsedTree { .map(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error")) } - fn walk_and_collect_kinds<'a>(&self, cursor: &mut TreeCursor<'a>, kinds: &[&str]) -> Vec> { + fn walk_and_collect_kinds<'a>( + &self, + cursor: &mut TreeCursor<'a>, + kinds: &[&str], + ) -> Vec> { let mut v = vec![]; loop { @@ -64,12 +68,69 @@ impl ParsedTree { v } + fn advance_cursor_to<'a>(&self, cursor: &mut TreeCursor<'a>, nid: usize) -> bool { + loop { + let node = cursor.node(); + if node.id() == nid { + return true; + } + if cursor.goto_first_child() { + if self.advance_cursor_to(cursor, nid) { + return true; + } + cursor.goto_parent(); + } + if !cursor.goto_next_sibling() { + return false; + } + } + } + + fn find_preceeding_comments(&self, nid: usize, content: impl AsRef<[u8]>) -> Option { + let root = self.tree.root_node(); + let mut cursor = root.walk(); + + info!("Looking for node with id: {nid}"); + + self.advance_cursor_to(&mut cursor, nid); + if !cursor.goto_parent() { + return None; + } + + if !cursor.goto_previous_sibling() { + return None; + } + + let mut comments = vec![]; + while cursor.node().kind() == "comment" { + let node = cursor.node(); + let text = node + .utf8_text(content.as_ref()) + .expect("utf-8 parser error") + .trim() + .trim_start_matches("//") + .trim(); + + comments.push(text); + + if !cursor.goto_previous_sibling() { + break; + } + } + return if comments.len() != 0 { + comments.reverse(); + Some(comments.join("\n")) + } else { + None + }; + } + 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) } - pub fn definition_for( + pub fn definition( &self, pos: &Position, uri: &Url, @@ -94,4 +155,19 @@ impl ParsedTree { None => vec![], } } + + pub fn hover(&self, pos: &Position, content: impl AsRef<[u8]>) -> Vec { + let text = self.get_node_text_at_position(pos, content.as_ref()); + info!("Looking for hover response on: {:?}", text); + match text { + Some(text) => self + .find_childrens_by_kinds(&["message_name", "enum_name", "service_name", "rpc_name"]) + .into_iter() + .filter(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error") == text) + .filter_map(|n| self.find_preceeding_comments(n.id(), content.as_ref())) + .map(|s| MarkedString::String(s)) + .collect(), + None => vec![], + } + } } diff --git a/src/simple.proto b/src/simple.proto index cdb56c9..1275be3 100644 --- a/src/simple.proto +++ b/src/simple.proto @@ -3,12 +3,17 @@ syntax = "proto3"; package com.book; message Book { + // This is a multi line comment on the field name + // Of a message called Book int64 isbn = 1; string title = 2; string author = 3; } +// This is a comment on message message GetBookRequest { + + // This is a sigle line comment on the field of a message int64 isbn = 1; } @@ -22,6 +27,8 @@ message GetBookViaAuthor { // It is a BookService Implementation service BookService { + // This is GetBook RPC that takes a book request + // and returns a Book, simple and sweet rpc GetBook (GetBookRequest) returns (Book) {} rpc GetBooksViaAuthor (GetBookViaAuthor) returns (stream Book) {} rpc GetGreatestBook (stream GetBookRequest) returns (Book) {} @@ -30,9 +37,14 @@ service BookService { message BookStore { string name = 1; + EnumSample sample = 3; map books = 2; } +// These are enum options representing some operation in the proto +// these are meant to be ony called from one place, + +// Note: Please set only to started or running enum EnumSample { option allow_alias = true; UNKNOWN = 0; From 0b84d15e27fad771189242828cf869cb893b6ade Mon Sep 17 00:00:00 2001 From: coder3101 Date: Sun, 16 Jun 2024 22:25:55 +0530 Subject: [PATCH 3/4] publish diagnostics for parse error --- src/lsp.rs | 31 ++++++++++++++++++++++------ src/main.rs | 5 +++-- src/parser.rs | 53 ++++++++++++++++++++++++++++++++++++------------ src/simple.proto | 5 ++++- src/utils.rs | 2 +- 5 files changed, 73 insertions(+), 23 deletions(-) diff --git a/src/lsp.rs b/src/lsp.rs index 1a1f1e2..f52ed42 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -1,14 +1,13 @@ use std::ops::ControlFlow; -use std::time::Duration; use tracing::{debug, info}; use async_lsp::lsp_types::{ DidChangeTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams, - HoverProviderCapability, InitializeParams, InitializeResult, MarkedString, OneOf, - ServerCapabilities, ServerInfo, TextDocumentSyncCapability, TextDocumentSyncKind, + HoverProviderCapability, InitializeParams, InitializeResult, OneOf, ServerCapabilities, + ServerInfo, TextDocumentSyncCapability, TextDocumentSyncKind, }; -use async_lsp::{ErrorCode, LanguageServer, ResponseError}; +use async_lsp::{ErrorCode, LanguageClient, LanguageServer, ResponseError}; use futures::future::BoxFuture; use crate::server::ServerState; @@ -138,14 +137,34 @@ impl LanguageServer for ServerState { let uri = params.text_document.uri; let contents = params.text_document.text; info!("Opened file at: {:}", uri); - self.documents.insert(uri, contents); + self.documents.insert(uri.clone(), contents.clone()); + + let Some(parsed) = self.parser.parse(contents.as_bytes()) else { + tracing::error!("failed to parse content"); + return ControlFlow::Continue(()); + }; + + let diagnostics = parsed.collect_parse_errors(&uri); + if let Err(e) = self.client.publish_diagnostics(diagnostics) { + tracing::error!("failed to publish diagnostics. {:?}", e) + } ControlFlow::Continue(()) } fn did_change(&mut self, params: DidChangeTextDocumentParams) -> Self::NotifyResult { let uri = params.text_document.uri; let contents = params.content_changes[0].text.clone(); - self.documents.insert(uri, contents); + self.documents.insert(uri.clone(), contents.clone()); + + let Some(parsed) = self.parser.parse(contents.as_bytes()) else { + tracing::error!("failed to parse content"); + return ControlFlow::Continue(()); + }; + + let diagnostics = parsed.collect_parse_errors(&uri); + if let Err(e) = self.client.publish_diagnostics(diagnostics) { + tracing::error!("failed to publish diagnostics. {:?}", e) + } ControlFlow::Continue(()) } } diff --git a/src/main.rs b/src/main.rs index fe12496..0d88698 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,9 +10,9 @@ use tower::ServiceBuilder; use tracing::Level; mod lsp; +mod parser; mod server; mod utils; -mod parser; #[tokio::main(flavor = "current_thread")] async fn main() { @@ -39,7 +39,8 @@ async fn main() { .service(ServerState::new_router(client)) }); - let file_appender = tracing_appender::rolling::daily("/Users/ashar/Developer/protols/logs", "lsp.log"); + let file_appender = + tracing_appender::rolling::daily("/Users/ashar/Developer/protols/logs", "lsp.log"); let (non_blocking, _gaurd) = tracing_appender::non_blocking(file_appender); tracing_subscriber::fmt() diff --git a/src/parser.rs b/src/parser.rs index e201ca6..4c133ad 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,4 +1,7 @@ -use async_lsp::lsp_types::{Location, MarkedString, Position, Range, Url}; +use async_lsp::lsp_types::{ + Diagnostic, DiagnosticSeverity, Location, MarkedString, Position, PublishDiagnosticsParams, + Range, Url, +}; use tracing::info; use tree_sitter::{Node, Tree, TreeCursor}; @@ -29,18 +32,6 @@ impl ProtoParser { } impl ParsedTree { - pub fn get_node_text_at_position<'a>( - &'a self, - 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) - .map(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error")) - } - fn walk_and_collect_kinds<'a>( &self, cursor: &mut TreeCursor<'a>, @@ -124,6 +115,20 @@ impl ParsedTree { None }; } +} + +impl ParsedTree { + pub fn get_node_text_at_position<'a>( + &'a self, + 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) + .map(|n| n.utf8_text(content.as_ref()).expect("utf-8 parse error")) + } pub fn find_childrens_by_kinds(&self, kinds: &[&str]) -> Vec { let mut cursor = self.tree.root_node().walk(); @@ -170,4 +175,26 @@ impl ParsedTree { None => vec![], } } + + pub fn collect_parse_errors(&self, uri: &Url) -> PublishDiagnosticsParams { + let diagnostics = self + .find_childrens_by_kinds(&["ERROR"]) + .into_iter() + .map(|n| Diagnostic { + range: Range { + start: ts_to_lsp_position(&n.start_position()), + end: ts_to_lsp_position(&n.end_position()), + }, + severity: Some(DiagnosticSeverity::ERROR), + source: Some("protols".to_string()), + message: "Syntax error".to_string(), + ..Default::default() + }) + .collect(); + PublishDiagnosticsParams { + uri: uri.clone(), + diagnostics, + version: None, + } + } } diff --git a/src/simple.proto b/src/simple.proto index 1275be3..a896698 100644 --- a/src/simple.proto +++ b/src/simple.proto @@ -25,6 +25,7 @@ message GetBookViaAuthor { string author = 1; } + // It is a BookService Implementation service BookService { // This is GetBook RPC that takes a book request @@ -36,9 +37,11 @@ service BookService { } message BookStore { + reserved 1; + Book book = 0; string name = 1; - EnumSample sample = 3; map books = 2; + EnumSample sample = 3; } // These are enum options representing some operation in the proto diff --git a/src/utils.rs b/src/utils.rs index 248ed5e..4b26ccb 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,5 @@ use async_lsp::lsp_types::Position; -use tree_sitter::{Node, Point, TreeCursor}; +use tree_sitter::Point; pub fn ts_to_lsp_position(p: &Point) -> Position { Position { From 8776cf9f2bc8174dcb90a8d512bd2611c4f5d05b Mon Sep 17 00:00:00 2001 From: coder3101 Date: Sun, 16 Jun 2024 23:28:52 +0530 Subject: [PATCH 4/4] update readme and logs to tempdir --- README.md | 15 ++++++++++++--- src/main.rs | 9 +++++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2254ebd..328a1e4 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,20 @@ # protols -Language server for proto files +A Language Server for **proto3** files. It only uses tree-sitter parser for all operations and always runs in **single file mode**. + +## Features +- [x] Hover +- [x] Go to definition +- [x] Diagnostics + + +## Installation and testing + +Clone the repository and run `cargo install --path .` to install locally in your `~/.cargo/bin` and the below to your `init.lua` until we start shipping this via Mason. -## Testing with neovim ```lua local client = vim.lsp.start_client({ name = "protols", - cmd = { "" }, + cmd = { vim.fn.expand("$HOME/.cargo/bin/protols") }, }) if not client then diff --git a/src/main.rs b/src/main.rs index 0d88698..7f9b132 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ use async_lsp::server::LifecycleLayer; use async_lsp::tracing::TracingLayer; use server::{ServerState, TickEvent}; use tower::ServiceBuilder; -use tracing::Level; +use tracing::{info, Level}; mod lsp; mod parser; @@ -39,8 +39,13 @@ async fn main() { .service(ServerState::new_router(client)) }); + let mut dir = std::env::temp_dir(); + dir.push("protols.log"); + + eprintln!("Logs are being written to {:?}", dir); + let file_appender = - tracing_appender::rolling::daily("/Users/ashar/Developer/protols/logs", "lsp.log"); + tracing_appender::rolling::daily(std::env::temp_dir().as_path(), "protols.log"); let (non_blocking, _gaurd) = tracing_appender::non_blocking(file_appender); tracing_subscriber::fmt()