exmap and obcommand commands

This commit is contained in:
Erez Shermer 2021-05-19 21:44:20 +03:00
parent 06d4016524
commit acf73336fa
5 changed files with 146 additions and 18 deletions

View file

@ -39,6 +39,8 @@ In addition to that:
- Special support for yanking to system clipboard can be activated by `set clipboard=unnamed` (`unnamedplus` will do the same thing).
- Support for the `tabstop` Vim option (e.g. `set tabstop=4`).
- Custom mapping/unmapping commands in addition to the defaults: `noremap` and `iunmap` (PRs are welcome to implement more :) )
- `exmap [commandName] [command...]`: a command to map Ex commands. This should basically be supported in regular `:map`, but doesn't work with multi-argument commands due to a CodeMirror bug, so this is a workaround.
- `obcommand` - execute Obsidian commands, see more details below.
Commands that fail don't generate any visible error for now.
@ -65,8 +67,54 @@ Things I'd love to add:
- Implement some standard `vim-markdown` motions for Obsidian, e.g. `[[`, or implement for CodeMirror the 1-2 missing Ex commands required to define these keymaps in the Vimrc.
- Relative line numbers.
## Change Vimrc File Location/Name
If you want the Vimrc file name or path to be different than the default, there is a plugin setting (under Settings | Plugin Options | Vimrc Support) to change that.
## Executing Obsidian Commands with `obcommand`
The plugin defines a custom Ex command named `obcommand` to execute various Obsidian commands as an Ex command.
This is useful to map external functionality of Obsidian or other plugins to Vim shortcuts, but it's not as easy to use as one would hope.
If you just type `:obcommand` you'll get in the Developer Console (Ctrl+Shift+I) the list of commands that are currently defined by the app.
The simple syntax `:obcommand [commandName]` will execute the named command.
Some useful examples:
- `obcommand editor:insert-link`
- `obcommand editor:toggle-comment`
- `obcommand app:go-back`
- `obcommand workspace:split-vertical`
And countless more.
**WARNING:** this is not a formal API that Obsidian provides and is done in a rather hacky manner.
It's definitely possible that some future version of Obsidian will break this functionality.
### Mapping Obsidian Commands Within Vim
Next thing you probably wanna ask is "how do I map the great Obsidian commands to Vim commands?"
The trivial answer should have been something along the line of `:nmap <C-o> :obcommand app:go-back`, but this **does not work** because of an annoying CodeMirror bug.
Turns out that the various mapping commands of CodeMirror pass only the first argument, so when you execute your mapping if defined as above, `:obcommand` would execute with no arguments.
Here comes a custom command to the rescue, `exmap`, which you can use to "alias" Ex commands for longer Ex commands: `:exmap back obcommand app:go-back`.
You now have a simple (0 argument) Ex command named `back` that goes back in Obsidian, and *that* is something you can map.
To summarize, here's how you map `C-o` to Back:
```
exmap back obcommand app:go-back
nmap <C-o> :back
```
Note how `exmap` lists command names without colons and in `nmap` the colon is required.
## Changelog
### 0.3.0
- Added a settings file for the Vimrc file name (thank you @SalmanAlSaigal!)
- Added the `exmap` Ex command.
- Added the `obcommand` Ex command.
### 0.2.4
Fixed a race condition with yank & paste: https://github.com/esm7/obsidian-vimrc-support/issues/11

108
main.ts
View file

