rust_analyzer/lsp/
utils.rs

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