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 FormatterPackConfig, JsDiagnostic, JsPosition, JsRange, JsTextPropertyEntry,
23 LanguagePackConfig, LayoutHints, LspServerPackConfig, SpawnResult, TextPropertiesAtCursor,
24 TsHighlightSpan, ViewTokenStyle, ViewTokenWire, ViewTokenWireKind, ViewportInfo,
25 VirtualBufferResult,
26};
27use fresh_core::command::Suggestion;
28use fresh_core::file_explorer::FileExplorerDecoration;
29
30fn get_type_decl(type_name: &str) -> Option<String> {
35 match type_name {
38 "BufferInfo" => Some(BufferInfo::decl()),
40 "CursorInfo" => Some(CursorInfo::decl()),
41 "ViewportInfo" => Some(ViewportInfo::decl()),
42 "ActionSpec" => Some(ActionSpec::decl()),
43 "BufferSavedDiff" => Some(BufferSavedDiff::decl()),
44 "LayoutHints" => Some(LayoutHints::decl()),
45
46 "SpawnResult" => Some(SpawnResult::decl()),
48 "BackgroundProcessResult" => Some(BackgroundProcessResult::decl()),
49
50 "TsCompositeLayoutConfig" | "CompositeLayoutConfig" => Some(CompositeLayoutConfig::decl()),
52 "TsCompositeSourceConfig" | "CompositeSourceConfig" => Some(CompositeSourceConfig::decl()),
53 "TsCompositePaneStyle" | "CompositePaneStyle" => Some(CompositePaneStyle::decl()),
54 "TsCompositeHunk" | "CompositeHunk" => Some(CompositeHunk::decl()),
55 "TsCreateCompositeBufferOptions" | "CreateCompositeBufferOptions" => {
56 Some(CreateCompositeBufferOptions::decl())
57 }
58
59 "ViewTokenWireKind" => Some(ViewTokenWireKind::decl()),
61 "ViewTokenStyle" => Some(ViewTokenStyle::decl()),
62 "ViewTokenWire" => Some(ViewTokenWire::decl()),
63
64 "TsActionPopupAction" | "ActionPopupAction" => Some(ActionPopupAction::decl()),
66 "ActionPopupOptions" => Some(ActionPopupOptions::decl()),
67 "TsHighlightSpan" => Some(TsHighlightSpan::decl()),
68 "FileExplorerDecoration" => Some(FileExplorerDecoration::decl()),
69
70 "TextPropertyEntry" | "JsTextPropertyEntry" => Some(JsTextPropertyEntry::decl()),
72 "CreateVirtualBufferOptions" => Some(CreateVirtualBufferOptions::decl()),
73 "CreateVirtualBufferInSplitOptions" => Some(CreateVirtualBufferInSplitOptions::decl()),
74 "CreateVirtualBufferInExistingSplitOptions" => {
75 Some(CreateVirtualBufferInExistingSplitOptions::decl())
76 }
77
78 "TextPropertiesAtCursor" => Some(TextPropertiesAtCursor::decl()),
80 "VirtualBufferResult" => Some(VirtualBufferResult::decl()),
81
82 "PromptSuggestion" | "Suggestion" => Some(Suggestion::decl()),
84 "DirEntry" => Some(DirEntry::decl()),
85
86 "JsDiagnostic" => Some(JsDiagnostic::decl()),
88 "JsRange" => Some(JsRange::decl()),
89 "JsPosition" => Some(JsPosition::decl()),
90
91 "LanguagePackConfig" => Some(LanguagePackConfig::decl()),
93 "LspServerPackConfig" => Some(LspServerPackConfig::decl()),
94 "FormatterPackConfig" => Some(FormatterPackConfig::decl()),
95
96 _ => None,
97 }
98}
99
100const DEPENDENCY_TYPES: &[&str] = &[
104 "TextPropertyEntry", "TsCompositeLayoutConfig", "TsCompositeSourceConfig", "TsCompositePaneStyle", "TsCompositeHunk", "TsCreateCompositeBufferOptions", "ViewportInfo", "LayoutHints", "ViewTokenWire", "ViewTokenWireKind", "ViewTokenStyle", "PromptSuggestion", "DirEntry", "BufferInfo", "JsDiagnostic", "JsRange", "JsPosition", "ActionSpec", "TsActionPopupAction", "ActionPopupOptions", "FileExplorerDecoration", "FormatterPackConfig", ];
127
128pub fn collect_ts_types() -> String {
133 use crate::backend::quickjs_backend::JSEDITORAPI_REFERENCED_TYPES;
134
135 let mut types = Vec::new();
136 let mut included = std::collections::HashSet::new();
137
138 for type_name in DEPENDENCY_TYPES {
140 if let Some(decl) = get_type_decl(type_name) {
141 types.push(decl);
142 included.insert(*type_name);
143 }
144 }
145
146 for type_name in JSEDITORAPI_REFERENCED_TYPES {
148 if included.contains(*type_name) {
149 continue;
150 }
151 if let Some(decl) = get_type_decl(type_name) {
152 types.push(decl);
153 included.insert(*type_name);
154 } else {
155 eprintln!(
157 "Warning: Type '{}' is referenced in API but not registered in get_type_decl()",
158 type_name
159 );
160 }
161 }
162
163 types.join("\n\n")
164}
165
166pub fn validate_typescript(source: &str) -> Result<(), String> {
170 let allocator = Allocator::default();
171 let source_type = SourceType::d_ts();
172
173 let parser_ret = Parser::new(&allocator, source, source_type).parse();
174
175 if parser_ret.errors.is_empty() {
176 Ok(())
177 } else {
178 let errors: Vec<String> = parser_ret
179 .errors
180 .iter()
181 .map(|e: &oxc_diagnostics::OxcDiagnostic| e.to_string())
182 .collect();
183 Err(format!("TypeScript parse errors:\n{}", errors.join("\n")))
184 }
185}
186
187pub fn format_typescript(source: &str) -> String {
192 let allocator = Allocator::default();
193 let source_type = SourceType::d_ts();
194
195 let parser_ret = Parser::new(&allocator, source, source_type).parse();
196
197 if !parser_ret.errors.is_empty() {
198 return source.to_string();
200 }
201
202 Codegen::new().build(&parser_ret.program).code
204}
205
206pub fn write_fresh_dts() -> Result<(), String> {
211 use crate::backend::quickjs_backend::{JSEDITORAPI_TS_EDITOR_API, JSEDITORAPI_TS_PREAMBLE};
212
213 let ts_types = collect_ts_types();
214
215 let content = format!(
216 "{}\n{}\n{}",
217 JSEDITORAPI_TS_PREAMBLE, ts_types, JSEDITORAPI_TS_EDITOR_API
218 );
219
220 validate_typescript(&content)?;
222
223 let formatted = format_typescript(&content);
225
226 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
228 let output_path = std::path::Path::new(&manifest_dir)
229 .parent() .and_then(|p| p.parent()) .map(|p| p.join("crates/fresh-editor/plugins/lib/fresh.d.ts"))
232 .unwrap_or_else(|| std::path::PathBuf::from("plugins/lib/fresh.d.ts"));
233
234 let should_write = match std::fs::read_to_string(&output_path) {
236 Ok(existing) => existing != formatted,
237 Err(_) => true,
238 };
239
240 if should_write {
241 if let Some(parent) = output_path.parent() {
242 std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
243 }
244 std::fs::write(&output_path, &formatted).map_err(|e| e.to_string())?;
245 }
246
247 Ok(())
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
257 #[ignore]
258 fn write_fresh_dts_file() {
259 write_fresh_dts().expect("Failed to write fresh.d.ts");
261 println!("Successfully generated, validated, and formatted fresh.d.ts");
262 }
263
264 #[test]
268 #[ignore]
269 fn type_check_plugins() {
270 let tsc_check = std::process::Command::new("tsc").arg("--version").output();
272
273 match tsc_check {
274 Ok(output) if output.status.success() => {
275 println!(
276 "Found tsc: {}",
277 String::from_utf8_lossy(&output.stdout).trim()
278 );
279 }
280 _ => {
281 println!("tsc not found in PATH, skipping type check test");
282 return;
283 }
284 }
285
286 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
288 let script_path = std::path::Path::new(&manifest_dir)
289 .parent()
290 .and_then(|p| p.parent())
291 .map(|p| p.join("crates/fresh-editor/plugins/check-types.sh"))
292 .expect("Failed to find check-types.sh");
293
294 println!("Running type check script: {}", script_path.display());
295
296 let output = std::process::Command::new("bash")
298 .arg(&script_path)
299 .output()
300 .expect("Failed to run check-types.sh");
301
302 let stdout = String::from_utf8_lossy(&output.stdout);
303 let stderr = String::from_utf8_lossy(&output.stderr);
304
305 println!("stdout:\n{}", stdout);
306 if !stderr.is_empty() {
307 println!("stderr:\n{}", stderr);
308 }
309
310 if stdout.contains("had type errors") || !output.status.success() {
312 panic!(
313 "TypeScript type check failed. Run 'crates/fresh-editor/plugins/check-types.sh' to see details."
314 );
315 }
316
317 println!("All plugins type check successfully!");
318 }
319}