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::Completion) => {
127 let completion_info = self
131 .active_state()
132 .popups
133 .top()
134 .and_then(|p| p.selected_item())
135 .map(|item| (item.text.clone(), item.data.clone()));
136 if let Some((label, insert_text)) = completion_info {
137 if let Some(text) = insert_text {
138 self.insert_completion_text(text);
139 }
140 self.apply_completion_additional_edits(&label);
141 }
142 self.hide_popup();
143 PopupConfirmResult::Done
144 }
145
146 Some(PopupResolver::None) | None => {
147 self.hide_popup();
148 PopupConfirmResult::Done
149 }
150 }
151 }
152
153 fn insert_completion_text(&mut self, text: String) {
161 use crate::model::event::CursorId;
162
163 let (insert_text, cursor_offset) = if is_snippet(&text) {
165 let expanded = expand_snippet(&text);
166 (expanded.text, Some(expanded.cursor_offset))
167 } else {
168 (text, None)
169 };
170
171 let cursor_data: Vec<(CursorId, usize, usize, String)> = {
173 let positions: Vec<(CursorId, usize)> = self
174 .active_cursors()
175 .iter()
176 .map(|(id, c)| (id, c.position))
177 .collect();
178 positions
179 .into_iter()
180 .map(|(id, pos)| {
181 let word_start = {
182 let state = self.active_state();
183 find_completion_word_start(&state.buffer, pos)
184 };
185 let prefix = if word_start < pos {
186 self.active_state_mut().get_text_range(word_start, pos)
187 } else {
188 String::new()
189 };
190 (id, pos, word_start, prefix)
191 })
192 .collect()
193 };
194
195 let mut events: Vec<Event> = Vec::new();
198 for (cursor_id, pos, word_start, prefix) in &cursor_data {
199 if *word_start < *pos {
200 events.push(Event::Delete {
201 range: *word_start..*pos,
202 deleted_text: prefix.clone(),
203 cursor_id: *cursor_id,
204 });
205 }
206 events.push(Event::Insert {
207 position: *word_start,
208 text: insert_text.clone(),
209 cursor_id: *cursor_id,
210 });
211 }
212
213 if events.is_empty() {
214 return;
215 }
216
217 let description = "Accept completion".to_string();
218 if cursor_data.len() > 1 || events.len() > 1 {
219 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description) {
221 self.active_event_log_mut().append(bulk_edit);
222 }
223 } else {
224 for event in events {
225 self.log_and_apply_event(&event);
226 }
227 }
228
229 if let Some(offset) = cursor_offset {
233 if offset != insert_text.len() {
234 let move_events: Vec<Event> = self
235 .active_cursors()
236 .iter()
237 .map(|(cursor_id, cursor)| {
238 let current = cursor.position;
239 let target = current.saturating_sub(insert_text.len()) + offset;
240 Event::MoveCursor {
241 cursor_id,
242 old_position: current,
243 new_position: target,
244 old_anchor: cursor.anchor,
245 new_anchor: None,
246 old_sticky_column: cursor.sticky_column,
247 new_sticky_column: 0,
248 }
249 })
250 .collect();
251 for event in move_events {
252 self.log_and_apply_event(&event);
253 }
254 }
255 }
256 }
257
258 fn apply_completion_additional_edits(&mut self, label: &str) {
263 let item = self
265 .active_window_mut()
266 .completion_items
267 .as_ref()
268 .and_then(|items| items.iter().find(|item| item.label == label).cloned());
269
270 let Some(item) = item else { return };
271
272 if let Some(edits) = &item.additional_text_edits {
273 if !edits.is_empty() {
274 tracing::info!(
275 "Applying {} additional text edits from completion '{}'",
276 edits.len(),
277 label
278 );
279 let buffer_id = self.active_buffer();
280 if let Err(e) = self.apply_lsp_text_edits(buffer_id, edits.clone()) {
281 tracing::error!("Failed to apply completion additional_text_edits: {}", e);
282 }
283 return;
284 }
285 }
286
287 if self.active_window().server_supports_completion_resolve() {
289 tracing::info!(
290 "Completion '{}' has no additional_text_edits, sending completionItem/resolve",
291 label
292 );
293 self.active_window_mut().send_completion_resolve(item);
294 }
295 }
296
297 pub fn handle_popup_cancel(&mut self) {
303 use crate::view::popup::PopupResolver;
304
305 let resolver = if self.global_popups.is_visible() {
306 self.global_popups.top().map(|p| p.resolver.clone())
307 } else {
308 self.active_state().popups.top().map(|p| p.resolver.clone())
309 };
310
311 match resolver {
312 Some(PopupResolver::PluginAction { popup_id }) => {
313 tracing::info!(
314 "handle_popup_cancel: dismissing action popup id={}",
315 popup_id
316 );
317 self.hide_popup();
318 self.plugin_manager.read().unwrap().run_hook(
319 "action_popup_result",
320 crate::services::plugins::hooks::HookArgs::ActionPopupResult {
321 popup_id,
322 action_id: "dismissed".to_string(),
323 },
324 );
325 }
326
327 Some(PopupResolver::LspStatus) => {
328 self.hide_popup();
329 }
330
331 Some(PopupResolver::CodeAction) => {
332 self.active_window_mut().pending_code_actions = None;
333 self.hide_popup();
334 }
335
336 Some(PopupResolver::LspConfirm { language: _ }) => {
337 self.set_status_message(t!("lsp.startup_cancelled_msg").to_string());
338 self.hide_popup();
339 }
340
341 Some(PopupResolver::Completion) => {
342 self.hide_popup();
343 self.active_window_mut().completion_items = None;
344 }
345
346 Some(PopupResolver::RemoteIndicator) => {
347 self.hide_popup();
348 }
349
350 Some(PopupResolver::None) | None => {
351 self.hide_popup();
352 self.active_window_mut().completion_items = None;
353 }
354 }
355 }
356
357 pub(crate) fn completion_accept_key_hint(&self) -> Option<String> {
360 Some("Tab".to_string())
362 }
363
364 pub(crate) fn popup_focus_key_hint(&self) -> Option<String> {
369 let kb = self.keybindings.read().ok()?;
370 kb.get_keybinding_for_action(
380 &crate::input::keybindings::Action::PopupFocus,
381 crate::input::keybindings::KeyContext::Normal,
382 )
383 .or_else(|| {
384 kb.get_keybinding_for_action(
385 &crate::input::keybindings::Action::PopupFocus,
386 crate::input::keybindings::KeyContext::FileExplorer,
387 )
388 })
389 .or_else(|| Some("Alt+T".to_string()))
390 }
391
392 pub fn handle_popup_focus(&mut self) {
403 if let Some(popup) = self.global_popups.top_mut() {
404 popup.focused = true;
405 return;
406 }
407 if let Some(popup) = self.active_state_mut().popups.top_mut() {
408 popup.focused = true;
409 }
410 }
411
412 pub fn handle_popup_type_char(&mut self, c: char) {
419 use crate::input::keybindings::Action;
420
421 if let Some(events) = self
422 .active_window_mut()
423 .action_to_events(Action::InsertChar(c))
424 {
425 if events.len() > 1 {
426 let description = format!("Insert '{}'", c);
427 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description) {
428 self.active_event_log_mut().append(bulk_edit);
429 }
430 } else {
431 for event in events {
432 self.log_and_apply_event(&event);
433 }
434 }
435 }
436
437 self.refilter_completion_popup();
438 }
439
440 pub fn handle_popup_backspace(&mut self) {
448 use crate::input::keybindings::Action;
449
450 if let Some(events) = self
451 .active_window_mut()
452 .action_to_events(Action::DeleteBackward)
453 {
454 if events.len() > 1 {
455 if let Some(bulk_edit) =
456 self.apply_events_as_bulk_edit(events, "Backspace".to_string())
457 {
458 self.active_event_log_mut().append(bulk_edit);
459 }
460 } else {
461 for event in events {
462 self.log_and_apply_event(&event);
463 }
464 }
465 }
466
467 self.refilter_completion_popup();
468 }
469
470 fn refilter_completion_popup(&mut self) {
473 let lsp_items = self
475 .active_window_mut()
476 .completion_items
477 .clone()
478 .unwrap_or_default();
479
480 let (word_start, cursor_pos) = {
482 let cursor_pos = self.active_cursors().primary().position;
483 let state = self.active_state();
484 let word_start = find_completion_word_start(&state.buffer, cursor_pos);
485 (word_start, cursor_pos)
486 };
487
488 let prefix = if word_start < cursor_pos {
489 self.active_state_mut()
490 .get_text_range(word_start, cursor_pos)
491 .to_lowercase()
492 } else {
493 String::new()
494 };
495
496 let filtered_lsp: Vec<&lsp_types::CompletionItem> = if prefix.is_empty() {
498 lsp_items.iter().collect()
499 } else {
500 lsp_items
501 .iter()
502 .filter(|item| {
503 item.label.to_lowercase().starts_with(&prefix)
504 || item
505 .filter_text
506 .as_ref()
507 .map(|ft| ft.to_lowercase().starts_with(&prefix))
508 .unwrap_or(false)
509 })
510 .collect()
511 };
512
513 let mut all_popup_items = lsp_items_to_popup_items(&filtered_lsp);
515 let buffer_word_items = self.get_buffer_completion_popup_items();
516 let lsp_labels: std::collections::HashSet<String> = all_popup_items
517 .iter()
518 .map(|i| i.text.to_lowercase())
519 .collect();
520 all_popup_items.extend(
521 buffer_word_items
522 .into_iter()
523 .filter(|item| !lsp_labels.contains(&item.text.to_lowercase())),
524 );
525
526 if all_popup_items.is_empty() {
528 self.hide_popup();
529 self.active_window_mut().completion_items = None;
530 return;
531 }
532
533 let current_selection = self
535 .active_state()
536 .popups
537 .top()
538 .and_then(|p| p.selected_item())
539 .map(|item| item.text.clone());
540
541 let selected = current_selection
543 .and_then(|sel| all_popup_items.iter().position(|item| item.text == sel))
544 .unwrap_or(0);
545
546 let popup_data = build_completion_popup_from_items(all_popup_items, selected);
547 let accept_hint = self.completion_accept_key_hint();
548
549 self.hide_popup();
551 let buffer_id = self.active_buffer();
552 let state = self
553 .windows
554 .get_mut(&self.active_window)
555 .map(|w| &mut w.buffers)
556 .expect("active window present")
557 .get_mut(&buffer_id)
558 .unwrap();
559 let mut popup_obj = crate::state::convert_popup_data_to_popup(&popup_data);
560 popup_obj.accept_key_hint = accept_hint;
561 popup_obj.resolver = crate::view::popup::PopupResolver::Completion;
562 state.popups.show_or_replace(popup_obj);
563 }
564}
565
566pub(crate) fn build_completion_popup_from_items(
570 items: Vec<crate::model::event::PopupListItemData>,
571 selected: usize,
572) -> crate::model::event::PopupData {
573 use crate::model::event::{PopupContentData, PopupKindHint, PopupPositionData};
574
575 crate::model::event::PopupData {
576 kind: PopupKindHint::Completion,
577 title: None,
578 description: None,
579 transient: false,
580 content: PopupContentData::List { items, selected },
581 position: PopupPositionData::BelowCursor,
582 width: 50,
583 max_height: 15,
584 bordered: true,
585 }
586}
587
588pub(crate) fn lsp_items_to_popup_items(
590 items: &[&lsp_types::CompletionItem],
591) -> Vec<crate::model::event::PopupListItemData> {
592 use crate::model::event::PopupListItemData;
593
594 items
595 .iter()
596 .map(|item| {
597 let icon = match item.kind {
598 Some(lsp_types::CompletionItemKind::FUNCTION)
599 | Some(lsp_types::CompletionItemKind::METHOD) => Some("λ".to_string()),
600 Some(lsp_types::CompletionItemKind::VARIABLE) => Some("v".to_string()),
601 Some(lsp_types::CompletionItemKind::STRUCT)
602 | Some(lsp_types::CompletionItemKind::CLASS) => Some("S".to_string()),
603 Some(lsp_types::CompletionItemKind::CONSTANT) => Some("c".to_string()),
604 Some(lsp_types::CompletionItemKind::KEYWORD) => Some("k".to_string()),
605 _ => None,
606 };
607
608 PopupListItemData {
609 text: item.label.clone(),
610 detail: item.detail.clone(),
611 icon,
612 data: item
613 .insert_text
614 .clone()
615 .or_else(|| Some(item.label.clone())),
616 }
617 })
618 .collect()
619}