aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbors[bot] <26634292+bors[bot]@users.noreply.github.com>2021-05-25 13:15:48 +0100
committerGitHub <[email protected]>2021-05-25 13:15:48 +0100
commit835cf55887527bd1953cb7004259214f7c215095 (patch)
treedadc26f171812663779a80cbe9344e9a004b37e7
parentb7414fa14a85f4acd37b5bdfdc2a4ab97a072bd2 (diff)
parenta90b9a5872c9c916733816e1e0d8c95cb09bfcba (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.rs4
-rw-r--r--crates/rust-analyzer/src/config.rs13
-rw-r--r--crates/rust-analyzer/src/handlers.rs239
-rw-r--r--crates/rust-analyzer/src/main_loop.rs1
-rw-r--r--docs/user/generated_config.adoc7
-rw-r--r--editors/code/package.json5
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.
2use std::env; 2use std::env;
3 3
4use lsp_types::{ 4use 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)]
307pub enum RustfmtConfig { 311pub 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};
28use project_model::TargetKind; 28use project_model::TargetKind;
29use serde::{Deserialize, Serialize}; 29use serde::{Deserialize, Serialize};
30use serde_json::to_value; 30use serde_json::{json, to_value};
31use stdx::format_to; 31use stdx::format_to;
32use syntax::{algo, ast, AstNode, TextRange, TextSize}; 32use 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, &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 };
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); 962pub(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
1058pub(crate) fn handle_code_action( 971pub(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
1591fn 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)]
1679struct CompletionResolveData { 1726struct 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`.
346Advanced option, fully override the command rust-analyzer uses for 346Advanced option, fully override the command rust-analyzer uses for
347formatting. 347formatting.
348-- 348--
349[[rust-analyzer.rustfmt.enableRangeFormatting]]rust-analyzer.rustfmt.enableRangeFormatting (default: `false`)::
350+
351--
352Enables the use of rustfmt's unstable range formatting command for the
353`textDocument/rangeFormatting` request. The rustfmt option is unstable and only
354available 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",