use std::collections::HashSet;
use std::path::Path;
use rho_providers::model::{ContentBlock, Message};
use serde_json::Value;
use super::markdown::opening_fence_info_token;
use super::syntax::{
syntax_name_for_language, syntax_name_for_path, warm_syntax_set, BlockHighlighter,
};
const MAX_WARMED_SYNTAXES: usize = 12;
#[derive(Debug, Default, PartialEq, Eq)]
struct SyntaxWarmupPlan {
tokens: Vec<String>,
paths: Vec<String>,
}
impl SyntaxWarmupPlan {
fn from_messages(messages: &[Message]) -> Self {
let mut plan = Self::default();
let mut seen_tokens = HashSet::new();
let mut seen_paths = HashSet::new();
for message in messages {
match message {
Message::User(blocks) | Message::Assistant(blocks) => {
plan.collect_blocks(blocks, &mut seen_tokens, &mut seen_paths);
}
Message::EnrichedAssistant(message) => {
plan.collect_blocks(&message.content, &mut seen_tokens, &mut seen_paths);
}
Message::AbortedAssistant(message) => {
plan.collect_blocks(&message.content, &mut seen_tokens, &mut seen_paths);
for call in &message.tool_calls {
if let Ok(arguments) = serde_json::from_str::<Value>(&call.arguments) {
plan.push_tool_path(&arguments, &mut seen_paths);
}
}
}
Message::ToolResult(_) | Message::System(_) => {}
}
}
plan
}
fn collect_blocks(
&mut self,
blocks: &[ContentBlock],
seen_tokens: &mut HashSet<String>,
seen_paths: &mut HashSet<String>,
) {
for block in blocks {
match block {
ContentBlock::Text(text) => self.collect_tokens(text, seen_tokens),
ContentBlock::ToolCall(call) => self.push_tool_path(&call.arguments, seen_paths),
ContentBlock::Image(_) => {}
}
}
}
fn collect_tokens(&mut self, text: &str, seen: &mut HashSet<String>) {
for line in text.lines() {
let Some(token) = opening_fence_info_token(line) else {
continue;
};
if !should_warmup_token(&token) || !seen.insert(token.clone()) {
continue;
}
self.tokens.push(token);
}
}
fn push_tool_path(&mut self, arguments: &Value, seen: &mut HashSet<String>) {
let Some(path) = tool_argument_path(arguments) else {
return;
};
if !should_warmup_path(path) || !seen.insert(path.to_string()) {
return;
}
self.paths.push(path.to_string());
}
}
pub(crate) fn spawn_syntax_warmup(messages: &[Message]) -> tokio::task::JoinHandle<()> {
let plan = SyntaxWarmupPlan::from_messages(messages);
tokio::task::spawn_blocking(move || {
let _span = tracing::info_span!("startup.syntax_set").entered();
warm_plan(plan);
})
}
fn warm_plan(plan: SyntaxWarmupPlan) {
warm_syntax_set();
for (_, highlighter) in planned_warmups(&plan) {
warm_highlighter(highlighter);
}
}
fn planned_warmups(plan: &SyntaxWarmupPlan) -> Vec<(&'static str, BlockHighlighter)> {
let mut budget = WarmBudget::default();
let mut planned = Vec::new();
for token in &plan.tokens {
let Some(name) = budget.claim(syntax_name_for_language(token)) else {
continue;
};
if let Some(highlighter) = BlockHighlighter::for_language(token) {
planned.push((name, highlighter));
}
}
for path in &plan.paths {
let Some(name) = budget.claim(syntax_name_for_path(path)) else {
continue;
};
if let Some(highlighter) = BlockHighlighter::for_path(path) {
planned.push((name, highlighter));
}
}
planned
}
#[derive(Default)]
struct WarmBudget {
seen: HashSet<&'static str>,
}
impl WarmBudget {
fn claim(&mut self, name: Option<&'static str>) -> Option<&'static str> {
let name = name?;
if self.seen.len() >= MAX_WARMED_SYNTAXES || !self.seen.insert(name) {
return None;
}
Some(name)
}
}
fn warm_highlighter(mut highlighter: BlockHighlighter) {
let _ = highlighter.highlight_line("x = 1");
}
fn tool_argument_path(arguments: &Value) -> Option<&str> {
arguments
.get("path")
.or_else(|| arguments.get("file_path"))
.and_then(Value::as_str)
.filter(|path| !path.is_empty())
}
fn should_warmup_token(token: &str) -> bool {
!matches!(
token,
"md" | "markdown" | "mermaid" | "text" | "plaintext" | "plain"
)
}
fn should_warmup_path(path: &str) -> bool {
Path::new(path)
.extension()
.and_then(|ext| ext.to_str())
.is_none_or(|ext| should_warmup_token(&ext.to_ascii_lowercase()))
}
#[cfg(test)]
#[path = "syntax_warmup_tests.rs"]
mod tests;