Skip to main content

hx_plugins/
context.rs

1//! Plugin execution context.
2//!
3//! Uses a thread-local pattern similar to Helix editor's Steel integration
4//! for implicit context access during plugin execution.
5
6use std::cell::RefCell;
7use std::collections::HashMap;
8use std::path::PathBuf;
9use std::time::Duration;
10
11/// Build result information available to post-build hooks.
12#[derive(Debug, Clone, Default)]
13pub struct BuildContext {
14    /// Whether the build succeeded.
15    pub success: bool,
16    /// Build duration.
17    pub duration: Duration,
18    /// Warning messages from the build.
19    pub warnings: Vec<String>,
20    /// Error messages from the build.
21    pub errors: Vec<String>,
22}
23
24/// Test result information available to post-test hooks.
25#[derive(Debug, Clone, Default)]
26pub struct TestContext {
27    /// Whether all tests passed.
28    pub passed: bool,
29    /// Number of tests passed.
30    pub passed_count: usize,
31    /// Number of tests failed.
32    pub failed_count: usize,
33    /// Number of tests skipped.
34    pub skipped_count: usize,
35    /// Test duration.
36    pub duration: Duration,
37}
38
39/// Context available during plugin execution.
40#[derive(Debug, Clone)]
41pub struct PluginContext {
42    /// Project root directory.
43    pub project_root: PathBuf,
44
45    /// Project name from hx.toml or cabal file.
46    pub project_name: String,
47
48    /// GHC version (if detected).
49    pub ghc_version: Option<String>,
50
51    /// Cabal file path (if found).
52    pub cabal_file: Option<PathBuf>,
53
54    /// Build context (for post-build hooks).
55    pub build: Option<BuildContext>,
56
57    /// Test context (for post-test hooks).
58    pub test: Option<TestContext>,
59
60    /// Environment variables to set for child processes.
61    pub env_vars: HashMap<String, String>,
62
63    /// Whether verbose output is enabled.
64    pub verbose: bool,
65}
66
67impl PluginContext {
68    /// Create a new plugin context.
69    pub fn new(project_root: PathBuf, project_name: String) -> Self {
70        PluginContext {
71            project_root,
72            project_name,
73            ghc_version: None,
74            cabal_file: None,
75            build: None,
76            test: None,
77            env_vars: HashMap::new(),
78            verbose: false,
79        }
80    }
81
82    /// Set the GHC version.
83    pub fn with_ghc_version(mut self, version: impl Into<String>) -> Self {
84        self.ghc_version = Some(version.into());
85        self
86    }
87
88    /// Set the cabal file path.
89    pub fn with_cabal_file(mut self, path: impl Into<PathBuf>) -> Self {
90        self.cabal_file = Some(path.into());
91        self
92    }
93
94    /// Set the build context.
95    pub fn with_build_context(mut self, build: BuildContext) -> Self {
96        self.build = Some(build);
97        self
98    }
99
100    /// Set the test context.
101    pub fn with_test_context(mut self, test: TestContext) -> Self {
102        self.test = Some(test);
103        self
104    }
105
106    /// Set verbose mode.
107    pub fn with_verbose(mut self, verbose: bool) -> Self {
108        self.verbose = verbose;
109        self
110    }
111
112    /// Set an environment variable.
113    pub fn set_env(&mut self, key: impl Into<String>, value: impl Into<String>) {
114        self.env_vars.insert(key.into(), value.into());
115    }
116}
117
118// Thread-local storage for the current plugin context.
119// This allows API functions to access context without explicit parameters.
120thread_local! {
121    static CONTEXT: RefCell<Option<PluginContext>> = const { RefCell::new(None) };
122}
123
124/// Set the current plugin context for this thread.
125pub fn set_context(ctx: PluginContext) {
126    CONTEXT.with(|c| {
127        *c.borrow_mut() = Some(ctx);
128    });
129}
130
131/// Clear the current plugin context.
132pub fn clear_context() {
133    CONTEXT.with(|c| {
134        *c.borrow_mut() = None;
135    });
136}
137
138/// Access the current plugin context.
139///
140/// Returns None if no context is set (i.e., not running inside a plugin).
141pub fn with_context<F, R>(f: F) -> Option<R>
142where
143    F: FnOnce(&PluginContext) -> R,
144{
145    CONTEXT.with(|c| c.borrow().as_ref().map(f))
146}
147
148/// Access the current plugin context mutably.
149pub fn with_context_mut<F, R>(f: F) -> Option<R>
150where
151    F: FnOnce(&mut PluginContext) -> R,
152{
153    CONTEXT.with(|c| c.borrow_mut().as_mut().map(f))
154}
155
156/// Guard that sets context on creation and clears it on drop.
157pub struct ContextGuard;
158
159impl ContextGuard {
160    /// Create a new context guard, setting the context.
161    pub fn new(ctx: PluginContext) -> Self {
162        set_context(ctx);
163        ContextGuard
164    }
165}
166
167impl Drop for ContextGuard {
168    fn drop(&mut self) {
169        clear_context();
170    }
171}