Skip to main content

osp_cli/repl/
engine.rs

1//! The REPL engine exists to own the editor/runtime boundary of interactive
2//! `osp`.
3//!
4//! High-level flow:
5//!
6//! - configure the line editor or basic fallback based on terminal capability
7//! - render prompts and prompt-right state for the current REPL frame
8//! - adapt completion, history search, and highlighting into editor-facing
9//!   menus and callbacks
10//! - expose debug/trace surfaces so host commands can inspect the live editor
11//!   state without reimplementing it
12//!
13//! Higher-level orchestration lives in [`super::host`] and
14//! [`super::lifecycle`]. Actual command execution lives in [`super::dispatch`].
15//!
16//! Contract:
17//!
18//! - this module may depend on editor adapters, completion, history, and UI
19//!   presentation helpers
20//! - it should not become the owner of generic command execution, config
21//!   precedence, or product-level restart policy
22//!
23//! Public API shape:
24//!
25//! - debug snapshots stay direct semantic payloads
26//! - host-facing REPL prompts, appearance, and run configuration live in the
27//!   dedicated [`config`] surface instead of inside the editor mechanics
28//! - host-style REPL runtime configuration uses concrete builders and
29//!   constructors such as [`ReplRunConfig::builder`],
30//!   [`ReplAppearance::builder`], and [`CompletionDebugOptions::new`]
31
32pub use super::highlight::{HighlightDebugSpan, debug_highlight};
33pub(crate) use super::history_store::expand_history;
34pub use super::history_store::{
35    HistoryConfig, HistoryConfigBuilder, HistoryEntry, HistoryShellContext, SharedHistory,
36};
37use crate::completion::CompletionTree;
38use anyhow::Result;
39
40mod adapter;
41mod config;
42mod debug;
43mod editor;
44mod overlay;
45mod session;
46
47pub(crate) use adapter::{
48    CompletionTraceEvent, CompletionTraceMenuState, trace_completion, trace_completion_enabled,
49};
50#[cfg(test)]
51pub(crate) use adapter::{
52    ReplCompleter, ReplHistoryCompleter, build_repl_highlighter, expand_home, path_suggestions,
53    split_path_stub,
54};
55pub use adapter::{color_from_style_spec, default_pipe_verbs};
56pub use config::{
57    LineProjection, LineProjector, PromptRightRenderer, ReplAppearance, ReplAppearanceBuilder,
58    ReplInputMode, ReplLineResult, ReplPrompt, ReplReloadKind, ReplRunConfig, ReplRunConfigBuilder,
59    ReplRunResult,
60};
61pub use debug::{
62    CompletionDebug, CompletionDebugFrame, CompletionDebugMatch, CompletionDebugOptions, DebugStep,
63    debug_completion, debug_completion_steps, debug_history_menu, debug_history_menu_steps,
64};
65#[cfg(test)]
66use editor::{
67    AutoCompleteEmacs, contains_cursor_position_report, is_cursor_position_error,
68    parse_cursor_position_report,
69};
70pub(crate) use editor::{BasicInputReason, OspPrompt, basic_input_reason};
71#[cfg(test)]
72use overlay::{build_history_menu, build_history_picker_options, history_picker_items};
73use session::{InteractiveLoopConfig, SubmissionContext, run_repl_basic, run_repl_interactive};
74#[cfg(test)]
75use session::{SubmissionResult, process_submission};
76
77const COMPLETION_MENU_NAME: &str = "completion_menu";
78const HISTORY_MENU_NAME: &str = "history_menu";
79const HOST_COMMAND_HISTORY_PICKER: &str = "\u{0}osp-repl-history-picker";
80
81struct ReplRunContext {
82    prompt: OspPrompt,
83    completion_words: Vec<String>,
84    completion_tree: Option<CompletionTree>,
85    appearance: ReplAppearance,
86    line_projector: Option<LineProjector>,
87    history_store: SharedHistory,
88}
89
90/// Runs the interactive REPL and delegates submitted lines to `execute`.
91///
92/// # Fallback behavior
93///
94/// This prefers the interactive editor loop, but it falls back to basic
95/// line-by-line stdin mode when interactive assumptions do not hold. That
96/// includes non-terminal stdin and terminals that do not support the cursor
97/// position probe used by the editor layer.
98///
99/// When that fallback happens, the function emits a warning to stderr unless
100/// the caller explicitly requested basic input mode through the config.
101///
102/// # Errors
103///
104/// Returns an error when the interactive editor layer fails or when the
105/// supplied `execute` callback returns an error for a submitted line.
106pub fn run_repl<F>(config: ReplRunConfig, mut execute: F) -> Result<ReplRunResult>
107where
108    F: FnMut(&str, &SharedHistory) -> Result<ReplLineResult>,
109{
110    let ReplRunConfig {
111        prompt,
112        completion_words,
113        completion_tree,
114        appearance,
115        history_config,
116        input_mode,
117        prompt_right,
118        line_projector,
119    } = config;
120    let history_store = SharedHistory::new(history_config);
121    let mut submission = SubmissionContext {
122        history_store: &history_store,
123        execute: &mut execute,
124    };
125    let prompt = OspPrompt::new(prompt.left, prompt.indicator, prompt_right);
126    let basic_reason = basic_input_reason(input_mode);
127
128    run_repl_with_reason(
129        ReplRunContext {
130            prompt,
131            completion_words,
132            completion_tree,
133            appearance,
134            line_projector,
135            history_store: history_store.clone(),
136        },
137        basic_reason,
138        &mut submission,
139        run_repl_basic,
140        run_repl_interactive,
141    )
142}
143
144fn run_repl_with_reason<F, B, I>(
145    context: ReplRunContext,
146    basic_reason: Option<BasicInputReason>,
147    submission: &mut SubmissionContext<'_, F>,
148    mut run_basic_fn: B,
149    mut run_interactive_fn: I,
150) -> Result<ReplRunResult>
151where
152    F: FnMut(&str, &SharedHistory) -> Result<ReplLineResult>,
153    B: FnMut(&OspPrompt, &mut SubmissionContext<'_, F>) -> Result<ReplRunResult>,
154    I: FnMut(
155        InteractiveLoopConfig<'_>,
156        SharedHistory,
157        &mut SubmissionContext<'_, F>,
158    ) -> Result<ReplRunResult>,
159{
160    let ReplRunContext {
161        prompt,
162        completion_words,
163        completion_tree,
164        appearance,
165        line_projector,
166        history_store,
167    } = context;
168
169    if let Some(reason) = basic_reason {
170        match reason {
171            BasicInputReason::NotATerminal => {
172                eprintln!("Warning: Input is not a terminal (fd=0).");
173            }
174            BasicInputReason::CursorProbeUnsupported => {
175                eprintln!(
176                    "Warning: terminal does not support cursor position requests; using basic input mode."
177                );
178            }
179            BasicInputReason::Explicit => {}
180        }
181        return run_basic_fn(&prompt, submission);
182    }
183
184    run_interactive_fn(
185        InteractiveLoopConfig {
186            prompt: &prompt,
187            completion_words,
188            completion_tree,
189            appearance,
190            line_projector,
191        },
192        history_store,
193        submission,
194    )
195}
196
197#[cfg(test)]
198mod tests;