From acf73336fa6024de0d70bbc4d8f9677d5a759fd2 Mon Sep 17 00:00:00 2001 From: Erez Shermer Date: Wed, 19 May 2021 21:44:20 +0300 Subject: [PATCH] exmap and obcommand commands --- README.md | 48 ++++++++++++++++++++++ main.ts | 108 +++++++++++++++++++++++++++++++++++++++++++------- manifest.json | 4 +- package.json | 3 +- types.d.ts | 1 + 5 files changed, 146 insertions(+), 18 deletions(-) create mode 100644 types.d.ts diff --git a/README.md b/README.md index 7f07f2b..c76aa05 100644 --- a/README.md +++ b/README.md @@ -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 :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 :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 diff --git a/main.ts b/main.ts index cbd2590..0b9b9d0 100644 --- a/main.ts +++ b/main.ts @@ -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(); }) }); } -} \ No newline at end of file +} diff --git a/manifest.json b/manifest.json index 5876362..55664e1 100644 --- a/manifest.json +++ b/manifest.json @@ -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": "", diff --git a/package.json b/package.json index 53dac13..f68c524 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/types.d.ts b/types.d.ts new file mode 100644 index 0000000..2c2e501 --- /dev/null +++ b/types.d.ts @@ -0,0 +1 @@ +declare module 'keyboardevent-from-electron-accelerator';