Skip to content

feat: add a Vim keybindings preset to the Monaco file editor - #9755

Open
xxjjvxb wants to merge 3 commits into
stablyai:mainfrom
xxjjvxb:feat/editor-keybindings
Open

xxjjvxb wants to merge 3 commits into
stablyai:mainfrom
xxjjvxb:feat/editor-keybindings

Conversation

@xxjjvxb

@xxjjvxb xxjjvxb commented Jul 21, 2026 •

Copy link
Copy Markdown

ELI5

The built-in file editor can use Vim keybindings via a setting. Default stays normal; turn on Vim for modal editing with a small mode status bar, without leaving Orca for an external editor.

What Changed

  • New setting editorKeybindings: 'default' | 'vim' (defaults to 'default', so existing users are unaffected).
  • 'vim' enables modal editing via monaco-vim, with a status bar (mode + pending keystrokes), a command cheatsheet, and Ex commands wired to Orca: :w (save), :q / :q! (close), :wq / :x (save & close), and :set for editor options.
  • Toggle lives in Settings → Editor next to Word Wrap, as a Default / Vim segmented control.
  • Modeled as a union so further presets can be added later.

Implementation notes

  • monaco-vim is imported lazily (await import) and attaches to the same locally-bundled monaco-editor instance Orca configures via loader.config({ monaco }) — no CDN/instance mismatch, and the default keymap never loads the adapter.
  • Vim mode is scoped to the editable, full-height file editor — not read-only diffs or auto-height inline preview editors.
  • The status bar is a real flex-column row below the editor (not an overlay), so it never covers the last line or the horizontal scrollbar; the editor body and its comment/annotation overlays live in their own wrapper.
  • The adapter and its focus listener dispose idempotently when the setting flips or the editor unmounts (no leaks across split panes; :w routes to the focused editor).

Why

Vim users can make quick manual edits with familiar modal keybindings while orchestrating agents — instead of dropping to an external editor.

Linked Issue

Closes #9699.

Visual Proof

Settings → Editor → Editor Keybindings (Default / Vim):
1

Vim INSERT mode and NORMAL mode with the status bar row below the editor:
2

No "before" shot for the editor itself: Vim mode is a new opt-in and there is no visual change while the setting stays on 'default'. The "before" for Settings → Editor is the same panel without the new control.

Vim command cheatsheet (the ⌨ hint button in the status bar) — the handled commands at a glance:

Vim command cheatsheet popover

Command coverage

The first pass wired the basics; this follow-up pins down (and documents) exactly how far the keymap goes, verified end-to-end against the live editor:

  • Verified working: motions & operators, registers (yy/p), visual mode, macros (qa…q / @a), and :%s/…/…/g substitution.
  • App-integration Ex commands: :w, :q/:q!, :wq/:x, routed to the focused editor's save/close.
  • :set mapped onto Monaco: wrap/nowrap, number/nonumber, tabstop/shiftwidth/expandtab. :set paste/nopaste and unknown options are graceful no-ops (Monaco has no terminal paste-mangling to disable).
  • Discoverability: the status-bar ⌨ hint opens a cheatsheet popover listing the above plus the limitations below.

Testing

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

  • EditorKeybindingsSetting.test.tsx — default/vim selection + persistence.

  • constants.test.ts — default is 'default'.

  • vim-keybindings-file-editor.spec.ts — live headless-Electron e2e: i/Esc/o/dd + :w save-to-disk, yank/paste + :%s + visual delete, macro record/replay, :set line-number toggle + :q close, and the cheatsheet popover.

AI Disclosure

This PR was written with the help of an AI coding agent. All code was reviewed, tested, and is maintained by me (@xxjjvxb).

Review

  • Cross-platform (mac/linux/win): pure renderer + Monaco; no platform APIs, paths, or shortcut labels added.
  • Local / remote / SSH: editor-only; no process, fs, or network assumptions.
  • Agent / integration / git-provider neutral.
  • Performance: no hot-path work; monaco-vim loads only when Vim is selected and disposes on toggle/unmount.
  • UI quality: segmented control reuses SettingsSegmentedControl; status bar uses standard tokens (border-border, bg-editor-surface, text-muted-foreground), light/dark safe, and occupies real layout space.
  • Security: no new IPC, eval, or network; monaco-vim is a keymap adapter over the existing editor.

