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]
136#[must_use]
137pub struct CompletionDebugOptions<'a> {
138 pub width: u16,
140 pub height: u16,
142 pub ansi: bool,
144 pub unicode: bool,
146 pub appearance: Option<&'a ReplAppearance>,
148}
149
150impl<'a> CompletionDebugOptions<'a> {
151 pub fn new(width: u16, height: u16) -> Self {
168 Self {
169 width,
170 height,
171 ansi: false,
172 unicode: false,
173 appearance: None,
174 }
175 }
176
177 pub fn with_ansi(mut self, ansi: bool) -> Self {
179 self.ansi = ansi;
180 self
181 }
182
183 pub fn with_unicode(mut self, unicode: bool) -> Self {
185 self.unicode = unicode;
186 self
187 }
188
189 pub fn with_appearance(mut self, appearance: Option<&'a ReplAppearance>) -> Self {
191 self.appearance = appearance;
192 self
193 }
194}
195
196pub fn debug_completion(
198 tree: &CompletionTree,
199 line: &str,
200 cursor: usize,
201 options: CompletionDebugOptions<'_>,
202) -> CompletionDebug {
203 let (editor, mut completer, mut menu) =
204 build_debug_completion_session(tree, line, cursor, options.appearance);
205 let mut editor = editor;
206
207 menu.menu_event(MenuEvent::Activate(false));
208 menu.apply_event(&mut editor, completer.as_mut());
209
210 snapshot_completion_debug(
211 tree,
212 &mut menu,
213 &editor,
214 options.width,
215 options.height,
216 options.ansi,
217 options.unicode,
218 )
219}
220
221pub fn debug_history_menu(
223 history: &SharedHistory,
224 line: &str,
225 cursor: usize,
226 options: CompletionDebugOptions<'_>,
227) -> CompletionDebug {
228 let (editor, mut completer, mut menu) =
229 build_debug_history_session(history, line, cursor, options.appearance);
230 let mut editor = editor;
231
232 menu.menu_event(MenuEvent::Activate(false));
233 menu.apply_event(&mut editor, completer.as_mut());
234
235 snapshot_history_debug(
236 &mut menu,
237 &editor,
238 cursor,
239 options.width,
240 options.height,
241 options.ansi,
242 options.unicode,
243 )
244}
245
246pub fn debug_completion_steps(
248 tree: &CompletionTree,
249 line: &str,
250 cursor: usize,
251 options: CompletionDebugOptions<'_>,
252 steps: &[DebugStep],
253) -> Vec<CompletionDebugFrame> {
254 let (mut editor, mut completer, mut menu) =
255 build_debug_completion_session(tree, line, cursor, options.appearance);
256
257 let steps = steps.to_vec();
258 if steps.is_empty() {
259 return Vec::new();
260 }
261
262 let mut frames = Vec::with_capacity(steps.len());
263 for step in steps {
264 apply_debug_step(step, &mut menu, &mut editor, completer.as_mut());
265 let state = snapshot_completion_debug(
266 tree,
267 &mut menu,
268 &editor,
269 options.width,
270 options.height,
271 options.ansi,
272 options.unicode,
273 );
274 frames.push(CompletionDebugFrame {
275 step: step.as_str().to_string(),
276 state,
277 });
278 }
279
280 frames
281}
282
283pub fn debug_history_menu_steps(
285 history: &SharedHistory,
286 line: &str,
287 cursor: usize,
288 options: CompletionDebugOptions<'_>,
289 steps: &[DebugStep],
290) -> Vec<CompletionDebugFrame> {
291 let (mut editor, mut completer, mut menu) =
292 build_debug_history_session(history, line, cursor, options.appearance);
293
294 let steps = steps.to_vec();
295 if steps.is_empty() {
296 return Vec::new();
297 }
298
299 let mut frames = Vec::with_capacity(steps.len());
300 for step in steps {
301 apply_debug_step(step, &mut menu, &mut editor, completer.as_mut());
302 let state = snapshot_history_debug(
303 &mut menu,
304 &editor,
305 cursor,
306 options.width,
307 options.height,
308 options.ansi,
309 options.unicode,
310 );
311 frames.push(CompletionDebugFrame {
312 step: step.as_str().to_string(),
313 state,
314 });
315 }
316
317 frames
318}
319
320fn build_debug_completion_session(
321 tree: &CompletionTree,
322 line: &str,
323 cursor: usize,
324 appearance: Option<&ReplAppearance>,
325) -> (Editor, Box<dyn Completer>, OspCompletionMenu) {
326 let mut editor = Editor::default();
327 editor.edit_buffer(
328 |buf| {
329 buf.set_buffer(line.to_string());
330 buf.set_insertion_point(cursor.min(buf.get_buffer().len()));
331 },
332 UndoBehavior::CreateUndoPoint,
333 );
334
335 let completer = Box::new(ReplCompleter::new(Vec::new(), Some(tree.clone()), None));
336 let menu = if let Some(appearance) = appearance {
337 build_completion_menu(appearance)
338 } else {
339 OspCompletionMenu::default()
340 };
341
342 (editor, completer, menu)
343}
344
345fn build_debug_history_session(
346 history: &SharedHistory,
347 line: &str,
348 cursor: usize,
349 appearance: Option<&ReplAppearance>,
350) -> (Editor, Box<dyn Completer>, OspCompletionMenu) {
351 let mut editor = Editor::default();
352 editor.edit_buffer(
353 |buf| {
354 buf.set_buffer(line.to_string());
355 buf.set_insertion_point(cursor.min(buf.get_buffer().len()));
356 },
357 UndoBehavior::CreateUndoPoint,
358 );
359
360 let completer = Box::new(ReplHistoryCompleter::new(history.clone()));
361 let menu = if let Some(appearance) = appearance {
362 build_history_menu(appearance)
363 } else {
364 OspCompletionMenu::default()
365 .with_name(HISTORY_MENU_NAME)
366 .with_quick_complete(false)
367 .with_columns(1)
368 .with_max_rows(DEFAULT_HISTORY_MENU_ROWS)
369 };
370
371 (editor, completer, menu)
372}
373
374fn apply_debug_step(
375 step: DebugStep,
376 menu: &mut OspCompletionMenu,
377 editor: &mut Editor,
378 completer: &mut dyn Completer,
379) {
380 match step {
381 DebugStep::Tab => {
382 if menu.is_active() {
383 dispatch_menu_event(menu, editor, completer, MenuEvent::NextElement);
384 } else {
385 dispatch_menu_event(menu, editor, completer, MenuEvent::Activate(false));
386 }
387 }
388 DebugStep::BackTab => {
389 if menu.is_active() {
390 dispatch_menu_event(menu, editor, completer, MenuEvent::PreviousElement);
391 } else {
392 dispatch_menu_event(menu, editor, completer, MenuEvent::Activate(false));
393 }
394 }
395 DebugStep::Up => {
396 if menu.is_active() {
397 dispatch_menu_event(menu, editor, completer, MenuEvent::MoveUp);
398 }
399 }
400 DebugStep::Down => {
401 if menu.is_active() {
402 dispatch_menu_event(menu, editor, completer, MenuEvent::MoveDown);
403 }
404 }
405 DebugStep::Left => {
406 if menu.is_active() {
407 dispatch_menu_event(menu, editor, completer, MenuEvent::MoveLeft);
408 }
409 }
410 DebugStep::Right => {
411 if menu.is_active() {
412 dispatch_menu_event(menu, editor, completer, MenuEvent::MoveRight);
413 }
414 }
415 DebugStep::Accept => {
416 if menu.is_active() {
417 menu.accept_selection_in_buffer(editor);
418 dispatch_menu_event(menu, editor, completer, MenuEvent::Deactivate);
419 }
420 }
421 DebugStep::Close => {
422 dispatch_menu_event(menu, editor, completer, MenuEvent::Deactivate);
423 }
424 }
425}
426
427fn dispatch_menu_event(
428 menu: &mut OspCompletionMenu,
429 editor: &mut Editor,
430 completer: &mut dyn Completer,
431 event: MenuEvent,
432) {
433 menu.menu_event(event);
434 menu.apply_event(editor, completer);
435}
436
437fn snapshot_completion_debug(
438 tree: &CompletionTree,
439 menu: &mut OspCompletionMenu,
440 editor: &Editor,
441 width: u16,
442 height: u16,
443 ansi: bool,
444 unicode: bool,
445) -> CompletionDebug {
446 let line = editor.get_buffer().to_string();
447 let cursor = editor.line_buffer().insertion_point();
448 let values = menu.get_values();
449 let engine = CompletionEngine::new(tree.clone());
450 let analysis = engine.analyze(&line, cursor);
451
452 let (stub, replace_range) = if let Some(first) = values.first() {
453 let start = first.span.start;
454 let end = first.span.end;
455 let stub = line.get(start..end).unwrap_or("").to_string();
456 (stub, [start, end])
457 } else {
458 (
459 analysis.cursor.raw_stub.clone(),
460 [
461 analysis.cursor.replace_range.start,
462 analysis.cursor.replace_range.end,
463 ],
464 )
465 };
466
467 let matches = values
468 .iter()
469 .map(|item| CompletionDebugMatch {
470 id: item.value.clone(),
471 label: display_text(item).to_string(),
472 description: item.description.clone(),
473 kind: engine
474 .classify_match(&analysis, &item.value)
475 .as_str()
476 .to_string(),
477 })
478 .collect::<Vec<_>>();
479
480 let MenuDebug {
481 columns,
482 rows,
483 visible_rows,
484 indent,
485 selected_index,
486 selected_row,
487 selected_col,
488 description,
489 description_rendered,
490 styles,
491 rendered,
492 } = debug_snapshot(menu, editor, width, height, ansi);
493
494 let selected = if matches.is_empty() {
495 -1
496 } else {
497 selected_index
498 };
499
500 CompletionDebug {
501 line,
502 cursor,
503 replace_range,
504 stub,
505 matches,
506 selected,
507 selected_row,
508 selected_col,
509 columns,
510 rows,
511 visible_rows,
512 menu_indent: indent,
513 menu_styles: styles,
514 menu_description: description,
515 menu_description_rendered: description_rendered,
516 width,
517 height,
518 unicode,
519 color: ansi,
520 rendered,
521 }
522}
523
524fn snapshot_history_debug(
525 menu: &mut OspCompletionMenu,
526 editor: &Editor,
527 cursor: usize,
528 width: u16,
529 height: u16,
530 ansi: bool,
531 unicode: bool,
532) -> CompletionDebug {
533 let line = editor.get_buffer().to_string();
534 let cursor = cursor
535 .min(editor.line_buffer().insertion_point())
536 .min(line.len());
537 let values = menu.get_values();
538 let query = line.get(..cursor).unwrap_or(&line).trim().to_string();
539
540 let (stub, replace_range) = if let Some(first) = values.first() {
541 let start = first.span.start;
542 let end = first.span.end;
543 let stub = line.get(start..end).unwrap_or("").to_string();
544 (stub, [start, end])
545 } else {
546 (query, [0, line.len()])
547 };
548
549 let matches = values
550 .iter()
551 .map(|item| CompletionDebugMatch {
552 id: item.value.clone(),
553 label: display_text(item).to_string(),
554 description: item.description.clone(),
555 kind: "history".to_string(),
556 })
557 .collect::<Vec<_>>();
558
559 let MenuDebug {
560 columns,
561 rows,
562 visible_rows,
563 indent,
564 selected_index,
565 selected_row,
566 selected_col,
567 description,
568 description_rendered,
569 styles,
570 rendered,
571 } = debug_snapshot(menu, editor, width, height, ansi);
572
573 let selected = if matches.is_empty() {
574 -1
575 } else {
576 selected_index
577 };
578
579 CompletionDebug {
580 line,
581 cursor,
582 replace_range,
583 stub,
584 matches,
585 selected,
586 selected_row,
587 selected_col,
588 columns,
589 rows,
590 visible_rows,
591 menu_indent: indent,
592 menu_styles: styles,
593 menu_description: description,
594 menu_description_rendered: description_rendered,
595 width,
596 height,
597 unicode,
598 color: ansi,
599 rendered,
600 }
601}