From 19d095883e6b4744060f10c1136fbcdc801fc488 Mon Sep 17 00:00:00 2001 From: Aly Thobani Date: Sat, 11 May 2024 12:23:23 -0700 Subject: [PATCH] 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]; +}