Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Buffer API #2074

Merged
merged 15 commits into from
May 12, 2019
52 changes: 50 additions & 2 deletions src/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* @license MIT
*/

import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter, IDisposable } from 'xterm';
import { ITerminalOptions as IPublicTerminalOptions, IEventEmitter, IDisposable, IMarker } from 'xterm';
import { IColorSet, IRenderer } from './renderer/Types';
import { ICharset, IAttributeData, ICellData, IBufferLine, CharData } from './core/Types';
import { ICircularList } from './common/Types';
Expand Down Expand Up @@ -195,7 +195,7 @@ export interface ILinkifierEvent {
fg: number;
}

export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
screenElement: HTMLElement;
selectionManager: ISelectionManager;
charMeasure: ICharMeasure;
Expand All @@ -220,6 +220,54 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce
showCursor(): void;
}

export interface IPublicTerminal extends IDisposable, IEventEmitter {
textarea: HTMLTextAreaElement;
rows: number;
cols: number;
buffer: IBuffer;
markers: IMarker[];
onCursorMove: IEvent<void>;
onData: IEvent<string>;
onKey: IEvent<{ key: string, domEvent: KeyboardEvent }>;
onLineFeed: IEvent<void>;
onScroll: IEvent<number>;
onSelectionChange: IEvent<void>;
onRender: IEvent<{ start: number, end: number }>;
onResize: IEvent<{ cols: number, rows: number }>;
onTitleChange: IEvent<string>;
blur(): void;
focus(): void;
resize(columns: number, rows: number): void;
writeln(data: string): void;
open(parent: HTMLElement): void;
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void;
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable;
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number;
deregisterLinkMatcher(matcherId: number): void;
registerCharacterJoiner(handler: (text: string) => [number, number][]): number;
deregisterCharacterJoiner(joinerId: number): void;
addMarker(cursorYOffset: number): IMarker;
hasSelection(): boolean;
getSelection(): string;
clearSelection(): void;
selectAll(): void;
selectLines(start: number, end: number): void;
dispose(): void;
destroy(): void;
scrollLines(amount: number): void;
scrollPages(pageCount: number): void;
scrollToTop(): void;
scrollToBottom(): void;
scrollToLine(line: number): void;
clear(): void;
write(data: string): void;
getOption(key: string): any;
setOption(key: string, value: any): void;
refresh(start: number, end: number): void;
reset(): void;
}

export interface IBufferAccessor {
buffer: IBuffer;
}
Expand Down
125 changes: 119 additions & 6 deletions src/public/Terminal.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ describe('API Integration Tests', () => {
window.term.write('foo');
window.term.write('bar');
`);
assert.equal(await page.evaluate(`window.term._core.buffer.translateBufferLineToString(0, true)`), 'foobar');
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foobar');
});

it('writeln', async function(): Promise<any> {
Expand All @@ -58,8 +58,8 @@ describe('API Integration Tests', () => {
window.term.writeln('foo');
window.term.writeln('bar');
`);
assert.equal(await page.evaluate(`window.term._core.buffer.translateBufferLineToString(0, true)`), 'foo');
assert.equal(await page.evaluate(`window.term._core.buffer.translateBufferLineToString(1, true)`), 'bar');
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foo');
assert.equal(await page.evaluate(`window.term.buffer.getLine(1).translateToString(true)`), 'bar');
});