Agent skill upstream boundary

Not applicable — this change touches only the Monaco editor renderer and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Known limitations (by design) — monaco-vim is a keymap emulator layered on Orca's viewer-style Monaco, not Neovim. These are library/editor constraints, not regressions in this PR:

  • gd (go to definition) doesn't work — Orca intentionally disables Monaco's language intelligence (monaco-setup.ts: noSemanticValidation), so there's no definition provider for any keymap to call.
  • No live :%s/…/…/g preview — that's Neovim's inccommand; monaco-vim runs the substitution without incremental preview.
  • Backspace inside a recorded macro isn't replayed — macros record and replay fine, but monaco-vim doesn't capture in-insert Backspace deletions into the recording. (Earlier notes claimed macros didn't replay at all; the e2e confirms they do — this is the actual, narrower gap.)
  • Linewise paste of a newline-terminated buffer adds one trailing blank line — a monaco-vim line-model quirk; Orca's save/sync are verified clean, so the extra line comes from the vendored paste, not this integration.

Users who need full Neovim (LSP, inccommand) should run it in the terminal; this feature covers in-editor motions/operators/registers/visual/macros/ex basics for quick edits.

i18n — new strings localized via localize-renderer-strings.mjs; en + ko reviewed by me. ja/zh/es are best-effort seeds and would benefit from a native-speaker pass.

X handle: @xxjjvxb

Checklist

  • This PR is small and focused
  • I explained what changed and why (including ELI5)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@coderabbitai

coderabbitai Bot commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

We couldn't safely recover the incremental review. No full review was started, and the last reviewed checkpoint was preserved. Retry later, or explicitly request a full review by commenting @coderabbitai full review.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

  • Last reviewed commit: aa1935974c383a75299b5a72a1c166c33e96d6fc
  • Latest commit, not reviewed: f7c1827

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
Walkthrough

Adds a persisted editorKeybindings setting with default and Vim options, settings UI, translations, and tests. Introduces lazy Monaco Vim integration with :w, :wq, and :x commands, active-editor save routing, focus handling, and disposal. Updates MonacoEditor to conditionally enable Vim mode, render its status bar, adjust layout, and anchor overlays to the editor body.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)

Check name Status Explanation
Linked Issues check ✅ Passed The PR satisfies #9699 by adding an editor keybindings preset, Vim integration, save-command interop, cleanup, tests, and localized UI strings.
Out of Scope Changes check ✅ Passed The changes appear aligned with the stated feature scope and do not introduce clearly unrelated code paths.
Title check ✅ Passed The title clearly and concisely identifies the main change: adding a Vim keybindings preset to the Monaco file editor.
Description check ✅ Passed The description covers the required sections with clear change details, rationale, linked issue, visual proof, testing, AI disclosure, review notes, and completed checklist items.
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/renderer/src/components/settings/EditorKeybindingsSetting.test.tsx (2)

17-51: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify test setup with @testing-library/react.

While the manual createRoot and DOM container management works well, you can eliminate this boilerplate and manual act wrapping by using @testing-library/react, which is the officially recommended testing approach for modern React. It also provides accessible queries that replace the custom radio() helper.

🛠️ Proposed refactor
-let root: Root | null = null
-let container: HTMLDivElement | null = null
-
-afterEach(() => {
-  if (root) {
-    act(() => root?.unmount())
-  }
-  container?.remove()
-  root = null
-  container = null
-})
-
-function renderSetting(
-  editorKeybindings: GlobalSettings['editorKeybindings'],
-  updateSettings = vi.fn()
-) {
-  container = document.createElement('div')
-  document.body.appendChild(container)
-  root = createRoot(container)
-  act(() => {
-    root?.render(
-      <EditorKeybindingsSetting
-        settings={{ ...getDefaultSettings(join('test', 'home')), editorKeybindings }}
-        updateSettings={updateSettings}
-      />
-    )
-  })
-  return { container, updateSettings }
-}
-
-function radio(container: HTMLDivElement, label: string): HTMLButtonElement | undefined {
-  return [...container.querySelectorAll<HTMLButtonElement>('[role="radio"]')].find(
-    (button) => button.textContent === label
-  )
-}

