Skip to content

Commit b133e35

Browse files
authored
feat: :set persistence, jump list, buffer dedup, gu/gU, Ctrl-o/Ctrl-i (#17)
* chore: add changeset for vim native batch2 * chore: release version bump * fix(renderer-vecto): deduplicate buffers on file reopen openBuffer now checks if a buffer with the same label already exists before creating a new one, preventing the same file from appearing in multiple tabs when mouse-clicked repeatedly. * feat(core): :set persistence, jump list, buffer dedup, gu/gU parser
1 parent 1f0b45d commit b133e35

3 files changed

Lines changed: 68 additions & 1 deletion

File tree

packages/core/src/editor.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,12 @@ export class VemEditorState {
218218
private scrollToLineCallbacks: ((line: number) => void)[] = [];
219219
private openFileUnderCursorCallbacks: ((path: string) => void)[] = [];
220220
private windowActionCallbacks: ((action: string) => void)[] = [];
221+
private optionsChangedCallbacks: ((key: string, value: string) => void)[] = [];
221222
/** The cursor position BEFORE the last change (for g;/g,). */
222223
private changeList: Position[] = [];
224+
/** Jump list (Ctrl-o/Ctrl-i): oldest entries at index 0. */
225+
private jumpList: Position[] = [];
226+
private jumpListIndex: number = -1;
223227
/** Number of visible lines in the viewport, set by the renderer (for H/M/L). */
224228
public visibleLines: number = 0;
225229
/** Current scroll line offset, set by the renderer (for H/M/L). */
@@ -345,6 +349,14 @@ export class VemEditorState {
345349
this.openFileUnderCursorCallbacks.push(callback);
346350
}
347351

352+
public onOptionsChanged(callback: (key: string, value: string) => void): void {
353+
this.optionsChangedCallbacks.push(callback);
354+
}
355+
356+
private triggerOptionsChanged(key: string, value: string): void {
357+
for (const cb of this.optionsChangedCallbacks) cb(key, value);
358+
}
359+
348360
private triggerScrollToLine(line: number): void {
349361
for (const cb of this.scrollToLineCallbacks) {
350362
cb(line);
@@ -1451,6 +1463,14 @@ export class VemEditorState {
14511463
this.addNumber(-cmd.count);
14521464
break;
14531465

1466+
// --- <C-o>/<C-i>: jump list navigation ---
1467+
case '<C-o>':
1468+
this.goBackInJumplist();
1469+
break;
1470+
case '<C-i>':
1471+
this.goForwardInJumplist();
1472+
break;
1473+
14541474
// --- m{a-zA-Z}: set mark ---
14551475
case 'm':
14561476
if (cmd.mark) {
@@ -1854,6 +1874,35 @@ export class VemEditorState {
18541874
}
18551875
}
18561876

1877+
/** Record a cursor position in the jump list. Called by moveCursorByMotion. */
1878+
public pushJumpList(): void {
1879+
this.jumpList.push({ ...this.cursor });
1880+
if (this.jumpList.length > 100) this.jumpList.shift();
1881+
this.jumpListIndex = this.jumpList.length - 1;
1882+
}
1883+
1884+
/** `<C-o>`: go to the previous (older) position in the jump list. */
1885+
private goBackInJumplist(): void {
1886+
if (this.jumpListIndex > 0 && this.jumpList.length > 1) {
1887+
this.jumpListIndex--;
1888+
const pos = this.jumpList[this.jumpListIndex];
1889+
this.cursor.line = pos.line;
1890+
this.cursor.character = pos.character;
1891+
this.desiredCol = pos.character;
1892+
}
1893+
}
1894+
1895+
/** `<C-i>`: go to the next (newer) position in the jump list. */
1896+
private goForwardInJumplist(): void {
1897+
if (this.jumpListIndex < this.jumpList.length - 1) {
1898+
this.jumpListIndex++;
1899+
const pos = this.jumpList[this.jumpListIndex];
1900+
this.cursor.line = pos.line;
1901+
this.cursor.character = pos.character;
1902+
this.desiredCol = pos.character;
1903+
}
1904+
}
1905+
18571906
/** `gJ`: join lines without inserting a space. */
18581907
private joinLinesNoSpace(count: number): void {
18591908
const start = this.cursor.line;
@@ -1893,7 +1942,7 @@ export class VemEditorState {
18931942
}
18941943
}
18951944

1896-
private executeSetOption(option: string): void {
1945+
public executeSetOption(option: string): void {
18971946
// Vim's real syntax for OS-clipboard integration: `:set clipboard=unnamed`
18981947
// (or `unnamedplus`) routes y/d/c/x/p/P through the system clipboard
18991948
// instead of just the internal `"` register; `:set clipboard=` reverts.
@@ -1935,14 +1984,18 @@ export class VemEditorState {
19351984

19361985
if (option === 'relativenumber' || option === 'rnu') {
19371986
this.layoutConfig = { ...this.layoutConfig, lineNumbers: 'relative' };
1987+
this.triggerOptionsChanged('relativenumber', 'relative');
19381988
} else if (option === 'norelativenumber' || option === 'nornu') {
19391989
// Falls back to absolute if numbers are on, else stays off.
19401990
const next = this.layoutConfig.lineNumbers === 'relative' ? 'absolute' : 'none';
19411991
this.layoutConfig = { ...this.layoutConfig, lineNumbers: next };
1992+
this.triggerOptionsChanged('number', next);
19421993
} else if (option === 'number' || option === 'nu') {
19431994
this.layoutConfig = { ...this.layoutConfig, lineNumbers: 'absolute' };
1995+
this.triggerOptionsChanged('number', 'absolute');
19441996
} else if (option === 'nonumber' || option === 'nonu') {
19451997
this.layoutConfig = { ...this.layoutConfig, lineNumbers: 'none' };
1998+
this.triggerOptionsChanged('number', 'none');
19461999
} else {
19472000
this.statusMessage = `E518: Unknown option: ${option}`;
19482001
}
@@ -1976,6 +2029,7 @@ export class VemEditorState {
19762029
* aborts rather than leaving the cursor somewhere half-moved.
19772030
*/
19782031
private moveCursorByMotion(motion: string, count: number, findChar?: string): boolean {
2032+
this.pushJumpList();
19792033
if (motion === 'f' || motion === 'F' || motion === 't' || motion === 'T') {
19802034
return this.moveByFindChar(motion, count, findChar);
19812035
}

packages/core/src/parser.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ const normalCommands = [
9494
'<C-y>',
9595
'<C-a>',
9696
'<C-x>',
97+
'<C-o>',
98+
'<C-i>',
9799
'Escape',
98100
];
99101

@@ -105,6 +107,10 @@ const gPrefixCommands: Record<string, string> = {
105107
i: 'gi', // go to last insert position
106108
';': 'g;', // go to previous change
107109
',': 'g,', // go to next change
110+
u: 'gu', // make lowercase
111+
U: 'gU', // make uppercase
112+
q: 'gq', // format text
113+
J: 'gJ', // join without space
108114
};
109115

110116
const zPrefixCommands: Record<string, string> = {

packages/renderer-vecto/src/Workspace.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ export class VemWorkspace extends UIComponent {
7676
* so the caller can associate it with a file handle for save-back.
7777
*/
7878
public openBuffer(text: string, label = 'untitled'): string {
79+
if (label !== 'untitled') {
80+
const existing = this.buffers.find((b) => b.label === label);
81+
if (existing) {
82+
this.syncTabs(existing.id);
83+
return existing.id;
84+
}
85+
}
7986
const entry = this.makeBuffer(text, label);
8087
this.syncTabs(entry.id);
8188
return entry.id;

0 commit comments

Comments
 (0)