1use oxc_allocator::Allocator;
12use oxc_codegen::Codegen;
13use oxc_parser::Parser;
14use oxc_span::SourceType;
15use ts_rs::TS;
16
17use fresh_core::api::{
18 ActionPopupAction, ActionPopupOptions, ActionSpec, BackgroundProcessResult, BufferInfo,
19 BufferSavedDiff, CompositeHunk, CompositeLayoutConfig, CompositePaneStyle,
20 CompositeSourceConfig, CreateCompositeBufferOptions, CreateTerminalOptions,
21 CreateVirtualBufferInExistingSplitOptions, CreateVirtualBufferInSplitOptions,
22 CreateVirtualBufferOptions, CursorInfo, DirEntry, FormatterPackConfig, JsDiagnostic,
23 JsPosition, JsRange, JsTextPropertyEntry, LanguagePackConfig, LayoutHints, LspServerPackConfig,
24 SpawnResult, TerminalResult, TextPropertiesAtCursor, TsHighlightSpan, ViewTokenStyle,
25 ViewTokenWire, ViewTokenWireKind, ViewportInfo, VirtualBufferResult,
26};
27use fresh_core::command::Suggestion;
28use fresh_core::file_explorer::FileExplorerDecoration;
29
30fn get_type_decl(type_name: &str) -> Option<String> {
35 match type_name {
38 "BufferInfo" => Some(BufferInfo::decl()),
40 "CursorInfo" => Some(CursorInfo::decl()),
41 "ViewportInfo" => Some(ViewportInfo::decl()),
42 "ActionSpec" => Some(ActionSpec::decl()),
43 "BufferSavedDiff" => Some(BufferSavedDiff::decl()),
44 "LayoutHints" => Some(LayoutHints::decl()),
45
46 "SpawnResult" => Some(SpawnResult::decl()),
48 "BackgroundProcessResult" => Some(BackgroundProcessResult::decl()),
49
50 "TerminalResult" => Some(TerminalResult::decl()),
52 "CreateTerminalOptions" => Some(CreateTerminalOptions::decl()),
53
54 "TsCompositeLayoutConfig" | "CompositeLayoutConfig" => Some(CompositeLayoutConfig::decl()),
56 "TsCompositeSourceConfig" | "CompositeSourceConfig" => Some(CompositeSourceConfig::decl()),
57 "TsCompositePaneStyle" | "CompositePaneStyle" => Some(CompositePaneStyle::decl()),
58 "TsCompositeHunk" | "CompositeHunk" => Some(CompositeHunk::decl()),
59 "TsCreateCompositeBufferOptions" | "CreateCompositeBufferOptions" => {
60 Some(CreateCompositeBufferOptions::decl())
61 }
62
63 "ViewTokenWireKind" => Some(ViewTokenWireKind::decl()),
65 "ViewTokenStyle" => Some(ViewTokenStyle::decl()),
66 "ViewTokenWire" => Some(ViewTokenWire::decl()),
67
68 "TsActionPopupAction" | "ActionPopupAction" => Some(ActionPopupAction::decl()),
70 "ActionPopupOptions" => Some(ActionPopupOptions::decl()),
71 "TsHighlightSpan" => Some(TsHighlightSpan::decl()),
72 "FileExplorerDecoration" => Some(FileExplorerDecoration::decl()),
73
74 "TextPropertyEntry" | "JsTextPropertyEntry" => Some(JsTextPropertyEntry::decl()),
76 "CreateVirtualBufferOptions" => Some(CreateVirtualBufferOptions::decl()),
77 "CreateVirtualBufferInSplitOptions" => Some(CreateVirtualBufferInSplitOptions::decl()),
78 "CreateVirtualBufferInExistingSplitOptions" => {
79 Some(CreateVirtualBufferInExistingSplitOptions::decl())
80 }
81
82 "TextPropertiesAtCursor" => Some(TextPropertiesAtCursor::decl()),
84 "VirtualBufferResult" => Some(VirtualBufferResult::decl()),
85
86 "PromptSuggestion" | "Suggestion" => Some(Suggestion::decl()),
88 "DirEntry" => Some(DirEntry::decl()),
89
90 "JsDiagnostic" => Some(JsDiagnostic::decl()),
92 "JsRange" => Some(JsRange::decl()),
93 "JsPosition" => Some(JsPosition::decl()),
94
95 "LanguagePackConfig" => Some(LanguagePackConfig::decl()),
97 "LspServerPackConfig" => Some(LspServerPackConfig::decl()),
98 "FormatterPackConfig" => Some(FormatterPackConfig::decl()),
99
100 _ => None,
101 }
102}
103
104const DEPENDENCY_TYPES: &[&str] = &[
108 "TextPropertyEntry", "TsCompositeLayoutConfig", "TsCompositeSourceConfig", "TsCompositePaneStyle", "TsCompositeHunk", "TsCreateCompositeBufferOptions", "ViewportInfo", "LayoutHints", "ViewTokenWire", "ViewTokenWireKind", "ViewTokenStyle", "PromptSuggestion", "DirEntry", "BufferInfo", "JsDiagnostic", "JsRange", "JsPosition", "ActionSpec", "TsActionPopupAction", "ActionPopupOptions", "FileExplorerDecoration", "FormatterPackConfig", "TerminalResult", "CreateTerminalOptions", "CursorInfo", ];
134
135pub fn collect_ts_types() -> String {
140 use crate::backend::quickjs_backend::JSEDITORAPI_REFERENCED_TYPES;
141
142 let mut types = Vec::new();
143 let mut included_decls = std::collections::HashSet::new();
146
147 for type_name in DEPENDENCY_TYPES {
149 if let Some(decl) = get_type_decl(type_name) {
150 if included_decls.insert(decl.clone()) {
151 types.push(decl);
152 }
153 }
154 }
155
156 for type_name in JSEDITORAPI_REFERENCED_TYPES {
158 if let Some(decl) = get_type_decl(type_name) {
159 if included_decls.insert(decl.clone()) {
160 types.push(decl);
161 }
162 } else {
163 eprintln!(
165 "Warning: Type '{}' is referenced in API but not registered in get_type_decl()",
166 type_name
167 );
168 }
169 }
170
171 types.join("\n\n")
172}
173
174pub fn validate_typescript(source: &str) -> Result<(), String> {
178 let allocator = Allocator::default();
179 let source_type = SourceType::d_ts();
180
181 let parser_ret = Parser::new(&allocator, source, source_type).parse();
182
183 if parser_ret.errors.is_empty() {
184 Ok(())
185 } else {
186 let errors: Vec<String> = parser_ret
187 .errors
188 .iter()
189 .map(|e: &oxc_diagnostics::OxcDiagnostic| e.to_string())
190 .collect();
191 Err(format!("TypeScript parse errors:\n{}", errors.join("\n")))
192 }
193}
194
195pub fn format_typescript(source: &str) -> String {
200 let allocator = Allocator::default();
201 let source_type = SourceType::d_ts();
202
203 let parser_ret = Parser::new(&allocator, source, source_type).parse();
204
205 if !parser_ret.errors.is_empty() {
206 return source.to_string();
208 }
209
210 Codegen::new().build(&parser_ret.program).code
212}
213
214pub fn write_fresh_dts() -> Result<(), String> {
219 use crate::backend::quickjs_backend::{JSEDITORAPI_TS_EDITOR_API, JSEDITORAPI_TS_PREAMBLE};
220
221 let ts_types = collect_ts_types();
222
223 let content = format!(
224 "{}\n{}\n{}",
225 JSEDITORAPI_TS_PREAMBLE, ts_types, JSEDITORAPI_TS_EDITOR_API
226 );
227
228 validate_typescript(&content)?;
230
231 let formatted = format_typescript(&content);
233
234 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
236 let output_path = std::path::Path::new(&manifest_dir)
237 .parent() .and_then(|p| p.parent()) .map(|p| p.join("crates/fresh-editor/plugins/lib/fresh.d.ts"))
240 .unwrap_or_else(|| std::path::PathBuf::from("plugins/lib/fresh.d.ts"));
241
242 let should_write = match std::fs::read_to_string(&output_path) {
244 Ok(existing) => existing != formatted,
245 Err(_) => true,
246 };
247
248 if should_write {
249 if let Some(parent) = output_path.parent() {
250 std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
251 }
252 std::fs::write(&output_path, &formatted).map_err(|e| e.to_string())?;
253 }
254
255 Ok(())
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
265 #[ignore]
266 fn write_fresh_dts_file() {
267 write_fresh_dts().expect("Failed to write fresh.d.ts");
269 println!("Successfully generated, validated, and formatted fresh.d.ts");
270 }
271
272 #[test]
276 #[ignore]
277 fn type_check_plugins() {
278 let tsc_check = std::process::Command::new("tsc").arg("--version").output();
280
281 match tsc_check {
282 Ok(output) if output.status.success() => {
283 println!(
284 "Found tsc: {}",
285 String::from_utf8_lossy(&output.stdout).trim()
286 );
287 }
288 _ => {
289 println!("tsc not found in PATH, skipping type check test");
290 return;
291 }
292 }
293
294 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
296 let script_path = std::path::Path::new(&manifest_dir)
297 .parent()
298 .and_then(|p| p.parent())
299 .map(|p| p.join("crates/fresh-editor/plugins/check-types.sh"))
300 .expect("Failed to find check-types.sh");
301
302 println!("Running type check script: {}", script_path.display());
303
304 let output = std::process::Command::new("bash")
306 .arg(&script_path)
307 .output()
308 .expect("Failed to run check-types.sh");
309
310 let stdout = String::from_utf8_lossy(&output.stdout);
311 let stderr = String::from_utf8_lossy(&output.stderr);
312
313 println!("stdout:\n{}", stdout);
314 if !stderr.is_empty() {
315 println!("stderr:\n{}", stderr);
316 }
317
318 if stdout.contains("had type errors") || !output.status.success() {
320 panic!(
321 "TypeScript type check failed. Run 'crates/fresh-editor/plugins/check-types.sh' to see details."
322 );
323 }
324
325 println!("All plugins type check successfully!");
326 }
327
328 #[test]
333 fn test_get_type_decl_returns_all_expected_types() {
334 let expected_types = vec![
335 "BufferInfo",
336 "CursorInfo",
337 "ViewportInfo",
338 "ActionSpec",
339 "BufferSavedDiff",
340 "LayoutHints",
341 "SpawnResult",
342 "BackgroundProcessResult",
343 "TerminalResult",
344 "CreateTerminalOptions",
345 "TsCompositeLayoutConfig",
346 "TsCompositeSourceConfig",
347 "TsCompositePaneStyle",
348 "TsCompositeHunk",
349 "TsCreateCompositeBufferOptions",
350 "ViewTokenWireKind",
351 "ViewTokenStyle",
352 "ViewTokenWire",
353 "TsActionPopupAction",
354 "ActionPopupOptions",
355 "TsHighlightSpan",
356 "FileExplorerDecoration",
357 "TextPropertyEntry",
358 "CreateVirtualBufferOptions",
359 "CreateVirtualBufferInSplitOptions",
360 "CreateVirtualBufferInExistingSplitOptions",
361 "TextPropertiesAtCursor",
362 "VirtualBufferResult",
363 "PromptSuggestion",
364 "DirEntry",
365 "JsDiagnostic",
366 "JsRange",
367 "JsPosition",
368 "LanguagePackConfig",
369 "LspServerPackConfig",
370 "FormatterPackConfig",
371 ];
372
373 for type_name in &expected_types {
374 assert!(
375 get_type_decl(type_name).is_some(),
376 "get_type_decl should return a declaration for '{}'",
377 type_name
378 );
379 }
380 }
381
382 #[test]
383 fn test_get_type_decl_aliases_resolve_same() {
384 let alias_pairs = vec![
386 ("CompositeHunk", "TsCompositeHunk"),
387 ("CompositeLayoutConfig", "TsCompositeLayoutConfig"),
388 ("CompositeSourceConfig", "TsCompositeSourceConfig"),
389 ("CompositePaneStyle", "TsCompositePaneStyle"),
390 (
391 "CreateCompositeBufferOptions",
392 "TsCreateCompositeBufferOptions",
393 ),
394 ("ActionPopupAction", "TsActionPopupAction"),
395 ("Suggestion", "PromptSuggestion"),
396 ("JsTextPropertyEntry", "TextPropertyEntry"),
397 ];
398
399 for (rust_name, ts_name) in &alias_pairs {
400 let rust_decl = get_type_decl(rust_name);
401 let ts_decl = get_type_decl(ts_name);
402 assert!(
403 rust_decl.is_some(),
404 "get_type_decl should handle Rust name '{}'",
405 rust_name
406 );
407 assert_eq!(
408 rust_decl, ts_decl,
409 "Alias '{}' and '{}' should produce identical declarations",
410 rust_name, ts_name
411 );
412 }
413 }
414
415 #[test]
416 fn test_terminal_types_exist() {
417 let terminal_result = get_type_decl("TerminalResult");
418 assert!(
419 terminal_result.is_some(),
420 "TerminalResult should be defined"
421 );
422 let decl = terminal_result.unwrap();
423 assert!(
424 decl.contains("bufferId"),
425 "TerminalResult should have bufferId field"
426 );
427 assert!(
428 decl.contains("terminalId"),
429 "TerminalResult should have terminalId field"
430 );
431 assert!(
432 decl.contains("splitId"),
433 "TerminalResult should have splitId field"
434 );
435
436 let terminal_opts = get_type_decl("CreateTerminalOptions");
437 assert!(
438 terminal_opts.is_some(),
439 "CreateTerminalOptions should be defined"
440 );
441 }
442
443 #[test]
444 fn test_cursor_info_type_exists() {
445 let cursor_info = get_type_decl("CursorInfo");
446 assert!(cursor_info.is_some(), "CursorInfo should be defined");
447 let decl = cursor_info.unwrap();
448 assert!(
449 decl.contains("position"),
450 "CursorInfo should have position field"
451 );
452 assert!(
453 decl.contains("selection"),
454 "CursorInfo should have selection field"
455 );
456 }
457
458 #[test]
459 fn test_collect_ts_types_no_duplicates() {
460 let output = collect_ts_types();
461 let lines: Vec<&str> = output.lines().collect();
462
463 let mut declarations = std::collections::HashSet::new();
465 for line in &lines {
466 let trimmed = line.trim();
467 if trimmed.starts_with("type ") && trimmed.contains('=') {
469 let name = trimmed
470 .strip_prefix("type ")
471 .unwrap()
472 .split(|c: char| c == '=' || c.is_whitespace())
473 .next()
474 .unwrap();
475 assert!(
476 declarations.insert(name.to_string()),
477 "Duplicate type declaration found: '{}'",
478 name
479 );
480 }
481 }
482 }
483
484 #[test]
485 fn test_collect_ts_types_includes_dependency_types() {
486 let output = collect_ts_types();
487 let required_types = [
488 "TextPropertyEntry",
489 "TsCompositeLayoutConfig",
490 "TsCompositeSourceConfig",
491 "TsCompositePaneStyle",
492 "TsCompositeHunk",
493 "TsCreateCompositeBufferOptions",
494 "PromptSuggestion",
495 "BufferInfo",
496 "CursorInfo",
497 "TerminalResult",
498 "CreateTerminalOptions",
499 ];
500
501 for type_name in &required_types {
502 assert!(
503 output.contains(type_name),
504 "collect_ts_types output should contain type '{}'",
505 type_name
506 );
507 }
508 }
509
510 #[test]
511 fn test_generated_dts_validates_as_typescript() {
512 use crate::backend::quickjs_backend::{JSEDITORAPI_TS_EDITOR_API, JSEDITORAPI_TS_PREAMBLE};
513
514 let ts_types = collect_ts_types();
515 let content = format!(
516 "{}\n{}\n{}",
517 JSEDITORAPI_TS_PREAMBLE, ts_types, JSEDITORAPI_TS_EDITOR_API
518 );
519
520 validate_typescript(&content).expect("Generated TypeScript should be syntactically valid");
521 }
522
523 #[test]
524 fn test_generated_dts_no_undefined_type_references() {
525 use crate::backend::quickjs_backend::{JSEDITORAPI_TS_EDITOR_API, JSEDITORAPI_TS_PREAMBLE};
526
527 let ts_types = collect_ts_types();
528 let content = format!(
529 "{}\n{}\n{}",
530 JSEDITORAPI_TS_PREAMBLE, ts_types, JSEDITORAPI_TS_EDITOR_API
531 );
532
533 let mut defined_types = std::collections::HashSet::new();
535 for builtin in &[
537 "number",
538 "string",
539 "boolean",
540 "void",
541 "unknown",
542 "null",
543 "undefined",
544 "Record",
545 "Array",
546 "Promise",
547 "ProcessHandle",
548 "PromiseLike",
549 "BufferId",
550 "SplitId",
551 "EditorAPI",
552 ] {
553 defined_types.insert(builtin.to_string());
554 }
555
556 for line in content.lines() {
558 let trimmed = line.trim();
559 if trimmed.starts_with("type ") && trimmed.contains('=') {
560 if let Some(name) = trimmed
561 .strip_prefix("type ")
562 .unwrap()
563 .split(|c: char| c == '=' || c.is_whitespace())
564 .next()
565 {
566 defined_types.insert(name.to_string());
567 }
568 }
569 if trimmed.starts_with("interface ") {
570 if let Some(name) = trimmed
571 .strip_prefix("interface ")
572 .unwrap()
573 .split(|c: char| !c.is_alphanumeric() && c != '_')
574 .next()
575 {
576 defined_types.insert(name.to_string());
577 }
578 }
579 }
580
581 let interface_section = JSEDITORAPI_TS_EDITOR_API;
584 let mut undefined_refs = Vec::new();
585
586 for line in interface_section.lines() {
587 let trimmed = line.trim();
588
589 if trimmed.starts_with('*')
591 || trimmed.starts_with("/*")
592 || trimmed.starts_with("//")
593 || trimmed.is_empty()
594 || trimmed == "{"
595 || trimmed == "}"
596 {
597 continue;
598 }
599
600 for word in trimmed.split(|c: char| !c.is_alphanumeric() && c != '_') {
602 if word.is_empty() {
603 continue;
604 }
605 if word.chars().next().is_some_and(|c| c.is_uppercase())
607 && !defined_types.contains(word)
608 {
609 undefined_refs.push(word.to_string());
610 }
611 }
612 }
613
614 undefined_refs.sort();
616 undefined_refs.dedup();
617
618 assert!(
619 undefined_refs.is_empty(),
620 "Found undefined type references in EditorAPI interface: {:?}",
621 undefined_refs
622 );
623 }
624
625 #[test]
626 fn test_editor_api_cursor_methods_have_typed_returns() {
627 use crate::backend::quickjs_backend::JSEDITORAPI_TS_EDITOR_API;
628
629 let api = JSEDITORAPI_TS_EDITOR_API;
630
631 assert!(
633 api.contains("getPrimaryCursor(): CursorInfo | null;"),
634 "getPrimaryCursor should return CursorInfo | null, got: {}",
635 api.lines()
636 .find(|l| l.contains("getPrimaryCursor"))
637 .unwrap_or("not found")
638 );
639
640 assert!(
642 api.contains("getAllCursors(): CursorInfo[];"),
643 "getAllCursors should return CursorInfo[], got: {}",
644 api.lines()
645 .find(|l| l.contains("getAllCursors"))
646 .unwrap_or("not found")
647 );
648
649 assert!(
651 api.contains("getAllCursorPositions(): number[];"),
652 "getAllCursorPositions should return number[], got: {}",
653 api.lines()
654 .find(|l| l.contains("getAllCursorPositions"))
655 .unwrap_or("not found")
656 );
657 }
658
659 #[test]
660 fn test_editor_api_terminal_methods_use_defined_types() {
661 use crate::backend::quickjs_backend::JSEDITORAPI_TS_EDITOR_API;
662
663 let api = JSEDITORAPI_TS_EDITOR_API;
664
665 assert!(
667 api.contains("CreateTerminalOptions"),
668 "createTerminal should reference CreateTerminalOptions"
669 );
670 assert!(
671 api.contains("TerminalResult"),
672 "createTerminal should reference TerminalResult"
673 );
674 }
675
676 #[test]
677 fn test_editor_api_composite_methods_use_ts_prefix_types() {
678 use crate::backend::quickjs_backend::JSEDITORAPI_TS_EDITOR_API;
679
680 let api = JSEDITORAPI_TS_EDITOR_API;
681
682 assert!(
684 api.contains("TsCompositeHunk[]"),
685 "updateCompositeAlignment should use TsCompositeHunk[], not CompositeHunk[]"
686 );
687
688 assert!(
690 api.contains("TsCreateCompositeBufferOptions"),
691 "createCompositeBuffer should use TsCreateCompositeBufferOptions"
692 );
693 }
694
695 #[test]
696 fn test_editor_api_prompt_suggestions_use_prompt_suggestion() {
697 use crate::backend::quickjs_backend::JSEDITORAPI_TS_EDITOR_API;
698
699 let api = JSEDITORAPI_TS_EDITOR_API;
700
701 assert!(
703 api.contains("PromptSuggestion[]"),
704 "setPromptSuggestions should use PromptSuggestion[], not Suggestion[]"
705 );
706 }
707
708 #[test]
709 fn test_all_editor_api_methods_present() {
710 use crate::backend::quickjs_backend::JSEDITORAPI_TS_EDITOR_API;
711
712 let api = JSEDITORAPI_TS_EDITOR_API;
713
714 let expected_methods = vec![
716 "apiVersion",
717 "getActiveBufferId",
718 "getActiveSplitId",
719 "listBuffers",
720 "debug",
721 "info",
722 "warn",
723 "error",
724 "setStatus",
725 "copyToClipboard",
726 "setClipboard",
727 "registerCommand",
728 "unregisterCommand",
729 "setContext",
730 "executeAction",
731 "getCursorPosition",
732 "getBufferPath",
733 "getBufferLength",
734 "isBufferModified",
735 "saveBufferToPath",
736 "getBufferInfo",
737 "getPrimaryCursor",
738 "getAllCursors",
739 "getAllCursorPositions",
740 "getViewport",
741 "getCursorLine",
742 "getLineStartPosition",
743 "getLineEndPosition",
744 "getBufferLineCount",
745 "scrollToLineCenter",
746 "findBufferByPath",
747 "getBufferSavedDiff",
748 "insertText",
749 "deleteRange",
750 "insertAtCursor",
751 "openFile",
752 "openFileInSplit",
753 "showBuffer",
754 "closeBuffer",
755 "on",
756 "off",
757 "getEnv",
758 "getCwd",
759 "pathJoin",
760 "pathDirname",
761 "pathBasename",
762 "pathExtname",
763 "pathIsAbsolute",
764 "utf8ByteLength",
765 "fileExists",
766 "readFile",
767 "writeFile",
768 "readDir",
769 "getConfig",
770 "getUserConfig",
771 "reloadConfig",
772 "reloadThemes",
773 "registerGrammar",
774 "registerLanguageConfig",
775 "registerLspServer",
776 "reloadGrammars",
777 "getConfigDir",
778 "getThemesDir",
779 "applyTheme",
780 "getThemeSchema",
781 "getBuiltinThemes",
782 "deleteTheme",
783 "fileStat",
784 "isProcessRunning",
785 "killProcess",
786 "pluginTranslate",
787 "createCompositeBuffer",
788 "updateCompositeAlignment",
789 "closeCompositeBuffer",
790 "getHighlights",
791 "addOverlay",
792 "clearNamespace",
793 "clearAllOverlays",
794 "clearOverlaysInRange",
795 "removeOverlay",
796 "addConceal",
797 "clearConcealNamespace",
798 "clearConcealsInRange",
799 "addSoftBreak",
800 "clearSoftBreakNamespace",
801 "clearSoftBreaksInRange",
802 "submitViewTransform",
803 "clearViewTransform",
804 "setLayoutHints",
805 "setFileExplorerDecorations",
806 "clearFileExplorerDecorations",
807 "addVirtualText",
808 "removeVirtualText",
809 "removeVirtualTextsByPrefix",
810 "clearVirtualTexts",
811 "clearVirtualTextNamespace",
812 "addVirtualLine",
813 "prompt",
814 "startPrompt",
815 "startPromptWithInitial",
816 "setPromptSuggestions",
817 "setPromptInputSync",
818 "defineMode",
819 "setEditorMode",
820 "getEditorMode",
821 "closeSplit",
822 "setSplitBuffer",
823 "focusSplit",
824 "setSplitScroll",
825 "setSplitRatio",
826 "setSplitLabel",
827 "clearSplitLabel",
828 "getSplitByLabel",
829 "distributeSplitsEvenly",
830 "setBufferCursor",
831 "setLineIndicator",
832 "clearLineIndicators",
833 "setLineNumbers",
834 "setViewMode",
835 "setViewState",
836 "getViewState",
837 "setLineWrap",
838 "createScrollSyncGroup",
839 "setScrollSyncAnchors",
840 "removeScrollSyncGroup",
841 "executeActions",
842 "showActionPopup",
843 "disableLspForLanguage",
844 "setLspRootUri",
845 "getAllDiagnostics",
846 "getHandlers",
847 "createVirtualBuffer",
848 "createVirtualBufferInSplit",
849 "createVirtualBufferInExistingSplit",
850 "setVirtualBufferContent",
851 "getTextPropertiesAtCursor",
852 "spawnProcess",
853 "spawnProcessWait",
854 "getBufferText",
855 "delay",
856 "sendLspRequest",
857 "spawnBackgroundProcess",
858 "killBackgroundProcess",
859 "createTerminal",
860 "sendTerminalInput",
861 "closeTerminal",
862 "refreshLines",
863 "getCurrentLocale",
864 "loadPlugin",
865 "unloadPlugin",
866 "reloadPlugin",
867 "listPlugins",
868 ];
869
870 let mut missing = Vec::new();
871 for method in &expected_methods {
872 let pattern = format!("{}(", method);
874 if !api.contains(&pattern) {
875 missing.push(*method);
876 }
877 }
878
879 assert!(
880 missing.is_empty(),
881 "Missing methods in EditorAPI interface: {:?}",
882 missing
883 );
884 }
885}