Skip to main content

hx_plugins/
loader.rs

1//! Plugin discovery and loading.
2
3use crate::config::PluginConfig;
4use crate::error::{PluginError, Result};
5use std::path::{Path, PathBuf};
6use tracing::{debug, info};
7
8/// Discovered plugin information.
9#[derive(Debug, Clone)]
10pub struct DiscoveredPlugin {
11    /// Path to the plugin file.
12    pub path: PathBuf,
13
14    /// Plugin name (filename without extension).
15    pub name: String,
16
17    /// Whether this is a project-local plugin.
18    pub is_local: bool,
19}
20
21impl DiscoveredPlugin {
22    /// Create a new discovered plugin.
23    pub fn new(path: PathBuf, is_local: bool) -> Self {
24        let name = path
25            .file_stem()
26            .and_then(|s| s.to_str())
27            .unwrap_or("unknown")
28            .to_string();
29
30        DiscoveredPlugin {
31            path,
32            name,
33            is_local,
34        }
35    }
36}
37
38/// Discover plugins in the configured paths.
39pub fn discover_plugins(
40    config: &PluginConfig,
41    project_root: &Path,
42) -> Result<Vec<DiscoveredPlugin>> {
43    let mut plugins = Vec::new();
44    let paths = config.all_paths(project_root);
45    let local_dir = PluginConfig::local_plugins_dir(project_root);
46
47    for base_path in paths.iter() {
48        if !base_path.exists() {
49            debug!("Plugin path does not exist: {}", base_path.display());
50            continue;
51        }
52
53        let is_local = *base_path == local_dir;
54
55        // Find all .scm files in this directory
56        let pattern = base_path.join("*.scm");
57        match glob::glob(&pattern.to_string_lossy()) {
58            Ok(entries) => {
59                for entry in entries.flatten() {
60                    debug!("Discovered plugin: {}", entry.display());
61                    plugins.push(DiscoveredPlugin::new(entry, is_local));
62                }
63            }
64            Err(e) => {
65                debug!("Failed to glob plugins in {}: {}", base_path.display(), e);
66            }
67        }
68    }
69
70    info!("Discovered {} plugins", plugins.len());
71    Ok(plugins)
72}
73
74/// Find a specific plugin by name.
75pub fn find_plugin(
76    name: &str,
77    config: &PluginConfig,
78    project_root: &Path,
79) -> Result<DiscoveredPlugin> {
80    let paths = config.all_paths(project_root);
81    let local_dir = PluginConfig::local_plugins_dir(project_root);
82
83    // Add .scm extension if not present
84    let filename = if name.ends_with(".scm") {
85        name.to_string()
86    } else {
87        format!("{}.scm", name)
88    };
89
90    for base_path in paths.iter() {
91        let plugin_path = base_path.join(&filename);
92        if plugin_path.exists() {
93            let is_local = *base_path == local_dir;
94            return Ok(DiscoveredPlugin::new(plugin_path, is_local));
95        }
96    }
97
98    Err(PluginError::not_found(PathBuf::from(name)))
99}
100
101/// Check if the plugins directory exists for a project.
102pub fn plugins_dir_exists(project_root: &Path) -> bool {
103    project_root.join(".hx").join("plugins").exists()
104}
105
106/// Create the plugins directory for a project.
107pub fn create_plugins_dir(project_root: &Path) -> Result<PathBuf> {
108    let plugins_dir = project_root.join(".hx").join("plugins");
109
110    if !plugins_dir.exists() {
111        std::fs::create_dir_all(&plugins_dir).map_err(|e| {
112            PluginError::io(
113                format!(
114                    "failed to create plugins directory: {}",
115                    plugins_dir.display()
116                ),
117                e,
118            )
119        })?;
120    }
121
122    Ok(plugins_dir)
123}
124
125/// Get information about plugin directories.
126pub struct PluginPaths {
127    /// Project-local plugins directory.
128    pub local: PathBuf,
129    /// Global plugins directory.
130    pub global: Option<PathBuf>,
131    /// Custom paths from configuration.
132    pub custom: Vec<PathBuf>,
133}
134
135impl PluginPaths {
136    /// Get plugin paths for a project.
137    pub fn for_project(config: &PluginConfig, project_root: &Path) -> Self {
138        let local = project_root.join(".hx").join("plugins");
139
140        let global =
141            directories::BaseDirs::new().map(|dirs| dirs.config_dir().join("hx").join("plugins"));
142
143        let custom = config
144            .paths
145            .iter()
146            .map(|p| {
147                let expanded = shellexpand(p);
148                PathBuf::from(expanded)
149            })
150            .collect();
151
152        PluginPaths {
153            local,
154            global,
155            custom,
156        }
157    }
158
159    /// Check which paths exist.
160    pub fn existing(&self) -> Vec<&PathBuf> {
161        let mut paths = Vec::new();
162
163        if self.local.exists() {
164            paths.push(&self.local);
165        }
166
167        for path in &self.custom {
168            if path.exists() {
169                paths.push(path);
170            }
171        }
172
173        if let Some(ref global) = self.global
174            && global.exists()
175        {
176            paths.push(global);
177        }
178
179        paths
180    }
181}
182
183fn shellexpand(path: &str) -> String {
184    if path.starts_with("~/")
185        && let Some(dirs) = directories::BaseDirs::new()
186    {
187        return format!("{}{}", dirs.home_dir().display(), &path[1..]);
188    }
189    path.to_string()
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use tempfile::tempdir;
196
197    #[test]
198    fn test_discover_plugins_empty() {
199        let temp = tempdir().unwrap();
200        let config = PluginConfig::default();
201        let plugins = discover_plugins(&config, temp.path()).unwrap();
202        assert!(plugins.is_empty());
203    }
204
205    #[test]
206    fn test_create_plugins_dir() {
207        let temp = tempdir().unwrap();
208        let plugins_dir = create_plugins_dir(temp.path()).unwrap();
209        assert!(plugins_dir.exists());
210        assert!(plugins_dir.ends_with(".hx/plugins"));
211    }
212}