Skip to content

Commit 835cf55

Browse files
bors[bot]euclio
andauthored
Merge #8767
8767: implement range formatting r=matklad a=euclio Fixes #7580. This PR implements the `textDocument/rangeFormatting` request using `rustfmt`'s `--file-lines` option. Still needs some tests. What I want to know is how I should handle the instability of the `--file-lines` option. It's still unstable in rustfmt, so it's only available on nightly, and needs a special flag to enable. Is there a way for `rust-analyzer` to detect if it's using nightly rustfmt, or for users to opt-in? Co-authored-by: Andy Russell <[email protected]>
2 parents b7414fa + a90b9a5 commit 835cf55

File tree

6 files changed

+167
-102
lines changed

6 files changed

+167
-102
lines changed

crates/rust-analyzer/src/caps.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//! Advertizes the capabilities of the LSP Server.
1+
//! Advertises the capabilities of the LSP Server.
22
use std::env;
33

44
use lsp_types::{
@@ -54,7 +54,7 @@ pub fn server_capabilities(client_caps: &ClientCapabilities) -> ServerCapabiliti
5454
code_action_provider: Some(code_action_capabilities(client_caps)),
5555
code_lens_provider: Some(CodeLensOptions { resolve_provider: Some(true) }),
5656
document_formatting_provider: Some(OneOf::Left(true)),
57-
document_range_formatting_provider: None,
57+
document_range_formatting_provider: Some(OneOf::Left(true)),
5858
document_on_type_formatting_provider: Some(DocumentOnTypeFormattingOptions {
5959
first_trigger_character: "=".to_string(),
6060
more_trigger_character: Some(vec![".".to_string(), ">".to_string(), "{".to_string()]),

crates/rust-analyzer/src/config.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,10 @@ config_data! {
218218
/// Advanced option, fully override the command rust-analyzer uses for
219219
/// formatting.
220220
rustfmt_overrideCommand: Option<Vec<String>> = "null",
221+
/// Enables the use of rustfmt's unstable range formatting command for the
222+
/// `textDocument/rangeFormatting` request. The rustfmt option is unstable and only
223+
/// available on a nightly build.
224+
rustfmt_enableRangeFormatting: bool = "false",
221225

222226
/// Workspace symbol search scope.
223227
workspace_symbol_search_scope: WorskpaceSymbolSearchScopeDef = "\"workspace\"",
@@ -305,7 +309,7 @@ pub struct NotificationsConfig {
305309

306310
#[derive(Debug, Clone)]
307311
pub enum RustfmtConfig {
308-
Rustfmt { extra_args: Vec<String> },
312+
Rustfmt { extra_args: Vec<String>, enable_range_formatting: bool },
309313
CustomCommand { command: String, args: Vec<String> },
310314
}
311315

@@ -584,9 +588,10 @@ impl Config {
584588
let command = args.remove(0);
585589
RustfmtConfig::CustomCommand { command, args }
586590
}
587-
Some(_) | None => {
588-
RustfmtConfig::Rustfmt { extra_args: self.data.rustfmt_extraArgs.clone() }
589-
}
591+
Some(_) | None => RustfmtConfig::Rustfmt {
592+
extra_args: self.data.rustfmt_extraArgs.clone(),
593+
enable_range_formatting: self.data.rustfmt_enableRangeFormatting,
594+
},
590595
}
591596
}
592597
pub fn flycheck(&self) -> Option<FlycheckConfig> {

crates/rust-analyzer/src/handlers.rs

Lines changed: 143 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use lsp_types::{
2727
};
2828
use project_model::TargetKind;
2929
use serde::{Deserialize, Serialize};
30-
use serde_json::to_value;
30+
use serde_json::{json, to_value};
3131
use stdx::format_to;
3232
use syntax::{algo, ast, AstNode, TextRange, TextSize};
3333

@@ -955,104 +955,17 @@ pub(crate) fn handle_formatting(
955955
params: DocumentFormattingParams,
956956
) -> Result<Option<Vec<lsp_types::TextEdit>>> {
957957
let _p = profile::span("handle_formatting");
958-
let file_id = from_proto::file_id(&snap, &params.text_document.uri)?;
959-
let file = snap.analysis.file_text(file_id)?;
960-
let crate_ids = snap.analysis.crate_for(file_id)?;
961-
962-
let line_index = snap.file_line_index(file_id)?;
963-
964-
let mut rustfmt = match snap.config.rustfmt() {
965-
RustfmtConfig::Rustfmt { extra_args } => {
966-
let mut cmd = process::Command::new(toolchain::rustfmt());
967-
cmd.args(extra_args);
968-
// try to chdir to the file so we can respect `rustfmt.toml`
969-
// FIXME: use `rustfmt --config-path` once
970-
// https://github.com/rust-lang/rustfmt/issues/4660 gets fixed
971-
match params.text_document.uri.to_file_path() {
972-
Ok(mut path) => {
973-
// pop off file name
974-
if path.pop() && path.is_dir() {
975-
cmd.current_dir(path);
976-
}
977-
}
978-
Err(_) => {
979-
log::error!(
980-
"Unable to get file path for {}, rustfmt.toml might be ignored",
981-
params.text_document.uri
982-
);
983-
}
984-
}
985-
if let Some(&crate_id) = crate_ids.first() {
986-
// Assume all crates are in the same edition
987-
let edition = snap.analysis.crate_edition(crate_id)?;
988-
cmd.arg("--edition");
989-
cmd.arg(edition.to_string());
990-
}
991-
cmd
992-
}
993-
RustfmtConfig::CustomCommand { command, args } => {
994-
let mut cmd = process::Command::new(command);
995-
cmd.args(args);
996-
cmd
997-
}
998-
};
999958

1000-
let mut rustfmt =
1001-
rustfmt.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn()?;
1002-
1003-
rustfmt.stdin.as_mut().unwrap().write_all(file.as_bytes())?;
1004-
1005-
let output = rustfmt.wait_with_output()?;
1006-
let captured_stdout = String::from_utf8(output.stdout)?;
1007-
let captured_stderr = String::from_utf8(output.stderr).unwrap_or_default();
1008-
1009-
if !output.status.success() {
1010-
let rustfmt_not_installed =
1011-
captured_stderr.contains("not installed") || captured_stderr.contains("not available");
1012-
1013-
return match output.status.code() {
1014-
Some(1) if !rustfmt_not_installed => {
1015-
// While `rustfmt` doesn't have a specific exit code for parse errors this is the
1016-
// likely cause exiting with 1. Most Language Servers swallow parse errors on
1017-
// formatting because otherwise an error is surfaced to the user on top of the
1018-
// syntax error diagnostics they're already receiving. This is especially jarring
1019-
// if they have format on save enabled.
1020-
log::info!("rustfmt exited with status 1, assuming parse error and ignoring");
1021-
Ok(None)
1022-
}
1023-
_ => {
1024-
// Something else happened - e.g. `rustfmt` is missing or caught a signal
1025-
Err(LspError::new(
1026-
-32900,
1027-
format!(
1028-
r#"rustfmt exited with:
1029-
Status: {}
1030-
stdout: {}
1031-
stderr: {}"#,
1032-
output.status, captured_stdout, captured_stderr,
1033-
),
1034-
)
1035-
.into())
1036-
}
1037-
};
1038-
}
959+
run_rustfmt(&snap, params.text_document, None)
960+
}
1039961

1040-
let (new_text, new_line_endings) = LineEndings::normalize(captured_stdout);
962+
pub(crate) fn handle_range_formatting(
963+
snap: GlobalStateSnapshot,
964+
params: lsp_types::DocumentRangeFormattingParams,
965+
) -> Result<Option<Vec<lsp_types::TextEdit>>> {
966+
let _p = profile::span("handle_range_formatting");
1041967

1042-
if line_index.endings != new_line_endings {
1043-
// If line endings are different, send the entire file.
1044-
// Diffing would not work here, as the line endings might be the only
1045-
// difference.
1046-
Ok(Some(to_proto::text_edit_vec(
1047-
&line_index,
1048-
TextEdit::replace(TextRange::up_to(TextSize::of(&*file)), new_text),
1049-
)))
1050-
} else if *file == new_text {
1051-
// The document is already formatted correctly -- no edits needed.
1052-
Ok(None)
1053-
} else {
1054-
Ok(Some(to_proto::text_edit_vec(&line_index, diff(&file, &new_text))))
1055-
}
968+
run_rustfmt(&snap, params.text_document, Some(params.range))
1056969
}
1057970

1058971
pub(crate) fn handle_code_action(
@@ -1675,6 +1588,140 @@ fn should_skip_target(runnable: &Runnable, cargo_spec: Option<&CargoTargetSpec>)
16751588
}
16761589
}
16771590

1591+
fn run_rustfmt(
1592+
snap: &GlobalStateSnapshot,
1593+
text_document: TextDocumentIdentifier,
1594+
range: Option<lsp_types::Range>,
1595+
) -> Result<Option<Vec<lsp_types::TextEdit>>> {
1596+
let file_id = from_proto::file_id(&snap, &text_document.uri)?;
1597+
let file = snap.analysis.file_text(file_id)?;
1598+
let crate_ids = snap.analysis.crate_for(file_id)?;
1599+
1600+
let line_index = snap.file_line_index(file_id)?;
1601+
1602+
let mut rustfmt = match snap.config.rustfmt() {
1603+
RustfmtConfig::Rustfmt { extra_args, enable_range_formatting } => {
1604+
let mut cmd = process::Command::new(toolchain::rustfmt());
1605+
cmd.args(extra_args);
1606+
// try to chdir to the file so we can respect `rustfmt.toml`
1607+
// FIXME: use `rustfmt --config-path` once
1608+
// https://github.com/rust-lang/rustfmt/issues/4660 gets fixed
1609+
match text_document.uri.to_file_path() {
1610+
Ok(mut path) => {
1611+
// pop off file name
1612+
if path.pop() && path.is_dir() {
1613+
cmd.current_dir(path);
1614+
}
1615+
}
1616+
Err(_) => {
1617+
log::error!(
1618+
"Unable to get file path for {}, rustfmt.toml might be ignored",
1619+
text_document.uri
1620+
);
1621+
}
1622+
}
1623+
if let Some(&crate_id) = crate_ids.first() {
1624+
// Assume all crates are in the same edition
1625+
let edition = snap.analysis.crate_edition(crate_id)?;
1626+
cmd.arg("--edition");
1627+
cmd.arg(edition.to_string());
1628+
}
1629+
1630+
if let Some(range) = range {
1631+
if !enable_range_formatting {
1632+
return Err(LspError::new(
1633+
ErrorCode::InvalidRequest as i32,
1634+
String::from(
1635+
"rustfmt range formatting is unstable. \
1636+
Opt-in by using a nightly build of rustfmt and setting \
1637+
`rustfmt.enableRangeFormatting` to true in your LSP configuration",
1638+
),
1639+
)
1640+
.into());
1641+
}
1642+
1643+
let frange = from_proto::file_range(&snap, text_document.clone(), range)?;
1644+
let start_line = line_index.index.line_col(frange.range.start()).line;
1645+
let end_line = line_index.index.line_col(frange.range.end()).line;
1646+
1647+
cmd.arg("--unstable-features");
1648+
cmd.arg("--file-lines");
1649+
cmd.arg(
1650+
json!([{
1651+
"file": "stdin",
1652+
"range": [start_line, end_line]
1653+
}])
1654+
.to_string(),
1655+
);
1656+
}
1657+
1658+
cmd
1659+
}
1660+
RustfmtConfig::CustomCommand { command, args } => {
1661+
let mut cmd = process::Command::new(command);
1662+
cmd.args(args);
1663+
cmd
1664+
}
1665+
};
1666+
1667+
let mut rustfmt =
1668+
rustfmt.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn()?;
1669+
1670+
rustfmt.stdin.as_mut().unwrap().write_all(file.as_bytes())?;
1671+
1672+
let output = rustfmt.wait_with_output()?;
1673+
let captured_stdout = String::from_utf8(output.stdout)?;
1674+
let captured_stderr = String::from_utf8(output.stderr).unwrap_or_default();
1675+
1676+
if !output.status.success() {
1677+
let rustfmt_not_installed =
1678+
captured_stderr.contains("not installed") || captured_stderr.contains("not available");
1679+
1680+
return match output.status.code() {
1681+
Some(1) if !rustfmt_not_installed => {
1682+
// While `rustfmt` doesn't have a specific exit code for parse errors this is the
1683+
// likely cause exiting with 1. Most Language Servers swallow parse errors on
1684+
// formatting because otherwise an error is surfaced to the user on top of the
1685+
// syntax error diagnostics they're already receiving. This is especially jarring
1686+
// if they have format on save enabled.
1687+
log::info!("rustfmt exited with status 1, assuming parse error and ignoring");
1688+
Ok(None)
1689+
}
1690+
_ => {
1691+
// Something else happened - e.g. `rustfmt` is missing or caught a signal
1692+
Err(LspError::new(
1693+
-32900,
1694+
format!(
1695+
r#"rustfmt exited with:
1696+
Status: {}
1697+
stdout: {}
1698+
stderr: {}"#,
1699+
output.status, captured_stdout, captured_stderr,
1700+
),
1701+
)
1702+
.into())
1703+
}
1704+
};
1705+
}
1706+
1707+
let (new_text, new_line_endings) = LineEndings::normalize(captured_stdout);
1708+
1709+
if line_index.endings != new_line_endings {
1710+
// If line endings are different, send the entire file.
1711+
// Diffing would not work here, as the line endings might be the only
1712+
// difference.
1713+
Ok(Some(to_proto::text_edit_vec(
1714+
&line_index,
1715+
TextEdit::replace(TextRange::up_to(TextSize::of(&*file)), new_text),
1716+
)))
1717+
} else if *file == new_text {
1718+
// The document is already formatted correctly -- no edits needed.
1719+
Ok(None)
1720+
} else {
1721+
Ok(Some(to_proto::text_edit_vec(&line_index, diff(&file, &new_text))))
1722+
}
1723+
}
1724+
16781725
#[derive(Debug, Serialize, Deserialize)]
16791726
struct CompletionResolveData {
16801727
position: lsp_types::TextDocumentPositionParams,

crates/rust-analyzer/src/main_loop.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,7 @@ impl GlobalState {
543543
.on::<lsp_types::request::Rename>(handlers::handle_rename)
544544
.on::<lsp_types::request::References>(handlers::handle_references)
545545
.on::<lsp_types::request::Formatting>(handlers::handle_formatting)
546+
.on::<lsp_types::request::RangeFormatting>(handlers::handle_range_formatting)
546547
.on::<lsp_types::request::DocumentHighlightRequest>(handlers::handle_document_highlight)
547548
.on::<lsp_types::request::CallHierarchyPrepare>(handlers::handle_call_hierarchy_prepare)
548549
.on::<lsp_types::request::CallHierarchyIncomingCalls>(

docs/user/generated_config.adoc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,13 @@ Additional arguments to `rustfmt`.
346346
Advanced option, fully override the command rust-analyzer uses for
347347
formatting.
348348
--
349+
[[rust-analyzer.rustfmt.enableRangeFormatting]]rust-analyzer.rustfmt.enableRangeFormatting (default: `false`)::
350+
+
351+
--
352+
Enables the use of rustfmt's unstable range formatting command for the
353+
`textDocument/rangeFormatting` request. The rustfmt option is unstable and only
354+
available on a nightly build.
355+
--
349356
[[rust-analyzer.workspace.symbol.search.scope]]rust-analyzer.workspace.symbol.search.scope (default: `"workspace"`)::
350357
+
351358
--

editors/code/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,11 @@
795795
"type": "string"
796796
}
797797
},
798+
"rust-analyzer.rustfmt.enableRangeFormatting": {
799+
"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.",
800+
"default": false,
801+
"type": "boolean"
802+
},
798803
"rust-analyzer.workspace.symbol.search.scope": {
799804
"markdownDescription": "Workspace symbol search scope.",
800805
"default": "workspace",

0 commit comments

Comments
 (0)