(You can then update the tests to use render() and screen.getByRole('radio', { name: 'Vim' }) from @testing-library/react)


17-51: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider simplifying the test setup with @testing-library/react.

While the manual createRoot, act wrapping, and DOM management correctly conform to React 19 API usage, this boilerplate can be significantly reduced by adopting @testing-library/react. It is the officially recommended approach for modern React testing and provides accessible queries that would replace the custom radio() helper.

For example, replacing the custom render and query logic with RTL would look like this:

import { render, screen, fireEvent } from '`@testing-library/react`'

// Inside tests:
render(
  <EditorKeybindingsSetting
    settings={{ ...getDefaultSettings(join('test', 'home')), editorKeybindings: 'default' }}
    updateSettings={updateSettings}
  />
)
fireEvent.click(screen.getByRole('radio', { name: 'Vim' }))

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 807fd82f-dbbb-4cde-9ccf-e51f2157617a

📥 Commits

Reviewing files that changed from the base of the PR and between 827cd49 and fd0e823c1e41e2cbeb641ed2f479a4f59b631906.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (14)
  • package.json
  • src/renderer/src/components/editor/MonacoEditor.tsx
  • src/renderer/src/components/editor/monaco-vim-mode.ts
  • src/renderer/src/components/settings/EditorKeybindingsSetting.test.tsx
  • src/renderer/src/components/settings/EditorKeybindingsSetting.tsx
  • src/renderer/src/components/settings/GeneralEditorSettingsSection.tsx
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
  • src/shared/constants.test.ts
  • src/shared/constants.ts
  • src/shared/types.ts

@xxjjvxb
xxjjvxb force-pushed the feat/editor-keybindings branch from fd0e823 to 8bf5d05 Compare July 21, 2026 17:47
@xxjjvxb

xxjjvxb commented Jul 21, 2026

Copy link
Copy Markdown
Author

Thanks for the review! Addressed in the latest push (8bf5d05):

  • :w focus hijack (monaco-vim-mode.ts) — Fixed. installMonacoVimMode now only claims the shared :w/:wq/:x handler at mount when editorInstance.hasTextFocus(). A newly-mounted, unfocused split/tab editor no longer steals the pointer from the currently-focused one; onDidFocusEditorText still claims it on focus.
  • Docstrings — Added to the new component/helpers (EditorKeybindingsSetting, MonacoEditor, ensureExCommands; installMonacoVimMode already had one).
  • Test setup (@testing-library/react) — Kept the manual createRoot/act pattern to stay consistent with the sibling EditorWordWrapSetting.test.tsx, the existing convention for these settings tests. Happy to migrate both to RTL in a follow-up if you'd prefer.

@jkryanchou

Copy link
Copy Markdown

+1

@xxjjvxb
xxjjvxb force-pushed the feat/editor-keybindings branch from 8bf5d05 to 691f4a3 Compare July 23, 2026 10:23
@nwparker nwparker added the enhancement New feature or request label Jul 27, 2026
@Pawel-Kica

Copy link
Copy Markdown

+1

@xxjjvxb

xxjjvxb commented Aug 13, 2026

Copy link
Copy Markdown
Author

Friendly bump 🙂

Just rebased this onto the latest main — it's conflict-free again and green locally (typecheck, tests, build). It's a small, opt-in setting that's off by default, so it doesn't touch existing users' workflow. #9699 now also has a detailed implementation spec, and this PR already matches it (Default/Vim preset, lazy-loaded monaco-vim, :w scoped to the focused editor, status row on the editor surface, read-only/preview editors untouched, cross-platform, Emacs deferred).

Would really appreciate a review whenever a maintainer has a moment — happy to rebase again or make any changes. Thanks for the great project! 🙏

@jkryanchou

jkryanchou commented Aug 20, 2026 •

Copy link
Copy Markdown

Have it released?

