Skip to content

Commit 55b9c2c

Browse files
committed
Use byte diff instead of line diff
1 parent 6d9c87e commit 55b9c2c

File tree

3 files changed

+72
-78
lines changed

3 files changed

+72
-78
lines changed

Cargo.lock

Lines changed: 12 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ num_cpus = "1.16.0"
5252
regex = "1.10.2"
5353
serde = "1.0.188"
5454
serde_json = "1.0.108"
55-
similar = { version = "2.3.0", features = ["text", "inline", "serde"] }
55+
similar = { version = "2.3.0", features = ["text", "inline", "serde", "bytes"] }
5656
strum = { version = "0.25.0", features = ["derive"], optional = true }
5757
thiserror = "1.0.49"
5858
threadpool = "1.8.1"

src/cli/lsp.rs

Lines changed: 59 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,26 @@ use lsp_textdocument::{FullTextDocument, TextDocuments};
55
use lsp_types::{
66
request::{Formatting, RangeFormatting, Request},
77
DocumentFormattingParams, DocumentRangeFormattingParams, FormattingOptions, InitializeResult,
8-
OneOf, Position, Range, ServerCapabilities, ServerInfo, TextDocumentSyncCapability,
9-
TextDocumentSyncKind, TextEdit, Uri,
8+
OneOf, Range, ServerCapabilities, ServerInfo, TextDocumentSyncCapability, TextDocumentSyncKind,
9+
TextEdit, Uri,
1010
};
1111
use similar::{DiffOp, TextDiff};
1212
use stylua_lib::{format_code, IndentType, OutputVerification};
1313

1414
use crate::{config::ConfigResolver, opt};
1515

