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 {
24 if self.pending_lsp_status_popup.is_some() {
26 let action_key = self
27 .active_state()
28 .popups
29 .top()
30 .and_then(|p| p.selected_item())
31 .and_then(|item| item.data.clone());
32
33 self.hide_popup();
34 self.pending_lsp_status_popup = None;
35
36 if let Some(key) = action_key {
37 self.handle_lsp_status_action(&key);
38 }
39 return PopupConfirmResult::EarlyReturn;
40 }
41
42 if let Some((popup_id, _actions)) = &self.active_action_popup {
44 let popup_id = popup_id.clone();
45 let action_id = self
46 .active_state()
47 .popups
48 .top()
49 .and_then(|p| p.selected_item())
50 .and_then(|item| item.data.clone())
51 .unwrap_or_else(|| "dismissed".to_string());
52
53 self.hide_popup();
54 self.active_action_popup = None;
55
56 self.plugin_manager.run_hook(
58 "action_popup_result",
59 crate::services::plugins::hooks::HookArgs::ActionPopupResult {
60 popup_id,
61 action_id,
62 },
63 );
64
65 return PopupConfirmResult::EarlyReturn;
66 }
67
68 if self.pending_code_actions.is_some() {
70 let selected_index = self
71 .active_state()
72 .popups
73 .top()
74 .and_then(|p| p.selected_item())
75 .and_then(|item| item.data.as_ref())
76 .and_then(|data| data.parse::<usize>().ok());
77
78 self.hide_popup();
79 if let Some(index) = selected_index {
80 self.execute_code_action(index);
81 }
82 self.pending_code_actions = None;
83 return PopupConfirmResult::EarlyReturn;
84 }
85
86 if self.pending_lsp_confirmation.is_some() {
88 let action = self
89 .active_state()
90 .popups
91 .top()
92 .and_then(|p| p.selected_item())
93 .and_then(|item| item.data.clone());
94 if let Some(action) = action {
95 self.hide_popup();
96 self.handle_lsp_confirmation_response(&action);
97 return PopupConfirmResult::EarlyReturn;
98 }
99 }
100
101 let completion_info = self
103 .active_state()
104 .popups
105 .top()
106 .filter(|p| p.kind == crate::view::popup::PopupKind::Completion)
107 .and_then(|p| p.selected_item())
108 .map(|item| (item.text.clone(), item.data.clone()));
109
110 if let Some((label, insert_text)) = completion_info {
112 if let Some(text) = insert_text {
113 self.insert_completion_text(text);
114 }
115
116 self.apply_completion_additional_edits(&label);
118 }
119
120 self.hide_popup();
121 PopupConfirmResult::Done
122 }
123
124 fn insert_completion_text(&mut self, text: String) {
127 let (insert_text, cursor_offset) = if is_snippet(&text) {
129 let expanded = expand_snippet(&text);
130 (expanded.text, Some(expanded.cursor_offset))
131 } else {
132 (text, None)
133 };
134
135 let (cursor_id, cursor_pos, word_start) = {
136 let cursors = self.active_cursors();
137 let cursor_id = cursors.primary_id();
138 let cursor_pos = cursors.primary().position;
139 let state = self.active_state();
140 let word_start = find_completion_word_start(&state.buffer, cursor_pos);
141 (cursor_id, cursor_pos, word_start)
142 };
143
144 let deleted_text = if word_start < cursor_pos {
145 self.active_state_mut()
146 .get_text_range(word_start, cursor_pos)
147 } else {
148 String::new()
149 };
150
151 let insert_pos = if word_start < cursor_pos {
152 let delete_event = Event::Delete {
153 range: word_start..cursor_pos,
154 deleted_text,
155 cursor_id,
156 };
157
158 self.log_and_apply_event(&delete_event);
159
160 let buffer_len = self.active_state().buffer.len();
161 word_start.min(buffer_len)
162 } else {
163 cursor_pos
164 };
165
166 let insert_event = Event::Insert {
167 position: insert_pos,
168 text: insert_text.clone(),
169 cursor_id,
170 };
171
172 self.log_and_apply_event(&insert_event);
173
174 if let Some(offset) = cursor_offset {
176 let new_cursor_pos = insert_pos + offset;
177 let current_pos = self.active_cursors().primary().position;
179 if current_pos != new_cursor_pos {
180 let move_event = Event::MoveCursor {
181 cursor_id,
182 old_position: current_pos,
183 new_position: new_cursor_pos,
184 old_anchor: None,
185 new_anchor: None,
186 old_sticky_column: 0,
187 new_sticky_column: 0,
188 };
189 let split_id = self.split_manager.active_split();
190 let buffer_id = self.active_buffer();
191 let state = self.buffers.get_mut(&buffer_id).unwrap();
192 let cursors = &mut self.split_view_states.get_mut(&split_id).unwrap().cursors;
193 state.apply(cursors, &move_event);
194 }
195 }
196 }
197
198 fn apply_completion_additional_edits(&mut self, label: &str) {
203 let item = self
205 .completion_items
206 .as_ref()
207 .and_then(|items| items.iter().find(|item| item.label == label).cloned());
208
209 let Some(item) = item else { return };
210
211 if let Some(edits) = &item.additional_text_edits {
212 if !edits.is_empty() {
213 tracing::info!(
214 "Applying {} additional text edits from completion '{}'",
215 edits.len(),
216 label
217 );
218 let buffer_id = self.active_buffer();
219 if let Err(e) = self.apply_lsp_text_edits(buffer_id, edits.clone()) {
220 tracing::error!("Failed to apply completion additional_text_edits: {}", e);
221 }
222 return;
223 }
224 }
225
226 if self.server_supports_completion_resolve() {
228 tracing::info!(
229 "Completion '{}' has no additional_text_edits, sending completionItem/resolve",
230 label
231 );
232 self.send_completion_resolve(item);
233 }
234 }
235
236 pub fn handle_popup_cancel(&mut self) {
238 tracing::info!(
239 "handle_popup_cancel: active_action_popup={:?}",
240 self.active_action_popup.as_ref().map(|(id, _)| id)
241 );
242
243 if self.pending_lsp_status_popup.take().is_some() {
245 self.hide_popup();
246 return;
247 }
248
249 if let Some((popup_id, _actions)) = self.active_action_popup.take() {
251 tracing::info!(
252 "handle_popup_cancel: dismissing action popup id={}",
253 popup_id
254 );
255 self.hide_popup();
256
257 self.plugin_manager.run_hook(
259 "action_popup_result",
260 crate::services::plugins::hooks::HookArgs::ActionPopupResult {
261 popup_id,
262 action_id: "dismissed".to_string(),
263 },
264 );
265 tracing::info!("handle_popup_cancel: action_popup_result hook fired");
266 return;
267 }
268
269 if self.pending_code_actions.is_some() {
270 self.pending_code_actions = None;
271 self.hide_popup();
272 return;
273 }
274
275 if self.pending_lsp_confirmation.is_some() {
276 self.pending_lsp_confirmation = None;
277 self.set_status_message(t!("lsp.startup_cancelled_msg").to_string());
278 }
279 self.hide_popup();
280 self.completion_items = None;
282 }
283
284 pub(crate) fn completion_accept_key_hint(&self) -> Option<String> {
287 Some("Tab".to_string())
289 }
290
291 pub fn handle_popup_type_char(&mut self, c: char) {
294 let (cursor_id, cursor_pos) = {
296 let cursors = self.active_cursors();
297 (cursors.primary_id(), cursors.primary().position)
298 };
299
300 let insert_event = Event::Insert {
301 position: cursor_pos,
302 text: c.to_string(),
303 cursor_id,
304 };
305
306 self.log_and_apply_event(&insert_event);
307
308 self.refilter_completion_popup();
310 }
311
312 pub fn handle_popup_backspace(&mut self) {
315 let (cursor_id, cursor_pos) = {
316 let cursors = self.active_cursors();
317 (cursors.primary_id(), cursors.primary().position)
318 };
319
320 if cursor_pos == 0 {
322 return;
323 }
324
325 let prev_pos = {
327 let state = self.active_state();
328 let text = match state.buffer.to_string() {
329 Some(t) => t,
330 None => return,
331 };
332 text[..cursor_pos]
334 .char_indices()
335 .last()
336 .map(|(i, _)| i)
337 .unwrap_or(0)
338 };
339
340 let deleted_text = self.active_state_mut().get_text_range(prev_pos, cursor_pos);
341
342 let delete_event = Event::Delete {
343 range: prev_pos..cursor_pos,
344 deleted_text,
345 cursor_id,
346 };
347
348 self.log_and_apply_event(&delete_event);
349
350 self.refilter_completion_popup();
352 }
353
354 fn refilter_completion_popup(&mut self) {
357 let lsp_items = self.completion_items.clone().unwrap_or_default();
359
360 let (word_start, cursor_pos) = {
362 let cursor_pos = self.active_cursors().primary().position;
363 let state = self.active_state();
364 let word_start = find_completion_word_start(&state.buffer, cursor_pos);
365 (word_start, cursor_pos)
366 };
367
368 let prefix = if word_start < cursor_pos {
369 self.active_state_mut()
370 .get_text_range(word_start, cursor_pos)
371 .to_lowercase()
372 } else {
373 String::new()
374 };
375
376 let filtered_lsp: Vec<&lsp_types::CompletionItem> = if prefix.is_empty() {
378 lsp_items.iter().collect()
379 } else {
380 lsp_items
381 .iter()
382 .filter(|item| {
383 item.label.to_lowercase().starts_with(&prefix)
384 || item
385 .filter_text
386 .as_ref()
387 .map(|ft| ft.to_lowercase().starts_with(&prefix))
388 .unwrap_or(false)
389 })
390 .collect()
391 };
392
393 let mut all_popup_items = lsp_items_to_popup_items(&filtered_lsp);
395 let buffer_word_items = self.get_buffer_completion_popup_items();
396 let lsp_labels: std::collections::HashSet<String> = all_popup_items
397 .iter()
398 .map(|i| i.text.to_lowercase())
399 .collect();
400 all_popup_items.extend(
401 buffer_word_items
402 .into_iter()
403 .filter(|item| !lsp_labels.contains(&item.text.to_lowercase())),
404 );
405
406 if all_popup_items.is_empty() {
408 self.hide_popup();
409 self.completion_items = None;
410 return;
411 }
412
413 let current_selection = self
415 .active_state()
416 .popups
417 .top()
418 .and_then(|p| p.selected_item())
419 .map(|item| item.text.clone());
420
421 let selected = current_selection
423 .and_then(|sel| all_popup_items.iter().position(|item| item.text == sel))
424 .unwrap_or(0);
425
426 let popup_data = build_completion_popup_from_items(all_popup_items, selected);
427 let accept_hint = self.completion_accept_key_hint();
428
429 self.hide_popup();
431 let buffer_id = self.active_buffer();
432 let state = self.buffers.get_mut(&buffer_id).unwrap();
433 let mut popup_obj = crate::state::convert_popup_data_to_popup(&popup_data);
434 popup_obj.accept_key_hint = accept_hint;
435 state.popups.show_or_replace(popup_obj);
436 }
437}
438
439pub(crate) fn build_completion_popup_from_items(
443 items: Vec<crate::model::event::PopupListItemData>,
444 selected: usize,
445) -> crate::model::event::PopupData {
446 use crate::model::event::{PopupContentData, PopupKindHint, PopupPositionData};
447
448 crate::model::event::PopupData {
449 kind: PopupKindHint::Completion,
450 title: None,
451 description: None,
452 transient: false,
453 content: PopupContentData::List { items, selected },
454 position: PopupPositionData::BelowCursor,
455 width: 50,
456 max_height: 15,
457 bordered: true,
458 }
459}
460
461pub(crate) fn lsp_items_to_popup_items(
463 items: &[&lsp_types::CompletionItem],
464) -> Vec<crate::model::event::PopupListItemData> {
465 use crate::model::event::PopupListItemData;
466
467 items
468 .iter()
469 .map(|item| {
470 let icon = match item.kind {
471 Some(lsp_types::CompletionItemKind::FUNCTION)
472 | Some(lsp_types::CompletionItemKind::METHOD) => Some("λ".to_string()),
473 Some(lsp_types::CompletionItemKind::VARIABLE) => Some("v".to_string()),
474 Some(lsp_types::CompletionItemKind::STRUCT)
475 | Some(lsp_types::CompletionItemKind::CLASS) => Some("S".to_string()),
476 Some(lsp_types::CompletionItemKind::CONSTANT) => Some("c".to_string()),
477 Some(lsp_types::CompletionItemKind::KEYWORD) => Some("k".to_string()),
478 _ => None,
479 };
480
481 PopupListItemData {
482 text: item.label.clone(),
483 detail: item.detail.clone(),
484 icon,
485 data: item
486 .insert_text
487 .clone()
488 .or_else(|| Some(item.label.clone())),
489 }
490 })
491 .collect()
492}