1use oxc_allocator::Allocator;
12use oxc_codegen::Codegen;
13use oxc_parser::Parser;
14use oxc_span::SourceType;
15use ts_rs::TS;
16
17use fresh_core::api::{
18 ActionPopupAction, ActionPopupOptions, ActionSpec, BackgroundProcessResult, BufferInfo,
19 BufferSavedDiff, CompositeHunk, CompositeLayoutConfig, CompositePaneStyle,
20 CompositeSourceConfig, CreateCompositeBufferOptions, CreateVirtualBufferInExistingSplitOptions,
21 CreateVirtualBufferInSplitOptions, CreateVirtualBufferOptions, CursorInfo, DirEntry,
22 JsDiagnostic, JsPosition, JsRange, JsTextPropertyEntry, LayoutHints, SpawnResult,
23 TextPropertiesAtCursor, TsHighlightSpan, ViewTokenStyle, ViewTokenWire, ViewTokenWireKind,
24 ViewportInfo, VirtualBufferResult,
25};
26use fresh_core::command::Suggestion;
27use fresh_core::file_explorer::FileExplorerDecoration;
28
29fn get_type_decl(type_name: &str) -> Option<String> {
34 match type_name {
37 "BufferInfo" => Some(BufferInfo::decl()),
39 "CursorInfo" => Some(CursorInfo::decl()),
40 "ViewportInfo" => Some(ViewportInfo::decl()),
41 "ActionSpec" => Some(ActionSpec::decl()),
42 "BufferSavedDiff" => Some(BufferSavedDiff::decl()),
43 "LayoutHints" => Some(LayoutHints::decl()),
44
45 "SpawnResult" => Some(SpawnResult::decl()),
47 "BackgroundProcessResult" => Some(BackgroundProcessResult::decl()),
48
49 "TsCompositeLayoutConfig" | "CompositeLayoutConfig" => Some(CompositeLayoutConfig::decl()),
51 "TsCompositeSourceConfig" | "CompositeSourceConfig" => Some(CompositeSourceConfig::decl()),
52 "TsCompositePaneStyle" | "CompositePaneStyle" => Some(CompositePaneStyle::decl()),
53 "TsCompositeHunk" | "CompositeHunk" => Some(CompositeHunk::decl()),
54 "TsCreateCompositeBufferOptions" | "CreateCompositeBufferOptions" => {
55 Some(CreateCompositeBufferOptions::decl())
56 }
57
58 "ViewTokenWireKind" => Some(ViewTokenWireKind::decl()),
60 "ViewTokenStyle" => Some(ViewTokenStyle::decl()),
61 "ViewTokenWire" => Some(ViewTokenWire::decl()),
62
63 "TsActionPopupAction" | "ActionPopupAction" => Some(ActionPopupAction::decl()),
65 "ActionPopupOptions" => Some(ActionPopupOptions::decl()),
66 "TsHighlightSpan" => Some(TsHighlightSpan::decl()),
67 "FileExplorerDecoration" => Some(FileExplorerDecoration::decl()),
68
69 "TextPropertyEntry" | "JsTextPropertyEntry" => Some(JsTextPropertyEntry::decl()),
71 "CreateVirtualBufferOptions" => Some(CreateVirtualBufferOptions::decl()),
72 "CreateVirtualBufferInSplitOptions" => Some(CreateVirtualBufferInSplitOptions::decl()),
73 "CreateVirtualBufferInExistingSplitOptions" => {
74 Some(CreateVirtualBufferInExistingSplitOptions::decl())
75 }
76
77 "TextPropertiesAtCursor" => Some(TextPropertiesAtCursor::decl()),
79 "VirtualBufferResult" => Some(VirtualBufferResult::decl()),
80
81 "PromptSuggestion" | "Suggestion" => Some(Suggestion::decl()),
83 "DirEntry" => Some(DirEntry::decl()),
84
85 "JsDiagnostic" => Some(JsDiagnostic::decl()),
87 "JsRange" => Some(JsRange::decl()),
88 "JsPosition" => Some(JsPosition::decl()),
89
90 _ => None,
91 }
92}
93
94const DEPENDENCY_TYPES: &[&str] = &[
98 "TextPropertyEntry", "TsCompositeLayoutConfig", "TsCompositeSourceConfig", "TsCompositePaneStyle", "TsCompositeHunk", "TsCreateCompositeBufferOptions", "ViewportInfo", "LayoutHints", "ViewTokenWire", "ViewTokenWireKind", "ViewTokenStyle", "PromptSuggestion", "DirEntry", "BufferInfo", "JsDiagnostic", "JsRange", "JsPosition", "ActionSpec", "TsActionPopupAction", "ActionPopupOptions", "FileExplorerDecoration", ];
120
121pub fn collect_ts_types() -> String {
126 use crate::backend::quickjs_backend::JSEDITORAPI_REFERENCED_TYPES;
127
128 let mut types = Vec::new();
129 let mut included = std::collections::HashSet::new();
130
131 for type_name in DEPENDENCY_TYPES {
133 if let Some(decl) = get_type_decl(type_name) {
134 types.push(decl);
135 included.insert(*type_name);
136 }
137 }
138
139 for type_name in JSEDITORAPI_REFERENCED_TYPES {
141 if included.contains(*type_name) {
142 continue;
143 }
144 if let Some(decl) = get_type_decl(type_name) {
145 types.push(decl);
146 included.insert(*type_name);
147 } else {
148 eprintln!(
150 "Warning: Type '{}' is referenced in API but not registered in get_type_decl()",
151 type_name
152 );
153 }
154 }
155
156 types.join("\n\n")
157}
158
159pub fn validate_typescript(source: &str) -> Result<(), String> {
163 let allocator = Allocator::default();
164 let source_type = SourceType::d_ts();
165
166 let parser_ret = Parser::new(&allocator, source, source_type).parse();
167
168 if parser_ret.errors.is_empty() {
169 Ok(())
170 } else {
171 let errors: Vec<String> = parser_ret
172 .errors
173 .iter()
174 .map(|e: &oxc_diagnostics::OxcDiagnostic| e.to_string())
175 .collect();
176 Err(format!("TypeScript parse errors:\n{}", errors.join("\n")))
177 }
178}
179
180pub fn format_typescript(source: &str) -> String {
185 let allocator = Allocator::default();
186 let source_type = SourceType::d_ts();
187
188 let parser_ret = Parser::new(&allocator, source, source_type).parse();
189
190 if !parser_ret.errors.is_empty() {
191 return source.to_string();
193 }
194
195 Codegen::new().build(&parser_ret.program).code
197}
198
199pub fn write_fresh_dts() -> Result<(), String> {
204 use crate::backend::quickjs_backend::{JSEDITORAPI_TS_EDITOR_API, JSEDITORAPI_TS_PREAMBLE};
205
206 let ts_types = collect_ts_types();
207
208 let content = format!(
209 "{}\n{}\n{}",
210 JSEDITORAPI_TS_PREAMBLE, ts_types, JSEDITORAPI_TS_EDITOR_API
211 );
212
213 validate_typescript(&content)?;
215
216 let formatted = format_typescript(&content);
218
219 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
221 let output_path = std::path::Path::new(&manifest_dir)
222 .parent() .and_then(|p| p.parent()) .map(|p| p.join("crates/fresh-editor/plugins/lib/fresh.d.ts"))
225 .unwrap_or_else(|| std::path::PathBuf::from("plugins/lib/fresh.d.ts"));
226
227 let should_write = match std::fs::read_to_string(&output_path) {
229 Ok(existing) => existing != formatted,
230 Err(_) => true,
231 };
232
233 if should_write {
234 if let Some(parent) = output_path.parent() {
235 std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
236 }
237 std::fs::write(&output_path, &formatted).map_err(|e| e.to_string())?;
238 }
239
240 Ok(())
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
250 #[ignore]
251 fn write_fresh_dts_file() {
252 write_fresh_dts().expect("Failed to write fresh.d.ts");
254 println!("Successfully generated, validated, and formatted fresh.d.ts");
255 }
256
257 #[test]
261 #[ignore]
262 fn type_check_plugins() {
263 let tsc_check = std::process::Command::new("tsc").arg("--version").output();
265
266 match tsc_check {
267 Ok(output) if output.status.success() => {
268 println!(
269 "Found tsc: {}",
270 String::from_utf8_lossy(&output.stdout).trim()
271 );
272 }
273 _ => {
274 println!("tsc not found in PATH, skipping type check test");
275 return;
276 }
277 }
278
279 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
281 let script_path = std::path::Path::new(&manifest_dir)
282 .parent()
283 .and_then(|p| p.parent())
284 .map(|p| p.join("crates/fresh-editor/plugins/check-types.sh"))
285 .expect("Failed to find check-types.sh");
286
287 println!("Running type check script: {}", script_path.display());
288
289 let output = std::process::Command::new("bash")
291 .arg(&script_path)
292 .output()
293 .expect("Failed to run check-types.sh");
294
295 let stdout = String::from_utf8_lossy(&output.stdout);
296 let stderr = String::from_utf8_lossy(&output.stderr);
297
298 println!("stdout:\n{}", stdout);
299 if !stderr.is_empty() {
300 println!("stderr:\n{}", stderr);
301 }
302
303 if stdout.contains("had type errors") || !output.status.success() {
305 panic!(
306 "TypeScript type check failed. Run 'crates/fresh-editor/plugins/check-types.sh' to see details."
307 );
308 }
309
310 println!("All plugins type check successfully!");
311 }
312}