use std::{mem, ops::Range, vec};
use lsp_server::Notification;
use lsp_types::{MessageActionItem, Request};
use triomphe::Arc;
use crate::{
global_state::GlobalState,
line_index::{LineEndings, LineIndex, PositionEncoding},
lsp::{LspError, from_proto},
lsp_ext,
};
pub(crate) fn invalid_params_error(message: String) -> LspError {
LspError { code: lsp_server::ErrorCode::InvalidParams as i32, message }
}
pub(crate) fn notification_is<N: lsp_types::Notification>(notification: &Notification) -> bool {
notification.method.as_str() == N::METHOD.as_str()
}
#[derive(Debug, Eq, PartialEq)]
pub(crate) enum Progress {
Begin,
Report,
End,
}
impl Progress {
pub(crate) fn fraction(done: usize, total: usize) -> f64 {
assert!(done <= total);
done as f64 / total.max(1) as f64
}
}
impl GlobalState {
pub(crate) fn show_message(
&mut self,
kind: lsp_types::MessageType,
message: String,
show_open_log_button: bool,
) {
match self.config.open_server_logs() && show_open_log_button {
true => self.send_request::<lsp_types::ShowMessageRequest>(
lsp_types::ShowMessageRequestParams {
kind,
message,
actions: Some(vec![lsp_types::MessageActionItem {
title: "Open server logs".to_owned(),
}]),
},
|this, resp| {
let lsp_server::Response { error: None, result: Some(result), .. } = resp
else {
return;
};
if let Ok(Some(_item)) = crate::from_json::<Option<MessageActionItem>>(
lsp_types::ShowMessageRequest::METHOD.as_str(),
&result,
) {
this.send_notification::<lsp_ext::OpenServerLogsNotification>(());
}
},
),
false => self.send_notification::<lsp_types::ShowMessageNotification>(
lsp_types::ShowMessageParams { kind, message },
),
}
}
pub(crate) fn show_and_log_error(&mut self, message: String, additional_info: Option<String>) {
match additional_info {
Some(additional_info) => {
tracing::error!("{message}:\n{additional_info}");
self.show_message(
lsp_types::MessageType::Error,
message,
tracing::enabled!(tracing::Level::ERROR),
);
}
None => {
tracing::error!("{message}");
self.send_notification::<lsp_types::ShowMessageNotification>(
lsp_types::ShowMessageParams { kind: lsp_types::MessageType::Error, message },
);
}
}
}
pub(crate) fn poke_ra_ap_rust_analyzer_developer(&mut self, message: String) {
let from_source_build = option_env!("POKE_RA_DEVS").is_some();
if from_source_build {
self.show_and_log_error(message, None);
}
}
pub(crate) fn report_progress(
&mut self,
title: &str,
state: Progress,
message: Option<String>,
fraction: Option<f64>,
cancel_token: Option<String>,
) {
if !self.config.work_done_progress() {
return;
}
let percentage = fraction.map(|f| {
assert!((0.0..=1.0).contains(&f));
(f * 100.0) as u32
});
let cancellable = Some(cancel_token.is_some());
let token = lsp_types::ProgressToken::String(
cancel_token.unwrap_or_else(|| format!("rustAnalyzer/{title}")),
);
tracing::debug!(?token, ?state, "report_progress {message:?}");
match state {
Progress::Begin => {
self.send_request::<lsp_types::WorkDoneProgressCreateRequest>(
lsp_types::WorkDoneProgressCreateParams { token: token.clone() },
|_, _| (),
);
self.send_notification::<lsp_types::ProgressNotification>(
lsp_types::ProgressParams {
token,
value: serde_json::to_value(lsp_types::WorkDoneProgressBegin {
title: title.into(),
cancellable,
message,
percentage,
})
.unwrap(),
},
);
}
Progress::Report => {
self.send_notification::<lsp_types::ProgressNotification>(
lsp_types::ProgressParams {
token,
value: serde_json::to_value(lsp_types::WorkDoneProgressReport {
cancellable,
message,
percentage,
})
.unwrap(),
},
);
}
Progress::End => {
self.send_notification::<lsp_types::ProgressNotification>(
lsp_types::ProgressParams {
token,
value: serde_json::to_value(lsp_types::WorkDoneProgressEnd { message })
.unwrap(),
},
);
}
}
}
}
pub(crate) fn apply_document_changes(
encoding: PositionEncoding,
file_contents: &str,
mut content_changes: Vec<lsp_types::TextDocumentContentChangeEvent>,
) -> String {
let (mut text, r_partial_changes);
match content_changes.iter_mut().rev().try_fold(Vec::new(), |mut acc, change| match change {
lsp_types::TextDocumentContentChangeEvent::TextDocumentContentChangePartial(partial) => {
acc.push(partial);
Ok(acc)
}
lsp_types::TextDocumentContentChangeEvent::TextDocumentContentChangeWholeDocument(
whole,
) => Err((whole, acc)),
}) {
Err((whole_document, reversed_partial_changes)) => {
text = mem::take(&mut whole_document.text);
r_partial_changes = reversed_partial_changes;
}
Ok(partials) => {
text = file_contents.to_owned();
r_partial_changes = partials;
}
}
if r_partial_changes.is_empty() {
return text;
}
let mut line_index = LineIndex {
index: Arc::new(ide::LineIndex::new(&text)),
endings: LineEndings::Unix,
encoding,
};
let mut index_valid = !0u32;
for change in r_partial_changes.iter().rev() {
if index_valid <= change.range.end.line {
*Arc::make_mut(&mut line_index.index) = ide::LineIndex::new(&text);
}
index_valid = change.range.start.line;
if let Ok(range) = from_proto::text_range(&line_index, change.range) {
text.replace_range(Range::<usize>::from(range), &change.text);
}
}
text
}
pub(crate) fn all_edits_are_disjoint(
completion: &lsp_types::CompletionItem,
additional_edits: &[lsp_types::TextEdit],
) -> bool {
let mut edit_ranges = Vec::new();
match completion.text_edit.as_ref() {
Some(lsp_types::CompletionItemTextEdit::TextEdit(edit)) => {
edit_ranges.push(edit.range);
}
Some(lsp_types::CompletionItemTextEdit::InsertReplaceEdit(edit)) => {
let replace = edit.replace;
let insert = edit.insert;
if replace.start != insert.start
|| insert.start > insert.end
|| insert.end > replace.end
{
return false;
}
edit_ranges.push(replace);
}
None => {}
}
if let Some(additional_changes) = completion.additional_text_edits.as_ref() {
edit_ranges.extend(additional_changes.iter().map(|edit| edit.range));
};
edit_ranges.extend(additional_edits.iter().map(|edit| edit.range));
edit_ranges.sort_by_key(|range| (range.start, range.end));
edit_ranges
.iter()
.zip(edit_ranges.iter().skip(1))
.all(|(previous, next)| previous.end <= next.start)
}
#[cfg(test)]
mod tests {
use ide_db::line_index::WideEncoding;
use lsp_types::{CompletionItem, InsertReplaceEdit, Position, Range};
use super::*;
#[test]
fn test_apply_document_changes() {
macro_rules! c {
[$($sl:expr, $sc:expr; $el:expr, $ec:expr => $text:expr),+] => {
vec![$(::lsp_types::TextDocumentContentChangeEvent::TextDocumentContentChangePartial(lsp_types::TextDocumentContentChangePartial {
range: Range {
start: Position { line: $sl, character: $sc },
end: Position { line: $el, character: $ec },
},
text: String::from($text),
..Default::default()
})),+]
};
}
let encoding = PositionEncoding::Wide(WideEncoding::Utf16);
let text = apply_document_changes(encoding, "", vec![]);
assert_eq!(text, "");
let text = apply_document_changes(
encoding,
&text,
vec![
lsp_types::TextDocumentContentChangeEvent::TextDocumentContentChangeWholeDocument(
lsp_types::TextDocumentContentChangeWholeDocument { text: String::from("the") },
),
],
);
assert_eq!(text, "the");
let text = apply_document_changes(encoding, &text, c![0, 3; 0, 3 => " quick"]);
assert_eq!(text, "the quick");
let text =
apply_document_changes(encoding, &text, c![0, 0; 0, 4 => "", 0, 5; 0, 5 => " foxes"]);
assert_eq!(text, "quick foxes");
let text = apply_document_changes(encoding, &text, c![0, 11; 0, 11 => "\ndream"]);
assert_eq!(text, "quick foxes\ndream");
let text = apply_document_changes(encoding, &text, c![1, 0; 1, 0 => "have "]);
assert_eq!(text, "quick foxes\nhave dream");
let text = apply_document_changes(
encoding,
&text,
c![0, 0; 0, 0 => "the ", 1, 4; 1, 4 => " quiet", 1, 16; 1, 16 => "s\n"],
);
assert_eq!(text, "the quick foxes\nhave quiet dreams\n");
let text =
apply_document_changes(encoding, &text, c![0, 15; 0, 15 => "\n", 2, 17; 2, 17 => "\n"]);
assert_eq!(text, "the quick foxes\n\nhave quiet dreams\n\n");
let text = apply_document_changes(
encoding,
&text,
c![1, 0; 1, 0 => "DREAM", 2, 0; 2, 0 => "they ", 3, 0; 3, 0 => "DON'T THEY?"],
);
assert_eq!(text, "the quick foxes\nDREAM\nthey have quiet dreams\nDON'T THEY?\n");
let text =
apply_document_changes(encoding, &text, c![0, 10; 1, 5 => "", 2, 0; 2, 12 => ""]);
assert_eq!(text, "the quick \nthey have quiet dreams\n");
let text = String::from("❤️");
let text = apply_document_changes(encoding, &text, c![0, 0; 0, 0 => "a"]);
assert_eq!(text, "a❤️");
let text = String::from("a\nb");
let text =
apply_document_changes(encoding, &text, c![0, 1; 1, 0 => "\nțc", 0, 1; 1, 1 => "d"]);
assert_eq!(text, "adcb");
let text = String::from("a\nb");
let text =
apply_document_changes(encoding, &text, c![0, 1; 1, 0 => "ț\nc", 0, 2; 0, 2 => "c"]);
assert_eq!(text, "ațc\ncb");
}
#[test]
fn empty_completion_disjoint_tests() {
let empty_completion = CompletionItem {
label: "label".to_owned(),
detail: Some("detail".to_owned()),
..Default::default()
};
let disjoint_edit_1 = lsp_types::TextEdit::new(
Range::new(Position::new(2, 2), Position::new(3, 3)),
"new_text".to_owned(),
);
let disjoint_edit_2 = lsp_types::TextEdit::new(
Range::new(Position::new(3, 3), Position::new(4, 4)),
"new_text".to_owned(),
);
let joint_edit = lsp_types::TextEdit::new(
Range::new(Position::new(1, 1), Position::new(5, 5)),
"new_text".to_owned(),
);
assert!(
all_edits_are_disjoint(&empty_completion, &[]),
"Empty completion has all its edits disjoint"
);
assert!(
all_edits_are_disjoint(
&empty_completion,
&[disjoint_edit_1.clone(), disjoint_edit_2.clone()]
),
"Empty completion is disjoint to whatever disjoint extra edits added"
);
assert!(
!all_edits_are_disjoint(
&empty_completion,
&[disjoint_edit_1, disjoint_edit_2, joint_edit]
),
"Empty completion does not prevent joint extra edits from failing the validation"
);
}
#[test]
fn completion_with_joint_edits_disjoint_tests() {
let disjoint_edit = lsp_types::TextEdit::new(
Range::new(Position::new(1, 1), Position::new(2, 2)),
"new_text".to_owned(),
);
let disjoint_edit_2 = lsp_types::TextEdit::new(
Range::new(Position::new(2, 2), Position::new(3, 3)),
"new_text".to_owned(),
);
let joint_edit = lsp_types::TextEdit::new(
Range::new(Position::new(1, 1), Position::new(5, 5)),
"new_text".to_owned(),
);
let mut completion_with_joint_edits = CompletionItem {
label: "label".to_owned(),
detail: Some("detail".to_owned()),
additional_text_edits: Some(vec![disjoint_edit.clone(), joint_edit.clone()]),
..Default::default()
};
assert!(
!all_edits_are_disjoint(&completion_with_joint_edits, &[]),
"Completion with disjoint edits fails the validation even with empty extra edits"
);
completion_with_joint_edits.text_edit =
Some(lsp_types::CompletionItemTextEdit::TextEdit(disjoint_edit.clone()));
completion_with_joint_edits.additional_text_edits = Some(vec![joint_edit.clone()]);
assert!(
!all_edits_are_disjoint(&completion_with_joint_edits, &[]),
"Completion with disjoint edits fails the validation even with empty extra edits"
);
completion_with_joint_edits.text_edit =
Some(lsp_types::CompletionItemTextEdit::InsertReplaceEdit(InsertReplaceEdit {
new_text: "new_text".to_owned(),
insert: disjoint_edit.range,
replace: disjoint_edit_2.range,
}));
completion_with_joint_edits.additional_text_edits = Some(vec![joint_edit]);
assert!(
!all_edits_are_disjoint(&completion_with_joint_edits, &[]),
"Completion with disjoint edits fails the validation even with empty extra edits"
);
}
#[test]
fn completion_with_disjoint_edits_disjoint_tests() {
let disjoint_edit = lsp_types::TextEdit::new(
Range::new(Position::new(1, 1), Position::new(2, 2)),
"new_text".to_owned(),
);
let disjoint_edit_2 = lsp_types::TextEdit::new(
Range::new(Position::new(2, 2), Position::new(3, 3)),
"new_text".to_owned(),
);
let joint_edit = lsp_types::TextEdit::new(
Range::new(Position::new(1, 1), Position::new(5, 5)),
"new_text".to_owned(),
);
let completion_with_disjoint_edits = CompletionItem {
label: "label".to_owned(),
detail: Some("detail".to_owned()),
text_edit: Some(lsp_types::CompletionItemTextEdit::TextEdit(disjoint_edit)),
..Default::default()
};
assert!(
all_edits_are_disjoint(&completion_with_disjoint_edits, &[]),
"Completion with disjoint edits is valid"
);
assert!(
!all_edits_are_disjoint(&completion_with_disjoint_edits, &[joint_edit]),
"Completion with disjoint edits and joint extra edit is invalid"
);
assert!(
all_edits_are_disjoint(&completion_with_disjoint_edits, &[disjoint_edit_2]),
"Completion with disjoint edits and joint extra edit is valid"
);
}
}