aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndy Russell <[email protected]>2021-05-04 22:13:51 +0100
committerAndy Russell <[email protected]>2021-05-23 20:50:36 +0100
commita90b9a5872c9c916733816e1e0d8c95cb09bfcba (patch)
tree6b170854407d2ed0e667623c8cdf1fb4a844230c
parent16054887102104208f4a0fc0e75e702b85a2eae8 (diff)
implement range formatting
-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 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)]
306pub enum RustfmtConfig { 310pub 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};
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
@@ -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, &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 };
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); 953pub(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
1049pub(crate) fn handle_code_action( 962pub(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
1582fn 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)]
1670struct CompletionResolveData { 1717struct 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`.
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",