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