Skip to main content

fresh_plugin_runtime/
ts_export.rs

1//! TypeScript type generation using ts-rs
2//!
3//! This module collects all API types with `#[derive(TS)]` and generates
4//! TypeScript declarations that are combined with the proc macro output.
5//! The generated TypeScript is validated and formatted using oxc.
6//!
7//! Types are automatically collected based on `JSEDITORAPI_REFERENCED_TYPES`
8//! from the proc macro, so when you add a new type to method signatures,
9//! it will automatically be included if it has `#[derive(TS)]`.
10
11use oxc_allocator::Allocator;
12use oxc_codegen::Codegen;
13use oxc_parser::Parser;
14use oxc_span::SourceType;
15use ts_rs::{Config as TsConfig, TS};
16
17use fresh_core::api::{
18    ActionPopupAction, ActionPopupOptions, ActionSpec, BackgroundProcessResult, BufferInfo,
19    BufferSavedDiff, CompositeHunk, CompositeLayoutConfig, CompositePaneStyle,
20    CompositeSourceConfig, CreateCompositeBufferOptions, CreateTerminalOptions,
21    CreateVirtualBufferInExistingSplitOptions, CreateVirtualBufferInSplitOptions,
22    CreateVirtualBufferOptions, CursorInfo, DirEntry, FormatterPackConfig, JsDiagnostic,
23    JsPosition, JsRange, JsTextPropertyEntry, LanguagePackConfig, LayoutHints, LspServerPackConfig,
24    OverlayColorSpec, OverlayOptions, ProcessLimitsPackConfig, SpawnResult, TerminalResult,
25    TextPropertiesAtCursor, TsHighlightSpan, ViewTokenStyle, ViewTokenWire, ViewTokenWireKind,
26    ViewportInfo, VirtualBufferResult,
27};
28use fresh_core::command::Suggestion;
29use fresh_core::file_explorer::FileExplorerDecoration;
30use fresh_core::text_property::InlineOverlay;
31
32/// Get the TypeScript declaration for a type by name
33///
34/// Returns None if the type is not known (not registered in this mapping).
35/// Add new types here when they're added to api.rs with `#[derive(TS)]`.
36fn get_type_decl(type_name: &str) -> Option<String> {
37    let cfg = TsConfig::default();
38    // Map TypeScript type names to their ts-rs declarations
39    // The type name should match either the Rust struct name or the ts(rename = "...") value
40    match type_name {
41        // Core types
42        "BufferInfo" => Some(BufferInfo::decl(&cfg)),
43        "CursorInfo" => Some(CursorInfo::decl(&cfg)),
44        "ViewportInfo" => Some(ViewportInfo::decl(&cfg)),
45        "ActionSpec" => Some(ActionSpec::decl(&cfg)),
46        "BufferSavedDiff" => Some(BufferSavedDiff::decl(&cfg)),
47        "LayoutHints" => Some(LayoutHints::decl(&cfg)),
48
49        // Process types
50        "SpawnResult" => Some(SpawnResult::decl(&cfg)),
51        "BackgroundProcessResult" => Some(BackgroundProcessResult::decl(&cfg)),
52
53        // Terminal types
54        "TerminalResult" => Some(TerminalResult::decl(&cfg)),
55        "CreateTerminalOptions" => Some(CreateTerminalOptions::decl(&cfg)),
56
57        // Composite buffer types (ts-rs renames these with Ts prefix)
58        "TsCompositeLayoutConfig" | "CompositeLayoutConfig" => {
59            Some(CompositeLayoutConfig::decl(&cfg))
60        }
61        "TsCompositeSourceConfig" | "CompositeSourceConfig" => {
62            Some(CompositeSourceConfig::decl(&cfg))
63        }
64        "TsCompositePaneStyle" | "CompositePaneStyle" => Some(CompositePaneStyle::decl(&cfg)),
65        "TsCompositeHunk" | "CompositeHunk" => Some(CompositeHunk::decl(&cfg)),
66        "TsCreateCompositeBufferOptions" | "CreateCompositeBufferOptions" => {
67            Some(CreateCompositeBufferOptions::decl(&cfg))
68        }
69
70        // View transform types
71        "ViewTokenWireKind" => Some(ViewTokenWireKind::decl(&cfg)),
72        "ViewTokenStyle" => Some(ViewTokenStyle::decl(&cfg)),
73        "ViewTokenWire" => Some(ViewTokenWire::decl(&cfg)),
74
75        // UI types (ts-rs renames these with Ts prefix)
76        "TsActionPopupAction" | "ActionPopupAction" => Some(ActionPopupAction::decl(&cfg)),
77        "ActionPopupOptions" => Some(ActionPopupOptions::decl(&cfg)),
78        "TsHighlightSpan" => Some(TsHighlightSpan::decl(&cfg)),
79        "FileExplorerDecoration" => Some(FileExplorerDecoration::decl(&cfg)),
80
81        // Virtual buffer option types
82        "TextPropertyEntry" | "JsTextPropertyEntry" => Some(JsTextPropertyEntry::decl(&cfg)),
83        "CreateVirtualBufferOptions" => Some(CreateVirtualBufferOptions::decl(&cfg)),
84        "CreateVirtualBufferInSplitOptions" => Some(CreateVirtualBufferInSplitOptions::decl(&cfg)),
85        "CreateVirtualBufferInExistingSplitOptions" => {
86            Some(CreateVirtualBufferInExistingSplitOptions::decl(&cfg))
87        }
88
89        // Return types
90        "TextPropertiesAtCursor" => Some(TextPropertiesAtCursor::decl(&cfg)),
91        "VirtualBufferResult" => Some(VirtualBufferResult::decl(&cfg)),
92
93        // Prompt and directory types
94        "PromptSuggestion" | "Suggestion" => Some(Suggestion::decl(&cfg)),
95        "DirEntry" => Some(DirEntry::decl(&cfg)),
96
97        // Diagnostic types
98        "JsDiagnostic" => Some(JsDiagnostic::decl(&cfg)),
99        "JsRange" => Some(JsRange::decl(&cfg)),
100        "JsPosition" => Some(JsPosition::decl(&cfg)),
101
102        // Language pack types
103        "LanguagePackConfig" => Some(LanguagePackConfig::decl(&cfg)),
104        "LspServerPackConfig" => Some(LspServerPackConfig::decl(&cfg)),
105        "ProcessLimitsPackConfig" => Some(ProcessLimitsPackConfig::decl(&cfg)),
106        "FormatterPackConfig" => Some(FormatterPackConfig::decl(&cfg)),
107
108        // Overlay/inline styling types
109        "OverlayOptions" => Some(OverlayOptions::decl(&cfg)),
110        "OverlayColorSpec" => Some(OverlayColorSpec::decl(&cfg)),
111        "InlineOverlay" => Some(InlineOverlay::decl(&cfg)),
112
113        _ => None,
114    }
115}
116
117/// Types that are dependencies of other types and must always be included.
118/// These are types referenced inside option structs or other complex types
119/// that aren't directly in method signatures.
120const DEPENDENCY_TYPES: &[&str] = &[
121    "TextPropertyEntry",              // Used in CreateVirtualBuffer*Options.entries
122    "TsCompositeLayoutConfig",        // Used in createCompositeBuffer opts
123    "TsCompositeSourceConfig",        // Used in createCompositeBuffer opts.sources
124    "TsCompositePaneStyle",           // Used in TsCompositeSourceConfig.style
125    "TsCompositeHunk",                // Used in createCompositeBuffer opts.hunks
126    "TsCreateCompositeBufferOptions", // Options for createCompositeBuffer
127    "ViewportInfo",                   // Used by plugins for viewport queries
128    "LayoutHints",                    // Used by plugins for view transforms
129    "ViewTokenWire",                  // Used by plugins for view transforms
130    "ViewTokenWireKind",              // Used by ViewTokenWire
131    "ViewTokenStyle",                 // Used by ViewTokenWire
132    "PromptSuggestion",               // Used by plugins for prompt suggestions
133    "DirEntry",                       // Used by plugins for directory entries
134    "BufferInfo",                     // Used by listBuffers, getBufferInfo
135    "JsDiagnostic",                   // Used by getAllDiagnostics
136    "JsRange",                        // Used by JsDiagnostic
137    "JsPosition",                     // Used by JsRange
138    "ActionSpec",                     // Used by executeActions
139    "TsActionPopupAction",            // Used by ActionPopupOptions.actions
140    "ActionPopupOptions",             // Used by showActionPopup
141    "FileExplorerDecoration",         // Used by setFileExplorerDecorations
142    "FormatterPackConfig",            // Used by LanguagePackConfig.formatter
143    "ProcessLimitsPackConfig",        // Used by LspServerPackConfig.process_limits
144    "TerminalResult",                 // Used by createTerminal return type
145    "CreateTerminalOptions",          // Used by createTerminal opts parameter
146    "CursorInfo",                     // Used by getPrimaryCursor, getAllCursors
147    "OverlayOptions",                 // Used by TextPropertyEntry.style and InlineOverlay
148    "OverlayColorSpec",               // Used by OverlayOptions.fg/bg
149    "InlineOverlay",                  // Used by TextPropertyEntry.inlineOverlays
150];
151
152/// Collect TypeScript type declarations based on referenced types from proc macro
153///
154/// Uses `JSEDITORAPI_REFERENCED_TYPES` to determine which types to include.
155/// Also includes dependency types that are referenced by other types.
156pub fn collect_ts_types() -> String {
157    use crate::backend::quickjs_backend::JSEDITORAPI_REFERENCED_TYPES;
158
159    let mut types = Vec::new();
160    // Track by declaration content to prevent duplicates from aliases
161    // (e.g., "CompositeHunk" and "TsCompositeHunk" both resolve to the same decl)
162    let mut included_decls = std::collections::HashSet::new();
163
164    // First, include dependency types (order matters - dependencies first)
165    for type_name in DEPENDENCY_TYPES {
166        if let Some(decl) = get_type_decl(type_name) {
167            if included_decls.insert(decl.clone()) {
168                types.push(decl);
169            }
170        }
171    }
172
173    // Collect types referenced by the API
174    for type_name in JSEDITORAPI_REFERENCED_TYPES {
175        if let Some(decl) = get_type_decl(type_name) {
176            if included_decls.insert(decl.clone()) {
177                types.push(decl);
178            }
179        } else {
180            // Log warning for unknown types (these need to be added to get_type_decl)
181            eprintln!(
182                "Warning: Type '{}' is referenced in API but not registered in get_type_decl()",
183                type_name
184            );
185        }
186    }
187
188    types.join("\n\n")
189}
190
191/// Validate TypeScript syntax using oxc parser
192///
193/// Returns Ok(()) if the syntax is valid, or an error with the parse errors.
194pub fn validate_typescript(source: &str) -> Result<(), String> {
195    let allocator = Allocator::default();
196    let source_type = SourceType::d_ts();
197
198    let parser_ret = Parser::new(&allocator, source, source_type).parse();
199
200    if parser_ret.errors.is_empty() {
201        Ok(())
202    } else {
203        let errors: Vec<String> = parser_ret
204            .errors
205            .iter()
206            .map(|e: &oxc_diagnostics::OxcDiagnostic| e.to_string())
207            .collect();
208        Err(format!("TypeScript parse errors:\n{}", errors.join("\n")))
209    }
210}
211
212/// Format TypeScript source code using oxc codegen
213///
214/// Parses the TypeScript and regenerates it with consistent formatting.
215/// Returns the original source if parsing fails.
216pub fn format_typescript(source: &str) -> String {
217    let allocator = Allocator::default();
218    let source_type = SourceType::d_ts();
219
220    let parser_ret = Parser::new(&allocator, source, source_type).parse();
221
222    if !parser_ret.errors.is_empty() {
223        // Return original source if parsing fails
224        return source.to_string();
225    }
226
227    // Generate formatted code from AST
228    Codegen::new().build(&parser_ret.program).code
229}
230
231/// Generate and write the complete fresh.d.ts file
232///
233/// Combines ts-rs generated types with proc macro output,
234/// validates the syntax, formats the output, and writes to disk.
235pub fn write_fresh_dts() -> Result<(), String> {
236    use crate::backend::quickjs_backend::{JSEDITORAPI_TS_EDITOR_API, JSEDITORAPI_TS_PREAMBLE};
237
238    let ts_types = collect_ts_types();
239
240    let content = format!(
241        "{}\n{}\n{}",
242        JSEDITORAPI_TS_PREAMBLE, ts_types, JSEDITORAPI_TS_EDITOR_API
243    );
244
245    // Validate the generated TypeScript syntax
246    validate_typescript(&content)?;
247
248    // Format the TypeScript
249    let formatted = format_typescript(&content);
250
251    // Determine output path - write to fresh-editor/plugins/lib/fresh.d.ts
252    let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
253    let output_path = std::path::Path::new(&manifest_dir)
254        .parent() // crates/
255        .and_then(|p| p.parent()) // workspace root
256        .map(|p| p.join("crates/fresh-editor/plugins/lib/fresh.d.ts"))
257        .unwrap_or_else(|| std::path::PathBuf::from("plugins/lib/fresh.d.ts"));
258
259    // Only write if content changed
260    let should_write = match std::fs::read_to_string(&output_path) {
261        Ok(existing) => existing != formatted,
262        Err(_) => true,
263    };
264
265    if should_write {
266        if let Some(parent) = output_path.parent() {
267            std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
268        }
269        std::fs::write(&output_path, &formatted).map_err(|e| e.to_string())?;
270    }
271
272    Ok(())
273}
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278
279    /// Generate, validate, format, and write fresh.d.ts
280    /// Run with: cargo test -p fresh-plugin-runtime write_fresh_dts_file -- --ignored --nocapture
281    #[test]
282    #[ignore]
283    fn write_fresh_dts_file() {
284        // write_fresh_dts validates syntax and formats before writing
285        write_fresh_dts().expect("Failed to write fresh.d.ts");
286        println!("Successfully generated, validated, and formatted fresh.d.ts");
287    }
288
289    /// Type check all plugins using TypeScript compiler
290    /// Skips if tsc is not available in PATH
291    /// Run with: cargo test -p fresh-plugin-runtime type_check_plugins -- --ignored --nocapture
292    #[test]
293    #[ignore]
294    fn type_check_plugins() {
295        // Check if tsc is available
296        let tsc_check = std::process::Command::new("tsc").arg("--version").output();
297
298        match tsc_check {
299            Ok(output) if output.status.success() => {
300                println!(
301                    "Found tsc: {}",
302                    String::from_utf8_lossy(&output.stdout).trim()
303                );
304            }
305            _ => {
306                println!("tsc not found in PATH, skipping type check test");
307                return;
308            }
309        }
310
311        // Find the check-types.sh script
312        let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
313        let script_path = std::path::Path::new(&manifest_dir)
314            .parent()
315            .and_then(|p| p.parent())
316            .map(|p| p.join("crates/fresh-editor/plugins/check-types.sh"))
317            .expect("Failed to find check-types.sh");
318
319        println!("Running type check script: {}", script_path.display());
320
321        // Run the check-types.sh script
322        let output = std::process::Command::new("bash")
323            .arg(&script_path)
324            .output()
325            .expect("Failed to run check-types.sh");
326
327        let stdout = String::from_utf8_lossy(&output.stdout);
328        let stderr = String::from_utf8_lossy(&output.stderr);
329
330        println!("stdout:\n{}", stdout);
331        if !stderr.is_empty() {
332            println!("stderr:\n{}", stderr);
333        }
334
335        // The script outputs "X file(s) had type errors" if there are errors
336        if stdout.contains("had type errors") || !output.status.success() {
337            panic!(
338                "TypeScript type check failed. Run 'crates/fresh-editor/plugins/check-types.sh' to see details."
339            );
340        }
341
342        println!("All plugins type check successfully!");
343    }
344
345    // ========================================================================
346    // Type declaration tests
347    // ========================================================================
348
349    #[test]
350    fn test_get_type_decl_returns_all_expected_types() {
351        let expected_types = vec![
352            "BufferInfo",
353            "CursorInfo",
354            "ViewportInfo",
355            "ActionSpec",
356            "BufferSavedDiff",
357            "LayoutHints",
358            "SpawnResult",
359            "BackgroundProcessResult",
360            "TerminalResult",
361            "CreateTerminalOptions",
362            "TsCompositeLayoutConfig",
363            "TsCompositeSourceConfig",
364            "TsCompositePaneStyle",
365            "TsCompositeHunk",
366            "TsCreateCompositeBufferOptions",
367            "ViewTokenWireKind",
368            "ViewTokenStyle",
369            "ViewTokenWire",
370            "TsActionPopupAction",
371            "ActionPopupOptions",
372            "TsHighlightSpan",
373            "FileExplorerDecoration",
374            "TextPropertyEntry",
375            "CreateVirtualBufferOptions",
376            "CreateVirtualBufferInSplitOptions",
377            "CreateVirtualBufferInExistingSplitOptions",
378            "TextPropertiesAtCursor",
379            "VirtualBufferResult",
380            "PromptSuggestion",
381            "DirEntry",
382            "JsDiagnostic",
383            "JsRange",
384            "JsPosition",
385            "LanguagePackConfig",
386            "LspServerPackConfig",
387            "ProcessLimitsPackConfig",
388            "FormatterPackConfig",
389        ];
390
391        for type_name in &expected_types {
392            assert!(
393                get_type_decl(type_name).is_some(),
394                "get_type_decl should return a declaration for '{}'",
395                type_name
396            );
397        }
398    }
399
400    #[test]
401    fn test_get_type_decl_aliases_resolve_same() {
402        // Rust name aliases should produce the same declaration as ts-rs name
403        let alias_pairs = vec![
404            ("CompositeHunk", "TsCompositeHunk"),
405            ("CompositeLayoutConfig", "TsCompositeLayoutConfig"),
406            ("CompositeSourceConfig", "TsCompositeSourceConfig"),
407            ("CompositePaneStyle", "TsCompositePaneStyle"),
408            (
409                "CreateCompositeBufferOptions",
410                "TsCreateCompositeBufferOptions",
411            ),
412            ("ActionPopupAction", "TsActionPopupAction"),
413            ("Suggestion", "PromptSuggestion"),
414            ("JsTextPropertyEntry", "TextPropertyEntry"),
415        ];
416
417        for (rust_name, ts_name) in &alias_pairs {
418            let rust_decl = get_type_decl(rust_name);
419            let ts_decl = get_type_decl(ts_name);
420            assert!(
421                rust_decl.is_some(),
422                "get_type_decl should handle Rust name '{}'",
423                rust_name
424            );
425            assert_eq!(
426                rust_decl, ts_decl,
427                "Alias '{}' and '{}' should produce identical declarations",
428                rust_name, ts_name
429            );
430        }
431    }
432
433    #[test]
434    fn test_terminal_types_exist() {
435        let terminal_result = get_type_decl("TerminalResult");
436        assert!(
437            terminal_result.is_some(),
438            "TerminalResult should be defined"
439        );
440        let decl = terminal_result.unwrap();
441        assert!(
442            decl.contains("bufferId"),
443            "TerminalResult should have bufferId field"
444        );
445        assert!(
446            decl.contains("terminalId"),
447            "TerminalResult should have terminalId field"
448        );
449        assert!(
450            decl.contains("splitId"),
451            "TerminalResult should have splitId field"
452        );
453
454        let terminal_opts = get_type_decl("CreateTerminalOptions");
455        assert!(
456            terminal_opts.is_some(),
457            "CreateTerminalOptions should be defined"
458        );
459    }
460
461    #[test]
462    fn test_cursor_info_type_exists() {
463        let cursor_info = get_type_decl("CursorInfo");
464        assert!(cursor_info.is_some(), "CursorInfo should be defined");
465        let decl = cursor_info.unwrap();
466        assert!(
467            decl.contains("position"),
468            "CursorInfo should have position field"
469        );
470        assert!(
471            decl.contains("selection"),
472            "CursorInfo should have selection field"
473        );
474    }
475
476    #[test]
477    fn test_collect_ts_types_no_duplicates() {
478        let output = collect_ts_types();
479        let lines: Vec<&str> = output.lines().collect();
480
481        // Check for duplicate type/interface declarations
482        let mut declarations = std::collections::HashSet::new();
483        for line in &lines {
484            let trimmed = line.trim();
485            // Match type declarations: "type Foo = {" or "type Foo ="
486            if trimmed.starts_with("type ") && trimmed.contains('=') {
487                let name = trimmed
488                    .strip_prefix("type ")
489                    .unwrap()
490                    .split(|c: char| c == '=' || c.is_whitespace())
491                    .next()
492                    .unwrap();
493                assert!(
494                    declarations.insert(name.to_string()),
495                    "Duplicate type declaration found: '{}'",
496                    name
497                );
498            }
499        }
500    }
501
502    #[test]
503    fn test_collect_ts_types_includes_dependency_types() {
504        let output = collect_ts_types();
505        let required_types = [
506            "TextPropertyEntry",
507            "TsCompositeLayoutConfig",
508            "TsCompositeSourceConfig",
509            "TsCompositePaneStyle",
510            "TsCompositeHunk",
511            "TsCreateCompositeBufferOptions",
512            "PromptSuggestion",
513            "BufferInfo",
514            "CursorInfo",
515            "TerminalResult",
516            "CreateTerminalOptions",
517        ];
518
519        for type_name in &required_types {
520            assert!(
521                output.contains(type_name),
522                "collect_ts_types output should contain type '{}'",
523                type_name
524            );
525        }
526    }
527
528    #[test]
529    fn test_generated_dts_validates_as_typescript() {
530        use crate::backend::quickjs_backend::{JSEDITORAPI_TS_EDITOR_API, JSEDITORAPI_TS_PREAMBLE};
531
532        let ts_types = collect_ts_types();
533        let content = format!(
534            "{}\n{}\n{}",
535            JSEDITORAPI_TS_PREAMBLE, ts_types, JSEDITORAPI_TS_EDITOR_API
536        );
537
538        validate_typescript(&content).expect("Generated TypeScript should be syntactically valid");
539    }
540
541    #[test]
542    fn test_generated_dts_no_undefined_type_references() {
543        use crate::backend::quickjs_backend::{JSEDITORAPI_TS_EDITOR_API, JSEDITORAPI_TS_PREAMBLE};
544
545        let ts_types = collect_ts_types();
546        let content = format!(
547            "{}\n{}\n{}",
548            JSEDITORAPI_TS_PREAMBLE, ts_types, JSEDITORAPI_TS_EDITOR_API
549        );
550
551        // Collect all defined type names
552        let mut defined_types = std::collections::HashSet::new();
553        // Built-in types
554        for builtin in &[
555            "number",
556            "string",
557            "boolean",
558            "void",
559            "unknown",
560            "null",
561            "undefined",
562            "Record",
563            "Array",
564            "Promise",
565            "ProcessHandle",
566            "PromiseLike",
567            "BufferId",
568            "SplitId",
569            "EditorAPI",
570        ] {
571            defined_types.insert(builtin.to_string());
572        }
573
574        // Extract defined types from declarations
575        for line in content.lines() {
576            let trimmed = line.trim();
577            if trimmed.starts_with("type ") && trimmed.contains('=') {
578                if let Some(name) = trimmed
579                    .strip_prefix("type ")
580                    .unwrap()
581                    .split(|c: char| c == '=' || c.is_whitespace())
582                    .next()
583                {
584                    defined_types.insert(name.to_string());
585                }
586            }
587            if trimmed.starts_with("interface ") {
588                if let Some(name) = trimmed
589                    .strip_prefix("interface ")
590                    .unwrap()
591                    .split(|c: char| !c.is_alphanumeric() && c != '_')
592                    .next()
593                {
594                    defined_types.insert(name.to_string());
595                }
596            }
597        }
598
599        // Extract capitalized identifiers from EditorAPI method signature lines only
600        // (skip JSDoc comment lines which contain prose with capitalized words)
601        let interface_section = JSEDITORAPI_TS_EDITOR_API;
602        let mut undefined_refs = Vec::new();
603
604        for line in interface_section.lines() {
605            let trimmed = line.trim();
606
607            // Skip JSDoc comments and blank lines
608            if trimmed.starts_with('*')
609                || trimmed.starts_with("/*")
610                || trimmed.starts_with("//")
611                || trimmed.is_empty()
612                || trimmed == "{"
613                || trimmed == "}"
614            {
615                continue;
616            }
617
618            // This should be a method signature line
619            for word in trimmed.split(|c: char| !c.is_alphanumeric() && c != '_') {
620                if word.is_empty() {
621                    continue;
622                }
623                // Type references start with uppercase letter
624                if word.chars().next().is_some_and(|c| c.is_uppercase())
625                    && !defined_types.contains(word)
626                {
627                    undefined_refs.push(word.to_string());
628                }
629            }
630        }
631
632        // Remove duplicates for clearer error message
633        undefined_refs.sort();
634        undefined_refs.dedup();
635
636        assert!(
637            undefined_refs.is_empty(),
638            "Found undefined type references in EditorAPI interface: {:?}",
639            undefined_refs
640        );
641    }
642
643    #[test]
644    fn test_editor_api_cursor_methods_have_typed_returns() {
645        use crate::backend::quickjs_backend::JSEDITORAPI_TS_EDITOR_API;
646
647        let api = JSEDITORAPI_TS_EDITOR_API;
648
649        // getPrimaryCursor should return CursorInfo | null, not unknown
650        assert!(
651            api.contains("getPrimaryCursor(): CursorInfo | null;"),
652            "getPrimaryCursor should return CursorInfo | null, got: {}",
653            api.lines()
654                .find(|l| l.contains("getPrimaryCursor"))
655                .unwrap_or("not found")
656        );
657
658        // getAllCursors should return CursorInfo[], not unknown
659        assert!(
660            api.contains("getAllCursors(): CursorInfo[];"),
661            "getAllCursors should return CursorInfo[], got: {}",
662            api.lines()
663                .find(|l| l.contains("getAllCursors"))
664                .unwrap_or("not found")
665        );
666
667        // getAllCursorPositions should return number[], not unknown
668        assert!(
669            api.contains("getAllCursorPositions(): number[];"),
670            "getAllCursorPositions should return number[], got: {}",
671            api.lines()
672                .find(|l| l.contains("getAllCursorPositions"))
673                .unwrap_or("not found")
674        );
675    }
676
677    #[test]
678    fn test_editor_api_terminal_methods_use_defined_types() {
679        use crate::backend::quickjs_backend::JSEDITORAPI_TS_EDITOR_API;
680
681        let api = JSEDITORAPI_TS_EDITOR_API;
682
683        // createTerminal should use CreateTerminalOptions and TerminalResult
684        assert!(
685            api.contains("CreateTerminalOptions"),
686            "createTerminal should reference CreateTerminalOptions"
687        );
688        assert!(
689            api.contains("TerminalResult"),
690            "createTerminal should reference TerminalResult"
691        );
692    }
693
694    #[test]
695    fn test_editor_api_composite_methods_use_ts_prefix_types() {
696        use crate::backend::quickjs_backend::JSEDITORAPI_TS_EDITOR_API;
697
698        let api = JSEDITORAPI_TS_EDITOR_API;
699
700        // updateCompositeAlignment should use TsCompositeHunk (not CompositeHunk)
701        assert!(
702            api.contains("TsCompositeHunk[]"),
703            "updateCompositeAlignment should use TsCompositeHunk[], not CompositeHunk[]"
704        );
705
706        // createCompositeBuffer should use TsCreateCompositeBufferOptions
707        assert!(
708            api.contains("TsCreateCompositeBufferOptions"),
709            "createCompositeBuffer should use TsCreateCompositeBufferOptions"
710        );
711    }
712
713    #[test]
714    fn test_editor_api_prompt_suggestions_use_prompt_suggestion() {
715        use crate::backend::quickjs_backend::JSEDITORAPI_TS_EDITOR_API;
716
717        let api = JSEDITORAPI_TS_EDITOR_API;
718
719        // setPromptSuggestions should use PromptSuggestion (not Suggestion)
720        assert!(
721            api.contains("PromptSuggestion[]"),
722            "setPromptSuggestions should use PromptSuggestion[], not Suggestion[]"
723        );
724    }
725
726    #[test]
727    fn test_all_editor_api_methods_present() {
728        use crate::backend::quickjs_backend::JSEDITORAPI_TS_EDITOR_API;
729
730        let api = JSEDITORAPI_TS_EDITOR_API;
731
732        // Comprehensive list of all expected methods
733        let expected_methods = vec![
734            "apiVersion",
735            "getActiveBufferId",
736            "getActiveSplitId",
737            "listBuffers",
738            "debug",
739            "info",
740            "warn",
741            "error",
742            "setStatus",
743            "copyToClipboard",
744            "setClipboard",
745            "registerCommand",
746            "unregisterCommand",
747            "setContext",
748            "executeAction",
749            "getCursorPosition",
750            "getBufferPath",
751            "getBufferLength",
752            "isBufferModified",
753            "saveBufferToPath",
754            "getBufferInfo",
755            "getPrimaryCursor",
756            "getAllCursors",
757            "getAllCursorPositions",
758            "getViewport",
759            "getCursorLine",
760            "getLineStartPosition",
761            "getLineEndPosition",
762            "getBufferLineCount",
763            "scrollToLineCenter",
764            "findBufferByPath",
765            "getBufferSavedDiff",
766            "insertText",
767            "deleteRange",
768            "insertAtCursor",
769            "openFile",
770            "openFileInSplit",
771            "showBuffer",
772            "closeBuffer",
773            "on",
774            "off",
775            "getEnv",
776            "getCwd",
777            "pathJoin",
778            "pathDirname",
779            "pathBasename",
780            "pathExtname",
781            "pathIsAbsolute",
782            "utf8ByteLength",
783            "fileExists",
784            "readFile",
785            "writeFile",
786            "readDir",
787            "getConfig",
788            "getUserConfig",
789            "reloadConfig",
790            "reloadThemes",
791            "reloadAndApplyTheme",
792            "registerGrammar",
793            "registerLanguageConfig",
794            "registerLspServer",
795            "reloadGrammars",
796            "getConfigDir",
797            "getThemesDir",
798            "applyTheme",
799            "getThemeSchema",
800            "getBuiltinThemes",
801            "getThemeData",
802            "saveThemeFile",
803            "themeFileExists",
804            "deleteTheme",
805            "fileStat",
806            "isProcessRunning",
807            "killProcess",
808            "pluginTranslate",
809            "createCompositeBuffer",
810            "updateCompositeAlignment",
811            "closeCompositeBuffer",
812            "getHighlights",
813            "addOverlay",
814            "clearNamespace",
815            "clearAllOverlays",
816            "clearOverlaysInRange",
817            "removeOverlay",
818            "addConceal",
819            "clearConcealNamespace",
820            "clearConcealsInRange",
821            "addSoftBreak",
822            "clearSoftBreakNamespace",
823            "clearSoftBreaksInRange",
824            "submitViewTransform",
825            "clearViewTransform",
826            "setLayoutHints",
827            "setFileExplorerDecorations",
828            "clearFileExplorerDecorations",
829            "addVirtualText",
830            "removeVirtualText",
831            "removeVirtualTextsByPrefix",
832            "clearVirtualTexts",
833            "clearVirtualTextNamespace",
834            "addVirtualLine",
835            "prompt",
836            "startPrompt",
837            "startPromptWithInitial",
838            "setPromptSuggestions",
839            "setPromptInputSync",
840            "defineMode",
841            "setEditorMode",
842            "getEditorMode",
843            "closeSplit",
844            "setSplitBuffer",
845            "focusSplit",
846            "setSplitScroll",
847            "setSplitRatio",
848            "setSplitLabel",
849            "clearSplitLabel",
850            "getSplitByLabel",
851            "distributeSplitsEvenly",
852            "setBufferCursor",
853            "setLineIndicator",
854            "clearLineIndicators",
855            "setLineNumbers",
856            "setViewMode",
857            "setViewState",
858            "getViewState",
859            "setLineWrap",
860            "createScrollSyncGroup",
861            "setScrollSyncAnchors",
862            "removeScrollSyncGroup",
863            "executeActions",
864            "showActionPopup",
865            "disableLspForLanguage",
866            "setLspRootUri",
867            "getAllDiagnostics",
868            "getHandlers",
869            "createVirtualBuffer",
870            "createVirtualBufferInSplit",
871            "createVirtualBufferInExistingSplit",
872            "setVirtualBufferContent",
873            "getTextPropertiesAtCursor",
874            "spawnProcess",
875            "spawnProcessWait",
876            "getBufferText",
877            "delay",
878            "sendLspRequest",
879            "spawnBackgroundProcess",
880            "killBackgroundProcess",
881            "createTerminal",
882            "sendTerminalInput",
883            "closeTerminal",
884            "refreshLines",
885            "getCurrentLocale",
886            "loadPlugin",
887            "unloadPlugin",
888            "reloadPlugin",
889            "listPlugins",
890        ];
891
892        let mut missing = Vec::new();
893        for method in &expected_methods {
894            // Check that the method name appears followed by ( in the API
895            let pattern = format!("{}(", method);
896            if !api.contains(&pattern) {
897                missing.push(*method);
898            }
899        }
900
901        assert!(
902            missing.is_empty(),
903            "Missing methods in EditorAPI interface: {:?}",
904            missing
905        );
906    }
907}