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