@@ -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 }
0 commit comments