@@ -6,45 +6,109 @@ use std::fs::File;
6
6
use std:: io:: Write ;
7
7
use std:: path:: { Path , PathBuf } ;
8
8
use std:: process:: { Command , Stdio } ;
9
+ use std:: string:: FromUtf8Error ;
9
10
10
11
use log:: debug;
12
+ use lsp_types:: { Position , Range , TextEdit } ;
11
13
use rand:: { distributions, thread_rng, Rng } ;
12
- use rustfmt_nightly:: { Config , Input , Session } ;
14
+ use rustfmt_nightly:: { Config , Input , ModifiedLines , NewlineStyle , Session } ;
13
15
use serde_json;
14
16
15
17
/// Specifies which `rustfmt` to use.
16
18
#[ derive( Clone ) ]
17
19
pub 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 } ,
20
22
/// Statically linked `rustfmt`.
21
23
Internal ,
22
24
}
23
25
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
+
24
54
impl From < Option < ( String , PathBuf ) > > for Rustfmt {
25
55
fn from ( value : Option < ( String , PathBuf ) > ) -> Rustfmt {
26
56
match value {
27
- Some ( ( path, cwd) ) => Rustfmt :: External ( PathBuf :: from ( path) , cwd) ,
57
+ Some ( ( path, cwd) ) => Rustfmt :: External { path : PathBuf :: from ( path) , cwd } ,
28
58
None => Rustfmt :: Internal ,
29
59
}
30
60
}
31
61
}
32
62
33
63
impl 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 > {
35
65
match self {
36
66
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) ,
38
68
}
39
69
}
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
+ }
40
104
}
41
105
42
106
fn format_external (
43
107
path : & PathBuf ,
44
108
cwd : & PathBuf ,
45
109
input : String ,
46
110
cfg : Config ,
47
- ) -> Result < String , String > {
111
+ ) -> Result < String , Error > {
48
112
let ( _file_handle, config_path) = gen_config_file ( & cfg) ?;
49
113
let args = rustfmt_args ( & cfg, & config_path) ;
50
114
@@ -54,25 +118,18 @@ fn format_external(
54
118
. stdin ( Stdio :: piped ( ) )
55
119
. stdout ( Stdio :: piped ( ) )
56
120
. spawn ( )
57
- . map_err ( |_| format ! ( "Couldn't spawn `{}`" , path . display ( ) ) ) ?;
121
+ . map_err ( Error :: Io ) ?;
58
122
59
123
{
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 ( ) ) ?;
65
126
}
66
127
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 ) ?)
73
130
}
74
131
75
- fn format_internal ( input : String , config : Config ) -> Result < String , String > {
132
+ fn format_internal ( input : String , config : Config ) -> Result < String , Error > {
76
133
let mut buf = Vec :: < u8 > :: new ( ) ;
77
134
78
135
{
@@ -85,37 +142,34 @@ fn format_internal(input: String, config: Config) -> Result<String, String> {
85
142
if session. has_operational_errors ( ) || session. has_parsing_errors ( ) {
86
143
debug ! ( "reformat: format_input failed: has errors, report = {}" , report) ;
87
144
88
- return Err ( "Reformat failed to complete successfully" . into ( ) ) ;
145
+ return Err ( Error :: Failed ) ;
89
146
}
90
147
}
91
148
Err ( e) => {
92
149
debug ! ( "Reformat failed: {:?}" , e) ;
93
150
94
- return Err ( "Reformat failed to complete successfully" . into ( ) ) ;
151
+ return Err ( Error :: Rustfmt ( e ) ) ;
95
152
}
96
153
}
97
154
}
98
155
99
- String :: from_utf8 ( buf) . map_err ( |_| "Reformat output is not a valid UTF-8" . into ( ) )
156
+ Ok ( String :: from_utf8 ( buf) ? )
100
157
}
101
158
102
- fn random_file ( ) -> Result < ( File , PathBuf ) , String > {
159
+ fn random_file ( ) -> Result < ( File , PathBuf ) , Error > {
103
160
const SUFFIX_LEN : usize = 10 ;
104
161
105
162
let suffix: String =
106
163
thread_rng ( ) . sample_iter ( & distributions:: Alphanumeric ) . take ( SUFFIX_LEN ) . collect ( ) ;
107
164
let path = temp_dir ( ) . join ( suffix) ;
108
165
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) ) ?)
112
167
}
113
168
114
- fn gen_config_file ( config : & Config ) -> Result < ( File , PathBuf ) , String > {
169
+ fn gen_config_file ( config : & Config ) -> Result < ( File , PathBuf ) , Error > {
115
170
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 ( ) ) ?;
119
173
120
174
Ok ( ( file, path) )
121
175
}
@@ -139,3 +193,43 @@ fn rustfmt_args(config: &Config, config_path: &Path) -> Vec<String> {
139
193
140
194
args
141
195
}
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