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