Skip to content

Commit 6badf70

Browse files
Check lsp completions' edits for disjointness
1 parent 077c1c3 commit 6badf70

File tree

2 files changed

+176
-1
lines changed

2 files changed

+176
-1
lines changed

crates/rust-analyzer/src/handlers.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ use crate::{
3636
from_json, from_proto,
3737
global_state::{GlobalState, GlobalStateSnapshot},
3838
lsp_ext::{self, InlayHint, InlayHintsParams},
39+
lsp_utils::all_edits_are_disjoint,
3940
to_proto, LspError, Result,
4041
};
4142

@@ -601,6 +602,14 @@ pub(crate) fn handle_completion_resolve(
601602
) -> Result<CompletionItem> {
602603
let _p = profile::span("handle_resolve_completion");
603604

605+
if !all_edits_are_disjoint(&original_completion, &[]) {
606+
return Err(LspError::new(
607+
ErrorCode::InvalidParams as i32,
608+
"Received a completion with disjoint edits".into(),
609+
)
610+
.into());
611+
}
612+
604613
// FIXME resolve the other capabilities also?
605614
if !snap
606615
.config
@@ -640,6 +649,14 @@ pub(crate) fn handle_completion_resolve(
640649
})
641650
.collect_vec();
642651

652+
if !all_edits_are_disjoint(&original_completion, &additional_edits) {
653+
return Err(LspError::new(
654+
ErrorCode::InternalError as i32,
655+
"Import edit is not disjoint with the original completion edits".into(),
656+
)
657+
.into());
658+
}
659+
643660
if let Some(original_additional_edits) = original_completion.additional_text_edits.as_mut() {
644661
original_additional_edits.extend(additional_edits.drain(..))
645662
} else {

crates/rust-analyzer/src/lsp_utils.rs

Lines changed: 159 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,36 @@ pub(crate) fn apply_document_changes(
129129
}
130130
}
131131

