From 69e32a5cab19e53dcf4835b7da7d999a22b61b7f Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Wed, 22 May 2024 22:18:16 +0800 Subject: [PATCH] Add `autocorrect server` command to start LSP server. (#199) Made this for Zed extension: image --- Cargo.toml | 43 +++-- Makefile | 2 + autocorrect-cli/Cargo.toml | 4 +- autocorrect-cli/src/cli.rs | 2 + autocorrect-cli/src/lib.rs | 5 + autocorrect-lsp/Cargo.toml | 15 ++ autocorrect-lsp/src/lib.rs | 382 +++++++++++++++++++++++++++++++++++++ autocorrect/Cargo.toml | 6 +- autocorrect/src/lib.rs | 2 +- 9 files changed, 439 insertions(+), 22 deletions(-) create mode 100644 autocorrect-lsp/Cargo.toml create mode 100644 autocorrect-lsp/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index d42bdc50..dc44392d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,23 +1,32 @@ [workspace] +# https://github.com/seanmonstar/reqwest/issues/1300#issuecomment-1368265203 +resolver = "2" default-members = [ - "autocorrect", - "autocorrect-derive", - "autocorrect-wasm", - "autocorrect-cli", -] -exclude = [ - "autocorrect-tauri", + "autocorrect", + "autocorrect-derive", + "autocorrect-wasm", + "autocorrect-cli", + "autocorrect-lsp", ] members = [ - "autocorrect", - "autocorrect-wasm", - "autocorrect-derive", - "autocorrect-cli", - "autocorrect-py", - "autocorrect-node", - "autocorrect-rb/ext/autocorrect", - "autocorrect-java", + "autocorrect", + "autocorrect-wasm", + "autocorrect-derive", + "autocorrect-cli", + "autocorrect-py", + "autocorrect-node", + "autocorrect-rb/ext/autocorrect", + "autocorrect-java", + "autocorrect-lsp", ] -# https://github.com/seanmonstar/reqwest/issues/1300#issuecomment-1368265203 -resolver = "2" +[workspace.dependencies] +autocorrect = { path = "autocorrect" } +autocorrect-derive = { path = "autocorrect-derive" } +autocorrect-wasm = { path = "autocorrect-wasm" } +autocorrect-cli = { path = "autocorrect-cli" } +autocorrect-lsp = { path = "autocorrect-lsp" } + +serde = { version = "1", features = ["derive"] } +serde_json = "1.0.64" +anyhow = "1.0.86" diff --git a/Makefile b/Makefile index 5eb6bc59..7ec0c24b 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,8 @@ run: cargo run -- --lint --config $(WORKDIR)/.autocorrectrc.template --no-diff-bg-color run\:json: cargo run -- --lint --format json +server: + cargo run -- server build: cargo build --manifest-path autocorrect-cli/Cargo.toml --release --target aarch64-apple-darwin ls -lha target/aarch64-apple-darwin/release/autocorrect diff --git a/autocorrect-cli/Cargo.toml b/autocorrect-cli/Cargo.toml index 87bb645f..b67c4de6 100644 --- a/autocorrect-cli/Cargo.toml +++ b/autocorrect-cli/Cargo.toml @@ -14,7 +14,9 @@ name = "autocorrect" path = "src/main.rs" [dependencies] -autocorrect = { path = "../autocorrect", version = ">1.0.0" } +autocorrect.workspace = true +autocorrect-lsp.workspace = true + clap = { version = "4", features = ['derive'] } ignore = "0.4" log = "0.4" diff --git a/autocorrect-cli/src/cli.rs b/autocorrect-cli/src/cli.rs index 783da3d8..d308fc34 100644 --- a/autocorrect-cli/src/cli.rs +++ b/autocorrect-cli/src/cli.rs @@ -104,6 +104,8 @@ pub(crate) enum Commands { about = "Update AutoCorrect to latest version." )] Update {}, + #[command(name = "server", about = "Start AutoCorrect LSP server.")] + Server {}, } impl Cli { diff --git a/autocorrect-cli/src/lib.rs b/autocorrect-cli/src/lib.rs index 0bff769f..f561aa6e 100644 --- a/autocorrect-cli/src/lib.rs +++ b/autocorrect-cli/src/lib.rs @@ -74,6 +74,11 @@ where } return; } + Some(cli::Commands::Server {}) => { + log::info!("Starting AutoCorrect LSP server..."); + autocorrect_lsp::start().await; + return; + } _ => {} } diff --git a/autocorrect-lsp/Cargo.toml b/autocorrect-lsp/Cargo.toml new file mode 100644 index 00000000..3fced4bd --- /dev/null +++ b/autocorrect-lsp/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "autocorrect-lsp" +version = "2.9.4" +edition = "2021" + +[dependencies] +autocorrect.workspace = true + +tokio = { version = "1.37.0", features = [ + "io-util", + "io-std", + "macros", + "rt-multi-thread", +] } +tower-lsp = "0.20.0" diff --git a/autocorrect-lsp/src/lib.rs b/autocorrect-lsp/src/lib.rs new file mode 100644 index 00000000..11139ec9 --- /dev/null +++ b/autocorrect-lsp/src/lib.rs @@ -0,0 +1,382 @@ +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::{Arc, RwLock}; + +use tower_lsp::jsonrpc::Result; +use tower_lsp::lsp_types::*; +use tower_lsp::{Client, LanguageServer, LspService, Server}; + +struct Backend { + client: Client, + work_dir: RwLock, + documents: RwLock>>, + ignorer: RwLock>, +} + +static DEFAULT_CONFIG_FILE: &str = ".autocorrectrc"; +static DEFAULT_IGNORE_FILE: &str = ".autocorrectignore"; + +impl Backend { + fn work_dir(&self) -> PathBuf { + self.work_dir.read().unwrap().clone() + } + + fn set_work_dir(&self, work_dir: PathBuf) { + *self.work_dir.write().unwrap() = work_dir; + } + + fn upsert_document(&self, doc: Arc) { + let uri = doc.uri.clone(); + self.documents + .write() + .unwrap() + .get_mut(&uri) + .map(|old| std::mem::replace(old, doc.clone())); + } + + fn get_document<'a>(&'a self, uri: &Url) -> Option> { + self.documents.read().unwrap().get(uri).map(|a| a.clone()) + } + + fn remove_document(&self, uri: &Url) { + self.documents.write().unwrap().remove(uri); + } + + async fn lint_document(&self, document: &TextDocumentItem) { + self.clear_diagnostics(&document.uri).await; + + let input = document.text.as_str(); + let path = document.uri.path(); + let result = autocorrect::lint_for(input, &path); + + let diagnostics = result + .lines + .iter() + .map(|result| { + let addition_lines = result.old.lines().count() - 1; + let (severity, source) = match result.severity { + autocorrect::Severity::Error => ( + Some(DiagnosticSeverity::WARNING), + Some("AutoCorrect".to_string()), + ), + autocorrect::Severity::Warning => ( + Some(DiagnosticSeverity::INFORMATION), + Some("Spellcheck".to_string()), + ), + _ => (None, None), + }; + + Diagnostic { + range: Range { + start: Position { + line: result.line as u32 - 1, + character: result.col as u32 - 1, + }, + end: Position { + line: (result.line + addition_lines - 1) as u32, + character: (result.col + result.old.chars().count() - 1) as u32, + }, + }, + source, + severity, + message: result.new.clone(), + ..Default::default() + } + }) + .collect(); + + self.send_diagnostics(document, diagnostics).await; + } + + async fn send_diagnostics(&self, document: &TextDocumentItem, diagnostics: Vec) { + self.client + .publish_diagnostics(document.uri.clone(), diagnostics, None) + .await; + } + + async fn clear_diagnostics(&self, uri: &Url) { + self.client + .publish_diagnostics(uri.clone(), vec![], None) + .await; + } + + async fn clear_all_diagnostic(&self) { + let uris = self + .documents + .read() + .unwrap() + .keys() + .cloned() + .collect::>(); + + for uri in uris.iter() { + self.clear_diagnostics(uri).await; + } + } + + fn reload_config(&self) { + let conf_file = self.work_dir().join(DEFAULT_CONFIG_FILE); + autocorrect::config::load_file(&conf_file.to_string_lossy()).ok(); + + let ignorer = autocorrect::ignorer::Ignorer::new(&self.work_dir().to_string_lossy()); + self.ignorer.write().unwrap().replace(ignorer); + } + + fn is_ignored(&self, uri: &Url) -> bool { + if let Some(ignorer) = self.ignorer.read().unwrap().as_ref() { + if let Some(filepath) = uri.to_file_path().ok() { + return ignorer.is_ignored(&filepath.to_string_lossy()); + } + } + + false + } +} + +#[tower_lsp::async_trait] +impl LanguageServer for Backend { + async fn initialize(&self, params: InitializeParams) -> Result { + if let Some(root_uri) = params.root_uri { + let root_path = root_uri.to_file_path().unwrap(); + self.set_work_dir(root_path.clone()); + self.client + .log_message( + MessageType::INFO, + format!("root_uri: {}\n", root_path.display()), + ) + .await; + + let ignorer = autocorrect::ignorer::Ignorer::new(&root_path.to_string_lossy()); + self.ignorer.write().unwrap().replace(ignorer); + } + + self.reload_config(); + + Ok(InitializeResult { + server_info: Some(ServerInfo { + name: "AutoCorrect".into(), + version: Some(env!("CARGO_PKG_VERSION").into()), + }), + capabilities: ServerCapabilities { + workspace: Some(WorkspaceServerCapabilities { + workspace_folders: Some(WorkspaceFoldersServerCapabilities { + supported: Some(true), + change_notifications: Some(OneOf::Left(true)), + }), + file_operations: None, + }), + text_document_sync: Some(TextDocumentSyncCapability::Options( + TextDocumentSyncOptions { + open_close: Some(true), + change: Some(TextDocumentSyncKind::FULL), + ..Default::default() + }, + )), + document_formatting_provider: Some(OneOf::Left(true)), + code_action_provider: Some(CodeActionProviderCapability::Options( + CodeActionOptions { + code_action_kinds: Some(vec![CodeActionKind::QUICKFIX]), + ..Default::default() + }, + )), + ..ServerCapabilities::default() + }, + }) + } + + async fn initialized(&self, _: InitializedParams) { + self.client + .log_message(MessageType::INFO, "server initialized!\n") + .await; + } + + async fn shutdown(&self) -> Result<()> { + self.client + .log_message(MessageType::INFO, "server shutdown!\n") + .await; + Ok(()) + } + + async fn did_open(&self, params: DidOpenTextDocumentParams) { + let DidOpenTextDocumentParams { text_document } = params; + + if self.is_ignored(&text_document.uri) { + return; + } + + self.client + .log_message( + MessageType::INFO, + format!( + "did_open {}, workdir: {:?}\n", + text_document.uri, + self.work_dir() + ), + ) + .await; + + self.upsert_document(Arc::new(text_document.clone())); + + self.lint_document(&text_document).await; + } + + async fn did_close(&self, params: DidCloseTextDocumentParams) { + let DidCloseTextDocumentParams { text_document } = params; + + if self.is_ignored(&text_document.uri) { + return; + } + + self.client + .log_message( + MessageType::INFO, + format!("did_close {}\n", text_document.uri), + ) + .await; + + self.remove_document(&text_document.uri); + self.clear_diagnostics(&text_document.uri).await; + } + + async fn did_change(&self, params: DidChangeTextDocumentParams) { + let DidChangeTextDocumentParams { + text_document, + content_changes, + } = params; + let VersionedTextDocumentIdentifier { uri, version } = text_document; + + if self.is_ignored(&uri) { + return; + } + + self.client + .log_message(MessageType::INFO, format!("did_change {}\n", uri)) + .await; + + assert_eq!(content_changes.len(), 1); + let change = content_changes.into_iter().next().unwrap(); + assert!(change.range.is_none()); + + let updated_doc = + TextDocumentItem::new(uri.clone(), "".to_string(), version, change.text.clone()); + self.upsert_document(Arc::new(updated_doc.clone())); + + self.lint_document(&updated_doc).await; + } + + async fn did_save(&self, params: DidSaveTextDocumentParams) { + let DidSaveTextDocumentParams { text_document, .. } = params; + self.client + .log_message( + MessageType::INFO, + format!("did_save {}\n", text_document.uri), + ) + .await; + + if text_document.uri.path().ends_with(DEFAULT_CONFIG_FILE) + || text_document.uri.path().ends_with(DEFAULT_IGNORE_FILE) + { + self.clear_all_diagnostic().await; + self.client + .log_message(MessageType::INFO, "reload config\n") + .await; + self.reload_config(); + } + } + + async fn formatting(&self, params: DocumentFormattingParams) -> Result>> { + let DocumentFormattingParams { text_document, .. } = params; + + if self.is_ignored(&text_document.uri) { + return Ok(None); + } + + self.client + .log_message( + MessageType::INFO, + format!("formatting {}\n", text_document.uri), + ) + .await; + + if let Some(document) = self.get_document(&text_document.uri) { + self.clear_diagnostics(&text_document.uri).await; + let input = document.text.as_str(); + + let result = autocorrect::format_for(input, &document.uri.path()); + let range = Range::new( + Position::new(0, 0), + Position { + line: u32::max_value(), + character: u32::max_value(), + }, + ); + return Ok(Some(vec![TextEdit::new(range, result.out)])); + } + + Ok(None) + } + + async fn code_action(&self, params: CodeActionParams) -> Result> { + let CodeActionParams { + text_document, + context, + .. + } = params; + + if self.is_ignored(&text_document.uri) { + return Ok(None); + } + + self.client + .log_message( + MessageType::INFO, + format!("code_action {}\n", text_document.uri), + ) + .await; + + let mut response = CodeActionResponse::new(); + for diagnostic in context.diagnostics.iter() { + let action = CodeAction { + title: diagnostic.source.clone().unwrap_or("AutoCorrect".into()), + kind: Some(CodeActionKind::QUICKFIX), + diagnostics: Some(vec![diagnostic.clone()]), + edit: Some(WorkspaceEdit { + changes: Some( + vec![( + text_document.uri.clone(), + vec![TextEdit { + range: diagnostic.range.clone(), + new_text: diagnostic.message.clone(), + }], + )] + .into_iter() + .collect(), + ), + document_changes: None, + change_annotations: None, + }), + command: None, + is_preferred: Some(true), + disabled: None, + data: None, + }; + response.push(CodeActionOrCommand::CodeAction(action)); + } + return Ok(Some(response)); + } +} + +pub async fn start() { + let stdin = tokio::io::stdin(); + let stdout = tokio::io::stdout(); + + let (service, socket) = LspService::new(|client| { + return Backend { + client, + work_dir: RwLock::new(PathBuf::new()), + documents: RwLock::new(HashMap::new()), + ignorer: RwLock::new(None), + }; + }); + Server::new(stdin, stdout, socket).serve(service).await; +} diff --git a/autocorrect/Cargo.toml b/autocorrect/Cargo.toml index 498b0063..17974caf 100644 --- a/autocorrect/Cargo.toml +++ b/autocorrect/Cargo.toml @@ -15,7 +15,7 @@ name = "autocorrect" path = "src/lib.rs" [dependencies] -autocorrect-derive = {version = "0.3.0", path = "../autocorrect-derive"} +autocorrect-derive = { version = "0.3.0", path = "../autocorrect-derive" } diff = "0.1.13" ignore = "0.4" lazy_static = "1.4.0" @@ -23,8 +23,8 @@ owo-colors = "3" pest = "2.6.1" pest_derive = "2.6.1" regex = "1" -serde = {version = "1", features = ["derive"]} -serde_json = "1.0.83" +serde.workspace = true +serde_json.workspace = true serde_repr = "0.1" serde_yaml = "0.9.9" diff --git a/autocorrect/src/lib.rs b/autocorrect/src/lib.rs index ba113ccf..919e2bce 100644 --- a/autocorrect/src/lib.rs +++ b/autocorrect/src/lib.rs @@ -150,7 +150,7 @@ pub mod ignorer; pub use code::{format_for, get_file_extension, is_support_type, lint_for}; pub use config::Config; pub use format::*; -pub use result::{json, rdjson, FormatResult, LineResult, LintResult}; +pub use result::{json, rdjson, FormatResult, LineResult, LintResult, Severity}; pub use rule::{halfwidth, spellcheck}; #[cfg(test)]