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::WorkspaceTrust) => {
127 let action_key = self
130 .global_popups
131 .top()
132 .or_else(|| self.active_state().popups.top())
133 .and_then(|p| p.selected_item())
134 .and_then(|item| item.data.clone());
135 self.hide_popup();
136 if let Some(key) = action_key {
137 self.handle_workspace_trust_action(&key);
138 }
139 PopupConfirmResult::EarlyReturn
140 }
141
142 Some(PopupResolver::Completion) => {
143 let completion_info = self
147 .active_state()
148 .popups
149 .top()
150 .and_then(|p| p.selected_item())
151 .map(|item| (item.text.clone(), item.data.clone()));
152 if let Some((label, insert_text)) = completion_info {
153 if let Some(text) = insert_text {
154 self.insert_completion_text(text);
155 }
156 self.apply_completion_additional_edits(&label);
157 }
158 self.hide_popup();
159 PopupConfirmResult::Done
160 }
161
162 Some(PopupResolver::None) | None => {
163 self.hide_popup();
164 PopupConfirmResult::Done
165 }
166 }
167 }
168
169 fn insert_completion_text(&mut self, text: String) {
177 use crate::model::event::CursorId;
178
179 let (insert_text, cursor_offset) = if is_snippet(&text) {
181 let expanded = expand_snippet(&text);
182 (expanded.text, Some(expanded.cursor_offset))
183 } else {
184 (text, None)
185 };
186
187 let cursor_data: Vec<(CursorId, usize, usize, String)> = {
189 let positions: Vec<(CursorId, usize)> = self
190 .active_cursors()
191 .iter()
192 .map(|(id, c)| (id, c.position))
193 .collect();
194 positions
195 .into_iter()
196 .map(|(id, pos)| {
197 let word_start = {
198 let state = self.active_state();
199 find_completion_word_start(&state.buffer, pos)
200 };
201 let prefix = if word_start < pos {
202 self.active_state_mut().get_text_range(word_start, pos)
203 } else {
204 String::new()
205 };
206 (id, pos, word_start, prefix)
207 })
208 .collect()
209 };
210
211 let mut events: Vec<Event> = Vec::new();
214 for (cursor_id, pos, word_start, prefix) in &cursor_data {
215 if *word_start < *pos {
216 events.push(Event::Delete {
217 range: *word_start..*pos,
218 deleted_text: prefix.clone(),
219 cursor_id: *cursor_id,
220 });
221 }
222 events.push(Event::Insert {
223 position: *word_start,
224 text: insert_text.clone(),
225 cursor_id: *cursor_id,
226 });
227 }
228
229 if events.is_empty() {
230 return;
231 }
232
233 let description = "Accept completion".to_string();
234 if cursor_data.len() > 1 || events.len() > 1 {
235 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description) {
237 self.active_event_log_mut().append(bulk_edit);
238 }
239 } else {
240 for event in events {
241 self.log_and_apply_event(&event);
242 }
243 }
244
245 if let Some(offset) = cursor_offset {
249 if offset != insert_text.len() {
250 let move_events: Vec<Event> = self
251 .active_cursors()
252 .iter()
253 .map(|(cursor_id, cursor)| {
254 let current = cursor.position;
255 let target = current.saturating_sub(insert_text.len()) + offset;
256 Event::MoveCursor {
257 cursor_id,
258 old_position: current,
259 new_position: target,
260 old_anchor: cursor.anchor,
261 new_anchor: None,
262 old_sticky_column: cursor.sticky_column,
263 new_sticky_column: 0,
264 }
265 })
266 .collect();
267 for event in move_events {
268 self.log_and_apply_event(&event);
269 }
270 }
271 }
272 }
273
274 fn apply_completion_additional_edits(&mut self, label: &str) {
279 let item = self
281 .active_window_mut()
282 .completion_items
283 .as_ref()
284 .and_then(|items| items.iter().find(|item| item.label == label).cloned());
285
286 let Some(item) = item else { return };
287
288 if let Some(edits) = &item.additional_text_edits {
289 if !edits.is_empty() {
290 tracing::info!(
291 "Applying {} additional text edits from completion '{}'",
292 edits.len(),
293 label
294 );
295 let buffer_id = self.active_buffer();
296 if let Err(e) = self.apply_lsp_text_edits(buffer_id, edits.clone()) {
297 tracing::error!("Failed to apply completion additional_text_edits: {}", e);
298 }
299 return;
300 }
301 }
302
303 if self.active_window().server_supports_completion_resolve() {
305 tracing::info!(
306 "Completion '{}' has no additional_text_edits, sending completionItem/resolve",
307 label
308 );
309 self.active_window_mut().send_completion_resolve(item);
310 }
311 }
312
313 pub fn handle_popup_cancel(&mut self) {
319 use crate::view::popup::PopupResolver;
320
321 let resolver = if self.global_popups.is_visible() {
322 self.global_popups.top().map(|p| p.resolver.clone())
323 } else {
324 self.active_state().popups.top().map(|p| p.resolver.clone())
325 };
326
327 match resolver {
328 Some(PopupResolver::PluginAction { popup_id }) => {
329 tracing::info!(
330 "handle_popup_cancel: dismissing action popup id={}",
331 popup_id
332 );
333 self.hide_popup();
334 self.plugin_manager.read().unwrap().run_hook(
335 "action_popup_result",
336 crate::services::plugins::hooks::HookArgs::ActionPopupResult {
337 popup_id,
338 action_id: "dismissed".to_string(),
339 },
340 );
341 }
342
343 Some(PopupResolver::LspStatus) => {
344 self.hide_popup();
345 }
346
347 Some(PopupResolver::CodeAction) => {
348 self.active_window_mut().pending_code_actions = None;
349 self.hide_popup();
350 }
351
352 Some(PopupResolver::LspConfirm { language: _ }) => {
353 self.set_status_message(t!("lsp.startup_cancelled_msg").to_string());
354 self.hide_popup();
355 }
356
357 Some(PopupResolver::Completion) => {
358 self.hide_popup();
359 self.active_window_mut().completion_items = None;
360 }
361
362 Some(PopupResolver::RemoteIndicator) => {
363 self.hide_popup();
364 }
365
366 Some(PopupResolver::WorkspaceTrust) => {
367 }
371
372 Some(PopupResolver::None) | None => {
373 self.hide_popup();
374 self.active_window_mut().completion_items = None;
375 }
376 }
377 }
378
379 pub(crate) fn completion_accept_key_hint(&self) -> Option<String> {
382 Some("Tab".to_string())
384 }
385
386 pub(crate) fn popup_focus_key_hint(&self) -> Option<String> {
391 let kb = self.keybindings.read().ok()?;
392 kb.get_keybinding_for_action(
402 &crate::input::keybindings::Action::PopupFocus,
403 crate::input::keybindings::KeyContext::Normal,
404 )
405 .or_else(|| {
406 kb.get_keybinding_for_action(
407 &crate::input::keybindings::Action::PopupFocus,
408 crate::input::keybindings::KeyContext::FileExplorer,
409 )
410 })
411 .or_else(|| Some("Alt+T".to_string()))
412 }
413
414 pub fn handle_popup_focus(&mut self) {
425 if let Some(popup) = self.global_popups.top_mut() {
426 popup.focused = true;
427 return;
428 }
429 if let Some(popup) = self.active_state_mut().popups.top_mut() {
430 popup.focused = true;
431 }
432 }
433
434 pub fn handle_popup_type_char(&mut self, c: char) {
441 use crate::input::keybindings::Action;
442
443 if let Some(events) = self
444 .active_window_mut()
445 .action_to_events(Action::InsertChar(c))
446 {
447 if events.len() > 1 {
448 let description = format!("Insert '{}'", c);
449 if let Some(bulk_edit) = self.apply_events_as_bulk_edit(events, description) {
450 self.active_event_log_mut().append(bulk_edit);
451 }
452 } else {
453 for event in events {
454 self.log_and_apply_event(&event);
455 }
456 }
457 }
458
459 self.refilter_completion_popup();
460 }
461
462 pub fn handle_popup_backspace(&mut self) {
470 use crate::input::keybindings::Action;
471
472 if let Some(events) = self
473 .active_window_mut()
474 .action_to_events(Action::DeleteBackward)
475 {
476 if events.len() > 1 {
477 if let Some(bulk_edit) =
478 self.apply_events_as_bulk_edit(events, "Backspace".to_string())
479 {
480 self.active_event_log_mut().append(bulk_edit);
481 }
482 } else {
483 for event in events {
484 self.log_and_apply_event(&event);
485 }
486 }
487 }
488
489 self.refilter_completion_popup();
490 }
491
492 fn refilter_completion_popup(&mut self) {
495 let lsp_items = self
497 .active_window_mut()
498 .completion_items
499 .clone()
500 .unwrap_or_default();
501
502 let (word_start, cursor_pos) = {
504 let cursor_pos = self.active_cursors().primary().position;
505 let state = self.active_state();
506 let word_start = find_completion_word_start(&state.buffer, cursor_pos);
507 (word_start, cursor_pos)
508 };
509
510 let prefix = if word_start < cursor_pos {
511 self.active_state_mut()
512 .get_text_range(word_start, cursor_pos)
513 .to_lowercase()
514 } else {
515 String::new()
516 };
517
518 let filtered_lsp: Vec<&lsp_types::CompletionItem> = if prefix.is_empty() {
520 lsp_items.iter().collect()
521 } else {
522 lsp_items
523 .iter()
524 .filter(|item| {
525 item.label.to_lowercase().starts_with(&prefix)
526 || item
527 .filter_text
528 .as_ref()
529 .map(|ft| ft.to_lowercase().starts_with(&prefix))
530 .unwrap_or(false)
531 })
532 .collect()
533 };
534
535 let mut all_popup_items = lsp_items_to_popup_items(&filtered_lsp);
537 let buffer_word_items = self.get_buffer_completion_popup_items();
538 let lsp_labels: std::collections::HashSet<String> = all_popup_items
539 .iter()
540 .map(|i| i.text.to_lowercase())
541 .collect();
542 all_popup_items.extend(
543 buffer_word_items
544 .into_iter()
545 .filter(|item| !lsp_labels.contains(&item.text.to_lowercase())),
546 );
547
548 if all_popup_items.is_empty() {
550 self.hide_popup();
551 self.active_window_mut().completion_items = None;
552 return;
553 }
554
555 let current_selection = self
557 .active_state()
558 .popups
559 .top()
560 .and_then(|p| p.selected_item())
561 .map(|item| item.text.clone());
562
563 let selected = current_selection
565 .and_then(|sel| all_popup_items.iter().position(|item| item.text == sel))
566 .unwrap_or(0);
567
568 let popup_data = build_completion_popup_from_items(all_popup_items, selected);
569 let accept_hint = self.completion_accept_key_hint();
570
571 self.hide_popup();
573 let buffer_id = self.active_buffer();
574 let state = self
575 .windows
576 .get_mut(&self.active_window)
577 .map(|w| &mut w.buffers)
578 .expect("active window present")
579 .get_mut(&buffer_id)
580 .unwrap();
581 let mut popup_obj = crate::state::convert_popup_data_to_popup(&popup_data);
582 popup_obj.accept_key_hint = accept_hint;
583 popup_obj.resolver = crate::view::popup::PopupResolver::Completion;
584 state.popups.show_or_replace(popup_obj);
585 }
586}
587
588pub(crate) fn build_completion_popup_from_items(
592 items: Vec<crate::model::event::PopupListItemData>,
593 selected: usize,
594) -> crate::model::event::PopupData {
595 use crate::model::event::{PopupContentData, PopupKindHint, PopupPositionData};
596
597 crate::model::event::PopupData {
598 kind: PopupKindHint::Completion,
599 title: None,
600 description: None,
601 transient: false,
602 content: PopupContentData::List { items, selected },
603 position: PopupPositionData::BelowCursor,
604 width: 50,
605 max_height: 15,
606 bordered: true,
607 }
608}
609
610pub(crate) fn lsp_items_to_popup_items(
612 items: &[&lsp_types::CompletionItem],
613) -> Vec<crate::model::event::PopupListItemData> {
614 use crate::model::event::PopupListItemData;
615
616 items
617 .iter()
618 .map(|item| {
619 let icon = match item.kind {
620 Some(lsp_types::CompletionItemKind::FUNCTION)
621 | Some(lsp_types::CompletionItemKind::METHOD) => Some("λ".to_string()),
622 Some(lsp_types::CompletionItemKind::VARIABLE) => Some("v".to_string()),
623 Some(lsp_types::CompletionItemKind::STRUCT)
624 | Some(lsp_types::CompletionItemKind::CLASS) => Some("S".to_string()),
625 Some(lsp_types::CompletionItemKind::CONSTANT) => Some("c".to_string()),
626 Some(lsp_types::CompletionItemKind::KEYWORD) => Some("k".to_string()),
627 _ => None,
628 };
629
630 PopupListItemData {
631 text: item.label.clone(),
632 detail: item.detail.clone(),
633 icon,
634 data: item
635 .insert_text
636 .clone()
637 .or_else(|| Some(item.label.clone())),
638 }
639 })
640 .collect()
641}