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, AnimationRect, BackgroundProcessResult,
19    BufferGroupResult, BufferInfo, BufferSavedDiff, CompositeHunk, CompositeLayoutConfig,
20    CompositePaneStyle, CompositeSourceConfig, CreateCompositeBufferOptions, CreateTerminalOptions,
21    CreateVirtualBufferInExistingSplitOptions, CreateVirtualBufferInSplitOptions,
22    CreateVirtualBufferOptions, CursorInfo, DirEntry, FormatterPackConfig, GrammarInfoSnapshot,
23    GrepMatch, JsDiagnostic, JsPosition, JsRange, JsTextPropertyEntry, KeyEventPayload,
24    LanguagePackConfig, LayoutHints, LspServerPackConfig, OverlayColorSpec, OverlayOptions,
25    PluginAnimationEdge, PluginAnimationKind, ProcessLimitsPackConfig, ReplaceResult, ScreenSize,
26    SearchTakeResult, SpawnResult, SplitSnapshot, TerminalResult, TextPropertiesAtCursor,
27    TokenColor, TsHighlightSpan, ViewTokenStyle, ViewTokenWire, ViewTokenWireKind, ViewportInfo,
28    VirtualBufferResult, WindowInfo,
29};
30use fresh_core::command::Suggestion;
31use fresh_core::file_explorer::FileExplorerDecoration;
32use fresh_core::text_property::InlineOverlay;
33
34/// Get the TypeScript declaration for a type by name
35///
36/// Returns None if the type is not known (not registered in this mapping).
37/// Add new types here when they're added to api.rs with `#[derive(TS)]`.
38fn get_type_decl(type_name: &str) -> Option<String> {
39    let cfg = TsConfig::default();
40    // Map TypeScript type names to their ts-rs declarations
41    // The type name should match either the Rust struct name or the ts(rename = "...") value
42    match type_name {
43        // Animation types
44        "AnimationRect" => Some(AnimationRect::decl(&cfg)),
45        "PluginAnimationEdge" => Some(PluginAnimationEdge::decl(&cfg)),
46        "PluginAnimationKind" => Some(PluginAnimationKind::decl(&cfg)),
47
48        // Core types
49        "BufferInfo" => Some(BufferInfo::decl(&cfg)),
50        "WindowInfo" => Some(WindowInfo::decl(&cfg)),
51        "CursorInfo" => Some(CursorInfo::decl(&cfg)),
52        "ViewportInfo" => Some(ViewportInfo::decl(&cfg)),
53        "ScreenSize" => Some(ScreenSize::decl(&cfg)),
54        "KeyEventPayload" => Some(KeyEventPayload::decl(&cfg)),
55        "SplitSnapshot" => Some(SplitSnapshot::decl(&cfg)),
56        "ActionSpec" => Some(ActionSpec::decl(&cfg)),
57        "BufferSavedDiff" => Some(BufferSavedDiff::decl(&cfg)),
58        "LayoutHints" => Some(LayoutHints::decl(&cfg)),
59
60        // Process types
61        "SpawnResult" => Some(SpawnResult::decl(&cfg)),
62        "BackgroundProcessResult" => Some(BackgroundProcessResult::decl(&cfg)),
63
64        // Grep/Replace types
65        "GrepMatch" => Some(GrepMatch::decl(&cfg)),
66        "ReplaceResult" => Some(ReplaceResult::decl(&cfg)),
67        "SearchTakeResult" => Some(SearchTakeResult::decl(&cfg)),
68        // SearchHandle is the JS-side wrapper over a numeric handle id.
69        // The Rust type can't be exported (non-serializable runtime state).
70        "SearchHandle" => Some(
71            "interface SearchHandle { searchId: number; take(): SearchTakeResult; cancel(): void; }".to_string(),
72        ),
73
74        // Terminal types
75        "TerminalResult" => Some(TerminalResult::decl(&cfg)),
76        "CreateTerminalOptions" => Some(CreateTerminalOptions::decl(&cfg)),
77        "CreateWindowWithTerminalOptions" => {
78            Some(fresh_core::api::CreateWindowWithTerminalOptions::decl(&cfg))
79        }
80        "SessionWithTerminalResult" => {
81            Some(fresh_core::api::SessionWithTerminalResult::decl(&cfg))
82        }
83
84        // Composite buffer types (ts-rs renames these with Ts prefix)
85        "TsCompositeLayoutConfig" | "CompositeLayoutConfig" => {
86            Some(CompositeLayoutConfig::decl(&cfg))
87        }
88        "TsCompositeSourceConfig" | "CompositeSourceConfig" => {
89            Some(CompositeSourceConfig::decl(&cfg))
90        }
91        "TsCompositePaneStyle" | "CompositePaneStyle" => Some(CompositePaneStyle::decl(&cfg)),
92        "TsCompositeHunk" | "CompositeHunk" => Some(CompositeHunk::decl(&cfg)),
93        "TsCreateCompositeBufferOptions" | "CreateCompositeBufferOptions" => {
94            Some(CreateCompositeBufferOptions::decl(&cfg))
95        }
96
97        // View transform types
98        "ViewTokenWireKind" => Some(ViewTokenWireKind::decl(&cfg)),
99        "TokenColor" => Some(TokenColor::decl(&cfg)),
100        "ViewTokenStyle" => Some(ViewTokenStyle::decl(&cfg)),
101        "ViewTokenWire" => Some(ViewTokenWire::decl(&cfg)),
102
103        // UI types (ts-rs renames these with Ts prefix)
104        "TsActionPopupAction" | "ActionPopupAction" => Some(ActionPopupAction::decl(&cfg)),
105        "ActionPopupOptions" => Some(ActionPopupOptions::decl(&cfg)),
106        "TsLspMenuItem" | "LspMenuItem" => Some(fresh_core::api::LspMenuItem::decl(&cfg)),
107        "TsHighlightSpan" => Some(TsHighlightSpan::decl(&cfg)),
108        "FileExplorerDecoration" => Some(FileExplorerDecoration::decl(&cfg)),
109
110        // Virtual buffer option types
111        "TextPropertyEntry" | "JsTextPropertyEntry" => Some(JsTextPropertyEntry::decl(&cfg)),
112        "CreateVirtualBufferOptions" => Some(CreateVirtualBufferOptions::decl(&cfg)),
113        "CreateVirtualBufferInSplitOptions" => Some(CreateVirtualBufferInSplitOptions::decl(&cfg)),
114        "CreateVirtualBufferInExistingSplitOptions" => {
115            Some(CreateVirtualBufferInExistingSplitOptions::decl(&cfg))
116        }
117
118        // Return types
119        "TextPropertiesAtCursor" => Some(TextPropertiesAtCursor::decl(&cfg)),
120        "VirtualBufferResult" => Some(VirtualBufferResult::decl(&cfg)),
121        "BufferGroupResult" => Some(BufferGroupResult::decl(&cfg)),
122
123        // Prompt and directory types
124        "PromptSuggestion" | "Suggestion" => Some(Suggestion::decl(&cfg)),
125        "DirEntry" => Some(DirEntry::decl(&cfg)),
126
127        // Diagnostic types
128        "JsDiagnostic" => Some(JsDiagnostic::decl(&cfg)),
129        "JsRange" => Some(JsRange::decl(&cfg)),
130        "JsPosition" => Some(JsPosition::decl(&cfg)),
131
132        // Grammar info types
133        "GrammarInfoSnapshot" => Some(GrammarInfoSnapshot::decl(&cfg)),
134
135        // Language pack types
136        "LanguagePackConfig" => Some(LanguagePackConfig::decl(&cfg)),
137        "LspServerPackConfig" => Some(LspServerPackConfig::decl(&cfg)),
138        "ProcessLimitsPackConfig" => Some(ProcessLimitsPackConfig::decl(&cfg)),
139        "FormatterPackConfig" => Some(FormatterPackConfig::decl(&cfg)),
140
141        // Overlay/inline styling types
142        "OverlayOptions" => Some(OverlayOptions::decl(&cfg)),
143        "OverlayColorSpec" => Some(OverlayColorSpec::decl(&cfg)),
144        "InlineOverlay" => Some(InlineOverlay::decl(&cfg)),
145        "OffsetUnit" => Some(fresh_core::text_property::OffsetUnit::decl(&cfg)),
146        "StyledSegment" => Some(fresh_core::text_property::StyledSegment::decl(&cfg)),
147        "StyledText" => Some(fresh_core::api::StyledText::decl(&cfg)),
148
149        // Widget library types — declarative plugin UI.
150        // See docs/internal/plugin-widget-library-design.md.
151        "WidgetSpec" => Some(fresh_core::api::WidgetSpec::decl(&cfg)),
152        "HintEntry" => Some(fresh_core::api::HintEntry::decl(&cfg)),
153        "ButtonKind" => Some(fresh_core::api::ButtonKind::decl(&cfg)),
154        "WidgetAction" => Some(fresh_core::api::WidgetAction::decl(&cfg)),
155        "WidgetMutation" => Some(fresh_core::api::WidgetMutation::decl(&cfg)),
156        "TreeNode" => Some(fresh_core::api::TreeNode::decl(&cfg)),
157
158        // Authority — payload schema for `editor.setAuthority(...)`.
159        // Hand-written because the authoritative struct lives in
160        // `fresh-editor` and this crate must not depend on it
161        // (principle 3: core is opaque to backend kinds). Keep this in
162        // sync with `crates/fresh-editor/src/services/authority/mod.rs`.
163        "AuthorityPayload" => Some(AUTHORITY_PAYLOAD_DECL.to_string()),
164
165        // Remote Indicator override — payload for
166        // `editor.setRemoteIndicatorState(...)`. Same hand-written
167        // rationale: the authoritative enum lives in
168        // `fresh-editor::view::ui::status_bar::RemoteIndicatorOverride`
169        // and this crate must not depend on it. Keep in sync.
170        "RemoteIndicatorStatePayload" => Some(REMOTE_INDICATOR_STATE_DECL.to_string()),
171
172        _ => None,
173    }
174}
175
176/// Hand-written declaration for `AuthorityPayload` and its helpers.
177/// See the doc comment on the match arm for why this isn't ts-rs.
178///
179/// Emitted as plain `type …` (not `export type …`) to match the rest of
180/// the file — the generated d.ts lives in global scope and plugins
181/// reference types by bare name without importing them.
182const AUTHORITY_PAYLOAD_DECL: &str = r#"type AuthorityFilesystem = { kind: "local" };
183
184type AuthoritySpawner =
185  | { kind: "local" }
186  | {
187      kind: "docker-exec";
188      container_id: string;
189      user?: string | null;
190      workspace?: string | null;
191      env?: [string, string][];
192    };
193
194type AuthorityTerminalWrapper =
195  | { kind: "host-shell" }
196  | {
197      kind: "explicit";
198      command: string;
199      args: string[];
200      manages_cwd?: boolean;
201    };
202
203type AuthorityPayload = {
204  filesystem: AuthorityFilesystem;
205  spawner: AuthoritySpawner;
206  terminal_wrapper: AuthorityTerminalWrapper;
207  display_label?: string;
208  /**
209  * Optional host↔remote workspace path mapping. The dev-container
210  * authority sets both roots (editor.getCwd() on host;
211  * remoteWorkspaceFolder on container) so LSP URIs translate at the
212  * host/container boundary. Local and SSH authorities omit it.
213  */
214  path_translation?: PathTranslationSpec;
215};
216type PathTranslationSpec = {
217  host_root: string;
218  remote_root: string;
219};"#;
220
221/// Hand-written declaration for `RemoteIndicatorStatePayload`. Keep in
222/// sync with
223/// `crates/fresh-editor/src/view/ui/status_bar.rs::RemoteIndicatorOverride`
224/// (the struct this crate must not depend on).
225const REMOTE_INDICATOR_STATE_DECL: &str = r#"type RemoteIndicatorStatePayload =
226  | { kind: "local" }
227  | { kind: "connecting"; label?: string | null }
228  | { kind: "connected"; label?: string | null }
229  | { kind: "failed_attach"; error?: string | null }
230  | { kind: "disconnected"; label?: string | null };"#;
231
232/// Types that are dependencies of other types and must always be included.
233/// These are types referenced inside option structs or other complex types
234/// that aren't directly in method signatures.
235const DEPENDENCY_TYPES: &[&str] = &[
236    "TextPropertyEntry",               // Used in CreateVirtualBuffer*Options.entries
237    "TsCompositeLayoutConfig",         // Used in createCompositeBuffer opts
238    "TsCompositeSourceConfig",         // Used in createCompositeBuffer opts.sources
239    "TsCompositePaneStyle",            // Used in TsCompositeSourceConfig.style
240    "TsCompositeHunk",                 // Used in createCompositeBuffer opts.hunks
241    "TsCreateCompositeBufferOptions",  // Options for createCompositeBuffer
242    "ViewportInfo",                    // Used by plugins for viewport queries
243    "ScreenSize",                      // Used by editor.getScreenSize()
244    "KeyEventPayload",                 // Used by editor.getNextKey()
245    "SplitSnapshot",                   // Used by editor.listSplits()
246    "LayoutHints",                     // Used by plugins for view transforms
247    "ViewTokenWire",                   // Used by plugins for view transforms
248    "ViewTokenWireKind",               // Used by ViewTokenWire
249    "TokenColor",                      // Used by ViewTokenStyle fg/bg
250    "ViewTokenStyle",                  // Used by ViewTokenWire
251    "PromptSuggestion",                // Used by plugins for prompt suggestions
252    "DirEntry",                        // Used by plugins for directory entries
253    "BufferInfo",                      // Used by listBuffers, getBufferInfo
254    "WindowInfo",                      // Used by listWindows
255    "JsDiagnostic",                    // Used by getAllDiagnostics
256    "JsRange",                         // Used by JsDiagnostic
257    "JsPosition",                      // Used by JsRange
258    "ActionSpec",                      // Used by executeActions
259    "TsActionPopupAction",             // Used by ActionPopupOptions.actions
260    "ActionPopupOptions",              // Used by showActionPopup
261    "TsLspMenuItem",                   // Used by setLspMenuContributions
262    "FileExplorerDecoration",          // Used by setFileExplorerDecorations
263    "FormatterPackConfig",             // Used by LanguagePackConfig.formatter
264    "ProcessLimitsPackConfig",         // Used by LspServerPackConfig.process_limits
265    "TerminalResult",                  // Used by createTerminal return type
266    "CreateWindowWithTerminalOptions", // Used by createWindowWithTerminal opts
267    "SessionWithTerminalResult",       // Used by createWindowWithTerminal return type
268    "CreateTerminalOptions",           // Used by createTerminal opts parameter
269    "CursorInfo",                      // Used by getPrimaryCursor, getAllCursors
270    "OverlayOptions",                  // Used by TextPropertyEntry.style and InlineOverlay
271    "OverlayColorSpec",                // Used by OverlayOptions.fg/bg
272    "InlineOverlay",                   // Used by TextPropertyEntry.inlineOverlays
273    "OffsetUnit",                      // Used by InlineOverlay.unit
274    "StyledSegment",                   // Used by TextPropertyEntry.segments
275    "GrammarInfoSnapshot",             // Used by listGrammars
276    "AnimationRect",                   // Used by animateArea
277    "PluginAnimationEdge",             // Used by PluginAnimationKind
278    "PluginAnimationKind",             // Used by animateArea/animateVirtualBuffer
279    // Widget library types (see docs/internal/plugin-widget-library-design.md)
280    "HintEntry",      // Used by WidgetSpec::HintBar
281    "ButtonKind",     // Used by WidgetSpec::Button.intent
282    "TreeNode",       // Used by WidgetSpec::Tree.nodes
283    "WidgetSpec",     // Used by mountWidgetPanel/updateWidgetPanel
284    "WidgetAction",   // Used by widgetCommand
285    "WidgetMutation", // Used by widgetMutate
286    // Streaming-search pull handle (referenced via ts_raw on beginSearch)
287    "SearchTakeResult",
288    "SearchHandle",
289];
290
291/// Collect TypeScript type declarations based on referenced types from proc macro
292///
293/// Uses `JSEDITORAPI_REFERENCED_TYPES` to determine which types to include.
294/// Also includes dependency types that are referenced by other types.
295pub fn collect_ts_types() -> String {
296    use crate::backend::quickjs_backend::JSEDITORAPI_REFERENCED_TYPES;
297
298    let mut types = Vec::new();
299    // Track by declaration content to prevent duplicates from aliases
300    // (e.g., "CompositeHunk" and "TsCompositeHunk" both resolve to the same decl)
301    let mut included_decls = std::collections::HashSet::new();
302
303    // First, include dependency types (order matters - dependencies first)
304    for type_name in DEPENDENCY_TYPES {
305        if let Some(decl) = get_type_decl(type_name) {
306            if included_decls.insert(decl.clone()) {
307                types.push(decl);
308            }
309        }
310    }
311
312    // Collect types referenced by the API
313    for type_name in JSEDITORAPI_REFERENCED_TYPES {
314        if let Some(decl) = get_type_decl(type_name) {
315            if included_decls.insert(decl.clone()) {
316                types.push(decl);
317            }
318        } else {
319            // Log warning for unknown types (these need to be added to get_type_decl)
320            eprintln!(
321                "Warning: Type '{}' is referenced in API but not registered in get_type_decl()",
322                type_name
323            );
324        }
325    }
326
327    types.join("\n\n")
328}
329
330/// Validate TypeScript syntax using oxc parser
331///
332/// Returns Ok(()) if the syntax is valid, or an error with the parse errors.
333pub fn validate_typescript(source: &str) -> Result<(), String> {
334    let allocator = Allocator::default();
335    let source_type = SourceType::d_ts();
336
337    let parser_ret = Parser::new(&allocator, source, source_type).parse();
338
339    if parser_ret.errors.is_empty() {
340        Ok(())
341    } else {
342        let errors: Vec<String> = parser_ret
343            .errors
344            .iter()
345            .map(|e: &oxc_diagnostics::OxcDiagnostic| e.to_string())
346            .collect();
347        Err(format!("TypeScript parse errors:\n{}", errors.join("\n")))
348    }
349}
350
351/// Format TypeScript source code using oxc codegen
352///
353/// Parses the TypeScript and regenerates it with consistent formatting.
354/// Returns the original source if parsing fails.
355pub fn format_typescript(source: &str) -> String {
356    let allocator = Allocator::default();
357    let source_type = SourceType::d_ts();
358
359    let parser_ret = Parser::new(&allocator, source, source_type).parse();
360
361    if !parser_ret.errors.is_empty() {
362        // Return original source if parsing fails
363        return source.to_string();
364    }
365
366    // Generate formatted code from AST
367    Codegen::new().build(&parser_ret.program).code
368}
369
370/// Generate and write the complete fresh.d.ts file
371///
372/// Combines ts-rs generated types with proc macro output,
373/// validates the syntax, formats the output, and writes to disk.
374pub fn write_fresh_dts() -> Result<(), String> {
375    use crate::backend::quickjs_backend::{JSEDITORAPI_TS_EDITOR_API, JSEDITORAPI_TS_PREAMBLE};
376
377    let ts_types = collect_ts_types();
378
379    // After the macro-generated EditorAPI interface, merge in a
380    // typed overload of `getPluginApi` that looks through the
381    // `FreshPluginRegistry` interface (declared in the preamble,
382    // augmented by each loaded plugin's `plugins.d.ts`). Declared
383    // AFTER the base interface so TypeScript's overload resolution
384    // prefers the typed form when the name is a known key; the
385    // untyped `getPluginApi(name: string): unknown | null` from the
386    // macro output is the fallback.
387    let plugin_api_trailer = r#"
388
389/**
390 * Typed overload of `editor.getPluginApi`. When the caller passes a
391 * key that some loaded plugin declared in `FreshPluginRegistry`, the
392 * return type is narrowed to that plugin's API. Unknown names fall
393 * through to the untyped `unknown | null` signature.
394 */
395interface EditorAPI {
396  getPluginApi<K extends keyof FreshPluginRegistry>(name: K): FreshPluginRegistry[K] | null;
397}
398
399/**
400 * Typed overload of `editor.defineConfigEnum`. The macro-generated
401 * signature can't express `<E extends string>` propagating from the
402 * `values` array into the return type, so it's declared here. Use
403 * `as const` on the `values` array to get a literal-union return:
404 *
405 * ```ts
406 * const mode = editor.defineConfigEnum("mode", {
407 *   values: ["normal", "insert"] as const,
408 *   default: "normal",
409 * });
410 * mode; // typed as "normal" | "insert"
411 * ```
412 *
413 * Typed overload of `editor.getPluginConfig`. Plugins that declared
414 * their fields via `defineConfigX` can pass the shape type explicitly:
415 * `editor.getPluginConfig<{ autoEnable: boolean; ... }>()`. Without
416 * the generic, falls back to `unknown`.
417 */
418interface EditorAPI {
419  defineConfigEnum<E extends string>(
420    name: string,
421    options: { values: readonly E[]; default: NoInfer<E>; description?: string },
422  ): E;
423  getPluginConfig<T = unknown>(): T;
424}
425
426/**
427 * Maps every hook event name to its payload type.
428 *
429 * Payloads match the flat JSON produced by `hook_args_to_json` on the Rust
430 * side (`HookArgs` is `#[serde(untagged)]`, so each variant serializes as its
431 * fields only). The TypeScript types here are derived directly from the Rust
432 * field definitions and must be kept in sync with `fresh-core/src/hooks.rs`.
433 *
434 * `action` in `pre_command`/`post_command` is the serde JSON of the `Action`
435 * enum: unit variants serialize as a plain string (e.g. `"MoveLeft"`),
436 * tuple variants as a single-key object (e.g. `{"InsertChar": "a"}`).
437 */
438interface HookEventMap {
439  // ── lifecycle ────────────────────────────────────────────────────────────
440  editor_initialized: Record<string, never>;
441  plugins_loaded: Record<string, never>;
442  ready: Record<string, never>;
443  focus_gained: Record<string, never>;
444  authority_changed: { label: string };
445
446  // ── buffer lifecycle ─────────────────────────────────────────────────────
447  buffer_activated: { buffer_id: number };
448  buffer_deactivated: { buffer_id: number };
449  buffer_closed: { buffer_id: number };
450
451  // ── file I/O ─────────────────────────────────────────────────────────────
452  before_file_open: { path: string };
453  after_file_open: { path: string; buffer_id: number };
454  before_file_save: { path: string; buffer_id: number };
455  after_file_save: { path: string; buffer_id: number };
456  /**
457   * Fired by the file explorer after a paste/duplicate/etc. mutates
458   * the filesystem without going through a buffer save. Plugins that
459   * surface FS-derived state (git status badges, etc.) should
460   * subscribe in addition to `after_file_save` to refresh on
461   * explorer-driven changes too.
462   */
463  after_file_explorer_change: { path: string };
464
465  // ── text edits ───────────────────────────────────────────────────────────
466  before_insert: { buffer_id: number; position: number; text: string };
467  after_insert: {
468    buffer_id: number;
469    position: number;
470    text: string;
471    affected_start: number;
472    affected_end: number;
473    start_line: number;
474    end_line: number;
475    lines_added: number;
476  };
477  before_delete: { buffer_id: number; start: number; end: number };
478  after_delete: {
479    buffer_id: number;
480    start: number;
481    end: number;
482    deleted_text: string;
483    affected_start: number;
484    deleted_len: number;
485    start_line: number;
486    end_line: number;
487    lines_removed: number;
488  };
489
490  // ── cursor & viewport ────────────────────────────────────────────────────
491  cursor_moved: {
492    buffer_id: number;
493    cursor_id: number;
494    old_position: number;
495    new_position: number;
496    line: number;
497    text_properties: Record<string, unknown>[];
498  };
499  viewport_changed: {
500    split_id: number;
501    buffer_id: number;
502    top_byte: number;
503    top_line: number | null;
504    width: number;
505    height: number;
506  };
507
508  // ── rendering ────────────────────────────────────────────────────────────
509  render_start: { buffer_id: number };
510  render_line: {
511    buffer_id: number;
512    line_number: number;
513    byte_start: number;
514    byte_end: number;
515    content: string;
516  };
517  lines_changed: {
518    buffer_id: number;
519    lines: { line_number: number; byte_start: number; byte_end: number; content: string }[];
520  };
521  view_transform_request: {
522    buffer_id: number;
523    split_id: number;
524    viewport_start: number;
525    viewport_end: number;
526    tokens: ViewTokenWire[];
527    cursor_positions: number[];
528  };
529
530  // ── commands ─────────────────────────────────────────────────────────────
531  pre_command: { action: string | Record<string, unknown> };
532  post_command: { action: string | Record<string, unknown> };
533  idle: { milliseconds: number };
534  resize: { width: number; height: number };
535
536  // ── prompts ──────────────────────────────────────────────────────────────
537  prompt_changed: { prompt_type: string; input: string };
538  prompt_confirmed: { prompt_type: string; input: string; selected_index: number | null };
539  prompt_cancelled: { prompt_type: string; input: string };
540  prompt_selection_changed: { prompt_type: string; selected_index: number };
541
542  // ── mouse ────────────────────────────────────────────────────────────────
543  mouse_click: MouseClickHookArgs;
544  mouse_move: { column: number; row: number; content_x: number; content_y: number };
545  mouse_scroll: { buffer_id: number; delta: number; col: number; row: number };
546
547  // ── LSP ──────────────────────────────────────────────────────────────────
548  diagnostics_updated: { uri: string; count: number };
549  lsp_references: {
550    symbol: string;
551    locations: { file: string; line: number; column: number }[];
552  };
553  lsp_server_request: {
554    language: string;
555    method: string;
556    server_command: string;
557    params: string | null;
558  };
559  lsp_server_error: {
560    language: string;
561    server_command: string;
562    error_type: string;
563    message: string;
564  };
565  lsp_status_clicked: {
566    language: string;
567    has_error: boolean;
568    missing_servers: string[];
569    user_dismissed: boolean;
570  };
571
572  // ── UI events ────────────────────────────────────────────────────────────
573  action_popup_result: { popup_id: string; action_id: string };
574  process_output: { process_id: number; data: string };
575  language_changed: { buffer_id: number; language: string };
576  theme_inspect_key: { theme_name: string; key: string };
577  keyboard_shortcuts: { bindings: { key: string; action: string }[] };
578
579  // ── PTY terminals (see crates/fresh-core/src/hooks.rs) ───────────────────
580  terminal_output: { terminal_id: number; last_line: string };
581  terminal_exit: { terminal_id: number; exit_code: number | null };
582
583  // ── filesystem watching (watchPath plugin API) ────────────────────────────
584  path_changed: {
585    handle: number;
586    path: string;
587    /** "modify" | "create" | "delete" | "rename" | "other" */
588    kind: string;
589  };
590
591  // ── editor sessions (Orchestrator; see orchestrator-sessions-design.md) ────────
592  window_created: { id: number; label: string; root: string };
593  window_closed: { id: number };
594  active_window_changed: { previous_id: number | null; active_id: number };
595
596  // ── widget runtime ───────────────────────────────────────────────────────
597  /**
598   * A widget mounted via `editor.mountWidgetPanel` emitted a
599   * semantic event. Fired when the host's hit-test routes a mouse
600   * click to a `Toggle` / `Button` widget node within a mounted
601   * widget panel. See `docs/internal/plugin-widget-library-design.md`.
602   *
603   * Routing is by `panel_id` (matches the id the plugin allocated
604   * at mount time) plus `widget_key` (the stable `key` set on the
605   * widget spec node, or empty when the spec did not assign one).
606   *
607   * `event_type` and `payload` shapes:
608   *   * Toggle: `event_type = "toggle"`, `payload = { checked: <new> }`.
609   *   * Button: `event_type = "activate"`, `payload = {}`.
610   */
611  widget_event: {
612    panel_id: number;
613    widget_key: string;
614    event_type: string;
615    payload: Record<string, unknown>;
616  };
617}
618
619/**
620 * Typed overloads of `editor.on` / `editor.off`.
621 *
622 * When the event name is a key of `HookEventMap` the handler receives a
623 * fully-typed payload — TypeScript will flag misspelled field accesses at
624 * compile time. Unknown event names fall through to the untyped base
625 * signatures in the EditorAPI interface.
626 *
627 * Both function-value and handler-name forms are supported:
628 *
629 * ```ts
630 * editor.on("buffer_activated", (args) => { /* args.buffer_id is number *\/ });
631 * editor.on("buffer_activated", "myHandler");   // registerHandler("myHandler", fn)
632 * ```
633 */
634interface EditorAPI {
635  on<K extends keyof HookEventMap>(
636    eventName: K,
637    handler: (args: HookEventMap[K]) => boolean | void | Promise<boolean | void>,
638  ): void;
639  on<K extends keyof HookEventMap>(eventName: K, handlerName: string): void;
640  off<K extends keyof HookEventMap>(
641    eventName: K,
642    handler: (args: HookEventMap[K]) => boolean | void | Promise<boolean | void>,
643  ): void;
644  off<K extends keyof HookEventMap>(eventName: K, handlerName: string): void;
645  /**
646   * Create a buffer group: multiple panels appearing as one tab.
647   * This is an async runtime binding (not a direct #[qjs] method).
648   */
649  createBufferGroup(
650    name: string,
651    mode: string,
652    layout: unknown,
653  ): Promise<BufferGroupResult>;
654}
655"#;
656
657    let content = format!(
658        "{}\n{}\n{}{}",
659        JSEDITORAPI_TS_PREAMBLE, ts_types, JSEDITORAPI_TS_EDITOR_API, plugin_api_trailer
660    );
661
662    // Validate the generated TypeScript syntax
663    validate_typescript(&content)?;
664
665    // Format the TypeScript
666    let formatted = format_typescript(&content);
667
668    // Determine output path - write to fresh-editor/plugins/lib/fresh.d.ts
669    let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
670    let output_path = std::path::Path::new(&manifest_dir)
671        .parent() // crates/
672        .and_then(|p| p.parent()) // workspace root
673        .map(|p| p.join("crates/fresh-editor/plugins/lib/fresh.d.ts"))
674        .unwrap_or_else(|| std::path::PathBuf::from("plugins/lib/fresh.d.ts"));
675
676    // Only write if content changed
677    let should_write = match std::fs::read_to_string(&output_path) {
678        Ok(existing) => existing != formatted,
679        Err(_) => true,
680    };
681
682    if should_write {
683        if let Some(parent) = output_path.parent() {
684            std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
685        }
686        std::fs::write(&output_path, &formatted).map_err(|e| e.to_string())?;
687    }
688
689    Ok(())
690}
691
692#[cfg(test)]
693mod tests {
694    use super::*;
695
696    /// Generate, validate, format, and write fresh.d.ts
697    /// Run with: cargo test -p fresh-plugin-runtime write_fresh_dts_file -- --ignored --nocapture
698    #[test]
699    #[ignore]
700    fn write_fresh_dts_file() {
701        // write_fresh_dts validates syntax and formats before writing
702        write_fresh_dts().expect("Failed to write fresh.d.ts");
703        println!("Successfully generated, validated, and formatted fresh.d.ts");
704    }
705
706    /// Type check all plugins using TypeScript compiler
707    /// Skips if tsc is not available in PATH
708    /// Run with: cargo test -p fresh-plugin-runtime type_check_plugins -- --ignored --nocapture
709    #[test]
710    #[ignore]
711    fn type_check_plugins() {
712        // Check if tsc is available
713        let tsc_check = std::process::Command::new("tsc").arg("--version").output();
714
715        match tsc_check {
716            Ok(output) if output.status.success() => {
717                println!(
718                    "Found tsc: {}",
719                    String::from_utf8_lossy(&output.stdout).trim()
720                );
721            }
722            _ => {
723                println!("tsc not found in PATH, skipping type check test");
724                return;
725            }
726        }
727
728        // Find the check-types.sh script
729        let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
730        let script_path = std::path::Path::new(&manifest_dir)
731            .parent()
732            .and_then(|p| p.parent())
733            .map(|p| p.join("crates/fresh-editor/plugins/check-types.sh"))
734            .expect("Failed to find check-types.sh");
735
736        println!("Running type check script: {}", script_path.display());
737
738        // Run the check-types.sh script
739        let output = std::process::Command::new("bash")
740            .arg(&script_path)
741            .output()
742            .expect("Failed to run check-types.sh");
743
744        let stdout = String::from_utf8_lossy(&output.stdout);
745        let stderr = String::from_utf8_lossy(&output.stderr);
746
747        println!("stdout:\n{}", stdout);
748        if !stderr.is_empty() {
749            println!("stderr:\n{}", stderr);
750        }
751
752        // The script outputs "X file(s) had type errors" if there are errors
753        if stdout.contains("had type errors") || !output.status.success() {
754            panic!(
755                "TypeScript type check failed. Run 'crates/fresh-editor/plugins/check-types.sh' to see details."
756            );
757        }
758
759        println!("All plugins type check successfully!");
760    }
761
762    // ========================================================================
763    // Type declaration tests
764    // ========================================================================
765
766    #[test]
767    fn test_get_type_decl_returns_all_expected_types() {
768        let expected_types = vec![
769            "BufferInfo",
770            "WindowInfo",
771            "CursorInfo",
772            "ViewportInfo",
773            "ScreenSize",
774            "KeyEventPayload",
775            "SplitSnapshot",
776            "ActionSpec",
777            "BufferSavedDiff",
778            "LayoutHints",
779            "SpawnResult",
780            "BackgroundProcessResult",
781            "TerminalResult",
782            "CreateTerminalOptions",
783            "CreateWindowWithTerminalOptions",
784            "SessionWithTerminalResult",
785            "TsCompositeLayoutConfig",
786            "TsCompositeSourceConfig",
787            "TsCompositePaneStyle",
788            "TsCompositeHunk",
789            "TsCreateCompositeBufferOptions",
790            "ViewTokenWireKind",
791            "TokenColor",
792            "ViewTokenStyle",
793            "ViewTokenWire",
794            "TsActionPopupAction",
795            "ActionPopupOptions",
796            "TsHighlightSpan",
797            "FileExplorerDecoration",
798            "TextPropertyEntry",
799            "CreateVirtualBufferOptions",
800            "CreateVirtualBufferInSplitOptions",
801            "CreateVirtualBufferInExistingSplitOptions",
802            "TextPropertiesAtCursor",
803            "VirtualBufferResult",
804            "PromptSuggestion",
805            "DirEntry",
806            "JsDiagnostic",
807            "JsRange",
808            "JsPosition",
809            "LanguagePackConfig",
810            "LspServerPackConfig",
811            "ProcessLimitsPackConfig",
812            "FormatterPackConfig",
813        ];
814
815        for type_name in &expected_types {
816            assert!(
817                get_type_decl(type_name).is_some(),
818                "get_type_decl should return a declaration for '{}'",
819                type_name
820            );
821        }
822    }
823
824    #[test]
825    fn test_get_type_decl_aliases_resolve_same() {
826        // Rust name aliases should produce the same declaration as ts-rs name
827        let alias_pairs = vec![
828            ("CompositeHunk", "TsCompositeHunk"),
829            ("CompositeLayoutConfig", "TsCompositeLayoutConfig"),
830            ("CompositeSourceConfig", "TsCompositeSourceConfig"),
831            ("CompositePaneStyle", "TsCompositePaneStyle"),
832            (
833                "CreateCompositeBufferOptions",
834                "TsCreateCompositeBufferOptions",
835            ),
836            ("ActionPopupAction", "TsActionPopupAction"),
837            ("Suggestion", "PromptSuggestion"),
838            ("JsTextPropertyEntry", "TextPropertyEntry"),
839        ];
840
841        for (rust_name, ts_name) in &alias_pairs {
842            let rust_decl = get_type_decl(rust_name);
843            let ts_decl = get_type_decl(ts_name);
844            assert!(
845                rust_decl.is_some(),
846                "get_type_decl should handle Rust name '{}'",
847                rust_name
848            );
849            assert_eq!(
850                rust_decl, ts_decl,
851                "Alias '{}' and '{}' should produce identical declarations",
852                rust_name, ts_name
853            );
854        }
855    }
856
857    #[test]
858    fn test_terminal_types_exist() {
859        let terminal_result = get_type_decl("TerminalResult");
860        assert!(
861            terminal_result.is_some(),
862            "TerminalResult should be defined"
863        );
864        let decl = terminal_result.unwrap();
865        assert!(
866            decl.contains("bufferId"),
867            "TerminalResult should have bufferId field"
868        );
869        assert!(
870            decl.contains("terminalId"),
871            "TerminalResult should have terminalId field"
872        );
873        assert!(
874            decl.contains("splitId"),
875            "TerminalResult should have splitId field"
876        );
877
878        let terminal_opts = get_type_decl("CreateTerminalOptions");
879        assert!(
880            terminal_opts.is_some(),
881            "CreateTerminalOptions should be defined"
882        );
883    }
884
885    #[test]
886    fn test_cursor_info_type_exists() {
887        let cursor_info = get_type_decl("CursorInfo");
888        assert!(cursor_info.is_some(), "CursorInfo should be defined");
889        let decl = cursor_info.unwrap();
890        assert!(
891            decl.contains("position"),
892            "CursorInfo should have position field"
893        );
894        assert!(
895            decl.contains("selection"),
896            "CursorInfo should have selection field"
897        );
898    }
899
900    #[test]
901    fn test_collect_ts_types_no_duplicates() {
902        let output = collect_ts_types();
903        let lines: Vec<&str> = output.lines().collect();
904
905        // Check for duplicate type/interface declarations
906        let mut declarations = std::collections::HashSet::new();
907        for line in &lines {
908            let trimmed = line.trim();
909            // Match type declarations: "type Foo = {" or "type Foo ="
910            if trimmed.starts_with("type ") && trimmed.contains('=') {
911                let name = trimmed
912                    .strip_prefix("type ")
913                    .unwrap()
914                    .split(|c: char| c == '=' || c.is_whitespace())
915                    .next()
916                    .unwrap();
917                assert!(
918                    declarations.insert(name.to_string()),
919                    "Duplicate type declaration found: '{}'",
920                    name
921                );
922            }
923        }
924    }
925
926    #[test]
927    fn test_collect_ts_types_includes_dependency_types() {
928        let output = collect_ts_types();
929        let required_types = [
930            "TextPropertyEntry",
931            "TsCompositeLayoutConfig",
932            "TsCompositeSourceConfig",
933            "TsCompositePaneStyle",
934            "TsCompositeHunk",
935            "TsCreateCompositeBufferOptions",
936            "PromptSuggestion",
937            "BufferInfo",
938            "CursorInfo",
939            "TerminalResult",
940            "CreateTerminalOptions",
941        ];
942
943        for type_name in &required_types {
944            assert!(
945                output.contains(type_name),
946                "collect_ts_types output should contain type '{}'",
947                type_name
948            );
949        }
950    }
951
952    #[test]
953    fn test_generated_dts_validates_as_typescript() {
954        use crate::backend::quickjs_backend::{JSEDITORAPI_TS_EDITOR_API, JSEDITORAPI_TS_PREAMBLE};
955
956        let ts_types = collect_ts_types();
957        let content = format!(
958            "{}\n{}\n{}",
959            JSEDITORAPI_TS_PREAMBLE, ts_types, JSEDITORAPI_TS_EDITOR_API
960        );
961
962        validate_typescript(&content).expect("Generated TypeScript should be syntactically valid");
963    }
964
965    #[test]
966    fn test_generated_dts_no_undefined_type_references() {
967        use crate::backend::quickjs_backend::{JSEDITORAPI_TS_EDITOR_API, JSEDITORAPI_TS_PREAMBLE};
968
969        let ts_types = collect_ts_types();
970        let content = format!(
971            "{}\n{}\n{}",
972            JSEDITORAPI_TS_PREAMBLE, ts_types, JSEDITORAPI_TS_EDITOR_API
973        );
974
975        // Collect all defined type names
976        let mut defined_types = std::collections::HashSet::new();
977        // Built-in types
978        for builtin in &[
979            "number",
980            "string",
981            "boolean",
982            "void",
983            "unknown",
984            "null",
985            "undefined",
986            "Record",
987            "Array",
988            "Promise",
989            "ProcessHandle",
990            "PromiseLike",
991            "BufferId",
992            "SplitId",
993            "EditorAPI",
994        ] {
995            defined_types.insert(builtin.to_string());
996        }
997
998        // Extract defined types from declarations
999        for line in content.lines() {
1000            let trimmed = line.trim();
1001            if trimmed.starts_with("type ") && trimmed.contains('=') {
1002                if let Some(name) = trimmed
1003                    .strip_prefix("type ")
1004                    .unwrap()
1005                    .split(|c: char| c == '=' || c.is_whitespace())
1006                    .next()
1007                {
1008                    defined_types.insert(name.to_string());
1009                }
1010            }
1011            if trimmed.starts_with("interface ") {
1012                if let Some(name) = trimmed
1013                    .strip_prefix("interface ")
1014                    .unwrap()
1015                    .split(|c: char| !c.is_alphanumeric() && c != '_')
1016                    .next()
1017                {
1018                    defined_types.insert(name.to_string());
1019                }
1020            }
1021        }
1022
1023        // Extract capitalized identifiers from EditorAPI method signature lines only
1024        // (skip JSDoc comment lines which contain prose with capitalized words)
1025        let interface_section = JSEDITORAPI_TS_EDITOR_API;
1026        let mut undefined_refs = Vec::new();
1027
1028        for line in interface_section.lines() {
1029            let trimmed = line.trim();
1030
1031            // Skip JSDoc comments and blank lines
1032            if trimmed.starts_with('*')
1033                || trimmed.starts_with("/*")
1034                || trimmed.starts_with("//")
1035                || trimmed.is_empty()
1036                || trimmed == "{"
1037                || trimmed == "}"
1038            {
1039                continue;
1040            }
1041
1042            // This should be a method signature line
1043            for word in trimmed.split(|c: char| !c.is_alphanumeric() && c != '_') {
1044                if word.is_empty() {
1045                    continue;
1046                }
1047                // Type references start with uppercase letter
1048                if word.chars().next().is_some_and(|c| c.is_uppercase())
1049                    && !defined_types.contains(word)
1050                {
1051                    undefined_refs.push(word.to_string());
1052                }
1053            }
1054        }
1055
1056        // Remove duplicates for clearer error message
1057        undefined_refs.sort();
1058        undefined_refs.dedup();
1059
1060        assert!(
1061            undefined_refs.is_empty(),
1062            "Found undefined type references in EditorAPI interface: {:?}",
1063            undefined_refs
1064        );
1065    }
1066
1067    #[test]
1068    fn test_editor_api_cursor_methods_have_typed_returns() {
1069        use crate::backend::quickjs_backend::JSEDITORAPI_TS_EDITOR_API;
1070
1071        let api = JSEDITORAPI_TS_EDITOR_API;
1072
1073        // getPrimaryCursor should return CursorInfo | null, not unknown
1074        assert!(
1075            api.contains("getPrimaryCursor(): CursorInfo | null;"),
1076            "getPrimaryCursor should return CursorInfo | null, got: {}",
1077            api.lines()
1078                .find(|l| l.contains("getPrimaryCursor"))
1079                .unwrap_or("not found")
1080        );
1081
1082        // getAllCursors should return CursorInfo[], not unknown
1083        assert!(
1084            api.contains("getAllCursors(): CursorInfo[];"),
1085            "getAllCursors should return CursorInfo[], got: {}",
1086            api.lines()
1087                .find(|l| l.contains("getAllCursors"))
1088                .unwrap_or("not found")
1089        );
1090
1091        // getAllCursorPositions should return number[], not unknown
1092        assert!(
1093            api.contains("getAllCursorPositions(): number[];"),
1094            "getAllCursorPositions should return number[], got: {}",
1095            api.lines()
1096                .find(|l| l.contains("getAllCursorPositions"))
1097                .unwrap_or("not found")
1098        );
1099    }
1100
1101    #[test]
1102    fn test_editor_api_terminal_methods_use_defined_types() {
1103        use crate::backend::quickjs_backend::JSEDITORAPI_TS_EDITOR_API;
1104
1105        let api = JSEDITORAPI_TS_EDITOR_API;
1106
1107        // createTerminal should use CreateTerminalOptions and TerminalResult
1108        assert!(
1109            api.contains("CreateTerminalOptions"),
1110            "createTerminal should reference CreateTerminalOptions"
1111        );
1112        assert!(
1113            api.contains("TerminalResult"),
1114            "createTerminal should reference TerminalResult"
1115        );
1116    }
1117
1118    #[test]
1119    fn test_editor_api_composite_methods_use_ts_prefix_types() {
1120        use crate::backend::quickjs_backend::JSEDITORAPI_TS_EDITOR_API;
1121
1122        let api = JSEDITORAPI_TS_EDITOR_API;
1123
1124        // updateCompositeAlignment should use TsCompositeHunk (not CompositeHunk)
1125        assert!(
1126            api.contains("TsCompositeHunk[]"),
1127            "updateCompositeAlignment should use TsCompositeHunk[], not CompositeHunk[]"
1128        );
1129
1130        // createCompositeBuffer should use TsCreateCompositeBufferOptions
1131        assert!(
1132            api.contains("TsCreateCompositeBufferOptions"),
1133            "createCompositeBuffer should use TsCreateCompositeBufferOptions"
1134        );
1135    }
1136
1137    #[test]
1138    fn test_editor_api_prompt_suggestions_use_prompt_suggestion() {
1139        use crate::backend::quickjs_backend::JSEDITORAPI_TS_EDITOR_API;
1140
1141        let api = JSEDITORAPI_TS_EDITOR_API;
1142
1143        // setPromptSuggestions should use PromptSuggestion (not Suggestion)
1144        assert!(
1145            api.contains("PromptSuggestion[]"),
1146            "setPromptSuggestions should use PromptSuggestion[], not Suggestion[]"
1147        );
1148    }
1149
1150    #[test]
1151    fn test_all_editor_api_methods_present() {
1152        use crate::backend::quickjs_backend::JSEDITORAPI_TS_EDITOR_API;
1153
1154        let api = JSEDITORAPI_TS_EDITOR_API;
1155
1156        // Comprehensive list of all expected methods
1157        let expected_methods = vec![
1158            "apiVersion",
1159            "getActiveBufferId",
1160            "getActiveSplitId",
1161            "listBuffers",
1162            "debug",
1163            "info",
1164            "warn",
1165            "error",
1166            "setStatus",
1167            "copyToClipboard",
1168            "setClipboard",
1169            "registerCommand",
1170            "unregisterCommand",
1171            "setContext",
1172            "executeAction",
1173            "getCursorPosition",
1174            "getBufferPath",
1175            "getBufferLength",
1176            "isBufferModified",
1177            "saveBufferToPath",
1178            "getBufferInfo",
1179            "getPrimaryCursor",
1180            "getAllCursors",
1181            "getAllCursorPositions",
1182            "getViewport",
1183            "getScreenSize",
1184            "getCursorLine",
1185            "getLineStartPosition",
1186            "getLineEndPosition",
1187            "getBufferLineCount",
1188            "scrollToLineCenter",
1189            "findBufferByPath",
1190            "getBufferSavedDiff",
1191            "insertText",
1192            "deleteRange",
1193            "insertAtCursor",
1194            "openFile",
1195            "openFileInSplit",
1196            "showBuffer",
1197            "closeBuffer",
1198            "animateArea",
1199            "animateVirtualBuffer",
1200            "cancelAnimation",
1201            "on",
1202            "off",
1203            "getEnv",
1204            "getCwd",
1205            "pathJoin",
1206            "pathDirname",
1207            "pathBasename",
1208            "pathExtname",
1209            "pathIsAbsolute",
1210            "utf8ByteLength",
1211            "fileExists",
1212            "readFile",
1213            "writeFile",
1214            "readDir",
1215            "createDir",
1216            "removePath",
1217            "renamePath",
1218            "copyPath",
1219            "getTempDir",
1220            "getConfig",
1221            "getUserConfig",
1222            "getPluginConfig",
1223            "defineConfigBoolean",
1224            "defineConfigInteger",
1225            "defineConfigNumber",
1226            "defineConfigString",
1227            // `defineConfigEnum` is hand-written in the d.ts trailer
1228            // (generic <E> propagates `values` into the return), so it's
1229            // NOT in the macro-generated EditorAPI interface.
1230            "defineConfigStringArray",
1231            "reloadConfig",
1232            "reloadThemes",
1233            "reloadAndApplyTheme",
1234            "registerGrammar",
1235            "registerLanguageConfig",
1236            "registerLspServer",
1237            "reloadGrammars",
1238            "getConfigDir",
1239            "getDataDir",
1240            "getWorkingDataDir",
1241            "getTerminalDir",
1242            "getThemesDir",
1243            "applyTheme",
1244            "getThemeSchema",
1245            "getBuiltinThemes",
1246            "getAllThemes",
1247            "getThemeData",
1248            "saveThemeFile",
1249            "themeFileExists",
1250            "deleteTheme",
1251            "fileStat",
1252            "isProcessRunning",
1253            "killProcess",
1254            "pluginTranslate",
1255            "createCompositeBuffer",
1256            "updateCompositeAlignment",
1257            "closeCompositeBuffer",
1258            "flushLayout",
1259            "compositeNextHunk",
1260            "compositePrevHunk",
1261            "getHighlights",
1262            "addOverlay",
1263            "clearNamespace",
1264            "clearAllOverlays",
1265            "clearOverlaysInRange",
1266            "removeOverlay",
1267            "addConceal",
1268            "clearConcealNamespace",
1269            "clearConcealsInRange",
1270            "addSoftBreak",
1271            "clearSoftBreakNamespace",
1272            "clearSoftBreaksInRange",
1273            "submitViewTransform",
1274            "clearViewTransform",
1275            "setLayoutHints",
1276            "setFileExplorerDecorations",
1277            "clearFileExplorerDecorations",
1278            "addVirtualText",
1279            "removeVirtualText",
1280            "removeVirtualTextsByPrefix",
1281            "clearVirtualTexts",
1282            "clearVirtualTextNamespace",
1283            "addVirtualLine",
1284            "prompt",
1285            "startPrompt",
1286            "startPromptWithInitial",
1287            "setPromptSuggestions",
1288            "setPromptSelectedIndex",
1289            "setPromptInputSync",
1290            "defineMode",
1291            "setEditorMode",
1292            "getEditorMode",
1293            "closeSplit",
1294            "setSplitBuffer",
1295            "focusSplit",
1296            "setSplitScroll",
1297            "setSplitRatio",
1298            "setSplitLabel",
1299            "clearSplitLabel",
1300            "getSplitByLabel",
1301            "distributeSplitsEvenly",
1302            "setBufferCursor",
1303            "setLineIndicator",
1304            "clearLineIndicators",
1305            "setLineNumbers",
1306            "setViewMode",
1307            "setViewState",
1308            "getViewState",
1309            "setGlobalState",
1310            "getGlobalState",
1311            "setLineWrap",
1312            "createScrollSyncGroup",
1313            "setScrollSyncAnchors",
1314            "removeScrollSyncGroup",
1315            "executeActions",
1316            "showActionPopup",
1317            "setLspMenuContributions",
1318            "disableLspForLanguage",
1319            "setLspRootUri",
1320            "getAllDiagnostics",
1321            "getHandlers",
1322            "createVirtualBuffer",
1323            "createVirtualBufferInSplit",
1324            "createVirtualBufferInExistingSplit",
1325            "setVirtualBufferContent",
1326            "getTextPropertiesAtCursor",
1327            "spawnProcess",
1328            "spawnProcessWait",
1329            "spawnHostProcess",
1330            "httpFetch",
1331            "setAuthority",
1332            "clearAuthority",
1333            "setRemoteIndicatorState",
1334            "clearRemoteIndicatorState",
1335            "getBufferText",
1336            "delay",
1337            "sendLspRequest",
1338            "spawnBackgroundProcess",
1339            "killBackgroundProcess",
1340            "createTerminal",
1341            "sendTerminalInput",
1342            "closeTerminal",
1343            "signalWindow",
1344            "refreshLines",
1345            "getCurrentLocale",
1346            "loadPlugin",
1347            "unloadPlugin",
1348            "reloadPlugin",
1349            "listPlugins",
1350            "mountFloatingWidget",
1351            "updateFloatingWidget",
1352            "unmountFloatingWidget",
1353        ];
1354
1355        let mut missing = Vec::new();
1356        for method in &expected_methods {
1357            // Check that the method name appears followed by ( in the API
1358            let pattern = format!("{}(", method);
1359            if !api.contains(&pattern) {
1360                missing.push(*method);
1361            }
1362        }
1363
1364        assert!(
1365            missing.is_empty(),
1366            "Missing methods in EditorAPI interface: {:?}",
1367            missing
1368        );
1369    }
1370}