From 4709bdf8b75f1d43556c6d09b8c74b843f4c0cb0 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Wed, 22 Jul 2026 18:16:14 -0400 Subject: [PATCH] No more Vim --- .obsidian.vimrc | 3 + app.json | 2 +- community-plugins.json | 3 +- hotkeys.json | 17 +- plugins/obsidian-vimrc-support/data.json | 14 - plugins/obsidian-vimrc-support/main.js | 1247 ------------------ plugins/obsidian-vimrc-support/manifest.json | 10 - 7 files changed, 21 insertions(+), 1275 deletions(-) delete mode 100644 plugins/obsidian-vimrc-support/data.json delete mode 100644 plugins/obsidian-vimrc-support/main.js delete mode 100644 plugins/obsidian-vimrc-support/manifest.json diff --git a/.obsidian.vimrc b/.obsidian.vimrc index dd58b93..5f0cfb2 100644 --- a/.obsidian.vimrc +++ b/.obsidian.vimrc @@ -10,3 +10,6 @@ nmap gT :tabprev exmap tabclose obcommand workspace:close exmap wq obcommand workspace:close exmap q obcommand workspace:close + +exmap followlink obcommand editor:follow-link +nmap gd :followlink diff --git a/app.json b/app.json index e0a6a0b..7155f01 100644 --- a/app.json +++ b/app.json @@ -1,5 +1,5 @@ { - "vimMode": true, + "vimMode": false, "showUnsupportedFiles": true, "livePreview": true, "showInlineTitle": false, diff --git a/community-plugins.json b/community-plugins.json index 63b0db9..aa7b56a 100644 --- a/community-plugins.json +++ b/community-plugins.json @@ -3,6 +3,5 @@ "obsidian-hider", "calendar", "multi-vault-links", - "typewriter-mode", - "obsidian-vimrc-support" + "typewriter-mode" ] \ No newline at end of file diff --git a/hotkeys.json b/hotkeys.json index 72f123c..93eed2d 100644 --- a/hotkeys.json +++ b/hotkeys.json @@ -371,5 +371,20 @@ "key": "O" } ], - "workspace:close": [] + "workspace:close": [ + { + "modifiers": [ + "Mod" + ], + "key": "W" + } + ], + "editor:follow-link": [ + { + "modifiers": [ + "Alt" + ], + "key": "Enter" + } + ] } \ No newline at end of file diff --git a/plugins/obsidian-vimrc-support/data.json b/plugins/obsidian-vimrc-support/data.json deleted file mode 100644 index 218688f..0000000 --- a/plugins/obsidian-vimrc-support/data.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "vimrcFileName": ".obsidian/.obsidian.vimrc", - "displayChord": false, - "displayVimMode": false, - "fixedNormalModeLayout": false, - "capturedKeyboardMap": {}, - "supportJsCommands": false, - "vimStatusPromptMap": { - "normal": "🟢", - "insert": "🟠", - "visual": "🟡", - "replace": "🔴" - } -} \ No newline at end of file diff --git a/plugins/obsidian-vimrc-support/main.js b/plugins/obsidian-vimrc-support/main.js deleted file mode 100644 index 10bbf0d..0000000 --- a/plugins/obsidian-vimrc-support/main.js +++ /dev/null @@ -1,1247 +0,0 @@ -'use strict'; - -var obsidian = require('obsidian'); - -const modifiers = /^(CommandOrControl|CmdOrCtrl|Command|Cmd|Control|Ctrl|AltGr|Option|Alt|Shift|Super)/i; -const keyCodes = /^(Plus|Space|Tab|Backspace|Delete|Insert|Return|Enter|Up|Down|Left|Right|Home|End|PageUp|PageDown|Escape|Esc|VolumeUp|VolumeDown|VolumeMute|MediaNextTrack|MediaPreviousTrack|MediaStop|MediaPlayPause|PrintScreen|F24|F23|F22|F21|F20|F19|F18|F17|F16|F15|F14|F13|F12|F11|F10|F9|F8|F7|F6|F5|F4|F3|F2|F1|[0-9A-Z)!@#$%^&*(:+<_>?~{|}";=,\-./`[\\\]'])/i; -const UNSUPPORTED = {}; - -function _command(accelerator, event, modifier) { - if (process.platform !== 'darwin') { - return UNSUPPORTED; - } - - if (event.metaKey) { - throw new Error('Double `Command` modifier specified.'); - } - - return { - event: Object.assign({}, event, {metaKey: true}), - accelerator: accelerator.slice(modifier.length) - }; -} - -function _super(accelerator, event, modifier) { - if (event.metaKey) { - throw new Error('Double `Super` modifier specified.'); - } - - return { - event: Object.assign({}, event, {metaKey: true}), - accelerator: accelerator.slice(modifier.length) - }; -} - -function _commandorcontrol(accelerator, event, modifier) { - if (process.platform === 'darwin') { - if (event.metaKey) { - throw new Error('Double `Command` modifier specified.'); - } - - return { - event: Object.assign({}, event, {metaKey: true}), - accelerator: accelerator.slice(modifier.length) - }; - } - - if (event.ctrlKey) { - throw new Error('Double `Control` modifier specified.'); - } - - return { - event: Object.assign({}, event, {ctrlKey: true}), - accelerator: accelerator.slice(modifier.length) - }; -} - -function _alt(accelerator, event, modifier) { - if (modifier === 'option' && process.platform !== 'darwin') { - return UNSUPPORTED; - } - - if (event.altKey) { - throw new Error('Double `Alt` modifier specified.'); - } - - return { - event: Object.assign({}, event, {altKey: true}), - accelerator: accelerator.slice(modifier.length) - }; -} - -function _shift(accelerator, event, modifier) { - if (event.shiftKey) { - throw new Error('Double `Shift` modifier specified.'); - } - - return { - event: Object.assign({}, event, {shiftKey: true}), - accelerator: accelerator.slice(modifier.length) - }; -} - -function _control(accelerator, event, modifier) { - if (event.ctrlKey) { - throw new Error('Double `Control` modifier specified.'); - } - - return { - event: Object.assign({}, event, {ctrlKey: true}), - accelerator: accelerator.slice(modifier.length) - }; -} - -function reduceModifier({accelerator, event}, modifier) { - switch (modifier) { - case 'command': - case 'cmd': { - return _command(accelerator, event, modifier); - } - - case 'super': { - return _super(accelerator, event, modifier); - } - - case 'control': - case 'ctrl': { - return _control(accelerator, event, modifier); - } - - case 'commandorcontrol': - case 'cmdorctrl': { - return _commandorcontrol(accelerator, event, modifier); - } - - case 'option': - case 'altgr': - case 'alt': { - return _alt(accelerator, event, modifier); - } - - case 'shift': { - return _shift(accelerator, event, modifier); - } - - default: - console.error(modifier); - } -} - -function reducePlus({accelerator, event}) { - return { - event, - accelerator: accelerator.trim().slice(1) - }; -} - -const virtualKeyCodes = { - 0: 'Digit0', - 1: 'Digit1', - 2: 'Digit2', - 3: 'Digit3', - 4: 'Digit4', - 5: 'Digit5', - 6: 'Digit6', - 7: 'Digit7', - 8: 'Digit8', - 9: 'Digit9', - '-': 'Minus', - '=': 'Equal', - Q: 'KeyQ', - W: 'KeyW', - E: 'KeyE', - R: 'KeyR', - T: 'KeyT', - Y: 'KeyY', - U: 'KeyU', - I: 'KeyI', - O: 'KeyO', - P: 'KeyP', - '[': 'BracketLeft', - ']': 'BracketRight', - A: 'KeyA', - S: 'KeyS', - D: 'KeyD', - F: 'KeyF', - G: 'KeyG', - H: 'KeyH', - J: 'KeyJ', - K: 'KeyK', - L: 'KeyL', - ';': 'Semicolon', - '\'': 'Quote', - '`': 'Backquote', - '/': 'Backslash', - Z: 'KeyZ', - X: 'KeyX', - C: 'KeyC', - V: 'KeyV', - B: 'KeyB', - N: 'KeyN', - M: 'KeyM', - ',': 'Comma', - '.': 'Period', - '\\': 'Slash', - ' ': 'Space' -}; - -function reduceKey({accelerator, event}, key) { - if (key.length > 1 || event.key) { - throw new Error(`Unvalid keycode \`${key}\`.`); - } - - const code = - key.toUpperCase() in virtualKeyCodes ? - virtualKeyCodes[key.toUpperCase()] : - null; - - return { - event: Object.assign({}, event, {key}, code ? {code} : null), - accelerator: accelerator.trim().slice(key.length) - }; -} - -const domKeys = Object.assign(Object.create(null), { - plus: 'Add', - space: 'Space', - tab: 'Tab', - backspace: 'Backspace', - delete: 'Delete', - insert: 'Insert', - return: 'Return', - enter: 'Return', - up: 'ArrowUp', - down: 'ArrowDown', - left: 'ArrowLeft', - right: 'ArrowRight', - home: 'Home', - end: 'End', - pageup: 'PageUp', - pagedown: 'PageDown', - escape: 'Escape', - esc: 'Escape', - volumeup: 'AudioVolumeUp', - volumedown: 'AudioVolumeDown', - volumemute: 'AudioVolumeMute', - medianexttrack: 'MediaTrackNext', - mediaprevioustrack: 'MediaTrackPrevious', - mediastop: 'MediaStop', - mediaplaypause: 'MediaPlayPause', - printscreen: 'PrintScreen' -}); - -// Add function keys -for (let i = 1; i <= 24; i++) { - domKeys[`f${i}`] = `F${i}`; -} - -function reduceCode({accelerator, event}, {code, key}) { - if (event.code) { - throw new Error(`Duplicated keycode \`${key}\`.`); - } - - return { - event: Object.assign({}, event, {key}, code ? {code} : null), - accelerator: accelerator.trim().slice((key && key.length) || 0) - }; -} - -/** - * This function transform an Electron Accelerator string into - * a DOM KeyboardEvent object. - * - * @param {string} accelerator an Electron Accelerator string, e.g. `Ctrl+C` or `Shift+Space`. - * @return {object} a DOM KeyboardEvent object derivate from the `accelerator` argument. - */ -function toKeyEvent(accelerator) { - let state = {accelerator, event: {}}; - while (state.accelerator !== '') { - const modifierMatch = state.accelerator.match(modifiers); - if (modifierMatch) { - const modifier = modifierMatch[0].toLowerCase(); - state = reduceModifier(state, modifier); - if (state === UNSUPPORTED) { - return {unsupportedKeyForPlatform: true}; - } - } else if (state.accelerator.trim()[0] === '+') { - state = reducePlus(state); - } else { - const codeMatch = state.accelerator.match(keyCodes); - if (codeMatch) { - const code = codeMatch[0].toLowerCase(); - if (code in domKeys) { - state = reduceCode(state, { - code: domKeys[code], - key: code - }); - } else { - state = reduceKey(state, code); - } - } else { - throw new Error(`Unvalid accelerator: "${state.accelerator}"`); - } - } - } - - return state.event; -} - -var keyboardeventFromElectronAccelerator = { - UNSUPPORTED, - reduceModifier, - reducePlus, - reduceKey, - reduceCode, - toKeyEvent -}; - -/** - * Follows the link under the cursor, temporarily moving the cursor if necessary for follow-link to - * work (i.e. if the cursor is on a starting square bracket). - */ -const followLinkUnderCursor = (vimrcPlugin) => { - const obsidianEditor = vimrcPlugin.getActiveObsidianEditor(); - const { line, ch } = obsidianEditor.getCursor(); - const firstTwoChars = obsidianEditor.getRange({ line, ch }, { line, ch: ch + 2 }); - let numCharsMoved = 0; - for (const char of firstTwoChars) { - if (char === "[") { - obsidianEditor.exec("goRight"); - numCharsMoved++; - } - } - vimrcPlugin.executeObsidianCommand("editor:follow-link"); - // Move the cursor back to where it was - for (let i = 0; i < numCharsMoved; i++) { - obsidianEditor.exec("goLeft"); - } -}; - -/** - * Moves the cursor down `repeat` lines, skipping over folded sections. - */ -const moveDownSkippingFolds = (vimrcPlugin, cm, { repeat }) => { - moveSkippingFolds(vimrcPlugin, repeat, "down"); -}; -/** - * Moves the cursor up `repeat` lines, skipping over folded sections. - */ -const moveUpSkippingFolds = (vimrcPlugin, cm, { repeat }) => { - moveSkippingFolds(vimrcPlugin, repeat, "up"); -}; -function moveSkippingFolds(vimrcPlugin, repeat, direction) { - const obsidianEditor = vimrcPlugin.getActiveObsidianEditor(); - let { line: oldLine, ch: oldCh } = obsidianEditor.getCursor(); - const commandName = direction === "up" ? "goUp" : "goDown"; - for (let i = 0; i < repeat; i++) { - obsidianEditor.exec(commandName); - const { line: newLine, ch: newCh } = obsidianEditor.getCursor(); - if (newLine === oldLine && newCh === oldCh) { - // Going in the specified direction doesn't do anything anymore, stop now - return; - } - [oldLine, oldCh] = [newLine, newCh]; - } -} - -/** - * Returns the position of the repeat-th instance of a pattern from a given cursor position, in the - * given direction; looping to the other end of the document when reaching one end. Returns the - * original cursor position if no match is found. - * - * Under the hood, to avoid repeated loops of the document: we get all matches at once, order them - * according to `direction` and `cursorPosition`, and use modulo arithmetic to return the - * appropriate match. - * - * @param cm The CodeMirror editor instance. - * @param cursorPosition The current cursor position. - * @param repeat The number of times to repeat the jump (e.g. 1 to jump to the very next match). Is - * modulo-ed for efficiency. - * @param regex The regex pattern to jump to. - * @param filterMatch Optional filter function to run on the regex matches. Return false to ignore - * a given match. - * @param direction The direction to jump in. - */ -function jumpToPattern({ cm, cursorPosition, repeat, regex, filterMatch = () => true, direction, }) { - const content = cm.getValue(); - const cursorIdx = cm.indexFromPos(cursorPosition); - const orderedMatches = getOrderedMatches({ content, regex, cursorIdx, filterMatch, direction }); - const effectiveRepeat = (repeat % orderedMatches.length) || orderedMatches.length; - const matchIdx = orderedMatches[effectiveRepeat - 1]?.index; - if (matchIdx === undefined) { - return cursorPosition; - } - const newCursorPosition = cm.posFromIndex(matchIdx); - return newCursorPosition; -} -/** - * Returns an ordered array of all matches of a regex in a string in the given direction from the - * cursor index (looping around to the other end of the document when reaching one end). - */ -function getOrderedMatches({ content, regex, cursorIdx, filterMatch, direction, }) { - const { previousMatches, currentMatches, nextMatches } = getAndGroupMatches(content, regex, cursorIdx, filterMatch); - if (direction === "next") { - return [...nextMatches, ...previousMatches, ...currentMatches]; - } - return [ - ...previousMatches.reverse(), - ...nextMatches.reverse(), - ...currentMatches.reverse(), - ]; -} -/** - * Finds all matches of a regex in a string and groups them by their positions relative to the - * cursor. - */ -function getAndGroupMatches(content, regex, cursorIdx, filterMatch) { - const globalRegex = makeGlobalRegex(regex); - const allMatches = [...content.matchAll(globalRegex)].filter(filterMatch); - const previousMatches = allMatches.filter((match) => match.index < cursorIdx && !isWithinMatch(match, cursorIdx)); - const currentMatches = allMatches.filter((match) => isWithinMatch(match, cursorIdx)); - const nextMatches = allMatches.filter((match) => match.index > cursorIdx); - return { previousMatches, currentMatches, nextMatches }; -} -function makeGlobalRegex(regex) { - const globalFlags = getGlobalFlags(regex); - return new RegExp(regex.source, globalFlags); -} -function getGlobalFlags(regex) { - const { flags } = regex; - return flags.includes("g") ? flags : `${flags}g`; -} -function isWithinMatch(match, index) { - return match.index <= index && index < match.index + match[0].length; -} - -/** Naive Regex for a Markdown heading (H1 through H6). "Naive" because it does not account for - * whether the match is within a codeblock (e.g. it could be a Python comment, not a heading). - */ -const NAIVE_HEADING_REGEX = /^#{1,6} /gm; -/** Regex for a Markdown fenced codeblock, which begins with some number >=3 of backticks at the - * start of a line. It either ends on the nearest future line that starts with at least as many - * backticks (\1 back-reference), or extends to the end of the string if no such future line exists. - */ -const FENCED_CODEBLOCK_REGEX = /(^```+)(.*?^\1|.*)/gms; -/** - * Jumps to the repeat-th next heading. - */ -const jumpToNextHeading = (cm, cursorPosition, { repeat }) => { - return jumpToHeading({ cm, cursorPosition, repeat, direction: "next" }); -}; -/** - * Jumps to the repeat-th previous heading. - */ -const jumpToPreviousHeading = (cm, cursorPosition, { repeat }) => { - return jumpToHeading({ cm, cursorPosition, repeat, direction: "previous" }); -}; -/** - * Jumps to the repeat-th heading in the given direction. - * - * Under the hood, we use the naive heading regex to find all headings, and then filter out those - * that are within codeblocks. `codeblockMatches` is passed in a closure to avoid repeated - * computation. - */ -function jumpToHeading({ cm, cursorPosition, repeat, direction, }) { - const codeblockMatches = findAllCodeblocks(cm); - const filterMatch = (match) => !isMatchWithinCodeblock(match, codeblockMatches); - return jumpToPattern({ - cm, - cursorPosition, - repeat, - regex: NAIVE_HEADING_REGEX, - filterMatch, - direction, - }); -} -function findAllCodeblocks(cm) { - const content = cm.getValue(); - return [...content.matchAll(FENCED_CODEBLOCK_REGEX)]; -} -function isMatchWithinCodeblock(match, codeblockMatches) { - return codeblockMatches.some((codeblockMatch) => isWithinMatch(codeblockMatch, match.index)); -} - -const WIKILINK_REGEX_STRING = "\\[\\[.*?\\]\\]"; -const MARKDOWN_LINK_REGEX_STRING = "\\[.*?\\]\\(.*?\\)"; -const URL_REGEX_STRING = "\\w+://\\S+"; -/** - * Regex for a link (which can be a wikilink, a markdown link, or a standalone URL). - */ -const LINK_REGEX_STRING = `${WIKILINK_REGEX_STRING}|${MARKDOWN_LINK_REGEX_STRING}|${URL_REGEX_STRING}`; -const LINK_REGEX = new RegExp(LINK_REGEX_STRING, "g"); -/** - * Jumps to the repeat-th next link. - * - * Note that `jumpToPattern` uses `String.matchAll`, which internally updates `lastIndex` after each - * match; and that `LINK_REGEX` matches wikilinks / markdown links first. So, this won't catch - * non-standalone URLs (e.g. the URL in a markdown link). This should be a good thing in most cases; - * otherwise it could be tedious (as a user) for each markdown link to contain two jumpable spots. -*/ -const jumpToNextLink = (cm, cursorPosition, { repeat }) => { - return jumpToPattern({ - cm, - cursorPosition, - repeat, - regex: LINK_REGEX, - direction: "next", - }); -}; -/** - * Jumps to the repeat-th previous link. - */ -const jumpToPreviousLink = (cm, cursorPosition, { repeat }) => { - return jumpToPattern({ - cm, - cursorPosition, - repeat, - regex: LINK_REGEX, - direction: "previous", - }); -}; - -/** - * Utility types and functions for defining Obsidian-specific Vim commands. - */ -function defineAndMapObsidianVimMotion(vimObject, motionFn, mapping) { - vimObject.defineMotion(motionFn.name, motionFn); - vimObject.mapCommand(mapping, "motion", motionFn.name, undefined, {}); -} -function defineAndMapObsidianVimAction(vimObject, vimrcPlugin, obsidianActionFn, mapping) { - vimObject.defineAction(obsidianActionFn.name, (cm, actionArgs) => { - obsidianActionFn(vimrcPlugin, cm, actionArgs); - }); - vimObject.mapCommand(mapping, "action", obsidianActionFn.name, undefined, {}); -} - -const DEFAULT_SETTINGS = { - vimrcFileName: ".obsidian.vimrc", - displayChord: false, - displayVimMode: false, - fixedNormalModeLayout: false, - capturedKeyboardMap: {}, - supportJsCommands: false, - vimStatusPromptMap: { - normal: '🟢', - insert: '🟠', - visual: '🟡', - replace: '🔴', - }, -}; -const vimStatusPromptClass = "vimrc-support-vim-mode"; -// NOTE: to future maintainers, please make sure all mapping commands are included in this array. -const mappingCommands = [ - "map", - "nmap", - "noremap", - "iunmap", - "nunmap", - "vunmap", -]; -function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} -class VimrcPlugin extends obsidian.Plugin { - constructor() { - super(...arguments); - this.codeMirrorVimObject = null; - this.initialized = false; - this.lastYankBuffer = [""]; - this.lastSystemClipboard = ""; - this.yankToSystemClipboard = false; - this.currentKeyChord = []; - this.vimChordStatusBar = null; - this.vimStatusBar = null; - this.currentVimStatus = "normal" /* vimStatus.normal */; - this.customVimKeybinds = {}; - this.currentSelection = null; - this.isInsertMode = false; - this.logVimModeChange = async (cm) => { - if (!cm) - return; - this.isInsertMode = cm.mode === 'insert'; - switch (cm.mode) { - case "insert": - this.currentVimStatus = "insert" /* vimStatus.insert */; - break; - case "normal": - this.currentVimStatus = "normal" /* vimStatus.normal */; - break; - case "visual": - this.currentVimStatus = "visual" /* vimStatus.visual */; - break; - case "replace": - this.currentVimStatus = "replace" /* vimStatus.replace */; - break; - } - if (this.settings.displayVimMode) - this.updateVimStatusBar(); - }; - this.onVimKeypress = async (vimKey) => { - if (vimKey != "") { // 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); - }; - this.onVimCommandDone = async (reason) => { - this.vimChordStatusBar?.setText(""); - this.currentKeyChord = []; - }; - this.onKeydown = (ev) => { - if (this.settings.fixedNormalModeLayout) { - const keyMap = this.settings.capturedKeyboardMap; - if (!this.isInsertMode && !ev.shiftKey && - ev.code in keyMap && ev.key != keyMap[ev.code]) { - let view = this.getActiveView(); - if (view) { - const cmEditor = this.getCodeMirror(view); - if (cmEditor) { - this.codeMirrorVimObject.handleKey(cmEditor, keyMap[ev.code], 'mapping'); - } - } - ev.preventDefault(); - return false; - } - } - }; - } - updateVimStatusBar() { - this.vimStatusBar.setText(this.settings.vimStatusPromptMap[this.currentVimStatus]); - this.vimStatusBar.dataset.vimMode = this.currentVimStatus; - } - async captureKeyboardLayout() { - // This is experimental API and it might break at some point: - // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardLayoutMap - let keyMap = {}; - let layout = await navigator.keyboard.getLayoutMap(); - let doneIterating = new Promise((resolve, reject) => { - let counted = 0; - layout.forEach((value, index) => { - keyMap[index] = value; - counted += 1; - if (counted === layout.size) - resolve(); - }); - }); - await doneIterating; - new obsidian.Notice('Keyboard layout captured'); - return keyMap; - } - async initialize() { - if (this.initialized) - return; - this.codeMirrorVimObject = window.CodeMirrorAdapter?.Vim; - this.registerYankEvents(activeWindow); - this.app.workspace.on("window-open", (workspaceWindow, w) => { - this.registerYankEvents(w); - }); - this.prepareChordDisplay(); - this.prepareVimModeDisplay(); - // Two events cos - // this don't trigger on loading/reloading obsidian with note opened - this.app.workspace.on("active-leaf-change", async () => { - this.updateSelectionEvent(); - this.updateVimEvents(); - }); - // and this don't trigger on opening same file in new pane - this.app.workspace.on("file-open", async () => { - this.updateSelectionEvent(); - this.updateVimEvents(); - }); - this.initialized = true; - } - registerYankEvents(win) { - this.registerDomEvent(win.document, 'click', () => { - this.captureYankBuffer(win); - }); - this.registerDomEvent(win.document, 'keyup', () => { - this.captureYankBuffer(win); - }); - this.registerDomEvent(win.document, 'focusin', () => { - this.captureYankBuffer(win); - }); - } - async updateSelectionEvent() { - const view = this.getActiveView(); - if (!view) - return; - let cm = this.getCodeMirror(view); - if (!cm) - return; - if (this.getCursorActivityHandlers(cm).some((e) => e.name === "updateSelection")) - return; - cm.on("cursorActivity", async (cm) => this.updateSelection(cm)); - } - async updateSelection(cm) { - this.currentSelection = cm.listSelections(); - } - getCursorActivityHandlers(cm) { - return cm._handlers.cursorActivity; - } - async updateVimEvents() { - if (!this.app.isVimEnabled()) - return; - let view = this.getActiveView(); - if (view) { - const cmEditor = this.getCodeMirror(view); - // See https://codemirror.net/doc/manual.html#vimapi_events for events. - this.isInsertMode = false; - this.currentVimStatus = "normal" /* vimStatus.normal */; - if (this.settings.displayVimMode) - this.updateVimStatusBar(); - if (!cmEditor) - return; - cmEditor.off('vim-mode-change', this.logVimModeChange); - cmEditor.on('vim-mode-change', this.logVimModeChange); - this.currentKeyChord = []; - cmEditor.off('vim-keypress', this.onVimKeypress); - cmEditor.on('vim-keypress', this.onVimKeypress); - cmEditor.off('vim-command-done', this.onVimCommandDone); - cmEditor.on('vim-command-done', this.onVimCommandDone); - CodeMirror.off(cmEditor.getInputField(), 'keydown', this.onKeydown); - CodeMirror.on(cmEditor.getInputField(), 'keydown', this.onKeydown); - } - } - async onload() { - await this.loadSettings(); - this.addSettingTab(new SettingsTab(this.app, this)); - console.log('loading Vimrc plugin'); - this.app.workspace.on('active-leaf-change', async () => { - if (!this.initialized) - await this.initialize(); - if (this.codeMirrorVimObject.loadedVimrc) - return; - let fileName = this.settings.vimrcFileName; - if (!fileName || fileName.trim().length === 0) { - fileName = DEFAULT_SETTINGS.vimrcFileName; - console.log('Configured Vimrc file name is illegal, falling-back to default'); - } - let vimrcContent = ''; - try { - vimrcContent = await this.app.vault.adapter.read(fileName); - } - catch (e) { - console.log('Error loading vimrc file', fileName, 'from the vault root', e.message); - } - this.readVimInit(vimrcContent); - }); - } - 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)'); - } - getActiveView() { - return this.app.workspace.getActiveViewOfType(obsidian.MarkdownView); - } - getActiveObsidianEditor() { - return this.getActiveView().editor; - } - getCodeMirror(view) { - return view.editMode?.editor?.cm?.cm; - } - readVimInit(vimCommands) { - let view = this.getActiveView(); - if (view) { - var cmEditor = this.getCodeMirror(view); - if (cmEditor && !this.codeMirrorVimObject.loadedVimrc) { - this.defineBasicCommands(this.codeMirrorVimObject); - this.defineAndMapObsidianVimCommands(this.codeMirrorVimObject); - this.defineSendKeys(this.codeMirrorVimObject); - this.defineObCommand(this.codeMirrorVimObject); - this.defineSurround(this.codeMirrorVimObject); - this.defineJsCommand(this.codeMirrorVimObject); - this.defineJsFile(this.codeMirrorVimObject); - this.defineSource(this.codeMirrorVimObject); - this.loadVimCommands(vimCommands); - // 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 - this.codeMirrorVimObject.loadedVimrc = true; - } - if (cmEditor) { - cmEditor.off('vim-mode-change', this.logVimModeChange); - cmEditor.on('vim-mode-change', this.logVimModeChange); - CodeMirror.off(cmEditor.getInputField(), 'keydown', this.onKeydown); - CodeMirror.on(cmEditor.getInputField(), 'keydown', this.onKeydown); - } - } - } - loadVimCommands(vimCommands) { - let view = this.getActiveView(); - if (view) { - var cmEditor = this.getCodeMirror(view); - vimCommands.split("\n").forEach(function (line, index, arr) { - 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; - } - this.codeMirrorVimObject.handleEx(cmEditor, line); - } - }.bind(this) // Faster than an arrow function. https://stackoverflow.com/questions/50375440/binding-vs-arrow-function-for-react-onclick-event - ); - } - } - defineBasicCommands(vimObject) { - vimObject.defineOption('clipboard', '', 'string', ['clip'], (value, cm) => { - 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)"); - } - } - }); - vimObject.defineOption('tabstop', 4, 'number', [], (value, cm) => { - if (value && cm) { - cm.setOption('tabSize', value); - } - }); - vimObject.defineEx('iunmap', '', (cm, params) => { - if (params.argString.trim()) { - this.codeMirrorVimObject.unmap(params.argString.trim(), 'insert'); - } - }); - vimObject.defineEx('nunmap', '', (cm, params) => { - if (params.argString.trim()) { - this.codeMirrorVimObject.unmap(params.argString.trim(), 'normal'); - } - }); - vimObject.defineEx('vunmap', '', (cm, params) => { - if (params.argString.trim()) { - this.codeMirrorVimObject.unmap(params.argString.trim(), 'visual'); - } - }); - vimObject.defineEx('noremap', '', (cm, params) => { - if (!params?.args?.length) { - throw new Error('Invalid mapping: noremap'); - } - if (params.argString.trim()) { - this.codeMirrorVimObject.noremap.apply(this.codeMirrorVimObject, params.args); - } - }); - // Allow the user to register an Ex command - vimObject.defineEx('exmap', '', (cm, params) => { - 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 - this.codeMirrorVimObject.defineEx(commandName, '', (cm, params) => { - this.codeMirrorVimObject.handleEx(cm, commandContent); - }); - }); - } - defineAndMapObsidianVimCommands(vimObject) { - defineAndMapObsidianVimMotion(vimObject, jumpToNextHeading, ']]'); - defineAndMapObsidianVimMotion(vimObject, jumpToPreviousHeading, '[['); - defineAndMapObsidianVimMotion(vimObject, jumpToNextLink, 'gl'); - defineAndMapObsidianVimMotion(vimObject, jumpToPreviousLink, 'gL'); - defineAndMapObsidianVimAction(vimObject, this, moveDownSkippingFolds, 'zj'); - defineAndMapObsidianVimAction(vimObject, this, moveUpSkippingFolds, 'zk'); - defineAndMapObsidianVimAction(vimObject, this, followLinkUnderCursor, 'gf'); - } - defineSendKeys(vimObject) { - vimObject.defineEx('sendkeys', '', async (cm, params) => { - 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 = []; - for (const key of params.args) { - if (key.startsWith('wait')) { - const delay = key.slice(4); - await sleep(delay * 1000); - } - else { - let keyEvent = null; - try { - keyEvent = new KeyboardEvent('keydown', keyboardeventFromElectronAccelerator.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); - } - } - } - }); - } - executeObsidianCommand(commandName) { - const availableCommands = this.app.commands.commands; - if (!(commandName in availableCommands)) { - throw new Error(`Command ${commandName} was not found, try 'obcommand' with no params to see in the developer console what's available`); - } - const view = this.getActiveView(); - const editor = view.editor; - const command = availableCommands[commandName]; - const { callback, checkCallback, editorCallback, editorCheckCallback } = command; - if (editorCheckCallback) - editorCheckCallback(false, editor, view); - else if (editorCallback) - editorCallback(editor, view); - else if (checkCallback) - checkCallback(false); - else if (callback) - callback(); - else - throw new Error(`Command ${commandName} doesn't have an Obsidian callback`); - } - defineObCommand(vimObject) { - vimObject.defineEx('obcommand', '', async (cm, params) => { - if (!params?.args?.length || params.args.length != 1) { - const availableCommands = this.app.commands.commands; - console.log(`Available commands: ${Object.keys(availableCommands).join('\n')}`); - throw new Error(`obcommand requires exactly 1 parameter`); - } - const commandName = params.args[0]; - this.executeObsidianCommand(commandName); - }); - } - defineSurround(vimObject) { - // Function to surround selected text or highlighted word. - var surroundFunc = (params) => { - var editor = this.getActiveView().editor; - if (!params?.length) { - throw new Error("surround requires exactly 2 parameters: prefix and postfix text."); - } - let newArgs = params.join(" ").match(/(\\.|[^\s\\\\]+)+/g); - if (newArgs.length != 2) { - throw new Error("surround requires exactly 2 parameters: prefix and postfix text."); - } - let beginning = newArgs[0].replace("\\\\", "\\").replace("\\ ", " "); // Get the beginning surround text - let ending = newArgs[1].replace("\\\\", "\\").replace("\\ ", " "); // Get the ending surround text - let currentSelections = this.currentSelection; - var chosenSelection = currentSelections?.[0] ? currentSelections[0] : { anchor: editor.getCursor(), head: editor.getCursor() }; - if (currentSelections?.length > 1) { - console.log("WARNING: Multiple selections in surround. Attempt to select matching cursor. (obsidian-vimrc-support)"); - const cursorPos = editor.getCursor(); - for (const selection of currentSelections) { - if (selection.head.line == cursorPos.line && selection.head.ch == cursorPos.ch) { - console.log("RESOLVED: Selection matching cursor found. (obsidian-vimrc-support)"); - chosenSelection = selection; - break; - } - } - } - if (editor.posToOffset(chosenSelection.anchor) === editor.posToOffset(chosenSelection.head)) { - // No range of selected text, so select word. - let wordAt = editor.wordAt(chosenSelection.head); - if (wordAt) { - chosenSelection = { anchor: wordAt.from, head: wordAt.to }; - } - } - if (editor.posToOffset(chosenSelection.anchor) > editor.posToOffset(chosenSelection.head)) { - [chosenSelection.anchor, chosenSelection.head] = [chosenSelection.head, chosenSelection.anchor]; - } - let currText = editor.getRange(chosenSelection.anchor, chosenSelection.head); - editor.replaceRange(beginning + currText + ending, chosenSelection.anchor, chosenSelection.head); - // If no selection, place cursor between beginning and ending - if (editor.posToOffset(chosenSelection.anchor) === editor.posToOffset(chosenSelection.head)) { - chosenSelection.head.ch += beginning.length; - editor.setCursor(chosenSelection.head); - } - }; - vimObject.defineEx("surround", "", (cm, params) => { surroundFunc(params.args); }); - vimObject.defineEx("pasteinto", "", (cm, params) => { - // Using the register for when this.yankToSystemClipboard == false - surroundFunc(['[', - '](' + vimObject.getRegisterController().getRegister('yank').keyBuffer + ")"]); - }); - this.getActiveView().editor; - // Handle the surround dialog input - var surroundDialogCallback = (value) => { - if ((/^\[+$/).test(value)) { // check for 1-inf [ and match them with ] - surroundFunc([value, "]".repeat(value.length)]); - } - else if ((/^\(+$/).test(value)) { // check for 1-inf ( and match them with ) - surroundFunc([value, ")".repeat(value.length)]); - } - else if ((/^\{+$/).test(value)) { // check for 1-inf { and match them with } - surroundFunc([value, "}".repeat(value.length)]); - } - else { // Else, just put it before and after. - surroundFunc([value, value]); - } - }; - vimObject.defineOperator("surroundOperator", () => { - let p = "Surround with: "; - CodeMirror.openDialog(p, surroundDialogCallback, { bottom: true, selectValueOnOpen: false }); - }); - vimObject.mapCommand("s", "operator", "surroundOperator"); - } - async captureYankBuffer(win) { - if (!this.yankToSystemClipboard) { - return; - } - const yankRegister = this.codeMirrorVimObject.getRegisterController().getRegister('yank'); - const currentYankBuffer = yankRegister.keyBuffer; - // yank -> clipboard - const buf = currentYankBuffer[0]; - if (buf !== this.lastYankBuffer[0]) { - await win.navigator.clipboard.writeText(buf); - this.lastYankBuffer = currentYankBuffer; - this.lastSystemClipboard = await win.navigator.clipboard.readText(); - return; - } - // clipboard -> yank - try { - const currentClipboardText = await win.navigator.clipboard.readText(); - if (currentClipboardText !== this.lastSystemClipboard) { - yankRegister.setText(currentClipboardText); - this.lastYankBuffer = yankRegister.keyBuffer; - this.lastSystemClipboard = currentClipboardText; - } - } - catch (e) { - // XXX: Avoid "Uncaught (in promise) DOMException: Document is not focused." - // XXX: It is not good but easy workaround - } - } - prepareChordDisplay() { - 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"; - const view = this.getActiveView(); - if (!view) - return; - let cmEditor = this.getCodeMirror(view); - // See https://codemirror.net/doc/manual.html#vimapi_events for events. - cmEditor.off('vim-keypress', this.onVimKeypress); - cmEditor.on('vim-keypress', this.onVimKeypress); - cmEditor.off('vim-command-done', this.onVimCommandDone); - cmEditor.on('vim-command-done', this.onVimCommandDone); - } - } - prepareVimModeDisplay() { - if (this.settings.displayVimMode) { - this.vimStatusBar = this.addStatusBarItem(); // Add status bar item - this.vimStatusBar.setText(this.settings.vimStatusPromptMap["normal" /* vimStatus.normal */]); // Init the vimStatusBar with normal mode - this.vimStatusBar.addClass(vimStatusPromptClass); - this.vimStatusBar.dataset.vimMode = this.currentVimStatus; - } - } - defineJsCommand(vimObject) { - vimObject.defineEx('jscommand', '', (cm, params) => { - if (!this.settings.supportJsCommands) - throw new Error("JS commands are turned off; enable them via the Vimrc plugin configuration if you're sure you know what you're doing"); - const jsCode = params.argString.trim(); - if (jsCode[0] != '{' || jsCode[jsCode.length - 1] != '}') - throw new Error("Expected an argument which is JS code surrounded by curly brackets: {...}"); - let currentSelections = this.currentSelection; - var chosenSelection = currentSelections && currentSelections.length > 0 ? currentSelections[0] : null; - const command = Function('editor', 'view', 'selection', jsCode); - const view = this.getActiveView(); - command(view.editor, view, chosenSelection); - }); - } - defineJsFile(vimObject) { - vimObject.defineEx('jsfile', '', async (cm, params) => { - if (!this.settings.supportJsCommands) - throw new Error("JS commands are turned off; enable them via the Vimrc plugin configuration if you're sure you know what you're doing"); - if (params?.args?.length < 1) - throw new Error("Expected format: fileName {extraCode}"); - let extraCode = ''; - const fileName = params.args[0]; - if (params.args.length > 1) { - params.args.shift(); - extraCode = params.args.join(' ').trim(); - if (extraCode[0] != '{' || extraCode[extraCode.length - 1] != '}') - throw new Error("Expected an extra code argument which is JS code surrounded by curly brackets: {...}"); - } - let currentSelections = this.currentSelection; - var chosenSelection = currentSelections && currentSelections.length > 0 ? currentSelections[0] : null; - let content = ''; - try { - content = await this.app.vault.adapter.read(fileName); - } - catch (e) { - throw new Error(`Cannot read file ${params.args[0]} from vault root: ${e.message}`); - } - const command = Function('editor', 'view', 'selection', content + extraCode); - const view = this.getActiveView(); - command(view.editor, view, chosenSelection); - }); - } - defineSource(vimObject) { - vimObject.defineEx('source', '', async (cm, params) => { - if (params?.args?.length > 1) - throw new Error("Expected format: source [fileName]"); - const fileName = params.argString.trim(); - try { - this.app.vault.adapter.read(fileName).then(vimrcContent => { - this.loadVimCommands(vimrcContent); - }); - } - catch (e) { - console.log('Error loading vimrc file', fileName, 'from the vault root', e.message); - } - }); - } -} -class SettingsTab extends obsidian.PluginSettingTab { - constructor(app, plugin) { - super(app, plugin); - this.plugin = plugin; - } - display() { - let { containerEl } = this; - containerEl.empty(); - containerEl.createEl('h2', { text: 'Vimrc Settings' }); - new obsidian.Setting(containerEl) - .setName('Vimrc file name') - .setDesc('Relative to vault directory (requires restart)') - .addText((text) => { - text.setPlaceholder(DEFAULT_SETTINGS.vimrcFileName); - text.setValue(this.plugin.settings.vimrcFileName || DEFAULT_SETTINGS.vimrcFileName); - text.onChange(value => { - this.plugin.settings.vimrcFileName = value; - this.plugin.saveSettings(); - }); - }); - new obsidian.Setting(containerEl) - .setName('Vim chord display') - .setDesc('Displays the current chord until completion. Ex: " f-" (requires restart)') - .addToggle((toggle) => { - toggle.setValue(this.plugin.settings.displayChord || DEFAULT_SETTINGS.displayChord); - toggle.onChange(value => { - this.plugin.settings.displayChord = value; - this.plugin.saveSettings(); - }); - }); - new obsidian.Setting(containerEl) - .setName('Vim mode display') - .setDesc('Displays the current vim mode (requires restart)') - .addToggle((toggle) => { - toggle.setValue(this.plugin.settings.displayVimMode || DEFAULT_SETTINGS.displayVimMode); - toggle.onChange(value => { - this.plugin.settings.displayVimMode = value; - this.plugin.saveSettings(); - }); - }); - new obsidian.Setting(containerEl) - .setName('Use a fixed keyboard layout for Normal mode') - .setDesc('Define a keyboard layout to always use when in Normal mode, regardless of the input language (experimental).') - .addButton(async (button) => { - button.setButtonText('Capture current layout'); - button.onClick(async () => { - this.plugin.settings.capturedKeyboardMap = await this.plugin.captureKeyboardLayout(); - this.plugin.saveSettings(); - }); - }) - .addToggle((toggle) => { - toggle.setValue(this.plugin.settings.fixedNormalModeLayout || DEFAULT_SETTINGS.fixedNormalModeLayout); - toggle.onChange(async (value) => { - this.plugin.settings.fixedNormalModeLayout = value; - if (value && Object.keys(this.plugin.settings.capturedKeyboardMap).length === 0) - this.plugin.settings.capturedKeyboardMap = await this.plugin.captureKeyboardLayout(); - this.plugin.saveSettings(); - }); - }); - new obsidian.Setting(containerEl) - .setName('Support JS commands (beware!)') - .setDesc("Support the 'jscommand' and 'jsfile' commands, which allow defining Ex commands using Javascript. WARNING! Review the README to understand why this may be dangerous before enabling.") - .addToggle(toggle => { - toggle.setValue(this.plugin.settings.supportJsCommands ?? DEFAULT_SETTINGS.supportJsCommands); - toggle.onChange(value => { - this.plugin.settings.supportJsCommands = value; - this.plugin.saveSettings(); - }); - }); - containerEl.createEl('h2', { text: 'Vim Mode Display Prompt' }); - new obsidian.Setting(containerEl) - .setName('Normal mode prompt') - .setDesc('Set the status prompt text for normal mode.') - .addText((text) => { - text.setPlaceholder('Default: 🟢'); - text.setValue(this.plugin.settings.vimStatusPromptMap.normal || - DEFAULT_SETTINGS.vimStatusPromptMap.normal); - text.onChange((value) => { - this.plugin.settings.vimStatusPromptMap.normal = value || - DEFAULT_SETTINGS.vimStatusPromptMap.normal; - this.plugin.saveSettings(); - }); - }); - new obsidian.Setting(containerEl) - .setName('Insert mode prompt') - .setDesc('Set the status prompt text for insert mode.') - .addText((text) => { - text.setPlaceholder('Default: 🟠'); - text.setValue(this.plugin.settings.vimStatusPromptMap.insert || - DEFAULT_SETTINGS.vimStatusPromptMap.insert); - text.onChange((value) => { - this.plugin.settings.vimStatusPromptMap.insert = value || - DEFAULT_SETTINGS.vimStatusPromptMap.insert; - console.log(this.plugin.settings.vimStatusPromptMap); - this.plugin.saveSettings(); - }); - }); - new obsidian.Setting(containerEl) - .setName('Visual mode prompt') - .setDesc('Set the status prompt text for visual mode.') - .addText((text) => { - text.setPlaceholder('Default: 🟡'); - text.setValue(this.plugin.settings.vimStatusPromptMap.visual || - DEFAULT_SETTINGS.vimStatusPromptMap.visual); - text.onChange((value) => { - this.plugin.settings.vimStatusPromptMap.visual = value || - DEFAULT_SETTINGS.vimStatusPromptMap.visual; - this.plugin.saveSettings(); - }); - }); - new obsidian.Setting(containerEl) - .setName('Replace mode prompt') - .setDesc('Set the status prompt text for replace mode.') - .addText((text) => { - text.setPlaceholder('Default: 🔴'); - text.setValue(this.plugin.settings.vimStatusPromptMap.replace || - DEFAULT_SETTINGS.vimStatusPromptMap.replace); - text.onChange((value) => { - this.plugin.settings.vimStatusPromptMap.replace = value || - DEFAULT_SETTINGS.vimStatusPromptMap.replace; - this.plugin.saveSettings(); - }); - }); - } -} - -module.exports = VimrcPlugin; - - -/* nosourcemap */ \ No newline at end of file diff --git a/plugins/obsidian-vimrc-support/manifest.json b/plugins/obsidian-vimrc-support/manifest.json deleted file mode 100644 index ca1e3e2..0000000 --- a/plugins/obsidian-vimrc-support/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "id": "obsidian-vimrc-support", - "name": "Vimrc Support", - "version": "0.10.2", - "description": "Auto-load a startup file with Obsidian Vim commands.", - "minAppVersion": "0.15.3", - "author": "esm", - "authorUrl": "", - "isDesktopOnly": false -}