From b45dbcc0913db77639750fcd5fee0f04ba72ae1b Mon Sep 17 00:00:00 2001 From: christphralden Date: Sat, 13 Jun 2026 06:09:01 +0700 Subject: [PATCH 1/9] feat(vim-mappings): add vimMappings into Prefs and Store --- packages/app-core/src/store.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/app-core/src/store.ts b/packages/app-core/src/store.ts index 052fb93e..c6cf4a8f 100644 --- a/packages/app-core/src/store.ts +++ b/packages/app-core/src/store.ts @@ -263,6 +263,8 @@ interface Prefs { /** Key sequence that exits insert mode (maps to ), e.g. "jk". * Empty disables it. */ vimInsertEscape: string + /* Stores .vimrc style mapping as plaintext */ + vimMappings: string keymapOverrides: KeymapOverrides /** When true, pressing the leader key shows the next available Vim-style actions. */ whichKeyHints: boolean @@ -409,6 +411,7 @@ function normalizeKanbanColumnTitles(raw: unknown): Record { const DEFAULT_PREFS: Prefs = { vimMode: true, vimInsertEscape: '', + vimMappings: '', keymapOverrides: {}, whichKeyHints: true, whichKeyHintMode: 'timed', @@ -483,6 +486,7 @@ function normalizePrefs(p: Partial): Prefs { : DEFAULT_PREFS.themeId return { vimMode: typeof p.vimMode === 'boolean' ? p.vimMode : DEFAULT_PREFS.vimMode, + vimMappings: typeof p.vimMappings === 'string' ? p.vimMappings : DEFAULT_PREFS.vimMappings, vimInsertEscape: typeof p.vimInsertEscape === 'string' ? p.vimInsertEscape.trim().slice(0, 5) @@ -1057,6 +1061,7 @@ async function rewriteTagAcrossVault( function collectPrefs(s: { vimMode: boolean vimInsertEscape: string + vimMappings: string keymapOverrides: KeymapOverrides whichKeyHints: boolean whichKeyHintMode: WhichKeyHintMode @@ -1114,6 +1119,7 @@ function collectPrefs(s: { return { vimMode: s.vimMode, vimInsertEscape: s.vimInsertEscape, + vimMappings: s.vimMappings, keymapOverrides: s.keymapOverrides, whichKeyHints: s.whichKeyHints, whichKeyHintMode: s.whichKeyHintMode, @@ -1464,6 +1470,7 @@ interface Store { vimMode: boolean /** Key sequence that exits insert mode (maps to ), e.g. "jk". Persisted. */ vimInsertEscape: string + vimMappings: string keymapOverrides: KeymapOverrides whichKeyHints: boolean whichKeyHintMode: WhichKeyHintMode @@ -1742,6 +1749,7 @@ interface Store { setFocusMode: (focus: boolean) => void setVimMode: (on: boolean) => void setVimInsertEscape: (sequence: string) => void + setVimMappings: (mappings: string) => void setKeymapBinding: (id: KeymapId, binding: string | null) => void resetAllKeymaps: () => void setWhichKeyHints: (on: boolean) => void @@ -2686,6 +2694,7 @@ export const useStore = create((set, get) => { zenRestoreState: null, vimMode: loadPrefs().vimMode, vimInsertEscape: loadPrefs().vimInsertEscape, + vimMappings: loadPrefs().vimMappings, keymapOverrides: loadPrefs().keymapOverrides, whichKeyHints: loadPrefs().whichKeyHints, whichKeyHintMode: loadPrefs().whichKeyHintMode, @@ -4022,6 +4031,9 @@ export const useStore = create((set, get) => { set({ vimInsertEscape: sequence.trim().slice(0, 5) }) savePrefs(collectPrefs(get())) }, + setVimMappings: (mappings) =>{ + set({vimMappings: mappings.trim()}) + }, setKeymapBinding: (id, binding) => { set((s) => { const nextOverrides = { ...s.keymapOverrides } From 4af9c5f6439ed3c49e460044047691c4a99d8db6 Mon Sep 17 00:00:00 2001 From: christphralden Date: Sat, 13 Jun 2026 06:57:41 +0700 Subject: [PATCH 2/9] feat(vim-mapping): run apply vimMappings after all maps synced on Editor load --- packages/app-core/src/components/Editor.tsx | 38 +++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/packages/app-core/src/components/Editor.tsx b/packages/app-core/src/components/Editor.tsx index c0f97a9b..46c1cebc 100644 --- a/packages/app-core/src/components/Editor.tsx +++ b/packages/app-core/src/components/Editor.tsx @@ -44,6 +44,7 @@ import { applyVimInsertEscape } from '../lib/vim-insert-escape' let vimCommandsRegistered = false let syncedVimBindings: Partial> = {} +let appliedVimMappingLhs: string[] = [] const DEFAULT_VIM_MAPPINGS_TO_CLEAR = [ 'gd', @@ -166,6 +167,36 @@ function editorHalfPage(view: EditorView | undefined, forward: boolean): void { scroller.scrollTop = nextTop } +function applyVimMappings(raw: string): void { + for (const lhs of appliedVimMappingLhs) { + try { + Vim.unmap(lhs, 'normal') + } catch { + /* ignore */ + } + } + appliedVimMappingLhs = [] + for (const line of raw.split('\n')) { + const trimmed = line.trim() + if (!trimmed || trimmed.startsWith('"')) continue + const [cmd, lhs, rhs] = trimmed.split(/\s+/) + if (!lhs) continue + try { + if ((cmd === 'noremap' || cmd === 'nnoremap') && rhs) { + Vim.noremap(lhs, rhs, 'normal') + appliedVimMappingLhs.push(lhs) + } else if ((cmd === 'map' || cmd === 'nmap') && rhs) { + Vim.map(lhs, rhs, 'normal') + appliedVimMappingLhs.push(lhs) + } else if (cmd === 'unmap' || cmd === 'nunmap') { + Vim.unmap(lhs, 'normal') + } + } catch { + /* ignore bad mappings */ + } + } +} + function syncVimKeymaps(overrides: KeymapOverrides): void { const mappings: Array<{ id: KeymapId; action: string; bindings: string[] }> = [ { @@ -1261,6 +1292,8 @@ export function Editor(): JSX.Element { const keymapOverrides = useStore((s) => s.keymapOverrides) const vimInsertEscape = useStore((s) => s.vimInsertEscape) const zenMode = useStore((s) => s.zenMode) + const vimMappings = useStore((s) => s.vimMappings) + const vimMode = useStore((s)=> s.vimMode) useEffect(() => { registerVimCommands() @@ -1275,6 +1308,11 @@ export function Editor(): JSX.Element { applyVimInsertEscape(vimInsertEscape) }, [vimInsertEscape]) + useEffect(() => { + if (!vimMode) return + applyVimMappings(vimMappings) + }, [vimMappings, vimMode]) + return (
From b623d3c7bf653268813af23d1a4beb663f6e58d0 Mon Sep 17 00:00:00 2001 From: christphralden Date: Sat, 13 Jun 2026 06:59:46 +0700 Subject: [PATCH 3/9] feat(vim-mapping): surface input for .vimrc on SettingsModal --- .../app-core/src/components/SettingsModal.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/app-core/src/components/SettingsModal.tsx b/packages/app-core/src/components/SettingsModal.tsx index 346064cc..2ec3c10a 100644 --- a/packages/app-core/src/components/SettingsModal.tsx +++ b/packages/app-core/src/components/SettingsModal.tsx @@ -2436,6 +2436,8 @@ function KeymapSettings({ onSetBinding: (id: KeymapId, binding: string | null) => void onResetAll: () => void }): JSX.Element { + const vimMappings = useStore((s)=> s.vimMappings) + const setVimMappings = useStore((s)=> s.setVimMappings) const [query, setQuery] = useState('') const [recording, setRecording] = useState(null) @@ -2465,6 +2467,21 @@ function KeymapSettings({ return (
+
+
+
.vimrc
+
+ Paste in your config here, one mapping per line. This will only apply into vim motions. +
+
+