- Introduced `EditorFileWatcher` for live file change detection and `GitStatusService` for displaying Git status in the file tree. - Added context menu for file operations (create, delete) and implemented multi-tab support for the editor. - Enhanced user experience with keyboard shortcuts, search functionality, and breadcrumb navigation. - Updated IPC channels for file operations and integrated conflict detection during file saves. - Improved performance with file watcher optimizations and virtualized file tree rendering.
147 lines
10 KiB
Markdown
147 lines
10 KiB
Markdown
# Требуемый ресёрч
|
||
|
||
> Зафиксировано после 4-агентного ревью плана. Каждый пункт — пробел в знаниях, без закрытия которого реализация рискованна.
|
||
|
||
## R1: Scope isolation горячих клавиш
|
||
|
||
**Проблема**: 6 из 12 шорткатов (`Cmd+W`, `Cmd+B`, `Cmd+F`, `Cmd+Shift+[/]`, `Ctrl+Tab`) конфликтуют с глобальными в `useKeyboardShortcuts.ts`.
|
||
|
||
**Нужно выяснить**:
|
||
- Как CM6 keymaps (через `keymap.of()`) взаимодействуют с глобальным `window.addEventListener('keydown')`?
|
||
- Останавливает ли CM6 propagation события?
|
||
- Какой паттерн используют VS Code / другие Electron-editors для scope isolation?
|
||
- Варианты: guard `isEditorOpen` в глобальном хуке? KeyboardEvent stack? Priority system?
|
||
|
||
**Статус**: COMPLETED
|
||
|
||
**Результат**:
|
||
- Глобальный handler в `useKeyboardShortcuts.ts` использует `window.addEventListener('keydown')` в bubble phase (строка 278)
|
||
- CM6 использует bubble phase на `.cm-content`, вызывает `preventDefault()` но НЕ `stopPropagation()` по умолчанию
|
||
- CM6 поддерживает `stopPropagation: true` опцию per keybinding
|
||
- `ChangeReviewDialog` уже использует capture-phase handler с guard (строки 379-408)
|
||
- **Рекомендация: Approach A** — Guard в глобальном handler с флагом `editorOverlayOpen` в store (~20-30 LOC, надёжность 8/10)
|
||
- В `useKeyboardShortcuts.ts`: `const editorOpen = useStore(s => s.editorProjectPath !== null);` → early return для конфликтующих shortcuts
|
||
- Плюс: добавить `stopPropagation: true` к CM6 keybindings как safety net
|
||
- Конкретные конфликты: `Cmd+W` (строка 155), `Cmd+B` (271), `Cmd+F` (241), `Cmd+Shift+[/]` (177), `Ctrl+Tab` (81)
|
||
|
||
---
|
||
|
||
## R2: CM6 Compartment + EditorState pooling
|
||
|
||
**Проблема**: План хранит Compartments в `useRef` (один экземпляр) и использует для 30+ EditorState в пуле. CM6 может не поддерживать sharing одного Compartment между разными EditorState.
|
||
|
||
**Нужно выяснить**:
|
||
- Документация CM6: привязан ли Compartment к конкретному EditorState?
|
||
- Что происходит при `compartment.of(X)` в extensions для разных EditorState?
|
||
- Что происходит при `dispatch({ effects: compartment.reconfigure(Y) })` если другой state в кэше использует тот же Compartment?
|
||
- Паттерн из CodeMirrorDiffView: один View + один State — работает, но не pooling.
|
||
- Альтернатива: Compartment-per-state в Map (рядом с EditorState).
|
||
|
||
**Статус**: COMPLETED
|
||
|
||
**Результат**:
|
||
- **Compartment — opaque identity token** без внутреннего state. Подтверждено Marijn Haverbeke (автор CM6): "Compartments can be shared without issue"
|
||
- Каждый EditorState имеет свой `Map<Compartment, Extension>` в config
|
||
- `reconfigure()` на одном View не влияет на cached states в пуле
|
||
- **Рекомендация: Option A** — Общие Compartment-инстансы для всех states (надёжность 9/10)
|
||
- useRef Compartments безопасны для sharing: `readOnlyCompartment.current.of(...)` в extensions для каждого нового EditorState
|
||
- При unmount+remount: кешированные states ссылаются на старые Compartments → при remount создать новые Compartments, заново создать EditorState для активного таба
|
||
- LRU eviction теряет undo history (ожидаемо), cursor сохраняется через EditorSelection
|
||
|
||
---
|
||
|
||
## R3: Store ↔ Component ref bridge (closeEditor + saveAllFiles)
|
||
|
||
**Проблема**: `closeEditor()` и `saveAllFiles()` в Zustand action должны работать с `stateCache` и `viewRef` из useRef компонента CodeMirrorEditor. Zustand actions не имеют доступа к React refs.
|
||
|
||
**Нужно выяснить**:
|
||
- Как существующий код решает аналогичные проблемы (например, terminal cleanup)?
|
||
- Варианты: (a) global ref registry; (b) store хранит cleanup callback через `registerCleanup(fn)`; (c) компонент слушает `editorProjectPath === null` в useEffect и делает cleanup сам; (d) zustand subscribe в компоненте.
|
||
- Какой вариант минимально инвазивен и надёжен при unmount?
|
||
|
||
**Статус**: COMPLETED
|
||
|
||
**Результат**:
|
||
Существующие паттерны в кодовой базе:
|
||
- `MembersJsonEditor` — компонент владеет lifecycle полностью
|
||
- `CodeMirrorDiffView` — внешний ref holder
|
||
- `changeReviewSlice` — module-level state (строки 5-12)
|
||
- `ConfirmDialog` — singleton-регистрация с module-level `globalSetState`
|
||
- `ChangeReviewDialog` — компонент оркестрирует
|
||
|
||
**Рекомендация: Hybrid C+D** — `editorBridge.ts` module-level singleton (надёжность 9/10):
|
||
```typescript
|
||
// src/renderer/utils/editorBridge.ts (module-level)
|
||
let stateCache: Map<string, EditorState> | null = null;
|
||
let scrollTopCache: Map<string, number> | null = null;
|
||
let viewRef: EditorView | null = null;
|
||
|
||
export const editorBridge = {
|
||
register(sc, stc, v) { stateCache = sc; scrollTopCache = stc; viewRef = v; },
|
||
unregister() { stateCache = null; scrollTopCache = null; viewRef = null; },
|
||
getContent(filePath) { return stateCache?.get(filePath)?.doc.toString() ?? null; },
|
||
getAllModifiedContent(modifiedFiles) { /* iterate stateCache */ },
|
||
destroy() { viewRef?.destroy(); stateCache?.clear(); scrollTopCache?.clear(); },
|
||
};
|
||
```
|
||
- Компонент вызывает `register()` при mount, `unregister()` при unmount
|
||
- Store actions (closeEditor, saveAllFiles) используют `editorBridge.getContent()` / `editorBridge.destroy()`
|
||
- `saveAllFiles`: компонент итерирует и вызывает `store.saveFile()` для каждого (паттерн ChangeReviewDialog)
|
||
- `discardChanges`: store делает IPC read, компонент применяет через `view.dispatch({ changes })`
|
||
|
||
---
|
||
|
||
## R4: fs.watch recursive на Linux + watcher reliability
|
||
|
||
**Проблема**: `fs.watch({ recursive: true })` экспериментальный на Linux в Node.js 18. Может тихо не работать. Нет fallback.
|
||
|
||
**Нужно выяснить**:
|
||
- Какую версию Node.js использует Electron 40? Поддерживается ли `recursive: true` на Linux?
|
||
- Существующий `FileWatcher.ts` в проекте: использует ли он `recursive: true`? Есть ли fallback?
|
||
- Альтернативы: chokidar (но добавляет зависимость), polling, non-recursive watch + manual traversal.
|
||
- `max_user_watches` лимит inotify — как обойти?
|
||
- macOS FSEvents: coalescing events — как существующий FileWatcher решает это?
|
||
|
||
**Статус**: COMPLETED
|
||
|
||
**Результат**:
|
||
- **Electron 40 = Node.js 24 (LTS)** — `recursive: true` работает на macOS (FSEvents, надёжность 9/10) и Linux (inotify, надёжность 6/10)
|
||
- Существующий `FileWatcher.ts` (1098 строк) — зрелый watcher с debounce (строки 1060-1074), recovery (424-457), catch-up scan (992-1051)
|
||
- **НЕ добавлять chokidar** — использовать паттерны из FileWatcher.ts
|
||
- macOS: fs.watch recursive reliable (FSEvents), burst coalescing для git checkout сценариев
|
||
- Linux: `ENOSPC` → fallback на polling (5-10 секунд интервал)
|
||
- `max_user_watches` лимит inotify: при ENOSPC не падать, а деградировать до polling
|
||
- **Рекомендация**: EditorFileWatcher ~250-300 LOC (вместо первоначальных ~60 LOC), включая:
|
||
- Burst coalescing (200ms debounce + batch)
|
||
- ENOSPC fallback to polling
|
||
- Фильтрация: node_modules/.git/dist
|
||
- Graceful stop/restart
|
||
|
||
---
|
||
|
||
## R5: Git CLI availability & performance
|
||
|
||
**Проблема**: `git status --porcelain` без проверки наличия git. На больших монорепо — десятки секунд. Non-git проекты не обработаны.
|
||
|
||
**Нужно выяснить**:
|
||
- Есть ли в проекте утилита проверки наличия git? (проверить существующие git-related сервисы)
|
||
- `git status --porcelain` performance: `--untracked-files=no` ускоряет? `--ignore-submodules`?
|
||
- Timeout стратегия: AbortSignal + child_process?
|
||
- Graceful degradation: что показывать если git недоступен / не git-repo?
|
||
- `.git/index.lock` — как обрабатывать concurrent git operations?
|
||
|
||
**Статус**: COMPLETED
|
||
|
||
**Результат**:
|
||
- **`isGitRepo()`** уже есть в `GitDiffFallback.ts` (строки 118-133) — переиспользовать
|
||
- Оптимальная команда: `git --no-optional-locks status --porcelain -z --untracked-files=normal --ignore-submodules`
|
||
- `--no-optional-locks` — критично, предотвращает `.git/index.lock` конфликты
|
||
- `-z` — NUL-separated вывод (безопасный парсинг путей с пробелами)
|
||
- `--ignore-submodules` — ускорение на монорепо
|
||
- Timeout: 10 секунд (паттерн из GitDiffFallback.ts), AbortSignal
|
||
- Добавить `'conflict'` статус в `GitFileStatus` для merge conflicts (`UU`, `AA`, `DD` коды)
|
||
- Graceful degradation: проверить git available → проверить is repo → timeout handling
|
||
- Нет git: скрыть git бейджи, показать "Git not available" в status bar
|
||
- Не git-repo: скрыть git бейджи
|
||
- Timeout: показать "Git status unavailable" + кнопка retry
|
||
- **GitStatusService ~200-250 LOC**, EditorFileWatcher **~250-300 LOC**
|