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