fresh/services/plugins/plugin_dev_workspace.rs
1//! Plugin development workspace for LSP support
2//!
3//! When a user loads a plugin from a buffer, this module creates a temporary
4//! workspace directory containing:
5//! - The buffer content as a `.ts` file
6//! - A copy of `fresh.d.ts` for type definitions
7//! - A `tsconfig.json` configured for the plugin environment
8//!
9//! This allows `typescript-language-server` to provide autocomplete, type checking,
10//! and hover documentation for plugin buffers — including unsaved/unnamed buffers.
11
12use std::path::{Path, PathBuf};
13
14/// Manages a temporary workspace for plugin development LSP support.
15pub struct PluginDevWorkspace {
16 /// Root directory of the workspace (e.g., `~/.cache/fresh/plugin-dev/{buffer_id}/`)
17 dir: PathBuf,
18 /// Path to the plugin source file within the workspace
19 pub plugin_file: PathBuf,
20}
21
22/// The tsconfig.json content for plugin development.
23/// - No DOM lib (plugins run in QuickJS, not a browser)
24/// - `types: []` prevents picking up @types/node or other ambient types
25/// - `skipLibCheck: true` avoids checking fresh.d.ts itself
26const TSCONFIG_CONTENT: &str = r#"{
27 "compilerOptions": {
28 "target": "ES2020",
29 "module": "ES2020",
30 "moduleResolution": "node",
31 "strict": true,
32 "noEmit": true,
33 "skipLibCheck": true,
34 "lib": ["ES2020"],
35 "types": []
36 },
37 "files": ["fresh.d.ts", "plugin.ts"]
38}
39"#;
40
41impl PluginDevWorkspace {
42 /// Create a new plugin dev workspace for the given buffer.
43 ///
44 /// This creates a temp directory, copies `fresh.d.ts` from the source,
45 /// writes `tsconfig.json`, and writes the buffer content to `plugin.ts`.
46 ///
47 /// # Arguments
48 /// * `buffer_id` - The buffer ID (used to create a unique directory)
49 /// * `content` - The current buffer content
50 /// * `fresh_dts_source` - Path to the `fresh.d.ts` file to copy
51 pub fn create(
52 buffer_id: usize,
53 content: &str,
54 fresh_dts_source: &Path,
55 ) -> Result<Self, String> {
56 let cache_dir =
57 dirs::cache_dir().ok_or_else(|| "Could not determine cache directory".to_string())?;
58 let dir = cache_dir
59 .join("fresh")
60 .join("plugin-dev")
61 .join(buffer_id.to_string());
62
63 // Create directory
64 std::fs::create_dir_all(&dir)
65 .map_err(|e| format!("Failed to create plugin dev workspace: {}", e))?;
66
67 // Copy fresh.d.ts
68 let dest_dts = dir.join("fresh.d.ts");
69 std::fs::copy(fresh_dts_source, &dest_dts)
70 .map_err(|e| format!("Failed to copy fresh.d.ts: {}", e))?;
71
72 // Write tsconfig.json
73 let tsconfig_path = dir.join("tsconfig.json");
74 std::fs::write(&tsconfig_path, TSCONFIG_CONTENT)
75 .map_err(|e| format!("Failed to write tsconfig.json: {}", e))?;
76
77 // Write buffer content as plugin.ts
78 let plugin_file = dir.join("plugin.ts");
79 std::fs::write(&plugin_file, content)
80 .map_err(|e| format!("Failed to write plugin.ts: {}", e))?;
81
82 tracing::info!(
83 "Created plugin dev workspace at {:?} for buffer {}",
84 dir,
85 buffer_id
86 );
87
88 Ok(Self { dir, plugin_file })
89 }
90
91 /// Get the path to the workspace directory.
92 pub fn dir(&self) -> &Path {
93 &self.dir
94 }
95
96 /// Clean up the workspace directory.
97 pub fn cleanup(&self) {
98 if self.dir.exists() {
99 if let Err(e) = std::fs::remove_dir_all(&self.dir) {
100 tracing::warn!(
101 "Failed to clean up plugin dev workspace {:?}: {}",
102 self.dir,
103 e
104 );
105 } else {
106 tracing::debug!("Cleaned up plugin dev workspace {:?}", self.dir);
107 }
108 }
109 }
110}
111
112impl Drop for PluginDevWorkspace {
113 fn drop(&mut self) {
114 self.cleanup();
115 }
116}