diff options
author | Kirill Bulatov <[email protected]> | 2020-12-04 20:55:36 +0000 |
---|---|---|
committer | Kirill Bulatov <[email protected]> | 2020-12-07 21:41:08 +0000 |
commit | 6badf705b380bf8a66c5a1d8349365cab7a8ed4d (patch) | |
tree | 440cbd68556dc2d84b817b38e3a5f6850b30b087 /crates | |
parent | 077c1c3c1f16b5387c9e20cfa087c517dac3f4c8 (diff) |
Check lsp completions' edits for disjointness
Diffstat (limited to 'crates')
-rw-r--r-- | crates/rust-analyzer/src/handlers.rs | 17 | ||||
-rw-r--r-- | crates/rust-analyzer/src/lsp_utils.rs | 160 |
2 files changed, 176 insertions, 1 deletions
diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs index f80c55df7..d6865e1d6 100644 --- a/crates/rust-analyzer/src/handlers.rs +++ b/crates/rust-analyzer/src/handlers.rs | |||
@@ -36,6 +36,7 @@ use crate::{ | |||
36 | from_json, from_proto, | 36 | from_json, from_proto, |
37 | global_state::{GlobalState, GlobalStateSnapshot}, | 37 | global_state::{GlobalState, GlobalStateSnapshot}, |
38 | lsp_ext::{self, InlayHint, InlayHintsParams}, | 38 | lsp_ext::{self, InlayHint, InlayHintsParams}, |
39 | lsp_utils::all_edits_are_disjoint, | ||
39 | to_proto, LspError, Result, | 40 | to_proto, LspError, Result, |
40 | }; | 41 | }; |
41 | 42 | ||
@@ -601,6 +602,14 @@ pub(crate) fn handle_completion_resolve( | |||
601 | ) -> Result<CompletionItem> { | 602 | ) -> Result<CompletionItem> { |
602 | let _p = profile::span("handle_resolve_completion"); | 603 | let _p = profile::span("handle_resolve_completion"); |
603 | 604 | ||
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 | |||
604 | // FIXME resolve the other capabilities also? | 613 | // FIXME resolve the other capabilities also? |
605 | if !snap | 614 | if !snap |
606 | .config | 615 | .config |
@@ -640,6 +649,14 @@ pub(crate) fn handle_completion_resolve( | |||
640 | }) | 649 | }) |
641 | .collect_vec(); | 650 | .collect_vec(); |
642 | 651 | ||
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 | |||
643 | if let Some(original_additional_edits) = original_completion.additional_text_edits.as_mut() { | 660 | if let Some(original_additional_edits) = original_completion.additional_text_edits.as_mut() { |
644 | original_additional_edits.extend(additional_edits.drain(..)) | 661 | original_additional_edits.extend(additional_edits.drain(..)) |
645 | } else { | 662 | } else { |
diff --git a/crates/rust-analyzer/src/lsp_utils.rs b/crates/rust-analyzer/src/lsp_utils.rs index 6427c7367..d5c1c1ad0 100644 --- a/crates/rust-analyzer/src/lsp_utils.rs +++ b/crates/rust-analyzer/src/lsp_utils.rs | |||
@@ -129,9 +129,36 @@ pub(crate) fn apply_document_changes( | |||
129 | } | 129 | } |
130 | } | 130 | } |
131 | 131 | ||
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 | |||
132 | #[cfg(test)] | 156 | #[cfg(test)] |
133 | mod tests { | 157 | mod tests { |
134 | use lsp_types::{Position, Range, TextDocumentContentChangeEvent}; | 158 | use lsp_types::{ |
159 | CompletionItem, CompletionTextEdit, InsertReplaceEdit, Position, Range, | ||
160 | TextDocumentContentChangeEvent, | ||
161 | }; | ||
135 | 162 | ||
136 | use super::*; | 163 | use super::*; |
137 | 164 | ||
@@ -197,4 +224,135 @@ mod tests { | |||
197 | apply_document_changes(&mut text, c![0, 1; 1, 0 => "ț\nc", 0, 2; 0, 2 => "c"]); | 224 | apply_document_changes(&mut text, c![0, 1; 1, 0 => "ț\nc", 0, 2; 0, 2 => "c"]); |
198 | assert_eq!(text, "ațc\ncb"); | 225 | assert_eq!(text, "ațc\ncb"); |
199 | } | 226 | } |
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 | } | ||
200 | } | 358 | } |