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 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.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) {
156 let (insert_text, cursor_offset) = if is_snippet(&text) {
158 let expanded = expand_snippet(&text);
159 (expanded.text, Some(expanded.cursor_offset))
160 } else {
161 (text, None)
162 };
163
164 let (cursor_id, cursor_pos, word_start) = {
165 let cursors = self.active_cursors();
166 let cursor_id = cursors.primary_id();
167 let cursor_pos = cursors.primary().position;
168 let state = self.active_state();
169 let word_start = find_completion_word_start(&state.buffer, cursor_pos);
170 (cursor_id, cursor_pos, word_start)
171 };
172
173 let deleted_text = if word_start < cursor_pos {
174 self.active_state_mut()
175 .get_text_range(word_start, cursor_pos)
176 } else {
177 String::new()
178 };
179
180 let insert_pos = if word_start < cursor_pos {
181 let delete_event = Event::Delete {
182 range: word_start..cursor_pos,
183 deleted_text,
184 cursor_id,
185 };
186
187 self.log_and_apply_event(&delete_event);
188
189 let buffer_len = self.active_state().buffer.len();
190 word_start.min(buffer_len)
191 } else {
192 cursor_pos
193 };
194
195 let insert_event = Event::Insert {
196 position: insert_pos,
197 text: insert_text.clone(),
198 cursor_id,
199 };
200
201 self.log_and_apply_event(&insert_event);
202
203 if let Some(offset) = cursor_offset {
205 let new_cursor_pos = insert_pos + offset;
206 let current_pos = self.active_cursors().primary().position;
208 if current_pos != new_cursor_pos {
209 let move_event = Event::MoveCursor {
210 cursor_id,
211 old_position: current_pos,
212 new_position: new_cursor_pos,
213 old_anchor: None,
214 new_anchor: None,
215 old_sticky_column: 0,
216 new_sticky_column: 0,
217 };
218 let split_id = self.split_manager.active_split();
219 let buffer_id = self.active_buffer();
220 let state = self.buffers.get_mut(&buffer_id).unwrap();
221 let cursors = &mut self.split_view_states.get_mut(&split_id).unwrap().cursors;
222 state.apply(cursors, &move_event);
223 }
224 }
225 }
226
227 fn apply_completion_additional_edits(&mut self, label: &str) {
232 let item = self
234 .completion_items
235 .as_ref()
236 .and_then(|items| items.iter().find(|item| item.label == label).cloned());
237
238 let Some(item) = item else { return };
239
240 if let Some(edits) = &item.additional_text_edits {
241 if !edits.is_empty() {
242 tracing::info!(
243 "Applying {} additional text edits from completion '{}'",
244 edits.len(),
245 label
246 );
247 let buffer_id = self.active_buffer();
248 if let Err(e) = self.apply_lsp_text_edits(buffer_id, edits.clone()) {
249 tracing::error!("Failed to apply completion additional_text_edits: {}", e);
250 }
251 return;
252 }
253 }
254
255 if self.server_supports_completion_resolve() {
257 tracing::info!(
258 "Completion '{}' has no additional_text_edits, sending completionItem/resolve",
259 label
260 );
261 self.send_completion_resolve(item);
262 }
263 }
264
265 pub fn handle_popup_cancel(&mut self) {
271 use crate::view::popup::PopupResolver;
272
273 let resolver = if self.global_popups.is_visible() {
274 self.global_popups.top().map(|p| p.resolver.clone())
275 } else {
276 self.active_state().popups.top().map(|p| p.resolver.clone())
277 };
278
279 match resolver {
280 Some(PopupResolver::PluginAction { popup_id }) => {
281 tracing::info!(
282 "handle_popup_cancel: dismissing action popup id={}",
283 popup_id
284 );
285 self.hide_popup();
286 self.plugin_manager.run_hook(
287 "action_popup_result",
288 crate::services::plugins::hooks::HookArgs::ActionPopupResult {
289 popup_id,
290 action_id: "dismissed".to_string(),
291 },
292 );
293 }
294
295 Some(PopupResolver::LspStatus) => {
296 self.hide_popup();
297 }
298
299 Some(PopupResolver::CodeAction) => {
300 self.pending_code_actions = None;
301 self.hide_popup();
302 }
303
304 Some(PopupResolver::LspConfirm { language: _ }) => {
305 self.set_status_message(t!("lsp.startup_cancelled_msg").to_string());
306 self.hide_popup();
307 }
308
309 Some(PopupResolver::Completion) => {
310 self.hide_popup();
311 self.completion_items = None;
312 }
313
314 Some(PopupResolver::RemoteIndicator) => {
315 self.hide_popup();
316 }
317
318 Some(PopupResolver::None) | None => {
319 self.hide_popup();
320 self.completion_items = None;
321 }
322 }
323 }
324
325 pub(crate) fn completion_accept_key_hint(&self) -> Option<String> {
328 Some("Tab".to_string())
330 }
331
332 pub(crate) fn popup_focus_key_hint(&self) -> Option<String> {
337 let kb = self.keybindings.read().ok()?;
338 kb.get_keybinding_for_action(
348 &crate::input::keybindings::Action::PopupFocus,
349 crate::input::keybindings::KeyContext::Normal,
350 )
351 .or_else(|| {
352 kb.get_keybinding_for_action(
353 &crate::input::keybindings::Action::PopupFocus,
354 crate::input::keybindings::KeyContext::FileExplorer,
355 )
356 })
357 .or_else(|| Some("Alt+T".to_string()))
358 }
359
360 pub fn handle_popup_focus(&mut self) {
371 if let Some(popup) = self.global_popups.top_mut() {
372 popup.focused = true;
373 return;
374 }
375 if let Some(popup) = self.active_state_mut().popups.top_mut() {
376 popup.focused = true;
377 }
378 }
379
380 pub fn handle_popup_type_char(&mut self, c: char) {
383 let (cursor_id, cursor_pos) = {
385 let cursors = self.active_cursors();
386 (cursors.primary_id(), cursors.primary().position)
387 };
388
389 let insert_event = Event::Insert {
390 position: cursor_pos,
391 text: c.to_string(),
392 cursor_id,
393 };
394
395 self.log_and_apply_event(&insert_event);
396
397 self.refilter_completion_popup();
399 }
400
401 pub fn handle_popup_backspace(&mut self) {
404 let (cursor_id, cursor_pos) = {
405 let cursors = self.active_cursors();
406 (cursors.primary_id(), cursors.primary().position)
407 };
408
409 if cursor_pos == 0 {
411 return;
412 }
413
414 let prev_pos = {
416 let state = self.active_state();
417 let text = match state.buffer.to_string() {
418 Some(t) => t,
419 None => return,
420 };
421 text[..cursor_pos]
423 .char_indices()
424 .last()
425 .map(|(i, _)| i)
426 .unwrap_or(0)
427 };
428
429 let deleted_text = self.active_state_mut().get_text_range(prev_pos, cursor_pos);
430
431 let delete_event = Event::Delete {
432 range: prev_pos..cursor_pos,
433 deleted_text,
434 cursor_id,
435 };
436
437 self.log_and_apply_event(&delete_event);
438
439 self.refilter_completion_popup();
441 }
442
443 fn refilter_completion_popup(&mut self) {
446 let lsp_items = self.completion_items.clone().unwrap_or_default();
448
449 let (word_start, cursor_pos) = {
451 let cursor_pos = self.active_cursors().primary().position;
452 let state = self.active_state();
453 let word_start = find_completion_word_start(&state.buffer, cursor_pos);
454 (word_start, cursor_pos)
455 };
456
457 let prefix = if word_start < cursor_pos {
458 self.active_state_mut()
459 .get_text_range(word_start, cursor_pos)
460 .to_lowercase()
461 } else {
462 String::new()
463 };
464
465 let filtered_lsp: Vec<&lsp_types::CompletionItem> = if prefix.is_empty() {
467 lsp_items.iter().collect()
468 } else {
469 lsp_items
470 .iter()
471 .filter(|item| {
472 item.label.to_lowercase().starts_with(&prefix)
473 || item
474 .filter_text
475 .as_ref()
476 .map(|ft| ft.to_lowercase().starts_with(&prefix))
477 .unwrap_or(false)
478 })
479 .collect()
480 };
481
482 let mut all_popup_items = lsp_items_to_popup_items(&filtered_lsp);
484 let buffer_word_items = self.get_buffer_completion_popup_items();
485 let lsp_labels: std::collections::HashSet<String> = all_popup_items
486 .iter()
487 .map(|i| i.text.to_lowercase())
488 .collect();
489 all_popup_items.extend(
490 buffer_word_items
491 .into_iter()
492 .filter(|item| !lsp_labels.contains(&item.text.to_lowercase())),
493 );
494
495 if all_popup_items.is_empty() {
497 self.hide_popup();
498 self.completion_items = None;
499 return;
500 }
501
502 let current_selection = self
504 .active_state()
505 .popups
506 .top()
507 .and_then(|p| p.selected_item())
508 .map(|item| item.text.clone());
509
510 let selected = current_selection
512 .and_then(|sel| all_popup_items.iter().position(|item| item.text == sel))
513 .unwrap_or(0);
514
515 let popup_data = build_completion_popup_from_items(all_popup_items, selected);
516 let accept_hint = self.completion_accept_key_hint();
517
518 self.hide_popup();
520 let buffer_id = self.active_buffer();
521 let state = self.buffers.get_mut(&buffer_id).unwrap();
522 let mut popup_obj = crate::state::convert_popup_data_to_popup(&popup_data);
523 popup_obj.accept_key_hint = accept_hint;
524 popup_obj.resolver = crate::view::popup::PopupResolver::Completion;
525 state.popups.show_or_replace(popup_obj);
526 }
527}
528
529pub(crate) fn build_completion_popup_from_items(
533 items: Vec<crate::model::event::PopupListItemData>,
534 selected: usize,
535) -> crate::model::event::PopupData {
536 use crate::model::event::{PopupContentData, PopupKindHint, PopupPositionData};
537
538 crate::model::event::PopupData {
539 kind: PopupKindHint::Completion,
540 title: None,
541 description: None,
542 transient: false,
543 content: PopupContentData::List { items, selected },
544 position: PopupPositionData::BelowCursor,
545 width: 50,
546 max_height: 15,
547 bordered: true,
548 }
549}
550
551pub(crate) fn lsp_items_to_popup_items(
553 items: &[&lsp_types::CompletionItem],
554) -> Vec<crate::model::event::PopupListItemData> {
555 use crate::model::event::PopupListItemData;
556
557 items
558 .iter()
559 .map(|item| {
560 let icon = match item.kind {
561 Some(lsp_types::CompletionItemKind::FUNCTION)
562 | Some(lsp_types::CompletionItemKind::METHOD) => Some("λ".to_string()),
563 Some(lsp_types::CompletionItemKind::VARIABLE) => Some("v".to_string()),
564 Some(lsp_types::CompletionItemKind::STRUCT)
565 | Some(lsp_types::CompletionItemKind::CLASS) => Some("S".to_string()),
566 Some(lsp_types::CompletionItemKind::CONSTANT) => Some("c".to_string()),
567 Some(lsp_types::CompletionItemKind::KEYWORD) => Some("k".to_string()),
568 _ => None,
569 };
570
571 PopupListItemData {
572 text: item.label.clone(),
573 detail: item.detail.clone(),
574 icon,
575 data: item
576 .insert_text
577 .clone()
578 .or_else(|| Some(item.label.clone())),
579 }
580 })
581 .collect()
582}