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.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 let active = self.active_buffer();
76 if let Some(language) = self.buffers.get(&active).map(|s| s.language.clone()) {
77 self.pending_auto_start_prompts.remove(&language);
78 self.auto_start_prompted_languages.insert(language);
79 }
80 if let Some(key) = action_key {
81 self.handle_lsp_status_action(&key);
82 }
83 PopupConfirmResult::EarlyReturn
84 }
85
86 Some(PopupResolver::CodeAction) => {
87 let selected_index = self
88 .active_state()
89 .popups
90 .top()
91 .and_then(|p| p.selected_item())
92 .and_then(|item| item.data.as_ref())
93 .and_then(|data| data.parse::<usize>().ok());
94 self.hide_popup();
95 if let Some(index) = selected_index {
96 self.execute_code_action(index);
97 }
98 self.pending_code_actions = None;
99 PopupConfirmResult::EarlyReturn
100 }
101
102 Some(PopupResolver::LspConfirm { language }) => {
103 let action = self
104 .active_state()
105 .popups
106 .top()
107 .and_then(|p| p.selected_item())
108 .and_then(|item| item.data.clone());
109 if let Some(action) = action {
110 self.hide_popup();
111 self.handle_lsp_confirmation_response(&language, &action);
112 PopupConfirmResult::EarlyReturn
113 } else {
114 self.hide_popup();
115 PopupConfirmResult::EarlyReturn
116 }
117 }
118
119 Some(PopupResolver::RemoteIndicator) => {
120 let action_key = self
121 .active_state()
122 .popups
123 .top()
124 .and_then(|p| p.selected_item())
125 .and_then(|item| item.data.clone());
126 self.hide_popup();
127 if let Some(key) = action_key {
128 self.handle_remote_indicator_action(&key);
129 }
130 PopupConfirmResult::EarlyReturn
131 }
132
133 Some(PopupResolver::Completion) => {
134 let completion_info = self
138 .active_state()
139 .popups
140 .top()
141 .and_then(|p| p.selected_item())
142 .map(|item| (item.text.clone(), item.data.clone()));
143 if let Some((label, insert_text)) = completion_info {
144 if let Some(text) = insert_text {
145 self.insert_completion_text(text);
146 }
147 self.apply_completion_additional_edits(&label);
148 }
149 self.hide_popup();
150 PopupConfirmResult::Done
151 }
152
153 Some(PopupResolver::None) | None => {
154 self.hide_popup();
155 PopupConfirmResult::Done
156 }
157 }
158 }
159
160 fn insert_completion_text(&mut self, text: String) {
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_id, cursor_pos, word_start) = {
172 let cursors = self.active_cursors();
173 let cursor_id = cursors.primary_id();
174 let cursor_pos = cursors.primary().position;
175 let state = self.active_state();
176 let word_start = find_completion_word_start(&state.buffer, cursor_pos);
177 (cursor_id, cursor_pos, word_start)
178 };
179
180 let deleted_text = if word_start < cursor_pos {
181 self.active_state_mut()
182 .get_text_range(word_start, cursor_pos)
183 } else {
184 String::new()
185 };
186
187 let insert_pos = if word_start < cursor_pos {
188 let delete_event = Event::Delete {
189 range: word_start..cursor_pos,
190 deleted_text,
191 cursor_id,
192 };
193
194 self.log_and_apply_event(&delete_event);
195
196 let buffer_len = self.active_state().buffer.len();
197 word_start.min(buffer_len)
198 } else {
199 cursor_pos
200 };
201
202 let insert_event = Event::Insert {
203 position: insert_pos,
204 text: insert_text.clone(),
205 cursor_id,
206 };
207
208 self.log_and_apply_event(&insert_event);
209
210 if let Some(offset) = cursor_offset {
212 let new_cursor_pos = insert_pos + offset;
213 let current_pos = self.active_cursors().primary().position;
215 if current_pos != new_cursor_pos {
216 let move_event = Event::MoveCursor {
217 cursor_id,
218 old_position: current_pos,
219 new_position: new_cursor_pos,
220 old_anchor: None,
221 new_anchor: None,
222 old_sticky_column: 0,
223 new_sticky_column: 0,
224 };
225 let split_id = self.split_manager.active_split();
226 let buffer_id = self.active_buffer();
227 let state = self.buffers.get_mut(&buffer_id).unwrap();
228 let cursors = &mut self.split_view_states.get_mut(&split_id).unwrap().cursors;
229 state.apply(cursors, &move_event);
230 }
231 }
232 }
233
234 fn apply_completion_additional_edits(&mut self, label: &str) {
239 let item = self
241 .completion_items
242 .as_ref()
243 .and_then(|items| items.iter().find(|item| item.label == label).cloned());
244
245 let Some(item) = item else { return };
246
247 if let Some(edits) = &item.additional_text_edits {
248 if !edits.is_empty() {
249 tracing::info!(
250 "Applying {} additional text edits from completion '{}'",
251 edits.len(),
252 label
253 );
254 let buffer_id = self.active_buffer();
255 if let Err(e) = self.apply_lsp_text_edits(buffer_id, edits.clone()) {
256 tracing::error!("Failed to apply completion additional_text_edits: {}", e);
257 }
258 return;
259 }
260 }
261
262 if self.server_supports_completion_resolve() {
264 tracing::info!(
265 "Completion '{}' has no additional_text_edits, sending completionItem/resolve",
266 label
267 );
268 self.send_completion_resolve(item);
269 }
270 }
271
272 pub fn handle_popup_cancel(&mut self) {
278 use crate::view::popup::PopupResolver;
279
280 let resolver = if self.global_popups.is_visible() {
281 self.global_popups.top().map(|p| p.resolver.clone())
282 } else {
283 self.active_state().popups.top().map(|p| p.resolver.clone())
284 };
285
286 match resolver {
287 Some(PopupResolver::PluginAction { popup_id }) => {
288 tracing::info!(
289 "handle_popup_cancel: dismissing action popup id={}",
290 popup_id
291 );
292 self.hide_popup();
293 self.plugin_manager.run_hook(
294 "action_popup_result",
295 crate::services::plugins::hooks::HookArgs::ActionPopupResult {
296 popup_id,
297 action_id: "dismissed".to_string(),
298 },
299 );
300 }
301
302 Some(PopupResolver::LspStatus) => {
303 let active = self.active_buffer();
307 if let Some(language) = self.buffers.get(&active).map(|s| s.language.clone()) {
308 self.pending_auto_start_prompts.remove(&language);
309 self.auto_start_prompted_languages.insert(language);
310 }
311 self.hide_popup();
312 }
313
314 Some(PopupResolver::CodeAction) => {
315 self.pending_code_actions = None;
316 self.hide_popup();
317 }
318
319 Some(PopupResolver::LspConfirm { language: _ }) => {
320 self.set_status_message(t!("lsp.startup_cancelled_msg").to_string());
321 self.hide_popup();
322 }
323
324 Some(PopupResolver::Completion) => {
325 self.hide_popup();
326 self.completion_items = None;
327 }
328
329 Some(PopupResolver::RemoteIndicator) => {
330 self.hide_popup();
331 }
332
333 Some(PopupResolver::None) | None => {
334 self.hide_popup();
335 self.completion_items = None;
336 }
337 }
338 }
339
340 pub(crate) fn completion_accept_key_hint(&self) -> Option<String> {
343 Some("Tab".to_string())
345 }
346
347 pub(crate) fn popup_focus_key_hint(&self) -> Option<String> {
352 let kb = self.keybindings.read().ok()?;
353 kb.get_keybinding_for_action(
363 &crate::input::keybindings::Action::PopupFocus,
364 crate::input::keybindings::KeyContext::Normal,
365 )
366 .or_else(|| {
367 kb.get_keybinding_for_action(
368 &crate::input::keybindings::Action::PopupFocus,
369 crate::input::keybindings::KeyContext::FileExplorer,
370 )
371 })
372 .or_else(|| Some("Alt+T".to_string()))
373 }
374
375 pub fn handle_popup_focus(&mut self) {
386 if let Some(popup) = self.global_popups.top_mut() {
387 popup.focused = true;
388 return;
389 }
390 if let Some(popup) = self.active_state_mut().popups.top_mut() {
391 popup.focused = true;
392 }
393 }
394
395 pub fn handle_popup_type_char(&mut self, c: char) {
398 let (cursor_id, cursor_pos) = {
400 let cursors = self.active_cursors();
401 (cursors.primary_id(), cursors.primary().position)
402 };
403
404 let insert_event = Event::Insert {
405 position: cursor_pos,
406 text: c.to_string(),
407 cursor_id,
408 };
409
410 self.log_and_apply_event(&insert_event);
411
412 self.refilter_completion_popup();
414 }
415
416 pub fn handle_popup_backspace(&mut self) {
419 let (cursor_id, cursor_pos) = {
420 let cursors = self.active_cursors();
421 (cursors.primary_id(), cursors.primary().position)
422 };
423
424 if cursor_pos == 0 {
426 return;
427 }
428
429 let prev_pos = {
431 let state = self.active_state();
432 let text = match state.buffer.to_string() {
433 Some(t) => t,
434 None => return,
435 };
436 text[..cursor_pos]
438 .char_indices()
439 .last()
440 .map(|(i, _)| i)
441 .unwrap_or(0)
442 };
443
444 let deleted_text = self.active_state_mut().get_text_range(prev_pos, cursor_pos);
445
446 let delete_event = Event::Delete {
447 range: prev_pos..cursor_pos,
448 deleted_text,
449 cursor_id,
450 };
451
452 self.log_and_apply_event(&delete_event);
453
454 self.refilter_completion_popup();
456 }
457
458 fn refilter_completion_popup(&mut self) {
461 let lsp_items = self.completion_items.clone().unwrap_or_default();
463
464 let (word_start, cursor_pos) = {
466 let cursor_pos = self.active_cursors().primary().position;
467 let state = self.active_state();
468 let word_start = find_completion_word_start(&state.buffer, cursor_pos);
469 (word_start, cursor_pos)
470 };
471
472 let prefix = if word_start < cursor_pos {
473 self.active_state_mut()
474 .get_text_range(word_start, cursor_pos)
475 .to_lowercase()
476 } else {
477 String::new()
478 };
479
480 let filtered_lsp: Vec<&lsp_types::CompletionItem> = if prefix.is_empty() {
482 lsp_items.iter().collect()
483 } else {
484 lsp_items
485 .iter()
486 .filter(|item| {
487 item.label.to_lowercase().starts_with(&prefix)
488 || item
489 .filter_text
490 .as_ref()
491 .map(|ft| ft.to_lowercase().starts_with(&prefix))
492 .unwrap_or(false)
493 })
494 .collect()
495 };
496
497 let mut all_popup_items = lsp_items_to_popup_items(&filtered_lsp);
499 let buffer_word_items = self.get_buffer_completion_popup_items();
500 let lsp_labels: std::collections::HashSet<String> = all_popup_items
501 .iter()
502 .map(|i| i.text.to_lowercase())
503 .collect();
504 all_popup_items.extend(
505 buffer_word_items
506 .into_iter()
507 .filter(|item| !lsp_labels.contains(&item.text.to_lowercase())),
508 );
509
510 if all_popup_items.is_empty() {
512 self.hide_popup();
513 self.completion_items = None;
514 return;
515 }
516
517 let current_selection = self
519 .active_state()
520 .popups
521 .top()
522 .and_then(|p| p.selected_item())
523 .map(|item| item.text.clone());
524
525 let selected = current_selection
527 .and_then(|sel| all_popup_items.iter().position(|item| item.text == sel))
528 .unwrap_or(0);
529
530 let popup_data = build_completion_popup_from_items(all_popup_items, selected);
531 let accept_hint = self.completion_accept_key_hint();
532
533 self.hide_popup();
535 let buffer_id = self.active_buffer();
536 let state = self.buffers.get_mut(&buffer_id).unwrap();
537 let mut popup_obj = crate::state::convert_popup_data_to_popup(&popup_data);
538 popup_obj.accept_key_hint = accept_hint;
539 popup_obj.resolver = crate::view::popup::PopupResolver::Completion;
540 state.popups.show_or_replace(popup_obj);
541 }
542}
543
544pub(crate) fn build_completion_popup_from_items(
548 items: Vec<crate::model::event::PopupListItemData>,
549 selected: usize,
550) -> crate::model::event::PopupData {
551 use crate::model::event::{PopupContentData, PopupKindHint, PopupPositionData};
552
553 crate::model::event::PopupData {
554 kind: PopupKindHint::Completion,
555 title: None,
556 description: None,
557 transient: false,
558 content: PopupContentData::List { items, selected },
559 position: PopupPositionData::BelowCursor,
560 width: 50,
561 max_height: 15,
562 bordered: true,
563 }
564}
565
566pub(crate) fn lsp_items_to_popup_items(
568 items: &[&lsp_types::CompletionItem],
569) -> Vec<crate::model::event::PopupListItemData> {
570 use crate::model::event::PopupListItemData;
571
572 items
573 .iter()
574 .map(|item| {
575 let icon = match item.kind {
576 Some(lsp_types::CompletionItemKind::FUNCTION)
577 | Some(lsp_types::CompletionItemKind::METHOD) => Some("λ".to_string()),
578 Some(lsp_types::CompletionItemKind::VARIABLE) => Some("v".to_string()),
579 Some(lsp_types::CompletionItemKind::STRUCT)
580 | Some(lsp_types::CompletionItemKind::CLASS) => Some("S".to_string()),
581 Some(lsp_types::CompletionItemKind::CONSTANT) => Some("c".to_string()),
582 Some(lsp_types::CompletionItemKind::KEYWORD) => Some("k".to_string()),
583 _ => None,
584 };
585
586 PopupListItemData {
587 text: item.label.clone(),
588 detail: item.detail.clone(),
589 icon,
590 data: item
591 .insert_text
592 .clone()
593 .or_else(|| Some(item.label.clone())),
594 }
595 })
596 .collect()
597}