@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
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { VimBuffer, UndoManager } from './buffer';
|
|
3
|
+
|
|
4
|
+
describe('VimBuffer', () => {
|
|
5
|
+
it('should initialize with empty line or given text', () => {
|
|
6
|
+
const buf1 = new VimBuffer();
|
|
7
|
+
expect(buf1.getLines()).toEqual(['']);
|
|
8
|
+
|
|
9
|
+
const buf2 = new VimBuffer('hello\nworld');
|
|
10
|
+
expect(buf2.getLines()).toEqual(['hello', 'world']);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should support line insertion, set, and delete', () => {
|
|
14
|
+
const buf = new VimBuffer('line1\nline2');
|
|
15
|
+
buf.insertLine(1, 'inserted');
|
|
16
|
+
expect(buf.getLines()).toEqual(['line1', 'inserted', 'line2']);
|
|
17
|
+
|
|
18
|
+
buf.setLine(2, 'modified');
|
|
19
|
+
expect(buf.getLines()).toEqual(['line1', 'inserted', 'modified']);
|
|
20
|
+
|
|
21
|
+
const deleted = buf.deleteLines(1, 1);
|
|
22
|
+
expect(deleted).toEqual(['inserted']);
|
|
23
|
+
expect(buf.getLines()).toEqual(['line1', 'modified']);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should support text insertion at a position', () => {
|
|
27
|
+
const buf = new VimBuffer('hello world');
|
|
28
|
+
const endPos = buf.insertText({ line: 0, character: 6 }, 'beautiful ');
|
|
29
|
+
expect(buf.getText()).toBe('hello beautiful world');
|
|
30
|
+
expect(endPos).toEqual({ line: 0, character: 16 });
|
|
31
|
+
|
|
32
|
+
const buf2 = new VimBuffer('line1\nline2');
|
|
33
|
+
const endPos2 = buf2.insertText({ line: 0, character: 5 }, '\nmiddle\nline');
|
|
34
|
+
expect(buf2.getText()).toBe('line1\nmiddle\nline\nline2');
|
|
35
|
+
expect(endPos2).toEqual({ line: 2, character: 4 });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should support deleting ranges of text', () => {
|
|
39
|
+
const buf = new VimBuffer('hello beautiful world');
|
|
40
|
+
buf.deleteRange({ line: 0, character: 5 }, { line: 0, character: 15 });
|
|
41
|
+
expect(buf.getText()).toBe('hello world');
|
|
42
|
+
|
|
43
|
+
const buf2 = new VimBuffer('line1\nmiddle\nline\nline2');
|
|
44
|
+
// Delete from middle of line1 to middle of line
|
|
45
|
+
buf2.deleteRange({ line: 0, character: 4 }, { line: 2, character: 4 });
|
|
46
|
+
expect(buf2.getText()).toBe('line\nline2');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('UndoManager', () => {
|
|
51
|
+
it('should handle push, undo, and redo correctly', () => {
|
|
52
|
+
const history = new UndoManager();
|
|
53
|
+
let current = ['hello'];
|
|
54
|
+
|
|
55
|
+
history.push(current);
|
|
56
|
+
current = ['hello', 'world'];
|
|
57
|
+
|
|
58
|
+
const undone = history.undo(current);
|
|
59
|
+
expect(undone).toEqual(['hello']);
|
|
60
|
+
|
|
61
|
+
const redone = history.redo(undone!);
|
|
62
|
+
expect(redone).toEqual(['hello', 'world']);
|
|
63
|
+
});
|
|
64
|
+
});
|
package/src/buffer.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { Position } from './index';
|
|
2
|
+
|
|
3
|
+
export class VimBuffer {
|
|
4
|
+
private lines: string[] = [''];
|
|
5
|
+
private changeCallbacks: (() => void)[] = [];
|
|
6
|
+
|
|
7
|
+
constructor(initialText?: string) {
|
|
8
|
+
if (initialText !== undefined) {
|
|
9
|
+
this.lines = initialText.split(/\r?\n/);
|
|
10
|
+
if (this.lines.length === 0) {
|
|
11
|
+
this.lines = [''];
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public onChange(callback: () => void): void {
|
|
17
|
+
this.changeCallbacks.push(callback);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private triggerChange(): void {
|
|
21
|
+
for (const cb of this.changeCallbacks) {
|
|
22
|
+
cb();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public getLines(): string[] {
|
|
27
|
+
return [...this.lines];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public setLines(lines: string[]): void {
|
|
31
|
+
this.lines = [...lines];
|
|
32
|
+
if (this.lines.length === 0) {
|
|
33
|
+
this.lines = [''];
|
|
34
|
+
}
|
|
35
|
+
this.triggerChange();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public getLine(lineIdx: number): string {
|
|
39
|
+
if (lineIdx < 0 || lineIdx >= this.lines.length) {
|
|
40
|
+
return '';
|
|
41
|
+
}
|
|
42
|
+
return this.lines[lineIdx];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public getLineCount(): number {
|
|
46
|
+
return this.lines.length;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public insertText(pos: Position, text: string): Position {
|
|
50
|
+
const line = this.getLine(pos.line);
|
|
51
|
+
const before = line.substring(0, pos.character);
|
|
52
|
+
const after = line.substring(pos.character);
|
|
53
|
+
const insertedLines = text.split(/\r?\n/);
|
|
54
|
+
|
|
55
|
+
if (insertedLines.length === 1) {
|
|
56
|
+
this.lines[pos.line] = before + insertedLines[0] + after;
|
|
57
|
+
this.triggerChange();
|
|
58
|
+
return {
|
|
59
|
+
line: pos.line,
|
|
60
|
+
character: pos.character + insertedLines[0].length,
|
|
61
|
+
};
|
|
62
|
+
} else {
|
|
63
|
+
this.lines[pos.line] = before + insertedLines[0];
|
|
64
|
+
const middleLines = insertedLines.slice(1, insertedLines.length - 1);
|
|
65
|
+
const lastLine = insertedLines[insertedLines.length - 1] + after;
|
|
66
|
+
|
|
67
|
+
this.lines.splice(pos.line + 1, 0, ...middleLines, lastLine);
|
|
68
|
+
this.triggerChange();
|
|
69
|
+
return {
|
|
70
|
+
line: pos.line + insertedLines.length - 1,
|
|
71
|
+
character: insertedLines[insertedLines.length - 1].length,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public deleteRange(start: Position, end: Position): void {
|
|
77
|
+
let s = { ...start };
|
|
78
|
+
let e = { ...end };
|
|
79
|
+
|
|
80
|
+
// Ensure start is before end
|
|
81
|
+
if (s.line > e.line || (s.line === e.line && s.character > e.character)) {
|
|
82
|
+
const temp = s;
|
|
83
|
+
s = e;
|
|
84
|
+
e = temp;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const startLine = this.getLine(s.line);
|
|
88
|
+
const endLine = this.getLine(e.line);
|
|
89
|
+
|
|
90
|
+
const before = startLine.substring(0, s.character);
|
|
91
|
+
const after = endLine.substring(e.character);
|
|
92
|
+
|
|
93
|
+
this.lines[s.line] = before + after;
|
|
94
|
+
if (e.line > s.line) {
|
|
95
|
+
this.lines.splice(s.line + 1, e.line - s.line);
|
|
96
|
+
}
|
|
97
|
+
if (this.lines.length === 0) {
|
|
98
|
+
this.lines = [''];
|
|
99
|
+
}
|
|
100
|
+
this.triggerChange();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public deleteLines(startLineIdx: number, endLineIdx: number): string[] {
|
|
104
|
+
const min = Math.max(0, Math.min(startLineIdx, endLineIdx));
|
|
105
|
+
const max = Math.min(this.lines.length - 1, Math.max(startLineIdx, endLineIdx));
|
|
106
|
+
const deleted = this.lines.splice(min, max - min + 1);
|
|
107
|
+
if (this.lines.length === 0) {
|
|
108
|
+
this.lines = [''];
|
|
109
|
+
}
|
|
110
|
+
this.triggerChange();
|
|
111
|
+
return deleted;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public insertLine(lineIdx: number, text: string): void {
|
|
115
|
+
this.lines.splice(lineIdx, 0, text);
|
|
116
|
+
this.triggerChange();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public setLine(lineIdx: number, text: string): void {
|
|
120
|
+
if (lineIdx >= 0 && lineIdx < this.lines.length) {
|
|
121
|
+
this.lines[lineIdx] = text;
|
|
122
|
+
this.triggerChange();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
public getText(): string {
|
|
127
|
+
return this.lines.join('\n');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export class UndoManager {
|
|
132
|
+
private undoStack: string[][] = [];
|
|
133
|
+
private redoStack: string[][] = [];
|
|
134
|
+
|
|
135
|
+
public push(lines: string[]): void {
|
|
136
|
+
this.undoStack.push([...lines]);
|
|
137
|
+
this.redoStack = [];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
public undo(currentLines: string[]): string[] | null {
|
|
141
|
+
if (this.undoStack.length === 0) return null;
|
|
142
|
+
this.redoStack.push([...currentLines]);
|
|
143
|
+
return this.undoStack.pop() || null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public redo(currentLines: string[]): string[] | null {
|
|
147
|
+
if (this.redoStack.length === 0) return null;
|
|
148
|
+
this.undoStack.push([...currentLines]);
|
|
149
|
+
return this.redoStack.pop() || null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { VemEditorState } from './editor';
|
|
3
|
+
|
|
4
|
+
describe('VemEditorState', () => {
|
|
5
|
+
it('should start in NORMAL mode with default cursor', () => {
|
|
6
|
+
const editor = new VemEditorState('line1\nline2');
|
|
7
|
+
expect(editor.getMode()).toBe('NORMAL');
|
|
8
|
+
expect(editor.getCursor()).toEqual({ line: 0, character: 0 });
|
|
9
|
+
expect(editor.getBuffer().getText()).toBe('line1\nline2');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should transition to INSERT mode and accept character insertions', () => {
|
|
13
|
+
const editor = new VemEditorState('hello');
|
|
14
|
+
editor.handleKey('i');
|
|
15
|
+
expect(editor.getMode()).toBe('INSERT');
|
|
16
|
+
|
|
17
|
+
editor.handleKey(' ');
|
|
18
|
+
editor.handleKey('w');
|
|
19
|
+
editor.handleKey('o');
|
|
20
|
+
editor.handleKey('r');
|
|
21
|
+
editor.handleKey('l');
|
|
22
|
+
editor.handleKey('d');
|
|
23
|
+
|
|
24
|
+
expect(editor.getBuffer().getText()).toBe(' worldhello');
|
|
25
|
+
expect(editor.getCursor()).toEqual({ line: 0, character: 6 });
|
|
26
|
+
|
|
27
|
+
// Exit insert mode
|
|
28
|
+
editor.handleKey('Escape');
|
|
29
|
+
expect(editor.getMode()).toBe('NORMAL');
|
|
30
|
+
// Cursor moves back one in Normal mode
|
|
31
|
+
expect(editor.getCursor()).toEqual({ line: 0, character: 5 });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should support Backspace and Enter in INSERT mode', () => {
|
|
35
|
+
const editor = new VemEditorState('hello');
|
|
36
|
+
editor.handleKey('i');
|
|
37
|
+
editor.handleKey('Backspace'); // At character 0, should do nothing as line 0 has no preceding line
|
|
38
|
+
expect(editor.getBuffer().getText()).toBe('hello');
|
|
39
|
+
|
|
40
|
+
editor.handleKey('l'); // character 1
|
|
41
|
+
editor.handleKey('Enter'); // should split line
|
|
42
|
+
expect(editor.getBuffer().getText()).toBe('l\nhello');
|
|
43
|
+
expect(editor.getCursor()).toEqual({ line: 1, character: 0 });
|
|
44
|
+
|
|
45
|
+
editor.handleKey('Backspace'); // should merge lines back
|
|
46
|
+
expect(editor.getBuffer().getText()).toBe('lhello');
|
|
47
|
+
expect(editor.getCursor()).toEqual({ line: 0, character: 1 });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should handle simple motions in NORMAL mode', () => {
|
|
51
|
+
const editor = new VemEditorState('hello beautiful world');
|
|
52
|
+
// move right 6 times to reach 'b'
|
|
53
|
+
for (let i = 0; i < 6; i++) {
|
|
54
|
+
editor.handleKey('l');
|
|
55
|
+
}
|
|
56
|
+
expect(editor.getCursor()).toEqual({ line: 0, character: 6 });
|
|
57
|
+
|
|
58
|
+
// move left 2 times
|
|
59
|
+
editor.handleKey('h');
|
|
60
|
+
editor.handleKey('h');
|
|
61
|
+
expect(editor.getCursor()).toEqual({ line: 0, character: 4 });
|
|
62
|
+
|
|
63
|
+
// Move to end of line
|
|
64
|
+
editor.handleKey('$');
|
|
65
|
+
expect(editor.getCursor()).toEqual({ line: 0, character: 20 });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should support dw, x, and delete operators', () => {
|
|
69
|
+
const editor = new VemEditorState('hello beautiful world');
|
|
70
|
+
// dw at start of line should delete 'hello '
|
|
71
|
+
editor.handleKey('d');
|
|
72
|
+
editor.handleKey('w');
|
|
73
|
+
expect(editor.getBuffer().getText()).toBe('beautiful world');
|
|
74
|
+
expect(editor.getCursor()).toEqual({ line: 0, character: 0 });
|
|
75
|
+
|
|
76
|
+
// x should delete 'b'
|
|
77
|
+
editor.handleKey('x');
|
|
78
|
+
expect(editor.getBuffer().getText()).toBe('eautiful world');
|
|
79
|
+
|
|
80
|
+
// diw should delete word 'eautiful'
|
|
81
|
+
editor.handleKey('d');
|
|
82
|
+
editor.handleKey('i');
|
|
83
|
+
editor.handleKey('w');
|
|
84
|
+
expect(editor.getBuffer().getText()).toBe(' world');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should support undo and redo', () => {
|
|
88
|
+
const editor = new VemEditorState('hello');
|
|
89
|
+
editor.handleKey('i');
|
|
90
|
+
editor.handleKey('!');
|
|
91
|
+
editor.handleKey('Escape');
|
|
92
|
+
expect(editor.getBuffer().getText()).toBe('!hello');
|
|
93
|
+
|
|
94
|
+
editor.handleKey('u');
|
|
95
|
+
expect(editor.getBuffer().getText()).toBe('hello');
|
|
96
|
+
|
|
97
|
+
editor.handleKey('<C-r>');
|
|
98
|
+
expect(editor.getBuffer().getText()).toBe('!hello');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should support VISUAL mode selection and operations', () => {
|
|
102
|
+
const editor = new VemEditorState('hello world');
|
|
103
|
+
editor.handleKey('v');
|
|
104
|
+
expect(editor.getMode()).toBe('VISUAL');
|
|
105
|
+
|
|
106
|
+
// Move cursor right 4 times to select 'hello'
|
|
107
|
+
for (let i = 0; i < 4; i++) {
|
|
108
|
+
editor.handleKey('l');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const selection = editor.getVisualSelection();
|
|
112
|
+
expect(selection).not.toBeNull();
|
|
113
|
+
expect(selection!.anchor).toEqual({ line: 0, character: 0 });
|
|
114
|
+
expect(selection!.active).toEqual({ line: 0, character: 4 });
|
|
115
|
+
|
|
116
|
+
// Yank selection
|
|
117
|
+
editor.handleKey('y');
|
|
118
|
+
expect(editor.getMode()).toBe('NORMAL');
|
|
119
|
+
expect(editor.getRegister()).toEqual({
|
|
120
|
+
text: 'hello',
|
|
121
|
+
type: 'char',
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Move cursor to end, and paste
|
|
125
|
+
editor.handleKey('$');
|
|
126
|
+
editor.handleKey('p');
|
|
127
|
+
expect(editor.getBuffer().getText()).toBe('hello worldhello');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('COMMAND mode', () => {
|
|
131
|
+
it('should transition to COMMAND mode and buffer input keys', () => {
|
|
132
|
+
const editor = new VemEditorState('test');
|
|
133
|
+
editor.handleKey(':');
|
|
134
|
+
expect(editor.getMode()).toBe('COMMAND');
|
|
135
|
+
expect(editor.getCommandText()).toBe('');
|
|
136
|
+
|
|
137
|
+
editor.handleKey('w');
|
|
138
|
+
editor.handleKey('q');
|
|
139
|
+
expect(editor.getCommandText()).toBe('wq');
|
|
140
|
+
|
|
141
|
+
editor.handleKey('Backspace');
|
|
142
|
+
expect(editor.getCommandText()).toBe('w');
|
|
143
|
+
|
|
144
|
+
editor.handleKey('Escape');
|
|
145
|
+
expect(editor.getMode()).toBe('NORMAL');
|
|
146
|
+
expect(editor.getCommandText()).toBe('');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should trigger events on Enter', () => {
|
|
150
|
+
const editor = new VemEditorState('test');
|
|
151
|
+
|
|
152
|
+
let saved = false;
|
|
153
|
+
editor.onSave(() => {
|
|
154
|
+
saved = true;
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
let splitDir: string | null = null;
|
|
158
|
+
editor.onSplit((dir) => {
|
|
159
|
+
splitDir = dir;
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Test :w
|
|
163
|
+
editor.handleKey(':');
|
|
164
|
+
editor.handleKey('w');
|
|
165
|
+
editor.handleKey('Enter');
|
|
166
|
+
expect(editor.getMode()).toBe('NORMAL');
|
|
167
|
+
expect(saved).toBe(true);
|
|
168
|
+
|
|
169
|
+
// Test :vsp
|
|
170
|
+
editor.handleKey(':');
|
|
171
|
+
editor.handleKey('v');
|
|
172
|
+
editor.handleKey('s');
|
|
173
|
+
editor.handleKey('p');
|
|
174
|
+
editor.handleKey('Enter');
|
|
175
|
+
expect(editor.getMode()).toBe('NORMAL');
|
|
176
|
+
expect(splitDir).toBe('vertical');
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
});
|