@toantvlumin

Copy link
Copy Markdown

+1 A feature I'm really looking forward to ❤️

@jkryanchou

Copy link
Copy Markdown

So looking forward to this feature could be added to Orca. Would be more effective for me to manipulate the file.

@arampp

arampp commented Aug 21, 2026

Copy link
Copy Markdown

+1 would appreciate having this feature since I always use vim mode in editors.

@xxjjvxb
xxjjvxb force-pushed the feat/editor-keybindings branch from bace026 to 9c0c3f3 Compare August 22, 2026 16:26
@xxjjvxb

xxjjvxb commented Aug 22, 2026

Copy link
Copy Markdown
Author

@nwparker — apologies for the direct tag; I've noticed you've been merging a lot of community PRs, so I'm hoping you might have a moment for this one 🙏

There's been steady demand for it — several 👍 and "really looking forward to this" comments here and on #9699, plus a detailed implementation spec in the issue that this PR already matches. It's a small, opt-in, off-by-default setting (Default / Vim), so it's low-risk for existing users.

I've kept it rebased on the latest main — it survived the recent types.ts → global-settings-types.ts split and the MonacoEditor refactor — and it's green locally (typecheck, tests, build). Known limitations (macro replay, gd, :%s preview) are documented in the description since monaco-vim is a keymap emulator, not Neovim.

No rush at all, and I'm happy to make any changes you'd like. Thanks for the great project!

@xxjjvxb
xxjjvxb force-pushed the feat/editor-keybindings branch from 9c0c3f3 to 6d9ae49 Compare September 1, 2026 18:11
@xxjjvxb

xxjjvxb commented Sep 1, 2026

Copy link
Copy Markdown
Author

@brennanb2025 — I see you're the assigned reviewer here, so hoping it's OK to gently flag this 🙏 Just a friendly check-in since it's been a while.

It's rebased on the latest main (it survived the recent settings-types split and the MonacoEditor refactor) and green locally — typecheck, tests, and build. It's a small, opt-in, off-by-default Vim setting, and there's been steady demand for it here and on #9699 (which also has a matching implementation spec).

No rush at all, and I'm happy to make any changes you'd like. Thanks for taking a look!

@tomkan-skio

Copy link
Copy Markdown

+1 looking forward to this feature

@xxjjvxb

xxjjvxb commented Sep 14, 2026 •

Copy link
Copy Markdown
Author

