@@ -6,45 +6,109 @@ use std::fs::File;
66use std:: io:: Write ;
77use std:: path:: { Path , PathBuf } ;
88use std:: process:: { Command , Stdio } ;
9+ use std:: string:: FromUtf8Error ;
910
1011use log:: debug;
12+ use lsp_types:: { Position , Range , TextEdit } ;
1113use rand:: { distributions, thread_rng, Rng } ;
12- use rustfmt_nightly:: { Config , Input , Session } ;
14+ use rustfmt_nightly:: { Config , Input , ModifiedLines , NewlineStyle , Session } ;
1315use serde_json;
1416
1517/// Specifies which `rustfmt` to use.
1618#[ derive( Clone ) ]
1719pub enum Rustfmt {
18- /// `(path to external `rustfmt`, current working directory to spawn at)`
19- External ( PathBuf , PathBuf ) ,
20+ /// Externally invoked `rustfmt` process.
21+ External { path : PathBuf , cwd : PathBuf } ,
2022 /// Statically linked `rustfmt`.
2123 Internal ,
2224}
2325
26+ /// Defines a formatting-related error.
27+ #[ derive( Fail , Debug ) ]
28+ pub enum Error {
29+ /// Generic variant of `Error::Rustfmt` error.
30+ #[ fail( display = "Formatting could not be completed." ) ]
31+ Failed ,
32+ #[ fail( display = "Could not format source code: {}" , _0) ]
33+ Rustfmt ( rustfmt_nightly:: ErrorKind ) ,
34+ #[ fail( display = "Encountered I/O error: {}" , _0) ]
35+ Io ( std:: io:: Error ) ,
36+ #[ fail( display = "Config couldn't be converted to TOML for Rustfmt purposes: {}" , _0) ]
37+ ConfigTomlOutput ( String ) ,
38+ #[ fail( display = "Formatted output is not valid UTF-8 source: {}" , _0) ]
39+ OutputNotUtf8 ( FromUtf8Error ) ,
40+ }
41+
42+ impl From < std:: io:: Error > for Error {
43+ fn from ( err : std:: io:: Error ) -> Error {
44+ Error :: Io ( err)
45+ }
46+ }
47+
48+ impl From < FromUtf8Error > for Error {
49+ fn from ( err : FromUtf8Error ) -> Error {
50+ Error :: OutputNotUtf8 ( err)
51+ }
52+ }
53+
2454impl From < Option < ( String , PathBuf ) > > for Rustfmt {
2555 fn from ( value : Option < ( String , PathBuf ) > ) -> Rustfmt {
2656 match value {
27- Some ( ( path, cwd) ) => Rustfmt :: External ( PathBuf :: from ( path) , cwd) ,
57+ Some ( ( path, cwd) ) => Rustfmt :: External { path : PathBuf :: from ( path) , cwd } ,
2858 None => Rustfmt :: Internal ,
2959 }
3060 }
3161}
3262
3363impl Rustfmt {
34- pub fn format ( & self , input : String , cfg : Config ) -> Result < String , String > {
64+ pub fn format ( & self , input : String , cfg : Config ) -> Result < String , Error > {
3565 match self {
3666 Rustfmt :: Internal => format_internal ( input, cfg) ,
37- Rustfmt :: External ( path, cwd) => format_external ( path, cwd, input, cfg) ,
67+ Rustfmt :: External { path, cwd } => format_external ( path, cwd, input, cfg) ,
3868 }
3969 }
70+
71+ pub fn calc_text_edits ( & self , input : String , mut cfg : Config ) -> Result < Vec < TextEdit > , Error > {
72+ cfg. set ( ) . emit_mode ( rustfmt_nightly:: EmitMode :: ModifiedLines ) ;
73+
74+ let native = if cfg ! ( windows) { "\r \n " } else { "\n " } ;
75+ let newline = match cfg. newline_style ( ) {
76+ NewlineStyle :: Windows => "\r \n " ,
77+ NewlineStyle :: Unix | NewlineStyle :: Auto => "\n " ,
78+ NewlineStyle :: Native => native,
79+ } ;
80+
81+ let output = self . format ( input, cfg) ?;
82+ let ModifiedLines { chunks } = output. parse ( ) . map_err ( |_| Error :: Failed ) ?;
83+
84+ Ok ( chunks
85+ . into_iter ( )
86+ . map ( |item| {
87+ // Rustfmt's line indices are 1-based
88+ let start_line = u64:: from ( item. line_number_orig ) - 1 ;
89+ // Could underflow if we don't remove lines and there's only one
90+ let removed = u64:: from ( item. lines_removed ) . saturating_sub ( 1 ) ;
91+ TextEdit {
92+ range : Range {
93+ start : Position :: new ( start_line, 0 ) ,
94+ // We don't extend the range past the last line because
95+ // sometimes it may not exist, skewing the diff and
96+ // making us add an invalid additional trailing newline.
97+ end : Position :: new ( start_line + removed, u64:: max_value ( ) ) ,
98+ } ,
99+ new_text : item. lines . join ( newline) ,
100+ }
101+ } )
102+ . collect ( ) )
103+ }
40104}
41105
42106fn format_external (
43107 path : & PathBuf ,
44108 cwd : & PathBuf ,
45109 input : String ,
46110 cfg : Config ,
47- ) -> Result < String , String > {
111+ ) -> Result < String , Error > {
48112 let ( _file_handle, config_path) = gen_config_file ( & cfg) ?;
49113 let args = rustfmt_args ( & cfg, & config_path) ;
50114
@@ -54,25 +118,18 @@ fn format_external(
54118 . stdin ( Stdio :: piped ( ) )
55119 . stdout ( Stdio :: piped ( ) )
56120 . spawn ( )
57- . map_err ( |_| format ! ( "Couldn't spawn `{}`" , path . display ( ) ) ) ?;
121+ . map_err ( Error :: Io ) ?;
58122
59123 {
60- let stdin =
61- rustfmt. stdin . as_mut ( ) . ok_or_else ( || "Failed to open rustfmt stdin" . to_string ( ) ) ?;
62- stdin
63- . write_all ( input. as_bytes ( ) )
64- . map_err ( |_| "Failed to pass input to rustfmt" . to_string ( ) ) ?;
124+ let stdin = rustfmt. stdin . as_mut ( ) . unwrap ( ) ; // Safe because stdin is piped
125+ stdin. write_all ( input. as_bytes ( ) ) ?;
65126 }
66127
67- rustfmt. wait_with_output ( ) . map_err ( |err| format ! ( "Error running rustfmt: {}" , err) ) . and_then (
68- |out| {
69- String :: from_utf8 ( out. stdout )
70- . map_err ( |_| "Formatted code is not valid UTF-8" . to_string ( ) )
71- } ,
72- )
128+ let output = rustfmt. wait_with_output ( ) ?;
129+ Ok ( String :: from_utf8 ( output. stdout ) ?)
73130}
74131
75- fn format_internal ( input : String , config : Config ) -> Result < String , String > {
132+ fn format_internal ( input : String , config : Config ) -> Result < String , Error > {
76133 let mut buf = Vec :: < u8 > :: new ( ) ;
77134
78135 {
@@ -85,37 +142,34 @@ fn format_internal(input: String, config: Config) -> Result<String, String> {
85142 if session. has_operational_errors ( ) || session. has_parsing_errors ( ) {
86143 debug ! ( "reformat: format_input failed: has errors, report = {}" , report) ;
87144
88- return Err ( "Reformat failed to complete successfully" . into ( ) ) ;
145+ return Err ( Error :: Failed ) ;
89146 }
90147 }
91148 Err ( e) => {
92149 debug ! ( "Reformat failed: {:?}" , e) ;
93150
94- return Err ( "Reformat failed to complete successfully" . into ( ) ) ;
151+ return Err ( Error :: Rustfmt ( e ) ) ;
95152 }
96153 }
97154 }
98155
99- String :: from_utf8 ( buf) . map_err ( |_| "Reformat output is not a valid UTF-8" . into ( ) )
156+ Ok ( String :: from_utf8 ( buf) ? )
100157}
101158
102- fn random_file ( ) -> Result < ( File , PathBuf ) , String > {
159+ fn random_file ( ) -> Result < ( File , PathBuf ) , Error > {
103160 const SUFFIX_LEN : usize = 10 ;
104161
105162 let suffix: String =
106163 thread_rng ( ) . sample_iter ( & distributions:: Alphanumeric ) . take ( SUFFIX_LEN ) . collect ( ) ;
107164 let path = temp_dir ( ) . join ( suffix) ;
108165
109- Ok ( File :: create ( & path)
110- . map ( |file| ( file, path) )
111- . map_err ( |_| "Config file could not be created" . to_string ( ) ) ?)
166+ Ok ( File :: create ( & path) . map ( |file| ( file, path) ) ?)
112167}
113168
114- fn gen_config_file ( config : & Config ) -> Result < ( File , PathBuf ) , String > {
169+ fn gen_config_file ( config : & Config ) -> Result < ( File , PathBuf ) , Error > {
115170 let ( mut file, path) = random_file ( ) ?;
116- let toml = config. all_options ( ) . to_toml ( ) ?;
117- file. write ( toml. as_bytes ( ) )
118- . map_err ( |_| "Could not write config TOML file contents" . to_string ( ) ) ?;
171+ let toml = config. all_options ( ) . to_toml ( ) . map_err ( Error :: ConfigTomlOutput ) ?;
172+ file. write_all ( toml. as_bytes ( ) ) ?;
119173
120174 Ok ( ( file, path) )
121175}
@@ -139,3 +193,43 @@ fn rustfmt_args(config: &Config, config_path: &Path) -> Vec<String> {
139193
140194 args
141195}
196+
197+ #[ cfg( test) ]
198+ mod tests {
199+ use super :: * ;
200+ use crate :: config:: FmtConfig ;
201+ use lsp_types:: { Position , Range , TextEdit } ;
202+
203+ #[ test]
204+ fn calc_text_edits ( ) {
205+ let config = || FmtConfig :: default ( ) . get_rustfmt_config ( ) . clone ( ) ;
206+ let format = |x : & str | Rustfmt :: Internal . calc_text_edits ( x. to_string ( ) , config ( ) ) . unwrap ( ) ;
207+ let line_range = |start, end| Range {
208+ start : Position { line : start, character : 0 } ,
209+ end : Position { line : end, character : u64:: max_value ( ) } ,
210+ } ;
211+ // Handle single-line text wrt. added/removed trailing newline
212+ assert_eq ! (
213+ format( "fn main() {} " ) ,
214+ vec![ TextEdit { range: line_range( 0 , 0 ) , new_text: "fn main() {}\n " . to_owned( ) } ]
215+ ) ;
216+
217+ assert_eq ! (
218+ format( "fn main() {} \n " ) ,
219+ vec![ TextEdit { range: line_range( 0 , 0 ) , new_text: "fn main() {}" . to_owned( ) } ]
220+ ) ;
221+
222+ assert_eq ! (
223+ format( "\n fn main() {} \n " ) ,
224+ vec![ TextEdit { range: line_range( 0 , 1 ) , new_text: "fn main() {}" . to_owned( ) } ]
225+ ) ;
226+ // Check that we send two separate edits
227+ assert_eq ! (
228+ format( " struct Upper ;\n \n struct Lower ;" ) ,
229+ vec![
230+ TextEdit { range: line_range( 0 , 0 ) , new_text: "struct Upper;" . to_owned( ) } ,
231+ TextEdit { range: line_range( 2 , 2 ) , new_text: "struct Lower;\n " . to_owned( ) }
232+ ]
233+ ) ;
234+ }
235+ }
0 commit comments