Vim mode and chord display, cleanup, surround, pasteinto (#37)

* Added indicators for vim mode and current vim command chord

* Added settings page. Fixed custom keybind problems. Desktop only.

Added the settings page toggles for the chord and vim mode display.
Fixed the custom keybinds not resetting when completing. The
"vim-command-done" event seems to be not registering them. Maybe a
problem with initialization of the keys?
Changed the plugin to be desktop only. It's unlikely that someone could
use this on mobile, and cmEditor is deprecated and will only work on
desktop for now.

* Added command for surrounding current selection with strings

* Hotfix: cleanup forgotten code

* Added surround command and cleaned up

* Added surround action

* Cleaned up, changed var to let, added pasteinto ex command
This commit is contained in:
Andr3wD 2021-07-28 10:20:18 +00:00 committed by GitHub
parent 00861b6544
commit 907c8fe59d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 226 additions and 12 deletions

3
.gitignore vendored
View file

@ -2,6 +2,9 @@
*.iml
.idea
# My deployment script
*.sh
# npm
node_modules
package-lock.json

View file

@ -48,6 +48,8 @@ In addition to that:
- 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.
- `surround` - surround your selected text in visual mode or word in normal mode with text.
- `pasteinto` - paste your current clipboard into your selected text in visual mode or word in normal mode. Useful for creating hyperlinks.
Commands that fail don't generate any visible error for now.
@ -97,6 +99,37 @@ And many 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.
## Surround Text with `surround`
The plugin defines a custom Ex command named `surround` to surround either your currently selected text in visual mode or the word your cursor is over in normal mode with text.
This is particularly useful for creating wikilinks in Obsidian `[[WikiLink]]`.
The syntax follows `:surround [prefixText] [postfixText]`.
Some examples:
- `surround ( )`
- `surround [[ ]]`
Here's my surround config as an example:
```
" Surround text with [[ ]] to make a wikilink
" NOTE: must use 'map' and not 'nmap'
exmap wiki surround [[ ]]
map [[ :wiki
```
## Inserting Links/Hyperlinks with `pasteinto`
The plugin defines a custom Ex command named `pasteinto` to paste text into your currently selected text in visual mode, or the word your cursor is over in normal mode.
This is particularly useful for creating links/hyperlinks `[selected-text](paste)`.
Here's my hyperlink config as an example:
```
" Maps pasteinto to Alt-p
map <A-p> :pasteinto
```
### Mapping Obsidian Commands Within Vim
Next thing you probably wanna ask is "how do I map the great Obsidian commands to Vim commands?"
@ -115,6 +148,12 @@ nmap <C-o> :back
Note how `exmap` lists command names without colons and in `nmap` the colon is required.
## Some Help with Binding Space Chords (Doom and Spacemacs fans)
`<Space>` can be bound to make chords, such as `<Space>fs` in conjunction with obcommand to save your current file and more.
But first `<Space>` must be unbound with `unmap <Space>`.
Afterwards `<Space>` can be mapped normally as any other key.
## Changelog
### 0.3.1

194
main.ts
View file

@ -1,16 +1,34 @@
import { App, Plugin, TFile, MarkdownView, PluginSettingTab, Setting } from 'obsidian';
import * as keyFromAccelerator from 'keyboardevent-from-electron-accelerator';
import { App, MarkdownView, Plugin, PluginSettingTab, Setting, TFile } from 'obsidian';
declare const CodeMirror: any;
interface Settings {
vimrcFileName: string
vimrcFileName: string,
displayChord: boolean,
displayVimMode: boolean
}
const DEFAULT_SETTINGS: Settings = {
vimrcFileName: ".obsidian.vimrc"
vimrcFileName: ".obsidian.vimrc",
displayChord: false,
displayVimMode: false
}
const enum vimStatus {
normal = "🟢",
insert = "🟠",
replace = "🔴",
visual = "🟡"
}
// NOTE: to future maintainers, please make sure all mapping commands are included in this array.
const mappingCommands: String[] = [
"map",
"nmap",
"noremap",
]
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
@ -21,6 +39,12 @@ export default class VimrcPlugin extends Plugin {
private lastYankBuffer = new Array<string>(0);
private lastSystemClipboard = "";
private yankToSystemClipboard: boolean = false;
private currentKeyChord: any = [];
private vimChordStatusBar: HTMLElement = null;
private vimStatusBar: HTMLElement = null;
private currentVimStatus: vimStatus = vimStatus.normal;
private customVimKeybinds: { [name: string]: boolean } = {};
private currentSelection: CodeMirror.Range = null;
async onload() {
console.log('loading Vimrc plugin');
@ -77,6 +101,7 @@ export default class VimrcPlugin extends Plugin {
}
}
});
CodeMirror.Vim.defineOption('tabstop', 4, 'number', [], (value: number, cm: any) => {
if (value) {
cmEditor.setOption('tabSize', value);
@ -93,7 +118,7 @@ export default class VimrcPlugin extends Plugin {
if (!params?.args?.length) {
throw new Error('Invalid mapping: noremap');
}
if (params.argString.trim()) {
CodeMirror.Vim.noremap.apply(CodeMirror.Vim, params.args);
}
@ -112,13 +137,13 @@ export default class VimrcPlugin extends Plugin {
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) {
@ -139,7 +164,7 @@ export default class VimrcPlugin extends Plugin {
if (allGood) {
for (keyEvent of events)
window.postMessage(JSON.parse(JSON.stringify(keyEvent)), '*');
// view.containerEl.dispatchEvent(keyEvent);
// view.containerEl.dispatchEvent(keyEvent);
}
}
}
@ -171,14 +196,137 @@ export default class VimrcPlugin extends Plugin {
throw new Error(`Command ${command} was not found, try 'obcommand' with no params to see in the developer console what's available`);
});
// Function to surround selected text or highlighted word.
var surroundFunc = (cm: CodeMirror.Editor, params: any) => {
if (!params?.args?.length || params.args.length != 2) {
throw new Error("surround requires exactly 2 parameters: prefix and postfix text.")
}
let beginning = params.args[0] // Get the beginning surround text
let ending = params.args[1] // Get the ending surround text
if (this.currentSelection.anchor == this.currentSelection.head) {
// No range of selected text, so select word.
let wordRange = cmEditor.findWordAt(this.currentSelection.anchor)
let currText = cmEditor.getRange(wordRange.from(), wordRange.to())
cmEditor.replaceRange(beginning + currText + ending, wordRange.from(), wordRange.to())
} else {
let currText = cmEditor.getRange(this.currentSelection.from(), this.currentSelection.to())
cmEditor.replaceRange(beginning + currText + ending, this.currentSelection.from(), this.currentSelection.to())
}
}
CodeMirror.Vim.defineEx("surround", "", surroundFunc);
CodeMirror.Vim.defineEx("pasteinto", "", (cm: CodeMirror.Editor, params: any) => {
// Using the register for when this.yankToSystemClipboard == false
surroundFunc(cm, { args: ["[", "](" + CodeMirror.Vim.getRegisterController().getRegister('yank').keyBuffer + ")"] })
})
// Handle the surround dialog input
var surroundDialogCallback = (value: string) => {
if ((/^\[+$/).test(value)) { // check for 1-inf [ and match them with ]
surroundFunc(cmEditor, { args: [value, "]".repeat(value.length)] })
} else if ((/^\(+$/).test(value)) { // check for 1-inf ( and match them with )
surroundFunc(cmEditor, { args: [value, ")".repeat(value.length)] })
} else if ((/^\{+$/).test(value)) { // check for 1-inf { and match them with }
surroundFunc(cmEditor, { args: [value, "}".repeat(value.length)] })
} else { // Else, just put it before and after.
surroundFunc(cmEditor, { args: [value, value] })
}
}
CodeMirror.Vim.defineOperator("surroundOperator", (cm: any, args: any, ranges: any) => {
let p = "<span>Surround with: <input type='text'></span>"
cm.openDialog(p, surroundDialogCallback, { bottom: true, selectValueOnOpen: false })
})
CodeMirror.Vim.mapCommand("<A-y>s", "operator", "surroundOperator")
// CodeMirror.Vim.mapCommand("<A-d>s", "operator", "surroundOperator")
// Record the position of selections
CodeMirror.on(cmEditor, "cursorActivity", async (cm: any) => {
this.currentSelection = cmEditor.listSelections()[0]
})
vimCommands.split("\n").forEach(
function(line, index, arr) {
function (line: string, index: number, arr: [string]) {
if (line.trim().length > 0 && line.trim()[0] != '"') {
let split = line.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
}
CodeMirror.Vim.handleEx(cmEditor, line);
}
}
}.bind(this) // Faster than an arrow function. https://stackoverflow.com/questions/50375440/binding-vs-arrow-function-for-react-onclick-event
)
if (this.settings.displayChord) {
// Add status bar item
this.vimChordStatusBar = this.addStatusBarItem()
// Move vimChordStatusBar to the leftmost position and center it.
let parent = this.vimChordStatusBar.parentElement
this.vimChordStatusBar.parentElement.insertBefore(this.vimChordStatusBar, parent.firstChild)
this.vimChordStatusBar.style.marginRight = "auto"
// this.vimChordStatusBar.style.marginLeft = "auto"
// See https://codemirror.net/doc/manual.html#vimapi_events for events.
CodeMirror.on(cmEditor, "vim-keypress", async (vimKey: any) => {
if (vimKey != "<Esc>") { // TODO figure out what to actually look for to exit commands.
this.currentKeyChord.push(vimKey);
if (this.customVimKeybinds[this.currentKeyChord.join("")] != undefined) { // Custom key chord exists.
this.currentKeyChord = [];
}
} else {
this.currentKeyChord = [];
}
// Build keychord text
let tempS = ""
for (const s of this.currentKeyChord) {
tempS += " " + s
}
if (tempS != "") {
tempS += "-"
}
this.vimChordStatusBar.setText(tempS);
});
CodeMirror.on(cmEditor, "vim-command-done", async (reason: any) => { // Reset display
this.vimChordStatusBar.setText("");
this.currentKeyChord = [];
});
}
if (this.settings.displayVimMode) {
this.vimStatusBar = this.addStatusBarItem() // Add status bar item
this.vimStatusBar.setText(vimStatus.normal) // Init the vimStatusBar with normal mode
// See https://codemirror.net/doc/manual.html#vimapi_events for events.
CodeMirror.on(cmEditor, "vim-mode-change", async (modeObj: any) => {
switch (modeObj.mode) {
case "insert":
this.currentVimStatus = vimStatus.insert;
break;
case "normal":
this.currentVimStatus = vimStatus.normal;
break;
case "visual":
this.currentVimStatus = vimStatus.visual;
break;
case "replace":
this.currentVimStatus = vimStatus.replace;
break;
default:
break;
}
this.vimStatusBar.setText(this.currentVimStatus);
});
}
// 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
@ -219,11 +367,11 @@ class SettingsTab extends PluginSettingTab {
}
display(): void {
let {containerEl} = this;
let { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', {text: 'Vimrc Settings'});
containerEl.createEl('h2', { text: 'Vimrc Settings' });
new Setting(containerEl)
.setName('Vimrc file name')
@ -237,5 +385,29 @@ class SettingsTab extends PluginSettingTab {
this.plugin.saveSettings();
})
});
new Setting(containerEl)
.setName('Vim chord display')
.setDesc('Displays the current chord until completion. Ex: "<Space> f-" (requires restart)')
.addToggle((toggle) => {
if (this.plugin.settings.displayChord !== DEFAULT_SETTINGS.displayChord)
toggle.setValue(this.plugin.settings.displayChord)
toggle.onChange(value => {
this.plugin.settings.displayChord = value || DEFAULT_SETTINGS.displayChord;
this.plugin.saveSettings();
})
});
new Setting(containerEl)
.setName('Vim mode display')
.setDesc('Displays the current vim mode (requires restart)')
.addToggle((toggle) => {
if (this.plugin.settings.displayVimMode !== DEFAULT_SETTINGS.displayVimMode)
toggle.setValue(this.plugin.settings.displayVimMode)
toggle.onChange(value => {
this.plugin.settings.displayVimMode = value || DEFAULT_SETTINGS.displayVimMode;
this.plugin.saveSettings();
})
});
}
}

View file

@ -5,5 +5,5 @@
"description": "Auto-load a startup file with Obsidian Vim commands.",
"author": "esm",
"authorUrl": "",
"isDesktopOnly": false
"isDesktopOnly": true
}