diff options
author | Andy Russell <[email protected]> | 2021-05-04 22:13:51 +0100 |
---|---|---|
committer | Andy Russell <[email protected]> | 2021-05-23 20:50:36 +0100 |
commit | a90b9a5872c9c916733816e1e0d8c95cb09bfcba (patch) | |
tree | 6b170854407d2ed0e667623c8cdf1fb4a844230c | |
parent | 16054887102104208f4a0fc0e75e702b85a2eae8 (diff) |
implement range formatting
-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 b700d025f..2e99db36c 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\"", |
@@ -304,7 +308,7 @@ pub struct NotificationsConfig { | |||
304 | 308 | ||
305 | #[derive(Debug, Clone)] | 309 | #[derive(Debug, Clone)] |
306 | pub enum RustfmtConfig { | 310 | pub enum RustfmtConfig { |
307 | Rustfmt { extra_args: Vec<String> }, | 311 | Rustfmt { extra_args: Vec<String>, enable_range_formatting: bool }, |
308 | CustomCommand { command: String, args: Vec<String> }, | 312 | CustomCommand { command: String, args: Vec<String> }, |
309 | } | 313 | } |
310 | 314 | ||
@@ -569,9 +573,10 @@ impl Config { | |||
569 | let command = args.remove(0); | 573 | let command = args.remove(0); |
570 | RustfmtConfig::CustomCommand { command, args } | 574 | RustfmtConfig::CustomCommand { command, args } |
571 | } | 575 | } |
572 | Some(_) | None => { | 576 | Some(_) | None => RustfmtConfig::Rustfmt { |
573 | RustfmtConfig::Rustfmt { extra_args: self.data.rustfmt_extraArgs.clone() } | 577 | extra_args: self.data.rustfmt_extraArgs.clone(), |
574 | } | 578 | enable_range_formatting: self.data.rustfmt_enableRangeFormatting, |
579 | }, | ||
575 | } | 580 | } |
576 | } | 581 | } |
577 | pub fn flycheck(&self) -> Option<FlycheckConfig> { | 582 | pub fn flycheck(&self) -> Option<FlycheckConfig> { |
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::{ | |||
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 | ||
@@ -946,104 +946,17 @@ pub(crate) fn handle_formatting( | |||
946 | params: DocumentFormattingParams, | 946 | params: DocumentFormattingParams, |
947 | ) -> Result<Option<Vec<lsp_types::TextEdit>>> { | 947 | ) -> Result<Option<Vec<lsp_types::TextEdit>>> { |
948 | let _p = profile::span("handle_formatting"); | 948 | let _p = profile::span("handle_formatting"); |
949 | let file_id = from_proto::file_id(&snap, ¶ms.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 | }; | ||
990 | 949 | ||
991 | let mut rustfmt = | 950 | run_rustfmt(&snap, params.text_document, None) |
992 | rustfmt.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn()?; | 951 | } |
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 | } | ||
1030 | 952 | ||
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"); | ||
1032 | 958 | ||
1033 | if line_index.endings != new_line_endings { | 959 | run_rustfmt(&snap, params.text_document, Some(params.range)) |
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 | } | ||
1047 | } | 960 | } |
1048 | 961 | ||
1049 | pub(crate) fn handle_code_action( | 962 | pub(crate) fn handle_code_action( |
@@ -1666,6 +1579,140 @@ fn should_skip_target(runnable: &Runnable, cargo_spec: Option<&CargoTargetSpec>) | |||
1666 | } | 1579 | } |
1667 | } | 1580 | } |
1668 | 1581 | ||
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 | |||
1669 | #[derive(Debug, Serialize, Deserialize)] | 1716 | #[derive(Debug, Serialize, Deserialize)] |
1670 | struct CompletionResolveData { | 1717 | struct CompletionResolveData { |
1671 | position: lsp_types::TextDocumentPositionParams, | 1718 | 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 { | |||
542 | .on::<lsp_types::request::Rename>(handlers::handle_rename) | 542 | .on::<lsp_types::request::Rename>(handlers::handle_rename) |
543 | .on::<lsp_types::request::References>(handlers::handle_references) | 543 | .on::<lsp_types::request::References>(handlers::handle_references) |
544 | .on::<lsp_types::request::Formatting>(handlers::handle_formatting) | 544 | .on::<lsp_types::request::Formatting>(handlers::handle_formatting) |
545 | .on::<lsp_types::request::RangeFormatting>(handlers::handle_range_formatting) | ||
545 | .on::<lsp_types::request::DocumentHighlightRequest>(handlers::handle_document_highlight) | 546 | .on::<lsp_types::request::DocumentHighlightRequest>(handlers::handle_document_highlight) |
546 | .on::<lsp_types::request::CallHierarchyPrepare>(handlers::handle_call_hierarchy_prepare) | 547 | .on::<lsp_types::request::CallHierarchyPrepare>(handlers::handle_call_hierarchy_prepare) |
547 | .on::<lsp_types::request::CallHierarchyIncomingCalls>( | 548 | .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", |