132+
/// Checks that the edits inside the completion and the additional edits are disjoint.
133+
pub(crate) fn all_edits_are_disjoint(
134+
completion: &lsp_types::CompletionItem,
135+
additional_edits: &[lsp_types::TextEdit],
136+
) -> bool {
137+
let mut edit_ranges = Vec::new();
138+
match completion.text_edit.as_ref() {
139+
Some(lsp_types::CompletionTextEdit::Edit(edit)) => {
140+
edit_ranges.push(edit.range);
141+
}
142+
Some(lsp_types::CompletionTextEdit::InsertAndReplace(edit)) => {
143+
edit_ranges.push(edit.insert);
144+
edit_ranges.push(edit.replace);
145+
}
146+
None => {}
147+
}
148+
if let Some(additional_changes) = completion.additional_text_edits.as_ref() {
149+
edit_ranges.extend(additional_changes.iter().map(|edit| edit.range));
150+
};
151+
edit_ranges.extend(additional_edits.iter().map(|edit| edit.range));
152+
edit_ranges.sort_by_key(|range| (range.start, range.end));
153+
edit_ranges.iter().zip(edit_ranges.iter().skip(1)).all(|(l, r)| l.end <= r.start)
154+
}
155+
132156
#[cfg(test)]
133157
mod tests {
134-
use lsp_types::{Position, Range, TextDocumentContentChangeEvent};
158+
use lsp_types::{
159+
CompletionItem, CompletionTextEdit, InsertReplaceEdit, Position, Range,
160+
TextDocumentContentChangeEvent,
161+
};
135162

136163
use super::*;
137164

@@ -197,4 +224,135 @@ mod tests {
197224
apply_document_changes(&mut text, c![0, 1; 1, 0 => \nc", 0, 2; 0, 2 => "c"]);
198225
assert_eq!(text, "ațc\ncb");
199226
}
227+
228+
#[test]
229+
fn empty_completion_disjoint_tests() {
230+
let empty_completion =
231+
CompletionItem::new_simple("label".to_string(), "detail".to_string());
232+
233+
let disjoint_edit_1 = lsp_types::TextEdit::new(
234+
Range::new(Position::new(2, 2), Position::new(3, 3)),
235+
"new_text".to_string(),
236+
);
237+
let disjoint_edit_2 = lsp_types::TextEdit::new(
238+
Range::new(Position::new(3, 3), Position::new(4, 4)),
239+
"new_text".to_string(),
240+
);
241+
242+
let joint_edit = lsp_types::TextEdit::new(
243+
Range::new(Position::new(1, 1), Position::new(5, 5)),
244+
"new_text".to_string(),
245+
);
246+
247+
assert!(
248+
all_edits_are_disjoint(&empty_completion, &[]),
249+
"Empty completion has all its edits disjoint"
250+
);
251+
assert!(
252+
all_edits_are_disjoint(
253+
&empty_completion,
254+
&[disjoint_edit_1.clone(), disjoint_edit_2.clone()]
255+
),
256+
"Empty completion is disjoint to whatever disjoint extra edits added"
257+
);
258+
259+
assert!(
260+
!all_edits_are_disjoint(
261+
&empty_completion,
262+
&[disjoint_edit_1, disjoint_edit_2, joint_edit]
263+
),
264+
"Empty completion does not prevent joint extra edits from failing the validation"
265+
);
266+
}
267+
268+
#[test]
269+
fn completion_with_joint_edits_disjoint_tests() {
270+
let disjoint_edit = lsp_types::TextEdit::new(
271+
Range::new(Position::new(1, 1), Position::new(2, 2)),
272+
"new_text".to_string(),
273+
);
274+
let disjoint_edit_2 = lsp_types::TextEdit::new(
275+
Range::new(Position::new(2, 2), Position::new(3, 3)),
276+
"new_text".to_string(),
277+
);
278+
let joint_edit = lsp_types::TextEdit::new(
279+
Range::new(Position::new(1, 1), Position::new(5, 5)),
280+
"new_text".to_string(),
281+
);
282+
283+
let mut completion_with_joint_edits =
284+
CompletionItem::new_simple("label".to_string(), "detail".to_string());
285+
completion_with_joint_edits.additional_text_edits =
286+
Some(vec![disjoint_edit.clone(), joint_edit.clone()]);
287+
assert!(
288+
!all_edits_are_disjoint(&completion_with_joint_edits, &[]),
289+
"Completion with disjoint edits fails the validaton even with empty extra edits"
290+
);
291+
292+
completion_with_joint_edits.text_edit =
293+
Some(CompletionTextEdit::Edit(disjoint_edit.clone()));
294+
completion_with_joint_edits.additional_text_edits = Some(vec![joint_edit.clone()]);
295+
assert!(
296+
!all_edits_are_disjoint(&completion_with_joint_edits, &[]),
297+
"Completion with disjoint edits fails the validaton even with empty extra edits"
298+
);
299+
300+
completion_with_joint_edits.text_edit =
301+
Some(CompletionTextEdit::InsertAndReplace(InsertReplaceEdit {
302+
new_text: "new_text".to_string(),
303+
insert: disjoint_edit.range,
304+
replace: joint_edit.range,
305+
}));
306+
completion_with_joint_edits.additional_text_edits = None;
307+
assert!(
308+
!all_edits_are_disjoint(&completion_with_joint_edits, &[]),
309+
"Completion with disjoint edits fails the validaton even with empty extra edits"
310+
);
311+
312+
completion_with_joint_edits.text_edit =
313+
Some(CompletionTextEdit::InsertAndReplace(InsertReplaceEdit {
314+
new_text: "new_text".to_string(),
315+
insert: disjoint_edit.range,
316+
replace: disjoint_edit_2.range,
317+
}));
318+
completion_with_joint_edits.additional_text_edits = Some(vec![joint_edit]);
319+
assert!(
320+
!all_edits_are_disjoint(&completion_with_joint_edits, &[]),
321+
"Completion with disjoint edits fails the validaton even with empty extra edits"
322+
);
323+
}
324+
325+
#[test]
326+
fn completion_with_disjoint_edits_disjoint_tests() {
327+
let disjoint_edit = lsp_types::TextEdit::new(
328+
Range::new(Position::new(1, 1), Position::new(2, 2)),
329+
"new_text".to_string(),
330+
);
331+
let disjoint_edit_2 = lsp_types::TextEdit::new(
332+
Range::new(Position::new(2, 2), Position::new(3, 3)),
333+
"new_text".to_string(),
334+
);
335+
let joint_edit = lsp_types::TextEdit::new(
336+
Range::new(Position::new(1, 1), Position::new(5, 5)),
337+
"new_text".to_string(),
338+
);
339+
340+
let mut completion_with_disjoint_edits =
341+
CompletionItem::new_simple("label".to_string(), "detail".to_string());
342+
completion_with_disjoint_edits.text_edit = Some(CompletionTextEdit::Edit(disjoint_edit));
343+
let completion_with_disjoint_edits = completion_with_disjoint_edits;
344+
345+
assert!(
346+
all_edits_are_disjoint(&completion_with_disjoint_edits, &[]),
347+
"Completion with disjoint edits is valid"
348+
);
349+
assert!(
350+
!all_edits_are_disjoint(&completion_with_disjoint_edits, &[joint_edit.clone()]),
351+
"Completion with disjoint edits and joint extra edit is invalid"
352+
);
353+
assert!(
354+
all_edits_are_disjoint(&completion_with_disjoint_edits, &[disjoint_edit_2.clone()]),
355+
"Completion with disjoint edits and joint extra edit is valid"
356+
);
357+
}
200358
}

0 commit comments

Comments
 (0)