@vemjs/core 0.1.0
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.
- package/CHANGELOG.md +69 -0
- package/README.md +173 -0
- package/dist/ConfigLoader.d.ts +16 -0
- package/dist/ConfigLoader.d.ts.map +1 -0
- package/dist/ConfigLoader.js +30 -0
- package/dist/ConfigLoader.js.map +1 -0
- package/dist/buffer.d.ts +26 -0
- package/dist/buffer.d.ts.map +1 -0
- package/dist/buffer.js +130 -0
- package/dist/buffer.js.map +1 -0
- package/dist/editor.d.ts +98 -0
- package/dist/editor.d.ts.map +1 -0
- package/dist/editor.js +818 -0
- package/dist/editor.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/motions.d.ts +13 -0
- package/dist/motions.d.ts.map +1 -0
- package/dist/motions.js +200 -0
- package/dist/motions.js.map +1 -0
- package/dist/parser.d.ts +12 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +187 -0
- package/dist/parser.js.map +1 -0
- package/package.json +15 -0
- package/src/ConfigLoader.ts +43 -0
- package/src/buffer.test.ts +64 -0
- package/src/buffer.ts +151 -0
- package/src/editor.test.ts +179 -0
- package/src/editor.ts +928 -0
- package/src/index.ts +32 -0
- package/src/motions.ts +216 -0
- package/src/parser.test.ts +121 -0
- package/src/parser.ts +219 -0
- package/tsconfig.json +13 -0
package/src/editor.ts
ADDED
|
@@ -0,0 +1,928 @@
|
|
|
1
|
+
import type { Position, EditorMode } from './index';
|
|
2
|
+
import { VimBuffer, UndoManager } from './buffer';
|
|
3
|
+
import { getWordForward, getWordBackward, getWordEndForward, getTextObjectRange } from './motions';
|
|
4
|
+
import { parseKeys } from './parser';
|
|
5
|
+
import type { ParsedCommand } from './parser';
|
|
6
|
+
|
|
7
|
+
export interface RegisterContent {
|
|
8
|
+
text: string;
|
|
9
|
+
type: 'char' | 'line' | 'block';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type DiagnosticSeverity = 'error' | 'warning' | 'info' | 'hint';
|
|
13
|
+
|
|
14
|
+
export interface Diagnostic {
|
|
15
|
+
/** Zero-based line number */
|
|
16
|
+
line: number;
|
|
17
|
+
/** Zero-based start character */
|
|
18
|
+
startCharacter: number;
|
|
19
|
+
/** Zero-based end character */
|
|
20
|
+
endCharacter: number;
|
|
21
|
+
severity: DiagnosticSeverity;
|
|
22
|
+
message: string;
|
|
23
|
+
/** Optional: source language server name, e.g. 'tsserver' */
|
|
24
|
+
source?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type VisualType = 'char' | 'line' | 'block';
|
|
28
|
+
|
|
29
|
+
export interface VisualSelection {
|
|
30
|
+
type: VisualType;
|
|
31
|
+
anchor: Position;
|
|
32
|
+
active: Position;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class VemEditorState {
|
|
36
|
+
private mode: EditorMode = 'NORMAL';
|
|
37
|
+
private cursor: Position = { line: 0, character: 0 };
|
|
38
|
+
private desiredCol: number = 0;
|
|
39
|
+
private buffer: VimBuffer;
|
|
40
|
+
private undoManager: UndoManager;
|
|
41
|
+
private register: RegisterContent | null = null;
|
|
42
|
+
private pendingKeys: string[] = [];
|
|
43
|
+
private visualSelection: VisualSelection | null = null;
|
|
44
|
+
private isInsertMutated = false;
|
|
45
|
+
private changeCallbacks: (() => void)[] = [];
|
|
46
|
+
private commandText = '';
|
|
47
|
+
private saveCallbacks: (() => void)[] = [];
|
|
48
|
+
private quitCallbacks: (() => void)[] = [];
|
|
49
|
+
private splitCallbacks: ((direction: 'horizontal' | 'vertical') => void)[] = [];
|
|
50
|
+
private customKeybindings: Map<EditorMode, Map<string, string>> = new Map();
|
|
51
|
+
private didOpenBufferCallbacks: (() => void)[] = [];
|
|
52
|
+
private changeBufferCallbacks: (() => void)[] = [];
|
|
53
|
+
private changeModeCallbacks: ((mode: EditorMode) => void)[] = [];
|
|
54
|
+
private pluginCommandCallbacks: ((commandName: string) => void)[] = [];
|
|
55
|
+
private diagnostics: Diagnostic[] = [];
|
|
56
|
+
private publishDiagnosticsCallbacks: ((diagnostics: Diagnostic[]) => void)[] = [];
|
|
57
|
+
|
|
58
|
+
constructor(initialText?: string) {
|
|
59
|
+
this.buffer = new VimBuffer(initialText);
|
|
60
|
+
this.undoManager = new UndoManager();
|
|
61
|
+
this.buffer.onChange(() => {
|
|
62
|
+
this.triggerChangeBuffer();
|
|
63
|
+
});
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
this.triggerDidOpenBuffer();
|
|
66
|
+
}, 0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// --- Callbacks & Events ---
|
|
70
|
+
public onChange(callback: () => void): void {
|
|
71
|
+
this.changeCallbacks.push(callback);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private triggerChange(): void {
|
|
75
|
+
for (const cb of this.changeCallbacks) {
|
|
76
|
+
cb();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public onSave(callback: () => void): void {
|
|
81
|
+
this.saveCallbacks.push(callback);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
public onQuit(callback: () => void): void {
|
|
85
|
+
this.quitCallbacks.push(callback);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public onSplit(callback: (direction: 'horizontal' | 'vertical') => void): void {
|
|
89
|
+
this.splitCallbacks.push(callback);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private triggerSave(): void {
|
|
93
|
+
for (const cb of this.saveCallbacks) {
|
|
94
|
+
cb();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private triggerQuit(): void {
|
|
99
|
+
for (const cb of this.quitCallbacks) {
|
|
100
|
+
cb();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private triggerSplit(direction: 'horizontal' | 'vertical'): void {
|
|
105
|
+
for (const cb of this.splitCallbacks) {
|
|
106
|
+
cb(direction);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public registerKeybinding(mode: EditorMode, keys: string, commandName: string): void {
|
|
111
|
+
if (!this.customKeybindings.has(mode)) {
|
|
112
|
+
this.customKeybindings.set(mode, new Map());
|
|
113
|
+
}
|
|
114
|
+
this.customKeybindings.get(mode)!.set(keys, commandName);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public onDidOpenBuffer(callback: () => void): void {
|
|
118
|
+
this.didOpenBufferCallbacks.push(callback);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public onDidChangeBuffer(callback: () => void): void {
|
|
122
|
+
this.changeBufferCallbacks.push(callback);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
public onDidChangeMode(callback: (mode: EditorMode) => void): void {
|
|
126
|
+
this.changeModeCallbacks.push(callback);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public onExecutePluginCommand(callback: (commandName: string) => void): void {
|
|
130
|
+
this.pluginCommandCallbacks.push(callback);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public onPublishDiagnostics(callback: (diagnostics: Diagnostic[]) => void): void {
|
|
134
|
+
this.publishDiagnosticsCallbacks.push(callback);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
public setDiagnostics(diagnostics: Diagnostic[]): void {
|
|
138
|
+
this.diagnostics = diagnostics;
|
|
139
|
+
for (const cb of this.publishDiagnosticsCallbacks) {
|
|
140
|
+
cb(this.diagnostics);
|
|
141
|
+
}
|
|
142
|
+
// Trigger a render-change so renderers can repaint highlight overlays
|
|
143
|
+
for (const cb of this.changeCallbacks) {
|
|
144
|
+
cb();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public getDiagnostics(): Diagnostic[] {
|
|
149
|
+
return this.diagnostics;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private triggerDidOpenBuffer(): void {
|
|
153
|
+
for (const cb of this.didOpenBufferCallbacks) {
|
|
154
|
+
cb();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private triggerChangeBuffer(): void {
|
|
159
|
+
for (const cb of this.changeBufferCallbacks) {
|
|
160
|
+
cb();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private triggerChangeMode(mode: EditorMode): void {
|
|
165
|
+
for (const cb of this.changeModeCallbacks) {
|
|
166
|
+
cb(mode);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private executePluginCommand(commandName: string): void {
|
|
171
|
+
for (const cb of this.pluginCommandCallbacks) {
|
|
172
|
+
cb(commandName);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// --- Getters & Setters ---
|
|
177
|
+
public getMode(): EditorMode {
|
|
178
|
+
return this.mode;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
public getText(): string {
|
|
182
|
+
return this.buffer.getText();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
public setMode(mode: EditorMode): void {
|
|
186
|
+
if (this.mode === mode) return;
|
|
187
|
+
|
|
188
|
+
// Handle exiting insert mode
|
|
189
|
+
if (this.mode === 'INSERT') {
|
|
190
|
+
this.isInsertMutated = false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
this.mode = mode;
|
|
194
|
+
|
|
195
|
+
// Initialize visual selection if entering visual mode
|
|
196
|
+
if (mode === 'VISUAL') {
|
|
197
|
+
this.visualSelection = {
|
|
198
|
+
type: 'char',
|
|
199
|
+
anchor: { ...this.cursor },
|
|
200
|
+
active: { ...this.cursor },
|
|
201
|
+
};
|
|
202
|
+
} else {
|
|
203
|
+
this.visualSelection = null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
this.triggerChangeMode(mode);
|
|
207
|
+
this.triggerChange();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
public getCursor(): Position {
|
|
211
|
+
return { ...this.cursor };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
public getBuffer(): VimBuffer {
|
|
215
|
+
return this.buffer;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
public getPendingKeys(): string[] {
|
|
219
|
+
return [...this.pendingKeys];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
public getVisualSelection(): VisualSelection | null {
|
|
223
|
+
return this.visualSelection;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
public getRegister(): RegisterContent | null {
|
|
227
|
+
return this.register;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
public getCommandText(): string {
|
|
231
|
+
return this.commandText;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// --- Key Input Entry Point ---
|
|
235
|
+
public handleKey(key: string): void {
|
|
236
|
+
if (this.mode === 'COMMAND') {
|
|
237
|
+
if (key === 'Escape') {
|
|
238
|
+
this.setMode('NORMAL');
|
|
239
|
+
this.commandText = '';
|
|
240
|
+
this.triggerChange();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (key === 'Enter') {
|
|
244
|
+
this.executeCommandLineText(this.commandText);
|
|
245
|
+
this.setMode('NORMAL');
|
|
246
|
+
this.commandText = '';
|
|
247
|
+
this.triggerChange();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (key === 'Backspace') {
|
|
251
|
+
this.commandText = this.commandText.substring(0, this.commandText.length - 1);
|
|
252
|
+
this.triggerChange();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (key.length === 1) {
|
|
256
|
+
this.commandText += key;
|
|
257
|
+
this.triggerChange();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Check custom keybindings
|
|
264
|
+
const modeBindings = this.customKeybindings.get(this.mode);
|
|
265
|
+
if (modeBindings) {
|
|
266
|
+
const currentSequence = [...this.pendingKeys, key].join('');
|
|
267
|
+
let hasExactMatch = false;
|
|
268
|
+
let hasPartialMatch = false;
|
|
269
|
+
let matchedCommand = '';
|
|
270
|
+
|
|
271
|
+
for (const [keys, cmd] of modeBindings.entries()) {
|
|
272
|
+
if (keys === currentSequence) {
|
|
273
|
+
hasExactMatch = true;
|
|
274
|
+
matchedCommand = cmd;
|
|
275
|
+
} else if (keys.startsWith(currentSequence)) {
|
|
276
|
+
hasPartialMatch = true;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (hasExactMatch && !hasPartialMatch) {
|
|
281
|
+
this.pendingKeys = [];
|
|
282
|
+
this.executePluginCommand(matchedCommand);
|
|
283
|
+
this.triggerChange();
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (hasPartialMatch) {
|
|
288
|
+
this.pendingKeys.push(key);
|
|
289
|
+
this.triggerChange();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (this.pendingKeys.length > 0) {
|
|
294
|
+
const keysToReplay = [...this.pendingKeys, key];
|
|
295
|
+
this.pendingKeys = [];
|
|
296
|
+
for (const k of keysToReplay) {
|
|
297
|
+
this.handleKeyStandard(k);
|
|
298
|
+
}
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
this.handleKeyStandard(key);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private handleKeyStandard(key: string): void {
|
|
307
|
+
if (this.mode === 'INSERT') {
|
|
308
|
+
if (key === 'Escape') {
|
|
309
|
+
this.setMode('NORMAL');
|
|
310
|
+
// Move cursor back one character in Normal mode
|
|
311
|
+
this.cursor.character = Math.max(0, this.cursor.character - 1);
|
|
312
|
+
this.desiredCol = this.cursor.character;
|
|
313
|
+
this.triggerChange();
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (key === 'Backspace') {
|
|
317
|
+
this.handleBackspaceInInsert();
|
|
318
|
+
this.triggerChange();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (key === 'Enter') {
|
|
322
|
+
this.handleEnterInInsert();
|
|
323
|
+
this.triggerChange();
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (key.length === 1) {
|
|
327
|
+
this.handleCharInputInInsert(key);
|
|
328
|
+
this.triggerChange();
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Normal or Visual Mode
|
|
335
|
+
if (key === 'Escape') {
|
|
336
|
+
this.pendingKeys = [];
|
|
337
|
+
if (this.mode === 'VISUAL') {
|
|
338
|
+
this.setMode('NORMAL');
|
|
339
|
+
}
|
|
340
|
+
this.triggerChange();
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
this.pendingKeys.push(key);
|
|
345
|
+
const parsed = parseKeys(this.pendingKeys, this.mode);
|
|
346
|
+
|
|
347
|
+
if (!parsed.isValid) {
|
|
348
|
+
this.pendingKeys = [];
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (parsed.isComplete) {
|
|
353
|
+
this.pendingKeys = [];
|
|
354
|
+
this.executeCommand(parsed);
|
|
355
|
+
this.triggerChange();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// --- Insertion Helpers ---
|
|
360
|
+
private handleCharInputInInsert(char: string): void {
|
|
361
|
+
if (!this.isInsertMutated) {
|
|
362
|
+
this.saveStateForUndo();
|
|
363
|
+
this.isInsertMutated = true;
|
|
364
|
+
}
|
|
365
|
+
const line = this.buffer.getLine(this.cursor.line);
|
|
366
|
+
const before = line.substring(0, this.cursor.character);
|
|
367
|
+
const after = line.substring(this.cursor.character);
|
|
368
|
+
this.buffer.setLine(this.cursor.line, before + char + after);
|
|
369
|
+
this.cursor.character++;
|
|
370
|
+
this.desiredCol = this.cursor.character;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private handleBackspaceInInsert(): void {
|
|
374
|
+
if (!this.isInsertMutated) {
|
|
375
|
+
this.saveStateForUndo();
|
|
376
|
+
this.isInsertMutated = true;
|
|
377
|
+
}
|
|
378
|
+
if (this.cursor.character > 0) {
|
|
379
|
+
const line = this.buffer.getLine(this.cursor.line);
|
|
380
|
+
const before = line.substring(0, this.cursor.character - 1);
|
|
381
|
+
const after = line.substring(this.cursor.character);
|
|
382
|
+
this.buffer.setLine(this.cursor.line, before + after);
|
|
383
|
+
this.cursor.character--;
|
|
384
|
+
this.desiredCol = this.cursor.character;
|
|
385
|
+
} else if (this.cursor.line > 0) {
|
|
386
|
+
const prevLine = this.buffer.getLine(this.cursor.line - 1);
|
|
387
|
+
const currLine = this.buffer.getLine(this.cursor.line);
|
|
388
|
+
this.buffer.setLine(this.cursor.line - 1, prevLine + currLine);
|
|
389
|
+
this.buffer.deleteLines(this.cursor.line, this.cursor.line);
|
|
390
|
+
this.cursor.line--;
|
|
391
|
+
this.cursor.character = prevLine.length;
|
|
392
|
+
this.desiredCol = this.cursor.character;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private handleEnterInInsert(): void {
|
|
397
|
+
if (!this.isInsertMutated) {
|
|
398
|
+
this.saveStateForUndo();
|
|
399
|
+
this.isInsertMutated = true;
|
|
400
|
+
}
|
|
401
|
+
const line = this.buffer.getLine(this.cursor.line);
|
|
402
|
+
const before = line.substring(0, this.cursor.character);
|
|
403
|
+
const after = line.substring(this.cursor.character);
|
|
404
|
+
|
|
405
|
+
this.buffer.setLine(this.cursor.line, before);
|
|
406
|
+
this.buffer.insertLine(this.cursor.line + 1, after);
|
|
407
|
+
this.cursor.line++;
|
|
408
|
+
this.cursor.character = 0;
|
|
409
|
+
this.desiredCol = 0;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// --- Command Execution ---
|
|
413
|
+
private executeCommand(cmd: ParsedCommand): void {
|
|
414
|
+
if (this.mode === 'VISUAL') {
|
|
415
|
+
this.executeVisualCommand(cmd);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// NORMAL mode commands
|
|
420
|
+
if (cmd.operator) {
|
|
421
|
+
this.executeOperatorCommand(cmd);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (cmd.motion) {
|
|
426
|
+
this.moveCursorByMotion(cmd.motion, cmd.count);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (cmd.command) {
|
|
431
|
+
switch (cmd.command) {
|
|
432
|
+
case 'i':
|
|
433
|
+
this.setMode('INSERT');
|
|
434
|
+
break;
|
|
435
|
+
case 'I':
|
|
436
|
+
this.moveToFirstNonWhitespace();
|
|
437
|
+
this.setMode('INSERT');
|
|
438
|
+
break;
|
|
439
|
+
case 'a':
|
|
440
|
+
this.moveCursorRightForInsert();
|
|
441
|
+
this.setMode('INSERT');
|
|
442
|
+
break;
|
|
443
|
+
case 'A':
|
|
444
|
+
this.moveToEndOfLine();
|
|
445
|
+
this.setMode('INSERT');
|
|
446
|
+
break;
|
|
447
|
+
case 'o':
|
|
448
|
+
this.saveStateForUndo();
|
|
449
|
+
this.buffer.insertLine(this.cursor.line + 1, '');
|
|
450
|
+
this.cursor.line++;
|
|
451
|
+
this.cursor.character = 0;
|
|
452
|
+
this.desiredCol = 0;
|
|
453
|
+
this.setMode('INSERT');
|
|
454
|
+
break;
|
|
455
|
+
case 'O':
|
|
456
|
+
this.saveStateForUndo();
|
|
457
|
+
this.buffer.insertLine(this.cursor.line, '');
|
|
458
|
+
this.cursor.character = 0;
|
|
459
|
+
this.desiredCol = 0;
|
|
460
|
+
this.setMode('INSERT');
|
|
461
|
+
break;
|
|
462
|
+
case 'x':
|
|
463
|
+
this.saveStateForUndo();
|
|
464
|
+
this.deleteCharUnderCursor(cmd.count);
|
|
465
|
+
break;
|
|
466
|
+
case 'u':
|
|
467
|
+
this.undo();
|
|
468
|
+
break;
|
|
469
|
+
case '<C-r>':
|
|
470
|
+
this.redo();
|
|
471
|
+
break;
|
|
472
|
+
case 'v':
|
|
473
|
+
this.setMode('VISUAL');
|
|
474
|
+
if (this.visualSelection) this.visualSelection.type = 'char';
|
|
475
|
+
break;
|
|
476
|
+
case 'V':
|
|
477
|
+
this.setMode('VISUAL');
|
|
478
|
+
if (this.visualSelection) this.visualSelection.type = 'line';
|
|
479
|
+
break;
|
|
480
|
+
case '<C-v>':
|
|
481
|
+
this.setMode('VISUAL');
|
|
482
|
+
if (this.visualSelection) this.visualSelection.type = 'block';
|
|
483
|
+
break;
|
|
484
|
+
case 'p':
|
|
485
|
+
this.saveStateForUndo();
|
|
486
|
+
this.paste(false);
|
|
487
|
+
break;
|
|
488
|
+
case 'P':
|
|
489
|
+
this.saveStateForUndo();
|
|
490
|
+
this.paste(true);
|
|
491
|
+
break;
|
|
492
|
+
case ':':
|
|
493
|
+
this.setMode('COMMAND');
|
|
494
|
+
this.commandText = '';
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private executeCommandLineText(cmd: string): void {
|
|
501
|
+
if (cmd === 'w') {
|
|
502
|
+
this.triggerSave();
|
|
503
|
+
} else if (cmd === 'q') {
|
|
504
|
+
this.triggerQuit();
|
|
505
|
+
} else if (cmd === 'vsp') {
|
|
506
|
+
this.triggerSplit('vertical');
|
|
507
|
+
} else if (cmd === 'sp') {
|
|
508
|
+
this.triggerSplit('horizontal');
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
private executeVisualCommand(cmd: ParsedCommand): void {
|
|
513
|
+
if (cmd.motion) {
|
|
514
|
+
this.moveCursorByMotion(cmd.motion, cmd.count);
|
|
515
|
+
if (this.visualSelection) {
|
|
516
|
+
this.visualSelection.active = { ...this.cursor };
|
|
517
|
+
}
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const op = cmd.command || cmd.operator;
|
|
522
|
+
if (op === 'd' || op === 'x' || op === 'c' || op === 'y') {
|
|
523
|
+
this.saveStateForUndo();
|
|
524
|
+
this.operateOnVisualSelection(op);
|
|
525
|
+
if (op === 'c') {
|
|
526
|
+
this.setMode('INSERT');
|
|
527
|
+
} else {
|
|
528
|
+
this.setMode('NORMAL');
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// --- Motions Execution ---
|
|
534
|
+
private moveCursorByMotion(motion: string, count: number): void {
|
|
535
|
+
for (let i = 0; i < count; i++) {
|
|
536
|
+
switch (motion) {
|
|
537
|
+
case 'h':
|
|
538
|
+
this.cursor.character = Math.max(0, this.cursor.character - 1);
|
|
539
|
+
this.desiredCol = this.cursor.character;
|
|
540
|
+
break;
|
|
541
|
+
case 'l': {
|
|
542
|
+
const lineLen = this.buffer.getLine(this.cursor.line).length;
|
|
543
|
+
const maxChar = this.mode === 'INSERT' ? lineLen : Math.max(0, lineLen - 1);
|
|
544
|
+
this.cursor.character = Math.min(maxChar, this.cursor.character + 1);
|
|
545
|
+
this.desiredCol = this.cursor.character;
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
case 'j':
|
|
549
|
+
if (this.cursor.line < this.buffer.getLineCount() - 1) {
|
|
550
|
+
this.cursor.line++;
|
|
551
|
+
const lineLen = this.buffer.getLine(this.cursor.line).length;
|
|
552
|
+
const maxChar = this.mode === 'INSERT' ? lineLen : Math.max(0, lineLen - 1);
|
|
553
|
+
this.cursor.character = Math.min(maxChar, this.desiredCol);
|
|
554
|
+
}
|
|
555
|
+
break;
|
|
556
|
+
case 'k':
|
|
557
|
+
if (this.cursor.line > 0) {
|
|
558
|
+
this.cursor.line--;
|
|
559
|
+
const lineLen = this.buffer.getLine(this.cursor.line).length;
|
|
560
|
+
const maxChar = this.mode === 'INSERT' ? lineLen : Math.max(0, lineLen - 1);
|
|
561
|
+
this.cursor.character = Math.min(maxChar, this.desiredCol);
|
|
562
|
+
}
|
|
563
|
+
break;
|
|
564
|
+
case 'w':
|
|
565
|
+
this.cursor = getWordForward(this.buffer, this.cursor);
|
|
566
|
+
this.desiredCol = this.cursor.character;
|
|
567
|
+
break;
|
|
568
|
+
case 'b':
|
|
569
|
+
this.cursor = getWordBackward(this.buffer, this.cursor);
|
|
570
|
+
this.desiredCol = this.cursor.character;
|
|
571
|
+
break;
|
|
572
|
+
case 'e':
|
|
573
|
+
this.cursor = getWordEndForward(this.buffer, this.cursor);
|
|
574
|
+
this.desiredCol = this.cursor.character;
|
|
575
|
+
break;
|
|
576
|
+
case '0':
|
|
577
|
+
this.cursor.character = 0;
|
|
578
|
+
this.desiredCol = 0;
|
|
579
|
+
break;
|
|
580
|
+
case '$': {
|
|
581
|
+
const lineLen = this.buffer.getLine(this.cursor.line).length;
|
|
582
|
+
this.cursor.character = Math.max(0, lineLen - 1);
|
|
583
|
+
this.desiredCol = Infinity;
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
case 'gg':
|
|
587
|
+
this.cursor.line = 0;
|
|
588
|
+
this.cursor.character = 0;
|
|
589
|
+
this.desiredCol = 0;
|
|
590
|
+
break;
|
|
591
|
+
case 'G':
|
|
592
|
+
this.cursor.line = this.buffer.getLineCount() - 1;
|
|
593
|
+
this.cursor.character = 0;
|
|
594
|
+
this.desiredCol = 0;
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// --- Operator Command Execution ---
|
|
601
|
+
private executeOperatorCommand(cmd: ParsedCommand): void {
|
|
602
|
+
const op = cmd.operator!;
|
|
603
|
+
const count = cmd.count;
|
|
604
|
+
|
|
605
|
+
// Double operator e.g. dd, cc, yy
|
|
606
|
+
if (cmd.command === op + op) {
|
|
607
|
+
this.saveStateForUndo();
|
|
608
|
+
const startLine = this.cursor.line;
|
|
609
|
+
const endLine = Math.min(this.buffer.getLineCount() - 1, this.cursor.line + count - 1);
|
|
610
|
+
|
|
611
|
+
// Extract text for yank
|
|
612
|
+
const yankedLines: string[] = [];
|
|
613
|
+
for (let l = startLine; l <= endLine; l++) {
|
|
614
|
+
yankedLines.push(this.buffer.getLine(l));
|
|
615
|
+
}
|
|
616
|
+
this.register = {
|
|
617
|
+
text: yankedLines.join('\n') + '\n',
|
|
618
|
+
type: 'line',
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
if (op === 'd' || op === 'c') {
|
|
622
|
+
this.buffer.deleteLines(startLine, endLine);
|
|
623
|
+
this.cursor.line = Math.min(this.cursor.line, this.buffer.getLineCount() - 1);
|
|
624
|
+
this.cursor.character = 0;
|
|
625
|
+
this.desiredCol = 0;
|
|
626
|
+
if (op === 'c') {
|
|
627
|
+
this.buffer.insertLine(this.cursor.line, '');
|
|
628
|
+
this.setMode('INSERT');
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Motions or Text Objects
|
|
635
|
+
let range: { start: Position; end: Position } | null = null;
|
|
636
|
+
let isLineWise = false;
|
|
637
|
+
|
|
638
|
+
if (cmd.textObject) {
|
|
639
|
+
range = getTextObjectRange(this.buffer, this.cursor, cmd.textObject);
|
|
640
|
+
} else if (cmd.motion) {
|
|
641
|
+
const startPos = { ...this.cursor };
|
|
642
|
+
this.moveCursorByMotion(cmd.motion, count);
|
|
643
|
+
const endPos = { ...this.cursor };
|
|
644
|
+
this.cursor = { ...startPos }; // Restore cursor before operation
|
|
645
|
+
|
|
646
|
+
range = { start: startPos, end: endPos };
|
|
647
|
+
|
|
648
|
+
// Line-wise motion check
|
|
649
|
+
if (cmd.motion === 'j' || cmd.motion === 'k' || cmd.motion === 'gg' || cmd.motion === 'G') {
|
|
650
|
+
isLineWise = true;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (!range) return;
|
|
655
|
+
|
|
656
|
+
this.saveStateForUndo();
|
|
657
|
+
|
|
658
|
+
// Ensure start is before end
|
|
659
|
+
let s = { ...range.start };
|
|
660
|
+
let e = { ...range.end };
|
|
661
|
+
if (s.line > e.line || (s.line === e.line && s.character > e.character)) {
|
|
662
|
+
const temp = s;
|
|
663
|
+
s = e;
|
|
664
|
+
e = temp;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (isLineWise) {
|
|
668
|
+
const yankedLines: string[] = [];
|
|
669
|
+
for (let l = s.line; l <= e.line; l++) {
|
|
670
|
+
yankedLines.push(this.buffer.getLine(l));
|
|
671
|
+
}
|
|
672
|
+
this.register = {
|
|
673
|
+
text: yankedLines.join('\n') + '\n',
|
|
674
|
+
type: 'line',
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
if (op === 'd' || op === 'c') {
|
|
678
|
+
this.buffer.deleteLines(s.line, e.line);
|
|
679
|
+
this.cursor.line = Math.min(s.line, this.buffer.getLineCount() - 1);
|
|
680
|
+
this.cursor.character = 0;
|
|
681
|
+
this.desiredCol = 0;
|
|
682
|
+
if (op === 'c') {
|
|
683
|
+
this.buffer.insertLine(this.cursor.line, '');
|
|
684
|
+
this.setMode('INSERT');
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
} else {
|
|
688
|
+
// Character-wise operation
|
|
689
|
+
// Inclusive vs Exclusive: motions like $ or text objects are inclusive. Others are exclusive.
|
|
690
|
+
// E.g. 'dw' deletes from start to start of next word (exclusive).
|
|
691
|
+
// 'd$' deletes to end of line (inclusive).
|
|
692
|
+
let isInclusive = cmd.textObject !== undefined || cmd.motion === '$';
|
|
693
|
+
|
|
694
|
+
const startLineText = this.buffer.getLine(s.line);
|
|
695
|
+
const endLineText = this.buffer.getLine(e.line);
|
|
696
|
+
|
|
697
|
+
let yankText = '';
|
|
698
|
+
if (s.line === e.line) {
|
|
699
|
+
yankText = startLineText.substring(s.character, e.character + (isInclusive ? 1 : 0));
|
|
700
|
+
} else {
|
|
701
|
+
yankText = startLineText.substring(s.character) + '\n';
|
|
702
|
+
for (let l = s.line + 1; l < e.line; l++) {
|
|
703
|
+
yankText += this.buffer.getLine(l) + '\n';
|
|
704
|
+
}
|
|
705
|
+
yankText += endLineText.substring(0, e.character + (isInclusive ? 1 : 0));
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
this.register = {
|
|
709
|
+
text: yankText,
|
|
710
|
+
type: 'char',
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
if (op === 'd' || op === 'c') {
|
|
714
|
+
const deleteEnd = { ...e };
|
|
715
|
+
if (isInclusive) {
|
|
716
|
+
deleteEnd.character++;
|
|
717
|
+
}
|
|
718
|
+
this.buffer.deleteRange(s, deleteEnd);
|
|
719
|
+
this.cursor = { ...s };
|
|
720
|
+
this.desiredCol = this.cursor.character;
|
|
721
|
+
if (op === 'c') {
|
|
722
|
+
this.setMode('INSERT');
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// --- Visual Mode Operations ---
|
|
729
|
+
private operateOnVisualSelection(op: string): void {
|
|
730
|
+
if (!this.visualSelection) return;
|
|
731
|
+
|
|
732
|
+
const { type, anchor, active } = this.visualSelection;
|
|
733
|
+
let s = { ...anchor };
|
|
734
|
+
let e = { ...active };
|
|
735
|
+
|
|
736
|
+
// Standard ordering
|
|
737
|
+
if (s.line > e.line || (s.line === e.line && s.character > e.character)) {
|
|
738
|
+
const temp = s;
|
|
739
|
+
s = e;
|
|
740
|
+
e = temp;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (type === 'line') {
|
|
744
|
+
const startLine = Math.min(s.line, e.line);
|
|
745
|
+
const endLine = Math.max(s.line, e.line);
|
|
746
|
+
|
|
747
|
+
const yankedLines: string[] = [];
|
|
748
|
+
for (let l = startLine; l <= endLine; l++) {
|
|
749
|
+
yankedLines.push(this.buffer.getLine(l));
|
|
750
|
+
}
|
|
751
|
+
this.register = {
|
|
752
|
+
text: yankedLines.join('\n') + '\n',
|
|
753
|
+
type: 'line',
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
if (op === 'd' || op === 'c') {
|
|
757
|
+
this.buffer.deleteLines(startLine, endLine);
|
|
758
|
+
this.cursor.line = Math.min(startLine, this.buffer.getLineCount() - 1);
|
|
759
|
+
this.cursor.character = 0;
|
|
760
|
+
this.desiredCol = 0;
|
|
761
|
+
if (op === 'c') {
|
|
762
|
+
this.buffer.insertLine(this.cursor.line, '');
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
} else if (type === 'char') {
|
|
766
|
+
const startLineText = this.buffer.getLine(s.line);
|
|
767
|
+
const endLineText = this.buffer.getLine(e.line);
|
|
768
|
+
|
|
769
|
+
let yankText = '';
|
|
770
|
+
if (s.line === e.line) {
|
|
771
|
+
yankText = startLineText.substring(s.character, e.character + 1);
|
|
772
|
+
} else {
|
|
773
|
+
yankText = startLineText.substring(s.character) + '\n';
|
|
774
|
+
for (let l = s.line + 1; l < e.line; l++) {
|
|
775
|
+
yankText += this.buffer.getLine(l) + '\n';
|
|
776
|
+
}
|
|
777
|
+
yankText += endLineText.substring(0, e.character + 1);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
this.register = {
|
|
781
|
+
text: yankText,
|
|
782
|
+
type: 'char',
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
if (op === 'd' || op === 'c') {
|
|
786
|
+
const deleteEnd = { ...e };
|
|
787
|
+
deleteEnd.character++; // Inclusive delete
|
|
788
|
+
this.buffer.deleteRange(s, deleteEnd);
|
|
789
|
+
this.cursor = { ...s };
|
|
790
|
+
this.desiredCol = this.cursor.character;
|
|
791
|
+
}
|
|
792
|
+
} else if (type === 'block') {
|
|
793
|
+
// Block-wise selection yanks/deletes character rectangles
|
|
794
|
+
const minCol = Math.min(anchor.character, active.character);
|
|
795
|
+
const maxCol = Math.max(anchor.character, active.character);
|
|
796
|
+
const minLine = Math.min(anchor.line, active.line);
|
|
797
|
+
const maxLine = Math.max(anchor.line, active.line);
|
|
798
|
+
|
|
799
|
+
const yankedBlocks: string[] = [];
|
|
800
|
+
for (let l = minLine; l <= maxLine; l++) {
|
|
801
|
+
const lineText = this.buffer.getLine(l);
|
|
802
|
+
const chunk = lineText.substring(minCol, maxCol + 1);
|
|
803
|
+
yankedBlocks.push(chunk);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
this.register = {
|
|
807
|
+
text: yankedBlocks.join('\n'),
|
|
808
|
+
type: 'block',
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
if (op === 'd' || op === 'c') {
|
|
812
|
+
for (let l = minLine; l <= maxLine; l++) {
|
|
813
|
+
const lineText = this.buffer.getLine(l);
|
|
814
|
+
const before = lineText.substring(0, minCol);
|
|
815
|
+
const after = lineText.substring(maxCol + 1);
|
|
816
|
+
this.buffer.setLine(l, before + after);
|
|
817
|
+
}
|
|
818
|
+
this.cursor = { line: minLine, character: minCol };
|
|
819
|
+
this.desiredCol = minCol;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// --- Editing Actions ---
|
|
825
|
+
private moveToFirstNonWhitespace(): void {
|
|
826
|
+
const line = this.buffer.getLine(this.cursor.line);
|
|
827
|
+
const match = /^[ \t]*/.exec(line);
|
|
828
|
+
const index = match ? match[0].length : 0;
|
|
829
|
+
this.cursor.character = Math.min(index, Math.max(0, line.length - 1));
|
|
830
|
+
this.desiredCol = this.cursor.character;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
private moveCursorRightForInsert(): void {
|
|
834
|
+
const lineLen = this.buffer.getLine(this.cursor.line).length;
|
|
835
|
+
this.cursor.character = Math.min(lineLen, this.cursor.character + 1);
|
|
836
|
+
this.desiredCol = this.cursor.character;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
private moveToEndOfLine(): void {
|
|
840
|
+
const lineLen = this.buffer.getLine(this.cursor.line).length;
|
|
841
|
+
this.cursor.character = lineLen;
|
|
842
|
+
this.desiredCol = Infinity;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
private deleteCharUnderCursor(count: number): void {
|
|
846
|
+
const line = this.buffer.getLine(this.cursor.line);
|
|
847
|
+
if (line.length === 0) return;
|
|
848
|
+
|
|
849
|
+
const deleteCount = Math.min(count, line.length - this.cursor.character);
|
|
850
|
+
const deletedText = line.substring(this.cursor.character, this.cursor.character + deleteCount);
|
|
851
|
+
|
|
852
|
+
this.register = {
|
|
853
|
+
text: deletedText,
|
|
854
|
+
type: 'char',
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
const before = line.substring(0, this.cursor.character);
|
|
858
|
+
const after = line.substring(this.cursor.character + deleteCount);
|
|
859
|
+
this.buffer.setLine(this.cursor.line, before + after);
|
|
860
|
+
|
|
861
|
+
// Adjust cursor if it's past the end of the line in Normal mode
|
|
862
|
+
const newLineLen = this.buffer.getLine(this.cursor.line).length;
|
|
863
|
+
this.cursor.character = Math.min(this.cursor.character, Math.max(0, newLineLen - 1));
|
|
864
|
+
this.desiredCol = this.cursor.character;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
private paste(before: boolean): void {
|
|
868
|
+
if (!this.register) return;
|
|
869
|
+
|
|
870
|
+
const { text, type } = this.register;
|
|
871
|
+
|
|
872
|
+
if (type === 'line') {
|
|
873
|
+
const targetLine = before ? this.cursor.line : this.cursor.line + 1;
|
|
874
|
+
const pasteLines = text.split('\n');
|
|
875
|
+
// If trailing newline, split leaves empty string at end, remove it
|
|
876
|
+
if (pasteLines[pasteLines.length - 1] === '') {
|
|
877
|
+
pasteLines.pop();
|
|
878
|
+
}
|
|
879
|
+
for (let i = 0; i < pasteLines.length; i++) {
|
|
880
|
+
this.buffer.insertLine(targetLine + i, pasteLines[i]);
|
|
881
|
+
}
|
|
882
|
+
this.cursor.line = targetLine;
|
|
883
|
+
this.cursor.character = 0;
|
|
884
|
+
this.desiredCol = 0;
|
|
885
|
+
} else {
|
|
886
|
+
// Character-wise or block-wise paste (block-wise simple fallback to char paste)
|
|
887
|
+
const line = this.buffer.getLine(this.cursor.line);
|
|
888
|
+
const insertIdx = before
|
|
889
|
+
? this.cursor.character
|
|
890
|
+
: Math.min(line.length, this.cursor.character + 1);
|
|
891
|
+
|
|
892
|
+
const endPos = this.buffer.insertText({ line: this.cursor.line, character: insertIdx }, text);
|
|
893
|
+
this.cursor = endPos;
|
|
894
|
+
// In Normal mode, leave cursor on last pasted character
|
|
895
|
+
this.cursor.character = Math.max(0, this.cursor.character - 1);
|
|
896
|
+
this.desiredCol = this.cursor.character;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// --- Undo & Redo ---
|
|
901
|
+
private saveStateForUndo(): void {
|
|
902
|
+
this.undoManager.push(this.buffer.getLines());
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
public undo(): void {
|
|
906
|
+
const prevState = this.undoManager.undo(this.buffer.getLines());
|
|
907
|
+
if (prevState) {
|
|
908
|
+
this.buffer.setLines(prevState);
|
|
909
|
+
// Clamp cursor
|
|
910
|
+
this.cursor.line = Math.min(this.cursor.line, this.buffer.getLineCount() - 1);
|
|
911
|
+
const lineLen = this.buffer.getLine(this.cursor.line).length;
|
|
912
|
+
this.cursor.character = Math.min(this.cursor.character, Math.max(0, lineLen - 1));
|
|
913
|
+
this.desiredCol = this.cursor.character;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
public redo(): void {
|
|
918
|
+
const nextState = this.undoManager.redo(this.buffer.getLines());
|
|
919
|
+
if (nextState) {
|
|
920
|
+
this.buffer.setLines(nextState);
|
|
921
|
+
// Clamp cursor
|
|
922
|
+
this.cursor.line = Math.min(this.cursor.line, this.buffer.getLineCount() - 1);
|
|
923
|
+
const lineLen = this.buffer.getLine(this.cursor.line).length;
|
|
924
|
+
this.cursor.character = Math.min(this.cursor.character, Math.max(0, lineLen - 1));
|
|
925
|
+
this.desiredCol = this.cursor.character;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|