Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,32 @@ But first `<Space>` must be unbound with `unmap <Space>`.
Afterwards `<Space>` can be mapped normally as any other key.


## Leader Key Support

You can use the standard Vim `<leader>` key in your mappings. Set your leader key with `let mapleader` and then use `<leader>` in any mapping command:

```vim
let mapleader = ","
nmap <leader>f :nohl<CR>
nmap <leader>w :obcommand editor:save-file<CR>
```

The default leader key is `\` (backslash), matching Vim's default.

`<leader>` works in all mapping commands (`map`, `nmap`, `noremap`, `imap`, `vmap`, etc.) and is case-insensitive — `<Leader>`, `<LEADER>`, and `<leader>` all work.

If you set `let mapleader = "<Space>"`, you should also add `unmap <Space>` before your leader mappings to free Space from its default binding:

```vim
let mapleader = "<Space>"
unmap <Space>
nmap <leader>fs :obcommand editor:save-file<CR>
```

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`.
Expand Down
40 changes: 38 additions & 2 deletions main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ const mappingCommands: String[] = [
"vunmap",
]

// All Ex commands that take key-sequence arguments where <leader> 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));
}
Expand All @@ -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(
Expand Down Expand Up @@ -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 <leader> 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 <leader> (case-insensitive) with the leader key.
return line.replace(/<leader>/gi, leaderKey);
}

defineBasicCommands(vimObject: any) {
vimObject.defineOption('clipboard', '', 'string', ['clip'], (value: string, cm: any) => {
if (value) {
Expand Down