Skip to content

Commit 1644eea

Browse files
committed
refactor(language_server): use minimal text edit for ServerFormatter
1 parent 27022ab commit 1644eea

File tree

3 files changed

+140
-4
lines changed

3 files changed

+140
-4
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/oxc_language_server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ doctest = false
2323

2424
[dependencies]
2525
oxc_allocator = { workspace = true }
26+
oxc_data_structures = { workspace = true, features = ["rope"] }
2627
oxc_diagnostics = { workspace = true }
2728
oxc_formatter = { workspace = true }
2829
oxc_linter = { workspace = true, features = ["language_server"] }

crates/oxc_language_server/src/formatter/server_formatter.rs

Lines changed: 138 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
use oxc_allocator::Allocator;
2+
use oxc_data_structures::rope::{Rope, get_line_column};
23
use oxc_formatter::{FormatOptions, Formatter, get_supported_source_type};
34
use oxc_parser::{ParseOptions, Parser};
45
use tower_lsp_server::{
56
UriExt,
67
lsp_types::{Position, Range, TextEdit, Uri},
78
};
89

9-
use crate::LSP_MAX_INT;
10-
1110
pub struct ServerFormatter;
1211

1312
impl ServerFormatter {
@@ -49,9 +48,144 @@ impl ServerFormatter {
4948
return Some(vec![]);
5049
}
5150

51+
let (start, end, replacement) = compute_minimal_text_edit(&source_text, &code);
52+
let rope = Rope::from(source_text.as_str());
53+
let (start_line, start_character) = get_line_column(&rope, start, &source_text);
54+
let (end_line, end_character) = get_line_column(&rope, end, &source_text);
55+
5256
Some(vec![TextEdit::new(
53-
Range::new(Position::new(0, 0), Position::new(LSP_MAX_INT, 0)),
54-
code,
57+
Range::new(
58+
Position::new(start_line, start_character),
59+
Position::new(end_line, end_character),
60+
),
61+
replacement.to_string(),
5562
)])
5663
}
5764
}
65+
66+
/// Returns the minimal text edit (start, end, replacement) to transform `source_text` into `formatted_text`
67+
#[expect(clippy::cast_possible_truncation)]
68+
fn compute_minimal_text_edit<'a>(
69+
source_text: &str,
70+
formatted_text: &'a str,
71+
) -> (u32, u32, &'a str) {
72+
debug_assert!(source_text != formatted_text);
73+
74+
// Find common prefix (byte offset)
75+
let mut prefix_byte = 0;
76+
for (a, b) in source_text.chars().zip(formatted_text.chars()) {
77+
if a == b {
78+
prefix_byte += a.len_utf8();
79+
} else {
80+
break;
81+
}
82+
}
83+
84+
// Find common suffix (byte offset from end)
85+
let mut suffix_byte = 0;
86+
let src_bytes = source_text.as_bytes();
87+
let fmt_bytes = formatted_text.as_bytes();
88+
let src_len = src_bytes.len();
89+
let fmt_len = fmt_bytes.len();
90+
91+
while suffix_byte < src_len - prefix_byte
92+
&& suffix_byte < fmt_len - prefix_byte
93+
&& src_bytes[src_len - 1 - suffix_byte] == fmt_bytes[fmt_len - 1 - suffix_byte]
94+
{
95+
suffix_byte += 1;
96+
}
97+
98+
let start = prefix_byte as u32;
99+
let end = (src_len - suffix_byte) as u32;
100+
let replacement_start = prefix_byte;
101+
let replacement_end = fmt_len - suffix_byte;
102+
let replacement = &formatted_text[replacement_start..replacement_end];
103+
104+
(start, end, replacement)
105+
}
106+
107+
#[cfg(test)]
108+
mod tests {
109+
use super::compute_minimal_text_edit;
110+
111+
#[test]
112+
#[should_panic(expected = "assertion failed")]
113+
fn test_no_change() {
114+
let src = "abc";
115+
let formatted = "abc";
116+
compute_minimal_text_edit(src, formatted);
117+
}
118+
119+
#[test]
120+
fn test_single_char_change() {
121+
let src = "abc";
122+
let formatted = "axc";
123+
let (start, end, replacement) = compute_minimal_text_edit(src, formatted);
124+
// Only 'b' replaced by 'x'
125+
assert_eq!((start, end, replacement), (1, 2, "x"));
126+
}
127+
128+
#[test]
129+
fn test_insert_char() {
130+
let src = "abc";
131+
let formatted = "abxc";
132+
let (start, end, replacement) = compute_minimal_text_edit(src, formatted);
133+
// Insert 'x' after 'b'
134+
assert_eq!((start, end, replacement), (2, 2, "x"));
135+
}
136+
137+
#[test]
138+
fn test_delete_char() {
139+
let src = "abc";
140+
let formatted = "ac";
141+
let (start, end, replacement) = compute_minimal_text_edit(src, formatted);
142+
// Delete 'b'
143+
assert_eq!((start, end, replacement), (1, 2, ""));
144+
}
145+
146+
#[test]
147+
fn test_replace_multiple_chars() {
148+
let src = "abcdef";
149+
let formatted = "abXYef";
150+
let (start, end, replacement) = compute_minimal_text_edit(src, formatted);
151+
// Replace "cd" with "XY"
152+
assert_eq!((start, end, replacement), (2, 4, "XY"));
153+
}
154+
155+
#[test]
156+
fn test_replace_multiple_chars_between_similars_complex() {
157+
let src = "aYabYb";
158+
let formatted = "aXabXb";
159+
let (start, end, replacement) = compute_minimal_text_edit(src, formatted);
160+
assert_eq!((start, end, replacement), (1, 5, "XabX"));
161+
}
162+
163+
#[test]
164+
fn test_unicode() {
165+
let src = "a😀b";
166+
let formatted = "a😃b";
167+
let (start, end, replacement) = compute_minimal_text_edit(src, formatted);
168+
// Replace 😀 with 😃
169+
assert_eq!((start, end, replacement), (1, 5, "😃"));
170+
}
171+
172+
#[test]
173+
fn test_append() {
174+
let src = "a".repeat(100);
175+
let mut formatted = src.clone();
176+
formatted.push('b'); // Add a character at the end
177+
178+
let (start, end, replacement) = compute_minimal_text_edit(&src, &formatted);
179+
assert_eq!((start, end, replacement), (100, 100, "b"));
180+
}
181+
182+
#[test]
183+
fn test_prepend() {
184+
let src = "a".repeat(100);
185+
let mut formatted = String::from("b");
186+
formatted.push_str(&src); // Add a character at the start
187+
188+
let (start, end, replacement) = compute_minimal_text_edit(&src, &formatted);
189+
assert_eq!((start, end, replacement), (0, 0, "b"));
190+
}
191+
}

0 commit comments

Comments
 (0)