16-
fn diffop_to_textedit(op: DiffOp, formatted_contents: &str) -> Option<TextEdit> {
16+
fn diffop_to_textedit(
17+
op: DiffOp,
18+
document: &FullTextDocument,
19+
formatted_contents: &str,
20+
) -> Option<TextEdit> {
21+
let range = |start: usize, len: usize| Range {
22+
start: document.position_at(start.try_into().expect("usize fits into u32")),
23+
end: document.position_at((start + len).try_into().expect("usize fits into u32")),
24+
};
25+
26+
let lookup = |start: usize, len: usize| formatted_contents[start..start + len].to_string();
27+
1728
match op {
1829
DiffOp::Equal {
1930
old_index: _,
@@ -25,66 +36,25 @@ fn diffop_to_textedit(op: DiffOp, formatted_contents: &str) -> Option<TextEdit>
2536
old_len,
2637
new_index: _,
2738
} => Some(TextEdit {
28-
range: Range {
29-
start: Position {
30-
line: old_index.try_into().expect("usize fits into u32"),
31-
character: 0,
32-
},
33-
end: Position {
34-
line: (old_index + old_len)
35-
.try_into()
36-
.expect("usize fits into u32"),
37-
character: 0,
38-
},
39-
},
39+
range: range(old_index, old_len),
4040
new_text: String::new(),
4141
}),
4242
DiffOp::Insert {
4343
old_index,
4444
new_index,
4545
new_len,
46-
} => {
47-
let insert_position = Position {
48-
line: old_index.try_into().expect("usize fits into u32"),
49-
character: 0,
50-
};
51-
Some(TextEdit {
52-
range: Range {
53-
start: insert_position,
54-
end: insert_position,
55-
},
56-
new_text: formatted_contents
57-
.lines()
58-
.skip(new_index)
59-
.take(new_len)
60-
.collect::<Vec<_>>()
61-
.join("\n"),
62-
})
63-
}
46+
} => Some(TextEdit {
47+
range: range(old_index, 0),
48+
new_text: lookup(new_index, new_len),
49+
}),
6450
DiffOp::Replace {
6551
old_index,
6652
old_len,
6753
new_index,
6854
new_len,
6955
} => Some(TextEdit {
70-
range: Range {
71-
start: Position {
72-
line: old_index.try_into().expect("usize fits into u32"),
73-
character: 0,
74-
},
75-
end: Position {
76-
line: (old_index + old_len)
77-
.try_into()
78-
.expect("usize fits into u32"),
79-
character: 0,
80-
},
81-
},
82-
new_text: formatted_contents
83-
.lines()
84-
.skip(new_index)
85-
.take(new_len)
86-
.collect::<Vec<_>>()
87-
.join("\n"),
56+
range: range(old_index, old_len),
57+
new_text: lookup(new_index, new_len),
8858
}),
8959
}
9060
}
@@ -118,13 +88,14 @@ fn handle_formatting(
11888

11989
let formatted_contents = format_code(contents, config, range, OutputVerification::None).ok()?;
12090

121-
let operations = TextDiff::from_lines(contents, &formatted_contents).grouped_ops(0);
91+
let operations =
92+
TextDiff::from_chars(contents.as_bytes(), formatted_contents.as_bytes()).grouped_ops(0);
12293
let edits = operations
12394
.into_iter()
12495
.flat_map(|operations| {
12596
operations
12697
.into_iter()
127-
.filter_map(|op| diffop_to_textedit(op, &formatted_contents))
98+
.filter_map(|op| diffop_to_textedit(op, document, &formatted_contents))
12899
})
129100
.collect();
130101
Some(edits)
@@ -267,6 +238,8 @@ pub fn run(opt: opt::Opt) -> anyhow::Result<()> {
267238

268239
#[cfg(test)]
269240
mod tests {
241+
use std::cmp::Ordering;
242+
use std::convert::TryInto;
270243
use std::str::FromStr;
271244

272245
use clap::Parser;
@@ -386,6 +359,35 @@ mod tests {
386359
assert!(client.receiver.is_empty());
387360
}
388361

362+
fn with_edits(text: &str, mut edits: Vec<TextEdit>) -> String {
363+
edits.sort_by(|a, b| match a.range.start.line.cmp(&b.range.start.line) {
364+
Ordering::Equal => a
365+
.range
366+
.start
367+
.character
368+
.cmp(&b.range.start.character)
369+
.reverse(),
370+
order => order.reverse(),
371+
});
372+
let mut text = text.to_string();
373+
for edit in edits {
374+
let start = text
375+
.lines()
376+
.take(edit.range.start.line.try_into().unwrap())
377+
.map(|line| line.len() + '\n'.len_utf8())
378+
.sum::<usize>()
379+
+ <u32 as TryInto<usize>>::try_into(edit.range.start.character).unwrap();
380+
let end = text
381+
.lines()
382+
.take(edit.range.end.line.try_into().unwrap())
383+
.map(|line| line.len() + '\n'.len_utf8())
384+
.sum::<usize>()
385+
+ <u32 as TryInto<usize>>::try_into(edit.range.end.character).unwrap();
386+
text.replace_range(start..end, &edit.new_text);
387+
}
388+
text
389+
}
390+
389391
#[test]
390392
fn test_lsp_document_formatting() {
391393
let uri = Uri::from_str("file:///home/documents/file.luau").unwrap();
@@ -433,17 +435,8 @@ mod tests {
433435
expect_server_initialized(&client.receiver, 1);
434436

435437
let edits: Vec<TextEdit> = expect_response(&client.receiver, 2);
436-
assert_eq!(edits.len(), 1);
437-
assert_eq!(
438-
edits[0],
439-
TextEdit {
440-
range: Range {
441-
start: Position::new(0, 0),
442-
end: Position::new(0, 14),
443-
},
444-
new_text: "local x = 1\n".to_string(),
445-
}
446-
);
438+
let formatted = with_edits(contents, edits);
439+
assert_eq!(formatted, "local x = 1\n");
447440

448441
expect_server_shutdown(&client.receiver, 3);
449442
assert!(client.receiver.is_empty());
@@ -497,17 +490,8 @@ mod tests {
497490
expect_server_initialized(&client.receiver, 1);
498491

499492
let edits: Vec<TextEdit> = expect_response(&client.receiver, 2);
500-
assert_eq!(edits.len(), 1);
501-
assert_eq!(
502-
edits[0],
503-
TextEdit {
504-
range: Range {
505-
start: Position::new(0, 0),
506-
end: Position::new(1, 18),
507-
},
508-
new_text: "local x = 1\nlocal y = 2\n".to_string(),
509-
}
510-
);
493+
let formatted = with_edits(contents, edits);
494+
assert_eq!(formatted, "local x = 1\nlocal y = 2\n");
511495

512496
expect_server_shutdown(&client.receiver, 3);
513497
assert!(client.receiver.is_empty());

0 commit comments

Comments
 (0)