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