From 045391216b22ea27e8b2676e118b932a3d90fb38 Mon Sep 17 00:00:00 2001 From: coder3101 Date: Mon, 26 Aug 2024 00:49:19 +0530 Subject: [PATCH 1/6] wip: working formatting --- sample/simple.proto | 80 ++++++++++++++++++------------------- sample/test.proto | 11 +++++ src/formatter/clang.rs | 56 ++++++++++++++++++++++++++ src/formatter/mod.rs | 8 ++++ src/lsp.rs | 40 +++++++++++++++---- src/main.rs | 1 + src/server.rs | 4 +- src/state.rs | 17 +++++++- src/workspace/definition.rs | 10 +++-- src/workspace/hover.rs | 10 +++-- src/workspace/rename.rs | 8 ++-- 11 files changed, 179 insertions(+), 66 deletions(-) create mode 100644 sample/test.proto create mode 100644 src/formatter/clang.rs create mode 100644 src/formatter/mod.rs diff --git a/sample/simple.proto b/sample/simple.proto index 9cc8717..f4448da 100644 --- a/sample/simple.proto +++ b/sample/simple.proto @@ -2,61 +2,57 @@ syntax = "proto3"; package com.book; -// This is a book represeted by some comments that we like to address in the review +// 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 - int64 isbn = 1; - string title = 2; - Author author = 3; - google.protobuf.Any data = 4; - BookState state = 5; + // This is a multi line comment on the field name + // Of a message called Book + int64 isbn = 1; + string title = 2; + Author author = 3; + google.protobuf.Any data = 4; + BookState state = 5; - // Author is a author of a book - message Author { - string name = 1; - int64 age = 2; - } + // Author is a author of a book + message Author { + string name = 1; + int64 age = 2; + } - enum BookState { - HARD_COVER = 1; - SOFT_COVER = 2; - } + enum BookState { + HARD_COVER = 1; + SOFT_COVER = 2; + } } // This is a comment on message message GetBookRequest { - - // This is a sigle line comment on the field of a message - int64 isbn = 1; -} -message GotoBookRequest { - bool flag = 1; + // This is a sigle line comment on the field of a message + int64 isbn = 1; } -message GetBookViaAuthor { - Book.Author author = 1; -} +message GotoBookRequest { bool flag = 1; } +message GetBookViaAuthor { Book.Author author = 1; } // 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 GetBookAuthor (GetBookRequest) returns (Book.Author) {} - rpc GetBooksViaAuthor (GetBookViaAuthor) returns (stream Book) {} - rpc GetGreatestBook (stream GetBookRequest) returns (Book) {} - rpc GetBooks (stream GetBookRequest) returns (stream Book) {} + // This is GetBook RPC that takes a book request + // and returns a Book, simple and sweet + rpc GetBook(GetBookRequest) returns (Book) {} + rpc GetBookAuthor(GetBookRequest) returns (Book.Author) {} + rpc GetBooksViaAuthor(GetBookViaAuthor) returns (stream Book) {} + rpc GetGreatestBook(stream GetBookRequest) returns (Book) {} + rpc GetBooks(stream GetBookRequest) returns (stream Book) {} } message BookStore { - reserved 1; - Book book = 0; - string name = 1; - map books = 2; - EnumSample sample = 3; + reserved 1; + Book book = 0; + string name = 1; + map books = 2; + EnumSample sample = 3; } // These are enum options representing some operation in the proto @@ -64,8 +60,8 @@ message BookStore { // Note: Please set only to started or running enum EnumSample { - option allow_alias = true; - UNKNOWN = 0; - STARTED = 1; - RUNNING = 1; + option allow_alias = true; + UNKNOWN = 0; + STARTED = 1; + RUNNING = 1; } diff --git a/sample/test.proto b/sample/test.proto new file mode 100644 index 0000000..1a36687 --- /dev/null +++ b/sample/test.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package a.b.c; + +message CustomType { bool attribute = 1; } + +message SomeMessage { + int64 someAttribute = 1; + + CustomType another = 2; +} diff --git a/src/formatter/clang.rs b/src/formatter/clang.rs new file mode 100644 index 0000000..4f3a657 --- /dev/null +++ b/src/formatter/clang.rs @@ -0,0 +1,56 @@ +use std::{error::Error, process::Command}; + +use async_lsp::lsp_types::{Position, Range, TextEdit, Url}; +use tracing::info; + +use super::ProtoFormatter; + +pub struct ClangFormatter { + path: String, + working_dir: String, +} + +impl ClangFormatter { + pub fn new(path: &str, workdir: &str) -> Result> { + let mut c = Command::new(path); + c.arg("--version").status()?; + + Ok(Self { + path: path.to_owned(), + working_dir: workdir.to_owned(), + }) + } +} + +impl ProtoFormatter for ClangFormatter { + fn format_document(&self, u: &Url) -> Option> { + let mut c = Command::new(self.path.as_str()); + c.current_dir(self.working_dir.as_str()); + let output = c.arg(u.path()).output().ok()?; + if !output.status.success() { + return None; + } + let output = String::from_utf8_lossy(&output.stdout); + Some(vec![TextEdit { + range: Range { + start: Position { + line: 0, + character: 0, + }, + end: Position { + line: u32::MAX, + character: u32::MAX, + }, + }, + new_text: output.to_string(), + }]) + } + + fn format_document_range( + &self, + _u: &async_lsp::lsp_types::Url, + _r: &async_lsp::lsp_types::Range, + ) -> Option> { + todo!() + } +} diff --git a/src/formatter/mod.rs b/src/formatter/mod.rs new file mode 100644 index 0000000..b4936c6 --- /dev/null +++ b/src/formatter/mod.rs @@ -0,0 +1,8 @@ +use async_lsp::lsp_types::{Range, TextEdit, Url}; + +pub mod clang; + +pub trait ProtoFormatter: Sized { + fn format_document(&self, u: &Url) -> Option>; + fn format_document_range(&self, u: &Url, r: &Range) -> Option>; +} diff --git a/src/lsp.rs b/src/lsp.rs index e96d43d..04dd89a 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -8,18 +8,21 @@ use tracing::{error, info}; use async_lsp::lsp_types::{ CompletionItem, CompletionItemKind, CompletionOptions, CompletionParams, CompletionResponse, CreateFilesParams, DeleteFilesParams, DidChangeTextDocumentParams, DidCloseTextDocumentParams, - DidOpenTextDocumentParams, DidSaveTextDocumentParams, DocumentSymbolParams, - 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, + DidOpenTextDocumentParams, DidSaveTextDocumentParams, DocumentFormattingParams, + DocumentSymbolParams, DocumentSymbolResponse, FileOperationFilter, FileOperationPattern, + FileOperationPatternKind, FileOperationRegistrationOptions, + GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams, + HoverProviderCapability, InitializeParams, InitializeResult, OneOf, 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; +use crate::formatter::clang::ClangFormatter; +use crate::formatter::ProtoFormatter; use crate::server::ProtoLanguageServer; impl LanguageServer for ProtoLanguageServer { @@ -73,7 +76,15 @@ impl LanguageServer for ProtoLanguageServer { }; let mut workspace_capabilities = None; + let mut formatter_provider = None; if let Some(folders) = params.workspace_folders { + if let Ok(formatter) = + ClangFormatter::new("clang-format", folders.first().unwrap().uri.path()) + { + self.state.add_formatter(formatter); + formatter_provider = Some(OneOf::Left(true)); + info!("Setting formatting client capability"); + } for workspace in folders { info!("Workspace folder: {workspace:?}"); self.state.add_workspace_folder_async(workspace, tx.clone()) @@ -120,6 +131,7 @@ impl LanguageServer for ProtoLanguageServer { document_symbol_provider: Some(OneOf::Left(true)), completion_provider: Some(CompletionOptions::default()), rename_provider: Some(rename_provider), + document_formatting_provider: formatter_provider, ..ServerCapabilities::default() }, @@ -318,6 +330,18 @@ impl LanguageServer for ProtoLanguageServer { Box::pin(async move { Ok(Some(response)) }) } + fn formatting( + &mut self, + params: DocumentFormattingParams, + ) -> BoxFuture<'static, Result>, Self::Error>> { + let response = self + .state + .get_formatter() + .and_then(|f| f.format_document(¶ms.text_document.uri)); + + Box::pin(async move { Ok(response) }) + } + fn did_save(&mut self, _: DidSaveTextDocumentParams) -> Self::NotifyResult { ControlFlow::Continue(()) } diff --git a/src/main.rs b/src/main.rs index ed10ffb..b77c643 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,6 +16,7 @@ mod server; mod state; mod utils; mod workspace; +mod formatter; #[tokio::main(flavor = "current_thread")] async fn main() { diff --git a/src/server.rs b/src/server.rs index 86a3f13..37d022e 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,13 +1,13 @@ use async_lsp::{router::Router, ClientSocket}; use std::ops::ControlFlow; -use crate::state::ProtoLanguageState; +use crate::{formatter::clang::ClangFormatter, state::ProtoLanguageState}; pub struct TickEvent; pub struct ProtoLanguageServer { pub client: ClientSocket, pub counter: i32, - pub state: ProtoLanguageState, + pub state: ProtoLanguageState, } impl ProtoLanguageServer { diff --git a/src/state.rs b/src/state.rs index fc20510..a2910cf 100644 --- a/src/state.rs +++ b/src/state.rs @@ -15,22 +15,27 @@ use tree_sitter::Node; use walkdir::WalkDir; use crate::{ + formatter::ProtoFormatter, nodekind::NodeKind, parser::{ParsedTree, ProtoParser}, }; -pub struct ProtoLanguageState { +pub struct ProtoLanguageState { + worspace: Option, documents: Arc>>, trees: Arc>>, + formatter: Option, parser: Arc>, } -impl ProtoLanguageState { +impl ProtoLanguageState { pub fn new() -> Self { ProtoLanguageState { documents: Default::default(), trees: Default::default(), + worspace: Default::default(), parser: Arc::new(Mutex::new(ProtoParser::new())), + formatter: Default::default(), } } @@ -94,6 +99,14 @@ impl ProtoLanguageState { Self::upsert_content_impl(parser, uri, content, docs, tree) } + pub fn add_formatter(&mut self, formatter: F) { + self.formatter.replace(formatter); + } + + pub fn get_formatter(&self) -> Option<&F> { + self.formatter.as_ref() + } + pub fn add_workspace_folder_async( &mut self, workspace: WorkspaceFolder, diff --git a/src/workspace/definition.rs b/src/workspace/definition.rs index f0be3f3..7d425a0 100644 --- a/src/workspace/definition.rs +++ b/src/workspace/definition.rs @@ -1,8 +1,10 @@ use async_lsp::lsp_types::Location; -use crate::{state::ProtoLanguageState, utils::split_identifier_package}; +use crate::{ + formatter::ProtoFormatter, state::ProtoLanguageState, utils::split_identifier_package, +}; -impl ProtoLanguageState { +impl ProtoLanguageState { pub fn definition(&self, curr_package: &str, identifier: &str) -> Vec { let (mut package, identifier) = split_identifier_package(identifier); if package.is_empty() { @@ -21,7 +23,7 @@ impl ProtoLanguageState { mod test { use insta::assert_yaml_snapshot; - use crate::state::ProtoLanguageState; + use crate::{formatter::clang::ClangFormatter, state::ProtoLanguageState}; #[test] fn workspace_test_definition() { @@ -33,7 +35,7 @@ mod test { let b = include_str!("input/b.proto"); let c = include_str!("input/c.proto"); - let mut state = ProtoLanguageState::new(); + let mut state: ProtoLanguageState = 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()); diff --git a/src/workspace/hover.rs b/src/workspace/hover.rs index a71f68b..70bb3e6 100644 --- a/src/workspace/hover.rs +++ b/src/workspace/hover.rs @@ -1,8 +1,10 @@ use async_lsp::lsp_types::MarkedString; -use crate::{state::ProtoLanguageState, utils::split_identifier_package}; +use crate::{ + formatter::ProtoFormatter, state::ProtoLanguageState, utils::split_identifier_package, +}; -impl ProtoLanguageState { +impl ProtoLanguageState { pub fn hover(&self, curr_package: &str, identifier: &str) -> Vec { let (mut package, identifier) = split_identifier_package(identifier); if package.is_empty() { @@ -22,7 +24,7 @@ impl ProtoLanguageState { mod test { use insta::assert_yaml_snapshot; - use crate::state::ProtoLanguageState; + use crate::{formatter::clang::ClangFormatter, state::ProtoLanguageState}; #[test] fn workspace_test_hover() { @@ -34,7 +36,7 @@ mod test { let b = include_str!("input/b.proto"); let c = include_str!("input/c.proto"); - let mut state = ProtoLanguageState::new(); + let mut state: ProtoLanguageState = 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()); diff --git a/src/workspace/rename.rs b/src/workspace/rename.rs index ab49f59..ea450d6 100644 --- a/src/workspace/rename.rs +++ b/src/workspace/rename.rs @@ -1,11 +1,11 @@ -use crate::utils::split_identifier_package; +use crate::{formatter::ProtoFormatter, utils::split_identifier_package}; use std::collections::HashMap; use async_lsp::lsp_types::{TextEdit, Url}; use crate::state::ProtoLanguageState; -impl ProtoLanguageState { +impl ProtoLanguageState { pub fn rename_fields( &self, current_package: &str, @@ -37,7 +37,7 @@ impl ProtoLanguageState { mod test { use insta::assert_yaml_snapshot; - use crate::state::ProtoLanguageState; + use crate::{formatter::clang::ClangFormatter, state::ProtoLanguageState}; #[test] fn test_rename() { @@ -49,7 +49,7 @@ mod test { let b = include_str!("input/b.proto"); let c = include_str!("input/c.proto"); - let mut state = ProtoLanguageState::new(); + let mut state: ProtoLanguageState = 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()); From e952c9b99c129c776d2ede361d573e4194b75a0c Mon Sep 17 00:00:00 2001 From: coder3101 Date: Mon, 26 Aug 2024 20:35:32 +0530 Subject: [PATCH 2/6] wip: add .clang-format and try replacements --- .clang-format | 3 ++ Cargo.lock | 28 +++++++++-- Cargo.toml | 2 + sample/simple.proto | 9 ++-- sample/test.proto | 4 +- src/formatter/clang.rs | 104 +++++++++++++++++++++++++++++++++-------- src/lsp.rs | 36 +++++++++----- src/state.rs | 2 - 8 files changed, 147 insertions(+), 41 deletions(-) create mode 100644 .clang-format diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..24d97f2 --- /dev/null +++ b/.clang-format @@ -0,0 +1,3 @@ +BasedOnStyle: Google +DerivePointerAlignment: false +PointerAlignment: Left diff --git a/Cargo.lock b/Cargo.lock index 297ca84..4657a2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -478,6 +478,8 @@ dependencies = [ "futures", "insta", "protols-tree-sitter-proto", + "serde", + "serde-xml-rs", "tokio", "tokio-util", "tower", @@ -587,18 +589,30 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.204" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" dependencies = [ "serde_derive", ] +[[package]] +name = "serde-xml-rs" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782" +dependencies = [ + "log", + "serde", + "thiserror", + "xml-rs", +] + [[package]] name = "serde_derive" -version = "1.0.204" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" dependencies = [ "proc-macro2", "quote", @@ -1144,3 +1158,9 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "xml-rs" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "539a77ee7c0de333dcc6da69b177380a0b81e0dacfa4f7344c465a36871ee601" diff --git a/Cargo.toml b/Cargo.toml index 2e4d717..0d28fe4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,8 @@ tree-sitter = "0.22.6" tracing-appender = "0.2.3" protols-tree-sitter-proto = "0.2.0" walkdir = "2.5.0" +serde-xml-rs = "0.6.0" +serde = { version = "1.0.209", features = ["derive"] } [dev-dependencies] insta = { version = "1.39.0", features = ["yaml"] } diff --git a/sample/simple.proto b/sample/simple.proto index f4448da..306e28d 100644 --- a/sample/simple.proto +++ b/sample/simple.proto @@ -27,14 +27,17 @@ message Book { // This is a comment on message message GetBookRequest { - // This is a sigle line comment on the field of a message int64 isbn = 1; } -message GotoBookRequest { bool flag = 1; } +message GotoBookRequest { + bool flag = 1; +} -message GetBookViaAuthor { Book.Author author = 1; } +message GetBookViaAuthor { + Book.Author author = 1; +} // It is a BookService Implementation service BookService { diff --git a/sample/test.proto b/sample/test.proto index 1a36687..04e994b 100644 --- a/sample/test.proto +++ b/sample/test.proto @@ -5,7 +5,7 @@ package a.b.c; message CustomType { bool attribute = 1; } message SomeMessage { - int64 someAttribute = 1; + int64 someAttribute = 1; - CustomType another = 2; + CustomType another = 2; } diff --git a/src/formatter/clang.rs b/src/formatter/clang.rs index 4f3a657..c5deb9a 100644 --- a/src/formatter/clang.rs +++ b/src/formatter/clang.rs @@ -1,7 +1,8 @@ +use serde::{Deserialize, Serialize}; +use serde_xml_rs::from_str; use std::{error::Error, process::Command}; use async_lsp::lsp_types::{Position, Range, TextEdit, Url}; -use tracing::info; use super::ProtoFormatter; @@ -10,6 +11,18 @@ pub struct ClangFormatter { working_dir: String, } +#[derive(Serialize, Deserialize, Debug, PartialEq)] +struct Replacements { + replacements: Vec, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +struct Replacement { + offset: u32, + length: u32, + value: String, +} + impl ClangFormatter { pub fn new(path: &str, workdir: &str) -> Result> { let mut c = Command::new(path); @@ -20,37 +33,90 @@ impl ClangFormatter { working_dir: workdir.to_owned(), }) } + + fn get_command(&self, u: &Url) -> Command { + let mut c = Command::new(self.path.as_str()); + c.current_dir(self.working_dir.as_str()); + c.args([u.path(), "--output-replacements-xml"]); + c + } } impl ProtoFormatter for ClangFormatter { fn format_document(&self, u: &Url) -> Option> { - let mut c = Command::new(self.path.as_str()); - c.current_dir(self.working_dir.as_str()); - let output = c.arg(u.path()).output().ok()?; + let output = self.get_command(u).output().ok()?; if !output.status.success() { return None; } let output = String::from_utf8_lossy(&output.stdout); - Some(vec![TextEdit { - range: Range { - start: Position { - line: 0, - character: 0, - }, - end: Position { - line: u32::MAX, - character: u32::MAX, + + let out: Replacements = from_str(&output).ok()?; + let edits = out + .replacements + .into_iter() + .map(|r| TextEdit { + range: Range { + start: Position { + line: 0, + character: 0, + }, + end: Position { + line: 1, + character: 3, + }, }, - }, - new_text: output.to_string(), - }]) + new_text: r.value, + }) + .collect(); + + tracing::info!("{edits:?}"); + Some(edits) } fn format_document_range( &self, - _u: &async_lsp::lsp_types::Url, - _r: &async_lsp::lsp_types::Range, + u: &Url, + r: &Range, ) -> Option> { - todo!() + let start = r.start.line + 1; + let end = r.end.line + 1; + let output = self + .get_command(u) + .args(["--lines", format!("{start}:{end}").as_str()]) + .output() + .ok()?; + + if !output.status.success() { + return None; + } + + let output = String::from_utf8_lossy(&output.stdout); + + tracing::info!("{output}"); + if let Err(e) = from_str::(&output) { + tracing::error!("{e}"); + return None; + } + let out: Replacements = from_str(&output).ok()?; + let edits = out + .replacements + .into_iter() + .map(|r| TextEdit { + range: Range { + start: Position { + line: 9, + character: 1, + }, + end: Position { + line: 12, + character: 6, + }, + }, + new_text: r.value, + }) + .collect(); + + tracing::info!("{edits:?}"); + Some(edits) } } diff --git a/src/lsp.rs b/src/lsp.rs index 04dd89a..51d6b00 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -9,14 +9,14 @@ use async_lsp::lsp_types::{ CompletionItem, CompletionItemKind, CompletionOptions, CompletionParams, CompletionResponse, CreateFilesParams, DeleteFilesParams, DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, DocumentFormattingParams, - DocumentSymbolParams, DocumentSymbolResponse, FileOperationFilter, FileOperationPattern, - FileOperationPatternKind, FileOperationRegistrationOptions, - GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams, - HoverProviderCapability, InitializeParams, InitializeResult, OneOf, PrepareRenameResponse, - ProgressParams, RenameFilesParams, RenameOptions, RenameParams, ServerCapabilities, ServerInfo, - TextDocumentPositionParams, TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit, Url, - WorkspaceEdit, WorkspaceFileOperationsServerCapabilities, WorkspaceFoldersServerCapabilities, - WorkspaceServerCapabilities, + DocumentRangeFormattingParams, DocumentSymbolParams, DocumentSymbolResponse, + FileOperationFilter, FileOperationPattern, FileOperationPatternKind, + FileOperationRegistrationOptions, GotoDefinitionParams, GotoDefinitionResponse, Hover, + HoverContents, HoverParams, HoverProviderCapability, InitializeParams, InitializeResult, OneOf, + 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; @@ -77,12 +77,13 @@ impl LanguageServer for ProtoLanguageServer { let mut workspace_capabilities = None; let mut formatter_provider = None; + let mut formatter_range_provider = None; if let Some(folders) = params.workspace_folders { - if let Ok(formatter) = - ClangFormatter::new("clang-format", folders.first().unwrap().uri.path()) + if let Ok(f) = ClangFormatter::new("clang-format", folders.first().unwrap().uri.path()) { - self.state.add_formatter(formatter); + self.state.add_formatter(f); formatter_provider = Some(OneOf::Left(true)); + formatter_range_provider = Some(OneOf::Left(true)); info!("Setting formatting client capability"); } for workspace in folders { @@ -132,6 +133,7 @@ impl LanguageServer for ProtoLanguageServer { completion_provider: Some(CompletionOptions::default()), rename_provider: Some(rename_provider), document_formatting_provider: formatter_provider, + document_range_formatting_provider: formatter_range_provider, ..ServerCapabilities::default() }, @@ -342,6 +344,18 @@ impl LanguageServer for ProtoLanguageServer { Box::pin(async move { Ok(response) }) } + fn range_formatting( + &mut self, + params: DocumentRangeFormattingParams, + ) -> BoxFuture<'static, Result>, Self::Error>> { + let response = self + .state + .get_formatter() + .and_then(|f| f.format_document_range(¶ms.text_document.uri, ¶ms.range)); + + Box::pin(async move { Ok(response) }) + } + fn did_save(&mut self, _: DidSaveTextDocumentParams) -> Self::NotifyResult { ControlFlow::Continue(()) } diff --git a/src/state.rs b/src/state.rs index a2910cf..901d8c4 100644 --- a/src/state.rs +++ b/src/state.rs @@ -21,7 +21,6 @@ use crate::{ }; pub struct ProtoLanguageState { - worspace: Option, documents: Arc>>, trees: Arc>>, formatter: Option, @@ -33,7 +32,6 @@ impl ProtoLanguageState { ProtoLanguageState { documents: Default::default(), trees: Default::default(), - worspace: Default::default(), parser: Arc::new(Mutex::new(ProtoParser::new())), formatter: Default::default(), } From f44802271a0becab04362f66dbc0bd62ab1d265f Mon Sep 17 00:00:00 2001 From: coder3101 Date: Sun, 1 Sep 2024 00:36:50 +0530 Subject: [PATCH 3/6] wip: formatting --- Cargo.lock | 114 +++++++++++++++++++++++++------ Cargo.toml | 4 +- src/formatter/clang.rs | 149 +++++++++++++++++++++-------------------- src/formatter/mod.rs | 6 +- src/lsp.rs | 10 ++- 5 files changed, 183 insertions(+), 100 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4657a2a..2d934b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -149,6 +149,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -214,7 +220,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.71", ] [[package]] @@ -253,6 +259,31 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +[[package]] +name = "hard-xml" +version = "1.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a344e0cef8802f37dc47f17c01a04354d3e66d9f6c8744108b0912f616efe266" +dependencies = [ + "hard-xml-derive", + "jetscii", + "lazy_static", + "memchr", + "xmlparser", +] + +[[package]] +name = "hard-xml-derive" +version = "1.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfae7cdfe23e50ea96929ccf1948d9ae1d8608353556461e5de247463d3a4f6" +dependencies = [ + "bitflags 2.6.0", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "hermit-abi" version = "0.3.9" @@ -288,6 +319,12 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jetscii" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47f142fe24a9c9944451e8349de0a56af5f3e7226dc46f3ed4d4ecc0b85af75e" + [[package]] name = "lazy_static" version = "1.5.0" @@ -476,10 +513,12 @@ version = "0.5.0" dependencies = [ "async-lsp", "futures", + "hard-xml", "insta", "protols-tree-sitter-proto", + "quick-xml", "serde", - "serde-xml-rs", + "tempfile", "tokio", "tokio-util", "tower", @@ -500,6 +539,16 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "quick-xml" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a05e2e8efddfa51a84ca47cec303fac86c8541b686d37cac5efc0e094417bc" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quote" version = "1.0.36" @@ -596,18 +645,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-xml-rs" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782" -dependencies = [ - "log", - "serde", - "thiserror", - "xml-rs", -] - [[package]] name = "serde_derive" version = "1.0.209" @@ -616,7 +653,7 @@ checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.71", ] [[package]] @@ -638,7 +675,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.71", ] [[package]] @@ -690,6 +727,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.71" @@ -701,6 +749,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "thiserror" version = "1.0.63" @@ -718,7 +779,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.71", ] [[package]] @@ -804,7 +865,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.71", ] [[package]] @@ -876,7 +937,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.71", ] [[package]] @@ -1038,6 +1099,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -1160,7 +1230,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "xml-rs" -version = "0.8.21" +name = "xmlparser" +version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "539a77ee7c0de333dcc6da69b177380a0b81e0dacfa4f7344c465a36871ee601" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" diff --git a/Cargo.toml b/Cargo.toml index 0d28fe4..08e7c81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,8 +23,10 @@ tree-sitter = "0.22.6" tracing-appender = "0.2.3" protols-tree-sitter-proto = "0.2.0" walkdir = "2.5.0" -serde-xml-rs = "0.6.0" serde = { version = "1.0.209", features = ["derive"] } +quick-xml = { version = "0.36.1", features = ["serde", "serialize"] } +hard-xml = "1.36.0" +tempfile = "3.12.0" [dev-dependencies] insta = { version = "1.39.0", features = ["yaml"] } diff --git a/src/formatter/clang.rs b/src/formatter/clang.rs index c5deb9a..037b107 100644 --- a/src/formatter/clang.rs +++ b/src/formatter/clang.rs @@ -1,26 +1,62 @@ -use serde::{Deserialize, Serialize}; -use serde_xml_rs::from_str; -use std::{error::Error, process::Command}; +use std::{borrow::Cow, error::Error, fs::File, io::Write, path::PathBuf, process::Command}; use async_lsp::lsp_types::{Position, Range, TextEdit, Url}; +use hard_xml::XmlRead; +use tempfile::{tempdir, TempDir}; use super::ProtoFormatter; pub struct ClangFormatter { path: String, working_dir: String, + temp_dir: TempDir, } -#[derive(Serialize, Deserialize, Debug, PartialEq)] -struct Replacements { - replacements: Vec, +#[derive(XmlRead, PartialEq, Debug)] +#[xml(tag = "replacements")] +struct Replacements<'a> { + #[xml(child = "replacement")] + replacements: Vec>, } -#[derive(Serialize, Deserialize, Debug, PartialEq)] -struct Replacement { - offset: u32, - length: u32, - value: String, +#[derive(XmlRead, PartialEq, Debug)] +#[xml(tag = "replacement")] +struct Replacement<'a> { + #[xml(attr = "offset")] + offset: usize, + #[xml(attr = "length")] + length: usize, + #[xml(text)] + text: Cow<'a, str>, +} + +impl<'a> Replacement<'a> { + fn offset_to_position(offset: usize, content: &str) -> Option { + if offset > content.len() { + return None; + } + let up_to_offset = &content[..offset]; + let line = up_to_offset.matches('\n').count(); + let last_newline = up_to_offset.rfind('\n').map_or(0, |pos| pos + 1); + let character = offset - last_newline; + + tracing::info!(line, character); + + Some(Position { + line: line as u32, + character: character as u32, + }) + } + + fn as_text_edit(&self, content: &str) -> Option { + Some(TextEdit { + range: Range { + start: Self::offset_to_position(self.offset, content)?, + end: Self::offset_to_position(self.offset + self.length, content)?, + }, + new_text: self.text.to_string(), + }) + } } impl ClangFormatter { @@ -29,59 +65,56 @@ impl ClangFormatter { c.arg("--version").status()?; Ok(Self { + temp_dir: tempdir()?, path: path.to_owned(), working_dir: workdir.to_owned(), }) } - fn get_command(&self, u: &Url) -> Command { + fn get_temp_file_path(&self, content: &str) -> Option { + let p = self.temp_dir.path().join(""); + let mut file = File::create(p.clone()).ok()?; + file.write_all(content.as_ref()).ok()?; + return Some(p); + } + + fn get_command(&self, u: &PathBuf) -> Command { let mut c = Command::new(self.path.as_str()); c.current_dir(self.working_dir.as_str()); - c.args([u.path(), "--output-replacements-xml"]); + c.args([u.as_path().to_str().unwrap(), "--output-replacements-xml"]); c } -} - -impl ProtoFormatter for ClangFormatter { - fn format_document(&self, u: &Url) -> Option> { - let output = self.get_command(u).output().ok()?; - if !output.status.success() { - return None; - } - let output = String::from_utf8_lossy(&output.stdout); - let out: Replacements = from_str(&output).ok()?; - let edits = out + fn output_to_textedit(&self, output: &str, content: &str) -> Option> { + let r = Replacements::from_str(&output).ok()?; + tracing::info!("{r:?}"); + let edits = r .replacements .into_iter() - .map(|r| TextEdit { - range: Range { - start: Position { - line: 0, - character: 0, - }, - end: Position { - line: 1, - character: 3, - }, - }, - new_text: r.value, - }) + .filter_map(|r| r.as_text_edit(content.as_ref())) .collect(); tracing::info!("{edits:?}"); Some(edits) } +} - fn format_document_range( - &self, - u: &Url, - r: &Range, - ) -> Option> { +impl ProtoFormatter for ClangFormatter { + fn format_document(&self, content: &str) -> Option> { + let p = self.get_temp_file_path(content)?; + let output = self.get_command(&p).output().ok()?; + if !output.status.success() { + return None; + } + self.output_to_textedit(&String::from_utf8_lossy(&output.stdout), content) + } + + fn format_document_range(&self, r: &Range, content: &str) -> Option> { + let p = self.get_temp_file_path(content)?; let start = r.start.line + 1; let end = r.end.line + 1; let output = self - .get_command(u) + .get_command(&p) .args(["--lines", format!("{start}:{end}").as_str()]) .output() .ok()?; @@ -89,34 +122,6 @@ impl ProtoFormatter for ClangFormatter { if !output.status.success() { return None; } - - let output = String::from_utf8_lossy(&output.stdout); - - tracing::info!("{output}"); - if let Err(e) = from_str::(&output) { - tracing::error!("{e}"); - return None; - } - let out: Replacements = from_str(&output).ok()?; - let edits = out - .replacements - .into_iter() - .map(|r| TextEdit { - range: Range { - start: Position { - line: 9, - character: 1, - }, - end: Position { - line: 12, - character: 6, - }, - }, - new_text: r.value, - }) - .collect(); - - tracing::info!("{edits:?}"); - Some(edits) + self.output_to_textedit(&String::from_utf8_lossy(&output.stdout), content) } } diff --git a/src/formatter/mod.rs b/src/formatter/mod.rs index b4936c6..67f6913 100644 --- a/src/formatter/mod.rs +++ b/src/formatter/mod.rs @@ -1,8 +1,8 @@ -use async_lsp::lsp_types::{Range, TextEdit, Url}; +use async_lsp::lsp_types::{Range, TextEdit}; pub mod clang; pub trait ProtoFormatter: Sized { - fn format_document(&self, u: &Url) -> Option>; - fn format_document_range(&self, u: &Url, r: &Range) -> Option>; + fn format_document(&self, content: &str) -> Option>; + fn format_document_range(&self, r: &Range, content: &str) -> Option>; } diff --git a/src/lsp.rs b/src/lsp.rs index 51d6b00..a1a8a34 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -336,10 +336,13 @@ impl LanguageServer for ProtoLanguageServer { &mut self, params: DocumentFormattingParams, ) -> BoxFuture<'static, Result>, Self::Error>> { + let uri = params.text_document.uri; + let content = self.state.get_content(&uri); + let response = self .state .get_formatter() - .and_then(|f| f.format_document(¶ms.text_document.uri)); + .and_then(|f| f.format_document(content.as_str())); Box::pin(async move { Ok(response) }) } @@ -348,10 +351,13 @@ impl LanguageServer for ProtoLanguageServer { &mut self, params: DocumentRangeFormattingParams, ) -> BoxFuture<'static, Result>, Self::Error>> { + let uri = params.text_document.uri; + let content = self.state.get_content(&uri); + let response = self .state .get_formatter() - .and_then(|f| f.format_document_range(¶ms.text_document.uri, ¶ms.range)); + .and_then(|f| f.format_document_range(¶ms.range, content.as_str())); Box::pin(async move { Ok(response) }) } From bd0659a58d222064ef2ed1711936a02ddf21c7a2 Mon Sep 17 00:00:00 2001 From: coder3101 Date: Sun, 1 Sep 2024 15:06:07 +0530 Subject: [PATCH 4/6] docs: update and add instruction --- Cargo.lock | 13 +--- Cargo.toml | 5 +- README.md | 36 ++++++++++- sample/simple.proto | 8 +-- sample/test.proto | 4 +- src/formatter/clang.rs | 64 +++++++++++++++---- src/formatter/input/empty.xml | 4 ++ src/formatter/input/replacement.xml | 6 ++ src/formatter/input/test.proto | 11 ++++ ...er__clang__test__offset_to_position-2.snap | 8 +++ ...er__clang__test__offset_to_position-3.snap | 8 +++ ...er__clang__test__offset_to_position-4.snap | 7 ++ ...tter__clang__test__offset_to_position.snap | 6 ++ ...atter__clang__test__reading_empty_xml.snap | 5 ++ ...__formatter__clang__test__reading_xml.snap | 11 ++++ src/lsp.rs | 4 +- 16 files changed, 161 insertions(+), 39 deletions(-) create mode 100644 src/formatter/input/empty.xml create mode 100644 src/formatter/input/replacement.xml create mode 100644 src/formatter/input/test.proto create mode 100644 src/formatter/snapshots/protols__formatter__clang__test__offset_to_position-2.snap create mode 100644 src/formatter/snapshots/protols__formatter__clang__test__offset_to_position-3.snap create mode 100644 src/formatter/snapshots/protols__formatter__clang__test__offset_to_position-4.snap create mode 100644 src/formatter/snapshots/protols__formatter__clang__test__offset_to_position.snap create mode 100644 src/formatter/snapshots/protols__formatter__clang__test__reading_empty_xml.snap create mode 100644 src/formatter/snapshots/protols__formatter__clang__test__reading_xml.snap diff --git a/Cargo.lock b/Cargo.lock index 2d934b3..5a84072 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -509,14 +509,13 @@ dependencies = [ [[package]] name = "protols" -version = "0.5.0" +version = "0.6.0" dependencies = [ "async-lsp", "futures", "hard-xml", "insta", "protols-tree-sitter-proto", - "quick-xml", "serde", "tempfile", "tokio", @@ -539,16 +538,6 @@ dependencies = [ "tree-sitter", ] -[[package]] -name = "quick-xml" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96a05e2e8efddfa51a84ca47cec303fac86c8541b686d37cac5efc0e094417bc" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "quote" version = "1.0.36" diff --git a/Cargo.toml b/Cargo.toml index 08e7c81..35ad41e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "protols" description = "Language server for proto3 files" -version = "0.5.0" +version = "0.6.0" edition = "2021" license = "MIT" homepage = "https://github.com/coder3101/protols" @@ -23,10 +23,9 @@ tree-sitter = "0.22.6" tracing-appender = "0.2.3" protols-tree-sitter-proto = "0.2.0" walkdir = "2.5.0" -serde = { version = "1.0.209", features = ["derive"] } -quick-xml = { version = "0.36.1", features = ["serde", "serialize"] } hard-xml = "1.36.0" tempfile = "3.12.0" +serde = { version = "1.0.209", features = ["derive"] } [dev-dependencies] insta = { version = "1.39.0", features = ["yaml"] } diff --git a/README.md b/README.md index 8c31f57..40ef51e 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,9 @@ A Language Server for **proto3** files. It uses tree-sitter parser for all opera ![](./assets/protols.mov) ## Features -- [x] Completion (keywords, enums and messages of the package) -- [x] Diagnostics - based on sytax errors +- [x] Completion +- [x] Diagnostics +- [x] Formatting - [x] Document Symbols - [x] Go to definition - [x] Hover @@ -29,3 +30,34 @@ require'lspconfig'.protols.setup{} You can install an extension called [Protobuf Language Support](https://marketplace.visualstudio.com/items?itemName=ianandhum.protobuf-support) which uses this LSP under the hood. > NOTE: It is [open-sourced](https://github.com/ianandhum/vscode-protobuf-support) but do not own or maintain it. + + +## Usage + +#### Completion + +Out of the box you will get auto Completion for Message, Enum of current package and Completion for keywords. + +#### Diagnostics + +Diagnostics is not reported by executing `protoc` so do not expect a full blown diagnostic result, we use tree-sitter parse for diagnostic which only displays parser errors. + +#### Formatting + +Formatting is enabled only if [`clang-format`](https://clang.llvm.org/docs/ClangFormat.html) is found. You can control the [formatting style](https://clang.llvm.org/docs/ClangFormatStyleOptions.html) by putting a `.clang-format` file at the root of the workspace. Both document and rage formatting is supported. + +#### Document Symbols + +Symbols for the document (i.e Message and Enums) along with support for nested symbols is available. + +#### Goto definition + +You can jump to definition of any custom symbols even across package boundaries. + +#### Hover + +Protobuf is usually documented by putting comments above symbol definition and hover feature utilises this assumption to present Hover text for symbols. This also works across package boundaries. + +#### Rename + +You can only document symbols such as Message and Enum names and all its usages will be renamed by the LSP, we do not support renaming field within a symbol however, you can jump to definition of field and rename the symbol. Renaming is also performed across pakages. diff --git a/sample/simple.proto b/sample/simple.proto index 306e28d..1692f33 100644 --- a/sample/simple.proto +++ b/sample/simple.proto @@ -31,13 +31,9 @@ message GetBookRequest { int64 isbn = 1; } -message GotoBookRequest { - bool flag = 1; -} +message GotoBookRequest { bool flag = 1; } -message GetBookViaAuthor { - Book.Author author = 1; -} +message GetBookViaAuthor { Book.Author author = 1; } // It is a BookService Implementation service BookService { diff --git a/sample/test.proto b/sample/test.proto index 04e994b..1a36687 100644 --- a/sample/test.proto +++ b/sample/test.proto @@ -5,7 +5,7 @@ package a.b.c; message CustomType { bool attribute = 1; } message SomeMessage { - int64 someAttribute = 1; + int64 someAttribute = 1; - CustomType another = 2; + CustomType another = 2; } diff --git a/src/formatter/clang.rs b/src/formatter/clang.rs index 037b107..7432c78 100644 --- a/src/formatter/clang.rs +++ b/src/formatter/clang.rs @@ -1,25 +1,26 @@ use std::{borrow::Cow, error::Error, fs::File, io::Write, path::PathBuf, process::Command}; -use async_lsp::lsp_types::{Position, Range, TextEdit, Url}; +use async_lsp::lsp_types::{Position, Range, TextEdit}; use hard_xml::XmlRead; +use serde::Serialize; use tempfile::{tempdir, TempDir}; use super::ProtoFormatter; pub struct ClangFormatter { path: String, - working_dir: String, + working_dir: Option, temp_dir: TempDir, } -#[derive(XmlRead, PartialEq, Debug)] +#[derive(XmlRead, Serialize, PartialEq, Debug)] #[xml(tag = "replacements")] struct Replacements<'a> { #[xml(child = "replacement")] replacements: Vec>, } -#[derive(XmlRead, PartialEq, Debug)] +#[derive(XmlRead, Serialize, PartialEq, Debug)] #[xml(tag = "replacement")] struct Replacement<'a> { #[xml(attr = "offset")] @@ -40,8 +41,6 @@ impl<'a> Replacement<'a> { let last_newline = up_to_offset.rfind('\n').map_or(0, |pos| pos + 1); let character = offset - last_newline; - tracing::info!(line, character); - Some(Position { line: line as u32, character: character as u32, @@ -60,19 +59,19 @@ impl<'a> Replacement<'a> { } impl ClangFormatter { - pub fn new(path: &str, workdir: &str) -> Result> { + pub fn new(path: &str, workdir: Option<&str>) -> Result> { let mut c = Command::new(path); c.arg("--version").status()?; Ok(Self { temp_dir: tempdir()?, path: path.to_owned(), - working_dir: workdir.to_owned(), + working_dir: workdir.map(ToOwned::to_owned), }) } fn get_temp_file_path(&self, content: &str) -> Option { - let p = self.temp_dir.path().join(""); + let p = self.temp_dir.path().join("format-temp.proto"); let mut file = File::create(p.clone()).ok()?; file.write_all(content.as_ref()).ok()?; return Some(p); @@ -80,21 +79,21 @@ impl ClangFormatter { fn get_command(&self, u: &PathBuf) -> Command { let mut c = Command::new(self.path.as_str()); - c.current_dir(self.working_dir.as_str()); + if let Some(wd) = self.working_dir.as_ref() { + c.current_dir(wd.as_str()); + } c.args([u.as_path().to_str().unwrap(), "--output-replacements-xml"]); c } fn output_to_textedit(&self, output: &str, content: &str) -> Option> { let r = Replacements::from_str(&output).ok()?; - tracing::info!("{r:?}"); let edits = r .replacements .into_iter() .filter_map(|r| r.as_text_edit(content.as_ref())) .collect(); - tracing::info!("{edits:?}"); Some(edits) } } @@ -104,6 +103,10 @@ impl ProtoFormatter for ClangFormatter { let p = self.get_temp_file_path(content)?; let output = self.get_command(&p).output().ok()?; if !output.status.success() { + tracing::error!( + status = output.status.code(), + "failed to execute clang-format" + ); return None; } self.output_to_textedit(&String::from_utf8_lossy(&output.stdout), content) @@ -120,8 +123,45 @@ impl ProtoFormatter for ClangFormatter { .ok()?; if !output.status.success() { + tracing::error!( + status = output.status.code(), + "failed to execute clang-format" + ); return None; } self.output_to_textedit(&String::from_utf8_lossy(&output.stdout), content) } } + +#[cfg(test)] +mod test { + use hard_xml::XmlRead; + use insta::{assert_yaml_snapshot, with_settings}; + + use super::{Replacement, Replacements}; + + #[test] + fn test_reading_xml() { + let c = include_str!("input/replacement.xml"); + let r = Replacements::from_str(c).unwrap(); + assert_yaml_snapshot!(r); + } + + #[test] + fn test_reading_empty_xml() { + let c = include_str!("input/empty.xml"); + let r = Replacements::from_str(c).unwrap(); + assert_yaml_snapshot!(r); + } + + #[test] + fn test_offset_to_position() { + let c = include_str!("input/test.proto"); + let pos = vec![0, 4, 22, 999]; + for i in pos { + with_settings!({description => c, info => &i}, { + assert_yaml_snapshot!(Replacement::offset_to_position(i, c)); + }) + } + } +} diff --git a/src/formatter/input/empty.xml b/src/formatter/input/empty.xml new file mode 100644 index 0000000..4d0b8ed --- /dev/null +++ b/src/formatter/input/empty.xml @@ -0,0 +1,4 @@ + + + + diff --git a/src/formatter/input/replacement.xml b/src/formatter/input/replacement.xml new file mode 100644 index 0000000..b759902 --- /dev/null +++ b/src/formatter/input/replacement.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/formatter/input/test.proto b/src/formatter/input/test.proto new file mode 100644 index 0000000..e6723dd --- /dev/null +++ b/src/formatter/input/test.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package foo.bar; + +message Box { + int64 height = 1; + int64 width = 2; + int64 depth = 3; +} + +service BoxAreaFinder { rpc FindArea(Box) returns (int64); } diff --git a/src/formatter/snapshots/protols__formatter__clang__test__offset_to_position-2.snap b/src/formatter/snapshots/protols__formatter__clang__test__offset_to_position-2.snap new file mode 100644 index 0000000..d23b267 --- /dev/null +++ b/src/formatter/snapshots/protols__formatter__clang__test__offset_to_position-2.snap @@ -0,0 +1,8 @@ +--- +source: src/formatter/clang.rs +description: "syntax = \"proto3\";\n\npackage foo.bar;\n\nmessage Box {\n int64 height = 1;\n int64 width = 2;\n int64 depth = 3;\n}\n\nservice BoxAreaFinder { rpc FindArea(Box) returns (int64); }\n" +expression: "Replacement::offset_to_position(i, c)" +info: 4 +--- +line: 0 +character: 4 diff --git a/src/formatter/snapshots/protols__formatter__clang__test__offset_to_position-3.snap b/src/formatter/snapshots/protols__formatter__clang__test__offset_to_position-3.snap new file mode 100644 index 0000000..5325058 --- /dev/null +++ b/src/formatter/snapshots/protols__formatter__clang__test__offset_to_position-3.snap @@ -0,0 +1,8 @@ +--- +source: src/formatter/clang.rs +description: "syntax = \"proto3\";\n\npackage foo.bar;\n\nmessage Box {\n int64 height = 1;\n int64 width = 2;\n int64 depth = 3;\n}\n\nservice BoxAreaFinder { rpc FindArea(Box) returns (int64); }\n" +expression: "Replacement::offset_to_position(i, c)" +info: 22 +--- +line: 2 +character: 2 diff --git a/src/formatter/snapshots/protols__formatter__clang__test__offset_to_position-4.snap b/src/formatter/snapshots/protols__formatter__clang__test__offset_to_position-4.snap new file mode 100644 index 0000000..ede75dc --- /dev/null +++ b/src/formatter/snapshots/protols__formatter__clang__test__offset_to_position-4.snap @@ -0,0 +1,7 @@ +--- +source: src/formatter/clang.rs +description: "syntax = \"proto3\";\n\npackage foo.bar;\n\nmessage Box {\n int64 height = 1;\n int64 width = 2;\n int64 depth = 3;\n}\n\nservice BoxAreaFinder { rpc FindArea(Box) returns (int64); }\n" +expression: "Replacement::offset_to_position(i, c)" +info: 999 +--- +~ diff --git a/src/formatter/snapshots/protols__formatter__clang__test__offset_to_position.snap b/src/formatter/snapshots/protols__formatter__clang__test__offset_to_position.snap new file mode 100644 index 0000000..35ccd6c --- /dev/null +++ b/src/formatter/snapshots/protols__formatter__clang__test__offset_to_position.snap @@ -0,0 +1,6 @@ +--- +source: src/formatter/clang.rs +expression: r +--- +line: 0 +character: 0 diff --git a/src/formatter/snapshots/protols__formatter__clang__test__reading_empty_xml.snap b/src/formatter/snapshots/protols__formatter__clang__test__reading_empty_xml.snap new file mode 100644 index 0000000..5530971 --- /dev/null +++ b/src/formatter/snapshots/protols__formatter__clang__test__reading_empty_xml.snap @@ -0,0 +1,5 @@ +--- +source: src/formatter/clang.rs +expression: r +--- +replacements: [] diff --git a/src/formatter/snapshots/protols__formatter__clang__test__reading_xml.snap b/src/formatter/snapshots/protols__formatter__clang__test__reading_xml.snap new file mode 100644 index 0000000..ec515fa --- /dev/null +++ b/src/formatter/snapshots/protols__formatter__clang__test__reading_xml.snap @@ -0,0 +1,11 @@ +--- +source: src/formatter/clang.rs +expression: r +--- +replacements: + - offset: 56 + length: 1 + text: "\n " + - offset: 76 + length: 1 + text: "\n" diff --git a/src/lsp.rs b/src/lsp.rs index a1a8a34..e4f3592 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -79,12 +79,12 @@ impl LanguageServer for ProtoLanguageServer { let mut formatter_provider = None; let mut formatter_range_provider = None; if let Some(folders) = params.workspace_folders { - if let Ok(f) = ClangFormatter::new("clang-format", folders.first().unwrap().uri.path()) + if let Ok(f) = ClangFormatter::new("clang-format", folders.first().map(|f| f.uri.path())) { self.state.add_formatter(f); formatter_provider = Some(OneOf::Left(true)); formatter_range_provider = Some(OneOf::Left(true)); - info!("Setting formatting client capability"); + info!("Setting formatting server capability"); } for workspace in folders { info!("Workspace folder: {workspace:?}"); From 175a837285ba5a8dfb8e0b8e50aa1e931f7e4890 Mon Sep 17 00:00:00 2001 From: coder3101 Date: Sun, 1 Sep 2024 15:27:41 +0530 Subject: [PATCH 5/6] lint: fix all --- src/formatter/clang.rs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/formatter/clang.rs b/src/formatter/clang.rs index 7432c78..d2e72d5 100644 --- a/src/formatter/clang.rs +++ b/src/formatter/clang.rs @@ -1,4 +1,12 @@ -use std::{borrow::Cow, error::Error, fs::File, io::Write, path::PathBuf, process::Command}; +#![allow(clippy::needless_late_init)] +use std::{ + borrow::Cow, + error::Error, + fs::File, + io::Write, + path::{Path, PathBuf}, + process::Command, +}; use async_lsp::lsp_types::{Position, Range, TextEdit}; use hard_xml::XmlRead; @@ -74,20 +82,20 @@ impl ClangFormatter { let p = self.temp_dir.path().join("format-temp.proto"); let mut file = File::create(p.clone()).ok()?; file.write_all(content.as_ref()).ok()?; - return Some(p); + Some(p) } - fn get_command(&self, u: &PathBuf) -> Command { + fn get_command(&self, u: &Path) -> Command { let mut c = Command::new(self.path.as_str()); if let Some(wd) = self.working_dir.as_ref() { c.current_dir(wd.as_str()); } - c.args([u.as_path().to_str().unwrap(), "--output-replacements-xml"]); + c.args([u.to_str().unwrap(), "--output-replacements-xml"]); c } fn output_to_textedit(&self, output: &str, content: &str) -> Option> { - let r = Replacements::from_str(&output).ok()?; + let r = Replacements::from_str(output).ok()?; let edits = r .replacements .into_iter() @@ -101,7 +109,7 @@ impl ClangFormatter { impl ProtoFormatter for ClangFormatter { fn format_document(&self, content: &str) -> Option> { let p = self.get_temp_file_path(content)?; - let output = self.get_command(&p).output().ok()?; + let output = self.get_command(p.as_ref()).output().ok()?; if !output.status.success() { tracing::error!( status = output.status.code(), @@ -117,7 +125,7 @@ impl ProtoFormatter for ClangFormatter { let start = r.start.line + 1; let end = r.end.line + 1; let output = self - .get_command(&p) + .get_command(p.as_ref()) .args(["--lines", format!("{start}:{end}").as_str()]) .output() .ok()?; From 9a2d093a4ab2cfb2fb0fc02aa0bb248007c9bb0a Mon Sep 17 00:00:00 2001 From: coder3101 Date: Sun, 1 Sep 2024 15:32:48 +0530 Subject: [PATCH 6/6] ai: gen readme --- README.md | 79 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 40ef51e..cbc1b60 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,76 @@ -# protols -[![Crates](https://img.shields.io/crates/v/protols.svg)](https://crates.io/crates/protols) +# Protols - Protobuf Language Server + +[![Crates.io](https://img.shields.io/crates/v/protols.svg)](https://crates.io/crates/protols) [![Build and Test](https://github.com/coder3101/protols/actions/workflows/ci.yml/badge.svg)](https://github.com/coder3101/protols/actions/workflows/ci.yml) -A Language Server for **proto3** files. It uses tree-sitter parser for all operations. +**Protols** is an open-source Language Server Protocol (LSP) for **proto3** files, powered by the robust and efficient [tree-sitter](https://tree-sitter.github.io/tree-sitter/) parser. With Protols, you get powerful code assistance for protobuf files, including auto-completion, syntax diagnostics, and more. ![](./assets/protols.mov) -## Features -- [x] Completion -- [x] Diagnostics -- [x] Formatting -- [x] Document Symbols -- [x] Go to definition -- [x] Hover -- [x] Rename +## ✨ Features + +- ✅ Code Completion +- ✅ Diagnostics +- ✅ Document Symbols +- ✅ Code Formatting +- ✅ Go to Definition +- ✅ Hover Information +- ✅ Rename Symbols + +## 🚀 Getting Started -## Installation +### Installation -### Neovim -Run `cargo install protols` to install and add below to setup using [`nvim-lspconfig`](https://github.com/neovim/nvim-lspconfig/blob/master/doc/server_configurations.md#protols) +#### For Neovim + +To install Protols, run: + +```bash +cargo install protols +``` + +Then, configure it with [`nvim-lspconfig`](https://github.com/neovim/nvim-lspconfig/blob/master/doc/server_configurations.md#protols): ```lua require'lspconfig'.protols.setup{} - ``` -### Visual Studio Code +#### For Visual Studio Code + +You can use the [Protobuf Language Support](https://marketplace.visualstudio.com/items?itemName=ianandhum.protobuf-support) extension, which leverages this LSP under the hood. -You can install an extension called [Protobuf Language Support](https://marketplace.visualstudio.com/items?itemName=ianandhum.protobuf-support) which uses this LSP under the hood. +> **Note:** This extension is [open source](https://github.com/ianandhum/vscode-protobuf-support) but is not maintained by us. -> NOTE: It is [open-sourced](https://github.com/ianandhum/vscode-protobuf-support) but do not own or maintain it. +## 🛠️ Usage +### Code Completion -## Usage +Protols provides intelligent autocompletion for messages, enums, and proto3 keywords within the current package. -#### Completion +### Diagnostics -Out of the box you will get auto Completion for Message, Enum of current package and Completion for keywords. +Diagnostics are powered by the tree-sitter parser, which catches syntax errors but does not utilize `protoc` for more advanced error reporting. -#### Diagnostics +### Code Formatting -Diagnostics is not reported by executing `protoc` so do not expect a full blown diagnostic result, we use tree-sitter parse for diagnostic which only displays parser errors. +Formatting is enabled if [clang-format](https://clang.llvm.org/docs/ClangFormat.html) is available. You can control the [formatting style](https://clang.llvm.org/docs/ClangFormatStyleOptions.html) by placing a `.clang-format` file in the root of your workspace. Both document and range formatting are supported. -#### Formatting +### Document Symbols -Formatting is enabled only if [`clang-format`](https://clang.llvm.org/docs/ClangFormat.html) is found. You can control the [formatting style](https://clang.llvm.org/docs/ClangFormatStyleOptions.html) by putting a `.clang-format` file at the root of the workspace. Both document and rage formatting is supported. +Provides symbols for the entire document, including nested symbols, messages, and enums. -#### Document Symbols +### Go to Definition -Symbols for the document (i.e Message and Enums) along with support for nested symbols is available. +Jump to the definition of any custom symbol, even across package boundaries. -#### Goto definition +### Hover Information -You can jump to definition of any custom symbols even across package boundaries. +Displays comments and documentation for protobuf symbols on hover. Works seamlessly across package boundaries. -#### Hover +### Rename Symbols -Protobuf is usually documented by putting comments above symbol definition and hover feature utilises this assumption to present Hover text for symbols. This also works across package boundaries. +Allows renaming of symbols like messages and enums, along with all their usages across packages. Currently, renaming fields within symbols is not supported directly. -#### Rename +--- -You can only document symbols such as Message and Enum names and all its usages will be renamed by the LSP, we do not support renaming field within a symbol however, you can jump to definition of field and rename the symbol. Renaming is also performed across pakages. +Protols is designed to supercharge your workflow with **proto3** files. We welcome contributions and feedback from the community! Feel free to check out the [repository](https://github.com/coder3101/protols) and join in on improving this tool! 🎉