Skip to content

Commit 0c93f33

Browse files
committed
refactor(language_server): use minimal text edit for ServerFormatter (#13960)
closes #13629 The Server now computed the minimal Span range, which is modified and send it to the client. This is helpful for big files where only one little change happened. Instead of sending the full text, the server sends only the minimal change.
1 parent 4758c55 commit 0c93f33

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)