]>
Commit | Line | Data |
---|---|---|
064997fb | 1 | //! Utilities for LSP-related boilerplate code. |
487cf647 | 2 | use std::{mem, ops::Range, sync::Arc}; |
064997fb FG |
3 | |
4 | use lsp_server::Notification; | |
9ffffee4 | 5 | use lsp_types::request::Request; |
064997fb FG |
6 | |
7 | use crate::{ | |
8 | from_proto, | |
9 | global_state::GlobalState, | |
487cf647 | 10 | line_index::{LineEndings, LineIndex, PositionEncoding}, |
9ffffee4 | 11 | lsp_ext, LspError, |
064997fb FG |
12 | }; |
13 | ||
14 | pub(crate) fn invalid_params_error(message: String) -> LspError { | |
15 | LspError { code: lsp_server::ErrorCode::InvalidParams as i32, message } | |
16 | } | |
17 | ||
18 | pub(crate) fn notification_is<N: lsp_types::notification::Notification>( | |
19 | notification: &Notification, | |
20 | ) -> bool { | |
21 | notification.method == N::METHOD | |
22 | } | |
23 | ||
24 | #[derive(Debug, Eq, PartialEq)] | |
25 | pub(crate) enum Progress { | |
26 | Begin, | |
27 | Report, | |
28 | End, | |
29 | } | |
30 | ||
31 | impl Progress { | |
32 | pub(crate) fn fraction(done: usize, total: usize) -> f64 { | |
33 | assert!(done <= total); | |
34 | done as f64 / total.max(1) as f64 | |
35 | } | |
36 | } | |
37 | ||
38 | impl GlobalState { | |
353b0b11 FG |
39 | pub(crate) fn show_message( |
40 | &mut self, | |
41 | typ: lsp_types::MessageType, | |
42 | message: String, | |
43 | show_open_log_button: bool, | |
44 | ) { | |
45 | match self.config.open_server_logs() && show_open_log_button { | |
46 | true => self.send_request::<lsp_types::request::ShowMessageRequest>( | |
47 | lsp_types::ShowMessageRequestParams { | |
48 | typ, | |
49 | message, | |
50 | actions: Some(vec![lsp_types::MessageActionItem { | |
51 | title: "Open server logs".to_owned(), | |
52 | properties: Default::default(), | |
53 | }]), | |
54 | }, | |
55 | |this, resp| { | |
56 | let lsp_server::Response { error: None, result: Some(result), .. } = resp | |
57 | else { return }; | |
58 | if let Ok(Some(_item)) = crate::from_json::< | |
59 | <lsp_types::request::ShowMessageRequest as lsp_types::request::Request>::Result, | |
60 | >( | |
61 | lsp_types::request::ShowMessageRequest::METHOD, &result | |
62 | ) { | |
63 | this.send_notification::<lsp_ext::OpenServerLogs>(()); | |
64 | } | |
65 | }, | |
66 | ), | |
67 | false => self.send_notification::<lsp_types::notification::ShowMessage>( | |
68 | lsp_types::ShowMessageParams { | |
69 | typ, | |
70 | message, | |
71 | }, | |
72 | ), | |
73 | } | |
064997fb FG |
74 | } |
75 | ||
76 | /// Sends a notification to the client containing the error `message`. | |
77 | /// If `additional_info` is [`Some`], appends a note to the notification telling to check the logs. | |
78 | /// This will always log `message` + `additional_info` to the server's error log. | |
79 | pub(crate) fn show_and_log_error(&mut self, message: String, additional_info: Option<String>) { | |
064997fb FG |
80 | match additional_info { |
81 | Some(additional_info) => { | |
9ffffee4 FG |
82 | tracing::error!("{}:\n{}", &message, &additional_info); |
83 | match self.config.open_server_logs() && tracing::enabled!(tracing::Level::ERROR) { | |
84 | true => self.send_request::<lsp_types::request::ShowMessageRequest>( | |
85 | lsp_types::ShowMessageRequestParams { | |
86 | typ: lsp_types::MessageType::ERROR, | |
87 | message, | |
88 | actions: Some(vec![lsp_types::MessageActionItem { | |
89 | title: "Open server logs".to_owned(), | |
90 | properties: Default::default(), | |
91 | }]), | |
92 | }, | |
93 | |this, resp| { | |
94 | let lsp_server::Response { error: None, result: Some(result), .. } = resp | |
95 | else { return }; | |
96 | if let Ok(Some(_item)) = crate::from_json::< | |
97 | <lsp_types::request::ShowMessageRequest as lsp_types::request::Request>::Result, | |
98 | >( | |
99 | lsp_types::request::ShowMessageRequest::METHOD, &result | |
100 | ) { | |
101 | this.send_notification::<lsp_ext::OpenServerLogs>(()); | |
102 | } | |
103 | }, | |
104 | ), | |
105 | false => self.send_notification::<lsp_types::notification::ShowMessage>( | |
106 | lsp_types::ShowMessageParams { | |
107 | typ: lsp_types::MessageType::ERROR, | |
108 | message, | |
109 | }, | |
110 | ), | |
064997fb FG |
111 | } |
112 | } | |
9ffffee4 FG |
113 | None => { |
114 | tracing::error!("{}", &message); | |
064997fb | 115 | |
9ffffee4 FG |
116 | self.send_notification::<lsp_types::notification::ShowMessage>( |
117 | lsp_types::ShowMessageParams { typ: lsp_types::MessageType::ERROR, message }, | |
118 | ); | |
119 | } | |
120 | } | |
064997fb FG |
121 | } |
122 | ||
123 | /// rust-analyzer is resilient -- if it fails, this doesn't usually affect | |
124 | /// the user experience. Part of that is that we deliberately hide panics | |
125 | /// from the user. | |
126 | /// | |
127 | /// We do however want to pester rust-analyzer developers with panics and | |
128 | /// other "you really gotta fix that" messages. The current strategy is to | |
129 | /// be noisy for "from source" builds or when profiling is enabled. | |
130 | /// | |
131 | /// It's unclear if making from source `cargo xtask install` builds more | |
132 | /// panicky is a good idea, let's see if we can keep our awesome bleeding | |
133 | /// edge users from being upset! | |
134 | pub(crate) fn poke_rust_analyzer_developer(&mut self, message: String) { | |
135 | let from_source_build = option_env!("POKE_RA_DEVS").is_some(); | |
136 | let profiling_enabled = std::env::var("RA_PROFILE").is_ok(); | |
137 | if from_source_build || profiling_enabled { | |
9ffffee4 | 138 | self.show_and_log_error(message, None); |
064997fb FG |
139 | } |
140 | } | |
141 | ||
142 | pub(crate) fn report_progress( | |
143 | &mut self, | |
144 | title: &str, | |
145 | state: Progress, | |
146 | message: Option<String>, | |
147 | fraction: Option<f64>, | |
2b03887a | 148 | cancel_token: Option<String>, |
064997fb FG |
149 | ) { |
150 | if !self.config.work_done_progress() { | |
151 | return; | |
152 | } | |
153 | let percentage = fraction.map(|f| { | |
154 | assert!((0.0..=1.0).contains(&f)); | |
155 | (f * 100.0) as u32 | |
156 | }); | |
2b03887a FG |
157 | let cancellable = Some(cancel_token.is_some()); |
158 | let token = lsp_types::ProgressToken::String( | |
9c376795 | 159 | cancel_token.unwrap_or_else(|| format!("rustAnalyzer/{title}")), |
2b03887a | 160 | ); |
064997fb FG |
161 | let work_done_progress = match state { |
162 | Progress::Begin => { | |
163 | self.send_request::<lsp_types::request::WorkDoneProgressCreate>( | |
164 | lsp_types::WorkDoneProgressCreateParams { token: token.clone() }, | |
165 | |_, _| (), | |
166 | ); | |
167 | ||
168 | lsp_types::WorkDoneProgress::Begin(lsp_types::WorkDoneProgressBegin { | |
169 | title: title.into(), | |
2b03887a | 170 | cancellable, |
064997fb FG |
171 | message, |
172 | percentage, | |
173 | }) | |
174 | } | |
175 | Progress::Report => { | |
176 | lsp_types::WorkDoneProgress::Report(lsp_types::WorkDoneProgressReport { | |
2b03887a | 177 | cancellable, |
064997fb FG |
178 | message, |
179 | percentage, | |
180 | }) | |
181 | } | |
182 | Progress::End => { | |
183 | lsp_types::WorkDoneProgress::End(lsp_types::WorkDoneProgressEnd { message }) | |
184 | } | |
185 | }; | |
186 | self.send_notification::<lsp_types::notification::Progress>(lsp_types::ProgressParams { | |
187 | token, | |
188 | value: lsp_types::ProgressParamsValue::WorkDone(work_done_progress), | |
189 | }); | |
190 | } | |
191 | } | |
192 | ||
193 | pub(crate) fn apply_document_changes( | |
9ffffee4 | 194 | encoding: PositionEncoding, |
487cf647 FG |
195 | file_contents: impl FnOnce() -> String, |
196 | mut content_changes: Vec<lsp_types::TextDocumentContentChangeEvent>, | |
197 | ) -> String { | |
198 | // Skip to the last full document change, as it invalidates all previous changes anyways. | |
199 | let mut start = content_changes | |
200 | .iter() | |
201 | .rev() | |
202 | .position(|change| change.range.is_none()) | |
203 | .map(|idx| content_changes.len() - idx - 1) | |
204 | .unwrap_or(0); | |
205 | ||
206 | let mut text: String = match content_changes.get_mut(start) { | |
207 | // peek at the first content change as an optimization | |
208 | Some(lsp_types::TextDocumentContentChangeEvent { range: None, text, .. }) => { | |
209 | let text = mem::take(text); | |
210 | start += 1; | |
211 | ||
212 | // The only change is a full document update | |
213 | if start == content_changes.len() { | |
214 | return text; | |
215 | } | |
216 | text | |
217 | } | |
218 | Some(_) => file_contents(), | |
219 | // we received no content changes | |
220 | None => return file_contents(), | |
221 | }; | |
222 | ||
064997fb | 223 | let mut line_index = LineIndex { |
487cf647 FG |
224 | // the index will be overwritten in the bottom loop's first iteration |
225 | index: Arc::new(ide::LineIndex::new(&text)), | |
9ffffee4 | 226 | // We don't care about line endings here. |
064997fb | 227 | endings: LineEndings::Unix, |
9ffffee4 | 228 | encoding, |
064997fb FG |
229 | }; |
230 | ||
231 | // The changes we got must be applied sequentially, but can cross lines so we | |
232 | // have to keep our line index updated. | |
233 | // Some clients (e.g. Code) sort the ranges in reverse. As an optimization, we | |
234 | // remember the last valid line in the index and only rebuild it if needed. | |
235 | // The VFS will normalize the end of lines to `\n`. | |
487cf647 | 236 | let mut index_valid = !0u32; |
064997fb | 237 | for change in content_changes { |
487cf647 FG |
238 | // The None case can't happen as we have handled it above already |
239 | if let Some(range) = change.range { | |
240 | if index_valid <= range.end.line { | |
241 | *Arc::make_mut(&mut line_index.index) = ide::LineIndex::new(&text); | |
064997fb | 242 | } |
487cf647 FG |
243 | index_valid = range.start.line; |
244 | if let Ok(range) = from_proto::text_range(&line_index, range) { | |
245 | text.replace_range(Range::<usize>::from(range), &change.text); | |
064997fb FG |
246 | } |
247 | } | |
248 | } | |
487cf647 | 249 | text |
064997fb FG |
250 | } |
251 | ||
252 | /// Checks that the edits inside the completion and the additional edits do not overlap. | |
253 | /// LSP explicitly forbids the additional edits to overlap both with the main edit and themselves. | |
254 | pub(crate) fn all_edits_are_disjoint( | |
255 | completion: &lsp_types::CompletionItem, | |
256 | additional_edits: &[lsp_types::TextEdit], | |
257 | ) -> bool { | |
258 | let mut edit_ranges = Vec::new(); | |
259 | match completion.text_edit.as_ref() { | |
260 | Some(lsp_types::CompletionTextEdit::Edit(edit)) => { | |
261 | edit_ranges.push(edit.range); | |
262 | } | |
263 | Some(lsp_types::CompletionTextEdit::InsertAndReplace(edit)) => { | |
264 | let replace = edit.replace; | |
265 | let insert = edit.insert; | |
266 | if replace.start != insert.start | |
267 | || insert.start > insert.end | |
268 | || insert.end > replace.end | |
269 | { | |
270 | // insert has to be a prefix of replace but it is not | |
271 | return false; | |
272 | } | |
273 | edit_ranges.push(replace); | |
274 | } | |
275 | None => {} | |
276 | } | |
277 | if let Some(additional_changes) = completion.additional_text_edits.as_ref() { | |
278 | edit_ranges.extend(additional_changes.iter().map(|edit| edit.range)); | |
279 | }; | |
280 | edit_ranges.extend(additional_edits.iter().map(|edit| edit.range)); | |
281 | edit_ranges.sort_by_key(|range| (range.start, range.end)); | |
282 | edit_ranges | |
283 | .iter() | |
284 | .zip(edit_ranges.iter().skip(1)) | |
285 | .all(|(previous, next)| previous.end <= next.start) | |
286 | } | |
287 | ||
288 | #[cfg(test)] | |
289 | mod tests { | |
9ffffee4 | 290 | use ide_db::line_index::WideEncoding; |
064997fb FG |
291 | use lsp_types::{ |
292 | CompletionItem, CompletionTextEdit, InsertReplaceEdit, Position, Range, | |
293 | TextDocumentContentChangeEvent, | |
294 | }; | |
295 | ||
296 | use super::*; | |
297 | ||
298 | #[test] | |
299 | fn test_apply_document_changes() { | |
300 | macro_rules! c { | |
301 | [$($sl:expr, $sc:expr; $el:expr, $ec:expr => $text:expr),+] => { | |
302 | vec![$(TextDocumentContentChangeEvent { | |
303 | range: Some(Range { | |
304 | start: Position { line: $sl, character: $sc }, | |
305 | end: Position { line: $el, character: $ec }, | |
306 | }), | |
307 | range_length: None, | |
308 | text: String::from($text), | |
309 | }),+] | |
310 | }; | |
311 | } | |
312 | ||
9ffffee4 FG |
313 | let encoding = PositionEncoding::Wide(WideEncoding::Utf16); |
314 | let text = apply_document_changes(encoding, || String::new(), vec![]); | |
064997fb | 315 | assert_eq!(text, ""); |
487cf647 | 316 | let text = apply_document_changes( |
9ffffee4 | 317 | encoding, |
487cf647 | 318 | || text, |
064997fb FG |
319 | vec![TextDocumentContentChangeEvent { |
320 | range: None, | |
321 | range_length: None, | |
322 | text: String::from("the"), | |
323 | }], | |
324 | ); | |
325 | assert_eq!(text, "the"); | |
9ffffee4 | 326 | let text = apply_document_changes(encoding, || text, c![0, 3; 0, 3 => " quick"]); |
064997fb | 327 | assert_eq!(text, "the quick"); |
9ffffee4 FG |
328 | let text = |
329 | apply_document_changes(encoding, || text, c![0, 0; 0, 4 => "", 0, 5; 0, 5 => " foxes"]); | |
064997fb | 330 | assert_eq!(text, "quick foxes"); |
9ffffee4 | 331 | let text = apply_document_changes(encoding, || text, c![0, 11; 0, 11 => "\ndream"]); |
064997fb | 332 | assert_eq!(text, "quick foxes\ndream"); |
9ffffee4 | 333 | let text = apply_document_changes(encoding, || text, c![1, 0; 1, 0 => "have "]); |
064997fb | 334 | assert_eq!(text, "quick foxes\nhave dream"); |
487cf647 | 335 | let text = apply_document_changes( |
9ffffee4 | 336 | encoding, |
487cf647 | 337 | || text, |
064997fb FG |
338 | c![0, 0; 0, 0 => "the ", 1, 4; 1, 4 => " quiet", 1, 16; 1, 16 => "s\n"], |
339 | ); | |
340 | assert_eq!(text, "the quick foxes\nhave quiet dreams\n"); | |
9ffffee4 FG |
341 | let text = apply_document_changes( |
342 | encoding, | |
343 | || text, | |
344 | c![0, 15; 0, 15 => "\n", 2, 17; 2, 17 => "\n"], | |
345 | ); | |
064997fb | 346 | assert_eq!(text, "the quick foxes\n\nhave quiet dreams\n\n"); |
487cf647 | 347 | let text = apply_document_changes( |
9ffffee4 | 348 | encoding, |
487cf647 | 349 | || text, |
064997fb FG |
350 | c![1, 0; 1, 0 => "DREAM", 2, 0; 2, 0 => "they ", 3, 0; 3, 0 => "DON'T THEY?"], |
351 | ); | |
352 | assert_eq!(text, "the quick foxes\nDREAM\nthey have quiet dreams\nDON'T THEY?\n"); | |
9ffffee4 FG |
353 | let text = |
354 | apply_document_changes(encoding, || text, c![0, 10; 1, 5 => "", 2, 0; 2, 12 => ""]); | |
064997fb FG |
355 | assert_eq!(text, "the quick \nthey have quiet dreams\n"); |
356 | ||
487cf647 | 357 | let text = String::from("❤️"); |
9ffffee4 | 358 | let text = apply_document_changes(encoding, || text, c![0, 0; 0, 0 => "a"]); |
064997fb FG |
359 | assert_eq!(text, "a❤️"); |
360 | ||
487cf647 | 361 | let text = String::from("a\nb"); |
9ffffee4 FG |
362 | let text = |
363 | apply_document_changes(encoding, || text, c![0, 1; 1, 0 => "\nțc", 0, 1; 1, 1 => "d"]); | |
064997fb FG |
364 | assert_eq!(text, "adcb"); |
365 | ||
487cf647 | 366 | let text = String::from("a\nb"); |
9ffffee4 FG |
367 | let text = |
368 | apply_document_changes(encoding, || text, c![0, 1; 1, 0 => "ț\nc", 0, 2; 0, 2 => "c"]); | |
064997fb FG |
369 | assert_eq!(text, "ațc\ncb"); |
370 | } | |
371 | ||
372 | #[test] | |
373 | fn empty_completion_disjoint_tests() { | |
374 | let empty_completion = | |
375 | CompletionItem::new_simple("label".to_string(), "detail".to_string()); | |
376 | ||
377 | let disjoint_edit_1 = lsp_types::TextEdit::new( | |
378 | Range::new(Position::new(2, 2), Position::new(3, 3)), | |
379 | "new_text".to_string(), | |
380 | ); | |
381 | let disjoint_edit_2 = lsp_types::TextEdit::new( | |
382 | Range::new(Position::new(3, 3), Position::new(4, 4)), | |
383 | "new_text".to_string(), | |
384 | ); | |
385 | ||
386 | let joint_edit = lsp_types::TextEdit::new( | |
387 | Range::new(Position::new(1, 1), Position::new(5, 5)), | |
388 | "new_text".to_string(), | |
389 | ); | |
390 | ||
391 | assert!( | |
392 | all_edits_are_disjoint(&empty_completion, &[]), | |
393 | "Empty completion has all its edits disjoint" | |
394 | ); | |
395 | assert!( | |
396 | all_edits_are_disjoint( | |
397 | &empty_completion, | |
398 | &[disjoint_edit_1.clone(), disjoint_edit_2.clone()] | |
399 | ), | |
400 | "Empty completion is disjoint to whatever disjoint extra edits added" | |
401 | ); | |
402 | ||
403 | assert!( | |
404 | !all_edits_are_disjoint( | |
405 | &empty_completion, | |
406 | &[disjoint_edit_1, disjoint_edit_2, joint_edit] | |
407 | ), | |
408 | "Empty completion does not prevent joint extra edits from failing the validation" | |
409 | ); | |
410 | } | |
411 | ||
412 | #[test] | |
413 | fn completion_with_joint_edits_disjoint_tests() { | |
414 | let disjoint_edit = lsp_types::TextEdit::new( | |
415 | Range::new(Position::new(1, 1), Position::new(2, 2)), | |
416 | "new_text".to_string(), | |
417 | ); | |
418 | let disjoint_edit_2 = lsp_types::TextEdit::new( | |
419 | Range::new(Position::new(2, 2), Position::new(3, 3)), | |
420 | "new_text".to_string(), | |
421 | ); | |
422 | let joint_edit = lsp_types::TextEdit::new( | |
423 | Range::new(Position::new(1, 1), Position::new(5, 5)), | |
424 | "new_text".to_string(), | |
425 | ); | |
426 | ||
427 | let mut completion_with_joint_edits = | |
428 | CompletionItem::new_simple("label".to_string(), "detail".to_string()); | |
429 | completion_with_joint_edits.additional_text_edits = | |
430 | Some(vec![disjoint_edit.clone(), joint_edit.clone()]); | |
431 | assert!( | |
432 | !all_edits_are_disjoint(&completion_with_joint_edits, &[]), | |
433 | "Completion with disjoint edits fails the validation even with empty extra edits" | |
434 | ); | |
435 | ||
436 | completion_with_joint_edits.text_edit = | |
437 | Some(CompletionTextEdit::Edit(disjoint_edit.clone())); | |
438 | completion_with_joint_edits.additional_text_edits = Some(vec![joint_edit.clone()]); | |
439 | assert!( | |
440 | !all_edits_are_disjoint(&completion_with_joint_edits, &[]), | |
441 | "Completion with disjoint edits fails the validation even with empty extra edits" | |
442 | ); | |
443 | ||
444 | completion_with_joint_edits.text_edit = | |
445 | Some(CompletionTextEdit::InsertAndReplace(InsertReplaceEdit { | |
446 | new_text: "new_text".to_string(), | |
447 | insert: disjoint_edit.range, | |
448 | replace: disjoint_edit_2.range, | |
449 | })); | |
450 | completion_with_joint_edits.additional_text_edits = Some(vec![joint_edit]); | |
451 | assert!( | |
452 | !all_edits_are_disjoint(&completion_with_joint_edits, &[]), | |
453 | "Completion with disjoint edits fails the validation even with empty extra edits" | |
454 | ); | |
455 | } | |
456 | ||
457 | #[test] | |
458 | fn completion_with_disjoint_edits_disjoint_tests() { | |
459 | let disjoint_edit = lsp_types::TextEdit::new( | |
460 | Range::new(Position::new(1, 1), Position::new(2, 2)), | |
461 | "new_text".to_string(), | |
462 | ); | |
463 | let disjoint_edit_2 = lsp_types::TextEdit::new( | |
464 | Range::new(Position::new(2, 2), Position::new(3, 3)), | |
465 | "new_text".to_string(), | |
466 | ); | |
467 | let joint_edit = lsp_types::TextEdit::new( | |
468 | Range::new(Position::new(1, 1), Position::new(5, 5)), | |
469 | "new_text".to_string(), | |
470 | ); | |
471 | ||
472 | let mut completion_with_disjoint_edits = | |
473 | CompletionItem::new_simple("label".to_string(), "detail".to_string()); | |
474 | completion_with_disjoint_edits.text_edit = Some(CompletionTextEdit::Edit(disjoint_edit)); | |
475 | let completion_with_disjoint_edits = completion_with_disjoint_edits; | |
476 | ||
477 | assert!( | |
478 | all_edits_are_disjoint(&completion_with_disjoint_edits, &[]), | |
479 | "Completion with disjoint edits is valid" | |
480 | ); | |
481 | assert!( | |
482 | !all_edits_are_disjoint(&completion_with_disjoint_edits, &[joint_edit]), | |
483 | "Completion with disjoint edits and joint extra edit is invalid" | |
484 | ); | |
485 | assert!( | |
486 | all_edits_are_disjoint(&completion_with_disjoint_edits, &[disjoint_edit_2]), | |
487 | "Completion with disjoint edits and joint extra edit is valid" | |
488 | ); | |
489 | } | |
490 | } |