From 128f1834e9c629f2cfa59f2704df0cd24eaa7bc3 Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Sat, 27 Apr 2024 10:28:04 -0700 Subject: [PATCH] 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 {