1use oxc_allocator::Allocator;
12use oxc_codegen::Codegen;
13use oxc_parser::Parser;
14use oxc_span::SourceType;
15use ts_rs::{Config as TsConfig, TS};
16
17use fresh_core::api::{
18 ActionPopupAction, ActionPopupOptions, ActionSpec, BackgroundProcessResult, BufferInfo,
19 BufferSavedDiff, CompositeHunk, CompositeLayoutConfig, CompositePaneStyle,
20 CompositeSourceConfig, CreateCompositeBufferOptions, CreateTerminalOptions,
21 CreateVirtualBufferInExistingSplitOptions, CreateVirtualBufferInSplitOptions,
22 CreateVirtualBufferOptions, CursorInfo, DirEntry, FormatterPackConfig, GrammarInfoSnapshot,
23 GrepMatch, JsDiagnostic, JsPosition, JsRange, JsTextPropertyEntry, LanguagePackConfig,
24 LayoutHints, LspServerPackConfig, OverlayColorSpec, OverlayOptions, ProcessLimitsPackConfig,
25 ReplaceResult, SpawnResult, TerminalResult, TextPropertiesAtCursor, TsHighlightSpan,
26 ViewTokenStyle, ViewTokenWire, ViewTokenWireKind, ViewportInfo, VirtualBufferResult,
27};
28use fresh_core::command::Suggestion;
29use fresh_core::file_explorer::FileExplorerDecoration;
30use fresh_core::text_property::InlineOverlay;
31
32fn get_type_decl(type_name: &str) -> Option<String> {
37 let cfg = TsConfig::default();
38 match type_name {
41 "BufferInfo" => Some(BufferInfo::decl(&cfg)),
43 "CursorInfo" => Some(CursorInfo::decl(&cfg)),
44 "ViewportInfo" => Some(ViewportInfo::decl(&cfg)),
45 "ActionSpec" => Some(ActionSpec::decl(&cfg)),
46 "BufferSavedDiff" => Some(BufferSavedDiff::decl(&cfg)),
47 "LayoutHints" => Some(LayoutHints::decl(&cfg)),
48
49 "SpawnResult" => Some(SpawnResult::decl(&cfg)),
51 "BackgroundProcessResult" => Some(BackgroundProcessResult::decl(&cfg)),
52
53 "GrepMatch" => Some(GrepMatch::decl(&cfg)),
55 "ReplaceResult" => Some(ReplaceResult::decl(&cfg)),
56
57 "TerminalResult" => Some(TerminalResult::decl(&cfg)),
59 "CreateTerminalOptions" => Some(CreateTerminalOptions::decl(&cfg)),
60
61 "TsCompositeLayoutConfig" | "CompositeLayoutConfig" => {
63 Some(CompositeLayoutConfig::decl(&cfg))
64 }
65 "TsCompositeSourceConfig" | "CompositeSourceConfig" => {
66 Some(CompositeSourceConfig::decl(&cfg))
67 }
68 "TsCompositePaneStyle" | "CompositePaneStyle" => Some(CompositePaneStyle::decl(&cfg)),
69 "TsCompositeHunk" | "CompositeHunk" => Some(CompositeHunk::decl(&cfg)),
70 "TsCreateCompositeBufferOptions" | "CreateCompositeBufferOptions" => {
71 Some(CreateCompositeBufferOptions::decl(&cfg))
72 }
73
74 "ViewTokenWireKind" => Some(ViewTokenWireKind::decl(&cfg)),
76 "ViewTokenStyle" => Some(ViewTokenStyle::decl(&cfg)),
77 "ViewTokenWire" => Some(ViewTokenWire::decl(&cfg)),
78
79 "TsActionPopupAction" | "ActionPopupAction" => Some(ActionPopupAction::decl(&cfg)),
81 "ActionPopupOptions" => Some(ActionPopupOptions::decl(&cfg)),
82 "TsHighlightSpan" => Some(TsHighlightSpan::decl(&cfg)),
83 "FileExplorerDecoration" => Some(FileExplorerDecoration::decl(&cfg)),
84
85 "TextPropertyEntry" | "JsTextPropertyEntry" => Some(JsTextPropertyEntry::decl(&cfg)),
87 "CreateVirtualBufferOptions" => Some(CreateVirtualBufferOptions::decl(&cfg)),
88 "CreateVirtualBufferInSplitOptions" => Some(CreateVirtualBufferInSplitOptions::decl(&cfg)),
89 "CreateVirtualBufferInExistingSplitOptions" => {
90 Some(CreateVirtualBufferInExistingSplitOptions::decl(&cfg))
91 }
92
93 "TextPropertiesAtCursor" => Some(TextPropertiesAtCursor::decl(&cfg)),
95 "VirtualBufferResult" => Some(VirtualBufferResult::decl(&cfg)),
96
97 "PromptSuggestion" | "Suggestion" => Some(Suggestion::decl(&cfg)),
99 "DirEntry" => Some(DirEntry::decl(&cfg)),
100
101 "JsDiagnostic" => Some(JsDiagnostic::decl(&cfg)),
103 "JsRange" => Some(JsRange::decl(&cfg)),
104 "JsPosition" => Some(JsPosition::decl(&cfg)),
105
106 "GrammarInfoSnapshot" => Some(GrammarInfoSnapshot::decl(&cfg)),
108
109 "LanguagePackConfig" => Some(LanguagePackConfig::decl(&cfg)),
111 "LspServerPackConfig" => Some(LspServerPackConfig::decl(&cfg)),
112 "ProcessLimitsPackConfig" => Some(ProcessLimitsPackConfig::decl(&cfg)),
113 "FormatterPackConfig" => Some(FormatterPackConfig::decl(&cfg)),
114
115 "OverlayOptions" => Some(OverlayOptions::decl(&cfg)),
117 "OverlayColorSpec" => Some(OverlayColorSpec::decl(&cfg)),
118 "InlineOverlay" => Some(InlineOverlay::decl(&cfg)),
119
120 "AuthorityPayload" => Some(AUTHORITY_PAYLOAD_DECL.to_string()),
126
127 "RemoteIndicatorStatePayload" => Some(REMOTE_INDICATOR_STATE_DECL.to_string()),
133
134 _ => None,
135 }
136}
137
138const AUTHORITY_PAYLOAD_DECL: &str = r#"type AuthorityFilesystem = { kind: "local" };
145
146type AuthoritySpawner =
147 | { kind: "local" }
148 | {
149 kind: "docker-exec";
150 container_id: string;
151 user?: string | null;
152 workspace?: string | null;
153 };
154
155type AuthorityTerminalWrapper =
156 | { kind: "host-shell" }
157 | {
158 kind: "explicit";
159 command: string;
160 args: string[];
161 manages_cwd?: boolean;
162 };
163
164type AuthorityPayload = {
165 filesystem: AuthorityFilesystem;
166 spawner: AuthoritySpawner;
167 terminal_wrapper: AuthorityTerminalWrapper;
168 display_label?: string;
169};"#;
170
171const REMOTE_INDICATOR_STATE_DECL: &str = r#"type RemoteIndicatorStatePayload =
176 | { kind: "local" }
177 | { kind: "connecting"; label?: string | null }
178 | { kind: "connected"; label?: string | null }
179 | { kind: "failed_attach"; error?: string | null }
180 | { kind: "disconnected"; label?: string | null };"#;
181
182const DEPENDENCY_TYPES: &[&str] = &[
186 "TextPropertyEntry", "TsCompositeLayoutConfig", "TsCompositeSourceConfig", "TsCompositePaneStyle", "TsCompositeHunk", "TsCreateCompositeBufferOptions", "ViewportInfo", "LayoutHints", "ViewTokenWire", "ViewTokenWireKind", "ViewTokenStyle", "PromptSuggestion", "DirEntry", "BufferInfo", "JsDiagnostic", "JsRange", "JsPosition", "ActionSpec", "TsActionPopupAction", "ActionPopupOptions", "FileExplorerDecoration", "FormatterPackConfig", "ProcessLimitsPackConfig", "TerminalResult", "CreateTerminalOptions", "CursorInfo", "OverlayOptions", "OverlayColorSpec", "InlineOverlay", "GrammarInfoSnapshot", ];
217
218pub fn collect_ts_types() -> String {
223 use crate::backend::quickjs_backend::JSEDITORAPI_REFERENCED_TYPES;
224
225 let mut types = Vec::new();
226 let mut included_decls = std::collections::HashSet::new();
229
230 for type_name in DEPENDENCY_TYPES {
232 if let Some(decl) = get_type_decl(type_name) {
233 if included_decls.insert(decl.clone()) {
234 types.push(decl);
235 }
236 }
237 }
238
239 for type_name in JSEDITORAPI_REFERENCED_TYPES {
241 if let Some(decl) = get_type_decl(type_name) {
242 if included_decls.insert(decl.clone()) {
243 types.push(decl);
244 }
245 } else {
246 eprintln!(
248 "Warning: Type '{}' is referenced in API but not registered in get_type_decl()",
249 type_name
250 );
251 }
252 }
253
254 types.join("\n\n")
255}
256
257pub fn validate_typescript(source: &str) -> Result<(), String> {
261 let allocator = Allocator::default();
262 let source_type = SourceType::d_ts();
263
264 let parser_ret = Parser::new(&allocator, source, source_type).parse();
265
266 if parser_ret.errors.is_empty() {
267 Ok(())
268 } else {
269 let errors: Vec<String> = parser_ret
270 .errors
271 .iter()
272 .map(|e: &oxc_diagnostics::OxcDiagnostic| e.to_string())
273 .collect();
274 Err(format!("TypeScript parse errors:\n{}", errors.join("\n")))
275 }
276}
277
278pub fn format_typescript(source: &str) -> String {
283 let allocator = Allocator::default();
284 let source_type = SourceType::d_ts();
285
286 let parser_ret = Parser::new(&allocator, source, source_type).parse();
287
288 if !parser_ret.errors.is_empty() {
289 return source.to_string();
291 }
292
293 Codegen::new().build(&parser_ret.program).code
295}
296
297pub fn write_fresh_dts() -> Result<(), String> {
302 use crate::backend::quickjs_backend::{JSEDITORAPI_TS_EDITOR_API, JSEDITORAPI_TS_PREAMBLE};
303
304 let ts_types = collect_ts_types();
305
306 let plugin_api_trailer = r#"
315
316/**
317 * Typed overload of `editor.getPluginApi`. When the caller passes a
318 * key that some loaded plugin declared in `FreshPluginRegistry`, the
319 * return type is narrowed to that plugin's API. Unknown names fall
320 * through to the untyped `unknown | null` signature.
321 */
322interface EditorAPI {
323 getPluginApi<K extends keyof FreshPluginRegistry>(name: K): FreshPluginRegistry[K] | null;
324}
325"#;
326
327 let content = format!(
328 "{}\n{}\n{}{}",
329 JSEDITORAPI_TS_PREAMBLE, ts_types, JSEDITORAPI_TS_EDITOR_API, plugin_api_trailer
330 );
331
332 validate_typescript(&content)?;
334
335 let formatted = format_typescript(&content);
337
338 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
340 let output_path = std::path::Path::new(&manifest_dir)
341 .parent() .and_then(|p| p.parent()) .map(|p| p.join("crates/fresh-editor/plugins/lib/fresh.d.ts"))
344 .unwrap_or_else(|| std::path::PathBuf::from("plugins/lib/fresh.d.ts"));
345
346 let should_write = match std::fs::read_to_string(&output_path) {
348 Ok(existing) => existing != formatted,
349 Err(_) => true,
350 };
351
352 if should_write {
353 if let Some(parent) = output_path.parent() {
354 std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
355 }
356 std::fs::write(&output_path, &formatted).map_err(|e| e.to_string())?;
357 }
358
359 Ok(())
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 #[test]
369 #[ignore]
370 fn write_fresh_dts_file() {
371 write_fresh_dts().expect("Failed to write fresh.d.ts");
373 println!("Successfully generated, validated, and formatted fresh.d.ts");
374 }
375
376 #[test]
380 #[ignore]
381 fn type_check_plugins() {
382 let tsc_check = std::process::Command::new("tsc").arg("--version").output();
384
385 match tsc_check {
386 Ok(output) if output.status.success() => {
387 println!(
388 "Found tsc: {}",
389 String::from_utf8_lossy(&output.stdout).trim()
390 );
391 }
392 _ => {
393 println!("tsc not found in PATH, skipping type check test");
394 return;
395 }
396 }
397
398 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
400 let script_path = std::path::Path::new(&manifest_dir)
401 .parent()
402 .and_then(|p| p.parent())
403 .map(|p| p.join("crates/fresh-editor/plugins/check-types.sh"))
404 .expect("Failed to find check-types.sh");
405
406 println!("Running type check script: {}", script_path.display());
407
408 let output = std::process::Command::new("bash")
410 .arg(&script_path)
411 .output()
412 .expect("Failed to run check-types.sh");
413
414 let stdout = String::from_utf8_lossy(&output.stdout);
415 let stderr = String::from_utf8_lossy(&output.stderr);
416
417 println!("stdout:\n{}", stdout);
418 if !stderr.is_empty() {
419 println!("stderr:\n{}", stderr);
420 }
421
422 if stdout.contains("had type errors") || !output.status.success() {
424 panic!(
425 "TypeScript type check failed. Run 'crates/fresh-editor/plugins/check-types.sh' to see details."
426 );
427 }
428
429 println!("All plugins type check successfully!");
430 }
431
432 #[test]
437 fn test_get_type_decl_returns_all_expected_types() {
438 let expected_types = vec![
439 "BufferInfo",
440 "CursorInfo",
441 "ViewportInfo",
442 "ActionSpec",
443 "BufferSavedDiff",
444 "LayoutHints",
445 "SpawnResult",
446 "BackgroundProcessResult",
447 "TerminalResult",
448 "CreateTerminalOptions",
449 "TsCompositeLayoutConfig",
450 "TsCompositeSourceConfig",
451 "TsCompositePaneStyle",
452 "TsCompositeHunk",
453 "TsCreateCompositeBufferOptions",
454 "ViewTokenWireKind",
455 "ViewTokenStyle",
456 "ViewTokenWire",
457 "TsActionPopupAction",
458 "ActionPopupOptions",
459 "TsHighlightSpan",
460 "FileExplorerDecoration",
461 "TextPropertyEntry",
462 "CreateVirtualBufferOptions",
463 "CreateVirtualBufferInSplitOptions",
464 "CreateVirtualBufferInExistingSplitOptions",
465 "TextPropertiesAtCursor",
466 "VirtualBufferResult",
467 "PromptSuggestion",
468 "DirEntry",
469 "JsDiagnostic",
470 "JsRange",
471 "JsPosition",
472 "LanguagePackConfig",
473 "LspServerPackConfig",
474 "ProcessLimitsPackConfig",
475 "FormatterPackConfig",
476 ];
477
478 for type_name in &expected_types {
479 assert!(
480 get_type_decl(type_name).is_some(),
481 "get_type_decl should return a declaration for '{}'",
482 type_name
483 );
484 }
485 }
486
487 #[test]
488 fn test_get_type_decl_aliases_resolve_same() {
489 let alias_pairs = vec![
491 ("CompositeHunk", "TsCompositeHunk"),
492 ("CompositeLayoutConfig", "TsCompositeLayoutConfig"),
493 ("CompositeSourceConfig", "TsCompositeSourceConfig"),
494 ("CompositePaneStyle", "TsCompositePaneStyle"),
495 (
496 "CreateCompositeBufferOptions",
497 "TsCreateCompositeBufferOptions",
498 ),
499 ("ActionPopupAction", "TsActionPopupAction"),
500 ("Suggestion", "PromptSuggestion"),
501 ("JsTextPropertyEntry", "TextPropertyEntry"),
502 ];
503
504 for (rust_name, ts_name) in &alias_pairs {
505 let rust_decl = get_type_decl(rust_name);
506 let ts_decl = get_type_decl(ts_name);
507 assert!(
508 rust_decl.is_some(),
509 "get_type_decl should handle Rust name '{}'",
510 rust_name
511 );
512 assert_eq!(
513 rust_decl, ts_decl,
514 "Alias '{}' and '{}' should produce identical declarations",
515 rust_name, ts_name
516 );
517 }
518 }
519
520 #[test]
521 fn test_terminal_types_exist() {
522 let terminal_result = get_type_decl("TerminalResult");
523 assert!(
524 terminal_result.is_some(),
525 "TerminalResult should be defined"
526 );
527 let decl = terminal_result.unwrap();
528 assert!(
529 decl.contains("bufferId"),
530 "TerminalResult should have bufferId field"
531 );
532 assert!(
533 decl.contains("terminalId"),
534 "TerminalResult should have terminalId field"
535 );
536 assert!(
537 decl.contains("splitId"),
538 "TerminalResult should have splitId field"
539 );
540
541 let terminal_opts = get_type_decl("CreateTerminalOptions");
542 assert!(
543 terminal_opts.is_some(),
544 "CreateTerminalOptions should be defined"
545 );
546 }
547
548 #[test]
549 fn test_cursor_info_type_exists() {
550 let cursor_info = get_type_decl("CursorInfo");
551 assert!(cursor_info.is_some(), "CursorInfo should be defined");
552 let decl = cursor_info.unwrap();
553 assert!(
554 decl.contains("position"),
555 "CursorInfo should have position field"
556 );
557 assert!(
558 decl.contains("selection"),
559 "CursorInfo should have selection field"
560 );
561 }
562
563 #[test]
564 fn test_collect_ts_types_no_duplicates() {
565 let output = collect_ts_types();
566 let lines: Vec<&str> = output.lines().collect();
567
568 let mut declarations = std::collections::HashSet::new();
570 for line in &lines {
571 let trimmed = line.trim();
572 if trimmed.starts_with("type ") && trimmed.contains('=') {
574 let name = trimmed
575 .strip_prefix("type ")
576 .unwrap()
577 .split(|c: char| c == '=' || c.is_whitespace())
578 .next()
579 .unwrap();
580 assert!(
581 declarations.insert(name.to_string()),
582 "Duplicate type declaration found: '{}'",
583 name
584 );
585 }
586 }
587 }
588
589 #[test]
590 fn test_collect_ts_types_includes_dependency_types() {
591 let output = collect_ts_types();
592 let required_types = [
593 "TextPropertyEntry",
594 "TsCompositeLayoutConfig",
595 "TsCompositeSourceConfig",
596 "TsCompositePaneStyle",
597 "TsCompositeHunk",
598 "TsCreateCompositeBufferOptions",
599 "PromptSuggestion",
600 "BufferInfo",
601 "CursorInfo",
602 "TerminalResult",
603 "CreateTerminalOptions",
604 ];
605
606 for type_name in &required_types {
607 assert!(
608 output.contains(type_name),
609 "collect_ts_types output should contain type '{}'",
610 type_name
611 );
612 }
613 }
614
615 #[test]
616 fn test_generated_dts_validates_as_typescript() {
617 use crate::backend::quickjs_backend::{JSEDITORAPI_TS_EDITOR_API, JSEDITORAPI_TS_PREAMBLE};
618
619 let ts_types = collect_ts_types();
620 let content = format!(
621 "{}\n{}\n{}",
622 JSEDITORAPI_TS_PREAMBLE, ts_types, JSEDITORAPI_TS_EDITOR_API
623 );
624
625 validate_typescript(&content).expect("Generated TypeScript should be syntactically valid");
626 }
627
628 #[test]
629 fn test_generated_dts_no_undefined_type_references() {
630 use crate::backend::quickjs_backend::{JSEDITORAPI_TS_EDITOR_API, JSEDITORAPI_TS_PREAMBLE};
631
632 let ts_types = collect_ts_types();
633 let content = format!(
634 "{}\n{}\n{}",
635 JSEDITORAPI_TS_PREAMBLE, ts_types, JSEDITORAPI_TS_EDITOR_API
636 );
637
638 let mut defined_types = std::collections::HashSet::new();
640 for builtin in &[
642 "number",
643 "string",
644 "boolean",
645 "void",
646 "unknown",
647 "null",
648 "undefined",
649 "Record",
650 "Array",
651 "Promise",
652 "ProcessHandle",
653 "PromiseLike",
654 "BufferId",
655 "SplitId",
656 "EditorAPI",
657 ] {
658 defined_types.insert(builtin.to_string());
659 }
660
661 for line in content.lines() {
663 let trimmed = line.trim();
664 if trimmed.starts_with("type ") && trimmed.contains('=') {
665 if let Some(name) = trimmed
666 .strip_prefix("type ")
667 .unwrap()
668 .split(|c: char| c == '=' || c.is_whitespace())
669 .next()
670 {
671 defined_types.insert(name.to_string());
672 }
673 }
674 if trimmed.starts_with("interface ") {
675 if let Some(name) = trimmed
676 .strip_prefix("interface ")
677 .unwrap()
678 .split(|c: char| !c.is_alphanumeric() && c != '_')
679 .next()
680 {
681 defined_types.insert(name.to_string());
682 }
683 }
684 }
685
686 let interface_section = JSEDITORAPI_TS_EDITOR_API;
689 let mut undefined_refs = Vec::new();
690
691 for line in interface_section.lines() {
692 let trimmed = line.trim();
693
694 if trimmed.starts_with('*')
696 || trimmed.starts_with("/*")
697 || trimmed.starts_with("//")
698 || trimmed.is_empty()
699 || trimmed == "{"
700 || trimmed == "}"
701 {
702 continue;
703 }
704
705 for word in trimmed.split(|c: char| !c.is_alphanumeric() && c != '_') {
707 if word.is_empty() {
708 continue;
709 }
710 if word.chars().next().is_some_and(|c| c.is_uppercase())
712 && !defined_types.contains(word)
713 {
714 undefined_refs.push(word.to_string());
715 }
716 }
717 }
718
719 undefined_refs.sort();
721 undefined_refs.dedup();
722
723 assert!(
724 undefined_refs.is_empty(),
725 "Found undefined type references in EditorAPI interface: {:?}",
726 undefined_refs
727 );
728 }
729
730 #[test]
731 fn test_editor_api_cursor_methods_have_typed_returns() {
732 use crate::backend::quickjs_backend::JSEDITORAPI_TS_EDITOR_API;
733
734 let api = JSEDITORAPI_TS_EDITOR_API;
735
736 assert!(
738 api.contains("getPrimaryCursor(): CursorInfo | null;"),
739 "getPrimaryCursor should return CursorInfo | null, got: {}",
740 api.lines()
741 .find(|l| l.contains("getPrimaryCursor"))
742 .unwrap_or("not found")
743 );
744
745 assert!(
747 api.contains("getAllCursors(): CursorInfo[];"),
748 "getAllCursors should return CursorInfo[], got: {}",
749 api.lines()
750 .find(|l| l.contains("getAllCursors"))
751 .unwrap_or("not found")
752 );
753
754 assert!(
756 api.contains("getAllCursorPositions(): number[];"),
757 "getAllCursorPositions should return number[], got: {}",
758 api.lines()
759 .find(|l| l.contains("getAllCursorPositions"))
760 .unwrap_or("not found")
761 );
762 }
763
764 #[test]
765 fn test_editor_api_terminal_methods_use_defined_types() {
766 use crate::backend::quickjs_backend::JSEDITORAPI_TS_EDITOR_API;
767
768 let api = JSEDITORAPI_TS_EDITOR_API;
769
770 assert!(
772 api.contains("CreateTerminalOptions"),
773 "createTerminal should reference CreateTerminalOptions"
774 );
775 assert!(
776 api.contains("TerminalResult"),
777 "createTerminal should reference TerminalResult"
778 );
779 }
780
781 #[test]
782 fn test_editor_api_composite_methods_use_ts_prefix_types() {
783 use crate::backend::quickjs_backend::JSEDITORAPI_TS_EDITOR_API;
784
785 let api = JSEDITORAPI_TS_EDITOR_API;
786
787 assert!(
789 api.contains("TsCompositeHunk[]"),
790 "updateCompositeAlignment should use TsCompositeHunk[], not CompositeHunk[]"
791 );
792
793 assert!(
795 api.contains("TsCreateCompositeBufferOptions"),
796 "createCompositeBuffer should use TsCreateCompositeBufferOptions"
797 );
798 }
799
800 #[test]
801 fn test_editor_api_prompt_suggestions_use_prompt_suggestion() {
802 use crate::backend::quickjs_backend::JSEDITORAPI_TS_EDITOR_API;
803
804 let api = JSEDITORAPI_TS_EDITOR_API;
805
806 assert!(
808 api.contains("PromptSuggestion[]"),
809 "setPromptSuggestions should use PromptSuggestion[], not Suggestion[]"
810 );
811 }
812
813 #[test]
814 fn test_all_editor_api_methods_present() {
815 use crate::backend::quickjs_backend::JSEDITORAPI_TS_EDITOR_API;
816
817 let api = JSEDITORAPI_TS_EDITOR_API;
818
819 let expected_methods = vec![
821 "apiVersion",
822 "getActiveBufferId",
823 "getActiveSplitId",
824 "listBuffers",
825 "debug",
826 "info",
827 "warn",
828 "error",
829 "setStatus",
830 "copyToClipboard",
831 "setClipboard",
832 "registerCommand",
833 "unregisterCommand",
834 "setContext",
835 "executeAction",
836 "getCursorPosition",
837 "getBufferPath",
838 "getBufferLength",
839 "isBufferModified",
840 "saveBufferToPath",
841 "getBufferInfo",
842 "getPrimaryCursor",
843 "getAllCursors",
844 "getAllCursorPositions",
845 "getViewport",
846 "getCursorLine",
847 "getLineStartPosition",
848 "getLineEndPosition",
849 "getBufferLineCount",
850 "scrollToLineCenter",
851 "findBufferByPath",
852 "getBufferSavedDiff",
853 "insertText",
854 "deleteRange",
855 "insertAtCursor",
856 "openFile",
857 "openFileInSplit",
858 "showBuffer",
859 "closeBuffer",
860 "on",
861 "off",
862 "getEnv",
863 "getCwd",
864 "pathJoin",
865 "pathDirname",
866 "pathBasename",
867 "pathExtname",
868 "pathIsAbsolute",
869 "utf8ByteLength",
870 "fileExists",
871 "readFile",
872 "writeFile",
873 "readDir",
874 "createDir",
875 "removePath",
876 "renamePath",
877 "copyPath",
878 "getTempDir",
879 "getConfig",
880 "getUserConfig",
881 "reloadConfig",
882 "reloadThemes",
883 "reloadAndApplyTheme",
884 "registerGrammar",
885 "registerLanguageConfig",
886 "registerLspServer",
887 "reloadGrammars",
888 "getConfigDir",
889 "getDataDir",
890 "getThemesDir",
891 "applyTheme",
892 "getThemeSchema",
893 "getBuiltinThemes",
894 "getThemeData",
895 "saveThemeFile",
896 "themeFileExists",
897 "deleteTheme",
898 "fileStat",
899 "isProcessRunning",
900 "killProcess",
901 "pluginTranslate",
902 "createCompositeBuffer",
903 "updateCompositeAlignment",
904 "closeCompositeBuffer",
905 "flushLayout",
906 "compositeNextHunk",
907 "compositePrevHunk",
908 "getHighlights",
909 "addOverlay",
910 "clearNamespace",
911 "clearAllOverlays",
912 "clearOverlaysInRange",
913 "removeOverlay",
914 "addConceal",
915 "clearConcealNamespace",
916 "clearConcealsInRange",
917 "addSoftBreak",
918 "clearSoftBreakNamespace",
919 "clearSoftBreaksInRange",
920 "submitViewTransform",
921 "clearViewTransform",
922 "setLayoutHints",
923 "setFileExplorerDecorations",
924 "clearFileExplorerDecorations",
925 "addVirtualText",
926 "removeVirtualText",
927 "removeVirtualTextsByPrefix",
928 "clearVirtualTexts",
929 "clearVirtualTextNamespace",
930 "addVirtualLine",
931 "prompt",
932 "startPrompt",
933 "startPromptWithInitial",
934 "setPromptSuggestions",
935 "setPromptInputSync",
936 "defineMode",
937 "setEditorMode",
938 "getEditorMode",
939 "closeSplit",
940 "setSplitBuffer",
941 "focusSplit",
942 "setSplitScroll",
943 "setSplitRatio",
944 "setSplitLabel",
945 "clearSplitLabel",
946 "getSplitByLabel",
947 "distributeSplitsEvenly",
948 "setBufferCursor",
949 "setLineIndicator",
950 "clearLineIndicators",
951 "setLineNumbers",
952 "setViewMode",
953 "setViewState",
954 "getViewState",
955 "setGlobalState",
956 "getGlobalState",
957 "setLineWrap",
958 "createScrollSyncGroup",
959 "setScrollSyncAnchors",
960 "removeScrollSyncGroup",
961 "executeActions",
962 "showActionPopup",
963 "disableLspForLanguage",
964 "setLspRootUri",
965 "getAllDiagnostics",
966 "getHandlers",
967 "createVirtualBuffer",
968 "createVirtualBufferInSplit",
969 "createVirtualBufferInExistingSplit",
970 "setVirtualBufferContent",
971 "getTextPropertiesAtCursor",
972 "spawnProcess",
973 "spawnProcessWait",
974 "spawnHostProcess",
975 "setAuthority",
976 "clearAuthority",
977 "setRemoteIndicatorState",
978 "clearRemoteIndicatorState",
979 "getBufferText",
980 "delay",
981 "sendLspRequest",
982 "spawnBackgroundProcess",
983 "killBackgroundProcess",
984 "createTerminal",
985 "sendTerminalInput",
986 "closeTerminal",
987 "refreshLines",
988 "getCurrentLocale",
989 "loadPlugin",
990 "unloadPlugin",
991 "reloadPlugin",
992 "listPlugins",
993 ];
994
995 let mut missing = Vec::new();
996 for method in &expected_methods {
997 let pattern = format!("{}(", method);
999 if !api.contains(&pattern) {
1000 missing.push(*method);
1001 }
1002 }
1003
1004 assert!(
1005 missing.is_empty(),
1006 "Missing methods in EditorAPI interface: {:?}",
1007 missing
1008 );
1009 }
1010}