@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.
@@ -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
+ });