diff options
author | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2021-05-25 13:15:48 +0100 |
---|---|---|
committer | GitHub <[email protected]> | 2021-05-25 13:15:48 +0100 |
commit | 835cf55887527bd1953cb7004259214f7c215095 (patch) | |
tree | dadc26f171812663779a80cbe9344e9a004b37e7 | |
parent | b7414fa14a85f4acd37b5bdfdc2a4ab97a072bd2 (diff) | |
parent | a90b9a5872c9c916733816e1e0d8c95cb09bfcba (diff) |
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]>
-rw-r--r-- | crates/rust-analyzer/src/caps.rs | 4 | ||||
-rw-r--r-- | crates/rust-analyzer/src/config.rs | 13 | ||||
-rw-r--r-- | crates/rust-analyzer/src/handlers.rs | 239 | ||||
-rw-r--r-- | crates/rust-analyzer/src/main_loop.rs | 1 | ||||
-rw-r--r-- | docs/user/generated_config.adoc | 7 | ||||
-rw-r--r-- | 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 @@ | |||
1 | //! Advertizes the capabilities of the LSP Server. | 1 | //! Advertises the capabilities of the LSP Server. |
2 | use std::env; | 2 | use std::env; |
3 | 3 | ||
4 | use lsp_types::{ | 4 | use lsp_types::{ |
@@ -54,7 +54,7 @@ pub fn server_capabilities(client_caps: &ClientCapabilities) -> ServerCapabiliti | |||
54 | code_action_provider: Some(code_action_capabilities(client_caps)), | 54 | code_action_provider: Some(code_action_capabilities(client_caps)), |
55 | code_lens_provider: Some(CodeLensOptions { resolve_provider: Some(true) }), | 55 | code_lens_provider: Some(CodeLensOptions { resolve_provider: Some(true) }), |
56 | document_formatting_provider: Some(OneOf::Left(true)), | 56 | document_formatting_provider: Some(OneOf::Left(true)), |
57 | document_range_formatting_provider: None, | 57 | document_range_formatting_provider: Some(OneOf::Left(true)), |
58 | document_on_type_formatting_provider: Some(DocumentOnTypeFormattingOptions { | 58 | document_on_type_formatting_provider: Some(DocumentOnTypeFormattingOptions { |
59 | first_trigger_character: "=".to_string(), | 59 | first_trigger_character: "=".to_string(), |
60 | more_trigger_character: Some(vec![".".to_string(), ">".to_string(), "{".to_string()]), | 60 | 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 7c02a507c..7620a2fe1 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs | |||
@@ -218,6 +218,10 @@ config_data! { | |||
218 | /// Advanced option, fully override the command rust-analyzer uses for | 218 | /// Advanced option, fully override the command rust-analyzer uses for |
219 | /// formatting. | 219 | /// formatting. |
220 | rustfmt_overrideCommand: Option<Vec<String>> = "null", | 220 | 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", | ||
221 | 225 | ||
222 | /// Workspace symbol search scope. | 226 | /// Workspace symbol search scope. |
223 | workspace_symbol_search_scope: WorskpaceSymbolSearchScopeDef = "\"workspace\"", | 227 | workspace_symbol_search_scope: WorskpaceSymbolSearchScopeDef = "\"workspace\"", |
@@ -305,7 +309,7 @@ pub struct NotificationsConfig { | |||
305 | 309 | ||
306 | #[derive(Debug, Clone)] | 310 | #[derive(Debug, Clone)] |
307 | pub enum RustfmtConfig { | 311 | pub enum RustfmtConfig { |
308 | Rustfmt { extra_args: Vec<String> }, | 312 | Rustfmt { extra_args: Vec<String>, enable_range_formatting: bool }, |
309 | CustomCommand { command: String, args: Vec<String> }, | 313 | CustomCommand { command: String, args: Vec<String> }, |
310 | } | 314 | } |
311 | 315 | ||
@@ -584,9 +588,10 @@ impl Config { | |||
584 | let command = args.remove(0); | 588 | let command = args.remove(0); |
585 | RustfmtConfig::CustomCommand { command, args } | 589 | RustfmtConfig::CustomCommand { command, args } |
586 | } | 590 | } |
587 | Some(_) | None => { | 591 | Some(_) | None => RustfmtConfig::Rustfmt { |
588 | RustfmtConfig::Rustfmt { extra_args: self.data.rustfmt_extraArgs.clone() } | 592 | extra_args: self.data.rustfmt_extraArgs.clone(), |
589 | } | 593 | enable_range_formatting: self.data.rustfmt_enableRangeFormatting, |
594 | }, | ||
590 | } | 595 | } |
591 | } | 596 | } |
592 | pub fn flycheck(&self) -> Option<FlycheckConfig> { | 597 | pub fn flycheck(&self) -> Option<FlycheckConfig> { |
diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs index f48210424..456744603 100644 --- a/crates/rust-analyzer/src/handlers.rs +++ b/crates/rust-analyzer/src/handlers.rs | |||
@@ -27,7 +27,7 @@ use lsp_types::{ | |||
27 | }; | 27 | }; |
28 | use project_model::TargetKind; | 28 | use project_model::TargetKind; |
29 | use serde::{Deserialize, Serialize}; | 29 | use serde::{Deserialize, Serialize}; |
30 | use serde_json::to_value; | 30 | use serde_json::{json, to_value}; |
31 | use stdx::format_to; | 31 | use stdx::format_to; |
32 | use syntax::{algo, ast, AstNode, TextRange, TextSize}; | 32 | use syntax::{algo, ast, AstNode, TextRange, TextSize}; |
33 | 33 | ||
@@ -955,104 +955,17 @@ pub(crate) fn handle_formatting( | |||
955 | params: DocumentFormattingParams, | 955 | params: DocumentFormattingParams, |
956 | ) -> Result<Option<Vec<lsp_types::TextEdit>>> { | 956 | ) -> Result<Option<Vec<lsp_types::TextEdit>>> { |
957 | let _p = profile::span("handle_formatting"); | 957 | let _p = profile::span("handle_formatting"); |
958 | let file_id = from_proto::file_id(&snap, ¶ms.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 | }; | ||
999 | 958 | ||
1000 | let mut rustfmt = | 959 | run_rustfmt(&snap, params.text_document, None) |
1001 | rustfmt.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn()?; | 960 | } |
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 | } | ||
1039 | 961 | ||
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"); | ||
1041 | 967 | ||
1042 | if line_index.endings != new_line_endings { | 968 | run_rustfmt(&snap, params.text_document, Some(params.range)) |
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 | } | ||
1056 | } | 969 | } |
1057 | 970 | ||
1058 | pub(crate) fn handle_code_action( | 971 | pub(crate) fn handle_code_action( |
@@ -1675,6 +1588,140 @@ fn should_skip_target(runnable: &Runnable, cargo_spec: Option<&CargoTargetSpec>) | |||
1675 | } | 1588 | } |
1676 | } | 1589 | } |
1677 | 1590 | ||
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 | |||
1678 | #[derive(Debug, Serialize, Deserialize)] | 1725 | #[derive(Debug, Serialize, Deserialize)] |
1679 | struct CompletionResolveData { | 1726 | struct CompletionResolveData { |
1680 | position: lsp_types::TextDocumentPositionParams, | 1727 | position: lsp_types::TextDocumentPositionParams, |
diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs index cb002f700..008758ea0 100644 --- a/crates/rust-analyzer/src/main_loop.rs +++ b/crates/rust-analyzer/src/main_loop.rs | |||
@@ -543,6 +543,7 @@ impl GlobalState { | |||
543 | .on::<lsp_types::request::Rename>(handlers::handle_rename) | 543 | .on::<lsp_types::request::Rename>(handlers::handle_rename) |
544 | .on::<lsp_types::request::References>(handlers::handle_references) | 544 | .on::<lsp_types::request::References>(handlers::handle_references) |
545 | .on::<lsp_types::request::Formatting>(handlers::handle_formatting) | 545 | .on::<lsp_types::request::Formatting>(handlers::handle_formatting) |
546 | .on::<lsp_types::request::RangeFormatting>(handlers::handle_range_formatting) | ||
546 | .on::<lsp_types::request::DocumentHighlightRequest>(handlers::handle_document_highlight) | 547 | .on::<lsp_types::request::DocumentHighlightRequest>(handlers::handle_document_highlight) |
547 | .on::<lsp_types::request::CallHierarchyPrepare>(handlers::handle_call_hierarchy_prepare) | 548 | .on::<lsp_types::request::CallHierarchyPrepare>(handlers::handle_call_hierarchy_prepare) |
548 | .on::<lsp_types::request::CallHierarchyIncomingCalls>( | 549 | .on::<lsp_types::request::CallHierarchyIncomingCalls>( |
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`. | |||
346 | Advanced option, fully override the command rust-analyzer uses for | 346 | Advanced option, fully override the command rust-analyzer uses for |
347 | formatting. | 347 | formatting. |
348 | -- | 348 | -- |
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 | -- | ||
349 | [[rust-analyzer.workspace.symbol.search.scope]]rust-analyzer.workspace.symbol.search.scope (default: `"workspace"`):: | 356 | [[rust-analyzer.workspace.symbol.search.scope]]rust-analyzer.workspace.symbol.search.scope (default: `"workspace"`):: |
350 | + | 357 | + |
351 | -- | 358 | -- |
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 @@ | |||
795 | "type": "string" | 795 | "type": "string" |
796 | } | 796 | } |
797 | }, | 797 | }, |
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 | }, | ||
798 | "rust-analyzer.workspace.symbol.search.scope": { | 803 | "rust-analyzer.workspace.symbol.search.scope": { |
799 | "markdownDescription": "Workspace symbol search scope.", | 804 | "markdownDescription": "Workspace symbol search scope.", |
800 | "default": "workspace", | 805 | "default": "workspace", |