use std::path::Path;
use crate::editor::Cursor;
use crate::lsp::{
self, CodeAction, CompletionItem, Diagnostic, Hover, Location, LspEvent, TextEdit,
WorkspaceEdit,
};
use super::completion::{CompletionState, prefix_slice};
use super::{App, LspEventOutcome, Toast, root_cause};
impl App {
pub fn handle_lsp_event(&mut self, ev: LspEvent) {
match self.lsp.handle_event(ev) {
LspEventOutcome::Nothing => {}
LspEventOutcome::InfoMessage(s) => self.toast = Toast::info(s),
LspEventOutcome::ErrorMessage(s) => self.toast = Toast::error(s),
LspEventOutcome::Jump { label, locations } => self.apply_jump_outcome(label, locations),
LspEventOutcome::References(locations) => self.apply_references_outcome(locations),
LspEventOutcome::Rename { new_name, edit } => self.apply_rename_outcome(new_name, edit),
LspEventOutcome::CodeActions(actions) => self.apply_code_actions_outcome(actions),
LspEventOutcome::CodeActionResolved(action) => {
self.apply_code_action_resolved_outcome(action)
}
LspEventOutcome::Hover(hover) => self.apply_hover_outcome(hover),
LspEventOutcome::Completion {
prefix_start,
items,
} => self.apply_completion_outcome(prefix_start, items),
LspEventOutcome::CompletionResolved { uri, edits } => {
self.apply_completion_resolved_outcome(uri, edits)
}
}
}
fn apply_completion_outcome(&mut self, prefix_start: Cursor, items: Vec<CompletionItem>) {
let cursor = self.buffer.cursor;
if cursor.row != prefix_start.row || cursor.col < prefix_start.col {
return;
}
if items.is_empty() {
self.completion = None;
return;
}
let line = &self.buffer.lines[cursor.row];
let prefix = prefix_slice(line, prefix_start.col, cursor.col);
let state = CompletionState::new(prefix_start, items, &prefix);
if state.is_empty() {
self.completion = None;
return;
}
self.completion = Some(state);
}
fn apply_completion_resolved_outcome(&mut self, uri: String, edits: Vec<TextEdit>) {
if edits.is_empty() {
return;
}
let Some(current) = self.lsp.current_uri() else {
return;
};
if current != uri {
return;
}
let cursor_row = self.buffer.cursor.row;
let row_shift: i64 = edits
.iter()
.filter(|e| (e.range.start.line as usize) < cursor_row)
.map(|e| {
let added = e.new_text.matches('\n').count() as i64;
let removed = (e.range.end.line - e.range.start.line) as i64;
added - removed
})
.sum();
self.buffer.snapshot();
let mut lines = std::mem::take(&mut self.buffer.lines);
lsp::apply_text_edits(&mut lines, edits);
self.buffer.lines = lines;
let new_row = (cursor_row as i64 + row_shift).max(0) as usize;
let last = self.buffer.lines.len().saturating_sub(1);
self.buffer.cursor.row = new_row.min(last);
self.buffer.bump_version();
self.buffer.dirty = true;
}
fn apply_jump_outcome(&mut self, label: &'static str, locations: Vec<Location>) {
let Some(first) = locations.into_iter().next() else {
self.toast = Toast::info(format!("no {}", label));
return;
};
if let Err(e) = self.jump_to_location(&first) {
self.toast = Toast::error(format!("jump: {}", root_cause(&e)));
}
}
fn apply_references_outcome(&mut self, locations: Vec<Location>) {
if locations.is_empty() {
self.toast = Toast::info("no references");
return;
}
if locations.len() == 1 {
if let Err(e) = self.jump_to_location(&locations[0]) {
self.toast = Toast::error(format!("jump: {}", root_cause(&e)));
}
return;
}
let items: Vec<String> = locations
.iter()
.map(|loc| format_location_label(loc, &self.startup_cwd))
.collect();
self.prompt.open_locations(items, locations);
}
fn apply_code_actions_outcome(&mut self, actions: Vec<CodeAction>) {
if actions.is_empty() {
self.toast = Toast::info("no code actions");
return;
}
self.prompt.open_code_actions(actions);
}
fn apply_code_action_resolved_outcome(&mut self, action: Option<CodeAction>) {
let Some(action) = action else {
self.toast = Toast::error("code action: server returned no action");
return;
};
self.apply_code_action(action);
}
fn apply_rename_outcome(&mut self, new_name: String, edit: Option<WorkspaceEdit>) {
let Some(edit) = edit else {
self.toast = Toast::info("rename: nothing to change");
return;
};
match self.lsp.apply_workspace_edit(edit) {
Ok(result) => {
if !result.current_buffer_edits.is_empty() {
self.buffer.snapshot();
let mut lines = std::mem::take(&mut self.buffer.lines);
lsp::apply_text_edits(&mut lines, result.current_buffer_edits);
self.buffer.lines = lines;
self.buffer.bump_version();
self.buffer.dirty = true;
}
self.toast = Toast::info(format!(
"renamed to {} ({} occurrences in {} files)",
new_name, result.total_edits, result.files_touched
));
}
Err(e) => {
self.toast = Toast::error(format!("rename: {}", root_cause(&e)));
}
}
}
fn apply_hover_outcome(&mut self, hover: Option<Hover>) {
let Some(h) = hover else {
self.toast = Toast::info("no hover info");
return;
};
self.prompt.open_hover(h.contents);
}
pub(super) fn apply_code_action(&mut self, action: CodeAction) {
let title = action.title.clone();
let Some(edit) = action.edit else {
self.toast = Toast::info(format!("code action: {} (no edit)", title));
return;
};
match self.lsp.apply_workspace_edit(edit) {
Ok(result) => {
if !result.current_buffer_edits.is_empty() {
self.buffer.snapshot();
let mut lines = std::mem::take(&mut self.buffer.lines);
lsp::apply_text_edits(&mut lines, result.current_buffer_edits);
self.buffer.lines = lines;
self.buffer.bump_version();
self.buffer.dirty = true;
}
self.toast = Toast::info(format!(
"{} ({} edits in {} files)",
title, result.total_edits, result.files_touched
));
}
Err(e) => {
self.toast = Toast::error(format!("code action: {}", root_cause(&e)));
}
}
}
pub fn current_diagnostics(&self) -> Option<Vec<Diagnostic>> {
self.lsp.current_diagnostics()
}
}
fn format_location_label(loc: &Location, root: &Path) -> String {
let path = match lsp::uri_to_path(&loc.uri) {
Some(p) => p,
None => return loc.uri.clone(),
};
let path_c = path.canonicalize().unwrap_or_else(|_| path.clone());
let root_c = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
let shown = path_c
.strip_prefix(&root_c)
.unwrap_or(&path_c)
.to_string_lossy()
.into_owned();
format!(
"{}:{}:{}",
shown,
loc.range.start.line + 1,
loc.range.start.character + 1
)
}