it('clear', async function(): Promise<any> {
Expand All @@ -72,10 +72,10 @@ describe('API Integration Tests', () => {
}
`);
await page.evaluate(`window.term.clear()`);
assert.equal(await page.evaluate(`window.term._core.buffer.lines.length`), '5');
assert.equal(await page.evaluate(`window.term._core.buffer.translateBufferLineToString(0, true)`), 'test9');
assert.equal(await page.evaluate(`window.term.buffer.length`), '5');
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'test9');
for (let i = 1; i < 5; i++) {
assert.equal(await page.evaluate(`window.term._core.buffer.translateBufferLineToString(${i}, true)`), '');
assert.equal(await page.evaluate(`window.term.buffer.getLine(${i}).translateToString(true)`), '');
}
});

Expand Down Expand Up @@ -231,6 +231,119 @@ describe('API Integration Tests', () => {
assert.deepEqual(await page.evaluate(`window.calls`), ['foo']);
});
});

describe('buffer', () => {
it('cursorX, cursorY', async function(): Promise<any> {
this.timeout(10000);
await openTerminal({ rows: 5, cols: 5 });
assert.equal(await page.evaluate(`window.term.buffer.cursorX`), 0);
assert.equal(await page.evaluate(`window.term.buffer.cursorY`), 0);
await page.evaluate(`window.term.write('foo')`);
assert.equal(await page.evaluate(`window.term.buffer.cursorX`), 3);
assert.equal(await page.evaluate(`window.term.buffer.cursorY`), 0);
await page.evaluate(`window.term.write('\\n')`);
assert.equal(await page.evaluate(`window.term.buffer.cursorX`), 3);
assert.equal(await page.evaluate(`window.term.buffer.cursorY`), 1);
await page.evaluate(`window.term.write('\\r')`);
assert.equal(await page.evaluate(`window.term.buffer.cursorX`), 0);
assert.equal(await page.evaluate(`window.term.buffer.cursorY`), 1);
await page.evaluate(`window.term.write('abcde')`);
assert.equal(await page.evaluate(`window.term.buffer.cursorX`), 5);
assert.equal(await page.evaluate(`window.term.buffer.cursorY`), 1);
await page.evaluate(`window.term.write('\\n\\r\\n\\n\\n\\n\\n')`);
assert.equal(await page.evaluate(`window.term.buffer.cursorX`), 0);
assert.equal(await page.evaluate(`window.term.buffer.cursorY`), 4);
});

it('viewportY', async function(): Promise<any> {
this.timeout(10000);
await openTerminal({ rows: 5 });
assert.equal(await page.evaluate(`window.term.buffer.viewportY`), 0);
await page.evaluate(`window.term.write('\\n\\n\\n\\n')`);
assert.equal(await page.evaluate(`window.term.buffer.viewportY`), 0);
await page.evaluate(`window.term.write('\\n')`);
assert.equal(await page.evaluate(`window.term.buffer.viewportY`), 1);
await page.evaluate(`window.term.write('\\n\\n\\n\\n')`);
assert.equal(await page.evaluate(`window.term.buffer.viewportY`), 5);
await page.evaluate(`window.term.scrollLines(-1)`);
assert.equal(await page.evaluate(`window.term.buffer.viewportY`), 4);
await page.evaluate(`window.term.scrollToTop()`);
assert.equal(await page.evaluate(`window.term.buffer.viewportY`), 0);
});

it('baseY', async function(): Promise<any> {
this.timeout(10000);
await openTerminal({ rows: 5 });
assert.equal(await page.evaluate(`window.term.buffer.baseY`), 0);
await page.evaluate(`window.term.write('\\n\\n\\n\\n')`);
assert.equal(await page.evaluate(`window.term.buffer.baseY`), 0);
await page.evaluate(`window.term.write('\\n')`);
assert.equal(await page.evaluate(`window.term.buffer.baseY`), 1);
await page.evaluate(`window.term.write('\\n\\n\\n\\n')`);
assert.equal(await page.evaluate(`window.term.buffer.baseY`), 5);
await page.evaluate(`window.term.scrollLines(-1)`);
assert.equal(await page.evaluate(`window.term.buffer.baseY`), 5);
await page.evaluate(`window.term.scrollToTop()`);
assert.equal(await page.evaluate(`window.term.buffer.baseY`), 5);
});

it('length', async function(): Promise<any> {
this.timeout(10000);
await openTerminal({ rows: 5 });
assert.equal(await page.evaluate(`window.term.buffer.length`), 5);
await page.evaluate(`window.term.write('\\n\\n\\n\\n')`);
assert.equal(await page.evaluate(`window.term.buffer.length`), 5);
await page.evaluate(`window.term.write('\\n')`);
assert.equal(await page.evaluate(`window.term.buffer.length`), 6);
await page.evaluate(`window.term.write('\\n\\n\\n\\n')`);
assert.equal(await page.evaluate(`window.term.buffer.length`), 10);
});

describe('getLine', () => {
it('isWrapped', async function(): Promise<any> {
this.timeout(10000);
await openTerminal({ cols: 5 });
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).isWrapped`), false);
assert.equal(await page.evaluate(`window.term.buffer.getLine(1).isWrapped`), false);
await page.evaluate(`window.term.write('abcde')`);
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).isWrapped`), false);
assert.equal(await page.evaluate(`window.term.buffer.getLine(1).isWrapped`), false);
await page.evaluate(`window.term.write('f')`);
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).isWrapped`), false);
assert.equal(await page.evaluate(`window.term.buffer.getLine(1).isWrapped`), true);
});

