From a90b9a5872c9c916733816e1e0d8c95cb09bfcba Mon Sep 17 00:00:00 2001 From: Andy Russell Date: Tue, 4 May 2021 17:13:51 -0400 Subject: implement range formatting --- crates/rust-analyzer/src/caps.rs | 4 +- crates/rust-analyzer/src/config.rs | 13 +- crates/rust-analyzer/src/handlers.rs | 239 ++++++++++++++++++++-------------- crates/rust-analyzer/src/main_loop.rs | 1 + docs/user/generated_config.adoc | 7 + editors/code/package.json | 5 + 6 files changed, 167 insertions(+), 102 deletions(-) diff --git a/crates/rust-analyzer/src/caps.rs b/crates/rust-analyzer/src/caps.rs index b2317618a..4d88932ca 100644 --- a/crates/rust-analyzer/src/caps.rs +++ b/crates/rust-analyzer/src/caps.rs @@ -1,4 +1,4 @@ -//! Advertizes the capabilities of the LSP Server. +//! Advertises the capabilities of the LSP Server. use std::env; use lsp_types::{ @@ -54,7 +54,7 @@ pub fn server_capabilities(client_caps: &ClientCapabilities) -> ServerCapabiliti code_action_provider: Some(code_action_capabilities(client_caps)), code_lens_provider: Some(CodeLensOptions { resolve_provider: Some(true) }), document_formatting_provider: Some(OneOf::Left(true)), - document_range_formatting_provider: None, + document_range_formatting_provider: Some(OneOf::Left(true)), document_on_type_formatting_provider: Some(DocumentOnTypeFormattingOptions { first_trigger_character: "=".to_string(), more_trigger_character: Some(vec![".".to_string(), ">".to_string(), "{".to_string()]), diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index b700d025f..2e99db36c 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -218,6 +218,10 @@ config_data! { /// Advanced option, fully override the command rust-analyzer uses for /// formatting. rustfmt_overrideCommand: Option> = "null", + /// Enables the use of rustfmt's unstable range formatting command for the + /// `textDocument/rangeFormatting` request. The rustfmt option is unstable and only + /// available on a nightly build. + rustfmt_enableRangeFormatting: bool = "false", /// Workspace symbol search scope. workspace_symbol_search_scope: WorskpaceSymbolSearchScopeDef = "\"workspace\"", @@ -304,7 +308,7 @@ pub struct NotificationsConfig { #[derive(Debug, Clone)] pub enum RustfmtConfig { - Rustfmt { extra_args: Vec }, + Rustfmt { extra_args: Vec, enable_range_formatting: bool }, CustomCommand { command: String, args: Vec }, } @@ -569,9 +573,10 @@ impl Config { let command = args.remove(0); RustfmtConfig::CustomCommand { command, args } } - Some(_) | None => { - RustfmtConfig::Rustfmt { extra_args: self.data.rustfmt_extraArgs.clone() } - } + Some(_) | None => RustfmtConfig::Rustfmt { + extra_args: self.data.rustfmt_extraArgs.clone(), + enable_range_formatting: self.data.rustfmt_enableRangeFormatting, + }, } } pub fn flycheck(&self) -> Option { diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs index aa12fd94b..53161eb3e 100644 --- a/crates/rust-analyzer/src/handlers.rs +++ b/crates/rust-analyzer/src/handlers.rs @@ -27,7 +27,7 @@ use lsp_types::{ }; use project_model::TargetKind; use serde::{Deserialize, Serialize}; -use serde_json::to_value; +use serde_json::{json, to_value}; use stdx::format_to; use syntax::{algo, ast, AstNode, TextRange, TextSize}; @@ -946,104 +946,17 @@ pub(crate) fn handle_formatting( params: DocumentFormattingParams, ) -> Result>> { let _p = profile::span("handle_formatting"); - let file_id = from_proto::file_id(&snap, ¶ms.text_document.uri)?; - let file = snap.analysis.file_text(file_id)?; - let crate_ids = snap.analysis.crate_for(file_id)?; - - let line_index = snap.file_line_index(file_id)?; - - let mut rustfmt = match snap.config.rustfmt() { - RustfmtConfig::Rustfmt { extra_args } => { - let mut cmd = process::Command::new(toolchain::rustfmt()); - cmd.args(extra_args); - // try to chdir to the file so we can respect `rustfmt.toml` - // FIXME: use `rustfmt --config-path` once - // https://github.com/rust-lang/rustfmt/issues/4660 gets fixed - match params.text_document.uri.to_file_path() { - Ok(mut path) => { - // pop off file name - if path.pop() && path.is_dir() { - cmd.current_dir(path); - } - } - Err(_) => { - log::error!( - "Unable to get file path for {}, rustfmt.toml might be ignored", - params.text_document.uri - ); - } - } - if let Some(&crate_id) = crate_ids.first() { - // Assume all crates are in the same edition - let edition = snap.analysis.crate_edition(crate_id)?; - cmd.arg("--edition"); - cmd.arg(edition.to_string()); - } - cmd - } - RustfmtConfig::CustomCommand { command, args } => { - let mut cmd = process::Command::new(command); - cmd.args(args); - cmd - } - }; - let mut rustfmt = - rustfmt.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn()?; - - rustfmt.stdin.as_mut().unwrap().write_all(file.as_bytes())?; - - let output = rustfmt.wait_with_output()?; - let captured_stdout = String::from_utf8(output.stdout)?; - let captured_stderr = String::from_utf8(output.stderr).unwrap_or_default(); - - if !output.status.success() { - let rustfmt_not_installed = - captured_stderr.contains("not installed") || captured_stderr.contains("not available"); - - return match output.status.code() { - Some(1) if !rustfmt_not_installed => { - // While `rustfmt` doesn't have a specific exit code for parse errors this is the - // likely cause exiting with 1. Most Language Servers swallow parse errors on - // formatting because otherwise an error is surfaced to the user on top of the - // syntax error diagnostics they're already receiving. This is especially jarring - // if they have format on save enabled. - log::info!("rustfmt exited with status 1, assuming parse error and ignoring"); - Ok(None) - } - _ => { - // Something else happened - e.g. `rustfmt` is missing or caught a signal - Err(LspError::new( - -32900, - format!( - r#"rustfmt exited with: - Status: {} - stdout: {} - stderr: {}"#, - output.status, captured_stdout, captured_stderr, - ), - ) - .into()) - } - }; - } + run_rustfmt(&snap, params.text_document, None) +} - let (new_text, new_line_endings) = LineEndings::normalize(captured_stdout); +pub(crate) fn handle_range_formatting( + snap: GlobalStateSnapshot, + params: lsp_types::DocumentRangeFormattingParams, +) -> Result>> { + let _p = profile::span("handle_range_formatting"); - if line_index.endings != new_line_endings { - // If line endings are different, send the entire file. - // Diffing would not work here, as the line endings might be the only - // difference. - Ok(Some(to_proto::text_edit_vec( - &line_index, - TextEdit::replace(TextRange::up_to(TextSize::of(&*file)), new_text), - ))) - } else if *file == new_text { - // The document is already formatted correctly -- no edits needed. - Ok(None) - } else { - Ok(Some(to_proto::text_edit_vec(&line_index, diff(&file, &new_text)))) - } + run_rustfmt(&snap, params.text_document, Some(params.range)) } pub(crate) fn handle_code_action( @@ -1666,6 +1579,140 @@ fn should_skip_target(runnable: &Runnable, cargo_spec: Option<&CargoTargetSpec>) } } +fn run_rustfmt( + snap: &GlobalStateSnapshot, + text_document: TextDocumentIdentifier, + range: Option, +) -> Result>> { + let file_id = from_proto::file_id(&snap, &text_document.uri)?; + let file = snap.analysis.file_text(file_id)?; + let crate_ids = snap.analysis.crate_for(file_id)?; + + let line_index = snap.file_line_index(file_id)?; + + let mut rustfmt = match snap.config.rustfmt() { + RustfmtConfig::Rustfmt { extra_args, enable_range_formatting } => { + let mut cmd = process::Command::new(toolchain::rustfmt()); + cmd.args(extra_args); + // try to chdir to the file so we can respect `rustfmt.toml` + // FIXME: use `rustfmt --config-path` once + // https://github.com/rust-lang/rustfmt/issues/4660 gets fixed + match text_document.uri.to_file_path() { + Ok(mut path) => { + // pop off file name + if path.pop() && path.is_dir() { + cmd.current_dir(path); + } + } + Err(_) => { + log::error!( + "Unable to get file path for {}, rustfmt.toml might be ignored", + text_document.uri + ); + } + } + if let Some(&crate_id) = crate_ids.first() { + // Assume all crates are in the same edition + let edition = snap.analysis.crate_edition(crate_id)?; + cmd.arg("--edition"); + cmd.arg(edition.to_string()); + } + + if let Some(range) = range { + if !enable_range_formatting { + return Err(LspError::new( + ErrorCode::InvalidRequest as i32, + String::from( + "rustfmt range formatting is unstable. \ + Opt-in by using a nightly build of rustfmt and setting \ + `rustfmt.enableRangeFormatting` to true in your LSP configuration", + ), + ) + .into()); + } + + let frange = from_proto::file_range(&snap, text_document.clone(), range)?; + let start_line = line_index.index.line_col(frange.range.start()).line; + let end_line = line_index.index.line_col(frange.range.end()).line; + + cmd.arg("--unstable-features"); + cmd.arg("--file-lines"); + cmd.arg( + json!([{ + "file": "stdin", + "range": [start_line, end_line] + }]) + .to_string(), + ); + } + + cmd + } + RustfmtConfig::CustomCommand { command, args } => { + let mut cmd = process::Command::new(command); + cmd.args(args); + cmd + } + }; + + let mut rustfmt = + rustfmt.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn()?; + + rustfmt.stdin.as_mut().unwrap().write_all(file.as_bytes())?; + + let output = rustfmt.wait_with_output()?; + let captured_stdout = String::from_utf8(output.stdout)?; + let captured_stderr = String::from_utf8(output.stderr).unwrap_or_default(); + + if !output.status.success() { + let rustfmt_not_installed = + captured_stderr.contains("not installed") || captured_stderr.contains("not available"); + + return match output.status.code() { + Some(1) if !rustfmt_not_installed => { + // While `rustfmt` doesn't have a specific exit code for parse errors this is the + // likely cause exiting with 1. Most Language Servers swallow parse errors on + // formatting because otherwise an error is surfaced to the user on top of the + // syntax error diagnostics they're already receiving. This is especially jarring + // if they have format on save enabled. + log::info!("rustfmt exited with status 1, assuming parse error and ignoring"); + Ok(None) + } + _ => { + // Something else happened - e.g. `rustfmt` is missing or caught a signal + Err(LspError::new( + -32900, + format!( + r#"rustfmt exited with: + Status: {} + stdout: {} + stderr: {}"#, + output.status, captured_stdout, captured_stderr, + ), + ) + .into()) + } + }; + } + + let (new_text, new_line_endings) = LineEndings::normalize(captured_stdout); + + if line_index.endings != new_line_endings { + // If line endings are different, send the entire file. + // Diffing would not work here, as the line endings might be the only + // difference. + Ok(Some(to_proto::text_edit_vec( + &line_index, + TextEdit::replace(TextRange::up_to(TextSize::of(&*file)), new_text), + ))) + } else if *file == new_text { + // The document is already formatted correctly -- no edits needed. + Ok(None) + } else { + Ok(Some(to_proto::text_edit_vec(&line_index, diff(&file, &new_text)))) + } +} + #[derive(Debug, Serialize, Deserialize)] struct CompletionResolveData { position: lsp_types::TextDocumentPositionParams, diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs index f837b89dd..e202da621 100644 --- a/crates/rust-analyzer/src/main_loop.rs +++ b/crates/rust-analyzer/src/main_loop.rs @@ -542,6 +542,7 @@ impl GlobalState { .on::(handlers::handle_rename) .on::(handlers::handle_references) .on::(handlers::handle_formatting) + .on::(handlers::handle_range_formatting) .on::(handlers::handle_document_highlight) .on::(handlers::handle_call_hierarchy_prepare) .on::( diff --git a/docs/user/generated_config.adoc b/docs/user/generated_config.adoc index c02bab7cc..f3da82feb 100644 --- a/docs/user/generated_config.adoc +++ b/docs/user/generated_config.adoc @@ -346,6 +346,13 @@ Additional arguments to `rustfmt`. Advanced option, fully override the command rust-analyzer uses for formatting. -- +[[rust-analyzer.rustfmt.enableRangeFormatting]]rust-analyzer.rustfmt.enableRangeFormatting (default: `false`):: ++ +-- +Enables the use of rustfmt's unstable range formatting command for the +`textDocument/rangeFormatting` request. The rustfmt option is unstable and only +available on a nightly build. +-- [[rust-analyzer.workspace.symbol.search.scope]]rust-analyzer.workspace.symbol.search.scope (default: `"workspace"`):: + -- diff --git a/editors/code/package.json b/editors/code/package.json index 17d9281ff..05cbccf94 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -795,6 +795,11 @@ "type": "string" } }, + "rust-analyzer.rustfmt.enableRangeFormatting": { + "markdownDescription": "Enables the use of rustfmt's unstable range formatting command for the\n`textDocument/rangeFormatting` request. The rustfmt option is unstable and only\navailable on a nightly build.", + "default": false, + "type": "boolean" + }, "rust-analyzer.workspace.symbol.search.scope": { "markdownDescription": "Workspace symbol search scope.", "default": "workspace", -- cgit v1.2.3