use super::Editor;
use crate::model::event::Event;
use crate::primitives::snippet::{expand_snippet, is_snippet};
use crate::primitives::word_navigation::find_completion_word_start;
use rust_i18n::t;
pub enum PopupConfirmResult {
Done,
EarlyReturn,
}
impl Editor {
pub fn handle_popup_confirm(&mut self) -> PopupConfirmResult {
if self.pending_lsp_status_popup.is_some() {
let action_key = self
.active_state()
.popups
.top()
.and_then(|p| p.selected_item())
.and_then(|item| item.data.clone());
self.hide_popup();
self.pending_lsp_status_popup = None;
if let Some(key) = action_key {
self.handle_lsp_status_action(&key);
}
return PopupConfirmResult::EarlyReturn;
}
if let Some((popup_id, _actions)) = &self.active_action_popup {
let popup_id = popup_id.clone();
let action_id = self
.active_state()
.popups
.top()
.and_then(|p| p.selected_item())
.and_then(|item| item.data.clone())
.unwrap_or_else(|| "dismissed".to_string());
self.hide_popup();
self.active_action_popup = None;
self.plugin_manager.run_hook(
"action_popup_result",
crate::services::plugins::hooks::HookArgs::ActionPopupResult {
popup_id,
action_id,
},
);
return PopupConfirmResult::EarlyReturn;
}
if self.pending_code_actions.is_some() {
let selected_index = self
.active_state()
.popups
.top()
.and_then(|p| p.selected_item())
.and_then(|item| item.data.as_ref())
.and_then(|data| data.parse::<usize>().ok());
self.hide_popup();
if let Some(index) = selected_index {
self.execute_code_action(index);
}
self.pending_code_actions = None;
return PopupConfirmResult::EarlyReturn;
}
if self.pending_lsp_confirmation.is_some() {
let action = self
.active_state()
.popups
.top()
.and_then(|p| p.selected_item())
.and_then(|item| item.data.clone());
if let Some(action) = action {
self.hide_popup();
self.handle_lsp_confirmation_response(&action);
return PopupConfirmResult::EarlyReturn;
}
}
let completion_info = self
.active_state()
.popups
.top()
.filter(|p| p.kind == crate::view::popup::PopupKind::Completion)
.and_then(|p| p.selected_item())
.map(|item| (item.text.clone(), item.data.clone()));
if let Some((label, insert_text)) = completion_info {
if let Some(text) = insert_text {
self.insert_completion_text(text);
}
self.apply_completion_additional_edits(&label);
}
self.hide_popup();
PopupConfirmResult::Done
}
fn insert_completion_text(&mut self, text: String) {
let (insert_text, cursor_offset) = if is_snippet(&text) {
let expanded = expand_snippet(&text);
(expanded.text, Some(expanded.cursor_offset))
} else {
(text, None)
};
let (cursor_id, cursor_pos, word_start) = {
let cursors = self.active_cursors();
let cursor_id = cursors.primary_id();
let cursor_pos = cursors.primary().position;
let state = self.active_state();
let word_start = find_completion_word_start(&state.buffer, cursor_pos);
(cursor_id, cursor_pos, word_start)
};
let deleted_text = if word_start < cursor_pos {
self.active_state_mut()
.get_text_range(word_start, cursor_pos)
} else {
String::new()
};
let insert_pos = if word_start < cursor_pos {
let delete_event = Event::Delete {
range: word_start..cursor_pos,
deleted_text,
cursor_id,
};
self.log_and_apply_event(&delete_event);
let buffer_len = self.active_state().buffer.len();
word_start.min(buffer_len)
} else {
cursor_pos
};
let insert_event = Event::Insert {
position: insert_pos,
text: insert_text.clone(),
cursor_id,
};
self.log_and_apply_event(&insert_event);
if let Some(offset) = cursor_offset {
let new_cursor_pos = insert_pos + offset;
let current_pos = self.active_cursors().primary().position;
if current_pos != new_cursor_pos {
let move_event = Event::MoveCursor {
cursor_id,
old_position: current_pos,
new_position: new_cursor_pos,
old_anchor: None,
new_anchor: None,
old_sticky_column: 0,
new_sticky_column: 0,
};
let split_id = self.split_manager.active_split();
let buffer_id = self.active_buffer();
let state = self.buffers.get_mut(&buffer_id).unwrap();
let cursors = &mut self.split_view_states.get_mut(&split_id).unwrap().cursors;
state.apply(cursors, &move_event);
}
}
}
fn apply_completion_additional_edits(&mut self, label: &str) {
let item = self
.completion_items
.as_ref()
.and_then(|items| items.iter().find(|item| item.label == label).cloned());
let Some(item) = item else { return };
if let Some(edits) = &item.additional_text_edits {
if !edits.is_empty() {
tracing::info!(
"Applying {} additional text edits from completion '{}'",
edits.len(),
label
);
let buffer_id = self.active_buffer();
if let Err(e) = self.apply_lsp_text_edits(buffer_id, edits.clone()) {
tracing::error!("Failed to apply completion additional_text_edits: {}", e);
}
return;
}
}
if self.server_supports_completion_resolve() {
tracing::info!(
"Completion '{}' has no additional_text_edits, sending completionItem/resolve",
label
);
self.send_completion_resolve(item);
}
}
pub fn handle_popup_cancel(&mut self) {
tracing::info!(
"handle_popup_cancel: active_action_popup={:?}",
self.active_action_popup.as_ref().map(|(id, _)| id)
);
if self.pending_lsp_status_popup.take().is_some() {
self.hide_popup();
return;
}
if let Some((popup_id, _actions)) = self.active_action_popup.take() {
tracing::info!(
"handle_popup_cancel: dismissing action popup id={}",
popup_id
);
self.hide_popup();
self.plugin_manager.run_hook(
"action_popup_result",
crate::services::plugins::hooks::HookArgs::ActionPopupResult {
popup_id,
action_id: "dismissed".to_string(),
},
);
tracing::info!("handle_popup_cancel: action_popup_result hook fired");
return;
}
if self.pending_code_actions.is_some() {
self.pending_code_actions = None;
self.hide_popup();
return;
}
if self.pending_lsp_confirmation.is_some() {
self.pending_lsp_confirmation = None;
self.set_status_message(t!("lsp.startup_cancelled_msg").to_string());
}
self.hide_popup();
self.completion_items = None;
}
pub(crate) fn completion_accept_key_hint(&self) -> Option<String> {
Some("Tab".to_string())
}
pub fn handle_popup_type_char(&mut self, c: char) {
let (cursor_id, cursor_pos) = {
let cursors = self.active_cursors();
(cursors.primary_id(), cursors.primary().position)
};
let insert_event = Event::Insert {
position: cursor_pos,
text: c.to_string(),
cursor_id,
};
self.log_and_apply_event(&insert_event);
self.refilter_completion_popup();
}
pub fn handle_popup_backspace(&mut self) {
let (cursor_id, cursor_pos) = {
let cursors = self.active_cursors();
(cursors.primary_id(), cursors.primary().position)
};
if cursor_pos == 0 {
return;
}
let prev_pos = {
let state = self.active_state();
let text = match state.buffer.to_string() {
Some(t) => t,
None => return,
};
text[..cursor_pos]
.char_indices()
.last()
.map(|(i, _)| i)
.unwrap_or(0)
};
let deleted_text = self.active_state_mut().get_text_range(prev_pos, cursor_pos);
let delete_event = Event::Delete {
range: prev_pos..cursor_pos,
deleted_text,
cursor_id,
};
self.log_and_apply_event(&delete_event);
self.refilter_completion_popup();
}
fn refilter_completion_popup(&mut self) {
let lsp_items = self.completion_items.clone().unwrap_or_default();
let (word_start, cursor_pos) = {
let cursor_pos = self.active_cursors().primary().position;
let state = self.active_state();
let word_start = find_completion_word_start(&state.buffer, cursor_pos);
(word_start, cursor_pos)
};
let prefix = if word_start < cursor_pos {
self.active_state_mut()
.get_text_range(word_start, cursor_pos)
.to_lowercase()
} else {
String::new()
};
let filtered_lsp: Vec<&lsp_types::CompletionItem> = if prefix.is_empty() {
lsp_items.iter().collect()
} else {
lsp_items
.iter()
.filter(|item| {
item.label.to_lowercase().starts_with(&prefix)
|| item
.filter_text
.as_ref()
.map(|ft| ft.to_lowercase().starts_with(&prefix))
.unwrap_or(false)
})
.collect()
};
let mut all_popup_items = lsp_items_to_popup_items(&filtered_lsp);
let buffer_word_items = self.get_buffer_completion_popup_items();
let lsp_labels: std::collections::HashSet<String> = all_popup_items
.iter()
.map(|i| i.text.to_lowercase())
.collect();
all_popup_items.extend(
buffer_word_items
.into_iter()
.filter(|item| !lsp_labels.contains(&item.text.to_lowercase())),
);
if all_popup_items.is_empty() {
self.hide_popup();
self.completion_items = None;
return;
}
let current_selection = self
.active_state()
.popups
.top()
.and_then(|p| p.selected_item())
.map(|item| item.text.clone());
let selected = current_selection
.and_then(|sel| all_popup_items.iter().position(|item| item.text == sel))
.unwrap_or(0);
let popup_data = build_completion_popup_from_items(all_popup_items, selected);
let accept_hint = self.completion_accept_key_hint();
self.hide_popup();
let buffer_id = self.active_buffer();
let state = self.buffers.get_mut(&buffer_id).unwrap();
let mut popup_obj = crate::state::convert_popup_data_to_popup(&popup_data);
popup_obj.accept_key_hint = accept_hint;
state.popups.show_or_replace(popup_obj);
}
}
pub(crate) fn build_completion_popup_from_items(
items: Vec<crate::model::event::PopupListItemData>,
selected: usize,
) -> crate::model::event::PopupData {
use crate::model::event::{PopupContentData, PopupKindHint, PopupPositionData};
crate::model::event::PopupData {
kind: PopupKindHint::Completion,
title: None,
description: None,
transient: false,
content: PopupContentData::List { items, selected },
position: PopupPositionData::BelowCursor,
width: 50,
max_height: 15,
bordered: true,
}
}
pub(crate) fn lsp_items_to_popup_items(
items: &[&lsp_types::CompletionItem],
) -> Vec<crate::model::event::PopupListItemData> {
use crate::model::event::PopupListItemData;
items
.iter()
.map(|item| {
let icon = match item.kind {
Some(lsp_types::CompletionItemKind::FUNCTION)
| Some(lsp_types::CompletionItemKind::METHOD) => Some("λ".to_string()),
Some(lsp_types::CompletionItemKind::VARIABLE) => Some("v".to_string()),
Some(lsp_types::CompletionItemKind::STRUCT)
| Some(lsp_types::CompletionItemKind::CLASS) => Some("S".to_string()),
Some(lsp_types::CompletionItemKind::CONSTANT) => Some("c".to_string()),
Some(lsp_types::CompletionItemKind::KEYWORD) => Some("k".to_string()),
_ => None,
};
PopupListItemData {
text: item.label.clone(),
detail: item.detail.clone(),
icon,
data: item
.insert_text
.clone()
.or_else(|| Some(item.label.clone())),
}
})
.collect()
}