publish codemirrot helix package flow
Some checks failed
Publish codemirror-helix / publish (push) Failing after 2m16s
Some checks failed
Publish codemirror-helix / publish (push) Failing after 2m16s
This commit is contained in:
180
packages/codemirror-helix/src/motions.ts
Normal file
180
packages/codemirror-helix/src/motions.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import type { EditorState } from "@codemirror/state";
|
||||
|
||||
export type CharClass = "space" | "word" | "punct";
|
||||
|
||||
export function charAt(state: EditorState, i: number): string {
|
||||
return i >= 0 && i < state.doc.length ? state.doc.sliceString(i, i + 1) : "";
|
||||
}
|
||||
|
||||
export function classOf(ch: string): CharClass {
|
||||
if (ch === "" || /\s/.test(ch)) return "space";
|
||||
if (/[\p{L}\p{N}_]/u.test(ch)) return "word";
|
||||
return "punct";
|
||||
}
|
||||
|
||||
// For WORD (big) motions only whitespace separates tokens.
|
||||
function bigClassOf(ch: string): CharClass {
|
||||
return ch === "" || /\s/.test(ch) ? "space" : "word";
|
||||
}
|
||||
|
||||
function classAt(state: EditorState, i: number, big: boolean): CharClass {
|
||||
const ch = charAt(state, i);
|
||||
return big ? bigClassOf(ch) : classOf(ch);
|
||||
}
|
||||
|
||||
/** Helix `w`/`W`: move to the start of the next word. */
|
||||
export function nextWordStart(state: EditorState, pos: number, big: boolean): number {
|
||||
const len = state.doc.length;
|
||||
let i = pos;
|
||||
if (i >= len) return len;
|
||||
if (classAt(state, i, big) === "space") {
|
||||
while (i < len && classAt(state, i, big) === "space") i++;
|
||||
} else {
|
||||
const cls = classAt(state, i, big);
|
||||
while (i < len && classAt(state, i, big) === cls) i++;
|
||||
while (i < len && classAt(state, i, big) === "space") i++;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
/** Helix `b`/`B`: move to the start of the previous word. */
|
||||
export function prevWordStart(state: EditorState, pos: number, big: boolean): number {
|
||||
let i = pos;
|
||||
if (i <= 0) return 0;
|
||||
i--;
|
||||
while (i > 0 && classAt(state, i, big) === "space") i--;
|
||||
const cls = classAt(state, i, big);
|
||||
while (i > 0 && classAt(state, i - 1, big) === cls) i--;
|
||||
return i;
|
||||
}
|
||||
|
||||
/** Helix `e`/`E`: move to the end of the next word. */
|
||||
export function nextWordEnd(state: EditorState, pos: number, big: boolean): number {
|
||||
const len = state.doc.length;
|
||||
let i = pos + 1;
|
||||
while (i < len && classAt(state, i, big) === "space") i++;
|
||||
const cls = classAt(state, i, big);
|
||||
while (i < len && classAt(state, i, big) === cls) i++;
|
||||
return Math.min(i, len);
|
||||
}
|
||||
|
||||
export function firstNonBlank(state: EditorState, line: number): number {
|
||||
const l = state.doc.line(line);
|
||||
return l.from + (l.text.match(/^\s*/)?.[0].length ?? 0);
|
||||
}
|
||||
|
||||
/** f/t/F/T: find `target` from `pos`. Returns null if not found on the line. */
|
||||
export function findCharOnLine(
|
||||
state: EditorState,
|
||||
pos: number,
|
||||
target: string,
|
||||
kind: "f" | "t" | "F" | "T",
|
||||
): number | null {
|
||||
const line = state.doc.lineAt(pos);
|
||||
const forward = kind === "f" || kind === "t";
|
||||
const till = kind === "t" || kind === "T";
|
||||
|
||||
if (forward) {
|
||||
let i = pos + 1;
|
||||
// `t` should skip an adjacent target so repeated presses advance.
|
||||
if (till && charAt(state, i) === target) i++;
|
||||
for (; i <= line.to; i++) {
|
||||
if (charAt(state, i) === target) return till ? i - 1 : i;
|
||||
}
|
||||
} else {
|
||||
let i = pos - 1;
|
||||
if (till && charAt(state, i) === target) i--;
|
||||
for (; i >= line.from; i--) {
|
||||
if (charAt(state, i) === target) return till ? i + 1 : i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const PAIRS: Record<string, { open: string; close: string }> = {
|
||||
"(": { open: "(", close: ")" },
|
||||
")": { open: "(", close: ")" },
|
||||
"[": { open: "[", close: "]" },
|
||||
"]": { open: "[", close: "]" },
|
||||
"{": { open: "{", close: "}" },
|
||||
"}": { open: "{", close: "}" },
|
||||
"<": { open: "<", close: ">" },
|
||||
">": { open: "<", close: ">" },
|
||||
};
|
||||
|
||||
export function pairFor(ch: string): { open: string; close: string } | null {
|
||||
return PAIRS[ch] ?? null;
|
||||
}
|
||||
|
||||
/** Scan forward for the close that matches an open at/after `pos` (nesting-aware). */
|
||||
export function findClose(state: EditorState, from: number, open: string, close: string): number | null {
|
||||
const len = state.doc.length;
|
||||
let depth = 0;
|
||||
for (let i = from; i < len; i++) {
|
||||
const ch = charAt(state, i);
|
||||
if (ch === open) depth++;
|
||||
else if (ch === close) {
|
||||
depth--;
|
||||
if (depth === 0) return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Scan backward for the open that matches a close at/before `pos` (nesting-aware). */
|
||||
export function findOpen(state: EditorState, from: number, open: string, close: string): number | null {
|
||||
let depth = 0;
|
||||
for (let i = from; i >= 0; i--) {
|
||||
const ch = charAt(state, i);
|
||||
if (ch === close) depth++;
|
||||
else if (ch === open) {
|
||||
depth--;
|
||||
if (depth === 0) return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** `mm`: position of the bracket matching the one at/under `pos`, or null. */
|
||||
export function matchingBracket(state: EditorState, pos: number): number | null {
|
||||
for (const probe of [pos, pos - 1]) {
|
||||
const ch = charAt(state, probe);
|
||||
const pair = pairFor(ch);
|
||||
if (!pair) continue;
|
||||
if (ch === pair.open) return findClose2(state, probe, pair.open, pair.close);
|
||||
return findOpen2(state, probe, pair.open, pair.close);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findClose2(state: EditorState, openPos: number, open: string, close: string): number | null {
|
||||
const len = state.doc.length;
|
||||
let depth = 0;
|
||||
for (let i = openPos; i < len; i++) {
|
||||
const ch = charAt(state, i);
|
||||
if (ch === open) depth++;
|
||||
else if (ch === close && --depth === 0) return i;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findOpen2(state: EditorState, closePos: number, open: string, close: string): number | null {
|
||||
let depth = 0;
|
||||
for (let i = closePos; i >= 0; i--) {
|
||||
const ch = charAt(state, i);
|
||||
if (ch === close) depth++;
|
||||
else if (ch === open && --depth === 0) return i;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Paragraph bounds (blank-line delimited) containing `pos`. */
|
||||
export function paragraphAt(state: EditorState, pos: number): { from: number; to: number } {
|
||||
const { doc } = state;
|
||||
let startLine = doc.lineAt(pos).number;
|
||||
let endLine = startLine;
|
||||
const blank = (n: number) => doc.line(n).text.trim() === "";
|
||||
while (startLine > 1 && !blank(startLine - 1)) startLine--;
|
||||
while (endLine < doc.lines && !blank(endLine + 1)) endLine++;
|
||||
return { from: doc.line(startLine).from, to: doc.line(endLine).to };
|
||||
}
|
||||
Reference in New Issue
Block a user