Skip to content
This repository was archived by the owner on Dec 29, 2022. It is now read-only.

Commit 49efc06

Browse files
committed
Auto merge of #1385 - Xanewok:rustfmt-modified-lines, r=Xanewok
Return only modified lines in formatting responses Ticks few boxes in #3. (that's an early issue!) Closes #334. This is currently based on rust-lang/rustfmt#3424 and so currently blocked until it merges. Configures Rustfmt to use `EmitMode::ModifiedLines` and parses the response - this should work with both statically-linked and externally provided Rustfmt. @alexheretic do you think you could take a look and double-check?
2 parents 6e14b56 + e17a9e4 commit 49efc06

File tree

4 files changed

+142
-51
lines changed

4 files changed

+142
-51
lines changed

rls/src/actions/format.rs

+125-31
Original file line numberDiff line numberDiff line change
@@ -6,45 +6,109 @@ use std::fs::File;
66
use std::io::Write;
77
use std::path::{Path, PathBuf};
88
use std::process::{Command, Stdio};
9+
use std::string::FromUtf8Error;
910

1011
use log::debug;
12+
use lsp_types::{Position, Range, TextEdit};
1113
use rand::{distributions, thread_rng, Rng};
12-
use rustfmt_nightly::{Config, Input, Session};
14+
use rustfmt_nightly::{Config, Input, ModifiedLines, NewlineStyle, Session};
1315
use serde_json;
1416

1517
/// Specifies which `rustfmt` to use.
1618
#[derive(Clone)]
1719
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 },
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+
2454
impl 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

3363
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> {
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

42106
fn 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("\nfn 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\nstruct 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+
}

rls/src/actions/requests.rs

+7-10
Original file line numberDiff line numberDiff line change
@@ -620,7 +620,7 @@ impl RequestAction for CodeAction {
620620
}
621621

622622
impl RequestAction for Formatting {
623-
type Response = [TextEdit; 1];
623+
type Response = Vec<TextEdit>;
624624

625625
fn fallback_response() -> Result<Self::Response, ResponseError> {
626626
Err(ResponseError::Message(
@@ -638,7 +638,7 @@ impl RequestAction for Formatting {
638638
}
639639

640640
impl RequestAction for RangeFormatting {
641-
type Response = [TextEdit; 1];
641+
type Response = Vec<TextEdit>;
642642

643643
fn fallback_response() -> Result<Self::Response, ResponseError> {
644644
Err(ResponseError::Message(
@@ -660,7 +660,7 @@ fn reformat(
660660
selection: Option<Range>,
661661
opts: &FormattingOptions,
662662
ctx: &InitActionContext,
663-
) -> Result<[TextEdit; 1], ResponseError> {
663+
) -> Result<Vec<TextEdit>, ResponseError> {
664664
ctx.quiescent.store(true, Ordering::SeqCst);
665665
trace!("Reformat: {:?} {:?} {} {}", doc, selection, opts.tab_size, opts.insert_spaces);
666666
let path = parse_file_path!(&doc.uri, "reformat")?;
@@ -683,7 +683,6 @@ fn reformat(
683683
}
684684
};
685685

686-
let range_whole_file = ls_util::range_from_file_string(&input);
687686
let mut config = ctx.fmt_config().get_rustfmt_config().clone();
688687
if !config.was_set().hard_tabs() {
689688
config.set().hard_tabs(!opts.insert_spaces);
@@ -722,10 +721,10 @@ fn reformat(
722721
config.set().file_lines(file_lines);
723722
};
724723

725-
let formatted_text = ctx
724+
let text_edits = ctx
726725
.formatter()
727-
.format(input, config)
728-
.map_err(|msg| ResponseError::Message(ErrorCode::InternalError, msg))?;
726+
.calc_text_edits(input, config)
727+
.map_err(|msg| ResponseError::Message(ErrorCode::InternalError, msg.to_string()))?;
729728

730729
// Note that we don't need to update the VFS, the client echos back the
731730
// change to us when it applies the returned `TextEdit`.
@@ -737,9 +736,7 @@ fn reformat(
737736
));
738737
}
739738

740-
// If Rustfmt returns range of text that changed,
741-
// we will be able to pass only range of changed text to the client.
742-
Ok([TextEdit { range: range_whole_file, new_text: formatted_text }])
739+
Ok(text_edits)
743740
}
744741

745742
impl RequestAction for ResolveCompletion {

rls/src/lib.rs

+3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
#![warn(clippy::all, rust_2018_idioms)]
1010
#![allow(clippy::cyclomatic_complexity, clippy::too_many_arguments, clippy::redundant_closure)]
1111

12+
#[macro_use]
13+
extern crate failure;
14+
1215
pub use rls_analysis::{AnalysisHost, Target};
1316
pub use rls_vfs::Vfs;
1417

tests/client.rs

+7-10
Original file line numberDiff line numberDiff line change
@@ -1763,9 +1763,9 @@ fn client_reformat() {
17631763
assert_eq!(result.unwrap()[0], TextEdit {
17641764
range: Range {
17651765
start: Position { line: 0, character: 0 },
1766-
end: Position { line: 2, character: 0 },
1766+
end: Position { line: 1, character: u64::max_value() },
17671767
},
1768-
new_text: "pub mod foo;\npub fn main() {\n let world = \"world\";\n println!(\"Hello, {}!\", world);\n}\n".to_string(),
1768+
new_text: "pub mod foo;\npub fn main() {\n let world = \"world\";\n println!(\"Hello, {}!\", world);\n}".to_string(),
17691769
});
17701770
}
17711771

@@ -1802,17 +1802,14 @@ fn client_reformat_with_range() {
18021802
let newline = if cfg!(windows) { "\r\n" } else { "\n" };
18031803
let formatted = r#"pub fn main() {
18041804
let world1 = "world";
1805-
println!("Hello, {}!", world1);
1806-
1807-
// Work around rustfmt#3494
1808-
let world2 = "world"; println!("Hello, {}!", world2);
1809-
let world3 = "world"; println!("Hello, {}!", world3);
1810-
}
1811-
"#
1805+
println!("Hello, {}!", world1);"#
18121806
.replace("\r", "")
18131807
.replace("\n", newline);
18141808

1815-
assert_eq!(result.unwrap()[0].new_text, formatted);
1809+
let edits = result.unwrap();
1810+
assert_eq!(edits.len(), 2);
1811+
assert_eq!(edits[0].new_text, formatted);
1812+
assert_eq!(edits[1].new_text, "");
18161813
}
18171814

18181815
#[test]

0 commit comments

Comments
 (0)