esm7_obsidian-vimrc-support/utils/jumpToPattern.ts
Aly Thobani 0b1168b2ae chore: replace unnecessary CodeMirrorEditor import aliases
- CodeMirror.Editor is globally available as a type (and already used in other files), so lets just
  stick consistently to using that
2026-05-24 17:32:38 -07:00

117 lines
3.8 KiB
TypeScript

import { EditorPosition } from "obsidian";
/**
* Returns the position of the repeat-th instance of a pattern from a given cursor position, in the
* given direction; looping to the other end of the document when reaching one end. Returns the
* original cursor position if no match is found.
*
* Under the hood, to avoid repeated loops of the document: we get all matches at once, order them
* according to `direction` and `cursorPosition`, and use modulo arithmetic to return the
* appropriate match.
*
* @param cm The CodeMirror editor instance.
* @param cursorPosition The current cursor position.
* @param repeat The number of times to repeat the jump (e.g. 1 to jump to the very next match). Is
* modulo-ed for efficiency.
* @param regex The regex pattern to jump to.
* @param filterMatch Optional filter function to run on the regex matches. Return false to ignore
* a given match.
* @param direction The direction to jump in.
*/
export function jumpToPattern({
cm,
cursorPosition,
repeat,
regex,
filterMatch = () => true,
direction,
}: {
cm: CodeMirror.Editor;
cursorPosition: EditorPosition;
repeat: number;
regex: RegExp;
filterMatch?: (match: RegExpExecArray) => boolean;
direction: "next" | "previous";
}): EditorPosition {
const content = cm.getValue();
const cursorIdx = cm.indexFromPos(cursorPosition);
const orderedMatches = getOrderedMatches({ content, regex, cursorIdx, filterMatch, direction });
const effectiveRepeat = (repeat % orderedMatches.length) || orderedMatches.length;
const matchIdx = orderedMatches[effectiveRepeat - 1]?.index;
if (matchIdx === undefined) {
return cursorPosition;
}
const newCursorPosition = cm.posFromIndex(matchIdx);
return newCursorPosition;
}
/**
* Returns an ordered array of all matches of a regex in a string in the given direction from the
* cursor index (looping around to the other end of the document when reaching one end).
*/
function getOrderedMatches({
content,
regex,
cursorIdx,
filterMatch,
direction,
}: {
content: string;
regex: RegExp;
cursorIdx: number;
filterMatch: (match: RegExpExecArray) => boolean;
direction: "next" | "previous";
}): RegExpExecArray[] {
const { previousMatches, currentMatches, nextMatches } = getAndGroupMatches(
content,
regex,
cursorIdx,
filterMatch
);
if (direction === "next") {
return [...nextMatches, ...previousMatches, ...currentMatches];
}
return [
...previousMatches.reverse(),
...nextMatches.reverse(),
...currentMatches.reverse(),
];
}
/**
* Finds all matches of a regex in a string and groups them by their positions relative to the
* cursor.
*/
function getAndGroupMatches(
content: string,
regex: RegExp,
cursorIdx: number,
filterMatch: (match: RegExpExecArray) => boolean
): {
previousMatches: RegExpExecArray[];
currentMatches: RegExpExecArray[];
nextMatches: RegExpExecArray[];
} {
const globalRegex = makeGlobalRegex(regex);
const allMatches = [...content.matchAll(globalRegex)].filter(filterMatch);
const previousMatches = allMatches.filter(
(match) => match.index < cursorIdx && !isWithinMatch(match, cursorIdx)
);
const currentMatches = allMatches.filter((match) => isWithinMatch(match, cursorIdx));
const nextMatches = allMatches.filter((match) => match.index > cursorIdx);
return { previousMatches, currentMatches, nextMatches };
}
function makeGlobalRegex(regex: RegExp): RegExp {
const globalFlags = getGlobalFlags(regex);
return new RegExp(regex.source, globalFlags);
}
function getGlobalFlags(regex: RegExp): string {
const { flags } = regex;
return flags.includes("g") ? flags : `${flags}g`;
}
export function isWithinMatch(match: RegExpExecArray, index: number): boolean {
return match.index <= index && index < match.index + match[0].length;
}