1use crate::commands::CustomCommand;
4use crate::config::PluginConfig;
5use crate::context::{ContextGuard, PluginContext};
6use crate::error::{PluginError, Result};
7use crate::hooks::{HookEvent, HookResult};
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10use std::time::Instant;
11use steel::steel_vm::engine::Engine;
12use tracing::{debug, info, warn};
13
14pub trait PluginSystem {
19 fn initialize(&mut self) -> Result<()>;
21
22 fn load_plugin(&mut self, path: &Path) -> Result<()>;
24
25 fn run_hook(&mut self, event: HookEvent, ctx: &PluginContext) -> Result<HookResult>;
27
28 fn run_command(&mut self, name: &str, args: &[String]) -> Result<i32>;
30
31 fn register_api(&mut self) -> Result<()>;
33
34 fn commands(&self) -> &HashMap<String, CustomCommand>;
36}
37
38pub struct SteelEngine {
40 engine: Engine,
42
43 loaded_plugins: Vec<PathBuf>,
45
46 commands: HashMap<String, CustomCommand>,
48
49 config: PluginConfig,
51}
52
53impl SteelEngine {
54 pub fn new(config: PluginConfig) -> Self {
56 SteelEngine {
57 engine: Engine::new(),
58 loaded_plugins: Vec::new(),
59 commands: HashMap::new(),
60 config,
61 }
62 }
63
64 pub fn with_defaults() -> Self {
66 Self::new(PluginConfig::new())
67 }
68
69 pub fn config(&self) -> &PluginConfig {
71 &self.config
72 }
73
74 pub fn is_loaded(&self, path: &Path) -> bool {
76 self.loaded_plugins.iter().any(|p| p == path)
77 }
78
79 pub fn loaded_plugins(&self) -> &[PathBuf] {
81 &self.loaded_plugins
82 }
83
84 pub fn eval(&mut self, code: String) -> Result<()> {
86 self.engine
87 .run(code)
88 .map_err(|e| PluginError::runtime("eval", e.to_string()))?;
89 Ok(())
90 }
91
92 fn load_file(&mut self, path: &Path) -> Result<()> {
94 let content = std::fs::read_to_string(path).map_err(|e| {
95 PluginError::io(format!("failed to read plugin file: {}", path.display()), e)
96 })?;
97
98 self.engine.run(content).map_err(|e| {
99 PluginError::load(path.to_path_buf(), format!("Steel evaluation error: {}", e))
100 })?;
101
102 Ok(())
103 }
104
105 fn has_function(&mut self, name: &str) -> bool {
107 let check_code = format!("(if (defined? '{}) #t #f)", name);
109 match self.engine.run(check_code) {
110 Ok(results) => {
111 if let Some(result) = results.into_iter().next() {
113 matches!(result, steel::SteelVal::BoolV(true))
114 } else {
115 false
116 }
117 }
118 Err(_) => false,
119 }
120 }
121
122 fn call_function(&mut self, name: &str) -> Result<()> {
124 let call_code = format!("({})", name);
125 self.engine
126 .run(call_code)
127 .map_err(|e| PluginError::runtime(name, e.to_string()))?;
128 Ok(())
129 }
130}
131
132impl PluginSystem for SteelEngine {
133 fn initialize(&mut self) -> Result<()> {
134 info!("Initializing Steel plugin engine");
135
136 self.register_api()?;
138
139 let prelude = include_str!("prelude.scm").to_string();
141 self.eval(prelude)?;
142
143 debug!("Steel engine initialized");
144 Ok(())
145 }
146
147 fn load_plugin(&mut self, path: &Path) -> Result<()> {
148 if !path.exists() {
149 return Err(PluginError::not_found(path.to_path_buf()));
150 }
151
152 if self.is_loaded(path) {
153 debug!("Plugin already loaded: {}", path.display());
154 return Ok(());
155 }
156
157 info!("Loading plugin: {}", path.display());
158 self.load_file(path)?;
159 self.loaded_plugins.push(path.to_path_buf());
160
161 Ok(())
162 }
163
164 fn run_hook(&mut self, event: HookEvent, ctx: &PluginContext) -> Result<HookResult> {
165 let scripts: Vec<String> = self.config.scripts_for_hook(event).to_vec();
167
168 if scripts.is_empty() {
169 return Ok(HookResult::skipped());
170 }
171
172 debug!("Running {} hook with {} scripts", event, scripts.len());
173
174 let _guard = ContextGuard::new(ctx.clone());
176 let start = Instant::now();
177
178 let project_root = ctx.project_root.clone();
179
180 for script in &scripts {
181 let script_path = self.find_script(script, &project_root)?;
183
184 if !self.is_loaded(&script_path) {
186 self.load_plugin(&script_path)?;
187 }
188
189 let hook_fn = event.scheme_function();
191 if self.has_function(hook_fn) {
192 match self.call_function(hook_fn) {
193 Ok(()) => {
194 debug!("Hook {} completed successfully", hook_fn);
195 }
196 Err(e) => {
197 let duration = start.elapsed();
198 warn!("Hook {} failed: {}", hook_fn, e);
199
200 if !self.config.continue_on_error {
201 return Ok(HookResult::failure(duration, e.to_string()));
202 }
203 }
204 }
205 }
206 }
207
208 let duration = start.elapsed();
209 Ok(HookResult::success(duration))
210 }
211
212 fn run_command(&mut self, name: &str, args: &[String]) -> Result<i32> {
213 if !self.commands.contains_key(name) {
214 return Err(PluginError::unknown_command(name));
215 }
216
217 let args_list = args
219 .iter()
220 .map(|a| format!("\"{}\"", a.replace('\\', "\\\\").replace('"', "\\\"")))
221 .collect::<Vec<_>>()
222 .join(" ");
223
224 let call_code = format!("(hx/run-command \"{}\" (list {}))", name, args_list);
225
226 match self.engine.run(call_code) {
227 Ok(results) => {
228 if let Some(steel::SteelVal::IntV(code)) = results.into_iter().next() {
230 return Ok(code as i32);
231 }
232 Ok(0)
233 }
234 Err(e) => Err(PluginError::runtime(name, e.to_string())),
235 }
236 }
237
238 fn register_api(&mut self) -> Result<()> {
239 crate::api::register_all(&mut self.engine)?;
242 Ok(())
243 }
244
245 fn commands(&self) -> &HashMap<String, CustomCommand> {
246 &self.commands
247 }
248}
249
250impl SteelEngine {
251 fn find_script(&self, name: &str, project_root: &Path) -> Result<PathBuf> {
253 let paths = self.config.all_paths(project_root);
254
255 for base_path in &paths {
256 let script_path = base_path.join(name);
257 if script_path.exists() {
258 return Ok(script_path);
259 }
260 }
261
262 Err(PluginError::not_found(PathBuf::from(name)))
263 }
264
265 pub fn register_command(&mut self, cmd: CustomCommand) {
267 info!("Registering custom command: {}", cmd.name);
268 self.commands.insert(cmd.name.clone(), cmd);
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn test_engine_creation() {
278 let engine = SteelEngine::with_defaults();
279 assert!(engine.loaded_plugins().is_empty());
280 assert!(engine.commands().is_empty());
281 }
282}