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_lsp_confirmation.is_some() {
52 let action = self
53 .active_state()
54 .popups
55 .top()
56 .and_then(|p| p.selected_item())
57 .and_then(|item| item.data.clone());
58 if let Some(action) = action {
59 self.hide_popup();
60 self.handle_lsp_confirmation_response(&action);
61 return PopupConfirmResult::EarlyReturn;
62 }
63 }
64
65 let completion_text = self
67 .active_state()
68 .popups
69 .top()
70 .filter(|p| p.kind == crate::view::popup::PopupKind::Completion)
71 .and_then(|p| p.selected_item())
72 .and_then(|item| item.data.clone());
73
74 if let Some(text) = completion_text {
76 self.insert_completion_text(text);
77 }
78
79 self.hide_popup();
80 PopupConfirmResult::Done
81 }
82
83 fn insert_completion_text(&mut self, text: String) {
86 let (insert_text, cursor_offset) = if is_snippet(&text) {
88 let expanded = expand_snippet(&text);
89 (expanded.text, Some(expanded.cursor_offset))
90 } else {
91 (text, None)
92 };
93
94 let (cursor_id, cursor_pos, word_start) = {
95 let cursors = self.active_cursors();
96 let cursor_id = cursors.primary_id();
97 let cursor_pos = cursors.primary().position;
98 let state = self.active_state();
99 let word_start = find_completion_word_start(&state.buffer, cursor_pos);
100 (cursor_id, cursor_pos, word_start)
101 };
102
103 let deleted_text = if word_start < cursor_pos {
104 self.active_state_mut()
105 .get_text_range(word_start, cursor_pos)
106 } else {
107 String::new()
108 };
109
110 let insert_pos = if word_start < cursor_pos {
111 let delete_event = Event::Delete {
112 range: word_start..cursor_pos,
113 deleted_text,
114 cursor_id,
115 };
116
117 self.active_event_log_mut().append(delete_event.clone());
118 self.apply_event_to_active_buffer(&delete_event);
119
120 let buffer_len = self.active_state().buffer.len();
121 word_start.min(buffer_len)
122 } else {
123 cursor_pos
124 };
125
126 let insert_event = Event::Insert {
127 position: insert_pos,
128 text: insert_text.clone(),
129 cursor_id,
130 };
131
132 self.active_event_log_mut().append(insert_event.clone());
133 self.apply_event_to_active_buffer(&insert_event);
134
135 if let Some(offset) = cursor_offset {
137 let new_cursor_pos = insert_pos + offset;
138 let current_pos = self.active_cursors().primary().position;
140 if current_pos != new_cursor_pos {
141 let move_event = Event::MoveCursor {
142 cursor_id,
143 old_position: current_pos,
144 new_position: new_cursor_pos,
145 old_anchor: None,
146 new_anchor: None,
147 old_sticky_column: 0,
148 new_sticky_column: 0,
149 };
150 let split_id = self.split_manager.active_split();
151 let buffer_id = self.active_buffer();
152 let state = self.buffers.get_mut(&buffer_id).unwrap();
153 let cursors = &mut self.split_view_states.get_mut(&split_id).unwrap().cursors;
154 state.apply(cursors, &move_event);
155 }
156 }
157 }
158
159 pub fn handle_popup_cancel(&mut self) {
161 tracing::info!(
162 "handle_popup_cancel: active_action_popup={:?}",
163 self.active_action_popup.as_ref().map(|(id, _)| id)
164 );
165
166 if let Some((popup_id, _actions)) = self.active_action_popup.take() {
168 tracing::info!(
169 "handle_popup_cancel: dismissing action popup id={}",
170 popup_id
171 );
172 self.hide_popup();
173
174 self.plugin_manager.run_hook(
176 "action_popup_result",
177 crate::services::plugins::hooks::HookArgs::ActionPopupResult {
178 popup_id,
179 action_id: "dismissed".to_string(),
180 },
181 );
182 tracing::info!("handle_popup_cancel: action_popup_result hook fired");
183 return;
184 }
185
186 if self.pending_lsp_confirmation.is_some() {
187 self.pending_lsp_confirmation = None;
188 self.set_status_message(t!("lsp.startup_cancelled_msg").to_string());
189 }
190 self.hide_popup();
191 self.completion_items = None;
193 }
194
195 pub fn handle_popup_type_char(&mut self, c: char) {
198 let (cursor_id, cursor_pos) = {
200 let cursors = self.active_cursors();
201 (cursors.primary_id(), cursors.primary().position)
202 };
203
204 let insert_event = Event::Insert {
205 position: cursor_pos,
206 text: c.to_string(),
207 cursor_id,
208 };
209
210 self.active_event_log_mut().append(insert_event.clone());
211 self.apply_event_to_active_buffer(&insert_event);
212
213 self.refilter_completion_popup();
215 }
216
217 pub fn handle_popup_backspace(&mut self) {
220 let (cursor_id, cursor_pos) = {
221 let cursors = self.active_cursors();
222 (cursors.primary_id(), cursors.primary().position)
223 };
224
225 if cursor_pos == 0 {
227 return;
228 }
229
230 let prev_pos = {
232 let state = self.active_state();
233 let text = match state.buffer.to_string() {
234 Some(t) => t,
235 None => return,
236 };
237 text[..cursor_pos]
239 .char_indices()
240 .last()
241 .map(|(i, _)| i)
242 .unwrap_or(0)
243 };
244
245 let deleted_text = self.active_state_mut().get_text_range(prev_pos, cursor_pos);
246
247 let delete_event = Event::Delete {
248 range: prev_pos..cursor_pos,
249 deleted_text,
250 cursor_id,
251 };
252
253 self.active_event_log_mut().append(delete_event.clone());
254 self.apply_event_to_active_buffer(&delete_event);
255
256 self.refilter_completion_popup();
258 }
259
260 fn refilter_completion_popup(&mut self) {
263 let items = match &self.completion_items {
265 Some(items) if !items.is_empty() => items.clone(),
266 _ => {
267 self.hide_popup();
268 return;
269 }
270 };
271
272 let (word_start, cursor_pos) = {
274 let cursor_pos = self.active_cursors().primary().position;
275 let state = self.active_state();
276 let word_start = find_completion_word_start(&state.buffer, cursor_pos);
277 (word_start, cursor_pos)
278 };
279
280 let prefix = if word_start < cursor_pos {
281 self.active_state_mut()
282 .get_text_range(word_start, cursor_pos)
283 .to_lowercase()
284 } else {
285 String::new()
286 };
287
288 let filtered_items: Vec<&lsp_types::CompletionItem> = if prefix.is_empty() {
290 items.iter().collect()
291 } else {
292 items
293 .iter()
294 .filter(|item| {
295 item.label.to_lowercase().starts_with(&prefix)
296 || item
297 .filter_text
298 .as_ref()
299 .map(|ft| ft.to_lowercase().starts_with(&prefix))
300 .unwrap_or(false)
301 })
302 .collect()
303 };
304
305 if filtered_items.is_empty() {
307 self.hide_popup();
308 self.completion_items = None;
309 return;
310 }
311
312 let current_selection = self
314 .active_state()
315 .popups
316 .top()
317 .and_then(|p| p.selected_item())
318 .map(|item| item.text.clone());
319
320 let selected = current_selection
322 .and_then(|sel| filtered_items.iter().position(|item| item.label == sel))
323 .unwrap_or(0);
324
325 let popup_data = build_completion_popup(&filtered_items, selected);
326
327 self.hide_popup();
329 let split_id = self.split_manager.active_split();
330 let buffer_id = self.active_buffer();
331 let state = self.buffers.get_mut(&buffer_id).unwrap();
332 let cursors = &mut self.split_view_states.get_mut(&split_id).unwrap().cursors;
333 state.apply(
334 cursors,
335 &crate::model::event::Event::ShowPopup { popup: popup_data },
336 );
337 }
338}
339
340pub(crate) fn build_completion_popup(
345 items: &[&lsp_types::CompletionItem],
346 selected: usize,
347) -> crate::model::event::PopupData {
348 use crate::model::event::{
349 PopupContentData, PopupKindHint, PopupListItemData, PopupPositionData,
350 };
351
352 let list_items: Vec<PopupListItemData> = items
353 .iter()
354 .map(|item| {
355 let icon = match item.kind {
356 Some(lsp_types::CompletionItemKind::FUNCTION)
357 | Some(lsp_types::CompletionItemKind::METHOD) => Some("λ".to_string()),
358 Some(lsp_types::CompletionItemKind::VARIABLE) => Some("v".to_string()),
359 Some(lsp_types::CompletionItemKind::STRUCT)
360 | Some(lsp_types::CompletionItemKind::CLASS) => Some("S".to_string()),
361 Some(lsp_types::CompletionItemKind::CONSTANT) => Some("c".to_string()),
362 Some(lsp_types::CompletionItemKind::KEYWORD) => Some("k".to_string()),
363 _ => None,
364 };
365
366 PopupListItemData {
367 text: item.label.clone(),
368 detail: item.detail.clone(),
369 icon,
370 data: item
371 .insert_text
372 .clone()
373 .or_else(|| Some(item.label.clone())),
374 }
375 })
376 .collect();
377
378 crate::model::event::PopupData {
379 kind: PopupKindHint::Completion,
380 title: None,
381 description: None,
382 transient: false,
383 content: PopupContentData::List {
384 items: list_items,
385 selected,
386 },
387 position: PopupPositionData::BelowCursor,
388 width: 50,
389 max_height: 15,
390 bordered: true,
391 }
392}