CI fix update:

  • Root cause: a type assertion in the monaco-vim helper, flagged by the changed-code quality gate (a diff-only CI check that a local lint/typecheck doesn't surface).
  • Fix: kept the cast with a SAFETY: annotation (per the repo's AGENTS.md convention) — removing it broke typecheck (TS2559), since monaco-vim doesn't type its runtime .Vim.
  • Validated on my fork: static analysis, typecheck, cross-version wire, tests, package — all green (24/24).
  • Rebased on latest main, MERGEABLE. Happy to adjust on review — thanks!

@leitxio

leitxio commented Sep 17, 2026

Copy link
Copy Markdown

+1 - I would highly appreciate this feature! It is really one of the few things I am missing right now

@crwecker

Copy link
Copy Markdown

+1 🙏

@VitaliBaiduk

Copy link
Copy Markdown

+1

1 similar comment
@haypho

haypho commented Sep 30, 2026

Copy link
Copy Markdown

+1

@brennon-tava

Copy link
Copy Markdown

PLEASE -- this is the one thing standing in the way of Orca being perfect for my workflow. Everything is amazing and then I go to manually edit or review code and it feels like my fingers have been tied together.

@unok

unok commented Oct 2, 2026

Copy link
Copy Markdown

+1

1 similar comment
@ujwalkomarla

Copy link
Copy Markdown

+1

@xxjjvxb
xxjjvxb force-pushed the feat/editor-keybindings branch from 535edb1 to 6612843 Compare October 6, 2026 14:06
@xxjjvxb

xxjjvxb commented Oct 6, 2026

Copy link
Copy Markdown
Author

Pushed a follow-up that expands and documents how far the Vim preset actually handles. The first pass wired the basics; this pins down the real coverage, all verified end-to-end against the live editor.

Added

  • :q / :q! (close) and :wq / :x (save & close), routed to the focused editor's save/close.
  • :set mapped onto Monaco: wrap/nowrap, number/nonumber, tabstop/shiftwidth/expandtab. :set paste/nopaste and unknown options are graceful no-ops (Monaco has no terminal paste-mangling to disable).
  • A command cheatsheet — the ⌨ hint in the status bar opens a popover listing the supported commands plus the limitations.
  • tests/e2e/vim-keybindings-file-editor.spec.ts — headless-Electron e2e: modes (i/Esc/o/dd) + :w save-to-disk, yank/paste + :%s + visual delete, macro record/replay, :set line-number toggle + :q close, and the cheatsheet popover.

Corrected the earlier limitation notes now that there is e2e evidence: registers, visual mode, macros, and :%s substitution all work. The real gaps are gd, :%s live-preview highlighting, Backspace replay inside a recorded macro, and monaco-vim's extra trailing blank line on linewise paste of a newline-terminated buffer (Orca's save/sync are verified clean, so that one is the vendored engine, not this integration).

Vim command cheatsheet

@ManishGotame

Copy link
Copy Markdown

+1

@xxjjvxb

xxjjvxb commented Oct 18, 2026

Copy link
Copy Markdown
Author

Rebased onto latest main — the branch had drifted into conflict over the last few months, so I've resolved it and it's green again.

Conflicts resolved across:

  • package.json / pnpm-lock.yaml (added monaco-vim)
  • MonacoEditor.tsx — kept upstream's modelUri / dropIntoEditor changes alongside the Vim status-bar layout
  • the settings + constants.test.ts defaults, and the five locale catalogs (en/es/ja/ko/zh)

Local checks pass: tc:web (0 errors across the web project), oxlint, and the localization catalog / extraction / coverage gates.

No functional changes from the last review pass — same Vim preset, cheatsheet, and :w / :q / :set handling. Ready for another look whenever you have a chance.

Add an `editorKeybindings: 'default' | 'vim'` setting (default 'default')
that enables monaco-vim modal editing in the built-in file editor, with a
status bar and :w/:wq/:x wired to save. The toggle lives in Settings →
Editor next to Word Wrap as a Default/Vim segmented control.

monaco-vim loads lazily and is scoped to the editable, full-height editor
(not read-only diffs or auto-height preview editors); the status bar is a
real flex-column row below the editor rather than an overlay. The adapter
disposes idempotently on toggle or unmount, and :w routes to the focused
editor. It shares Orca's locally-bundled monaco instance via loader.config.

monaco-vim is a keymap emulator, so LSP-backed :gd, :%s live preview, and
macro replay @q are unsupported — documented as known limitations.

Closes stablyai#9699.
…tsheet

Build on the initial Vim keybindings preset with the app-integration and
discoverability pieces the first pass left out:

- Wire :q/:q! (close tab) and make :wq/:x save-and-close, routed through the
  focused editor's Orca save/close like :w already was.
- Map editor-display :set options onto Monaco -- wrap/nowrap, number/nonumber,
  tabstop/shiftwidth/expandtab -- with :set paste/nopaste and unknown options as
  graceful no-ops (Monaco has no terminal paste-mangling to disable).
- Add a command cheatsheet: a hint button in the Vim status bar opens a popover
  listing supported motions/operators/registers/visual/macro/Ex commands plus the
  documented limitations.
- Correct the preset's limitation notes: registers, visual mode, macros, and :%s
  substitution are verified working (save/sync proven clean); the real gaps are
  gd, :%s live-preview, Backspace replay inside a macro, and monaco-vim's extra
  trailing blank line on linewise paste of a newline-terminated buffer.
Drive the real monaco-vim adapter in a headless Electron run: mode switching
with i/Esc/o/dd, :w save-to-disk, yank/paste + :%s substitution + visual-mode
delete, macro record/replay, and the :set line-number toggle + :q close. Also
exercises the status-bar cheatsheet popover.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Configurable editor keybinding presets (Vim, Emacs, …) for the built-in Monaco editor