esm7_obsidian-vimrc-support/main.ts
2021-05-19 21:44:20 +03:00

241 lines
8.2 KiB
TypeScript

import { App, Plugin, TFile, MarkdownView, PluginSettingTab, Setting } from 'obsidian';
import * as keyFromAccelerator from 'keyboardevent-from-electron-accelerator';
declare const CodeMirror: any;
interface Settings {
vimrcFileName: string
}
const DEFAULT_SETTINGS: Settings = {
vimrcFileName: ".obsidian.vimrc"
}
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
export default class VimrcPlugin extends Plugin {
settings: Settings;
private lastYankBuffer = new Array<string>(0);
private lastSystemClipboard = "";
private yankToSystemClipboard: boolean = false;
async onload() {
console.log('loading Vimrc plugin');
await this.loadSettings();
this.addSettingTab(new SettingsTab(this.app, this))
this.registerEvent(this.app.workspace.on('file-open', (file: TFile) => {
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') });
}));
this.registerDomEvent(document, 'click', () => {
this.captureYankBuffer();
});
this.registerDomEvent(document, 'keyup', () => {
this.captureYankBuffer();
});
this.registerDomEvent(document, 'focusin', () => {
this.captureYankBuffer();
})
}
async loadSettings() {
const data = await this.loadData();
this.settings = Object.assign({}, DEFAULT_SETTINGS, data);
}
async saveSettings() {
await this.saveData(this.settings);
}
onunload() {
console.log('unloading Vimrc plugin (but Vim commands that were already loaded will still work)');
}
readVimInit(vimCommands: string) {
var view = this.app.workspace.activeLeaf.view;
if (view.getViewType() == 'markdown') {
var markdownView = view as MarkdownView;
var cmEditor = markdownView.sourceMode.cmEditor;
if (cmEditor && !CodeMirror.Vim.loadedVimrc) {
CodeMirror.Vim.defineOption('clipboard', '', 'string', ['clip'], (value: string, cm: any) => {
if (value) {
if (value.trim() == 'unnamed' || value.trim() == 'unnamedplus') {
if (!this.yankToSystemClipboard) {
this.yankToSystemClipboard = true;
console.log("Vim is now set to yank to system clipboard.");
}
} else {
throw new Error("Unrecognized clipboard option, supported are 'unnamed' and 'unnamedplus' (and they do the same)")
}
}
});
CodeMirror.Vim.defineOption('tabstop', 4, 'number', [], (value: number, cm: any) => {
if (value) {
cmEditor.setOption('tabSize', value);
}
});
CodeMirror.Vim.defineEx('iunmap', '', (cm: any, params: any) => {
if (params.argString.trim()) {
CodeMirror.Vim.unmap(params.argString.trim(), 'insert');
}
});
CodeMirror.Vim.defineEx('noremap', '', (cm: any, params: any) => {
if (!params?.args?.length) {
throw new Error('Invalid mapping: noremap');
}
if (params.argString.trim()) {
CodeMirror.Vim.noremap.apply(CodeMirror.Vim, params.args);
}
});
// 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] != '"') {
CodeMirror.Vim.handleEx(cmEditor, line);
}
}
)
// Make sure that we load it just once per CodeMirror instance.
// This is supposed to work because the Vim state is kept at the keymap level, hopefully
// there will not be bugs caused by operations that are kept at the object level instead
CodeMirror.Vim.loadedVimrc = true;
}
}
}
captureYankBuffer() {
if (this.yankToSystemClipboard) {
let currentBuffer = CodeMirror.Vim.getRegisterController().getRegister('yank').keyBuffer;
if (currentBuffer != this.lastYankBuffer) {
if (this.lastYankBuffer.length > 0 && currentBuffer.length > 0 && currentBuffer[0]) {
navigator.clipboard.writeText(currentBuffer[0]);
navigator.clipboard.readText().then((value) => { this.lastSystemClipboard = value; });
}
this.lastYankBuffer = currentBuffer;
return;
}
let currentClipboard = navigator.clipboard.readText().then((value) => {
if (value != this.lastSystemClipboard) {
let yankRegister = CodeMirror.Vim.getRegisterController().getRegister('yank')
yankRegister.setText(value);
this.lastYankBuffer = yankRegister.keyBuffer;
this.lastSystemClipboard = value;
}
})
}
}
}
class SettingsTab extends PluginSettingTab {
plugin: VimrcPlugin;
constructor(app: App, plugin: VimrcPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
let {containerEl} = this;
containerEl.empty();
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.vimrcFileName);
if (this.plugin.settings.vimrcFileName !== DEFAULT_SETTINGS.vimrcFileName)
text.setValue(this.plugin.settings.vimrcFileName)
text.onChange(value => {
this.plugin.settings.vimrcFileName = value || DEFAULT_SETTINGS.vimrcFileName;
this.plugin.saveSettings();
})
});
}
}