1use super::Editor;
6use crate::model::event::Event;
7use crate::primitives::snippet::{expand_snippet, is_snippet};
8use crate::primitives::word_navigation::find_completion_word_start;
9use rust_i18n::t;
10
11pub enum PopupConfirmResult {
13 Done,
15 EarlyReturn,
17}
18
19impl Editor {
20 pub fn handle_popup_confirm(&mut self) -> PopupConfirmResult {
34 use crate::view::popup::PopupResolver;
35
36 let resolver = if self.global_popups.is_visible() {
40 self.global_popups.top().map(|p| p.resolver.clone())
41 } else {
42 self.active_state().popups.top().map(|p| p.resolver.clone())
43 };
44
45 match resolver {
46 Some(PopupResolver::PluginAction { popup_id }) => {
47 let action_id = self
48 .global_popups
49 .top()
50 .or_else(|| self.active_state().popups.top())
51 .and_then(|p| p.selected_item())
52 .and_then(|item| item.data.clone())
53 .unwrap_or_else(|| "dismissed".to_string());
54 self.hide_popup();
55 self.plugin_manager.read().unwrap().run_hook(
56 "action_popup_result",
57 crate::services::plugins::hooks::HookArgs::ActionPopupResult {
58 popup_id,
59 action_id,
60 },
61 );
62 PopupConfirmResult::EarlyReturn
63 }
64
65 Some(PopupResolver::LspStatus) => {
66 let action_key = self
67 .active_state()
68 .popups
69 .top()
70 .and_then(|p| p.selected_item())
71 .and_then(|item| item.data.clone());
72 self.hide_popup();
73 if let Some(key) = action_key {
74 self.handle_lsp_status_action(&key);
75 }
76 PopupConfirmResult::EarlyReturn
77 }
78
79 Some(PopupResolver::CodeAction) => {
80 let selected_index = self
81 .active_state()
82 .popups
83 .top()
84 .and_then(|p| p.selected_item())
85 .and_then(|item| item.data.as_ref())
86 .and_then(|data| data.parse::<usize>().ok());
87 self.hide_popup();
88 if let Some(index) = selected_index {
89 self.execute_code_action(index);
90 }
91 self.active_window_mut().pending_code_actions = None;
92 PopupConfirmResult::EarlyReturn
93 }
94
95 Some(PopupResolver::LspConfirm { language }) => {
96 let action = self
97 .active_state()
98 .popups
99 .top()
100 .and_then(|p| p.selected_item())
101 .and_then(|item| item.data.clone());
102 if let Some(action) = action {
103 self.hide_popup();
104 self.handle_lsp_confirmation_response(&language, &action);
105 PopupConfirmResult::EarlyReturn
106 } else {
107 self.hide_popup();
108 PopupConfirmResult::EarlyReturn
109 }
110 }
111
112 Some(PopupResolver::RemoteIndicator) => {
113 let action_key = self
114 .active_state()
115 .popups
116 .top()
117 .and_then(|p| p.selected_item())
118 .and_then(|item| item.data.clone());
119 self.hide_popup();
120 if let Some(key) = action_key {
121 self.handle_remote_indicator_action(&key);
122 }
123 PopupConfirmResult::EarlyReturn
124 }
125
126 Some(PopupResolver::ReadOnly) => {
127 let action_key = self
128 .active_state()
129 .popups
130 .top()
131 .and_then(|p| p.selected_item())
132 .and_then(|item| item.data.clone());
133 self.hide_popup();
134 if let Some(key) = action_key {
135 self.handle_read_only_menu_action(&key);
136 }
137 PopupConfirmResult::EarlyReturn
138 }
139
140 Some(PopupResolver::WorkspaceTrust) => {
141 let action_key = self
144 .global_popups
145 .top()
146 .or_else(|| self.active_state().popups.top())
147 .and_then(|p| p.selected_item())
148 .and_then(|item| item.data.clone());
149 self.hide_popup();
150 if let Some(key) = action_key {
151 self.handle_workspace_trust_action(&key);
152 }
153 PopupConfirmResult::EarlyReturn
154 }
155
156 Some(PopupResolver::Completion) => {
157 let completion_info = self
161 .active_state()
162 .popups
163 .top()
164 .and_then(|p| p.selected_item())
165 .map(|item| (item.text.clone(), item.data.clone()));
166 if let Some((label, insert_text)) = completion_info {
167 if let Some(text) = insert_text {
168 self.insert_completion_text(text);
169 }
170 self.apply_completion_additional_edits(&label);
171 }
172 self.hide_popup();
173 PopupConfirmResult::Done
174 }
175
176 Some(PopupResolver::SettingsSaveError { layer }) => {
177 self.hide_popup();
180 if let Err(e) = self.open_config_file(layer) {
181 tracing::warn!("Failed to open config file after save error: {}", e);
182 }
183 PopupConfirmResult::Done
184 }
185
186 Some(PopupResolver::None) | None => {
187 self.hide_popup();
188 PopupConfirmResult::Done
189 }
190 }
191 }
192
193 fn insert_completion_text(&mut self, text: String) {
201 use crate::model::event::CursorId;
202
203 let (insert_text, cursor_offset) = if is_snippet(&text) {
205 let expanded = expand_snippet(&text);
206 (expanded.text, Some(expanded.cursor_offset))
207 } else {
208 (text, None)
209 };
210
211 let cursor_data: Vec<(CursorId, usize, usize, String)> = {
213 let positions: Vec<(CursorId, usize)> = self
214 .active_cursors()
215 .iter()
216 .map(|(id, c)| (id, c.position))
217 .collect();
218 positions
219 .into_iter()
220 .map(|(id, pos)| {
221 let word_start = {
222 let state = self.active_state();
223 find_completion_word_start(&state.buffer, pos)
224 };
225 let prefix = if word_start < pos {
226 self.active_state_mut().get_text_range(word_start, pos)
227 } else {
228 String::new()
229 };
230 (id, pos, word_start, prefix)
231 })
232 .collect()
233 };
234
235 let mut events: Vec<Event> = Vec::new();
238 for (cursor_id, pos, word_start, prefix) in &cursor_data {
239 if *word_start < *pos {
240 events.push(Event::Delete {
241 range: *word_start..*pos,
242 deleted_text: prefix.clone(),
243 cursor_id: *cursor_id,
244 });
245 }
246 events.push(Event::Insert {
247 position: *word_start,
248 text: insert_text.clone(),
249 cursor_id: *cursor_id,
250 });
251 }
252
253 if events.is_empty() {
254 return;
255 }
256
257 let description = "Accept completion".to_string();
258 if cursor_data.len() > 1 || events.len() > 1 {
259 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description) {
261 self.active_event_log_mut().append(bulk_edit);
262 }
263 } else {
264 for event in events {
265 self.log_and_apply_event(&event);
266 }
267 }
268
269 if let Some(offset) = cursor_offset {
273 if offset != insert_text.len() {
274 let move_events: Vec<Event> = self
275 .active_cursors()
276 .iter()
277 .map(|(cursor_id, cursor)| {
278 let current = cursor.position;
279 let target = current.saturating_sub(insert_text.len()) + offset;
280 Event::MoveCursor {
281 cursor_id,
282 old_position: current,
283 new_position: target,
284 old_anchor: cursor.anchor,
285 new_anchor: None,
286 old_sticky_column: cursor.sticky_column,
287 new_sticky_column: 0,
288 }
289 })
290 .collect();
291 for event in move_events {
292 self.log_and_apply_event(&event);
293 }
294 }
295 }
296 }
297
298 fn apply_completion_additional_edits(&mut self, label: &str) {
303 let item = self
305 .active_window_mut()
306 .completion_items
307 .as_ref()
308 .and_then(|items| items.iter().find(|item| item.label == label).cloned());
309
310 let Some(item) = item else { return };
311
312 if let Some(edits) = &item.additional_text_edits {
313 if !edits.is_empty() {
314 tracing::info!(
315 "Applying {} additional text edits from completion '{}'",
316 edits.len(),
317 label
318 );
319 let buffer_id = self.active_buffer();
320 if let Err(e) = self.apply_lsp_text_edits(buffer_id, edits.clone()) {
321 tracing::error!("Failed to apply completion additional_text_edits: {}", e);
322 }
323 return;
324 }
325 }
326
327 if self.active_window().server_supports_completion_resolve() {
329 tracing::info!(
330 "Completion '{}' has no additional_text_edits, sending completionItem/resolve",
331 label
332 );
333 self.active_window_mut().send_completion_resolve(item);
334 }
335 }
336
337 pub fn handle_popup_cancel(&mut self) {
343 use crate::view::popup::PopupResolver;
344
345 let resolver = if self.global_popups.is_visible() {
346 self.global_popups.top().map(|p| p.resolver.clone())
347 } else {
348 self.active_state().popups.top().map(|p| p.resolver.clone())
349 };
350
351 match resolver {
352 Some(PopupResolver::PluginAction { popup_id }) => {
353 tracing::info!(
354 "handle_popup_cancel: dismissing action popup id={}",
355 popup_id
356 );
357 self.hide_popup();
358 self.plugin_manager.read().unwrap().run_hook(
359 "action_popup_result",
360 crate::services::plugins::hooks::HookArgs::ActionPopupResult {
361 popup_id,
362 action_id: "dismissed".to_string(),
363 },
364 );
365 }
366
367 Some(PopupResolver::LspStatus) => {
368 self.hide_popup();
369 }
370
371 Some(PopupResolver::CodeAction) => {
372 self.active_window_mut().pending_code_actions = None;
373 self.hide_popup();
374 }
375
376 Some(PopupResolver::LspConfirm { language: _ }) => {
377 self.set_status_message(t!("lsp.startup_cancelled_msg").to_string());
378 self.hide_popup();
379 }
380
381 Some(PopupResolver::Completion) => {
382 self.hide_popup();
383 self.active_window_mut().completion_items = None;
384 }
385
386 Some(PopupResolver::RemoteIndicator) => {
387 self.hide_popup();
388 }
389
390 Some(PopupResolver::ReadOnly) => {
391 self.hide_popup();
392 }
393
394 Some(PopupResolver::WorkspaceTrust) => {
395 }
399
400 Some(PopupResolver::SettingsSaveError { layer }) => {
401 self.hide_popup();
404 if let Err(e) = self.open_config_file(layer) {
405 tracing::warn!("Failed to open config file after save error: {}", e);
406 }
407 }
408
409 Some(PopupResolver::None) | None => {
410 self.hide_popup();
411 self.active_window_mut().completion_items = None;
412 }
413 }
414 }
415
416 pub(crate) fn completion_accept_key_hint(&self) -> Option<String> {
419 Some("Tab".to_string())
421 }
422
423 pub(crate) fn popup_focus_key_hint(&self) -> Option<String> {
428 let kb = self.keybindings.read().ok()?;
429 kb.get_keybinding_for_action(
439 &crate::input::keybindings::Action::PopupFocus,
440 crate::input::keybindings::KeyContext::Normal,
441 )
442 .or_else(|| {
443 kb.get_keybinding_for_action(
444 &crate::input::keybindings::Action::PopupFocus,
445 crate::input::keybindings::KeyContext::FileExplorer,
446 )
447 })
448 .or_else(|| Some("Alt+T".to_string()))
449 }
450
451 pub fn handle_popup_focus(&mut self) {
462 if let Some(popup) = self.global_popups.top_mut() {
463 popup.focused = true;
464 return;
465 }
466 if let Some(popup) = self.active_state_mut().popups.top_mut() {
467 popup.focused = true;
468 }
469 }
470
471 pub fn handle_popup_type_char(&mut self, c: char) {
478 use crate::input::keybindings::Action;
479
480 if let Some(events) = self
481 .active_window_mut()
482 .action_to_events(Action::InsertChar(c))
483 {
484 if events.len() > 1 {
485 let description = format!("Insert '{}'", c);
486 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description) {
487 self.active_event_log_mut().append(bulk_edit);
488 }
489 } else {
490 for event in events {
491 self.log_and_apply_event(&event);
492 }
493 }
494 }
495
496 self.refilter_completion_popup();
497 }
498
499 pub fn handle_popup_backspace(&mut self) {
507 use crate::input::keybindings::Action;
508
509 if let Some(events) = self
510 .active_window_mut()
511 .action_to_events(Action::DeleteBackward)
512 {
513 if events.len() > 1 {
514 if let Some(bulk_edit) =
515 self.apply_events_as_bulk_edit(events, "Backspace".to_string())
516 {
517 self.active_event_log_mut().append(bulk_edit);
518 }
519 } else {
520 for event in events {
521 self.log_and_apply_event(&event);
522 }
523 }
524 }
525
526 self.refilter_completion_popup();
527 }
528
529 fn refilter_completion_popup(&mut self) {
532 let lsp_items = self
534 .active_window_mut()
535 .completion_items
536 .clone()
537 .unwrap_or_default();
538
539 let (word_start, cursor_pos) = {
541 let cursor_pos = self.active_cursors().primary().position;
542 let state = self.active_state();
543 let word_start = find_completion_word_start(&state.buffer, cursor_pos);
544 (word_start, cursor_pos)
545 };
546
547 let prefix = if word_start < cursor_pos {
548 self.active_state_mut()
549 .get_text_range(word_start, cursor_pos)
550 .to_lowercase()
551 } else {
552 String::new()
553 };
554
555 let filtered_lsp: Vec<&lsp_types::CompletionItem> = if prefix.is_empty() {
557 lsp_items.iter().collect()
558 } else {
559 lsp_items
560 .iter()
561 .filter(|item| {
562 item.label.to_lowercase().starts_with(&prefix)
563 || item
564 .filter_text
565 .as_ref()
566 .map(|ft| ft.to_lowercase().starts_with(&prefix))
567 .unwrap_or(false)
568 })
569 .collect()
570 };
571
572 let mut all_popup_items = lsp_items_to_popup_items(&filtered_lsp);
574 let buffer_word_items = self.get_buffer_completion_popup_items();
575 let lsp_labels: std::collections::HashSet<String> = all_popup_items
576 .iter()
577 .map(|i| i.text.to_lowercase())
578 .collect();
579 all_popup_items.extend(
580 buffer_word_items
581 .into_iter()
582 .filter(|item| !lsp_labels.contains(&item.text.to_lowercase())),
583 );
584
585 if all_popup_items.is_empty() {
587 self.hide_popup();
588 self.active_window_mut().completion_items = None;
589 return;
590 }
591
592 let current_selection = self
594 .active_state()
595 .popups
596 .top()
597 .and_then(|p| p.selected_item())
598 .map(|item| item.text.clone());
599
600 let selected = current_selection
602 .and_then(|sel| all_popup_items.iter().position(|item| item.text == sel))
603 .unwrap_or(0);
604
605 let popup_data = build_completion_popup_from_items(all_popup_items, selected);
606 let accept_hint = self.completion_accept_key_hint();
607 let (popup_bg, popup_border_fg) = {
608 let theme = self.theme();
609 (theme.popup_bg, theme.popup_border_fg)
610 };
611
612 self.hide_popup();
614 let buffer_id = self.active_buffer();
615 let state = self
616 .windows
617 .get_mut(&self.active_window)
618 .map(|w| &mut w.buffers)
619 .expect("active window present")
620 .get_mut(&buffer_id)
621 .unwrap();
622 let mut popup_obj =
623 crate::state::convert_popup_data_to_popup(&popup_data, popup_bg, popup_border_fg);
624 popup_obj.accept_key_hint = accept_hint;
625 popup_obj.resolver = crate::view::popup::PopupResolver::Completion;
626 state.popups.show_or_replace(popup_obj);
627 }
628}
629
630pub(crate) fn build_completion_popup_from_items(
634 items: Vec<crate::model::event::PopupListItemData>,
635 selected: usize,
636) -> crate::model::event::PopupData {
637 use crate::model::event::{PopupContentData, PopupKindHint, PopupPositionData};
638
639 crate::model::event::PopupData {
640 kind: PopupKindHint::Completion,
641 title: None,
642 description: None,
643 transient: false,
644 content: PopupContentData::List { items, selected },
645 position: PopupPositionData::BelowCursor,
646 width: 50,
647 max_height: 15,
648 bordered: true,
649 }
650}
651
652pub(crate) fn lsp_items_to_popup_items(
654 items: &[&lsp_types::CompletionItem],
655) -> Vec<crate::model::event::PopupListItemData> {
656 use crate::model::event::PopupListItemData;
657
658 items
659 .iter()
660 .map(|item| {
661 let icon = match item.kind {
662 Some(lsp_types::CompletionItemKind::FUNCTION)
663 | Some(lsp_types::CompletionItemKind::METHOD) => Some("λ".to_string()),
664 Some(lsp_types::CompletionItemKind::VARIABLE) => Some("v".to_string()),
665 Some(lsp_types::CompletionItemKind::STRUCT)
666 | Some(lsp_types::CompletionItemKind::CLASS) => Some("S".to_string()),
667 Some(lsp_types::CompletionItemKind::CONSTANT) => Some("c".to_string()),
668 Some(lsp_types::CompletionItemKind::KEYWORD) => Some("k".to_string()),
669 _ => None,
670 };
671
672 PopupListItemData {
673 text: item.label.clone(),
674 detail: item.detail.clone(),
675 icon,
676 data: item
677 .insert_text
678 .clone()
679 .or_else(|| Some(item.label.clone())),
680 }
681 })
682 .collect()
683}