]> git.proxmox.com Git - rustc.git/blame - src/tools/rust-analyzer/crates/rust-analyzer/src/lsp_utils.rs
New upstream version 1.70.0+dfsg1
[rustc.git] / src / tools / rust-analyzer / crates / rust-analyzer / src / lsp_utils.rs
CommitLineData
064997fb 1//! Utilities for LSP-related boilerplate code.
487cf647 2use std::{mem, ops::Range, sync::Arc};
064997fb
FG
3
4use lsp_server::Notification;
9ffffee4 5use lsp_types::request::Request;
064997fb
FG
6
7use crate::{
8 from_proto,
9 global_state::GlobalState,
487cf647 10 line_index::{LineEndings, LineIndex, PositionEncoding},
9ffffee4 11 lsp_ext, LspError,
064997fb
FG
12};
13
14pub(crate) fn invalid_params_error(message: String) -> LspError {
15 LspError { code: lsp_server::ErrorCode::InvalidParams as i32, message }
16}
17
18pub(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)]
25pub(crate) enum Progress {
26 Begin,
27 Report,
28 End,
29}
30
31impl 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
38impl 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
193pub(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.
254pub(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)]
289mod 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}