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