@ -1,12 +1,18 @@
import { App, Plugin, TFile, MarkdownView, PluginSettingTab, Setting } from 'obsidian';
import * as keyFromAccelerator from 'keyboardevent-from-electron-accelerator';
declare const CodeMirror: any;
interface Settings {
VIMRC_NAME: string;
vimrcFileName: string
}
const DEFAULT_SETTINGS: Settings = {
VIMRC_NAME: ".obsidian.vimrc"
vimrcFileName: ".obsidian.vimrc"
}
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
export default class VimrcPlugin extends Plugin {
@ -23,7 +29,7 @@ export default class VimrcPlugin extends Plugin {
this.addSettingTab(new SettingsTab(this.app, this))
this.registerEvent(this.app.workspace.on('file-open', (file: TFile) => {
const VIMRC_FILE_NAME = this.settings.VIMRC_NAME;
const VIMRC_FILE_NAME = this.settings.vimrcFileName;
this.app.vault.adapter.read(VIMRC_FILE_NAME).
then((lines) => this.readVimInit(lines)).
catch(error => { console.log('Error loading vimrc file', VIMRC_FILE_NAME, 'from the vault root') });
@ -40,12 +46,12 @@ export default class VimrcPlugin extends Plugin {
})
}
async loadSettings(){
async loadSettings() {
const data = await this.loadData();
this.settings = Object.assign({}, DEFAULT_SETTINGS, data);
}
async saveSettings(){
async saveSettings() {
await this.saveData(this.settings);
}
@ -59,7 +65,7 @@ export default class VimrcPlugin extends Plugin {
var markdownView = view as MarkdownView;
var cmEditor = markdownView.sourceMode.cmEditor;
if (cmEditor && !CodeMirror.Vim.loadedVimrc) {
CodeMirror.Vim.defineOption('clipboard', '', 'string', ['clip'], (value, cm) => {
CodeMirror.Vim.defineOption('clipboard', '', 'string', ['clip'], (value: string, cm: any) => {
if (value) {
if (value.trim() == 'unnamed' || value.trim() == 'unnamedplus') {
if (!this.yankToSystemClipboard) {
@ -71,19 +77,19 @@ export default class VimrcPlugin extends Plugin {
}
}
});
CodeMirror.Vim.defineOption('tabstop', 4, 'number', [], (value, cm) => {
CodeMirror.Vim.defineOption('tabstop', 4, 'number', [], (value: number, cm: any) => {
if (value) {
cmEditor.setOption('tabSize', value);
}
});
CodeMirror.Vim.defineEx('iunmap', '', (cm, params) => {
CodeMirror.Vim.defineEx('iunmap', '', (cm: any, params: any) => {
if (params.argString.trim()) {
CodeMirror.Vim.unmap(params.argString.trim(), 'insert');
}
});
CodeMirror.Vim.defineEx('noremap', '', (cm, params) => {
CodeMirror.Vim.defineEx('noremap', '', (cm: any, params: any) => {
if (!params?.args?.length) {
throw new Error('Invalid mapping: noremap');
}
@ -93,6 +99,78 @@ export default class VimrcPlugin extends Plugin {
}
});
// Allow the user to register an Ex command
CodeMirror.Vim.defineEx('exmap', '', (cm: any, params: any) => {
if (params?.args?.length && params.args.length < 2) {
throw new Error(`exmap requires at least 2 parameters: [name] [actions...]`);
}
let commandName = params.args[0];
params.args.shift();
let commandContent = params.args.join(' ');
// The content of the user's Ex command is just the remaining parameters of the exmap command
CodeMirror.Vim.defineEx(commandName, '', (cm: any, params: any) => {
CodeMirror.Vim.handleEx(cm, commandContent);
});
});
CodeMirror.Vim.defineEx('sendkeys', '', async (cm: any, params: any) => {
if (!params?.args?.length) {
console.log(params);
throw new Error(`The sendkeys command requires a list of keys, e.g. sendKeys Ctrl+p a b Enter`);
}
let allGood = true;
let events: KeyboardEvent[] = [];
for (const key of params.args) {
if (key.startsWith('wait')) {
const delay = key.slice(4);
await sleep(delay * 1000);
}
else {
let keyEvent: KeyboardEvent = null;
try {
keyEvent = new KeyboardEvent('keydown', keyFromAccelerator.toKeyEvent(key));
events.push(keyEvent);
}
catch (e) {
allGood = false;
throw new Error(`Key '${key}' couldn't be read as an Electron Accelerator`);
}
if (allGood) {
for (keyEvent of events)
window.postMessage(JSON.parse(JSON.stringify(keyEvent)), '*');
// view.containerEl.dispatchEvent(keyEvent);
}
}
}
});
CodeMirror.Vim.defineEx('obcommand', '', async (cm: any, params: any) => {
const availableCommands = (this.app as any).commands.commands;
if (!params?.args?.length || params.args.length != 1) {
console.log(`Available commands: ${Object.keys(availableCommands).join('\n')}`)
throw new Error(`obcommand requires exactly 1 parameter`);
}
const command = params.args[0];
if (command in availableCommands) {
let callback = availableCommands[command].callback;
let checkCallback = availableCommands[command].checkCallback;
let editorCallback = availableCommands[command].editorCallback;
let editorCheckCallback = availableCommands[command].editorCheckCallback;
if (editorCheckCallback)
editorCheckCallback(markdownView.editor, false);
else if (editorCallback)
editorCallback(markdownView.editor);
else if (checkCallback)
checkCallback(false);
else if (callback)
callback();
else
throw new Error(`Command ${command} doesn't have an Obsidian callback`);
} else
throw new Error(`Command ${command} was not found, try 'obcommand' with no params to see in the developer console what's available`);
});
vimCommands.split("\n").forEach(
function(line, index, arr) {
if (line.trim().length > 0 && line.trim()[0] != '"') {
@ -145,19 +223,19 @@ class SettingsTab extends PluginSettingTab {
containerEl.empty();
containerEl.createEl('h2', {text: 'Obsidian Vimrc Support Settings'});
containerEl.createEl('h2', {text: 'Vimrc Settings'});
new Setting(containerEl)
.setName('Vimrc file name')
.setDesc('Relative to vault directory (requires restart)')
.addText((text) => {
text.setPlaceholder(DEFAULT_SETTINGS.VIMRC_NAME);
if(this.plugin.settings.VIMRC_NAME !== DEFAULT_SETTINGS.VIMRC_NAME)
text.setValue(this.plugin.settings.VIMRC_NAME)
text.setPlaceholder(DEFAULT_SETTINGS.vimrcFileName);
if (this.plugin.settings.vimrcFileName !== DEFAULT_SETTINGS.vimrcFileName)
text.setValue(this.plugin.settings.vimrcFileName)
text.onChange(value => {
this.plugin.settings.VIMRC_NAME = value || DEFAULT_SETTINGS.VIMRC_NAME;
this.plugin.settings.vimrcFileName = value || DEFAULT_SETTINGS.vimrcFileName;
this.plugin.saveSettings();
})
});
}
}
}

View file

@ -1,7 +1,7 @@
{
"id": "obsidian-vimrc-support",
"name": "Obsidian Vimrc Support",
"version": "0.2.4",
"name": "Vimrc Support",
"version": "0.3.0",
"description": "Auto-load a startup file with Obsidian Vim commands.",
"author": "esm",
"authorUrl": "",

View file

@ -1,6 +1,6 @@
{
"name": "obsidian-vimrc-support",
"version": "0.2.4",
"version": "0.3.0",
"description": "Auto-load a startup file with Obsidian Vim commands.",
"main": "main.js",
"scripts": {
@ -15,6 +15,7 @@
"@rollup/plugin-node-resolve": "^9.0.0",
"@rollup/plugin-typescript": "^6.1.0",
"@types/node": "^14.14.6",
"keyboardevent-from-electron-accelerator": "*",
"obsidian": "https://github.com/obsidianmd/obsidian-api/tarball/master",
"rollup": "^2.33.0",
"tslib": "^2.0.3",

1
types.d.ts vendored Normal file
View file

@ -0,0 +1 @@
declare module 'keyboardevent-from-electron-accelerator';