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..5a84072 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" @@ -472,12 +509,15 @@ dependencies = [ [[package]] name = "protols" -version = "0.5.0" +version = "0.6.0" dependencies = [ "async-lsp", "futures", + "hard-xml", "insta", "protols-tree-sitter-proto", + "serde", + "tempfile", "tokio", "tokio-util", "tower", @@ -587,22 +627,22 @@ 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_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", - "syn", + "syn 2.0.71", ] [[package]] @@ -624,7 +664,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.71", ] [[package]] @@ -676,6 +716,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" @@ -687,6 +738,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" @@ -704,7 +768,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.71", ] [[package]] @@ -790,7 +854,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.71", ] [[package]] @@ -862,7 +926,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.71", ] [[package]] @@ -1024,6 +1088,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" @@ -1144,3 +1217,9 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" diff --git a/Cargo.toml b/Cargo.toml index 2e4d717..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,6 +23,9 @@ tree-sitter = "0.22.6" tracing-appender = "0.2.3" protols-tree-sitter-proto = "0.2.0" walkdir = "2.5.0" +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..cbc1b60 100644 --- a/README.md +++ b/README.md @@ -1,31 +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 (keywords, enums and messages of the package) -- [x] Diagnostics - based on sytax errors -- [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 +#### For Neovim + +To install Protols, run: + +```bash +cargo install protols +``` -### 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) +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. + +> **Note:** This extension is [open source](https://github.com/ianandhum/vscode-protobuf-support) but is not maintained by us. + +## 🛠️ Usage + +### Code Completion + +Protols provides intelligent autocompletion for messages, enums, and proto3 keywords within the current package. + +### Diagnostics + +Diagnostics are powered by the tree-sitter parser, which catches syntax errors but does not utilize `protoc` for more advanced error reporting. + +### Code Formatting + +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. + +### Document Symbols + +Provides symbols for the entire document, including nested symbols, messages, and enums. + +### Go to Definition + +Jump to the definition of any custom symbol, even across package boundaries. + +### Hover Information + +Displays comments and documentation for protobuf symbols on hover. Works seamlessly across package boundaries. + +### Rename Symbols + +Allows renaming of symbols like messages and enums, along with all their usages across packages. Currently, renaming fields within symbols is not supported directly. -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. +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! 🎉 diff --git a/sample/simple.proto b/sample/simple.proto index 9cc8717..1692f33 100644 --- a/sample/simple.proto +++ b/sample/simple.proto @@ -2,61 +2,56 @@ 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; + // This is a sigle line comment on the field of a message + int64 isbn = 1; } -message GotoBookRequest { - bool flag = 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 +59,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..d2e72d5 --- /dev/null +++ b/src/formatter/clang.rs @@ -0,0 +1,175 @@ +#![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; +use serde::Serialize; +use tempfile::{tempdir, TempDir}; + +use super::ProtoFormatter; + +pub struct ClangFormatter { + path: String, + working_dir: Option, + temp_dir: TempDir, +} + +#[derive(XmlRead, Serialize, PartialEq, Debug)] +#[xml(tag = "replacements")] +struct Replacements<'a> { + #[xml(child = "replacement")] + replacements: Vec>, +} + +#[derive(XmlRead, Serialize, 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; + + 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 { + 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.map(ToOwned::to_owned), + }) + } + + fn get_temp_file_path(&self, content: &str) -> Option { + 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()?; + Some(p) + } + + 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.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 edits = r + .replacements + .into_iter() + .filter_map(|r| r.as_text_edit(content.as_ref())) + .collect(); + + Some(edits) + } +} + +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.as_ref()).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) + } + + 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(p.as_ref()) + .args(["--lines", format!("{start}:{end}").as_str()]) + .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) + } +} + +#[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/mod.rs b/src/formatter/mod.rs new file mode 100644 index 0000000..67f6913 --- /dev/null +++ b/src/formatter/mod.rs @@ -0,0 +1,8 @@ +use async_lsp::lsp_types::{Range, TextEdit}; + +pub mod clang; + +pub trait ProtoFormatter: Sized { + fn format_document(&self, content: &str) -> Option>; + fn format_document_range(&self, r: &Range, content: &str) -> Option>; +} 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 e96d43d..e4f3592 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, + DidOpenTextDocumentParams, DidSaveTextDocumentParams, DocumentFormattingParams, + 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, Url, WorkspaceEdit, WorkspaceFileOperationsServerCapabilities, + 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,16 @@ 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(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 server capability"); + } for workspace in folders { info!("Workspace folder: {workspace:?}"); self.state.add_workspace_folder_async(workspace, tx.clone()) @@ -120,6 +132,8 @@ 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, + document_range_formatting_provider: formatter_range_provider, ..ServerCapabilities::default() }, @@ -318,6 +332,36 @@ impl LanguageServer for ProtoLanguageServer { Box::pin(async move { Ok(Some(response)) }) } + fn formatting( + &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(content.as_str())); + + Box::pin(async move { Ok(response) }) + } + + fn range_formatting( + &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.range, content.as_str())); + + 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..901d8c4 100644 --- a/src/state.rs +++ b/src/state.rs @@ -15,22 +15,25 @@ use tree_sitter::Node; use walkdir::WalkDir; use crate::{ + formatter::ProtoFormatter, nodekind::NodeKind, parser::{ParsedTree, ProtoParser}, }; -pub struct ProtoLanguageState { +pub struct ProtoLanguageState { documents: Arc>>, trees: Arc>>, + formatter: Option, parser: Arc>, } -impl ProtoLanguageState { +impl ProtoLanguageState { pub fn new() -> Self { ProtoLanguageState { documents: Default::default(), trees: Default::default(), parser: Arc::new(Mutex::new(ProtoParser::new())), + formatter: Default::default(), } } @@ -94,6 +97,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());