aboutsummaryrefslogtreecommitdiff
path: root/crates/rust-analyzer/src/handlers.rs
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 /crates/rust-analyzer/src/handlers.rs
parent16054887102104208f4a0fc0e75e702b85a2eae8 (diff)
implement range formatting
Diffstat (limited to 'crates/rust-analyzer/src/handlers.rs')
-rw-r--r--crates/rust-analyzer/src/handlers.rs239
1 files changed, 143 insertions, 96 deletions
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,