use crate::commands::CustomCommand;
use crate::config::PluginConfig;
use crate::context::{ContextGuard, PluginContext};
use crate::error::{PluginError, Result};
use crate::hooks::{HookEvent, HookResult};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::time::Instant;
use steel::steel_vm::engine::Engine;
use tracing::{debug, info, warn};
pub trait PluginSystem {
fn initialize(&mut self) -> Result<()>;
fn load_plugin(&mut self, path: &Path) -> Result<()>;
fn run_hook(&mut self, event: HookEvent, ctx: &PluginContext) -> Result<HookResult>;
fn run_command(&mut self, name: &str, args: &[String]) -> Result<i32>;
fn register_api(&mut self) -> Result<()>;
fn commands(&self) -> &HashMap<String, CustomCommand>;
}
pub struct SteelEngine {
engine: Engine,
loaded_plugins: Vec<PathBuf>,
commands: HashMap<String, CustomCommand>,
config: PluginConfig,
}
impl SteelEngine {
pub fn new(config: PluginConfig) -> Self {
SteelEngine {
engine: Engine::new(),
loaded_plugins: Vec::new(),
commands: HashMap::new(),
config,
}
}
pub fn with_defaults() -> Self {
Self::new(PluginConfig::new())
}
pub fn config(&self) -> &PluginConfig {
&self.config
}
pub fn is_loaded(&self, path: &Path) -> bool {
self.loaded_plugins.iter().any(|p| p == path)
}
pub fn loaded_plugins(&self) -> &[PathBuf] {
&self.loaded_plugins
}
pub fn eval(&mut self, code: String) -> Result<()> {
self.engine
.run(code)
.map_err(|e| PluginError::runtime("eval", e.to_string()))?;
Ok(())
}
fn load_file(&mut self, path: &Path) -> Result<()> {
let content = std::fs::read_to_string(path).map_err(|e| {
PluginError::io(format!("failed to read plugin file: {}", path.display()), e)
})?;
self.engine.run(content).map_err(|e| {
PluginError::load(path.to_path_buf(), format!("Steel evaluation error: {}", e))
})?;
Ok(())
}
fn has_function(&mut self, name: &str) -> bool {
let check_code = format!("(if (defined? '{}) #t #f)", name);
match self.engine.run(check_code) {
Ok(results) => {
if let Some(result) = results.into_iter().next() {
matches!(result, steel::SteelVal::BoolV(true))
} else {
false
}
}
Err(_) => false,
}
}
fn call_function(&mut self, name: &str) -> Result<()> {
let call_code = format!("({})", name);
self.engine
.run(call_code)
.map_err(|e| PluginError::runtime(name, e.to_string()))?;
Ok(())
}
}
impl PluginSystem for SteelEngine {
fn initialize(&mut self) -> Result<()> {
info!("Initializing Steel plugin engine");
self.register_api()?;
let prelude = include_str!("prelude.scm").to_string();
self.eval(prelude)?;
debug!("Steel engine initialized");
Ok(())
}
fn load_plugin(&mut self, path: &Path) -> Result<()> {
if !path.exists() {
return Err(PluginError::not_found(path.to_path_buf()));
}
if self.is_loaded(path) {
debug!("Plugin already loaded: {}", path.display());
return Ok(());
}
info!("Loading plugin: {}", path.display());
self.load_file(path)?;
self.loaded_plugins.push(path.to_path_buf());
Ok(())
}
fn run_hook(&mut self, event: HookEvent, ctx: &PluginContext) -> Result<HookResult> {
let scripts: Vec<String> = self.config.scripts_for_hook(event).to_vec();
if scripts.is_empty() {
return Ok(HookResult::skipped());
}
debug!("Running {} hook with {} scripts", event, scripts.len());
let _guard = ContextGuard::new(ctx.clone());
let start = Instant::now();
let project_root = ctx.project_root.clone();
for script in &scripts {
let script_path = self.find_script(script, &project_root)?;
if !self.is_loaded(&script_path) {
self.load_plugin(&script_path)?;
}
let hook_fn = event.scheme_function();
if self.has_function(hook_fn) {
match self.call_function(hook_fn) {
Ok(()) => {
debug!("Hook {} completed successfully", hook_fn);
}
Err(e) => {
let duration = start.elapsed();
warn!("Hook {} failed: {}", hook_fn, e);
if !self.config.continue_on_error {
return Ok(HookResult::failure(duration, e.to_string()));
}
}
}
}
}
let duration = start.elapsed();
Ok(HookResult::success(duration))
}
fn run_command(&mut self, name: &str, args: &[String]) -> Result<i32> {
if !self.commands.contains_key(name) {
return Err(PluginError::unknown_command(name));
}
let args_list = args
.iter()
.map(|a| format!("\"{}\"", a.replace('\\', "\\\\").replace('"', "\\\"")))
.collect::<Vec<_>>()
.join(" ");
let call_code = format!("(hx/run-command \"{}\" (list {}))", name, args_list);
match self.engine.run(call_code) {
Ok(results) => {
if let Some(steel::SteelVal::IntV(code)) = results.into_iter().next() {
return Ok(code as i32);
}
Ok(0)
}
Err(e) => Err(PluginError::runtime(name, e.to_string())),
}
}
fn register_api(&mut self) -> Result<()> {
crate::api::register_all(&mut self.engine)?;
Ok(())
}
fn commands(&self) -> &HashMap<String, CustomCommand> {
&self.commands
}
}
impl SteelEngine {
fn find_script(&self, name: &str, project_root: &Path) -> Result<PathBuf> {
let paths = self.config.all_paths(project_root);
for base_path in &paths {
let script_path = base_path.join(name);
if script_path.exists() {
return Ok(script_path);
}
}
Err(PluginError::not_found(PathBuf::from(name)))
}
pub fn register_command(&mut self, cmd: CustomCommand) {
info!("Registering custom command: {}", cmd.name);
self.commands.insert(cmd.name.clone(), cmd);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_engine_creation() {
let engine = SteelEngine::with_defaults();
assert!(engine.loaded_plugins().is_empty());
assert!(engine.commands().is_empty());
}
}