it('translateToString', async function(): Promise<any> {
this.timeout(10000);
await openTerminal({ cols: 5 });
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString()`), ' ');
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), '');
await page.evaluate(`window.term.write('foo')`);
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString()`), 'foo ');
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foo');
await page.evaluate(`window.term.write('bar')`);
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString()`), 'fooba');
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'fooba');
assert.equal(await page.evaluate(`window.term.buffer.getLine(1).translateToString(true)`), 'r');
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(false, 1)`), 'ooba');
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(false, 1, 3)`), 'oo');
});

it('getCell', async function(): Promise<any> {
this.timeout(10000);
await openTerminal();
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(0).char`), '');
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(0).width`), 1);
await page.evaluate(`window.term.write('a文')`);
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(0).char`), 'a');
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(0).width`), 1);
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(1).char`), '文');
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(1).width`), 2);
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(2).char`), '');
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(2).width`), 0);
});
});
});
});

async function openTerminal(options: ITerminalOptions = {}): Promise<void> {
Expand Down
33 changes: 31 additions & 2 deletions src/public/Terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
* @license MIT
*/

import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings } from 'xterm';
import { ITerminal } from '../Types';
import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi } from 'xterm';
import { ITerminal, IBuffer } from '../Types';
import { IBufferLine } from '../core/Types';
import { Terminal as TerminalCore } from '../Terminal';
import * as Strings from '../Strings';
import { IEvent } from '../common/EventEmitter2';
Expand All @@ -30,6 +31,7 @@ export class Terminal implements ITerminalApi {
public get textarea(): HTMLTextAreaElement { return this._core.textarea; }
public get rows(): number { return this._core.rows; }
public get cols(): number { return this._core.cols; }
public get buffer(): IBufferApi { return new BufferApiView(this._core.buffer); }
public get markers(): ReadonlyArray<IMarker> { return this._core.markers; }
public blur(): void {
this._core.blur();
Expand Down Expand Up @@ -169,3 +171,30 @@ export class Terminal implements ITerminalApi {
return Strings;
}
}

class BufferApiView implements IBufferApi {
constructor(private _buffer: IBuffer) {}

public get cursorY(): number { return this._buffer.y; }
public get cursorX(): number { return this._buffer.x; }
public get viewportY(): number { return this._buffer.ydisp; }
public get baseY(): number { return this._buffer.ybase; }
public get length(): number { return this._buffer.lines.length; }
public getLine(y: number): IBufferLineApi { return new BufferLineApiView(this._buffer.lines.get(y)); }
Tyriar marked this conversation as resolved.
Show resolved Hide resolved
}

class BufferLineApiView implements IBufferLineApi {
constructor(private _line: IBufferLine) {}

public get isWrapped(): boolean { return this._line.isWrapped; }
public getCell(x: number): IBufferCellApi { return new BufferCellApiView(this._line, x); }
Tyriar marked this conversation as resolved.
Show resolved Hide resolved
public translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string {
return this._line.translateToString(trimRight, startColumn, endColumn);
}
}

class BufferCellApiView implements IBufferCellApi {
constructor(private _line: IBufferLine, private _x: number) {}
public get char(): string { return this._line.getString(this._x); }
public get width(): number { return this._line.getWidth(this._x); }
}
4 changes: 2 additions & 2 deletions src/renderer/dom/DomRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,8 @@ export class DomRenderer extends Disposable implements IRenderer {
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} {` +
` color: ${this.colorManager.colors.foreground.css};` +
` background-color: ${this.colorManager.colors.background.css};` +
` font-family: ${this._terminal.getOption('fontFamily')};` +
` font-size: ${this._terminal.getOption('fontSize')}px;` +
` font-family: ${this._terminal.options.fontFamily};` +
` font-size: ${this._terminal.options.fontSize}px;` +
`}`;
// Text styles
styles +=
Expand Down
87 changes: 86 additions & 1 deletion typings/xterm.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ declare module 'xterm' {
* A callback that fires when the mouse leaves a link. Note that this can
* happen even when tooltipCallback hasn't fired for the link yet.
*/
leaveCallback?: (event: MouseEvent, uri: string) => boolean | void;
leaveCallback?: () => void;

/**
* The priority of the link matcher, this defines the order in which the link
Expand Down Expand Up @@ -355,6 +355,13 @@ declare module 'xterm' {
*/
readonly cols: number;

/**
* (EXPERIMENTAL) The terminal's current buffer, note that this might be
* either the normal buffer or the alt buffer depending on what's running in
* the terminal.
*/
readonly buffer: IBuffer;

/**
* (EXPERIMENTAL) Get all markers registered against the buffer. If the alt
* buffer is active this will always return [].
Expand Down Expand Up @@ -856,4 +863,82 @@ declare module 'xterm' {
*/
static applyAddon(addon: any): void;
}

interface IBuffer {
/**
* The y position of the cursor. This ranges between `0` (when the
* cursor is at baseY) and `Terminal.rows - 1` (when the cursor is on the
* last row).
*/
readonly cursorY: number;

/**
* The x position of the cursor. This ranges between `0` (left side) and
* `Terminal.cols - 1` (right side).
*/
readonly cursorX: number;

/**
* The line within the buffer where the top of the viewport is.
*/
readonly viewportY: number;

/**
* The line within the buffer where the top of the bottom page is (when
* fully scrolled down);
*/
readonly baseY: number;

/**
* The amount of lines in the buffer.
*/
readonly length: number;

/**
* Gets a line from the buffer.
*
* @param y The line index to get.
*/
getLine(y: number): IBufferLine;
}

interface IBufferLine {
/**
* Whether the line is wrapped from the previous line.
*/
readonly isWrapped: boolean;

/**
* Gets a cell from the line.
*
* @param x The character index to get.
*/
getCell(x: number): IBufferCell;

/**
* Gets the line as a string. Note that this is gets only the string for the line, not taking
* isWrapped into account.
*
* @param trimRight Whether to trim any whitespace at the right of the line.
* @param startColumn The column to start from (inclusive).
* @param endColumn The column to end at (exclusive).
*/
translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string;
}

interface IBufferCell {
/**
* The character within the cell.
*/
readonly char: string;

/**
* The width of the character. Some examples:
*
* - This is `1` for most cells.
* - This is `2` for wide character like CJK glyphs.
* - This is `0` for cells immediately following cells with a width of `2`.
*/
readonly width: number;
}
}
Loading