agent-ecosystem/src/renderer/utils/editorBridge.ts
iliya 5b0c7d13fc feat: add project editor with drag & drop file management
- Backend: ProjectFileService with file CRUD, search, git status, file watcher
- IPC: 12 editor channels with security validation and path containment
- Store: editorSlice with multi-tab management, draft persistence, conflict detection
- UI: CodeMirror 6 editor, file tree with DnD, search-in-files, context menus
- Move: fs.rename with EXDEV fallback, full path remapping across all caches
- Tests: comprehensive coverage for services, IPC handlers, store, and utilities
2026-02-28 23:40:41 +02:00

90 lines
2.7 KiB
TypeScript

/**
* Module-level singleton bridging Zustand store ↔ CodeMirror refs.
*
* CodeMirrorEditor calls register() on mount, unregister() on unmount.
* Store actions (saveFile, saveAllFiles, closeEditor) use getContent()/destroy().
*
* Pattern: analogous to ConfirmDialog.tsx (module-level globalSetState).
*/
import type { EditorState } from '@codemirror/state';
import type { EditorView } from '@codemirror/view';
let stateCache: Map<string, EditorState> | null = null;
let scrollTopCache: Map<string, number> | null = null;
let activeView: EditorView | null = null;
export const editorBridge = {
/** Called by CodeMirrorEditor on mount */
register(sc: Map<string, EditorState>, stc: Map<string, number>, view: EditorView): void {
stateCache = sc;
scrollTopCache = stc;
activeView = view;
},
/** Called by CodeMirrorEditor on unmount */
unregister(): void {
stateCache = null;
scrollTopCache = null;
activeView = null;
},
/** Check if bridge is registered (HMR guard) */
get isRegistered(): boolean {
return stateCache !== null;
},
/** Get content for a single file from cached EditorState */
getContent(filePath: string): string | null {
return stateCache?.get(filePath)?.doc.toString() ?? null;
},
/** Get content for all modified files */
getAllModifiedContent(modifiedFiles: Record<string, boolean>): Map<string, string> {
const result = new Map<string, string>();
for (const fp of Object.keys(modifiedFiles)) {
if (!modifiedFiles[fp]) continue;
const content = stateCache?.get(fp)?.doc.toString();
if (content !== undefined) result.set(fp, content);
}
return result;
},
/** Remove cached state for a single tab — called by closeTab() */
deleteState(tabId: string): void {
stateCache?.delete(tabId);
scrollTopCache?.delete(tabId);
},
/** Full cleanup — called by closeEditor() */
destroy(): void {
activeView?.destroy();
stateCache?.clear();
scrollTopCache?.clear();
activeView = null;
},
/** Remap cached state from oldPath to newPath (used by moveFileInTree) */
remapState(oldPath: string, newPath: string): void {
const state = stateCache?.get(oldPath);
if (state) {
stateCache!.delete(oldPath);
stateCache!.set(newPath, state);
}
const scroll = scrollTopCache?.get(oldPath);
if (scroll !== undefined) {
scrollTopCache!.delete(oldPath);
scrollTopCache!.set(newPath, scroll);
}
},
/** Update view reference (on tab switch, view may be recreated) */
updateView(view: EditorView): void {
activeView = view;
},
/** Get current EditorView (for undo/redo toolbar) */
getView(): EditorView | null {
return activeView;
},
};