From ab1a283aa10ab22db1c757ea2eef5a1fb33d9bb0 Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Thu, 25 Apr 2024 12:55:30 -0700 Subject: [PATCH 01/20] feat: define and expose obsidian-specific vim commands jumpToNextHeading: g] jumpToPreviousHeading: g[ --- main.ts | 20 +++++++++++++- motions/jumpToHeading.ts | 54 ++++++++++++++++++++++++++++++++++++++ motions/moveAndSkipFold.ts | 19 ++++++++++++++ 3 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 motions/jumpToHeading.ts create mode 100644 motions/moveAndSkipFold.ts diff --git a/main.ts b/main.ts index aa55406..c7426e6 100644 --- a/main.ts +++ b/main.ts @@ -1,5 +1,7 @@ import * as keyFromAccelerator from 'keyboardevent-from-electron-accelerator'; -import { EditorSelection, Notice, App, MarkdownView, Plugin, PluginSettingTab, Setting, TFile } from 'obsidian'; +import { App, EditorSelection, MarkdownView, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian'; +import { jumpToNextHeading, jumpToPreviousHeading } from './motions/jumpToHeading'; +import { moveDownSkipFold, moveUpSkipFold } from './motions/moveAndSkipFold'; declare const CodeMirror: any; @@ -257,6 +259,7 @@ export default class VimrcPlugin extends Plugin { var cmEditor = this.getCodeMirror(view); if (cmEditor && !this.codeMirrorVimObject.loadedVimrc) { this.defineBasicCommands(this.codeMirrorVimObject); + this.defineObsidianVimMotions(this.codeMirrorVimObject); this.defineSendKeys(this.codeMirrorVimObject); this.defineObCommand(this.codeMirrorVimObject); this.defineSurround(this.codeMirrorVimObject); @@ -367,6 +370,21 @@ export default class VimrcPlugin extends Plugin { }); } + + defineObsidianVimMotions(vimObject: any) { + vimObject.defineMotion('jumpToNextHeading', jumpToNextHeading); + vimObject.mapCommand('g]', 'motion', 'jumpToNextHeading'); + + vimObject.defineMotion('jumpToPreviousHeading', jumpToPreviousHeading); + vimObject.mapCommand('g[', 'motion', 'jumpToPreviousHeading'); + + vimObject.defineMotion('moveUpSkipFold', moveUpSkipFold); + vimObject.mapCommand('zk', 'motion', 'moveUpSkipFold'); + + vimObject.defineEx('moveDownSkipFold', moveDownSkipFold); + vimObject.mapCommand('zj', 'motion', 'moveDownSkipFold'); + } + defineSendKeys(vimObject: any) { vimObject.defineEx('sendkeys', '', async (cm: any, params: any) => { if (!params?.args?.length) { diff --git a/motions/jumpToHeading.ts b/motions/jumpToHeading.ts new file mode 100644 index 0000000..7a143cb --- /dev/null +++ b/motions/jumpToHeading.ts @@ -0,0 +1,54 @@ +import { Editor, EditorPosition } from "obsidian"; + +export function jumpToNextHeading( + cm: Editor, + { line, ch }: EditorPosition, + motionArgs: { repeat: number } +): EditorPosition { + const { repeat } = motionArgs; + const noteContent = cm.getValue(); + const nextContentLines = noteContent.split("\n").slice(line + 1); + const nextHeadingIdx = + getNthHeadingIndex(nextContentLines, repeat) + line + 1; + if (nextHeadingIdx === -1) { + return { line, ch }; + } + return { line: nextHeadingIdx, ch: 0 }; +} + +export function jumpToPreviousHeading( + cm: Editor, + { line, ch }: EditorPosition, + motionArgs: { repeat: number } +): EditorPosition { + const { repeat } = motionArgs; + const noteContent = cm.getValue(); + const isAlreadyOnHeading = cm.getLine(line).startsWith("#"); + const lastIdxToConsider = isAlreadyOnHeading ? line - 1 : line; + const previousContentLines = noteContent + .split("\n") + .slice(0, lastIdxToConsider + 1) + .reverse(); + const previousHeadingIdx = getNthHeadingIndex(previousContentLines, repeat); + if (previousHeadingIdx === -1) { + return { line, ch }; + } + return { line: lastIdxToConsider - previousHeadingIdx, ch: 0 }; +} + +function getNthHeadingIndex(contentLines: string[], n: number): number { + let numHeadingsFound = 0; + let currHeadingIndex = -1; + for (let i = 0; i < contentLines.length; i++) { + const headingRegex = /^#+ /; + if (!headingRegex.test(contentLines[i])) { + continue; + } + currHeadingIndex = i; + numHeadingsFound++; + if (numHeadingsFound === n) { + return currHeadingIndex; + } + } + return currHeadingIndex; +} diff --git a/motions/moveAndSkipFold.ts b/motions/moveAndSkipFold.ts new file mode 100644 index 0000000..d129b8e --- /dev/null +++ b/motions/moveAndSkipFold.ts @@ -0,0 +1,19 @@ +import { Editor, EditorPosition } from "obsidian"; + +export function moveUpSkipFold( + cm: Editor, + { line, ch }: EditorPosition, + motionArgs: { repeat: number } +): EditorPosition { + // TODO: Implement this + return { line, ch }; +} + +export function moveDownSkipFold( + cm: Editor, + { line, ch }: EditorPosition, + motionArgs: { repeat: number } +): EditorPosition { + // TODO: Implement this + return { line, ch }; +} From 9e98ddf1c3f6247235759fe3e6428cf6adca769d Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Fri, 26 Apr 2024 19:02:30 -0700 Subject: [PATCH 02/20] Implement jumpToPreviousLink motion --- main.ts | 23 +++++--- motions/jumpToHeading.ts | 7 ++- motions/jumpToLink.ts | 28 +++++++++ motions/motionUtils.ts | 113 +++++++++++++++++++++++++++++++++++++ motions/moveAndSkipFold.ts | 19 ------- package.json | 4 ++ tsconfig.json | 4 +- 7 files changed, 166 insertions(+), 32 deletions(-) create mode 100644 motions/jumpToLink.ts create mode 100644 motions/motionUtils.ts delete mode 100644 motions/moveAndSkipFold.ts diff --git a/main.ts b/main.ts index c7426e6..f119f8b 100644 --- a/main.ts +++ b/main.ts @@ -1,7 +1,8 @@ +import { Editor as CodeMirrorEditor } from 'codemirror'; import * as keyFromAccelerator from 'keyboardevent-from-electron-accelerator'; -import { App, EditorSelection, MarkdownView, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian'; +import { App, EditorPosition, EditorSelection, MarkdownView, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian'; import { jumpToNextHeading, jumpToPreviousHeading } from './motions/jumpToHeading'; -import { moveDownSkipFold, moveUpSkipFold } from './motions/moveAndSkipFold'; +import { jumpToPreviousLink } from './motions/jumpToLink'; declare const CodeMirror: any; @@ -373,16 +374,22 @@ export default class VimrcPlugin extends Plugin { defineObsidianVimMotions(vimObject: any) { vimObject.defineMotion('jumpToNextHeading', jumpToNextHeading); - vimObject.mapCommand('g]', 'motion', 'jumpToNextHeading'); + vimObject.mapCommand('gh', 'motion', 'jumpToNextHeading'); vimObject.defineMotion('jumpToPreviousHeading', jumpToPreviousHeading); - vimObject.mapCommand('g[', 'motion', 'jumpToPreviousHeading'); + vimObject.mapCommand('gH', 'motion', 'jumpToPreviousHeading'); - vimObject.defineMotion('moveUpSkipFold', moveUpSkipFold); - vimObject.mapCommand('zk', 'motion', 'moveUpSkipFold'); + // vimObject.defineMotion('jumpToNextLink', jumpToNextLink); + // vimObject.mapCommand('gl', 'motion', 'jumpToNextLink'); - vimObject.defineEx('moveDownSkipFold', moveDownSkipFold); - vimObject.mapCommand('zj', 'motion', 'moveDownSkipFold'); + vimObject.defineMotion( + "jumpToPreviousLink", + (cm: CodeMirrorEditor, pos: EditorPosition, mArgs: { repeat: number }) => { + const obsidianEditor = this.getActiveView().editor; + return jumpToPreviousLink(obsidianEditor, cm, pos, mArgs); + } + ); + vimObject.mapCommand('gL', 'motion', 'jumpToPreviousLink'); } defineSendKeys(vimObject: any) { diff --git a/motions/jumpToHeading.ts b/motions/jumpToHeading.ts index 7a143cb..6ad6c57 100644 --- a/motions/jumpToHeading.ts +++ b/motions/jumpToHeading.ts @@ -1,7 +1,8 @@ -import { Editor, EditorPosition } from "obsidian"; +import { Editor as CodeMirrorEditor } from "codemirror"; +import { EditorPosition } from "obsidian"; export function jumpToNextHeading( - cm: Editor, + cm: CodeMirrorEditor, { line, ch }: EditorPosition, motionArgs: { repeat: number } ): EditorPosition { @@ -17,7 +18,7 @@ export function jumpToNextHeading( } export function jumpToPreviousHeading( - cm: Editor, + cm: CodeMirrorEditor, { line, ch }: EditorPosition, motionArgs: { repeat: number } ): EditorPosition { diff --git a/motions/jumpToLink.ts b/motions/jumpToLink.ts new file mode 100644 index 0000000..13fa234 --- /dev/null +++ b/motions/jumpToLink.ts @@ -0,0 +1,28 @@ +import { Editor as CodeMirrorEditor } from "codemirror"; +import { EditorPosition, Editor as ObsidianEditor } from "obsidian"; +import { + getNthPreviousInstanceOfPattern +} from "./motionUtils"; + +const LINK_REGEX = /\[\[[^\]\]]+?\]\]/g; + +export function jumpToPreviousLink( + obsidianEditor: ObsidianEditor, + cm: CodeMirrorEditor, + { line, ch }: EditorPosition, + motionArgs: { repeat: number } +): EditorPosition { + const content = cm.getValue(); + const cursorOffset = obsidianEditor.posToOffset({ line, ch }); + const previousLinkIdx = getNthPreviousInstanceOfPattern({ + content, + regex: LINK_REGEX, + startingIdx: cursorOffset, + n: motionArgs.repeat, + }); + if (previousLinkIdx === undefined) { + return { line, ch }; + } + const newPosition = obsidianEditor.offsetToPos(previousLinkIdx + 2); + return newPosition; +} diff --git a/motions/motionUtils.ts b/motions/motionUtils.ts new file mode 100644 index 0000000..d648203 --- /dev/null +++ b/motions/motionUtils.ts @@ -0,0 +1,113 @@ +import { shim as matchAllShim } from "string.prototype.matchall"; +matchAllShim(); + +/** + * Returns the index of the first instance of a pattern in a string after a given starting index. + * If the pattern is not found, returns the starting index. + */ +export function getNthNextInstanceOfPattern( + content: string, + regex: RegExp, + startingIdx: number, + n: number +): number { + let numMatchesFound = 0; + let currMatchIdx = startingIdx; + const globalRegex = addGlobalFlagIfNeeded(regex); + while (currMatchIdx < content.length - 1 && numMatchesFound < n) { + const contentToSearch = content.substring(currMatchIdx + 1); + const substringMatch = globalRegex.exec(contentToSearch); + if (!substringMatch) { + return currMatchIdx; + } + currMatchIdx = currMatchIdx + substringMatch.index + 1; + numMatchesFound++; + } + return currMatchIdx; +} + +/** + * Returns the index of the last found instance of a pattern in a string before a given starting + * index. If the pattern is not found, returns undefined. + */ +export function getNthPreviousInstanceOfPattern({ + content, + regex, + startingIdx, + n, +}: { + content: string; + regex: RegExp; + startingIdx: number; + n: number; +}): number | undefined { + const globalRegex = addGlobalFlagIfNeeded(regex); + const contentToSearch = content.substring(0, startingIdx); + const previousMatches = [...contentToSearch.matchAll(globalRegex)]; + if (previousMatches.length < n) { + return previousMatches[0]?.index; + } + return previousMatches[previousMatches.length - n].index; +} + +function addGlobalFlagIfNeeded(regex: RegExp): RegExp { + return regex.global ? regex : new RegExp(regex.source, getGlobalFlags(regex)); +} + +function getGlobalFlags(regex: RegExp): string { + const { flags } = regex; + return flags.includes("g") ? flags : `${flags}g`; +} + +/** + * Returns the index of the last found instance of a pattern in a string before a given starting + * index. If the pattern is not found, returns -1. + * + * This version of the function, to find each previous match, instead of reversing the string, will + * call globalRegex.exec until it can't find any more matches. + */ +export function getNthPreviousInstanceOfPatternV2( + content: string, + regex: RegExp, + startingIdx: number, + n: number +): number { + let numMatchesFound = 0; + let currMatchIdx = startingIdx; + const globalRegex = addGlobalFlagIfNeeded(regex); + while (currMatchIdx > 0 && numMatchesFound < n) { + const previousContent = content.substring(0, currMatchIdx); + const previousMatchIdx = getIndexOfLastMatchV2( + globalRegex, + previousContent + ); + if (!previousMatchIdx) { + return currMatchIdx; + } + currMatchIdx = previousMatchIdx; + numMatchesFound++; + } + return currMatchIdx; +} + +/** + * Returns the index of the last found instance of a pattern in a string. If the pattern is not + * found, returns undefined. + * + * Under the hood, this function will call globalRegex.exec from the start of the string until it + * can't find any more matches. + */ +function getIndexOfLastMatchV2( + globalRegex: RegExp, + content: string +): number | undefined { + if (!globalRegex.global) { + throw new TypeError("Regex must be global"); + } + let currMatchIdx = undefined; + let result: RegExpExecArray | null; + while ((result = globalRegex.exec(content)) != null) { + currMatchIdx = result.index; + } + return currMatchIdx; +} diff --git a/motions/moveAndSkipFold.ts b/motions/moveAndSkipFold.ts deleted file mode 100644 index d129b8e..0000000 --- a/motions/moveAndSkipFold.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Editor, EditorPosition } from "obsidian"; - -export function moveUpSkipFold( - cm: Editor, - { line, ch }: EditorPosition, - motionArgs: { repeat: number } -): EditorPosition { - // TODO: Implement this - return { line, ch }; -} - -export function moveDownSkipFold( - cm: Editor, - { line, ch }: EditorPosition, - motionArgs: { repeat: number } -): EditorPosition { - // TODO: Implement this - return { line, ch }; -} diff --git a/package.json b/package.json index c9b1394..008e523 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,15 @@ "@rollup/plugin-node-resolve": "^9.0.0", "@rollup/plugin-typescript": "^11.0.0", "@types/node": "^14.14.6", + "@types/string.prototype.matchall": "^4.0.4", "codemirror": "^5.62.2", "keyboardevent-from-electron-accelerator": "*", "obsidian": "^1.1.1", "rollup": "^2.33.0", "tslib": "^2.0.3", "typescript": "^4.9.4" + }, + "dependencies": { + "string.prototype.matchall": "^4.0.11" } } diff --git a/tsconfig.json b/tsconfig.json index d148fe0..951df80 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,12 +8,12 @@ "allowJs": true, "noImplicitAny": true, "moduleResolution": "node", + "downlevelIteration": true, "importHelpers": true, "lib": [ "dom", - "es5", "scripthost", - "es2015" + "ES2020" ] }, "include": [ From 0053c8f1b2365b0dd8eff1000a23a65c1b373cb0 Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Fri, 26 Apr 2024 23:57:11 -0700 Subject: [PATCH 03/20] Refactoring and implementing jumpToNextLink --- main.ts | 27 ++---- motions/jumpToHeading.ts | 29 +++--- motions/jumpToLink.ts | 41 +++++--- motions/motionUtils.ts | 113 ----------------------- motions/utils/defineObsidianVimMotion.ts | 29 ++++++ motions/utils/getNthInstanceOfPattern.ts | 65 +++++++++++++ 6 files changed, 140 insertions(+), 164 deletions(-) delete mode 100644 motions/motionUtils.ts create mode 100644 motions/utils/defineObsidianVimMotion.ts create mode 100644 motions/utils/getNthInstanceOfPattern.ts diff --git a/main.ts b/main.ts index f119f8b..e86f5d7 100644 --- a/main.ts +++ b/main.ts @@ -1,8 +1,8 @@ -import { Editor as CodeMirrorEditor } from 'codemirror'; import * as keyFromAccelerator from 'keyboardevent-from-electron-accelerator'; -import { App, EditorPosition, EditorSelection, MarkdownView, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian'; +import { App, EditorSelection, MarkdownView, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian'; import { jumpToNextHeading, jumpToPreviousHeading } from './motions/jumpToHeading'; -import { jumpToPreviousLink } from './motions/jumpToLink'; +import { jumpToNextLink, jumpToPreviousLink } from './motions/jumpToLink'; +import { defineObsidianVimMotion } from './motions/utils/defineObsidianVimMotion'; declare const CodeMirror: any; @@ -373,23 +373,10 @@ export default class VimrcPlugin extends Plugin { defineObsidianVimMotions(vimObject: any) { - vimObject.defineMotion('jumpToNextHeading', jumpToNextHeading); - vimObject.mapCommand('gh', 'motion', 'jumpToNextHeading'); - - vimObject.defineMotion('jumpToPreviousHeading', jumpToPreviousHeading); - vimObject.mapCommand('gH', 'motion', 'jumpToPreviousHeading'); - - // vimObject.defineMotion('jumpToNextLink', jumpToNextLink); - // vimObject.mapCommand('gl', 'motion', 'jumpToNextLink'); - - vimObject.defineMotion( - "jumpToPreviousLink", - (cm: CodeMirrorEditor, pos: EditorPosition, mArgs: { repeat: number }) => { - const obsidianEditor = this.getActiveView().editor; - return jumpToPreviousLink(obsidianEditor, cm, pos, mArgs); - } - ); - vimObject.mapCommand('gL', 'motion', 'jumpToPreviousLink'); + defineObsidianVimMotion(vimObject, jumpToNextHeading, 'gh') + defineObsidianVimMotion(vimObject, jumpToPreviousHeading, 'gH') + defineObsidianVimMotion(vimObject, jumpToNextLink, 'gl') + defineObsidianVimMotion(vimObject, jumpToPreviousLink, 'gL') } defineSendKeys(vimObject: any) { diff --git a/motions/jumpToHeading.ts b/motions/jumpToHeading.ts index 6ad6c57..7c04d81 100644 --- a/motions/jumpToHeading.ts +++ b/motions/jumpToHeading.ts @@ -1,12 +1,9 @@ -import { Editor as CodeMirrorEditor } from "codemirror"; -import { EditorPosition } from "obsidian"; +import { MotionFn } from "./utils/defineObsidianVimMotion"; -export function jumpToNextHeading( - cm: CodeMirrorEditor, - { line, ch }: EditorPosition, - motionArgs: { repeat: number } -): EditorPosition { - const { repeat } = motionArgs; +const HEADING_REGEX = /^#+ /; + +export const jumpToNextHeading: MotionFn = (cm, oldPosition, { repeat }) => { + const { line, ch } = oldPosition; const noteContent = cm.getValue(); const nextContentLines = noteContent.split("\n").slice(line + 1); const nextHeadingIdx = @@ -15,14 +12,14 @@ export function jumpToNextHeading( return { line, ch }; } return { line: nextHeadingIdx, ch: 0 }; -} +}; -export function jumpToPreviousHeading( - cm: CodeMirrorEditor, - { line, ch }: EditorPosition, - motionArgs: { repeat: number } -): EditorPosition { - const { repeat } = motionArgs; +export const jumpToPreviousHeading: MotionFn = ( + cm, + oldPosition, + { repeat } +) => { + const { line, ch } = oldPosition; const noteContent = cm.getValue(); const isAlreadyOnHeading = cm.getLine(line).startsWith("#"); const lastIdxToConsider = isAlreadyOnHeading ? line - 1 : line; @@ -35,7 +32,7 @@ export function jumpToPreviousHeading( return { line, ch }; } return { line: lastIdxToConsider - previousHeadingIdx, ch: 0 }; -} +}; function getNthHeadingIndex(contentLines: string[], n: number): number { let numHeadingsFound = 0; diff --git a/motions/jumpToLink.ts b/motions/jumpToLink.ts index 13fa234..62a6b71 100644 --- a/motions/jumpToLink.ts +++ b/motions/jumpToLink.ts @@ -1,28 +1,39 @@ -import { Editor as CodeMirrorEditor } from "codemirror"; -import { EditorPosition, Editor as ObsidianEditor } from "obsidian"; +import { MotionFn } from "./utils/defineObsidianVimMotion"; import { - getNthPreviousInstanceOfPattern -} from "./motionUtils"; + getNthNextInstanceOfPattern, + getNthPreviousInstanceOfPattern, +} from "./utils/getNthInstanceOfPattern"; const LINK_REGEX = /\[\[[^\]\]]+?\]\]/g; -export function jumpToPreviousLink( - obsidianEditor: ObsidianEditor, - cm: CodeMirrorEditor, - { line, ch }: EditorPosition, - motionArgs: { repeat: number } -): EditorPosition { +export const jumpToNextLink: MotionFn = (cm, oldPosition, { repeat }) => { const content = cm.getValue(); - const cursorOffset = obsidianEditor.posToOffset({ line, ch }); + const cursorOffset = cm.indexFromPos(oldPosition); + const nextLinkIdx = getNthNextInstanceOfPattern({ + content, + regex: LINK_REGEX, + startingIdx: cursorOffset, + n: repeat, + }); + if (nextLinkIdx === undefined) { + return oldPosition; + } + const newPosition = cm.posFromIndex(nextLinkIdx + 2); + return newPosition; +}; + +export const jumpToPreviousLink: MotionFn = (cm, oldPosition, { repeat }) => { + const content = cm.getValue(); + const cursorOffset = cm.indexFromPos(oldPosition); const previousLinkIdx = getNthPreviousInstanceOfPattern({ content, regex: LINK_REGEX, startingIdx: cursorOffset, - n: motionArgs.repeat, + n: repeat, }); if (previousLinkIdx === undefined) { - return { line, ch }; + return oldPosition; } - const newPosition = obsidianEditor.offsetToPos(previousLinkIdx + 2); + const newPosition = cm.posFromIndex(previousLinkIdx + 2); return newPosition; -} +}; diff --git a/motions/motionUtils.ts b/motions/motionUtils.ts deleted file mode 100644 index d648203..0000000 --- a/motions/motionUtils.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { shim as matchAllShim } from "string.prototype.matchall"; -matchAllShim(); - -/** - * Returns the index of the first instance of a pattern in a string after a given starting index. - * If the pattern is not found, returns the starting index. - */ -export function getNthNextInstanceOfPattern( - content: string, - regex: RegExp, - startingIdx: number, - n: number -): number { - let numMatchesFound = 0; - let currMatchIdx = startingIdx; - const globalRegex = addGlobalFlagIfNeeded(regex); - while (currMatchIdx < content.length - 1 && numMatchesFound < n) { - const contentToSearch = content.substring(currMatchIdx + 1); - const substringMatch = globalRegex.exec(contentToSearch); - if (!substringMatch) { - return currMatchIdx; - } - currMatchIdx = currMatchIdx + substringMatch.index + 1; - numMatchesFound++; - } - return currMatchIdx; -} - -/** - * Returns the index of the last found instance of a pattern in a string before a given starting - * index. If the pattern is not found, returns undefined. - */ -export function getNthPreviousInstanceOfPattern({ - content, - regex, - startingIdx, - n, -}: { - content: string; - regex: RegExp; - startingIdx: number; - n: number; -}): number | undefined { - const globalRegex = addGlobalFlagIfNeeded(regex); - const contentToSearch = content.substring(0, startingIdx); - const previousMatches = [...contentToSearch.matchAll(globalRegex)]; - if (previousMatches.length < n) { - return previousMatches[0]?.index; - } - return previousMatches[previousMatches.length - n].index; -} - -function addGlobalFlagIfNeeded(regex: RegExp): RegExp { - return regex.global ? regex : new RegExp(regex.source, getGlobalFlags(regex)); -} - -function getGlobalFlags(regex: RegExp): string { - const { flags } = regex; - return flags.includes("g") ? flags : `${flags}g`; -} - -/** - * Returns the index of the last found instance of a pattern in a string before a given starting - * index. If the pattern is not found, returns -1. - * - * This version of the function, to find each previous match, instead of reversing the string, will - * call globalRegex.exec until it can't find any more matches. - */ -export function getNthPreviousInstanceOfPatternV2( - content: string, - regex: RegExp, - startingIdx: number, - n: number -): number { - let numMatchesFound = 0; - let currMatchIdx = startingIdx; - const globalRegex = addGlobalFlagIfNeeded(regex); - while (currMatchIdx > 0 && numMatchesFound < n) { - const previousContent = content.substring(0, currMatchIdx); - const previousMatchIdx = getIndexOfLastMatchV2( - globalRegex, - previousContent - ); - if (!previousMatchIdx) { - return currMatchIdx; - } - currMatchIdx = previousMatchIdx; - numMatchesFound++; - } - return currMatchIdx; -} - -/** - * Returns the index of the last found instance of a pattern in a string. If the pattern is not - * found, returns undefined. - * - * Under the hood, this function will call globalRegex.exec from the start of the string until it - * can't find any more matches. - */ -function getIndexOfLastMatchV2( - globalRegex: RegExp, - content: string -): number | undefined { - if (!globalRegex.global) { - throw new TypeError("Regex must be global"); - } - let currMatchIdx = undefined; - let result: RegExpExecArray | null; - while ((result = globalRegex.exec(content)) != null) { - currMatchIdx = result.index; - } - return currMatchIdx; -} diff --git a/motions/utils/defineObsidianVimMotion.ts b/motions/utils/defineObsidianVimMotion.ts new file mode 100644 index 0000000..405edc0 --- /dev/null +++ b/motions/utils/defineObsidianVimMotion.ts @@ -0,0 +1,29 @@ +import { Editor as CodeMirrorEditor } from "codemirror"; +import { EditorPosition } from "obsidian"; + +export type MotionFn = ( + cm: CodeMirrorEditor, + oldPosition: EditorPosition, + motionArgs: { repeat: number } +) => EditorPosition; + +// Reference: @replit/codemirror-vim/src/vim.js +type VimApi = { + defineMotion: (name: string, fn: MotionFn) => void; + mapCommand: ( + keys: string, + type: string, + name: string, + args: any, + extra: { [x: string]: any } + ) => void; +}; + +export function defineObsidianVimMotion( + vimObject: VimApi, + motionFn: MotionFn, + mapping: string +) { + vimObject.defineMotion(motionFn.name, motionFn); + vimObject.mapCommand(mapping, "motion", motionFn.name, undefined, {}); +} diff --git a/motions/utils/getNthInstanceOfPattern.ts b/motions/utils/getNthInstanceOfPattern.ts new file mode 100644 index 0000000..c06f0e4 --- /dev/null +++ b/motions/utils/getNthInstanceOfPattern.ts @@ -0,0 +1,65 @@ +import { shim as matchAllShim } from "string.prototype.matchall"; +matchAllShim(); + +/** + * Returns the index of the first instance of a pattern in a string after a given starting index. + * If the pattern is not found, returns the starting index. + */ +export function getNthNextInstanceOfPattern({ + content, + regex, + startingIdx, + n, +}: { + content: string; + regex: RegExp; + startingIdx: number; + n: number; +}): number { + let numMatchesFound = 0; + let currMatchIdx = startingIdx; + const globalRegex = addGlobalFlagIfNeeded(regex); + while (currMatchIdx < content.length - 1 && numMatchesFound < n) { + const contentToSearch = content.substring(currMatchIdx + 1); + const substringMatchIdx = contentToSearch.search(globalRegex); + if (substringMatchIdx === -1) { + return currMatchIdx; + } + currMatchIdx = currMatchIdx + substringMatchIdx + 1; + numMatchesFound++; + } + return currMatchIdx; +} + +/** + * Returns the index of the last found instance of a pattern in a string before a given starting + * index. If the pattern is not found, returns undefined. + */ +export function getNthPreviousInstanceOfPattern({ + content, + regex, + startingIdx, + n, +}: { + content: string; + regex: RegExp; + startingIdx: number; + n: number; +}): number | undefined { + const globalRegex = addGlobalFlagIfNeeded(regex); + const contentToSearch = content.substring(0, startingIdx); + const previousMatches = [...contentToSearch.matchAll(globalRegex)]; + if (previousMatches.length < n) { + return previousMatches[0]?.index; + } + return previousMatches[previousMatches.length - n].index; +} + +function addGlobalFlagIfNeeded(regex: RegExp): RegExp { + return regex.global ? regex : new RegExp(regex.source, getGlobalFlags(regex)); +} + +function getGlobalFlags(regex: RegExp): string { + const { flags } = regex; + return flags.includes("g") ? flags : `${flags}g`; +} From 128f1834e9c629f2cfa59f2704df0cd24eaa7bc3 Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Sat, 27 Apr 2024 10:28:04 -0700 Subject: [PATCH 04/20] refactor: new jumpToPattern function that can be used for motions --- motions/jumpToHeading.ts | 56 +++++++--------------- motions/jumpToLink.ts | 37 +++++---------- motions/utils/defineObsidianVimMotion.ts | 8 +++- motions/utils/getNthInstanceOfPattern.ts | 59 +++++++++++++++++------- 4 files changed, 75 insertions(+), 85 deletions(-) diff --git a/motions/jumpToHeading.ts b/motions/jumpToHeading.ts index 7c04d81..c29d6f2 100644 --- a/motions/jumpToHeading.ts +++ b/motions/jumpToHeading.ts @@ -1,17 +1,16 @@ import { MotionFn } from "./utils/defineObsidianVimMotion"; +import { jumpToPattern } from "./utils/getNthInstanceOfPattern"; -const HEADING_REGEX = /^#+ /; +const HEADING_REGEX = /^#+ /gm; export const jumpToNextHeading: MotionFn = (cm, oldPosition, { repeat }) => { - const { line, ch } = oldPosition; - const noteContent = cm.getValue(); - const nextContentLines = noteContent.split("\n").slice(line + 1); - const nextHeadingIdx = - getNthHeadingIndex(nextContentLines, repeat) + line + 1; - if (nextHeadingIdx === -1) { - return { line, ch }; - } - return { line: nextHeadingIdx, ch: 0 }; + return jumpToPattern({ + cm, + oldPosition, + repeat, + regex: HEADING_REGEX, + direction: "next", + }); }; export const jumpToPreviousHeading: MotionFn = ( @@ -19,34 +18,11 @@ export const jumpToPreviousHeading: MotionFn = ( oldPosition, { repeat } ) => { - const { line, ch } = oldPosition; - const noteContent = cm.getValue(); - const isAlreadyOnHeading = cm.getLine(line).startsWith("#"); - const lastIdxToConsider = isAlreadyOnHeading ? line - 1 : line; - const previousContentLines = noteContent - .split("\n") - .slice(0, lastIdxToConsider + 1) - .reverse(); - const previousHeadingIdx = getNthHeadingIndex(previousContentLines, repeat); - if (previousHeadingIdx === -1) { - return { line, ch }; - } - return { line: lastIdxToConsider - previousHeadingIdx, ch: 0 }; + return jumpToPattern({ + cm, + oldPosition, + repeat, + regex: HEADING_REGEX, + direction: "previous", + }); }; - -function getNthHeadingIndex(contentLines: string[], n: number): number { - let numHeadingsFound = 0; - let currHeadingIndex = -1; - for (let i = 0; i < contentLines.length; i++) { - const headingRegex = /^#+ /; - if (!headingRegex.test(contentLines[i])) { - continue; - } - currHeadingIndex = i; - numHeadingsFound++; - if (numHeadingsFound === n) { - return currHeadingIndex; - } - } - return currHeadingIndex; -} diff --git a/motions/jumpToLink.ts b/motions/jumpToLink.ts index 62a6b71..712ede4 100644 --- a/motions/jumpToLink.ts +++ b/motions/jumpToLink.ts @@ -1,39 +1,24 @@ import { MotionFn } from "./utils/defineObsidianVimMotion"; -import { - getNthNextInstanceOfPattern, - getNthPreviousInstanceOfPattern, -} from "./utils/getNthInstanceOfPattern"; +import { jumpToPattern } from "./utils/getNthInstanceOfPattern"; const LINK_REGEX = /\[\[[^\]\]]+?\]\]/g; export const jumpToNextLink: MotionFn = (cm, oldPosition, { repeat }) => { - const content = cm.getValue(); - const cursorOffset = cm.indexFromPos(oldPosition); - const nextLinkIdx = getNthNextInstanceOfPattern({ - content, + return jumpToPattern({ + cm, + oldPosition, + repeat, regex: LINK_REGEX, - startingIdx: cursorOffset, - n: repeat, + direction: "next", }); - if (nextLinkIdx === undefined) { - return oldPosition; - } - const newPosition = cm.posFromIndex(nextLinkIdx + 2); - return newPosition; }; export const jumpToPreviousLink: MotionFn = (cm, oldPosition, { repeat }) => { - const content = cm.getValue(); - const cursorOffset = cm.indexFromPos(oldPosition); - const previousLinkIdx = getNthPreviousInstanceOfPattern({ - content, + return jumpToPattern({ + cm, + oldPosition, + repeat, regex: LINK_REGEX, - startingIdx: cursorOffset, - n: repeat, + direction: "previous", }); - if (previousLinkIdx === undefined) { - return oldPosition; - } - const newPosition = cm.posFromIndex(previousLinkIdx + 2); - return newPosition; }; diff --git a/motions/utils/defineObsidianVimMotion.ts b/motions/utils/defineObsidianVimMotion.ts index 405edc0..abcf238 100644 --- a/motions/utils/defineObsidianVimMotion.ts +++ b/motions/utils/defineObsidianVimMotion.ts @@ -7,8 +7,12 @@ export type MotionFn = ( motionArgs: { repeat: number } ) => EditorPosition; -// Reference: @replit/codemirror-vim/src/vim.js -type VimApi = { +/** + * Partial representation of the CodeMirror Vim API that we use to define motions, commands, etc. + * + * Reference: https://github.com/replit/codemirror-vim/blob/master/src/vim.js + */ +export type VimApi = { defineMotion: (name: string, fn: MotionFn) => void; mapCommand: ( keys: string, diff --git a/motions/utils/getNthInstanceOfPattern.ts b/motions/utils/getNthInstanceOfPattern.ts index c06f0e4..7488f03 100644 --- a/motions/utils/getNthInstanceOfPattern.ts +++ b/motions/utils/getNthInstanceOfPattern.ts @@ -1,9 +1,38 @@ +import { Editor as CodeMirrorEditor } from "codemirror"; +import { EditorPosition } from "obsidian"; import { shim as matchAllShim } from "string.prototype.matchall"; matchAllShim(); +export function jumpToPattern({ + cm, + oldPosition, + repeat, + regex, + direction, +}: { + cm: CodeMirrorEditor; + oldPosition: EditorPosition; + repeat: number; + regex: RegExp; + direction: "next" | "previous"; +}): EditorPosition { + const content = cm.getValue(); + const startingIdx = cm.indexFromPos(oldPosition); + const jumpFn = + direction === "next" + ? getNthNextInstanceOfPattern + : getNthPreviousInstanceOfPattern; + const matchIdx = jumpFn({ content, regex, startingIdx, n: repeat }); + if (matchIdx === undefined) { + return oldPosition; + } + const newPosition = cm.posFromIndex(matchIdx); + return newPosition; +} + /** - * Returns the index of the first instance of a pattern in a string after a given starting index. - * If the pattern is not found, returns the starting index. + * Returns the index of (up to) the n-th instance of a pattern in a string after a given starting + * index. If the pattern is not found at all, returns undefined. */ export function getNthNextInstanceOfPattern({ content, @@ -16,24 +45,19 @@ export function getNthNextInstanceOfPattern({ startingIdx: number; n: number; }): number { + const globalRegex = makeGlobalRegex(regex); + globalRegex.lastIndex = startingIdx + 1; + let currMatch; let numMatchesFound = 0; - let currMatchIdx = startingIdx; - const globalRegex = addGlobalFlagIfNeeded(regex); - while (currMatchIdx < content.length - 1 && numMatchesFound < n) { - const contentToSearch = content.substring(currMatchIdx + 1); - const substringMatchIdx = contentToSearch.search(globalRegex); - if (substringMatchIdx === -1) { - return currMatchIdx; - } - currMatchIdx = currMatchIdx + substringMatchIdx + 1; + while (numMatchesFound < n && (currMatch = globalRegex.exec(content)) != null) { numMatchesFound++; } - return currMatchIdx; + return currMatch?.index; } /** - * Returns the index of the last found instance of a pattern in a string before a given starting - * index. If the pattern is not found, returns undefined. + * Returns the index of (up to) the nth-last instance of a pattern in a string before a given + * starting index. If the pattern is not found at all, returns undefined. */ export function getNthPreviousInstanceOfPattern({ content, @@ -46,7 +70,7 @@ export function getNthPreviousInstanceOfPattern({ startingIdx: number; n: number; }): number | undefined { - const globalRegex = addGlobalFlagIfNeeded(regex); + const globalRegex = makeGlobalRegex(regex); const contentToSearch = content.substring(0, startingIdx); const previousMatches = [...contentToSearch.matchAll(globalRegex)]; if (previousMatches.length < n) { @@ -55,8 +79,9 @@ export function getNthPreviousInstanceOfPattern({ return previousMatches[previousMatches.length - n].index; } -function addGlobalFlagIfNeeded(regex: RegExp): RegExp { - return regex.global ? regex : new RegExp(regex.source, getGlobalFlags(regex)); +function makeGlobalRegex(regex: RegExp): RegExp { + const globalFlags = getGlobalFlags(regex); + return new RegExp(regex.source, globalFlags); } function getGlobalFlags(regex: RegExp): string { From 7ca0e78ebc3b2a67212baa8e44c309363a50458b Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Sat, 27 Apr 2024 10:39:16 -0700 Subject: [PATCH 05/20] refactor: renamed file and removed unneeded exports --- motions/jumpToHeading.ts | 2 +- motions/jumpToLink.ts | 2 +- .../utils/{getNthInstanceOfPattern.ts => jumpToPattern.ts} | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename motions/utils/{getNthInstanceOfPattern.ts => jumpToPattern.ts} (96%) diff --git a/motions/jumpToHeading.ts b/motions/jumpToHeading.ts index c29d6f2..020c967 100644 --- a/motions/jumpToHeading.ts +++ b/motions/jumpToHeading.ts @@ -1,5 +1,5 @@ import { MotionFn } from "./utils/defineObsidianVimMotion"; -import { jumpToPattern } from "./utils/getNthInstanceOfPattern"; +import { jumpToPattern } from "./utils/jumpToPattern"; const HEADING_REGEX = /^#+ /gm; diff --git a/motions/jumpToLink.ts b/motions/jumpToLink.ts index 712ede4..7745abe 100644 --- a/motions/jumpToLink.ts +++ b/motions/jumpToLink.ts @@ -1,5 +1,5 @@ import { MotionFn } from "./utils/defineObsidianVimMotion"; -import { jumpToPattern } from "./utils/getNthInstanceOfPattern"; +import { jumpToPattern } from "./utils/jumpToPattern"; const LINK_REGEX = /\[\[[^\]\]]+?\]\]/g; diff --git a/motions/utils/getNthInstanceOfPattern.ts b/motions/utils/jumpToPattern.ts similarity index 96% rename from motions/utils/getNthInstanceOfPattern.ts rename to motions/utils/jumpToPattern.ts index 7488f03..ebaff9f 100644 --- a/motions/utils/getNthInstanceOfPattern.ts +++ b/motions/utils/jumpToPattern.ts @@ -34,7 +34,7 @@ export function jumpToPattern({ * Returns the index of (up to) the n-th instance of a pattern in a string after a given starting * index. If the pattern is not found at all, returns undefined. */ -export function getNthNextInstanceOfPattern({ +function getNthNextInstanceOfPattern({ content, regex, startingIdx, @@ -59,7 +59,7 @@ export function getNthNextInstanceOfPattern({ * Returns the index of (up to) the nth-last instance of a pattern in a string before a given * starting index. If the pattern is not found at all, returns undefined. */ -export function getNthPreviousInstanceOfPattern({ +function getNthPreviousInstanceOfPattern({ content, regex, startingIdx, From 52159150146555a6e2fdc3a1a427404fbd25bd35 Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Sat, 27 Apr 2024 10:59:24 -0700 Subject: [PATCH 06/20] fix: return last found index even if fewer than n instances found, instead of undefined --- motions/utils/jumpToPattern.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/motions/utils/jumpToPattern.ts b/motions/utils/jumpToPattern.ts index ebaff9f..cff6a9d 100644 --- a/motions/utils/jumpToPattern.ts +++ b/motions/utils/jumpToPattern.ts @@ -47,12 +47,13 @@ function getNthNextInstanceOfPattern({ }): number { const globalRegex = makeGlobalRegex(regex); globalRegex.lastIndex = startingIdx + 1; - let currMatch; + let currMatch, lastMatch; let numMatchesFound = 0; while (numMatchesFound < n && (currMatch = globalRegex.exec(content)) != null) { + lastMatch = currMatch; numMatchesFound++; } - return currMatch?.index; + return lastMatch?.index; } /** From 6779419ffec0faf8f6e3e95692e33591131ce9e9 Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Sat, 27 Apr 2024 12:08:18 -0700 Subject: [PATCH 07/20] feat: implement moveUpSkipFold and moveDownSkipFold --- main.ts | 47 ++++++++++++++++--- motions/jumpToHeading.ts | 4 +- motions/jumpToLink.ts | 4 +- .../defineObsidianVimMotion.ts | 7 +++ {motions/utils => utils}/jumpToPattern.ts | 0 5 files changed, 51 insertions(+), 11 deletions(-) rename {motions/utils => utils}/defineObsidianVimMotion.ts (83%) rename {motions/utils => utils}/jumpToPattern.ts (100%) diff --git a/main.ts b/main.ts index e86f5d7..d0d2ab8 100644 --- a/main.ts +++ b/main.ts @@ -1,8 +1,8 @@ import * as keyFromAccelerator from 'keyboardevent-from-electron-accelerator'; -import { App, EditorSelection, MarkdownView, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian'; +import { App, EditorSelection, MarkdownView, Notice, Editor as ObsidianEditor, Plugin, PluginSettingTab, Setting } from 'obsidian'; import { jumpToNextHeading, jumpToPreviousHeading } from './motions/jumpToHeading'; import { jumpToNextLink, jumpToPreviousLink } from './motions/jumpToLink'; -import { defineObsidianVimMotion } from './motions/utils/defineObsidianVimMotion'; +import { VimApi, defineObsidianVimMotion } from './utils/defineObsidianVimMotion'; declare const CodeMirror: any; @@ -250,6 +250,10 @@ export default class VimrcPlugin extends Plugin { return this.app.workspace.getActiveViewOfType(MarkdownView); } + private getActiveObsidianEditor(): ObsidianEditor { + return this.getActiveView().editor; + } + private getCodeMirror(view: MarkdownView): CodeMirror.Editor { return (view as any).editMode?.editor?.cm?.cm; } @@ -372,11 +376,40 @@ export default class VimrcPlugin extends Plugin { } - defineObsidianVimMotions(vimObject: any) { - defineObsidianVimMotion(vimObject, jumpToNextHeading, 'gh') - defineObsidianVimMotion(vimObject, jumpToPreviousHeading, 'gH') - defineObsidianVimMotion(vimObject, jumpToNextLink, 'gl') - defineObsidianVimMotion(vimObject, jumpToPreviousLink, 'gL') + defineObsidianVimMotions(vimObject: VimApi) { + defineObsidianVimMotion(vimObject, jumpToNextHeading, 'gh'); + defineObsidianVimMotion(vimObject, jumpToPreviousHeading, 'gH'); + defineObsidianVimMotion(vimObject, jumpToNextLink, 'gl'); + defineObsidianVimMotion(vimObject, jumpToPreviousLink, 'gL'); + + vimObject.defineAction('moveDownSkipFold', (cm, {repeat}) => { + const obsidianEditor = this.getActiveObsidianEditor(); + let {line: oldLine, ch: oldCh} = obsidianEditor.getCursor(); + for (let i = 0; i < repeat; i++) { + obsidianEditor.exec("goDown"); + const {line: newLine, ch: newCh} = obsidianEditor.getCursor(); + if (newLine === oldLine && newCh === oldCh) { + // Going down doesn't do anything anymore, stop now + return; + } + [oldLine, oldCh] = [newLine, newCh]; + } + }); + vimObject.mapCommand('zj', 'action', 'moveDownSkipFold', undefined, {}); + vimObject.defineAction('moveUpSkipFold', (cm, {repeat}) => { + const obsidianEditor = this.getActiveObsidianEditor(); + let {line: oldLine, ch: oldCh} = obsidianEditor.getCursor(); + for (let i = 0; i < repeat; i++) { + obsidianEditor.exec("goUp"); + const {line: newLine, ch: newCh} = obsidianEditor.getCursor(); + if (newLine === oldLine && newCh === oldCh) { + // Going up doesn't do anything anymore, stop now + return; + } + [oldLine, oldCh] = [newLine, newCh]; + } + }); + vimObject.mapCommand('zk', 'action', 'moveUpSkipFold', undefined, {}); } defineSendKeys(vimObject: any) { diff --git a/motions/jumpToHeading.ts b/motions/jumpToHeading.ts index 020c967..b4f2180 100644 --- a/motions/jumpToHeading.ts +++ b/motions/jumpToHeading.ts @@ -1,5 +1,5 @@ -import { MotionFn } from "./utils/defineObsidianVimMotion"; -import { jumpToPattern } from "./utils/jumpToPattern"; +import { MotionFn } from "../utils/defineObsidianVimMotion"; +import { jumpToPattern } from "../utils/jumpToPattern"; const HEADING_REGEX = /^#+ /gm; diff --git a/motions/jumpToLink.ts b/motions/jumpToLink.ts index 7745abe..b61ce0e 100644 --- a/motions/jumpToLink.ts +++ b/motions/jumpToLink.ts @@ -1,5 +1,5 @@ -import { MotionFn } from "./utils/defineObsidianVimMotion"; -import { jumpToPattern } from "./utils/jumpToPattern"; +import { MotionFn } from "../utils/defineObsidianVimMotion"; +import { jumpToPattern } from "../utils/jumpToPattern"; const LINK_REGEX = /\[\[[^\]\]]+?\]\]/g; diff --git a/motions/utils/defineObsidianVimMotion.ts b/utils/defineObsidianVimMotion.ts similarity index 83% rename from motions/utils/defineObsidianVimMotion.ts rename to utils/defineObsidianVimMotion.ts index abcf238..b520cdf 100644 --- a/motions/utils/defineObsidianVimMotion.ts +++ b/utils/defineObsidianVimMotion.ts @@ -7,6 +7,12 @@ export type MotionFn = ( motionArgs: { repeat: number } ) => EditorPosition; +export type ActionFn = ( + cm: CodeMirrorEditor, + actionArgs: { repeat: number }, + vimState: any +) => void; + /** * Partial representation of the CodeMirror Vim API that we use to define motions, commands, etc. * @@ -14,6 +20,7 @@ export type MotionFn = ( */ export type VimApi = { defineMotion: (name: string, fn: MotionFn) => void; + defineAction: (name: string, fn: ActionFn) => void; mapCommand: ( keys: string, type: string, diff --git a/motions/utils/jumpToPattern.ts b/utils/jumpToPattern.ts similarity index 100% rename from motions/utils/jumpToPattern.ts rename to utils/jumpToPattern.ts From be41f0261be8a725d833ed33a5afed5a8eb63418 Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Sat, 27 Apr 2024 12:44:14 -0700 Subject: [PATCH 08/20] refactor: extract out helper functions for defining obsidian vim actions --- actions/moveSkippingFolds.ts | 36 +++++++++++++++++++ main.ts | 34 +++--------------- motions/jumpToHeading.ts | 2 +- motions/jumpToLink.ts | 2 +- .../{defineObsidianVimMotion.ts => vimApi.ts} | 23 +++++++++++- 5 files changed, 65 insertions(+), 32 deletions(-) create mode 100644 actions/moveSkippingFolds.ts rename utils/{defineObsidianVimMotion.ts => vimApi.ts} (56%) diff --git a/actions/moveSkippingFolds.ts b/actions/moveSkippingFolds.ts new file mode 100644 index 0000000..f26294f --- /dev/null +++ b/actions/moveSkippingFolds.ts @@ -0,0 +1,36 @@ +import { Editor as ObsidianEditor } from "obsidian"; +import { ObsidianActionFn } from "../utils/vimApi"; + +export const moveDownSkippingFolds: ObsidianActionFn = ( + obsidianEditor, + cm, + { repeat } +) => { + moveSkippingFolds(obsidianEditor, repeat, "down"); +}; + +export const moveUpSkippingFolds: ObsidianActionFn = ( + obsidianEditor, + cm, + { repeat } +) => { + moveSkippingFolds(obsidianEditor, repeat, "up"); +}; + +function moveSkippingFolds( + obsidianEditor: ObsidianEditor, + repeat: number, + direction: "up" | "down" +) { + 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]; + } +} diff --git a/main.ts b/main.ts index d0d2ab8..4b1b697 100644 --- a/main.ts +++ b/main.ts @@ -1,8 +1,9 @@ import * as keyFromAccelerator from 'keyboardevent-from-electron-accelerator'; import { App, EditorSelection, MarkdownView, Notice, Editor as ObsidianEditor, Plugin, PluginSettingTab, Setting } from 'obsidian'; +import { moveDownSkippingFolds, moveUpSkippingFolds } from './actions/moveSkippingFolds'; import { jumpToNextHeading, jumpToPreviousHeading } from './motions/jumpToHeading'; import { jumpToNextLink, jumpToPreviousLink } from './motions/jumpToLink'; -import { VimApi, defineObsidianVimMotion } from './utils/defineObsidianVimMotion'; +import { VimApi, defineObsidianVimAction, defineObsidianVimMotion } from './utils/vimApi'; declare const CodeMirror: any; @@ -382,34 +383,9 @@ export default class VimrcPlugin extends Plugin { defineObsidianVimMotion(vimObject, jumpToNextLink, 'gl'); defineObsidianVimMotion(vimObject, jumpToPreviousLink, 'gL'); - vimObject.defineAction('moveDownSkipFold', (cm, {repeat}) => { - const obsidianEditor = this.getActiveObsidianEditor(); - let {line: oldLine, ch: oldCh} = obsidianEditor.getCursor(); - for (let i = 0; i < repeat; i++) { - obsidianEditor.exec("goDown"); - const {line: newLine, ch: newCh} = obsidianEditor.getCursor(); - if (newLine === oldLine && newCh === oldCh) { - // Going down doesn't do anything anymore, stop now - return; - } - [oldLine, oldCh] = [newLine, newCh]; - } - }); - vimObject.mapCommand('zj', 'action', 'moveDownSkipFold', undefined, {}); - vimObject.defineAction('moveUpSkipFold', (cm, {repeat}) => { - const obsidianEditor = this.getActiveObsidianEditor(); - let {line: oldLine, ch: oldCh} = obsidianEditor.getCursor(); - for (let i = 0; i < repeat; i++) { - obsidianEditor.exec("goUp"); - const {line: newLine, ch: newCh} = obsidianEditor.getCursor(); - if (newLine === oldLine && newCh === oldCh) { - // Going up doesn't do anything anymore, stop now - return; - } - [oldLine, oldCh] = [newLine, newCh]; - } - }); - vimObject.mapCommand('zk', 'action', 'moveUpSkipFold', undefined, {}); + const getActiveObsidianEditor: () => ObsidianEditor = this.getActiveObsidianEditor.bind(this); + defineObsidianVimAction(vimObject, getActiveObsidianEditor, moveDownSkippingFolds, 'zj'); + defineObsidianVimAction(vimObject, getActiveObsidianEditor, moveUpSkippingFolds, 'zk'); } defineSendKeys(vimObject: any) { diff --git a/motions/jumpToHeading.ts b/motions/jumpToHeading.ts index b4f2180..babea46 100644 --- a/motions/jumpToHeading.ts +++ b/motions/jumpToHeading.ts @@ -1,5 +1,5 @@ -import { MotionFn } from "../utils/defineObsidianVimMotion"; import { jumpToPattern } from "../utils/jumpToPattern"; +import { MotionFn } from "../utils/vimApi"; const HEADING_REGEX = /^#+ /gm; diff --git a/motions/jumpToLink.ts b/motions/jumpToLink.ts index b61ce0e..4a3748f 100644 --- a/motions/jumpToLink.ts +++ b/motions/jumpToLink.ts @@ -1,5 +1,5 @@ -import { MotionFn } from "../utils/defineObsidianVimMotion"; import { jumpToPattern } from "../utils/jumpToPattern"; +import { MotionFn } from "../utils/vimApi"; const LINK_REGEX = /\[\[[^\]\]]+?\]\]/g; diff --git a/utils/defineObsidianVimMotion.ts b/utils/vimApi.ts similarity index 56% rename from utils/defineObsidianVimMotion.ts rename to utils/vimApi.ts index b520cdf..1e3d4ef 100644 --- a/utils/defineObsidianVimMotion.ts +++ b/utils/vimApi.ts @@ -1,5 +1,5 @@ import { Editor as CodeMirrorEditor } from "codemirror"; -import { EditorPosition } from "obsidian"; +import { EditorPosition, Editor as ObsidianEditor } from "obsidian"; export type MotionFn = ( cm: CodeMirrorEditor, @@ -13,6 +13,13 @@ export type ActionFn = ( vimState: any ) => void; +export type ObsidianActionFn = ( + obsidianEditor: ObsidianEditor, + cm: CodeMirrorEditor, + actionArgs: { repeat: number }, + vimState: any +) => void; + /** * Partial representation of the CodeMirror Vim API that we use to define motions, commands, etc. * @@ -38,3 +45,17 @@ export function defineObsidianVimMotion( vimObject.defineMotion(motionFn.name, motionFn); vimObject.mapCommand(mapping, "motion", motionFn.name, undefined, {}); } + +export function defineObsidianVimAction( + vimObject: VimApi, + getActiveObsidianEditor: () => ObsidianEditor, + obsidianActionFn: ObsidianActionFn, + mapping: string +) { + const actionFn = (cm: CodeMirrorEditor, actionArgs: { repeat: number }, vimState: any) => { + const obsidianEditor = getActiveObsidianEditor(); + obsidianActionFn(obsidianEditor, cm, actionArgs, vimState); + } + vimObject.defineAction(obsidianActionFn.name, actionFn); + vimObject.mapCommand(mapping, "action", obsidianActionFn.name, undefined, {}); +} \ No newline at end of file From 45c47890606f9aea09f2815606342d69219eaf51 Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Sat, 27 Apr 2024 13:07:57 -0700 Subject: [PATCH 09/20] refactor: split vimApi.ts into two files --- actions/moveSkippingFolds.ts | 3 ++- main.ts | 4 +++- utils/obsidianVimCommand.ts | 42 +++++++++++++++++++++++++++++++++ utils/vimApi.ts | 45 ++++++++---------------------------- 4 files changed, 56 insertions(+), 38 deletions(-) create mode 100644 utils/obsidianVimCommand.ts diff --git a/actions/moveSkippingFolds.ts b/actions/moveSkippingFolds.ts index f26294f..7bfb5b1 100644 --- a/actions/moveSkippingFolds.ts +++ b/actions/moveSkippingFolds.ts @@ -1,5 +1,6 @@ import { Editor as ObsidianEditor } from "obsidian"; -import { ObsidianActionFn } from "../utils/vimApi"; + +import { ObsidianActionFn } from "../utils/obsidianVimCommand"; export const moveDownSkippingFolds: ObsidianActionFn = ( obsidianEditor, diff --git a/main.ts b/main.ts index 4b1b697..860aa53 100644 --- a/main.ts +++ b/main.ts @@ -1,9 +1,11 @@ import * as keyFromAccelerator from 'keyboardevent-from-electron-accelerator'; import { App, EditorSelection, MarkdownView, Notice, Editor as ObsidianEditor, Plugin, PluginSettingTab, Setting } from 'obsidian'; + import { moveDownSkippingFolds, moveUpSkippingFolds } from './actions/moveSkippingFolds'; import { jumpToNextHeading, jumpToPreviousHeading } from './motions/jumpToHeading'; import { jumpToNextLink, jumpToPreviousLink } from './motions/jumpToLink'; -import { VimApi, defineObsidianVimAction, defineObsidianVimMotion } from './utils/vimApi'; +import { defineObsidianVimAction, defineObsidianVimMotion } from './utils/obsidianVimCommand'; +import { VimApi } from './utils/vimApi'; declare const CodeMirror: any; diff --git a/utils/obsidianVimCommand.ts b/utils/obsidianVimCommand.ts new file mode 100644 index 0000000..0dc77e8 --- /dev/null +++ b/utils/obsidianVimCommand.ts @@ -0,0 +1,42 @@ +/** + * Utility types and functions for defining Obsidian-specific Vim commands. + */ + +import { Editor as CodeMirrorEditor } from "codemirror"; +import { Editor as ObsidianEditor } from "obsidian"; + +import { MotionFn, VimApi } from "./vimApi"; + +export type ObsidianActionFn = ( + obsidianEditor: ObsidianEditor, + cm: CodeMirrorEditor, + actionArgs: { repeat: number }, + vimState: any +) => void; + +export function defineObsidianVimMotion( + vimObject: VimApi, + motionFn: MotionFn, + mapping: string +) { + vimObject.defineMotion(motionFn.name, motionFn); + vimObject.mapCommand(mapping, "motion", motionFn.name, undefined, {}); +} + +export function defineObsidianVimAction( + vimObject: VimApi, + getActiveObsidianEditor: () => ObsidianEditor, + obsidianActionFn: ObsidianActionFn, + mapping: string +) { + const actionFn = ( + cm: CodeMirrorEditor, + actionArgs: { repeat: number }, + vimState: any + ) => { + const obsidianEditor = getActiveObsidianEditor(); + obsidianActionFn(obsidianEditor, cm, actionArgs, vimState); + }; + vimObject.defineAction(obsidianActionFn.name, actionFn); + vimObject.mapCommand(mapping, "action", obsidianActionFn.name, undefined, {}); +} diff --git a/utils/vimApi.ts b/utils/vimApi.ts index 1e3d4ef..348be43 100644 --- a/utils/vimApi.ts +++ b/utils/vimApi.ts @@ -1,5 +1,13 @@ +/** + * Partial representation of the CodeMirror Vim API that we use to define motions, commands, etc. + * + * References: + * https://github.com/replit/codemirror-vim/blob/master/src/vim.js + * https://libvoyant.ucr.edu/resources/codemirror/doc/manual.html + */ + import { Editor as CodeMirrorEditor } from "codemirror"; -import { EditorPosition, Editor as ObsidianEditor } from "obsidian"; +import { EditorPosition } from "obsidian"; export type MotionFn = ( cm: CodeMirrorEditor, @@ -13,18 +21,6 @@ export type ActionFn = ( vimState: any ) => void; -export type ObsidianActionFn = ( - obsidianEditor: ObsidianEditor, - cm: CodeMirrorEditor, - actionArgs: { repeat: number }, - vimState: any -) => void; - -/** - * Partial representation of the CodeMirror Vim API that we use to define motions, commands, etc. - * - * Reference: https://github.com/replit/codemirror-vim/blob/master/src/vim.js - */ export type VimApi = { defineMotion: (name: string, fn: MotionFn) => void; defineAction: (name: string, fn: ActionFn) => void; @@ -36,26 +32,3 @@ export type VimApi = { extra: { [x: string]: any } ) => void; }; - -export function defineObsidianVimMotion( - vimObject: VimApi, - motionFn: MotionFn, - mapping: string -) { - vimObject.defineMotion(motionFn.name, motionFn); - vimObject.mapCommand(mapping, "motion", motionFn.name, undefined, {}); -} - -export function defineObsidianVimAction( - vimObject: VimApi, - getActiveObsidianEditor: () => ObsidianEditor, - obsidianActionFn: ObsidianActionFn, - mapping: string -) { - const actionFn = (cm: CodeMirrorEditor, actionArgs: { repeat: number }, vimState: any) => { - const obsidianEditor = getActiveObsidianEditor(); - obsidianActionFn(obsidianEditor, cm, actionArgs, vimState); - } - vimObject.defineAction(obsidianActionFn.name, actionFn); - vimObject.mapCommand(mapping, "action", obsidianActionFn.name, undefined, {}); -} \ No newline at end of file From 51fc17cf985344a8ea563d3579f4c4ce34107fb8 Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Sat, 27 Apr 2024 13:09:03 -0700 Subject: [PATCH 10/20] refactor: add comment --- utils/jumpToPattern.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/utils/jumpToPattern.ts b/utils/jumpToPattern.ts index cff6a9d..f44f98b 100644 --- a/utils/jumpToPattern.ts +++ b/utils/jumpToPattern.ts @@ -1,6 +1,8 @@ import { Editor as CodeMirrorEditor } from "codemirror"; import { EditorPosition } from "obsidian"; import { shim as matchAllShim } from "string.prototype.matchall"; + +// Polyfill for String.prototype.matchAll, in case it's not available (pre-ES2020) matchAllShim(); export function jumpToPattern({ From 57bf405cb04be3f63330a097c2c20b2389958b6a Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Sat, 27 Apr 2024 13:59:22 -0700 Subject: [PATCH 11/20] refactor: update names, types, etc --- main.ts | 4 ++-- utils/jumpToPattern.ts | 25 +++++++++++++------------ utils/obsidianVimCommand.ts | 8 ++------ 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/main.ts b/main.ts index 860aa53..1a06123 100644 --- a/main.ts +++ b/main.ts @@ -267,7 +267,7 @@ export default class VimrcPlugin extends Plugin { var cmEditor = this.getCodeMirror(view); if (cmEditor && !this.codeMirrorVimObject.loadedVimrc) { this.defineBasicCommands(this.codeMirrorVimObject); - this.defineObsidianVimMotions(this.codeMirrorVimObject); + this.defineObsidianVimCommands(this.codeMirrorVimObject); this.defineSendKeys(this.codeMirrorVimObject); this.defineObCommand(this.codeMirrorVimObject); this.defineSurround(this.codeMirrorVimObject); @@ -379,7 +379,7 @@ export default class VimrcPlugin extends Plugin { } - defineObsidianVimMotions(vimObject: VimApi) { + defineObsidianVimCommands(vimObject: VimApi) { defineObsidianVimMotion(vimObject, jumpToNextHeading, 'gh'); defineObsidianVimMotion(vimObject, jumpToPreviousHeading, 'gH'); defineObsidianVimMotion(vimObject, jumpToNextLink, 'gl'); diff --git a/utils/jumpToPattern.ts b/utils/jumpToPattern.ts index f44f98b..e694d57 100644 --- a/utils/jumpToPattern.ts +++ b/utils/jumpToPattern.ts @@ -20,11 +20,9 @@ export function jumpToPattern({ }): EditorPosition { const content = cm.getValue(); const startingIdx = cm.indexFromPos(oldPosition); - const jumpFn = - direction === "next" - ? getNthNextInstanceOfPattern - : getNthPreviousInstanceOfPattern; - const matchIdx = jumpFn({ content, regex, startingIdx, n: repeat }); + const findNthMatchFn = + direction === "next" ? findNthNextRegexMatch : findNthPreviousRegexMatch; + const matchIdx = findNthMatchFn({ content, regex, startingIdx, n: repeat }); if (matchIdx === undefined) { return oldPosition; } @@ -33,10 +31,10 @@ export function jumpToPattern({ } /** - * Returns the index of (up to) the n-th instance of a pattern in a string after a given starting - * index. If the pattern is not found at all, returns undefined. + * Returns the index of (up to) the n-th next instance of a pattern in a string after a given + * starting index. If the pattern is not found at all, returns undefined. */ -function getNthNextInstanceOfPattern({ +function findNthNextRegexMatch({ content, regex, startingIdx, @@ -46,12 +44,15 @@ function getNthNextInstanceOfPattern({ regex: RegExp; startingIdx: number; n: number; -}): number { +}): number | undefined { const globalRegex = makeGlobalRegex(regex); globalRegex.lastIndex = startingIdx + 1; let currMatch, lastMatch; let numMatchesFound = 0; - while (numMatchesFound < n && (currMatch = globalRegex.exec(content)) != null) { + while ( + numMatchesFound < n && + (currMatch = globalRegex.exec(content)) != null + ) { lastMatch = currMatch; numMatchesFound++; } @@ -59,10 +60,10 @@ function getNthNextInstanceOfPattern({ } /** - * Returns the index of (up to) the nth-last instance of a pattern in a string before a given + * Returns the index of (up to) the nth-previous instance of a pattern in a string before a given * starting index. If the pattern is not found at all, returns undefined. */ -function getNthPreviousInstanceOfPattern({ +function findNthPreviousRegexMatch({ content, regex, startingIdx, diff --git a/utils/obsidianVimCommand.ts b/utils/obsidianVimCommand.ts index 0dc77e8..e0490df 100644 --- a/utils/obsidianVimCommand.ts +++ b/utils/obsidianVimCommand.ts @@ -5,7 +5,7 @@ import { Editor as CodeMirrorEditor } from "codemirror"; import { Editor as ObsidianEditor } from "obsidian"; -import { MotionFn, VimApi } from "./vimApi"; +import { ActionFn, MotionFn, VimApi } from "./vimApi"; export type ObsidianActionFn = ( obsidianEditor: ObsidianEditor, @@ -29,11 +29,7 @@ export function defineObsidianVimAction( obsidianActionFn: ObsidianActionFn, mapping: string ) { - const actionFn = ( - cm: CodeMirrorEditor, - actionArgs: { repeat: number }, - vimState: any - ) => { + const actionFn: ActionFn = (cm, actionArgs, vimState) => { const obsidianEditor = getActiveObsidianEditor(); obsidianActionFn(obsidianEditor, cm, actionArgs, vimState); }; From 520914934bc0f283d6971542928091878b3ae9bb Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Sat, 27 Apr 2024 15:48:46 -0700 Subject: [PATCH 12/20] feat: followLinkUnderCursor action --- actions/followLinkUnderCursor.ts | 23 ++++++++++++++ actions/moveSkippingFolds.ts | 14 ++++----- main.ts | 54 +++++++++++++++++--------------- utils/obsidianVimCommand.ts | 16 +++++----- 4 files changed, 66 insertions(+), 41 deletions(-) create mode 100644 actions/followLinkUnderCursor.ts diff --git a/actions/followLinkUnderCursor.ts b/actions/followLinkUnderCursor.ts new file mode 100644 index 0000000..d25dd91 --- /dev/null +++ b/actions/followLinkUnderCursor.ts @@ -0,0 +1,23 @@ +import { ObsidianActionFn } from "../utils/obsidianVimCommand"; + +export const followLinkUnderCursor: ObsidianActionFn = (vimrcPlugin) => { + // If the cursor is on the starting square bracket(s), we need to move it inside them + const obsidianEditor = vimrcPlugin.getActiveObsidianEditor(); + const { line, ch } = obsidianEditor.getCursor(); + const firstTwoChars = obsidianEditor.getRange( + { line, ch }, + { line, ch: ch + 2 } + ); + let charOffset = 0; + for (const char of firstTwoChars) { + if (char === "[") { + obsidianEditor.exec("goRight"); + charOffset++; + } + } + vimrcPlugin.executeObsidianCommand("editor:follow-link"); + // Move the cursor back to where it was + for (let i = 0; i < charOffset; i++) { + obsidianEditor.exec("goLeft"); + } +}; diff --git a/actions/moveSkippingFolds.ts b/actions/moveSkippingFolds.ts index 7bfb5b1..8527db9 100644 --- a/actions/moveSkippingFolds.ts +++ b/actions/moveSkippingFolds.ts @@ -1,28 +1,28 @@ -import { Editor as ObsidianEditor } from "obsidian"; - +import VimrcPlugin from "../main"; import { ObsidianActionFn } from "../utils/obsidianVimCommand"; export const moveDownSkippingFolds: ObsidianActionFn = ( - obsidianEditor, + vimrcPlugin, cm, { repeat } ) => { - moveSkippingFolds(obsidianEditor, repeat, "down"); + moveSkippingFolds(vimrcPlugin, repeat, "down"); }; export const moveUpSkippingFolds: ObsidianActionFn = ( - obsidianEditor, + vimrcPlugin, cm, { repeat } ) => { - moveSkippingFolds(obsidianEditor, repeat, "up"); + moveSkippingFolds(vimrcPlugin, repeat, "up"); }; function moveSkippingFolds( - obsidianEditor: ObsidianEditor, + vimrcPlugin: VimrcPlugin, repeat: number, direction: "up" | "down" ) { + const obsidianEditor = vimrcPlugin.getActiveObsidianEditor(); let { line: oldLine, ch: oldCh } = obsidianEditor.getCursor(); const commandName = direction === "up" ? "goUp" : "goDown"; for (let i = 0; i < repeat; i++) { diff --git a/main.ts b/main.ts index 1a06123..4c6f417 100644 --- a/main.ts +++ b/main.ts @@ -1,6 +1,7 @@ import * as keyFromAccelerator from 'keyboardevent-from-electron-accelerator'; import { App, EditorSelection, MarkdownView, Notice, Editor as ObsidianEditor, Plugin, PluginSettingTab, Setting } from 'obsidian'; +import { followLinkUnderCursor } from './actions/followLinkUnderCursor'; import { moveDownSkippingFolds, moveUpSkippingFolds } from './actions/moveSkippingFolds'; import { jumpToNextHeading, jumpToPreviousHeading } from './motions/jumpToHeading'; import { jumpToNextLink, jumpToPreviousLink } from './motions/jumpToLink'; @@ -253,7 +254,7 @@ export default class VimrcPlugin extends Plugin { return this.app.workspace.getActiveViewOfType(MarkdownView); } - private getActiveObsidianEditor(): ObsidianEditor { + getActiveObsidianEditor(): ObsidianEditor { return this.getActiveView().editor; } @@ -385,9 +386,9 @@ export default class VimrcPlugin extends Plugin { defineObsidianVimMotion(vimObject, jumpToNextLink, 'gl'); defineObsidianVimMotion(vimObject, jumpToPreviousLink, 'gL'); - const getActiveObsidianEditor: () => ObsidianEditor = this.getActiveObsidianEditor.bind(this); - defineObsidianVimAction(vimObject, getActiveObsidianEditor, moveDownSkippingFolds, 'zj'); - defineObsidianVimAction(vimObject, getActiveObsidianEditor, moveUpSkippingFolds, 'zk'); + defineObsidianVimAction(vimObject, this, moveDownSkippingFolds, 'zj'); + defineObsidianVimAction(vimObject, this, moveUpSkippingFolds, 'zk'); + defineObsidianVimAction(vimObject, this, followLinkUnderCursor, 'gf'); } defineSendKeys(vimObject: any) { @@ -424,33 +425,36 @@ export default class VimrcPlugin extends Plugin { }); } + executeObsidianCommand(commandName: string) { + const availableCommands = (this.app as any).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: any) { vimObject.defineEx('obcommand', '', async (cm: any, params: any) => { - const availableCommands = (this.app as any).commands.commands; if (!params?.args?.length || params.args.length != 1) { + const availableCommands = (this.app as any).commands.commands; console.log(`Available commands: ${Object.keys(availableCommands).join('\n')}`) throw new Error(`obcommand requires exactly 1 parameter`); } - let view = this.getActiveView(); - let editor = view.editor; - 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(false, editor, view); - else if (editorCallback) - editorCallback(editor, view); - 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`); + const commandName = params.args[0]; + this.executeObsidianCommand(commandName); }); } diff --git a/utils/obsidianVimCommand.ts b/utils/obsidianVimCommand.ts index e0490df..2bbf855 100644 --- a/utils/obsidianVimCommand.ts +++ b/utils/obsidianVimCommand.ts @@ -3,12 +3,12 @@ */ import { Editor as CodeMirrorEditor } from "codemirror"; -import { Editor as ObsidianEditor } from "obsidian"; -import { ActionFn, MotionFn, VimApi } from "./vimApi"; +import VimrcPlugin from "../main"; +import { MotionFn, VimApi } from "./vimApi"; export type ObsidianActionFn = ( - obsidianEditor: ObsidianEditor, + vimrcPlugin: VimrcPlugin, cm: CodeMirrorEditor, actionArgs: { repeat: number }, vimState: any @@ -25,14 +25,12 @@ export function defineObsidianVimMotion( export function defineObsidianVimAction( vimObject: VimApi, - getActiveObsidianEditor: () => ObsidianEditor, + vimrcPlugin: VimrcPlugin, obsidianActionFn: ObsidianActionFn, mapping: string ) { - const actionFn: ActionFn = (cm, actionArgs, vimState) => { - const obsidianEditor = getActiveObsidianEditor(); - obsidianActionFn(obsidianEditor, cm, actionArgs, vimState); - }; - vimObject.defineAction(obsidianActionFn.name, actionFn); + vimObject.defineAction(obsidianActionFn.name, (cm, actionArgs, vimState) => { + obsidianActionFn(vimrcPlugin, cm, actionArgs, vimState); + }); vimObject.mapCommand(mapping, "action", obsidianActionFn.name, undefined, {}); } From 585c68c0eabda50adcf4544b894e150331093a66 Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Sat, 27 Apr 2024 15:57:05 -0700 Subject: [PATCH 13/20] feat: jumpToLink now jumps to both markdown and wiki links --- motions/jumpToLink.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/motions/jumpToLink.ts b/motions/jumpToLink.ts index 4a3748f..ae2af27 100644 --- a/motions/jumpToLink.ts +++ b/motions/jumpToLink.ts @@ -1,7 +1,10 @@ import { jumpToPattern } from "../utils/jumpToPattern"; import { MotionFn } from "../utils/vimApi"; -const LINK_REGEX = /\[\[[^\]\]]+?\]\]/g; +const WIKILINK_REGEX_STRING = "\\[\\[[^\\]\\]]+?\\]\\]"; +const MARKDOWN_LINK_REGEX_STRING = "\\[[^\\]]+?\\]\\([^)]+?\\)"; +const LINK_REGEX_STRING = `${WIKILINK_REGEX_STRING}|${MARKDOWN_LINK_REGEX_STRING}`; +const LINK_REGEX = new RegExp(LINK_REGEX_STRING, "g"); export const jumpToNextLink: MotionFn = (cm, oldPosition, { repeat }) => { return jumpToPattern({ From 3d457c2f8c69a7b603916d55a30362b51f92a04b Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Sun, 28 Apr 2024 09:10:12 -0700 Subject: [PATCH 14/20] refactor: rename fns --- main.ts | 21 ++++++++++----------- utils/obsidianVimCommand.ts | 4 ++-- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/main.ts b/main.ts index 4c6f417..4170397 100644 --- a/main.ts +++ b/main.ts @@ -5,7 +5,7 @@ import { followLinkUnderCursor } from './actions/followLinkUnderCursor'; import { moveDownSkippingFolds, moveUpSkippingFolds } from './actions/moveSkippingFolds'; import { jumpToNextHeading, jumpToPreviousHeading } from './motions/jumpToHeading'; import { jumpToNextLink, jumpToPreviousLink } from './motions/jumpToLink'; -import { defineObsidianVimAction, defineObsidianVimMotion } from './utils/obsidianVimCommand'; +import { defineAndMapObsidianVimAction, defineAndMapObsidianVimMotion } from './utils/obsidianVimCommand'; import { VimApi } from './utils/vimApi'; declare const CodeMirror: any; @@ -268,7 +268,7 @@ export default class VimrcPlugin extends Plugin { var cmEditor = this.getCodeMirror(view); if (cmEditor && !this.codeMirrorVimObject.loadedVimrc) { this.defineBasicCommands(this.codeMirrorVimObject); - this.defineObsidianVimCommands(this.codeMirrorVimObject); + this.defineAndMapObsidianVimCommands(this.codeMirrorVimObject); this.defineSendKeys(this.codeMirrorVimObject); this.defineObCommand(this.codeMirrorVimObject); this.defineSurround(this.codeMirrorVimObject); @@ -379,16 +379,15 @@ export default class VimrcPlugin extends Plugin { }); } + defineAndMapObsidianVimCommands(vimObject: VimApi) { + defineAndMapObsidianVimMotion(vimObject, jumpToNextHeading, 'gh'); + defineAndMapObsidianVimMotion(vimObject, jumpToPreviousHeading, 'gH'); + defineAndMapObsidianVimMotion(vimObject, jumpToNextLink, 'gl'); + defineAndMapObsidianVimMotion(vimObject, jumpToPreviousLink, 'gL'); - defineObsidianVimCommands(vimObject: VimApi) { - defineObsidianVimMotion(vimObject, jumpToNextHeading, 'gh'); - defineObsidianVimMotion(vimObject, jumpToPreviousHeading, 'gH'); - defineObsidianVimMotion(vimObject, jumpToNextLink, 'gl'); - defineObsidianVimMotion(vimObject, jumpToPreviousLink, 'gL'); - - defineObsidianVimAction(vimObject, this, moveDownSkippingFolds, 'zj'); - defineObsidianVimAction(vimObject, this, moveUpSkippingFolds, 'zk'); - defineObsidianVimAction(vimObject, this, followLinkUnderCursor, 'gf'); + defineAndMapObsidianVimAction(vimObject, this, moveDownSkippingFolds, 'zj'); + defineAndMapObsidianVimAction(vimObject, this, moveUpSkippingFolds, 'zk'); + defineAndMapObsidianVimAction(vimObject, this, followLinkUnderCursor, 'gf'); } defineSendKeys(vimObject: any) { diff --git a/utils/obsidianVimCommand.ts b/utils/obsidianVimCommand.ts index 2bbf855..d0dbd45 100644 --- a/utils/obsidianVimCommand.ts +++ b/utils/obsidianVimCommand.ts @@ -14,7 +14,7 @@ export type ObsidianActionFn = ( vimState: any ) => void; -export function defineObsidianVimMotion( +export function defineAndMapObsidianVimMotion( vimObject: VimApi, motionFn: MotionFn, mapping: string @@ -23,7 +23,7 @@ export function defineObsidianVimMotion( vimObject.mapCommand(mapping, "motion", motionFn.name, undefined, {}); } -export function defineObsidianVimAction( +export function defineAndMapObsidianVimAction( vimObject: VimApi, vimrcPlugin: VimrcPlugin, obsidianActionFn: ObsidianActionFn, From 2adf45cdbc2acbb61a524ba6af7155eaa52c3ead Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Fri, 10 May 2024 11:20:21 -0700 Subject: [PATCH 15/20] refactor: add docstrings / change var names --- actions/followLinkUnderCursor.ts | 11 +++++++---- actions/moveSkippingFolds.ts | 6 ++++++ motions/jumpToHeading.ts | 6 ++++++ motions/jumpToLink.ts | 6 ++++++ 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/actions/followLinkUnderCursor.ts b/actions/followLinkUnderCursor.ts index d25dd91..fe4faaa 100644 --- a/actions/followLinkUnderCursor.ts +++ b/actions/followLinkUnderCursor.ts @@ -1,23 +1,26 @@ import { ObsidianActionFn } from "../utils/obsidianVimCommand"; +/** + * 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). + */ export const followLinkUnderCursor: ObsidianActionFn = (vimrcPlugin) => { - // If the cursor is on the starting square bracket(s), we need to move it inside them const obsidianEditor = vimrcPlugin.getActiveObsidianEditor(); const { line, ch } = obsidianEditor.getCursor(); const firstTwoChars = obsidianEditor.getRange( { line, ch }, { line, ch: ch + 2 } ); - let charOffset = 0; + let numCharsMoved = 0; for (const char of firstTwoChars) { if (char === "[") { obsidianEditor.exec("goRight"); - charOffset++; + numCharsMoved++; } } vimrcPlugin.executeObsidianCommand("editor:follow-link"); // Move the cursor back to where it was - for (let i = 0; i < charOffset; i++) { + for (let i = 0; i < numCharsMoved; i++) { obsidianEditor.exec("goLeft"); } }; diff --git a/actions/moveSkippingFolds.ts b/actions/moveSkippingFolds.ts index 8527db9..a513a93 100644 --- a/actions/moveSkippingFolds.ts +++ b/actions/moveSkippingFolds.ts @@ -1,6 +1,9 @@ import VimrcPlugin from "../main"; import { ObsidianActionFn } from "../utils/obsidianVimCommand"; +/** + * Moves the cursor down `repeat` lines, skipping over folded sections. + */ export const moveDownSkippingFolds: ObsidianActionFn = ( vimrcPlugin, cm, @@ -9,6 +12,9 @@ export const moveDownSkippingFolds: ObsidianActionFn = ( moveSkippingFolds(vimrcPlugin, repeat, "down"); }; +/** + * Moves the cursor up `repeat` lines, skipping over folded sections. + */ export const moveUpSkippingFolds: ObsidianActionFn = ( vimrcPlugin, cm, diff --git a/motions/jumpToHeading.ts b/motions/jumpToHeading.ts index babea46..38541ac 100644 --- a/motions/jumpToHeading.ts +++ b/motions/jumpToHeading.ts @@ -3,6 +3,9 @@ import { MotionFn } from "../utils/vimApi"; const HEADING_REGEX = /^#+ /gm; +/** + * Jumps to the repeat-th next heading. + */ export const jumpToNextHeading: MotionFn = (cm, oldPosition, { repeat }) => { return jumpToPattern({ cm, @@ -13,6 +16,9 @@ export const jumpToNextHeading: MotionFn = (cm, oldPosition, { repeat }) => { }); }; +/** + * Jumps to the repeat-th previous heading. + */ export const jumpToPreviousHeading: MotionFn = ( cm, oldPosition, diff --git a/motions/jumpToLink.ts b/motions/jumpToLink.ts index ae2af27..9ba9624 100644 --- a/motions/jumpToLink.ts +++ b/motions/jumpToLink.ts @@ -6,6 +6,9 @@ const MARKDOWN_LINK_REGEX_STRING = "\\[[^\\]]+?\\]\\([^)]+?\\)"; const LINK_REGEX_STRING = `${WIKILINK_REGEX_STRING}|${MARKDOWN_LINK_REGEX_STRING}`; const LINK_REGEX = new RegExp(LINK_REGEX_STRING, "g"); +/** + * Jumps to the repeat-th next link. +*/ export const jumpToNextLink: MotionFn = (cm, oldPosition, { repeat }) => { return jumpToPattern({ cm, @@ -16,6 +19,9 @@ export const jumpToNextLink: MotionFn = (cm, oldPosition, { repeat }) => { }); }; +/** + * Jumps to the repeat-th previous link. + */ export const jumpToPreviousLink: MotionFn = (cm, oldPosition, { repeat }) => { return jumpToPattern({ cm, From 19d095883e6b4744060f10c1136fbcdc801fc488 Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Sat, 11 May 2024 12:23:23 -0700 Subject: [PATCH 16/20] feat: implement looping around --- utils/jumpToPattern.ts | 75 +++++++++++++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 20 deletions(-) diff --git a/utils/jumpToPattern.ts b/utils/jumpToPattern.ts index e694d57..13e8c46 100644 --- a/utils/jumpToPattern.ts +++ b/utils/jumpToPattern.ts @@ -5,6 +5,12 @@ import { shim as matchAllShim } from "string.prototype.matchall"; // Polyfill for String.prototype.matchAll, in case it's not available (pre-ES2020) matchAllShim(); +/** + * Returns the position of the repeat-th instance of a pattern from a given starting position, in + * the given direction; looping to the other end of the document when reaching one end. + * + * Under the hood, we avoid repeated loops around the document by using modulo arithmetic. + */ export function jumpToPattern({ cm, oldPosition, @@ -31,8 +37,11 @@ export function jumpToPattern({ } /** - * Returns the index of (up to) the n-th next instance of a pattern in a string after a given - * starting index. If the pattern is not found at all, returns undefined. + * Returns the index (from the start of the content) of the nth-next instance of a pattern after a + * given starting index. + * + * "Loops" to the top of the document when the bottom is reached; under the hood, we avoid repeated + * loops by using modulo arithmetic. */ function findNthNextRegexMatch({ content, @@ -46,22 +55,23 @@ function findNthNextRegexMatch({ n: number; }): number | undefined { const globalRegex = makeGlobalRegex(regex); - globalRegex.lastIndex = startingIdx + 1; - let currMatch, lastMatch; - let numMatchesFound = 0; - while ( - numMatchesFound < n && - (currMatch = globalRegex.exec(content)) != null - ) { - lastMatch = currMatch; - numMatchesFound++; + const allMatches = [...content.matchAll(globalRegex)]; + const previousMatches = allMatches.filter((match) => match.index <= startingIdx); + const nextMatches = allMatches.filter((match) => match.index > startingIdx); + const nModulo = n % allMatches.length; + const effectiveN = nModulo === 0 ? allMatches.length : nModulo; + if (effectiveN <= nextMatches.length) { + return nextMatches[effectiveN - 1].index; } - return lastMatch?.index; + return previousMatches[effectiveN - nextMatches.length - 1].index; } /** - * Returns the index of (up to) the nth-previous instance of a pattern in a string before a given - * starting index. If the pattern is not found at all, returns undefined. + * Returns the index (from the start of the content) of the nth-previous instance of a pattern + * before a given starting index. + * + * "Loops" to the bottom of the document when the top is reached; under the hood, we avoid repeated + * loops by using modulo arithmetic. */ function findNthPreviousRegexMatch({ content, @@ -75,12 +85,11 @@ function findNthPreviousRegexMatch({ n: number; }): number | undefined { const globalRegex = makeGlobalRegex(regex); - const contentToSearch = content.substring(0, startingIdx); - const previousMatches = [...contentToSearch.matchAll(globalRegex)]; - if (previousMatches.length < n) { - return previousMatches[0]?.index; - } - return previousMatches[previousMatches.length - n].index; + const allMatches = [...content.matchAll(globalRegex)]; + const previousMatches = allMatches.filter((match) => match.index < startingIdx); + const nextMatches = allMatches.filter((match) => match.index >= startingIdx); + const match = getNthPreviousMatch(previousMatches, nextMatches, n); + return match?.index; } function makeGlobalRegex(regex: RegExp): RegExp { @@ -92,3 +101,29 @@ function getGlobalFlags(regex: RegExp): string { const { flags } = regex; return flags.includes("g") ? flags : `${flags}g`; } + +function getNthPreviousMatch( + previousMatches: RegExpExecArray[], + nextMatches: RegExpExecArray[], + n: number +): RegExpExecArray | undefined { + const numMatches = previousMatches.length + nextMatches.length; + const effectiveN = n % numMatches; // every `numMatches` is a full loop + if (effectiveN <= previousMatches.length) { + return getNthItemFromEnd(previousMatches, effectiveN); + } + return getNthItemFromEnd(nextMatches, effectiveN - previousMatches.length); +} + +/** + * Returns the nth (1-indexed) item from the end of an array. Expects 1 <= n <= items.length, but + * just returns undefined if n is out of bounds. + */ +function getNthItemFromEnd(items: T[], n: number): T | undefined { + const numItems = items.length; + if (n < 1 || n > numItems) { + console.warn(`Invalid n: ${n} for array of length ${numItems}`); + return undefined; + } + return items[numItems - n]; +} From 5953f5e662ca9d95d2b5e2e48c7f84614f8cf840 Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Sat, 11 May 2024 15:05:46 -0700 Subject: [PATCH 17/20] refactor: cleaner implementation of jumpToPattern --- motions/jumpToHeading.ts | 8 +-- motions/jumpToLink.ts | 8 +-- utils/jumpToPattern.ts | 137 ++++++++++++++++-------------------- utils/obsidianVimCommand.ts | 7 +- utils/vimApi.ts | 3 +- 5 files changed, 73 insertions(+), 90 deletions(-) diff --git a/motions/jumpToHeading.ts b/motions/jumpToHeading.ts index 38541ac..74e9b67 100644 --- a/motions/jumpToHeading.ts +++ b/motions/jumpToHeading.ts @@ -6,10 +6,10 @@ const HEADING_REGEX = /^#+ /gm; /** * Jumps to the repeat-th next heading. */ -export const jumpToNextHeading: MotionFn = (cm, oldPosition, { repeat }) => { +export const jumpToNextHeading: MotionFn = (cm, cursorPosition, { repeat }) => { return jumpToPattern({ cm, - oldPosition, + cursorPosition, repeat, regex: HEADING_REGEX, direction: "next", @@ -21,12 +21,12 @@ export const jumpToNextHeading: MotionFn = (cm, oldPosition, { repeat }) => { */ export const jumpToPreviousHeading: MotionFn = ( cm, - oldPosition, + cursorPosition, { repeat } ) => { return jumpToPattern({ cm, - oldPosition, + cursorPosition, repeat, regex: HEADING_REGEX, direction: "previous", diff --git a/motions/jumpToLink.ts b/motions/jumpToLink.ts index 9ba9624..c60a6b3 100644 --- a/motions/jumpToLink.ts +++ b/motions/jumpToLink.ts @@ -9,10 +9,10 @@ const LINK_REGEX = new RegExp(LINK_REGEX_STRING, "g"); /** * Jumps to the repeat-th next link. */ -export const jumpToNextLink: MotionFn = (cm, oldPosition, { repeat }) => { +export const jumpToNextLink: MotionFn = (cm, cursorPosition, { repeat }) => { return jumpToPattern({ cm, - oldPosition, + cursorPosition, repeat, regex: LINK_REGEX, direction: "next", @@ -22,10 +22,10 @@ export const jumpToNextLink: MotionFn = (cm, oldPosition, { repeat }) => { /** * Jumps to the repeat-th previous link. */ -export const jumpToPreviousLink: MotionFn = (cm, oldPosition, { repeat }) => { +export const jumpToPreviousLink: MotionFn = (cm, cursorPosition, { repeat }) => { return jumpToPattern({ cm, - oldPosition, + cursorPosition, repeat, regex: LINK_REGEX, direction: "previous", diff --git a/utils/jumpToPattern.ts b/utils/jumpToPattern.ts index 13e8c46..ef7e028 100644 --- a/utils/jumpToPattern.ts +++ b/utils/jumpToPattern.ts @@ -6,90 +6,97 @@ import { shim as matchAllShim } from "string.prototype.matchall"; matchAllShim(); /** - * Returns the position of the repeat-th instance of a pattern from a given starting position, in - * the given direction; looping to the other end of the document when reaching one end. + * 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, we avoid repeated loops around the document by using modulo arithmetic. + * 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. */ export function jumpToPattern({ cm, - oldPosition, + cursorPosition, repeat, regex, direction, }: { cm: CodeMirrorEditor; - oldPosition: EditorPosition; + cursorPosition: EditorPosition; repeat: number; regex: RegExp; direction: "next" | "previous"; }): EditorPosition { const content = cm.getValue(); - const startingIdx = cm.indexFromPos(oldPosition); - const findNthMatchFn = - direction === "next" ? findNthNextRegexMatch : findNthPreviousRegexMatch; - const matchIdx = findNthMatchFn({ content, regex, startingIdx, n: repeat }); + const cursorIdx = cm.indexFromPos(cursorPosition); + const orderedMatches = getOrderedMatches({ + content, + regex, + cursorIdx, + direction, + }); + const effectiveRepeat = (repeat % orderedMatches.length) || orderedMatches.length; + const matchIdx = orderedMatches[effectiveRepeat - 1]?.index; if (matchIdx === undefined) { - return oldPosition; + return cursorPosition; } - const newPosition = cm.posFromIndex(matchIdx); - return newPosition; + const newCursorPosition = cm.posFromIndex(matchIdx); + return newCursorPosition; } /** - * Returns the index (from the start of the content) of the nth-next instance of a pattern after a - * given starting index. - * - * "Loops" to the top of the document when the bottom is reached; under the hood, we avoid repeated - * loops by using modulo arithmetic. + * 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 findNthNextRegexMatch({ +function getOrderedMatches({ content, regex, - startingIdx, - n, + cursorIdx, + direction, }: { content: string; regex: RegExp; - startingIdx: number; - n: number; -}): number | undefined { - const globalRegex = makeGlobalRegex(regex); - const allMatches = [...content.matchAll(globalRegex)]; - const previousMatches = allMatches.filter((match) => match.index <= startingIdx); - const nextMatches = allMatches.filter((match) => match.index > startingIdx); - const nModulo = n % allMatches.length; - const effectiveN = nModulo === 0 ? allMatches.length : nModulo; - if (effectiveN <= nextMatches.length) { - return nextMatches[effectiveN - 1].index; + cursorIdx: number; + direction: "next" | "previous"; +}): RegExpExecArray[] { + const { previousMatches, currentMatches, nextMatches } = getAndGroupMatches( + content, + regex, + cursorIdx + ); + if (direction === "next") { + return [...nextMatches, ...previousMatches, ...currentMatches]; } - return previousMatches[effectiveN - nextMatches.length - 1].index; + return [ + ...previousMatches.reverse(), + ...nextMatches.reverse(), + ...currentMatches.reverse(), + ]; } /** - * Returns the index (from the start of the content) of the nth-previous instance of a pattern - * before a given starting index. - * - * "Loops" to the bottom of the document when the top is reached; under the hood, we avoid repeated - * loops by using modulo arithmetic. + * Finds all matches of a regex in a string and groups them by their positions relative to the + * cursor. */ -function findNthPreviousRegexMatch({ - content, - regex, - startingIdx, - n, -}: { - content: string; - regex: RegExp; - startingIdx: number; - n: number; -}): number | undefined { +function getAndGroupMatches( + content: string, + regex: RegExp, + cursorIdx: number +): { + previousMatches: RegExpExecArray[]; + currentMatches: RegExpExecArray[]; + nextMatches: RegExpExecArray[]; +} { const globalRegex = makeGlobalRegex(regex); const allMatches = [...content.matchAll(globalRegex)]; - const previousMatches = allMatches.filter((match) => match.index < startingIdx); - const nextMatches = allMatches.filter((match) => match.index >= startingIdx); - const match = getNthPreviousMatch(previousMatches, nextMatches, n); - return match?.index; + const previousMatches = allMatches.filter( + (match) => match.index < cursorIdx && !isCursorOnMatch(match, cursorIdx) + ); + const currentMatches = allMatches.filter((match) => + isCursorOnMatch(match, cursorIdx) + ); + const nextMatches = allMatches.filter((match) => match.index > cursorIdx); + return { previousMatches, currentMatches, nextMatches }; } function makeGlobalRegex(regex: RegExp): RegExp { @@ -102,28 +109,6 @@ function getGlobalFlags(regex: RegExp): string { return flags.includes("g") ? flags : `${flags}g`; } -function getNthPreviousMatch( - previousMatches: RegExpExecArray[], - nextMatches: RegExpExecArray[], - n: number -): RegExpExecArray | undefined { - const numMatches = previousMatches.length + nextMatches.length; - const effectiveN = n % numMatches; // every `numMatches` is a full loop - if (effectiveN <= previousMatches.length) { - return getNthItemFromEnd(previousMatches, effectiveN); - } - return getNthItemFromEnd(nextMatches, effectiveN - previousMatches.length); -} - -/** - * Returns the nth (1-indexed) item from the end of an array. Expects 1 <= n <= items.length, but - * just returns undefined if n is out of bounds. - */ -function getNthItemFromEnd(items: T[], n: number): T | undefined { - const numItems = items.length; - if (n < 1 || n > numItems) { - console.warn(`Invalid n: ${n} for array of length ${numItems}`); - return undefined; - } - return items[numItems - n]; +function isCursorOnMatch(match: RegExpExecArray, cursorIdx: number): boolean { + return match.index <= cursorIdx && cursorIdx < match.index + match[0].length; } diff --git a/utils/obsidianVimCommand.ts b/utils/obsidianVimCommand.ts index d0dbd45..39f1499 100644 --- a/utils/obsidianVimCommand.ts +++ b/utils/obsidianVimCommand.ts @@ -8,10 +8,9 @@ import VimrcPlugin from "../main"; import { MotionFn, VimApi } from "./vimApi"; export type ObsidianActionFn = ( - vimrcPlugin: VimrcPlugin, + vimrcPlugin: VimrcPlugin, // Included so we can run Obsidian commands as part of the action cm: CodeMirrorEditor, actionArgs: { repeat: number }, - vimState: any ) => void; export function defineAndMapObsidianVimMotion( @@ -29,8 +28,8 @@ export function defineAndMapObsidianVimAction( obsidianActionFn: ObsidianActionFn, mapping: string ) { - vimObject.defineAction(obsidianActionFn.name, (cm, actionArgs, vimState) => { - obsidianActionFn(vimrcPlugin, cm, actionArgs, vimState); + vimObject.defineAction(obsidianActionFn.name, (cm, actionArgs) => { + obsidianActionFn(vimrcPlugin, cm, actionArgs); }); vimObject.mapCommand(mapping, "action", obsidianActionFn.name, undefined, {}); } diff --git a/utils/vimApi.ts b/utils/vimApi.ts index 348be43..60a662c 100644 --- a/utils/vimApi.ts +++ b/utils/vimApi.ts @@ -11,14 +11,13 @@ import { EditorPosition } from "obsidian"; export type MotionFn = ( cm: CodeMirrorEditor, - oldPosition: EditorPosition, + cursorPosition: EditorPosition, // called `head` in the API motionArgs: { repeat: number } ) => EditorPosition; export type ActionFn = ( cm: CodeMirrorEditor, actionArgs: { repeat: number }, - vimState: any ) => void; export type VimApi = { From 5b2a07d4662a4a99c0ea2c9730f951ab4070d846 Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Sat, 11 May 2024 16:14:58 -0700 Subject: [PATCH 18/20] Change mappings for next/prev heading to [[ and ]] --- main.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.ts b/main.ts index 4170397..af3ed82 100644 --- a/main.ts +++ b/main.ts @@ -380,8 +380,8 @@ export default class VimrcPlugin extends Plugin { } defineAndMapObsidianVimCommands(vimObject: VimApi) { - defineAndMapObsidianVimMotion(vimObject, jumpToNextHeading, 'gh'); - defineAndMapObsidianVimMotion(vimObject, jumpToPreviousHeading, 'gH'); + defineAndMapObsidianVimMotion(vimObject, jumpToNextHeading, ']]'); + defineAndMapObsidianVimMotion(vimObject, jumpToPreviousHeading, '[['); defineAndMapObsidianVimMotion(vimObject, jumpToNextLink, 'gl'); defineAndMapObsidianVimMotion(vimObject, jumpToPreviousLink, 'gL'); From cd7d5e929303453113d2e730002b37695b55ad2a Mon Sep 17 00:00:00 2001 From: Erez Shermer Date: Tue, 9 Jul 2024 18:58:55 +0300 Subject: [PATCH 19/20] Tiny fixes --- package.json | 4 ++-- tsconfig.json | 2 +- utils/jumpToPattern.ts | 4 ---- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 008e523..112f2ae 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,8 @@ "keyboardevent-from-electron-accelerator": "*", "obsidian": "^1.1.1", "rollup": "^2.33.0", - "tslib": "^2.0.3", - "typescript": "^4.9.4" + "tslib": "^2.6.3", + "typescript": "^5.5.3" }, "dependencies": { "string.prototype.matchall": "^4.0.11" diff --git a/tsconfig.json b/tsconfig.json index 951df80..502e950 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "inlineSourceMap": true, "inlineSources": true, "module": "ESNext", - "target": "es5", + "target": "ES2020", "allowJs": true, "noImplicitAny": true, "moduleResolution": "node", diff --git a/utils/jumpToPattern.ts b/utils/jumpToPattern.ts index ef7e028..3fe23ec 100644 --- a/utils/jumpToPattern.ts +++ b/utils/jumpToPattern.ts @@ -1,9 +1,5 @@ import { Editor as CodeMirrorEditor } from "codemirror"; import { EditorPosition } from "obsidian"; -import { shim as matchAllShim } from "string.prototype.matchall"; - -// Polyfill for String.prototype.matchAll, in case it's not available (pre-ES2020) -matchAllShim(); /** * Returns the position of the repeat-th instance of a pattern from a given cursor position, in the From 6789094e0b90503eebbce8d05345dedade22896a Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Wed, 17 Jul 2024 10:15:57 -0700 Subject: [PATCH 20/20] docs: update docs now that some more motions are provided by default --- JsSnippets.md | 7 +++---- README.md | 9 ++++++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/JsSnippets.md b/JsSnippets.md index c23ba00..4ccc6eb 100644 --- a/JsSnippets.md +++ b/JsSnippets.md @@ -4,12 +4,11 @@ In this document I will collect some of my and user-contributed ideas for how to If you have interesting snippets, please contribute by opening a pull request! +Note that these examples are included for demonstration purposes, and many of them are now provided by default in this plugin. Their actual implementations can be found under [`motions/`](https://github.com/esm7/obsidian-vimrc-support/blob/master/motions/), which you can also use as reference (either for your own custom motions, or if you wish to submit a PR for a new motion to be provided by this plugin). -## Jump to Next/Prev Markdown Header +## Jump to Next/Previous Markdown Heading -To map `]]` and `[[` to next/prev markdown header, I use the following. - -In a file I call `mdHelpers.js`, put this: +In a file you can call `mdHelpers.js`, put this: ```js // Taken from https://stackoverflow.com/questions/273789/is-there-a-version-of-javascripts-string-indexof-that-allows-for-regular-expr diff --git a/README.md b/README.md index b87cb02..9ada5c1 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,13 @@ Commands that fail don't generate any visible error for now. CodeMirror's Vim mode has some limitations and bugs and not all commands will work like you'd expect. In some cases you can find workarounds by experimenting, and the easiest way to do that is by trying interactively rather than via the Vimrc file. +Finally, this plugin also provides the following motions/mappings by default: + +- `[[` and `]]` to jump to the previous and next Markdown heading. +- `zk` and `zj` to move up and down while skipping folds. +- `gl` and `gL` to jump to the next and previous link. +- `gf` to open the link or file under the cursor (temporarily moving the cursor if necessary—e.g. if it's on the first square bracket of a [[Wikilink]]). + ## Installation In the Obsidian.md settings under "Community plugins", click on "Turn on community plugins", then browse to this plugin. @@ -284,7 +291,7 @@ The `jsfile` should be placed in your vault (alongside, e.g., your markdown file As above, the code running as part of `jsfile` has the arguments `editor: Editor`, `view: MarkdownView` and `selection: EditorSelection`. -Here's an example from my own `.obsidian.vimrc` that maps `]]` and `[[` to jump to the next/previous Markdown header: +Here's an example `.obsidian.vimrc` entry that maps `]]` and `[[` to jump to the next/previous Markdown heading. Note that `]]` and `[[` are already provided by default in this plugin, but this is a good example of how to use `jsfile`: ``` exmap nextHeading jsfile mdHelpers.js {jumpHeading(true)}