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