When users type prompts in interactive mode, the input is now
reformatted in place with enhanced highlighting:
- ALL CAPS words (2+ chars) become bold green (e.g., FIX, BUG, HTTP2)
- Quoted text ("..." or ...) becomes cyan
- Standard markdown formatting is also supported
New module: input_formatter.rs with 10 unit tests
Integrated into interactive.rs for both single-line and multiline input
199 lines
6.8 KiB
Rust
199 lines
6.8 KiB
Rust
//! Input formatting for interactive mode.
|
|
//!
|
|
//! Formats user input with markdown-style highlighting:
|
|
//! - ALL CAPS words become bold
|
|
//! - Quoted text ("..." or '...') becomes cyan
|
|
//! - Standard markdown formatting (bold, italic, code) is applied
|
|
|
|
use regex::Regex;
|
|
use std::io::Write;
|
|
use termimad::MadSkin;
|
|
|
|
use crate::streaming_markdown::StreamingMarkdownFormatter;
|
|
|
|
/// Pre-process input text to add markdown markers for special formatting.
|
|
///
|
|
/// This pass runs BEFORE markdown formatting:
|
|
/// 1. ALL CAPS words (2+ chars) → wrapped in ** for bold
|
|
/// 2. Quoted text "..." or '...' → wrapped in special markers for cyan
|
|
///
|
|
/// Returns the preprocessed text ready for markdown formatting.
|
|
pub fn preprocess_input(input: &str) -> String {
|
|
let mut result = input.to_string();
|
|
|
|
// First, handle ALL CAPS words (2+ uppercase letters, may include numbers)
|
|
// Must be a standalone word (word boundaries)
|
|
let caps_re = Regex::new(r"\b([A-Z][A-Z0-9]{1,}[A-Z0-9]*)\b").unwrap();
|
|
result = caps_re.replace_all(&result, "**$1**").to_string();
|
|
|
|
// Then, handle quoted text - wrap in a special marker that we'll process after markdown
|
|
// Use lowercase placeholders that won't be matched by the ALL CAPS regex
|
|
let double_quote_re = Regex::new(r#""([^"]+)""#).unwrap();
|
|
result = double_quote_re.replace_all(&result, "\x00qdbl\x00$1\x00qend\x00").to_string();
|
|
|
|
let single_quote_re = Regex::new(r"'([^']+)'").unwrap();
|
|
result = single_quote_re.replace_all(&result, "\x00qsgl\x00$1\x00qend\x00").to_string();
|
|
|
|
result
|
|
}
|
|
|
|
/// Apply cyan highlighting to quoted text markers.
|
|
/// This runs AFTER markdown formatting to apply the cyan color.
|
|
fn apply_quote_highlighting(text: &str) -> String {
|
|
let mut result = text.to_string();
|
|
|
|
// Replace double-quote markers with cyan formatting
|
|
// \x1b[36m = cyan, \x1b[0m = reset
|
|
result = result.replace("\x00qdbl\x00", "\x1b[36m\"");
|
|
result = result.replace("\x00qsgl\x00", "\x1b[36m'");
|
|
result = result.replace("\x00qend\x00", "\x1b[0m");
|
|
|
|
// Add back the closing quotes
|
|
// We need to insert them before the reset code
|
|
let re = Regex::new(r#"(\x1b\[36m")([^\x1b]*)\x1b\[0m"#).unwrap();
|
|
result = re.replace_all(&result, |caps: ®ex::Captures| {
|
|
format!("{}{}\"\x1b[0m", &caps[1], &caps[2])
|
|
}).to_string();
|
|
|
|
let re = Regex::new(r"(\x1b\[36m')([^\x1b]*)\x1b\[0m").unwrap();
|
|
result = re.replace_all(&result, |caps: ®ex::Captures| {
|
|
format!("{}{}'\x1b[0m", &caps[1], &caps[2])
|
|
}).to_string();
|
|
|
|
result
|
|
}
|
|
|
|
/// Format user input with markdown and special highlighting.
|
|
///
|
|
/// Applies:
|
|
/// 1. ALL CAPS → bold (green)
|
|
/// 2. Quoted text → cyan
|
|
/// 3. Standard markdown (bold, italic, inline code)
|
|
pub fn format_input(input: &str) -> String {
|
|
// Pre-process to add markdown markers
|
|
let preprocessed = preprocess_input(input);
|
|
|
|
// Apply markdown formatting using the streaming formatter
|
|
let skin = MadSkin::default();
|
|
let mut formatter = StreamingMarkdownFormatter::new(skin);
|
|
let formatted = formatter.process(&preprocessed);
|
|
let formatted = formatted + &formatter.finish();
|
|
|
|
// Apply quote highlighting (after markdown so colors don't interfere)
|
|
apply_quote_highlighting(&formatted)
|
|
}
|
|
|
|
/// Reprint user input in place with formatting.
|
|
///
|
|
/// This moves the cursor up to overwrite the original input line,
|
|
/// then prints the formatted version.
|
|
pub fn reprint_formatted_input(input: &str, prompt: &str) {
|
|
// Format the input
|
|
let formatted = format_input(input);
|
|
|
|
// Count how many lines the input spans (for multiline input)
|
|
let line_count = input.lines().count().max(1);
|
|
|
|
// Move cursor up by the number of lines and clear
|
|
for _ in 0..line_count {
|
|
// Move up one line and clear it
|
|
print!("\x1b[1A\x1b[2K");
|
|
}
|
|
|
|
// Reprint with prompt and formatted input
|
|
// Use dim color for the prompt to distinguish from the formatted input
|
|
print!("\x1b[2m{}\x1b[0m{}", prompt, formatted);
|
|
|
|
// Ensure output is flushed
|
|
let _ = std::io::stdout().flush();
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_preprocess_all_caps() {
|
|
let input = "please FIX the BUG in this CODE";
|
|
let result = preprocess_input(input);
|
|
assert!(result.contains("**FIX**"));
|
|
assert!(result.contains("**BUG**"));
|
|
assert!(result.contains("**CODE**"));
|
|
// "please", "the", "in", "this" should not be wrapped
|
|
assert!(!result.contains("**please**"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_preprocess_single_caps_not_matched() {
|
|
// Single letter caps should not be matched
|
|
let input = "I am A person";
|
|
let result = preprocess_input(input);
|
|
// "I" and "A" are single letters, should not be wrapped
|
|
assert!(!result.contains("**I**"));
|
|
assert!(!result.contains("**A**"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_preprocess_double_quotes() {
|
|
let input = r#"say "hello world" please"#;
|
|
let result = preprocess_input(input);
|
|
assert!(result.contains("\x00qdbl\x00hello world\x00qend\x00"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_preprocess_single_quotes() {
|
|
let input = "use the 'special' method";
|
|
let result = preprocess_input(input);
|
|
assert!(result.contains("\x00qsgl\x00special\x00qend\x00"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_preprocess_mixed() {
|
|
let input = r#"FIX the "critical" BUG"#;
|
|
let result = preprocess_input(input);
|
|
assert!(result.contains("**FIX**"));
|
|
assert!(result.contains("**BUG**"));
|
|
assert!(result.contains("\x00qdbl\x00critical\x00qend\x00"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_apply_quote_highlighting() {
|
|
let input = "\x00qdbl\x00hello\x00qend\x00";
|
|
let result = apply_quote_highlighting(input);
|
|
assert!(result.contains("\x1b[36m"));
|
|
assert!(result.contains("\x1b[0m"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_input_caps_become_bold() {
|
|
let input = "FIX this";
|
|
let result = format_input(input);
|
|
// Should contain bold ANSI code (\x1b[1;32m for bold green)
|
|
assert!(result.contains("\x1b[1;32m") || result.contains("FIX"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_format_input_quotes_become_cyan() {
|
|
let input = r#"say "hello""#;
|
|
let result = format_input(input);
|
|
// Should contain cyan ANSI code
|
|
assert!(result.contains("\x1b[36m"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_caps_with_numbers() {
|
|
let input = "check HTTP2 and TLS13";
|
|
let result = preprocess_input(input);
|
|
assert!(result.contains("**HTTP2**"));
|
|
assert!(result.contains("**TLS13**"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_two_letter_caps() {
|
|
let input = "use IO and DB";
|
|
let result = preprocess_input(input);
|
|
assert!(result.contains("**IO**"));
|
|
assert!(result.contains("**DB**"));
|
|
}
|
|
}
|