@@ -27,7 +27,7 @@ use lsp_types::{
27
27
} ;
28
28
use project_model:: TargetKind ;
29
29
use serde:: { Deserialize , Serialize } ;
30
- use serde_json:: to_value;
30
+ use serde_json:: { json , to_value} ;
31
31
use stdx:: format_to;
32
32
use syntax:: { algo, ast, AstNode , TextRange , TextSize } ;
33
33
@@ -955,104 +955,17 @@ pub(crate) fn handle_formatting(
955
955
params : DocumentFormattingParams ,
956
956
) -> Result < Option < Vec < lsp_types:: TextEdit > > > {
957
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 =
1001
- rustfmt. stdin ( Stdio :: piped ( ) ) . stdout ( Stdio :: piped ( ) ) . stderr ( Stdio :: piped ( ) ) . spawn ( ) ?;
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
- }
959
+ run_rustfmt ( & snap, params. text_document , None )
960
+ }
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 {
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
- }
968
+ run_rustfmt ( & snap, params. text_document , Some ( params. range ) )
1056
969
}
1057
970
1058
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
1725
#[ derive( Debug , Serialize , Deserialize ) ]
1679
1726
struct CompletionResolveData {
1680
1727
position : lsp_types:: TextDocumentPositionParams ,
0 commit comments