@@ -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
@@ -946,104 +946,17 @@ pub(crate) fn handle_formatting(
946
946
params : DocumentFormattingParams ,
947
947
) -> Result < Option < Vec < lsp_types:: TextEdit > > > {
948
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 =
992
- rustfmt. stdin ( Stdio :: piped ( ) ) . stdout ( Stdio :: piped ( ) ) . stderr ( Stdio :: piped ( ) ) . spawn ( ) ?;
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
- }
950
+ run_rustfmt ( & snap, params. text_document , None )
951
+ }
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 {
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
- }
959
+ run_rustfmt ( & snap, params. text_document , Some ( params. range ) )
1047
960
}
1048
961
1049
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
1716
#[ derive( Debug , Serialize , Deserialize ) ]
1670
1717
struct CompletionResolveData {
1671
1718
position : lsp_types:: TextDocumentPositionParams ,
0 commit comments