use super::adapter::{ReplCompleter, ReplHistoryCompleter};
use super::config::{DEFAULT_HISTORY_MENU_ROWS, ReplAppearance};
use super::overlay::{build_completion_menu, build_history_menu};
use super::{HISTORY_MENU_NAME, SharedHistory};
use crate::completion::{CompletionEngine, CompletionTree};
use crate::repl::menu::{
MenuDebug, MenuStyleDebug, OspCompletionMenu, debug_snapshot, display_text,
};
use reedline::{Completer, Editor, Menu, MenuEvent, UndoBehavior};
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct CompletionDebugMatch {
pub id: String,
pub label: String,
pub description: Option<String>,
pub kind: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct CompletionDebug {
pub line: String,
pub cursor: usize,
pub replace_range: [usize; 2],
pub stub: String,
pub matches: Vec<CompletionDebugMatch>,
pub selected: i64,
pub selected_row: u16,
pub selected_col: u16,
pub columns: u16,
pub rows: u16,
pub visible_rows: u16,
pub menu_indent: u16,
pub menu_styles: MenuStyleDebug,
pub menu_description: Option<String>,
pub menu_description_rendered: Option<String>,
pub width: u16,
pub height: u16,
pub unicode: bool,
pub color: bool,
pub rendered: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DebugStep {
Tab,
BackTab,
Up,
Down,
Left,
Right,
Accept,
Close,
}
impl DebugStep {
pub fn parse(raw: &str) -> Option<Self> {
match raw.trim().to_ascii_lowercase().as_str() {
"tab" => Some(Self::Tab),
"backtab" | "shift-tab" | "shift_tab" => Some(Self::BackTab),
"up" => Some(Self::Up),
"down" => Some(Self::Down),
"left" => Some(Self::Left),
"right" => Some(Self::Right),
"accept" | "enter" => Some(Self::Accept),
"close" | "esc" | "escape" => Some(Self::Close),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Tab => "tab",
Self::BackTab => "backtab",
Self::Up => "up",
Self::Down => "down",
Self::Left => "left",
Self::Right => "right",
Self::Accept => "accept",
Self::Close => "close",
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct CompletionDebugFrame {
pub step: String,
pub state: CompletionDebug,
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
#[must_use]
pub struct CompletionDebugOptions<'a> {
pub width: u16,
pub height: u16,
pub ansi: bool,
pub unicode: bool,
pub appearance: Option<&'a ReplAppearance>,
}
impl<'a> CompletionDebugOptions<'a> {
pub fn new(width: u16, height: u16) -> Self {
Self {
width,
height,
ansi: false,
unicode: false,
appearance: None,
}
}
pub fn with_ansi(mut self, ansi: bool) -> Self {
self.ansi = ansi;
self
}
pub fn with_unicode(mut self, unicode: bool) -> Self {
self.unicode = unicode;
self
}
pub fn with_appearance(mut self, appearance: Option<&'a ReplAppearance>) -> Self {
self.appearance = appearance;
self
}
}
pub fn debug_completion(
tree: &CompletionTree,
line: &str,
cursor: usize,
options: CompletionDebugOptions<'_>,
) -> CompletionDebug {
let (editor, mut completer, mut menu) =
build_debug_completion_session(tree, line, cursor, options.appearance);
let mut editor = editor;
menu.menu_event(MenuEvent::Activate(false));
menu.apply_event(&mut editor, completer.as_mut());
snapshot_completion_debug(
tree,
&mut menu,
&editor,
options.width,
options.height,
options.ansi,
options.unicode,
)
}
pub fn debug_history_menu(
history: &SharedHistory,
line: &str,
cursor: usize,
options: CompletionDebugOptions<'_>,
) -> CompletionDebug {
let (editor, mut completer, mut menu) =
build_debug_history_session(history, line, cursor, options.appearance);
let mut editor = editor;
menu.menu_event(MenuEvent::Activate(false));
menu.apply_event(&mut editor, completer.as_mut());
snapshot_history_debug(
&mut menu,
&editor,
cursor,
options.width,
options.height,
options.ansi,
options.unicode,
)
}
pub fn debug_completion_steps(
tree: &CompletionTree,
line: &str,
cursor: usize,
options: CompletionDebugOptions<'_>,
steps: &[DebugStep],
) -> Vec<CompletionDebugFrame> {
let (mut editor, mut completer, mut menu) =
build_debug_completion_session(tree, line, cursor, options.appearance);
let steps = steps.to_vec();
if steps.is_empty() {
return Vec::new();
}
let mut frames = Vec::with_capacity(steps.len());
for step in steps {
apply_debug_step(step, &mut menu, &mut editor, completer.as_mut());
let state = snapshot_completion_debug(
tree,
&mut menu,
&editor,
options.width,
options.height,
options.ansi,
options.unicode,
);
frames.push(CompletionDebugFrame {
step: step.as_str().to_string(),
state,
});
}
frames
}
pub fn debug_history_menu_steps(
history: &SharedHistory,
line: &str,
cursor: usize,
options: CompletionDebugOptions<'_>,
steps: &[DebugStep],
) -> Vec<CompletionDebugFrame> {
let (mut editor, mut completer, mut menu) =
build_debug_history_session(history, line, cursor, options.appearance);
let steps = steps.to_vec();
if steps.is_empty() {
return Vec::new();
}
let mut frames = Vec::with_capacity(steps.len());
for step in steps {
apply_debug_step(step, &mut menu, &mut editor, completer.as_mut());
let state = snapshot_history_debug(
&mut menu,
&editor,
cursor,
options.width,
options.height,
options.ansi,
options.unicode,
);
frames.push(CompletionDebugFrame {
step: step.as_str().to_string(),
state,
});
}
frames
}
fn build_debug_completion_session(
tree: &CompletionTree,
line: &str,
cursor: usize,
appearance: Option<&ReplAppearance>,
) -> (Editor, Box<dyn Completer>, OspCompletionMenu) {
let mut editor = Editor::default();
editor.edit_buffer(
|buf| {
buf.set_buffer(line.to_string());
buf.set_insertion_point(cursor.min(buf.get_buffer().len()));
},
UndoBehavior::CreateUndoPoint,
);
let completer = Box::new(ReplCompleter::new(Vec::new(), Some(tree.clone()), None));
let menu = if let Some(appearance) = appearance {
build_completion_menu(appearance)
} else {
OspCompletionMenu::default()
};
(editor, completer, menu)
}
fn build_debug_history_session(
history: &SharedHistory,
line: &str,
cursor: usize,
appearance: Option<&ReplAppearance>,
) -> (Editor, Box<dyn Completer>, OspCompletionMenu) {
let mut editor = Editor::default();
editor.edit_buffer(
|buf| {
buf.set_buffer(line.to_string());
buf.set_insertion_point(cursor.min(buf.get_buffer().len()));
},
UndoBehavior::CreateUndoPoint,
);
let completer = Box::new(ReplHistoryCompleter::new(history.clone()));
let menu = if let Some(appearance) = appearance {
build_history_menu(appearance)
} else {
OspCompletionMenu::default()
.with_name(HISTORY_MENU_NAME)
.with_quick_complete(false)
.with_columns(1)
.with_max_rows(DEFAULT_HISTORY_MENU_ROWS)
};
(editor, completer, menu)
}
fn apply_debug_step(
step: DebugStep,
menu: &mut OspCompletionMenu,
editor: &mut Editor,
completer: &mut dyn Completer,
) {
match step {
DebugStep::Tab => {
if menu.is_active() {
dispatch_menu_event(menu, editor, completer, MenuEvent::NextElement);
} else {
dispatch_menu_event(menu, editor, completer, MenuEvent::Activate(false));
}
}
DebugStep::BackTab => {
if menu.is_active() {
dispatch_menu_event(menu, editor, completer, MenuEvent::PreviousElement);
} else {
dispatch_menu_event(menu, editor, completer, MenuEvent::Activate(false));
}
}
DebugStep::Up => {
if menu.is_active() {
dispatch_menu_event(menu, editor, completer, MenuEvent::MoveUp);
}
}
DebugStep::Down => {
if menu.is_active() {
dispatch_menu_event(menu, editor, completer, MenuEvent::MoveDown);
}
}
DebugStep::Left => {
if menu.is_active() {
dispatch_menu_event(menu, editor, completer, MenuEvent::MoveLeft);
}
}
DebugStep::Right => {
if menu.is_active() {
dispatch_menu_event(menu, editor, completer, MenuEvent::MoveRight);
}
}
DebugStep::Accept => {
if menu.is_active() {
menu.accept_selection_in_buffer(editor);
dispatch_menu_event(menu, editor, completer, MenuEvent::Deactivate);
}
}
DebugStep::Close => {
dispatch_menu_event(menu, editor, completer, MenuEvent::Deactivate);
}
}
}
fn dispatch_menu_event(
menu: &mut OspCompletionMenu,
editor: &mut Editor,
completer: &mut dyn Completer,
event: MenuEvent,
) {
menu.menu_event(event);
menu.apply_event(editor, completer);
}
fn snapshot_completion_debug(
tree: &CompletionTree,
menu: &mut OspCompletionMenu,
editor: &Editor,
width: u16,
height: u16,
ansi: bool,
unicode: bool,
) -> CompletionDebug {
let line = editor.get_buffer().to_string();
let cursor = editor.line_buffer().insertion_point();
let values = menu.get_values();
let engine = CompletionEngine::new(tree.clone());
let analysis = engine.analyze(&line, cursor);
let (stub, replace_range) = if let Some(first) = values.first() {
let start = first.span.start;
let end = first.span.end;
let stub = line.get(start..end).unwrap_or("").to_string();
(stub, [start, end])
} else {
(
analysis.cursor.raw_stub.clone(),
[
analysis.cursor.replace_range.start,
analysis.cursor.replace_range.end,
],
)
};
let matches = values
.iter()
.map(|item| CompletionDebugMatch {
id: item.value.clone(),
label: display_text(item).to_string(),
description: item.description.clone(),
kind: engine
.classify_match(&analysis, &item.value)
.as_str()
.to_string(),
})
.collect::<Vec<_>>();
let MenuDebug {
columns,
rows,
visible_rows,
indent,
selected_index,
selected_row,
selected_col,
description,
description_rendered,
styles,
rendered,
} = debug_snapshot(menu, editor, width, height, ansi);
let selected = if matches.is_empty() {
-1
} else {
selected_index
};
CompletionDebug {
line,
cursor,
replace_range,
stub,
matches,
selected,
selected_row,
selected_col,
columns,
rows,
visible_rows,
menu_indent: indent,
menu_styles: styles,
menu_description: description,
menu_description_rendered: description_rendered,
width,
height,
unicode,
color: ansi,
rendered,
}
}
fn snapshot_history_debug(
menu: &mut OspCompletionMenu,
editor: &Editor,
cursor: usize,
width: u16,
height: u16,
ansi: bool,
unicode: bool,
) -> CompletionDebug {
let line = editor.get_buffer().to_string();
let cursor = cursor
.min(editor.line_buffer().insertion_point())
.min(line.len());
let values = menu.get_values();
let query = line.get(..cursor).unwrap_or(&line).trim().to_string();
let (stub, replace_range) = if let Some(first) = values.first() {
let start = first.span.start;
let end = first.span.end;
let stub = line.get(start..end).unwrap_or("").to_string();
(stub, [start, end])
} else {
(query, [0, line.len()])
};
let matches = values
.iter()
.map(|item| CompletionDebugMatch {
id: item.value.clone(),
label: display_text(item).to_string(),
description: item.description.clone(),
kind: "history".to_string(),
})
.collect::<Vec<_>>();
let MenuDebug {
columns,
rows,
visible_rows,
indent,
selected_index,
selected_row,
selected_col,
description,
description_rendered,
styles,
rendered,
} = debug_snapshot(menu, editor, width, height, ansi);
let selected = if matches.is_empty() {
-1
} else {
selected_index
};
CompletionDebug {
line,
cursor,
replace_range,
stub,
matches,
selected,
selected_row,
selected_col,
columns,
rows,
visible_rows,
menu_indent: indent,
menu_styles: styles,
menu_description: description,
menu_description_rendered: description_rendered,
width,
height,
unicode,
color: ansi,
rendered,
}
}