use std::thread;
use anyhow::Result;
use crate::event::AppEvent;
use crate::grammar::recipe::GrammarRecipe;
use crate::prompt::{GrammarRow, GrammarState};
use super::{App, Toast, root_cause};
pub struct GrammarSubcommand {
pub name: &'static str,
pub aliases: &'static [&'static str],
pub description: &'static str,
}
pub const GRAMMAR_SUBCOMMANDS: &[GrammarSubcommand] = &[
GrammarSubcommand {
name: "list",
aliases: &["ls"],
description: "browse, install & remove (modal)",
},
GrammarSubcommand {
name: "install",
aliases: &["add"],
description: "install <name>... | --all",
},
GrammarSubcommand {
name: "remove",
aliases: &["rm", "uninstall"],
description: "remove <name>...",
},
];
impl App {
pub(super) fn run_grammar_command(&mut self, sub: &str) {
let sub = sub.trim();
let (cmd, rest) = match sub.split_once(char::is_whitespace) {
Some((c, r)) => (c, r.trim()),
None => (sub, ""),
};
let canonical = if cmd.is_empty() {
"list"
} else {
match GRAMMAR_SUBCOMMANDS
.iter()
.find(|s| s.name == cmd || s.aliases.contains(&cmd))
{
Some(s) => s.name,
None => {
self.push_toast(Toast::error(format!("unknown grammar subcommand: {cmd}")));
return;
}
}
};
match canonical {
"list" => self.open_grammar_list(),
"install" => self.grammar_install_cmd(rest),
"remove" => self.grammar_remove_cmd(rest),
_ => unreachable!("canonical comes from GRAMMAR_SUBCOMMANDS"),
}
}
pub(super) fn maybe_prompt_grammar_install(&mut self, spec: &crate::config::Language) -> bool {
let grammar = &spec.grammar;
if spec.grammar_dir.is_some() || spec.query_dir.is_some() {
return false;
}
if crate::grammar::build::is_fully_installed(
grammar,
&self.config.grammar_dir,
&self.config.query_dir,
) {
return false;
}
let has_recipe = self
.grammar_recipes
.iter()
.any(|r| r.name == grammar.as_str());
if !has_recipe {
return false;
}
if self.asked_grammars.contains(grammar) {
return true;
}
if self.prompt.is_open() {
return true;
}
self.asked_grammars.insert(grammar.clone());
self.prompt
.open_grammar_install_prompt(grammar.clone(), spec.name.clone());
true
}
fn open_grammar_list(&mut self) {
let grammar_dir = self.config.grammar_dir.as_path();
let mut rows: Vec<GrammarRow> = self
.grammar_recipes
.iter()
.map(|r| {
let installed =
crate::grammar::build::installed_path(r.name, grammar_dir).is_some();
GrammarRow {
name: r.name.to_string(),
state: if installed {
GrammarState::Installed
} else {
GrammarState::Missing
},
}
})
.collect();
rows.sort_by(|a, b| a.name.cmp(&b.name));
self.prompt.open_grammar_list(rows);
}
fn grammar_install_cmd(&mut self, rest: &str) {
let recipes = self.grammar_recipes.clone();
let names: Vec<String> = if rest == "--all" {
recipes.iter().map(|r| r.name.to_string()).collect()
} else {
rest.split_whitespace().map(str::to_string).collect()
};
if names.is_empty() {
self.push_toast(Toast::error("usage: :grammar install <name>... | --all"));
return;
}
let mut started = Vec::new();
for name in &names {
let Some(recipe) = recipes.iter().find(|r| r.name == name.as_str()) else {
self.push_toast(Toast::error(format!("unknown grammar: {name}")));
continue;
};
if crate::grammar::build::installed_path(recipe.name, &self.config.grammar_dir)
.is_some()
{
self.push_toast(Toast::info(format!("{name} already installed")));
continue;
}
self.spawn_grammar_install(recipe.clone());
started.push(name.clone());
}
if !started.is_empty() {
self.push_toast(Toast::info(format!("installing: {}", started.join(", "))));
}
}
fn grammar_remove_cmd(&mut self, rest: &str) {
let names: Vec<&str> = rest.split_whitespace().collect();
if names.is_empty() {
self.push_toast(Toast::error("usage: :grammar remove <name>..."));
return;
}
for name in names {
self.remove_grammar(name);
}
}
pub(super) fn remove_grammar(&mut self, name: &str) {
match crate::grammar::build::remove(name, &self.config.grammar_dir) {
Ok(true) => self.push_toast(Toast::info(format!("grammar removed: {name}"))),
Ok(false) => self.push_toast(Toast::info(format!("not installed: {name}"))),
Err(e) => self.push_toast(Toast::error(format!("remove {name}: {}", root_cause(&e)))),
}
}
pub(super) fn spawn_grammar_install_by_name(&mut self, name: &str) {
let recipe = self
.grammar_recipes
.iter()
.find(|r| r.name == name)
.cloned();
match recipe {
Some(recipe) => {
self.spawn_grammar_install(recipe);
self.push_toast(Toast::info(format!("installing {name}…")));
}
None => {
self.prompt.grammar_set_state(name, GrammarState::Missing);
self.push_toast(Toast::error(format!("unknown grammar: {name}")));
}
}
}
pub(super) fn spawn_grammar_install(&self, recipe: GrammarRecipe) {
let tx = self.event_tx.clone();
let grammar_dir = self.config.grammar_dir.clone();
let query_dir = self.config.query_dir.clone();
let name = recipe.name.to_string();
thread::spawn(move || {
let result =
crate::grammar::build::install(&recipe, &grammar_dir, &query_dir).map(|_| ());
let _ = tx.send(AppEvent::GrammarInstalled { name, result });
});
}
pub fn handle_grammar_installed(&mut self, name: String, result: Result<()>) {
match result {
Ok(()) => {
self.prompt
.grammar_set_state(&name, GrammarState::Installed);
self.push_toast(Toast::info(format!("grammar installed: {name}")));
self.rehighlight_for_grammar(&name);
}
Err(e) => {
self.prompt.grammar_set_state(&name, GrammarState::Missing);
self.push_toast(Toast::fatal(format!(
"grammar install failed ({name}): {}",
root_cause(&e)
)));
}
}
}
fn rehighlight_for_grammar(&mut self, grammar: &str) {
if let Some(path) = self.buffer.path.clone() {
let uses = self
.config
.languages
.by_path(&path)
.is_some_and(|spec| spec.grammar == grammar);
if uses {
self.spawn_engine_worker(&path);
}
}
let languages = &self.config.languages;
let loader = &self.loader;
for buf in self.parked_buffers.values_mut() {
let Some(path) = buf.path.clone() else {
continue;
};
let Some(spec) = languages.by_path(&path) else {
continue;
};
if spec.grammar != grammar {
continue;
}
if let Ok(mut engine) = loader.lock().unwrap().engine_for(spec) {
let source = buf.lines.join("\n");
engine.refresh(&source, buf.version);
buf.highlighter = Some(engine);
}
}
}
}