Skip to content

Commit a90b9a5

Browse files
committed
implement range formatting
1 parent 1605488 commit a90b9a5

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\"",
@@ -304,7 +308,7 @@ pub struct NotificationsConfig {
304308

305309
#[derive(Debug, Clone)]
306310
pub enum RustfmtConfig {
307-
Rustfmt { extra_args: Vec<String> },
311+
Rustfmt { extra_args: Vec<String>, enable_range_formatting: bool },
308312
CustomCommand { command: String, args: Vec<String> },
309313
}
310314

@@ -569,9 +573,10 @@ impl Config {
569573
let command = args.remove(0);
570574
RustfmtConfig::CustomCommand { command, args }
571575
}
572-
Some(_) | None => {
573-
RustfmtConfig::Rustfmt { extra_args: self.data.rustfmt_extraArgs.clone() }
574-
}
576+
Some(_) | None => RustfmtConfig::Rustfmt {
577+
extra_args: self.data.rustfmt_extraArgs.clone(),
578+
enable_range_formatting: self.data.rustfmt_enableRangeFormatting,
579+
},
575580
}
576581
}
577582
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

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

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

1031-
let (new_text, new_line_endings) = LineEndings::normalize(captured_stdout);
953+
pub(crate) fn handle_range_formatting(
954+
snap: GlobalStateSnapshot,
955+
params: lsp_types::DocumentRangeFormattingParams,
956+
) -> Result<Option<Vec<lsp_types::TextEdit>>> {
957+
let _p = profile::span("handle_range_formatting");
1032958

1033-
if line_index.endings != new_line_endings {
1034-
// If line endings are different, send the entire file.
1035-
// Diffing would not work here, as the line endings might be the only
1036-
// difference.
1037-
Ok(Some(to_proto::text_edit_vec(
1038-
&line_index,
1039-
TextEdit::replace(TextRange::up_to(TextSize::of(&*file)), new_text),
1040-
)))
1041-
} else if *file == new_text {
1042-
// The document is already formatted correctly -- no edits needed.
1043-
Ok(None)
1044-
} else {
1045-
Ok(Some(to_proto::text_edit_vec(&line_index, diff(&file, &new_text))))
1046-
}
959+
run_rustfmt(&snap, params.text_document, Some(params.range))
1047960
}
1048961

1049962
pub(crate) fn handle_code_action(
@@ -1666,6 +1579,140 @@ fn should_skip_target(runnable: &Runnable, cargo_spec: Option<&CargoTargetSpec>)
16661579
}
16671580
}
16681581

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