refactor: new jumpToPattern function that can be used for motions

This commit is contained in:
Aly Thobani 2024-04-27 10:28:04 -07:00
parent 0053c8f1b2
commit 128f1834e9
4 changed files with 75 additions and 85 deletions

View file

@ -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;
}

View file

@ -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;
};

View file

@ -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,

View file

@ -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 {