@wwwyo/pi-vim 0.0.0-stage → 0.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Line-local cache for Vim word motion boundaries.
3
+ *
4
+ * Keyed by semantic class + exact line content to avoid stale boundary reuse.
5
+ */
6
+
7
+ import type { WordMotionClass } from "./motions.js";
8
+
9
+ export type WordMotionDirection = "forward" | "backward";
10
+ export type WordMotionTarget = "start" | "end";
11
+
12
+ enum CharType {
13
+ Space = 0,
14
+ Word = 1,
15
+ Other = 2,
16
+ }
17
+
18
+ export interface WordBoundaryData {
19
+ readonly length: number;
20
+ readonly charTypes: Uint8Array;
21
+ readonly runStartByIndex: Int32Array;
22
+ readonly runEndByIndex: Int32Array;
23
+ readonly nextNonSpaceAtOrAfter: Int32Array;
24
+ readonly prevNonSpaceAtOrBefore: Int32Array;
25
+ }
26
+
27
+ function getCharType(
28
+ ch: string | undefined,
29
+ semanticClass: WordMotionClass = "word",
30
+ ): CharType {
31
+ if (!ch || /\s/.test(ch)) return CharType.Space;
32
+ if (semanticClass === "WORD") return CharType.Word;
33
+ if (/\w/.test(ch)) return CharType.Word;
34
+ return CharType.Other;
35
+ }
36
+
37
+ function buildWordBoundaryData(
38
+ line: string,
39
+ semanticClass: WordMotionClass = "word",
40
+ ): WordBoundaryData {
41
+ const len = line.length;
42
+ const charTypes = new Uint8Array(len);
43
+ const runStartByIndex = new Int32Array(len);
44
+ const runEndByIndex = new Int32Array(len);
45
+ const nextNonSpaceAtOrAfter = new Int32Array(len + 1);
46
+ const prevNonSpaceAtOrBefore = new Int32Array(len);
47
+
48
+ nextNonSpaceAtOrAfter.fill(-1);
49
+ prevNonSpaceAtOrBefore.fill(-1);
50
+
51
+ for (let i = 0; i < len; i++) {
52
+ charTypes[i] = getCharType(line[i], semanticClass);
53
+ }
54
+
55
+ for (let runStart = 0; runStart < len; ) {
56
+ const runType = charTypes[runStart] ?? CharType.Space;
57
+ let runEnd = runStart;
58
+ while (runEnd + 1 < len && charTypes[runEnd + 1] === runType) {
59
+ runEnd++;
60
+ }
61
+
62
+ for (let i = runStart; i <= runEnd; i++) {
63
+ runStartByIndex[i] = runStart;
64
+ runEndByIndex[i] = runEnd;
65
+ }
66
+
67
+ runStart = runEnd + 1;
68
+ }
69
+
70
+ let nextNonSpace = -1;
71
+ for (let i = len - 1; i >= 0; i--) {
72
+ if (charTypes[i] !== CharType.Space) {
73
+ nextNonSpace = i;
74
+ }
75
+ nextNonSpaceAtOrAfter[i] = nextNonSpace;
76
+ }
77
+
78
+ let prevNonSpace = -1;
79
+ for (let i = 0; i < len; i++) {
80
+ if (charTypes[i] !== CharType.Space) {
81
+ prevNonSpace = i;
82
+ }
83
+ prevNonSpaceAtOrBefore[i] = prevNonSpace;
84
+ }
85
+
86
+ return {
87
+ length: len,
88
+ charTypes,
89
+ runStartByIndex,
90
+ runEndByIndex,
91
+ nextNonSpaceAtOrAfter,
92
+ prevNonSpaceAtOrBefore,
93
+ };
94
+ }
95
+
96
+ function findTargetInLine(
97
+ data: WordBoundaryData,
98
+ col: number,
99
+ direction: WordMotionDirection,
100
+ target: WordMotionTarget,
101
+ ): number {
102
+ const len = data.length;
103
+ if (len === 0) return 0;
104
+
105
+ let i = Math.max(0, Math.min(col, len));
106
+
107
+ if (direction === "forward") {
108
+ if (i >= len) return len;
109
+
110
+ if (target === "start") {
111
+ if (data.charTypes[i] !== CharType.Space) {
112
+ i = (data.runEndByIndex[i] ?? i) + 1;
113
+ }
114
+
115
+ if (i >= len) return len;
116
+
117
+ if (data.charTypes[i] === CharType.Space) {
118
+ const next = data.nextNonSpaceAtOrAfter[i] ?? -1;
119
+ return next === -1 ? len : next;
120
+ }
121
+
122
+ return i;
123
+ }
124
+
125
+ if (i < len - 1) i++;
126
+
127
+ if (i >= len) return len;
128
+
129
+ if (data.charTypes[i] === CharType.Space) {
130
+ const next = data.nextNonSpaceAtOrAfter[i] ?? -1;
131
+ if (next === -1) return len;
132
+ i = next;
133
+ }
134
+
135
+ return data.runEndByIndex[i] ?? i;
136
+ }
137
+
138
+ if (i >= len) i = len - 1;
139
+ if (i > 0) i--;
140
+
141
+ if (data.charTypes[i] === CharType.Space) {
142
+ const prev = data.prevNonSpaceAtOrBefore[i] ?? -1;
143
+ if (prev !== -1) i = prev;
144
+ }
145
+
146
+ return data.runStartByIndex[i] ?? i;
147
+ }
148
+
149
+ const DEFAULT_MAX_CACHE_ENTRIES = 256;
150
+
151
+ export class WordBoundaryCache {
152
+ private readonly entries = new Map<string, WordBoundaryData>();
153
+ private readonly maxEntries: number;
154
+
155
+ constructor(maxEntries: number = DEFAULT_MAX_CACHE_ENTRIES) {
156
+ this.maxEntries =
157
+ Number.isInteger(maxEntries) && maxEntries > 0
158
+ ? maxEntries
159
+ : DEFAULT_MAX_CACHE_ENTRIES;
160
+ }
161
+
162
+ private makeCacheKey(line: string, semanticClass: WordMotionClass): string {
163
+ return `${semanticClass}\u0000${line}`;
164
+ }
165
+
166
+ get(line: string, semanticClass: WordMotionClass = "word"): WordBoundaryData {
167
+ const key = this.makeCacheKey(line, semanticClass);
168
+ const cached = this.entries.get(key);
169
+ if (cached) return cached;
170
+
171
+ const built = buildWordBoundaryData(line, semanticClass);
172
+
173
+ if (this.entries.size >= this.maxEntries) {
174
+ const oldestKey = this.entries.keys().next().value;
175
+ if (oldestKey !== undefined) {
176
+ this.entries.delete(oldestKey);
177
+ }
178
+ }
179
+
180
+ this.entries.set(key, built);
181
+ return built;
182
+ }
183
+
184
+ tryFindTarget(
185
+ line: string,
186
+ col: number,
187
+ direction: WordMotionDirection,
188
+ target: WordMotionTarget,
189
+ semanticClass: WordMotionClass = "word",
190
+ ): number | null {
191
+ if (!Number.isInteger(col) || col < 0) return null;
192
+
193
+ const boundaries = this.get(line, semanticClass);
194
+ return findTargetInLine(boundaries, col, direction, target);
195
+ }
196
+ }