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 return 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 fn handle_popup_type_char(&mut self, c: char) {
350 let (cursor_id, cursor_pos) = {
352 let cursors = self.active_cursors();
353 (cursors.primary_id(), cursors.primary().position)
354 };
355
356 let insert_event = Event::Insert {
357 position: cursor_pos,
358 text: c.to_string(),
359 cursor_id,
360 };
361
362 self.log_and_apply_event(&insert_event);
363
364 self.refilter_completion_popup();
366 }
367
368 pub fn handle_popup_backspace(&mut self) {
371 let (cursor_id, cursor_pos) = {
372 let cursors = self.active_cursors();
373 (cursors.primary_id(), cursors.primary().position)
374 };
375
376 if cursor_pos == 0 {
378 return;
379 }
380
381 let prev_pos = {
383 let state = self.active_state();
384 let text = match state.buffer.to_string() {
385 Some(t) => t,
386 None => return,
387 };
388 text[..cursor_pos]
390 .char_indices()
391 .last()
392 .map(|(i, _)| i)
393 .unwrap_or(0)
394 };
395
396 let deleted_text = self.active_state_mut().get_text_range(prev_pos, cursor_pos);
397
398 let delete_event = Event::Delete {
399 range: prev_pos..cursor_pos,
400 deleted_text,
401 cursor_id,
402 };
403
404 self.log_and_apply_event(&delete_event);
405
406 self.refilter_completion_popup();
408 }
409
410 fn refilter_completion_popup(&mut self) {
413 let lsp_items = self.completion_items.clone().unwrap_or_default();
415
416 let (word_start, cursor_pos) = {
418 let cursor_pos = self.active_cursors().primary().position;
419 let state = self.active_state();
420 let word_start = find_completion_word_start(&state.buffer, cursor_pos);
421 (word_start, cursor_pos)
422 };
423
424 let prefix = if word_start < cursor_pos {
425 self.active_state_mut()
426 .get_text_range(word_start, cursor_pos)
427 .to_lowercase()
428 } else {
429 String::new()
430 };
431
432 let filtered_lsp: Vec<&lsp_types::CompletionItem> = if prefix.is_empty() {
434 lsp_items.iter().collect()
435 } else {
436 lsp_items
437 .iter()
438 .filter(|item| {
439 item.label.to_lowercase().starts_with(&prefix)
440 || item
441 .filter_text
442 .as_ref()
443 .map(|ft| ft.to_lowercase().starts_with(&prefix))
444 .unwrap_or(false)
445 })
446 .collect()
447 };
448
449 let mut all_popup_items = lsp_items_to_popup_items(&filtered_lsp);
451 let buffer_word_items = self.get_buffer_completion_popup_items();
452 let lsp_labels: std::collections::HashSet<String> = all_popup_items
453 .iter()
454 .map(|i| i.text.to_lowercase())
455 .collect();
456 all_popup_items.extend(
457 buffer_word_items
458 .into_iter()
459 .filter(|item| !lsp_labels.contains(&item.text.to_lowercase())),
460 );
461
462 if all_popup_items.is_empty() {
464 self.hide_popup();
465 self.completion_items = None;
466 return;
467 }
468
469 let current_selection = self
471 .active_state()
472 .popups
473 .top()
474 .and_then(|p| p.selected_item())
475 .map(|item| item.text.clone());
476
477 let selected = current_selection
479 .and_then(|sel| all_popup_items.iter().position(|item| item.text == sel))
480 .unwrap_or(0);
481
482 let popup_data = build_completion_popup_from_items(all_popup_items, selected);
483 let accept_hint = self.completion_accept_key_hint();
484
485 self.hide_popup();
487 let buffer_id = self.active_buffer();
488 let state = self.buffers.get_mut(&buffer_id).unwrap();
489 let mut popup_obj = crate::state::convert_popup_data_to_popup(&popup_data);
490 popup_obj.accept_key_hint = accept_hint;
491 popup_obj.resolver = crate::view::popup::PopupResolver::Completion;
492 state.popups.show_or_replace(popup_obj);
493 }
494}
495
496pub(crate) fn build_completion_popup_from_items(
500 items: Vec<crate::model::event::PopupListItemData>,
501 selected: usize,
502) -> crate::model::event::PopupData {
503 use crate::model::event::{PopupContentData, PopupKindHint, PopupPositionData};
504
505 crate::model::event::PopupData {
506 kind: PopupKindHint::Completion,
507 title: None,
508 description: None,
509 transient: false,
510 content: PopupContentData::List { items, selected },
511 position: PopupPositionData::BelowCursor,
512 width: 50,
513 max_height: 15,
514 bordered: true,
515 }
516}
517
518pub(crate) fn lsp_items_to_popup_items(
520 items: &[&lsp_types::CompletionItem],
521) -> Vec<crate::model::event::PopupListItemData> {
522 use crate::model::event::PopupListItemData;
523
524 items
525 .iter()
526 .map(|item| {
527 let icon = match item.kind {
528 Some(lsp_types::CompletionItemKind::FUNCTION)
529 | Some(lsp_types::CompletionItemKind::METHOD) => Some("λ".to_string()),
530 Some(lsp_types::CompletionItemKind::VARIABLE) => Some("v".to_string()),
531 Some(lsp_types::CompletionItemKind::STRUCT)
532 | Some(lsp_types::CompletionItemKind::CLASS) => Some("S".to_string()),
533 Some(lsp_types::CompletionItemKind::CONSTANT) => Some("c".to_string()),
534 Some(lsp_types::CompletionItemKind::KEYWORD) => Some("k".to_string()),
535 _ => None,
536 };
537
538 PopupListItemData {
539 text: item.label.clone(),
540 detail: item.detail.clone(),
541 icon,
542 data: item
543 .insert_text
544 .clone()
545 .or_else(|| Some(item.label.clone())),
546 }
547 })
548 .collect()
549}