diff --git a/crates/starpls/src/commands/server.rs b/crates/starpls/src/commands/server.rs index 7dc07e9..879cd4d 100644 --- a/crates/starpls/src/commands/server.rs +++ b/crates/starpls/src/commands/server.rs @@ -62,6 +62,7 @@ impl ServerCommand { }), declaration_provider: Some(DeclarationCapability::Simple(true)), definition_provider: Some(OneOf::Left(true)), + document_formatting_provider: Some(OneOf::Left(true)), document_symbol_provider: Some(OneOf::Left(true)), hover_provider: Some(HoverProviderCapability::Simple(true)), references_provider: Some(OneOf::Left(true)), diff --git a/crates/starpls/src/config.rs b/crates/starpls/src/config.rs index c332916..6e0cc05 100644 --- a/crates/starpls/src/config.rs +++ b/crates/starpls/src/config.rs @@ -1,9 +1,24 @@ use lsp_types::ClientCapabilities; +use serde::Deserialize; +use serde_json::Value; use crate::commands::server::ServerCommand; +#[derive(Deserialize, Default)] +#[serde(default)] +pub(crate) struct BuildifierConfig { + pub(crate) path: Option, + pub(crate) args: Vec, +} + +#[derive(Deserialize)] +struct InitializationOptions { + buildifier: Option, +} + #[derive(Default)] pub(crate) struct ServerConfig { + pub(crate) buildifier: Option, pub(crate) args: ServerCommand, pub(crate) caps: ClientCapabilities, } @@ -15,6 +30,13 @@ macro_rules! try_or_default { } impl ServerConfig { + pub(crate) fn from_json(value: Value) -> Self { + let mut config = ServerConfig::default(); + if let Ok(opts) = serde_json::from_value::(value) { + config.buildifier = opts.buildifier; + } + config + } pub(crate) fn has_text_document_definition_link_support(&self) -> bool { try_or_default!(self.caps.text_document.as_ref()?.definition?.link_support) } diff --git a/crates/starpls/src/event_loop.rs b/crates/starpls/src/event_loop.rs index 329d708..470c1c9 100644 --- a/crates/starpls/src/event_loop.rs +++ b/crates/starpls/src/event_loop.rs @@ -77,10 +77,10 @@ pub fn process_connection( initialize_params: InitializeParams, ) -> anyhow::Result<()> { debug!("initializing state and starting event loop"); - let config = ServerConfig { - args, - caps: initialize_params.capabilities, - }; + let mut config = + ServerConfig::from_json(initialize_params.initialization_options.unwrap_or_default()); + config.args = args; + config.caps = initialize_params.capabilities; let server = Server::new(connection, config)?; server.run() } @@ -218,6 +218,7 @@ impl Server { .on::(requests::hover) .on::(requests::find_references) .on::(requests::signature_help) + .on::(requests::formatting) .finish(); } diff --git a/crates/starpls/src/handlers/requests.rs b/crates/starpls/src/handlers/requests.rs index 08d676f..cacad34 100644 --- a/crates/starpls/src/handlers/requests.rs +++ b/crates/starpls/src/handlers/requests.rs @@ -1,3 +1,5 @@ +use std::io::Write; + use anyhow::Ok; use starpls_ide::CompletionItemKind; use starpls_ide::CompletionMode::InsertText; @@ -287,6 +289,76 @@ pub(crate) fn document_symbols( })) } +pub(crate) fn formatting( + snapshot: &ServerSnapshot, + params: lsp_types::DocumentFormattingParams, +) -> anyhow::Result>> { + let path = path_buf_from_url(¶ms.text_document.uri)?; + let file_id = try_opt!(snapshot.document_manager.read().lookup_by_path_buf(&path)); + let line_index = try_opt!(snapshot.analysis_snapshot.line_index(file_id)?); + let default_buildifier_config; + let buildifier = match &snapshot.config.buildifier { + Some(config) => config, + None => { + default_buildifier_config = Default::default(); + &default_buildifier_config + } + }; + + // Read the file's contents. + let contents = try_opt!(snapshot.analysis_snapshot.file_contents(file_id)?); + + // Spawn buildifier. + let mut command = + std::process::Command::new(buildifier.path.as_deref().unwrap_or("buildifier")); + command.args(&buildifier.args); + + // Set the `--type` argument based on the file extension. + let file_name = path.file_name().and_then(|name| name.to_str()); + let file_type = match file_name { + Some("BUILD") | Some("BUILD.bazel") => "build", + Some("WORKSPACE") | Some("WORKSPACE.bazel") => "workspace", + Some("MODULE.bazel") => "module", + _ => match path.extension().and_then(|ext| ext.to_str()) { + Some("bzl") => "bzl", + _ => "default", + }, + }; + command.args(["--type", file_type]); + + command.arg("-"); + let mut child = command + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .spawn()?; + + // Write the file's contents to stdin. + let mut stdin = child.stdin.take().unwrap(); + stdin.write_all(contents.as_bytes())?; + drop(stdin); + + // Read the formatted output from stdout. + let output = child.wait_with_output()?; + if !output.status.success() { + return Ok(None); + } + let new_text = String::from_utf8(output.stdout)?; + + // Replace the entire document with the formatted text. + let range = lsp_types::Range { + start: lsp_types::Position { + line: 0, + character: 0, + }, + end: lsp_types::Position { + line: line_index.len().into(), + character: 0, + }, + }; + + Ok(Some(vec![lsp_types::TextEdit { range, new_text }])) +} + fn to_markup_doc(doc: String) -> lsp_types::Documentation { lsp_types::Documentation::MarkupContent(lsp_types::MarkupContent { kind: lsp_types::MarkupKind::Markdown, diff --git a/crates/starpls_ide/src/lib.rs b/crates/starpls_ide/src/lib.rs index b5f8d10..6136199 100644 --- a/crates/starpls_ide/src/lib.rs +++ b/crates/starpls_ide/src/lib.rs @@ -398,6 +398,10 @@ impl AnalysisSnapshot { self.query(|db| signature_help::signature_help(db, pos)) } + pub fn file_contents(&self, file_id: FileId) -> Cancellable> { + self.query(|db| db.get_file(file_id).map(|file| file.contents(db).clone())) + } + /// Helper method to handle Salsa cancellations. fn query<'a, F, T>(&'a self, f: F) -> Cancellable where