Skip to content

Global Editor → full editor experience: LSP-wired editing, file search, fullscreen, isolation #513

Description

@Juliusolsson05

Global Editor → full editor experience (LSP-wired, pluggable languages)

Why

The Global Editor overlay (⌘⇧E) is currently a file viewer with a half-finished editing layer bolted on. A full audit (2026-07-09) found that most of the missing "editor experience" is not missing backend — src/main/ipc/editorFs.ts already has read/write-with-mtime-guard/create/rename/delete, and src/main/lspManager.ts already runs a real typescript-language-server — it's missing wiring and UX. This issue is the umbrella for turning the overlay into a real editor.

Scope level (decided): between "solid lightweight editor" and "full IDE" — the TS/JS LSP experience gets fully wired into the real editor (semantic tokens, diagnostics, hover, go-to-definition, completions), and the LSP layer is refactored into a pluggable server registry so additional languages (pyright, rust-analyzer, gopls, …) become "detect on PATH + config entry", without bundling any server binaries (third_party policy: binaries are never committed; bundling via manifest is a possible follow-up per the #119/#120 pattern).

Confirmed bugs (audit findings, with root causes)

1. Closing file tabs does not work

  • useGlobalEditorStore.closeFile() refuses dirty buffers and returns false expecting the caller to confirm — but GlobalEditorShell.tsx (onCloseFile={path => closeFileAction(activeCwd, path)}) drops the return value and no confirm dialog exists anywhere. One keystroke in a file ⇒ its tab's × silently no-ops forever.
    • src/renderer/src/features/global-editor/store.ts:339-369
    • src/renderer/src/features/global-editor/ui/GlobalEditorShell.tsx:298
  • Compounding: a failed save (mtime conflict) sets buffer.error, and MonacoFileEditor.tsx:154-160 renders the error instead of the editor — the user loses sight of their code, and the tab is now both dirty and uncloseable.
  • Same dropped-return pattern re-implemented in AiWorkspaceEditor.tsx (closeFile local variant).

2. Syntax highlighting is intermittently completely blank

Three stacked causes, worst first:

  • .tsx/.jsx files get a language ID Monaco doesn't have. normalizeCodeLanguage maps tsx → typescriptreact, jsx → javascriptreact (src/shared/code/language.ts). Those are VS Code IDs; Monaco only ships typescript/javascript and nothing ever calls monaco.languages.register for the react variants. Result: the plaintext tokenizer, i.e. zero highlighting for every .tsx/.jsx file — which is "half the files" in a TS/React repo and exactly matches the "works half the time" symptom. (MonacoFileEditor.tsx:52 creates the model with file.language verbatim.)
  • Semantic highlighting is globally dead. ensureSemanticProvider registers LSP semantic-token providers (src/renderer/src/lib/code/monacoRuntime.ts:169-215), but no editor instance ever sets 'editor.semanticHighlighting.enabled': true. Monaco's default is configuredByTheme, and all our custom themes leave it off — so LSP semantic tokens are computed (IPC + tsserver work) and never painted.
  • Worker wiring is racy and CSP-fragile. MonacoEnvironment.getWorker is installed only after the lazy import('monaco-editor') resolves (monacoRuntime.ts:146), and src/renderer/index.html CSP omits worker-src (falls back to script-src 'self'; any blob: worker path dies silently). A worker that fails to start = monarch-only or no tokenization with no visible error.
  • Bonus theme bug: every getMonaco() call re-runs defineThemes()monaco.editor.setTheme(currentThemeName()) (monacoRuntime.ts:163). Monaco themes are global, so any transcript CodeBlock mounting while the Global Editor is open yanks the editor back onto the dark slab theme, fighting activateEditorTheme/deactivateEditorTheme (features/editor/lib/monacoEditorTheme.ts).

3. LSP exists but the real editor never uses it

  • src/main/lspManager.ts spawns typescript-language-server --stdio per workspace root, with document sync, semantic tokens, and push diagnostics; IPC in src/main/ipc/lsp.ts, preload in src/preload/api/lsp.ts.
  • Only transcript code slabs consume it (CodeBlock.tsx calls ensureSemanticProvider + openLspDocument + onLspDiagnostics). MonacoFileEditor.tsx — the actual file editor — calls none of it. No diagnostics, no hover, no go-to-def, no completions in the editor.
  • LspManager today only implements legend/semantic-tokens/diagnostics; hover/definition/completion/references/documentSymbol requests don't exist yet.

4. Editing-experience gaps (backend exists, UX doesn't)

  • No file watching: an agent/git write to a clean open buffer is never noticed (buffer goes silently stale; only the next ⌘S fails). editorFsCache invalidates only on the editor's own writes.
  • No create/rename/delete UI — editor-fs:create-file/create-directory/rename/delete IPC handlers exist (editorFs.ts:256-340) with zero callers in the renderer.
  • No save feedback, no dirty-close confirm, no conflict-resolution UX (see bug 1).
  • Undo history dies on every tab switch: MonacoFileEditor disposes the model per switch (MonacoFileEditor.tsx:96-113); buffers live as plain strings in zustand.
  • No persistence: open tabs, active file, splitter ratio, tree width all reset on app restart (deliberate-for-now per store.ts:27-35, revisit here — persist paths and geometry, never unsaved contents).

5. No file search of any kind

  • No quick-open (fuzzy filename), no project content search. features/spotlight is an unrelated agent-view toggle. No ripgrep binding exists anywhere.
  • Reusable pieces: the command palette fuzzy scorer (features/command-palette/lib/rankCommands.ts) and the junk-filtered, root-contained editor-fs:list-directory (single-level; editorFs.ts:117 — its header comment already anticipates a quick-open surface). Needs a recursive-walk IPC and a content-search IPC.

6. No fullscreen editor

  • The overlay splitter is hard-clamped to 0.2–0.8 (store.ts:142-150); the editor can never take the full window. No command, no keybind, no state.

7. Isolation gaps (structure)

  • features/editor/ is already a clean pure-view layer (good). But:
    • Editor commands live in features/workspace/commands/layoutCommands.ts — there is no editor-owned command module registered in command-palette/registry.ts; new editor commands have no natural home.
    • CommandPalette.tsx reaches into useGlobalEditorStore directly instead of the ui bridge; editor-store access is scattered across three features.
    • AiWorkspaceEditor.tsx:19-163 re-implements the entire buffer lifecycle (open/dirty/save/close) in component-local state — parallel to global-editor/store.ts, differing only in the IO adapter (editorFs vs aiWorkspace* IPC).
    • Two theme modules (monacoRuntime.ts + monacoEditorTheme.ts) fight over Monaco's global theme (see bug 2 bonus).
    • Stale comments reference a deleted features/editor/store.ts (global-editor/store.ts:17-25 etc.).

Work plan (one branch/PR, fat implementation plan to follow)

A. Correctness first

  1. Fix .tsx/.jsx language mapping for Monaco (model language typescript/javascript + ts-worker JSX compiler options; keep the react IDs only where the LSP protocol needs them).
  2. Enable semantic highlighting on editor instances; single-owner Monaco theme module (kill the getMonaco() setTheme-on-every-call fight).
  3. Install MonacoEnvironment before/independently of the lazy import; add worker-src 'self' blob: to CSP.
  4. Dirty-close confirm dialog (shared component, used by Global Editor and AI Workspace); save-conflict banner that keeps the editor visible (reload-from-disk / keep-mine actions) instead of replacing the pane.

B. LSP into the real editor + pluggable registry
5. Refactor LspManager → server registry: per-language server spec (command, args, init options), PATH detection for user-installed servers (pyright-langserver, rust-analyzer, gopls, …), tsserver stays the built-in default. No bundled binaries.
6. Add LSP request methods: hover, definition, completion (+resolve), references, documentSymbol; IPC + preload plumbing.
7. Wire MonacoFileEditor to LSP: document open/change/close sync, diagnostics markers, semantic tokens, hover provider, go-to-definition (cross-file: opens target in a new editor tab at position), completion provider.
8. Model/document registry with ref-counting in the renderer so undo history survives tab switches and models are shared with future split views.

C. Editor experience
9. File watcher on open buffers (clean buffers auto-refresh; dirty buffers get a "changed on disk" state feeding the conflict banner).
10. Explorer context menu: new file / new folder / rename / delete (IPC already exists).
11. Quick Open (⌘P): recursive file-walk IPC (junk-filtered, bounded) + rankCommands fuzzy scoring, editor-owned palette surface.
12. Project content search: main-process search IPC (streamed results, bounded); rg-via-third_party-manifest as a follow-up if JS walk proves too slow.
13. Fullscreen editor command + keybind: editor takes 100% of the workspace area (workspace pane unmount-hidden, not destroyed), Esc/toggle restores previous ratio.
14. Persist per-cwd open-file paths, active file, splitter/tree geometry (never file contents) across restarts.

D. Isolation
15. Editor-owned command module (features/global-editor/commands/) registered in the palette registry; move the 5 editor commands out of layoutCommands.ts; palette uses the bridge, not direct store reach-ins.
16. Extract shared buffer-lifecycle (store factory or hook parameterized by an IO adapter) so AI Workspace stops duplicating it.
17. Comment hygiene: fix stale references to the deleted features/editor/store.ts.

Out of scope (explicit): bundling language-server binaries (follow-up via third_party manifest pattern), git gutter/blame decorations, breadcrumbs, split editor views, multi-root workspaces, refactoring providers (rename-symbol etc.), settings UI for LSP servers.

Verification

  • Raw tsc on both projects (build/vitest don't type-check), plus manual end-to-end: open .tsx file → colored + semantic highlights; introduce type error → squiggle + hover; ⌘-click → cross-file jump; dirty tab close → confirm; external write → refresh/banner; ⌘P → fuzzy open; content search; fullscreen toggle.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions