diff --git a/README.md b/README.md index 5960457..11b1b06 100644 --- a/README.md +++ b/README.md @@ -217,6 +217,32 @@ But first `` must be unbound with `unmap `. Afterwards `` can be mapped normally as any other key. +## Leader Key Support + +You can use the standard Vim `` key in your mappings. Set your leader key with `let mapleader` and then use `` in any mapping command: + +```vim +let mapleader = "," +nmap f :nohl +nmap w :obcommand editor:save-file +``` + +The default leader key is `\` (backslash), matching Vim's default. + +`` works in all mapping commands (`map`, `nmap`, `noremap`, `imap`, `vmap`, etc.) and is case-insensitive — ``, ``, and `` all work. + +If you set `let mapleader = ""`, you should also add `unmap ` before your leader mappings to free Space from its default binding: + +```vim +let mapleader = "" +unmap +nmap fs :obcommand editor:save-file +``` + +Space continues to work normally in insert mode. + +You can change the leader key mid-file — each `let mapleader` only affects mappings that come after it, matching Vim's behavior. + ## Emulate Common Vim Commands via Obsidian commands Using `obcommand`, it is possible to emulate some additional Vim commands that aren't included in Obsidian's Vim mode, like for example `gt` and `zo`. diff --git a/main.ts b/main.ts index 2a1471b..d0ec80b 100644 --- a/main.ts +++ b/main.ts @@ -58,6 +58,14 @@ const mappingCommands: String[] = [ "vunmap", ] +// All Ex commands that take key-sequence arguments where should be expanded. +// This is broader than mappingCommands because CodeMirror supports more mapping variants. +const leaderMapCommands: string[] = [ + "map", "nmap", "imap", "vmap", "omap", + "noremap", "nnoremap", "vnoremap", "inoremap", "onoremap", + "unmap", "iunmap", "nunmap", "vunmap", +]; + function sleep(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); } @@ -79,6 +87,7 @@ export default class VimrcPlugin extends Plugin { private customVimKeybinds: { [name: string]: boolean } = {}; private currentSelection: [EditorSelection] = null; private isInsertMode: boolean = false; + private leaderKey: string = "\\"; updateVimStatusBar() { this.vimStatusBar.setText( @@ -315,18 +324,45 @@ export default class VimrcPlugin extends Plugin { vimCommands.split("\n").forEach( function (line: string, index: number, arr: [string]) { if (line.trim().length > 0 && line.trim()[0] != '"') { - let split = line.split(" ") + // Parse "let mapleader" directives (consumed, not forwarded to CodeMirror). + const leader = this.parseLeaderDirective(line.trim()); + if (leader !== null) { + this.leaderKey = leader; + return; + } + + // Substitute in mapping commands before handing to CodeMirror. + const processedLine = this.substituteLeader(line, this.leaderKey); + + let split = processedLine.split(" ") if (mappingCommands.includes(split[0])) { // Have to do this because "vim-command-done" event doesn't actually work properly, or something. this.customVimKeybinds[split[1]] = true } - this.codeMirrorVimObject.handleEx(cmEditor, line); + this.codeMirrorVimObject.handleEx(cmEditor, processedLine); } }.bind(this) // Faster than an arrow function. https://stackoverflow.com/questions/50375440/binding-vs-arrow-function-for-react-onclick-event ) } } + private parseLeaderDirective(line: string): string | null { + // Match: let mapleader = "x" or let mapleader = 'x' + // Case-insensitive on "let mapleader" to be forgiving, matching Vim behavior. + const match = line.match(/^\s*let\s+mapleader\s*=\s*["'](.+?)["']\s*$/i); + return match ? match[1] : null; + } + + private substituteLeader(line: string, leaderKey: string): string { + const trimmed = line.trim(); + const firstSpace = trimmed.indexOf(" "); + if (firstSpace === -1) return line; + const command = trimmed.substring(0, firstSpace); + if (!leaderMapCommands.includes(command)) return line; + // Replace all occurrences of (case-insensitive) with the leader key. + return line.replace(//gi, leaderKey); + } + defineBasicCommands(vimObject: any) { vimObject.defineOption('clipboard', '', 'string', ['clip'], (value: string, cm: any) => { if (value) {