1use super::adapter::{ReplCompleter, ReplHistoryCompleter};
2use super::config::{DEFAULT_HISTORY_MENU_ROWS, ReplAppearance};
3use super::overlay::{build_completion_menu, build_history_menu};
4use super::{HISTORY_MENU_NAME, SharedHistory};
5use crate::completion::{CompletionEngine, CompletionTree};
6use crate::repl::menu::{
7 MenuDebug, MenuStyleDebug, OspCompletionMenu, debug_snapshot, display_text,
8};
9use reedline::{Completer, Editor, Menu, MenuEvent, UndoBehavior};
10use serde::Serialize;
11
12#[derive(Debug, Clone, Serialize)]
14pub struct CompletionDebugMatch {
15 pub id: String,
17 pub label: String,
19 pub description: Option<String>,
21 pub kind: String,
23}
24
25#[derive(Debug, Clone, Serialize)]
29pub struct CompletionDebug {
30 pub line: String,
32 pub cursor: usize,
34 pub replace_range: [usize; 2],
36 pub stub: String,
38 pub matches: Vec<CompletionDebugMatch>,
40 pub selected: i64,
42 pub selected_row: u16,
44 pub selected_col: u16,
46 pub columns: u16,
48 pub rows: u16,
50 pub visible_rows: u16,
52 pub menu_indent: u16,
54 pub menu_styles: MenuStyleDebug,
56 pub menu_description: Option<String>,
58 pub menu_description_rendered: Option<String>,
60 pub width: u16,
62 pub height: u16,
64 pub unicode: bool,
66 pub color: bool,
68 pub rendered: Vec<String>,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum DebugStep {
75 Tab,
77 BackTab,
79 Up,
81 Down,
83 Left,
85 Right,
87 Accept,
89 Close,
91}
92
93impl DebugStep {
94 pub fn parse(raw: &str) -> Option<Self> {
96 match raw.trim().to_ascii_lowercase().as_str() {
97 "tab" => Some(Self::Tab),
98 "backtab" | "shift-tab" | "shift_tab" => Some(Self::BackTab),
99 "up" => Some(Self::Up),
100 "down" => Some(Self::Down),
101 "left" => Some(Self::Left),
102 "right" => Some(Self::Right),
103 "accept" | "enter" => Some(Self::Accept),
104 "close" | "esc" | "escape" => Some(Self::Close),
105 _ => None,
106 }
107 }
108
109 pub fn as_str(&self) -> &'static str {
111 match self {
112 Self::Tab => "tab",
113 Self::BackTab => "backtab",
114 Self::Up => "up",
115 Self::Down => "down",
116 Self::Left => "left",
117 Self::Right => "right",
118 Self::Accept => "accept",
119 Self::Close => "close",
120 }
121 }
122}
123
124#[derive(Debug, Clone, Serialize)]
126pub struct CompletionDebugFrame {
127 pub step: String,
129 pub state: CompletionDebug,
131}
132
133#[derive(Debug, Clone, Copy)]
135#[non_exhaustive]
136pub struct CompletionDebugOptions<'a> {
137 pub width: u16,
139 pub height: u16,
141 pub ansi: bool,
143 pub unicode: bool,
145 pub appearance: Option<&'a ReplAppearance>,
147}
148
149impl<'a> CompletionDebugOptions<'a> {
150 pub fn new(width: u16, height: u16) -> Self {
167 Self {
168 width,
169 height,
170 ansi: false,
171 unicode: false,
172 appearance: None,
173 }
174 }
175
176 pub fn with_ansi(mut self, ansi: bool) -> Self {
178 self.ansi = ansi;
179 self
180 }
181
182 pub fn with_unicode(mut self, unicode: bool) -> Self {
184 self.unicode = unicode;
185 self
186 }
187
188 pub fn with_appearance(mut self, appearance: Option<&'a ReplAppearance>) -> Self {
190 self.appearance = appearance;
191 self
192 }
193}
194
195pub fn debug_completion(
197 tree: &CompletionTree,
198 line: &str,
199 cursor: usize,
200 options: CompletionDebugOptions<'_>,
201) -> CompletionDebug {
202 let (editor, mut completer, mut menu) =
203 build_debug_completion_session(tree, line, cursor, options.appearance);
204 let mut editor = editor;
205
206 menu.menu_event(MenuEvent::Activate(false));
207 menu.apply_event(&mut editor, completer.as_mut());
208
209 snapshot_completion_debug(
210 tree,
211 &mut menu,
212 &editor,
213 options.width,
214 options.height,
215 options.ansi,
216 options.unicode,
217 )
218}
219
220pub fn debug_history_menu(
222 history: &SharedHistory,
223 line: &str,
224 cursor: usize,
225 options: CompletionDebugOptions<'_>,
226) -> CompletionDebug {
227 let (editor, mut completer, mut menu) =
228 build_debug_history_session(history, line, cursor, options.appearance);
229 let mut editor = editor;
230
231 menu.menu_event(MenuEvent::Activate(false));
232 menu.apply_event(&mut editor, completer.as_mut());
233
234 snapshot_history_debug(
235 &mut menu,
236 &editor,
237 cursor,
238 options.width,
239 options.height,
240 options.ansi,
241 options.unicode,
242 )
243}
244
245pub fn debug_completion_steps(
247 tree: &CompletionTree,
248 line: &str,
249 cursor: usize,
250 options: CompletionDebugOptions<'_>,
251 steps: &[DebugStep],
252) -> Vec<CompletionDebugFrame> {
253 let (mut editor, mut completer, mut menu) =
254 build_debug_completion_session(tree, line, cursor, options.appearance);
255
256 let steps = steps.to_vec();
257 if steps.is_empty() {
258 return Vec::new();
259 }
260
261 let mut frames = Vec::with_capacity(steps.len());
262 for step in steps {
263 apply_debug_step(step, &mut menu, &mut editor, completer.as_mut());
264 let state = snapshot_completion_debug(
265 tree,
266 &mut menu,
267 &editor,
268 options.width,
269 options.height,
270 options.ansi,
271 options.unicode,
272 );
273 frames.push(CompletionDebugFrame {
274 step: step.as_str().to_string(),
275 state,
276 });
277 }
278
279 frames
280}
281
282pub fn debug_history_menu_steps(
284 history: &SharedHistory,
285 line: &str,
286 cursor: usize,
287 options: CompletionDebugOptions<'_>,
288 steps: &[DebugStep],
289) -> Vec<CompletionDebugFrame> {
290 let (mut editor, mut completer, mut menu) =
291 build_debug_history_session(history, line, cursor, options.appearance);
292
293 let steps = steps.to_vec();
294 if steps.is_empty() {
295 return Vec::new();
296 }
297
298 let mut frames = Vec::with_capacity(steps.len());
299 for step in steps {
300 apply_debug_step(step, &mut menu, &mut editor, completer.as_mut());
301 let state = snapshot_history_debug(
302 &mut menu,
303 &editor,
304 cursor,
305 options.width,
306 options.height,
307 options.ansi,
308 options.unicode,
309 );
310 frames.push(CompletionDebugFrame {
311 step: step.as_str().to_string(),
312 state,
313 });
314 }
315
316 frames
317}
318
319fn build_debug_completion_session(
320 tree: &CompletionTree,
321 line: &str,
322 cursor: usize,
323 appearance: Option<&ReplAppearance>,
324) -> (Editor, Box<dyn Completer>, OspCompletionMenu) {
325 let mut editor = Editor::default();
326 editor.edit_buffer(
327 |buf| {
328 buf.set_buffer(line.to_string());
329 buf.set_insertion_point(cursor.min(buf.get_buffer().len()));
330 },
331 UndoBehavior::CreateUndoPoint,
332 );
333
334 let completer = Box::new(ReplCompleter::new(Vec::new(), Some(tree.clone()), None));
335 let menu = if let Some(appearance) = appearance {
336 build_completion_menu(appearance)
337 } else {
338 OspCompletionMenu::default()
339 };
340
341 (editor, completer, menu)
342}
343
344fn build_debug_history_session(
345 history: &SharedHistory,
346 line: &str,
347 cursor: usize,
348 appearance: Option<&ReplAppearance>,
349) -> (Editor, Box<dyn Completer>, OspCompletionMenu) {
350 let mut editor = Editor::default();
351 editor.edit_buffer(
352 |buf| {
353 buf.set_buffer(line.to_string());
354 buf.set_insertion_point(cursor.min(buf.get_buffer().len()));
355 },
356 UndoBehavior::CreateUndoPoint,
357 );
358
359 let completer = Box::new(ReplHistoryCompleter::new(history.clone()));
360 let menu = if let Some(appearance) = appearance {
361 build_history_menu(appearance)
362 } else {
363 OspCompletionMenu::default()
364 .with_name(HISTORY_MENU_NAME)
365 .with_quick_complete(false)
366 .with_columns(1)
367 .with_max_rows(DEFAULT_HISTORY_MENU_ROWS)
368 };
369
370 (editor, completer, menu)
371}
372
373fn apply_debug_step(
374 step: DebugStep,
375 menu: &mut OspCompletionMenu,
376 editor: &mut Editor,
377 completer: &mut dyn Completer,
378) {
379 match step {
380 DebugStep::Tab => {
381 if menu.is_active() {
382 dispatch_menu_event(menu, editor, completer, MenuEvent::NextElement);
383 } else {
384 dispatch_menu_event(menu, editor, completer, MenuEvent::Activate(false));
385 }
386 }
387 DebugStep::BackTab => {
388 if menu.is_active() {
389 dispatch_menu_event(menu, editor, completer, MenuEvent::PreviousElement);
390 } else {
391 dispatch_menu_event(menu, editor, completer, MenuEvent::Activate(false));
392 }
393 }
394 DebugStep::Up => {
395 if menu.is_active() {
396 dispatch_menu_event(menu, editor, completer, MenuEvent::MoveUp);
397 }
398 }
399 DebugStep::Down => {
400 if menu.is_active() {
401 dispatch_menu_event(menu, editor, completer, MenuEvent::MoveDown);
402 }
403 }
404 DebugStep::Left => {
405 if menu.is_active() {
406 dispatch_menu_event(menu, editor, completer, MenuEvent::MoveLeft);
407 }
408 }
409 DebugStep::Right => {
410 if menu.is_active() {
411 dispatch_menu_event(menu, editor, completer, MenuEvent::MoveRight);
412 }
413 }
414 DebugStep::Accept => {
415 if menu.is_active() {
416 menu.accept_selection_in_buffer(editor);
417 dispatch_menu_event(menu, editor, completer, MenuEvent::Deactivate);
418 }
419 }
420 DebugStep::Close => {
421 dispatch_menu_event(menu, editor, completer, MenuEvent::Deactivate);
422 }
423 }
424}
425
426fn dispatch_menu_event(
427 menu: &mut OspCompletionMenu,
428 editor: &mut Editor,
429 completer: &mut dyn Completer,
430 event: MenuEvent,
431) {
432 menu.menu_event(event);
433 menu.apply_event(editor, completer);
434}
435
436fn snapshot_completion_debug(
437 tree: &CompletionTree,
438 menu: &mut OspCompletionMenu,
439 editor: &Editor,
440 width: u16,
441 height: u16,
442 ansi: bool,
443 unicode: bool,
444) -> CompletionDebug {
445 let line = editor.get_buffer().to_string();
446 let cursor = editor.line_buffer().insertion_point();
447 let values = menu.get_values();
448 let engine = CompletionEngine::new(tree.clone());
449 let analysis = engine.analyze(&line, cursor);
450
451 let (stub, replace_range) = if let Some(first) = values.first() {
452 let start = first.span.start;
453 let end = first.span.end;
454 let stub = line.get(start..end).unwrap_or("").to_string();
455 (stub, [start, end])
456 } else {
457 (
458 analysis.cursor.raw_stub.clone(),
459 [
460 analysis.cursor.replace_range.start,
461 analysis.cursor.replace_range.end,
462 ],
463 )
464 };
465
466 let matches = values
467 .iter()
468 .map(|item| CompletionDebugMatch {
469 id: item.value.clone(),
470 label: display_text(item).to_string(),
471 description: item.description.clone(),
472 kind: engine
473 .classify_match(&analysis, &item.value)
474 .as_str()
475 .to_string(),
476 })
477 .collect::<Vec<_>>();
478
479 let MenuDebug {
480 columns,
481 rows,
482 visible_rows,
483 indent,
484 selected_index,
485 selected_row,
486 selected_col,
487 description,
488 description_rendered,
489 styles,
490 rendered,
491 } = debug_snapshot(menu, editor, width, height, ansi);
492
493 let selected = if matches.is_empty() {
494 -1
495 } else {
496 selected_index
497 };
498
499 CompletionDebug {
500 line,
501 cursor,
502 replace_range,
503 stub,
504 matches,
505 selected,
506 selected_row,
507 selected_col,
508 columns,
509 rows,
510 visible_rows,
511 menu_indent: indent,
512 menu_styles: styles,
513 menu_description: description,
514 menu_description_rendered: description_rendered,
515 width,
516 height,
517 unicode,
518 color: ansi,
519 rendered,
520 }
521}
522
523fn snapshot_history_debug(
524 menu: &mut OspCompletionMenu,
525 editor: &Editor,
526 cursor: usize,
527 width: u16,
528 height: u16,
529 ansi: bool,
530 unicode: bool,
531) -> CompletionDebug {
532 let line = editor.get_buffer().to_string();
533 let cursor = cursor
534 .min(editor.line_buffer().insertion_point())
535 .min(line.len());
536 let values = menu.get_values();
537 let query = line.get(..cursor).unwrap_or(&line).trim().to_string();
538
539 let (stub, replace_range) = if let Some(first) = values.first() {
540 let start = first.span.start;
541 let end = first.span.end;
542 let stub = line.get(start..end).unwrap_or("").to_string();
543 (stub, [start, end])
544 } else {
545 (query, [0, line.len()])
546 };
547
548 let matches = values
549 .iter()
550 .map(|item| CompletionDebugMatch {
551 id: item.value.clone(),
552 label: display_text(item).to_string(),
553 description: item.description.clone(),
554 kind: "history".to_string(),
555 })
556 .collect::<Vec<_>>();
557
558 let MenuDebug {
559 columns,
560 rows,
561 visible_rows,
562 indent,
563 selected_index,
564 selected_row,
565 selected_col,
566 description,
567 description_rendered,
568 styles,
569 rendered,
570 } = debug_snapshot(menu, editor, width, height, ansi);
571
572 let selected = if matches.is_empty() {
573 -1
574 } else {
575 selected_index
576 };
577
578 CompletionDebug {
579 line,
580 cursor,
581 replace_range,
582 stub,
583 matches,
584 selected,
585 selected_row,
586 selected_col,
587 columns,
588 rows,
589 visible_rows,
590 menu_indent: indent,
591 menu_styles: styles,
592 menu_description: description,
593 menu_description_rendered: description_rendered,
594 width,
595 height,
596 unicode,
597 color: ansi,
598 rendered,
599 }
600}