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