esm7_obsidian-vimrc-support/utils/jumpToPattern.ts
Aly Thobani d2583078ac
Implement and provide default mappings for some Obsidian-specific Vim motions/commands (#222)
* feat: define and expose obsidian-specific vim commands

jumpToNextHeading: g]
jumpToPreviousHeading: g[

* Implement jumpToPreviousLink motion

* Refactoring and implementing jumpToNextLink

* refactor: new jumpToPattern function that can be used for motions

* refactor: renamed file and removed unneeded exports

* fix: return last found index even if fewer than n instances found, instead of undefined

* feat: implement moveUpSkipFold and moveDownSkipFold

* refactor: extract out helper functions for defining obsidian vim actions

* refactor: split vimApi.ts into two files

* refactor: add comment

* refactor: update names, types, etc

* feat: followLinkUnderCursor action

* feat: jumpToLink now jumps to both markdown and wiki links

* refactor: rename fns

* refactor: add docstrings / change var names

* feat: implement looping around

* refactor: cleaner implementation of jumpToPattern

* Change mappings for next/prev heading to [[ and ]]

* Tiny fixes

* docs: update docs now that some more motions are provided by default

---------

Co-authored-by: Erez Shermer <erezshermer@gmail.com>
2024-07-23 20:21:29 +03:00

110 lines
3.2 KiB
TypeScript

import { Editor as CodeMirrorEditor } from "codemirror";
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.
*/
export function jumpToPattern({
cm,
cursorPosition,
repeat,
regex,
direction,
}: {
cm: CodeMirrorEditor;
cursorPosition: EditorPosition;
repeat: number;
regex: RegExp;
direction: "next" | "previous";
}): EditorPosition {
const content = cm.getValue();
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 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,
direction,
}: {
content: string;
regex: RegExp;
cursorIdx: number;
direction: "next" | "previous";
}): RegExpExecArray[] {
const { previousMatches, currentMatches, nextMatches } = getAndGroupMatches(
content,
regex,
cursorIdx
);
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
): {
previousMatches: RegExpExecArray[];
currentMatches: RegExpExecArray[];
nextMatches: RegExpExecArray[];
} {
const globalRegex = makeGlobalRegex(regex);
const allMatches = [...content.matchAll(globalRegex)];
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 {
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`;
}
function isCursorOnMatch(match: RegExpExecArray, cursorIdx: number): boolean {
return match.index <= cursorIdx && cursorIdx < match.index + match[0].length;
}