Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/headless/Terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export class Terminal extends CoreTerminal {
public readonly onA11yChar = this._onA11yCharEmitter.event;
private readonly _onA11yTabEmitter = this._register(new Emitter<number>());
public readonly onA11yTab = this._onA11yTabEmitter.event;
private readonly _onRowChange = this._register(new Emitter<{ start: number, end: number }>());
public readonly onRowChange = this._onRowChange.event;

constructor(
options: ITerminalOptions = {}
Expand All @@ -53,6 +55,11 @@ export class Terminal extends CoreTerminal {
this._register(Event.forward(this._inputHandler.onTitleChange, this._onTitleChange));
this._register(Event.forward(this._inputHandler.onA11yChar, this._onA11yCharEmitter));
this._register(Event.forward(this._inputHandler.onA11yTab, this._onA11yTabEmitter));
this._register(this._inputHandler.onRequestRefreshRows(e => {
if (e) {
this._onRowChange.fire({ start: e.start, end: e.end });
}
}));
}

/**
Expand Down
86 changes: 86 additions & 0 deletions src/headless/public/Terminal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,92 @@ describe('Headless API Tests', function (): void {
await writeSync('\x07');
deepStrictEqual(calls, [true]);
});

it('onRowChange', async () => {
let callCount = 0;
term.onRowChange(() => callCount++);
strictEqual(callCount, 0);
await writeSync('hello');
strictEqual(callCount, 1);
await writeSync('\nworld');
strictEqual(callCount, 2);
});

it('onRowChange - cursor positioning', async () => {
term = new Terminal({ rows: 25, cols: 80, allowProposedApi: true });
const calls: Array<{ start: number, end: number }> = [];
term.onRowChange(e => calls.push(e));

// Basic cursor movements
await writeSync('\x1b[10;40Htest'); // Move to middle, write
strictEqual(calls.length, 1);
strictEqual(calls[0].start, 0);
strictEqual(calls[0].end, 9);

// Multiple cursor movements in one write
calls.length = 0;
await writeSync('\x1b[5;20Ha\x1b[15;60Hb\x1b[20;10Hc');
strictEqual(calls.length, 1);
strictEqual(calls[0].start, 4);
strictEqual(calls[0].end, 19);

// Large scrollback buffer test
const termScroll = new Terminal({ rows: 10, cols: 80, scrollback: 1000, allowProposedApi: true });
const scrollCalls: Array<{ start: number, end: number }> = [];
termScroll.onRowChange(e => scrollCalls.push(e));

// Fill and scroll beyond viewport - validate all events
for (let i = 0; i < 30; i++) {
const beforeCount = scrollCalls.length;
await new Promise<void>(resolve => termScroll.write(`Line ${i}\n`, resolve));
strictEqual(scrollCalls.length, beforeCount + 1);
const event = scrollCalls[scrollCalls.length - 1];
if (i < 9) {
// Before scrolling: current row + linefeed to next row
strictEqual(event.start, i);
strictEqual(event.end, i + 1);
} else {
// After scrolling starts: entire viewport
strictEqual(event.start, 0);
strictEqual(event.end, 9);
}
}

// Cursor movement in scrolled terminal
scrollCalls.length = 0;
await new Promise<void>(resolve => termScroll.write('\x1b[5;40HX', resolve));
strictEqual(scrollCalls.length, 1);
strictEqual(scrollCalls[0].start, 4);
strictEqual(scrollCalls[0].end, 9);
});

it('onRowChange - scrolling', async () => {
term = new Terminal({ rows: 3, cols: 5, allowProposedApi: true });
const calls: Array<{ start: number, end: number }> = [];
term.onRowChange(e => calls.push(e));

// Fill terminal
await writeSync('line1\nline2\nline3');
calls.length = 0;

// Cause scroll
await writeSync('\nline4');
strictEqual(calls.length, 1);
strictEqual(calls[0].start, 0);
strictEqual(calls[0].end, 2);
});

it('onRowChange - text wrapping', async () => {
term = new Terminal({ rows: 3, cols: 5, allowProposedApi: true });
const calls: Array<{ start: number, end: number }> = [];
term.onRowChange(e => calls.push(e));

// Write text that wraps
await writeSync('verylongtext');
strictEqual(calls.length, 1);
strictEqual(calls[0].start, 0);
strictEqual(calls[0].end, 2);
});
});

describe('buffer', () => {
Expand Down
8 changes: 6 additions & 2 deletions src/headless/public/Terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { Terminal as TerminalCore } from 'headless/Terminal';
import { AddonManager } from 'common/public/AddonManager';
import { ITerminalOptions } from 'common/Types';
import { Disposable } from 'vs/base/common/lifecycle';
import type { Event } from 'vs/base/common/event';
import { Event } from 'vs/base/common/event';
/**
* The set of options that only have an effect when set in the Terminal constructor.
*/
Expand Down Expand Up @@ -81,6 +81,10 @@ export class Terminal extends Disposable implements ITerminalApi {
public get onScroll(): Event<number> { return this._core.onScroll; }
public get onTitleChange(): Event<string> { return this._core.onTitleChange; }
public get onWriteParsed(): Event<void> { return this._core.onWriteParsed; }
public get onRowChange(): Event<{ start: number, end: number }> {
this._checkProposedApi();
return Event.map(this._core.onRowChange, e => ({ start: e.start, end: e.end }));
}

public get parser(): IParser {
this._checkProposedApi();
Expand Down Expand Up @@ -186,7 +190,7 @@ export class Terminal extends Disposable implements ITerminalApi {
}
public loadAddon(addon: ITerminalAddon): void {
// TODO: This could cause issues if the addon calls renderer apis
this._addonManager.loadAddon(this as any, addon);
this._addonManager.loadAddon(this as any, addon as any);
}

private _verifyIntegers(...values: number[]): void {
Expand Down
7 changes: 7 additions & 0 deletions typings/xterm-headless.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,13 @@ declare module '@xterm/headless' {
*/
onResize: IEvent<{ cols: number, rows: number }>;

/**
* Adds an event listener for when buffer rows change during parsing. The event
* value contains the range of rows that changed.
* @returns an `IDisposable` to stop listening.
*/
onRowChange: IEvent<{ start: number, end: number }>;

/**
* Adds an event listener for when a scroll occurs. The event value is the
* new position of the viewport.
Expand Down