diff --git a/Cargo.lock b/Cargo.lock index 9f9b9e45..5cb4e5bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -310,7 +310,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.100", "unicode-xid", ] @@ -375,6 +375,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +[[package]] +name = "fluent-uri" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "full_moon" version = "1.2.0" @@ -574,6 +583,42 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "lsp-server" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9462c4dc73e17f971ec1f171d44bfffb72e65a130117233388a0ebc7ec5656f9" +dependencies = [ + "crossbeam-channel", + "log", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "lsp-textdocument" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d564d595f4e3dcd3c071bf472dbd2cac53bc3665ae7222d2abfecd18feaed2c" +dependencies = [ + "lsp-types", + "serde_json", +] + +[[package]] +name = "lsp-types" +version = "0.97.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53353550a17c04ac46c585feb189c2db82154fc84b79c7a66c96c2c644f66071" +dependencies = [ + "bitflags 1.3.2", + "fluent-uri", + "serde", + "serde_json", + "serde_repr", +] + [[package]] name = "memchr" version = "2.7.1" @@ -704,9 +749,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] @@ -814,22 +859,22 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.196" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.196" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.100", ] [[package]] @@ -843,6 +888,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "serde_spanned" version = "0.6.5" @@ -896,7 +952,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.48", + "syn 2.0.100", ] [[package]] @@ -919,6 +975,9 @@ dependencies = [ "insta", "lazy_static", "log", + "lsp-server", + "lsp-textdocument", + "lsp-types", "num_cpus", "regex", "serde", @@ -944,9 +1003,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.48" +version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2", "quote", @@ -1004,7 +1063,7 @@ checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.100", ] [[package]] @@ -1124,7 +1183,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.100", "wasm-bindgen-shared", ] @@ -1146,7 +1205,7 @@ checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.100", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/Cargo.toml b/Cargo.toml index e3c19140..af4fe069 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,10 @@ thiserror = "1.0.49" threadpool = "1.8.1" toml = "0.8.1" +lsp-server = "0.7" +lsp-types = "0.97" +lsp-textdocument = "0.4.2" + [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen = { version = "0.2.81", optional = true } diff --git a/src/cli/lsp.rs b/src/cli/lsp.rs new file mode 100644 index 00000000..4e611da7 --- /dev/null +++ b/src/cli/lsp.rs @@ -0,0 +1,108 @@ +use lsp_server::{Connection, Message, RequestId, Response}; +use lsp_textdocument::TextDocuments; +use lsp_types::{ + request::{Formatting, RangeFormatting, Request}, + DocumentFormattingParams, OneOf, Position, Range, ServerCapabilities, + TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit, +}; + +use crate::{config::ConfigResolver, opt}; + +pub fn run(opt: opt::Opt) -> anyhow::Result<()> { + // Load the configuration + let opt_for_config_resolver = opt.clone(); + let mut config_resolver = ConfigResolver::new(&opt_for_config_resolver)?; + + let (connection, io_threads) = Connection::stdio(); + + let capabilities = ServerCapabilities { + document_formatting_provider: Some(OneOf::Left(true)), + text_document_sync: Some(TextDocumentSyncCapability::Kind( + TextDocumentSyncKind::INCREMENTAL, + )), + ..Default::default() + }; + + connection.initialize(serde_json::to_value(capabilities)?)?; + + let mut documents = TextDocuments::new(); + + for msg in &connection.receiver { + match msg { + Message::Request(req) => { + if connection.handle_shutdown(&req)? { + break; + } + + match req.method.as_str() { + Formatting::METHOD => { + let result = + match serde_json::from_value::(req.params) { + Ok(params) => { + let res = handle_formatting( + req.id.clone(), + params, + &mut config_resolver, + &documents, + ); + + res.unwrap_or(Response::new_ok(req.id, serde_json::Value::Null)) + } + Err(err) => Response::new_err(req.id, 1, err.to_string()), + }; + + connection.sender.send(Message::Response(result))?; + } + RangeFormatting::METHOD => { + // TODO: Would be cool to support this in the future + } + _ => {} + } + } + Message::Response(_) => {} + Message::Notification(notification) => { + documents.listen(notification.method.as_str(), ¬ification.params); + } + } + } + + io_threads.join()?; + + Ok(()) +} + +fn handle_formatting( + id: RequestId, + params: DocumentFormattingParams, + config_resolver: &mut ConfigResolver, + documents: &TextDocuments, +) -> Option { + let uri = params.text_document.uri; + let path = uri.path().as_str(); + + let src = documents.get_document_content(&uri, None)?; + + let config = config_resolver + .load_configuration(path.as_ref()) + .unwrap_or_default(); + + let new_text = + stylua_lib::format_code(src, config, None, stylua_lib::OutputVerification::None).ok()?; + + if new_text == src { + return None; + } + + // TODO: We can be smarter about this in the future, and update only the parts that changed + let edit = TextEdit { + range: Range { + start: Position::new(0, 0), + end: Position::new(u32::MAX, 0), + }, + new_text, + }; + + let edits: Vec = vec![edit]; + + Some(Response::new_ok(id, edits)) +} diff --git a/src/cli/main.rs b/src/cli/main.rs index 40450be9..d696d7dc 100644 --- a/src/cli/main.rs +++ b/src/cli/main.rs @@ -19,6 +19,7 @@ use stylua_lib::{format_code, Config, OutputVerification, Range}; use crate::config::find_ignore_file_path; mod config; +mod lsp; mod opt; mod output_diff; @@ -255,6 +256,11 @@ fn path_is_stylua_ignored(path: &Path, search_parent_directories: bool) -> Resul fn format(opt: opt::Opt) -> Result { debug!("resolved options: {:#?}", opt); + if opt.lsp { + lsp::run(opt)?; + return Ok(0); + } + if opt.files.is_empty() { bail!("no files provided"); } diff --git a/src/cli/opt.rs b/src/cli/opt.rs index 3769317f..bf96c1fa 100644 --- a/src/cli/opt.rs +++ b/src/cli/opt.rs @@ -108,6 +108,10 @@ pub struct Opt { /// Respect .styluaignore and glob matching for file paths provided directly to the tool #[structopt(long)] pub respect_ignores: bool, + + /// Run Stylua LSP server + #[structopt(long)] + pub lsp: bool, } #[derive(ArgEnum, Clone, Copy, Debug, PartialEq, Eq)]