Files
owlen/crates/owlen-tui/src/highlight.rs
vikingowl 4935a64a13 refactor: complete Rust 2024 let-chain migration
Migrate all remaining collapsible_if patterns to Rust 2024 let-chain
syntax across the entire codebase. This modernizes conditional logic
by replacing nested if statements with single-level expressions using
the && operator with let patterns.

Changes:
- storage.rs: 2 let-chain conversions (database dir creation, legacy archiving)
- session.rs: 3 let-chain conversions (empty content check, ledger dir creation, consent flow)
- ollama.rs: 8 let-chain conversions (socket parsing, cloud validation, model caching, capabilities)
- main.rs: 2 let-chain conversions (API key validation, provider enablement)
- owlen-tui: ~50 let-chain conversions across app/mod.rs, chat_app.rs, ui.rs, highlight.rs, and state modules

Test fixes:
- prompt_server.rs: Add missing .await on async RemoteMcpClient::new_with_config
- presets.rs, prompt_server.rs: Add missing rpc_timeout_secs field to McpServerConfig
- file_write.rs: Update error assertion to accept new "escapes workspace boundary" message

Verification:
- cargo build --all:  succeeds
- cargo clippy --all -- -D clippy::collapsible_if:  zero warnings
- cargo test --all:  109+ tests pass

Net result: -46 lines of code, improved readability and maintainability.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-29 14:10:12 +01:00

163 lines
5.0 KiB
Rust

use once_cell::sync::Lazy;
use ratatui::style::{Color as TuiColor, Modifier, Style as TuiStyle};
use std::path::{Path, PathBuf};
use syntect::easy::HighlightLines;
use syntect::highlighting::{FontStyle, Style as SynStyle, Theme, ThemeSet};
use syntect::parsing::{SyntaxReference, SyntaxSet};
static SYNTAX_SET: Lazy<SyntaxSet> = Lazy::new(SyntaxSet::load_defaults_newlines);
static THEME_SET: Lazy<ThemeSet> = Lazy::new(ThemeSet::load_defaults);
static THEME: Lazy<Theme> = Lazy::new(|| {
THEME_SET
.themes
.get("base16-ocean.dark")
.cloned()
.or_else(|| THEME_SET.themes.values().next().cloned())
.unwrap_or_default()
});
fn select_syntax(path_hint: Option<&Path>) -> &'static SyntaxReference {
if let Some(path) = path_hint
&& let Ok(Some(syntax)) = SYNTAX_SET.find_syntax_for_file(path)
{
return syntax;
}
if let Some(path) = path_hint
&& let Some(ext) = path.extension().and_then(|ext| ext.to_str())
&& let Some(syntax) = SYNTAX_SET.find_syntax_by_extension(ext)
{
return syntax;
}
if let Some(path) = path_hint
&& let Some(name) = path.file_name().and_then(|name| name.to_str())
&& let Some(syntax) = SYNTAX_SET.find_syntax_by_token(name)
{
return syntax;
}
SYNTAX_SET.find_syntax_plain_text()
}
fn select_syntax_for_language(language: Option<&str>) -> &'static SyntaxReference {
let token = language
.map(|lang| lang.trim().to_ascii_lowercase())
.filter(|lang| !lang.is_empty());
if let Some(token) = token {
let mut attempts: Vec<&str> = vec![token.as_str()];
match token.as_str() {
"c++" => attempts.extend(["cpp", "c"]),
"c#" | "cs" => attempts.extend(["csharp", "cs"]),
"shell" => attempts.extend(["bash", "sh"]),
"typescript" | "ts" => attempts.extend(["typescript", "ts", "tsx"]),
"javascript" | "js" => attempts.extend(["javascript", "js", "jsx"]),
"py" => attempts.push("python"),
"rs" => attempts.push("rust"),
"yml" => attempts.push("yaml"),
other => {
if let Some(stripped) = other.strip_prefix('.') {
attempts.push(stripped);
}
}
}
for candidate in attempts {
if let Some(syntax) = SYNTAX_SET.find_syntax_by_token(candidate) {
return syntax;
}
if let Some(syntax) = SYNTAX_SET.find_syntax_by_extension(candidate) {
return syntax;
}
}
}
SYNTAX_SET.find_syntax_plain_text()
}
fn path_hint_from_components(absolute: Option<&Path>, display: Option<&str>) -> Option<PathBuf> {
if let Some(abs) = absolute {
return Some(abs.to_path_buf());
}
display.map(PathBuf::from)
}
fn style_from_syntect(style: SynStyle) -> TuiStyle {
let mut tui_style = TuiStyle::default().fg(TuiColor::Rgb(
style.foreground.r,
style.foreground.g,
style.foreground.b,
));
let mut modifiers = Modifier::empty();
if style.font_style.contains(FontStyle::BOLD) {
modifiers |= Modifier::BOLD;
}
if style.font_style.contains(FontStyle::ITALIC) {
modifiers |= Modifier::ITALIC;
}
if style.font_style.contains(FontStyle::UNDERLINE) {
modifiers |= Modifier::UNDERLINED;
}
if !modifiers.is_empty() {
tui_style = tui_style.add_modifier(modifiers);
}
tui_style
}
pub fn build_highlighter(
absolute: Option<&Path>,
display: Option<&str>,
) -> HighlightLines<'static> {
let hint_path = path_hint_from_components(absolute, display);
let syntax = select_syntax(hint_path.as_deref());
HighlightLines::new(syntax, &THEME)
}
pub fn highlight_line(
highlighter: &mut HighlightLines<'static>,
line: &str,
) -> Vec<(TuiStyle, String)> {
let mut segments = Vec::new();
match highlighter.highlight_line(line, &SYNTAX_SET) {
Ok(result) => {
for (style, piece) in result {
let tui_style = style_from_syntect(style);
segments.push((tui_style, piece.to_string()));
}
}
Err(_) => {
segments.push((TuiStyle::default(), line.to_string()));
}
}
if segments.is_empty() {
segments.push((TuiStyle::default(), String::new()));
}
segments
}
pub fn build_highlighter_for_language(language: Option<&str>) -> HighlightLines<'static> {
let syntax = select_syntax_for_language(language);
HighlightLines::new(syntax, &THEME)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rust_highlighting_produces_colored_segment() {
let mut highlighter = build_highlighter_for_language(Some("rust"));
let segments = highlight_line(&mut highlighter, "fn main() {}");
assert!(
segments
.iter()
.any(|(style, text)| style.fg.is_some() && !text.trim().is_empty()),
"Expected at least one colored segment"
);
}
}