Merge branch 'master' into nodetarget
This commit is contained in:
@@ -158,7 +158,7 @@ export class CircularList<T> implements ICircularList<T> {
|
||||
this._array[this._getCyclicIndex(i)] = this._array[this._getCyclicIndex(i + deleteCount)];
|
||||
}
|
||||
this._length -= deleteCount;
|
||||
this.onDeleteEmitter.fire({index: start, amount: deleteCount});
|
||||
this.onDeleteEmitter.fire({ index: start, amount: deleteCount });
|
||||
}
|
||||
|
||||
// Add items
|
||||
@@ -169,7 +169,7 @@ export class CircularList<T> implements ICircularList<T> {
|
||||
this._array[this._getCyclicIndex(start + i)] = items[i];
|
||||
}
|
||||
if (items.length) {
|
||||
this.onInsertEmitter.fire({index: start, amount: items.length});
|
||||
this.onInsertEmitter.fire({ index: start, amount: items.length });
|
||||
}
|
||||
|
||||
// Adjust length as needed
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert, expect } from 'chai';
|
||||
import { assert } from 'chai';
|
||||
import { clone } from 'common/Clone';
|
||||
|
||||
describe('clone', () => {
|
||||
@@ -124,6 +124,6 @@ describe('clone', () => {
|
||||
|
||||
test.a.b.c = test;
|
||||
|
||||
expect(() => clone(test)).to.not.throw();
|
||||
assert.doesNotThrow(() => clone(test));
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ export function clone<T>(val: T, depth: number = 5): T {
|
||||
|
||||
for (const key in val) {
|
||||
// Recursively clone eack item unless we're at the maximum depth
|
||||
clonedObject[key] = depth <= 1 ? val[key] : (val[key] ? clone(val[key], depth - 1) : val[key]);
|
||||
clonedObject[key] = depth <= 1 ? val[key] : (val[key] && clone(val[key], depth - 1));
|
||||
}
|
||||
|
||||
return clonedObject as T;
|
||||
|
||||
+60
-107
@@ -22,12 +22,12 @@
|
||||
*/
|
||||
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, IDirtyRowService } from 'common/services/Services';
|
||||
import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, IDirtyRowService, LogLevelEnum } from 'common/services/Services';
|
||||
import { InstantiationService } from 'common/services/InstantiationService';
|
||||
import { LogService } from 'common/services/LogService';
|
||||
import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService';
|
||||
import { OptionsService } from 'common/services/OptionsService';
|
||||
import { ITerminalOptions, IDisposable, IBufferLine, IAttributeData, ICoreTerminal } from 'common/Types';
|
||||
import { ITerminalOptions, IDisposable, IBufferLine, IAttributeData, ICoreTerminal, IKeyboardEvent, IScrollEvent, ScrollSource } from 'common/Types';
|
||||
import { CoreService } from 'common/services/CoreService';
|
||||
import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter';
|
||||
import { CoreMouseService } from 'common/services/CoreMouseService';
|
||||
@@ -40,6 +40,9 @@ import { IBufferSet } from 'common/buffer/Types';
|
||||
import { InputHandler } from 'common/InputHandler';
|
||||
import { WriteBuffer } from 'common/input/WriteBuffer';
|
||||
|
||||
// Only trigger this warning a single time per session
|
||||
let hasWriteSyncWarnHappened = false;
|
||||
|
||||
export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
protected readonly _instantiationService: IInstantiationService;
|
||||
protected readonly _bufferService: IBufferService;
|
||||
@@ -55,8 +58,6 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
protected _inputHandler: InputHandler;
|
||||
private _writeBuffer: WriteBuffer;
|
||||
private _windowsMode: IDisposable | undefined;
|
||||
/** An IBufferline to clone/copy from for new blank lines */
|
||||
private _cachedBlankLine: IBufferLine | undefined;
|
||||
|
||||
private _onBinary = new EventEmitter<string>();
|
||||
public get onBinary(): IEvent<string> { return this._onBinary.event; }
|
||||
@@ -66,8 +67,21 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
public get onLineFeed(): IEvent<void> { return this._onLineFeed.event; }
|
||||
private _onResize = new EventEmitter<{ cols: number, rows: number }>();
|
||||
public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; }
|
||||
protected _onScroll = new EventEmitter<number>();
|
||||
public get onScroll(): IEvent<number> { return this._onScroll.event; }
|
||||
protected _onScroll = new EventEmitter<IScrollEvent, void>();
|
||||
/**
|
||||
* Internally we track the source of the scroll but this is meaningless outside the library so
|
||||
* it's filtered out.
|
||||
*/
|
||||
protected _onScrollApi?: EventEmitter<number, void>;
|
||||
public get onScroll(): IEvent<number, void> {
|
||||
if (!this._onScrollApi) {
|
||||
this._onScrollApi = new EventEmitter<number, void>();
|
||||
this.register(this._onScroll.event(ev => {
|
||||
this._onScrollApi?.fire(ev.position);
|
||||
}));
|
||||
}
|
||||
return this._onScrollApi.event;
|
||||
}
|
||||
|
||||
public get cols(): number { return this._bufferService.cols; }
|
||||
public get rows(): number { return this._bufferService.rows; }
|
||||
@@ -107,9 +121,17 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
this.register(forwardEvent(this._coreService.onData, this._onData));
|
||||
this.register(forwardEvent(this._coreService.onBinary, this._onBinary));
|
||||
this.register(this.optionsService.onOptionChange(key => this._updateOptions(key)));
|
||||
this.register(this._bufferService.onScroll(event => {
|
||||
this._onScroll.fire({ position: this._bufferService.buffer.ydisp, source: ScrollSource.TERMINAL });
|
||||
this._dirtyRowService.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);
|
||||
}));
|
||||
this.register(this._inputHandler.onScroll(event => {
|
||||
this._onScroll.fire({ position: this._bufferService.buffer.ydisp, source: ScrollSource.TERMINAL });
|
||||
this._dirtyRowService.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);
|
||||
}));
|
||||
|
||||
// Setup WriteBuffer
|
||||
this._writeBuffer = new WriteBuffer(data => this._inputHandler.parse(data));
|
||||
this._writeBuffer = new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
@@ -125,8 +147,21 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
this._writeBuffer.write(data, callback);
|
||||
}
|
||||
|
||||
public writeSync(data: string | Uint8Array): void {
|
||||
this._writeBuffer.writeSync(data);
|
||||
/**
|
||||
* Write data to terminal synchonously.
|
||||
*
|
||||
* This method is unreliable with async parser handlers, thus should not
|
||||
* be used anymore. If you need blocking semantics on data input consider
|
||||
* `write` with a callback instead.
|
||||
*
|
||||
* @deprecated Unreliable, will be removed soon.
|
||||
*/
|
||||
public writeSync(data: string | Uint8Array, maxSubsequentCalls?: number): void {
|
||||
if (this._logService.logLevel <= LogLevelEnum.WARN && !hasWriteSyncWarnHappened) {
|
||||
this._logService.warn('writeSync is unreliable and will be removed soon.');
|
||||
hasWriteSyncWarnHappened = true;
|
||||
}
|
||||
this._writeBuffer.writeSync(data, maxSubsequentCalls);
|
||||
}
|
||||
|
||||
public resize(x: number, y: number): void {
|
||||
@@ -145,66 +180,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
* @param isWrapped Whether the new line is wrapped from the previous line.
|
||||
*/
|
||||
public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void {
|
||||
const buffer = this._bufferService.buffer;
|
||||
|
||||
let newLine: IBufferLine | undefined;
|
||||
newLine = this._cachedBlankLine;
|
||||
if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) {
|
||||
newLine = buffer.getBlankLine(eraseAttr, isWrapped);
|
||||
this._cachedBlankLine = newLine;
|
||||
}
|
||||
newLine.isWrapped = isWrapped;
|
||||
|
||||
const topRow = buffer.ybase + buffer.scrollTop;
|
||||
const bottomRow = buffer.ybase + buffer.scrollBottom;
|
||||
|
||||
if (buffer.scrollTop === 0) {
|
||||
// Determine whether the buffer is going to be trimmed after insertion.
|
||||
const willBufferBeTrimmed = buffer.lines.isFull;
|
||||
|
||||
// Insert the line using the fastest method
|
||||
if (bottomRow === buffer.lines.length - 1) {
|
||||
if (willBufferBeTrimmed) {
|
||||
buffer.lines.recycle().copyFrom(newLine);
|
||||
} else {
|
||||
buffer.lines.push(newLine.clone());
|
||||
}
|
||||
} else {
|
||||
buffer.lines.splice(bottomRow + 1, 0, newLine.clone());
|
||||
}
|
||||
|
||||
// Only adjust ybase and ydisp when the buffer is not trimmed
|
||||
if (!willBufferBeTrimmed) {
|
||||
buffer.ybase++;
|
||||
// Only scroll the ydisp with ybase if the user has not scrolled up
|
||||
if (!this._bufferService.isUserScrolling) {
|
||||
buffer.ydisp++;
|
||||
}
|
||||
} else {
|
||||
// When the buffer is full and the user has scrolled up, keep the text
|
||||
// stable unless ydisp is right at the top
|
||||
if (this._bufferService.isUserScrolling) {
|
||||
buffer.ydisp = Math.max(buffer.ydisp - 1, 0);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// scrollTop is non-zero which means no line will be going to the
|
||||
// scrollback, instead we can just shift them in-place.
|
||||
const scrollRegionHeight = bottomRow - topRow + 1 /* as it's zero-based */;
|
||||
buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1);
|
||||
buffer.lines.set(bottomRow, newLine.clone());
|
||||
}
|
||||
|
||||
// Move the viewport to the bottom of the buffer unless the user is
|
||||
// scrolling.
|
||||
if (!this._bufferService.isUserScrolling) {
|
||||
buffer.ydisp = buffer.ybase;
|
||||
}
|
||||
|
||||
// Flag rows that need updating
|
||||
this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom);
|
||||
|
||||
this._onScroll.fire(buffer.ydisp);
|
||||
this._bufferService.scroll(eraseAttr, isWrapped);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -214,28 +190,8 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
* to avoid unwanted events being handled by the viewport when the event was triggered from the
|
||||
* viewport originally.
|
||||
*/
|
||||
public scrollLines(disp: number, suppressScrollEvent?: boolean): void {
|
||||
const buffer = this._bufferService.buffer;
|
||||
if (disp < 0) {
|
||||
if (buffer.ydisp === 0) {
|
||||
return;
|
||||
}
|
||||
this._bufferService.isUserScrolling = true;
|
||||
} else if (disp + buffer.ydisp >= buffer.ybase) {
|
||||
this._bufferService.isUserScrolling = false;
|
||||
}
|
||||
|
||||
const oldYdisp = buffer.ydisp;
|
||||
buffer.ydisp = Math.max(Math.min(buffer.ydisp + disp, buffer.ybase), 0);
|
||||
|
||||
// No change occurred, don't trigger scroll/refresh
|
||||
if (oldYdisp === buffer.ydisp) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!suppressScrollEvent) {
|
||||
this._onScroll.fire(buffer.ydisp);
|
||||
}
|
||||
public scrollLines(disp: number, suppressScrollEvent?: boolean, source?: ScrollSource): void {
|
||||
this._bufferService.scrollLines(disp, suppressScrollEvent, source);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -243,48 +199,45 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
* @param pageCount The number of pages to scroll (negative scrolls up).
|
||||
*/
|
||||
public scrollPages(pageCount: number): void {
|
||||
this.scrollLines(pageCount * (this.rows - 1));
|
||||
this._bufferService.scrollPages(pageCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls the display of the terminal to the top.
|
||||
*/
|
||||
public scrollToTop(): void {
|
||||
this.scrollLines(-this._bufferService.buffer.ydisp);
|
||||
this._bufferService.scrollToTop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls the display of the terminal to the bottom.
|
||||
*/
|
||||
public scrollToBottom(): void {
|
||||
this.scrollLines(this._bufferService.buffer.ybase - this._bufferService.buffer.ydisp);
|
||||
this._bufferService.scrollToBottom();
|
||||
}
|
||||
|
||||
public scrollToLine(line: number): void {
|
||||
const scrollAmount = line - this._bufferService.buffer.ydisp;
|
||||
if (scrollAmount !== 0) {
|
||||
this.scrollLines(scrollAmount);
|
||||
}
|
||||
this._bufferService.scrollToLine(line);
|
||||
}
|
||||
|
||||
/** Add handler for ESC escape sequence. See xterm.d.ts for details. */
|
||||
public addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable {
|
||||
return this._inputHandler.addEscHandler(id, callback);
|
||||
public registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise<boolean>): IDisposable {
|
||||
return this._inputHandler.registerEscHandler(id, callback);
|
||||
}
|
||||
|
||||
/** Add handler for DCS escape sequence. See xterm.d.ts for details. */
|
||||
public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable {
|
||||
return this._inputHandler.addDcsHandler(id, callback);
|
||||
public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable {
|
||||
return this._inputHandler.registerDcsHandler(id, callback);
|
||||
}
|
||||
|
||||
/** Add handler for CSI escape sequence. See xterm.d.ts for details. */
|
||||
public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable {
|
||||
return this._inputHandler.addCsiHandler(id, callback);
|
||||
public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise<boolean>): IDisposable {
|
||||
return this._inputHandler.registerCsiHandler(id, callback);
|
||||
}
|
||||
|
||||
/** Add handler for OSC escape sequence. See xterm.d.ts for details. */
|
||||
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
|
||||
return this._inputHandler.addOscHandler(ident, callback);
|
||||
public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
|
||||
return this._inputHandler.registerOscHandler(ident, callback);
|
||||
}
|
||||
|
||||
protected _setup(): void {
|
||||
@@ -322,7 +275,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
|
||||
if (!this._windowsMode) {
|
||||
const disposables: IDisposable[] = [];
|
||||
disposables.push(this.onLineFeed(updateWindowsModeWrappedState.bind(null, this._bufferService)));
|
||||
disposables.push(this.addCsiHandler({ final: 'H' }, () => {
|
||||
disposables.push(this.registerCsiHandler({ final: 'H' }, () => {
|
||||
updateWindowsModeWrappedState(this._bufferService);
|
||||
return false;
|
||||
}));
|
||||
|
||||
+926
-555
File diff suppressed because it is too large
Load Diff
+398
-202
File diff suppressed because it is too large
Load Diff
@@ -3,13 +3,13 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IPartialTerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider } from 'common/services/Services';
|
||||
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IPartialTerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum } from 'common/services/Services';
|
||||
import { IEvent, EventEmitter } from 'common/EventEmitter';
|
||||
import { clone } from 'common/Clone';
|
||||
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
|
||||
import { IBufferSet, IBuffer } from 'common/buffer/Types';
|
||||
import { BufferSet } from 'common/buffer/BufferSet';
|
||||
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes } from 'common/Types';
|
||||
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes, IAttributeData } from 'common/Types';
|
||||
import { UnicodeV6 } from 'common/input/UnicodeV6';
|
||||
|
||||
export class MockBufferService implements IBufferService {
|
||||
@@ -17,6 +17,7 @@ export class MockBufferService implements IBufferService {
|
||||
public get buffer(): IBuffer { return this.buffers.active; }
|
||||
public buffers: IBufferSet = {} as any;
|
||||
public onResize: IEvent<{ cols: number, rows: number }> = new EventEmitter<{ cols: number, rows: number }>().event;
|
||||
public onScroll: IEvent<number> = new EventEmitter<number>().event;
|
||||
public isUserScrolling: boolean = false;
|
||||
constructor(
|
||||
public cols: number,
|
||||
@@ -25,23 +26,41 @@ export class MockBufferService implements IBufferService {
|
||||
) {
|
||||
this.buffers = new BufferSet(optionsService, this);
|
||||
}
|
||||
public scrollPages(pageCount: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public scrollToTop(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public scrollToLine(line: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public scroll(eraseAttr: IAttributeData, isWrapped: boolean): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public scrollToBottom(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public scrollLines(disp: number, suppressScrollEvent?: boolean): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public resize(cols: number, rows: number): void {
|
||||
this.cols = cols;
|
||||
this.rows = rows;
|
||||
}
|
||||
public reset(): void {}
|
||||
public reset(): void { }
|
||||
}
|
||||
|
||||
export class MockCoreMouseService implements ICoreMouseService {
|
||||
public areMouseEventsActive: boolean = false;
|
||||
public activeEncoding: string = '';
|
||||
public activeProtocol: string = '';
|
||||
public addEncoding(name: string): void {}
|
||||
public addProtocol(name: string): void {}
|
||||
public reset(): void {}
|
||||
public addEncoding(name: string): void { }
|
||||
public addProtocol(name: string): void { }
|
||||
public reset(): void { }
|
||||
public triggerMouseEvent(event: ICoreMouseEvent): boolean { return false; }
|
||||
public onProtocolChange: IEvent<CoreMouseEventType> = new EventEmitter<CoreMouseEventType>().event;
|
||||
public explainEvents(events: CoreMouseEventType): {[event: string]: boolean} {
|
||||
public explainEvents(events: CoreMouseEventType): { [event: string]: boolean } {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
@@ -50,9 +69,9 @@ export class MockCharsetService implements ICharsetService {
|
||||
public serviceBrand: any;
|
||||
public charset: ICharset | undefined;
|
||||
public glevel: number = 0;
|
||||
public reset(): void {}
|
||||
public setgLevel(g: number): void {}
|
||||
public setgCharset(g: number, charset: ICharset): void {}
|
||||
public reset(): void { }
|
||||
public setgLevel(g: number): void { }
|
||||
public setgCharset(g: number, charset: ICharset): void { }
|
||||
}
|
||||
|
||||
export class MockCoreService implements ICoreService {
|
||||
@@ -75,27 +94,28 @@ export class MockCoreService implements ICoreService {
|
||||
public onData: IEvent<string> = new EventEmitter<string>().event;
|
||||
public onUserInput: IEvent<void> = new EventEmitter<void>().event;
|
||||
public onBinary: IEvent<string> = new EventEmitter<string>().event;
|
||||
public reset(): void {}
|
||||
public triggerDataEvent(data: string, wasUserInput?: boolean): void {}
|
||||
public triggerBinaryEvent(data: string): void {}
|
||||
public reset(): void { }
|
||||
public triggerDataEvent(data: string, wasUserInput?: boolean): void { }
|
||||
public triggerBinaryEvent(data: string): void { }
|
||||
}
|
||||
|
||||
export class MockDirtyRowService implements IDirtyRowService {
|
||||
public serviceBrand: any;
|
||||
public start: number = 0;
|
||||
public end: number = 0;
|
||||
public clearRange(): void {}
|
||||
public markDirty(y: number): void {}
|
||||
public markRangeDirty(y1: number, y2: number): void {}
|
||||
public markAllDirty(): void {}
|
||||
public clearRange(): void { }
|
||||
public markDirty(y: number): void { }
|
||||
public markRangeDirty(y1: number, y2: number): void { }
|
||||
public markAllDirty(): void { }
|
||||
}
|
||||
|
||||
export class MockLogService implements ILogService {
|
||||
public serviceBrand: any;
|
||||
public debug(message: any, ...optionalParams: any[]): void {}
|
||||
public info(message: any, ...optionalParams: any[]): void {}
|
||||
public warn(message: any, ...optionalParams: any[]): void {}
|
||||
public error(message: any, ...optionalParams: any[]): void {}
|
||||
public logLevel = LogLevelEnum.DEBUG;
|
||||
public debug(message: any, ...optionalParams: any[]): void { }
|
||||
public info(message: any, ...optionalParams: any[]): void { }
|
||||
public warn(message: any, ...optionalParams: any[]): void { }
|
||||
public error(message: any, ...optionalParams: any[]): void { }
|
||||
}
|
||||
|
||||
export class MockOptionsService implements IOptionsService {
|
||||
@@ -105,7 +125,7 @@ export class MockOptionsService implements IOptionsService {
|
||||
constructor(testOptions?: IPartialTerminalOptions) {
|
||||
if (testOptions) {
|
||||
for (const key of Object.keys(testOptions)) {
|
||||
this.options[key] = (<any>testOptions)[key];
|
||||
this.options[key] = (testOptions as any)[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+46
-3
@@ -3,15 +3,21 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminalOptions as IPublicTerminalOptions } from 'xterm';
|
||||
import { IFunctionIdentifier, ITerminalOptions as IPublicTerminalOptions } from 'xterm';
|
||||
import { IEvent, IEventEmitter } from 'common/EventEmitter';
|
||||
import { IDeleteEvent, IInsertEvent } from 'common/CircularList';
|
||||
import { IParams } from 'common/parser/Types';
|
||||
import { IOptionsService, IUnicodeService } from 'common/services/Services';
|
||||
import { IBufferSet } from 'common/buffer/Types';
|
||||
|
||||
export interface ICoreTerminal {
|
||||
optionsService: IOptionsService;
|
||||
unicodeService: IUnicodeService;
|
||||
buffers: IBufferSet;
|
||||
registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise<boolean>): IDisposable;
|
||||
registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable;
|
||||
registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise<boolean>): IDisposable;
|
||||
registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
|
||||
}
|
||||
|
||||
export interface IDisposable {
|
||||
@@ -42,6 +48,16 @@ export interface IKeyboardEvent {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface IScrollEvent {
|
||||
position: number;
|
||||
source: ScrollSource;
|
||||
}
|
||||
|
||||
export const enum ScrollSource {
|
||||
TERMINAL,
|
||||
VIEWPORT,
|
||||
}
|
||||
|
||||
export interface ICircularList<T> {
|
||||
length: number;
|
||||
maxLength: number;
|
||||
@@ -107,6 +123,7 @@ export interface IAttributeData {
|
||||
isInvisible(): number;
|
||||
isItalic(): number;
|
||||
isDim(): number;
|
||||
isStrikethrough(): number;
|
||||
|
||||
// color modes
|
||||
getFgColorMode(): number;
|
||||
@@ -328,15 +345,32 @@ export interface IWindowOptions {
|
||||
setWinLines?: boolean;
|
||||
}
|
||||
|
||||
export interface IAnsiColorChangeEventColor {
|
||||
colorIndex: number;
|
||||
red: number;
|
||||
green: number;
|
||||
blue: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event fired for OSC 4 command - to change ANSI color based on its index.
|
||||
*/
|
||||
export interface IAnsiColorChangeEvent {
|
||||
colors: IAnsiColorChangeEventColor[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the parser and handles actions generated by the parser.
|
||||
*/
|
||||
export interface IInputHandler {
|
||||
onTitleChange: IEvent<string>;
|
||||
onRequestScroll: IEvent<IAttributeData, boolean | void>;
|
||||
|
||||
parse(data: string | Uint8Array): void;
|
||||
parse(data: string | Uint8Array, promiseResult?: boolean): void | Promise<boolean>;
|
||||
print(data: Uint32Array, start: number, end: number): void;
|
||||
registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise<boolean>): IDisposable;
|
||||
registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable;
|
||||
registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise<boolean>): IDisposable;
|
||||
registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
|
||||
|
||||
/** C0 BEL */ bell(): void;
|
||||
/** C0 LF */ lineFeed(): void;
|
||||
@@ -389,6 +423,7 @@ export interface IInputHandler {
|
||||
/** CSI ' ~ */ deleteColumns(params: IParams): void;
|
||||
/** OSC 0
|
||||
OSC 2 */ setTitle(data: string): void;
|
||||
/** OSC 4 */ setAnsiColor(data: string): void;
|
||||
/** ESC E */ nextLine(): void;
|
||||
/** ESC = */ keypadApplicationMode(): void;
|
||||
/** ESC > */ keypadNumericMode(): void;
|
||||
@@ -412,3 +447,11 @@ export interface IInputHandler {
|
||||
ESC ~ */ setgLevel(level: number): void;
|
||||
/** ESC # 8 */ screenAlignmentPattern(): void;
|
||||
}
|
||||
|
||||
interface IParseStack {
|
||||
paused: boolean;
|
||||
cursorStartX: number;
|
||||
cursorStartY: number;
|
||||
decodedLength: number;
|
||||
position: number;
|
||||
}
|
||||
|
||||
@@ -33,13 +33,14 @@ export class AttributeData implements IAttributeData {
|
||||
public extended = new ExtendedAttrs();
|
||||
|
||||
// flags
|
||||
public isInverse(): number { return this.fg & FgFlags.INVERSE; }
|
||||
public isBold(): number { return this.fg & FgFlags.BOLD; }
|
||||
public isUnderline(): number { return this.fg & FgFlags.UNDERLINE; }
|
||||
public isBlink(): number { return this.fg & FgFlags.BLINK; }
|
||||
public isInvisible(): number { return this.fg & FgFlags.INVISIBLE; }
|
||||
public isItalic(): number { return this.bg & BgFlags.ITALIC; }
|
||||
public isDim(): number { return this.bg & BgFlags.DIM; }
|
||||
public isInverse(): number { return this.fg & FgFlags.INVERSE; }
|
||||
public isBold(): number { return this.fg & FgFlags.BOLD; }
|
||||
public isUnderline(): number { return this.fg & FgFlags.UNDERLINE; }
|
||||
public isBlink(): number { return this.fg & FgFlags.BLINK; }
|
||||
public isInvisible(): number { return this.fg & FgFlags.INVISIBLE; }
|
||||
public isItalic(): number { return this.bg & BgFlags.ITALIC; }
|
||||
public isDim(): number { return this.bg & BgFlags.DIM; }
|
||||
public isStrikethrough(): number { return this.fg & FgFlags.STRIKETHROUGH; }
|
||||
|
||||
// color modes
|
||||
public getFgColorMode(): number { return this.fg & Attributes.CM_MASK; }
|
||||
|
||||
@@ -671,6 +671,6 @@ export class BufferStringIterator implements IBufferStringIterator {
|
||||
content += this._buffer.translateBufferLineToString(i, this._trimRight);
|
||||
}
|
||||
this._current = range.last + 1;
|
||||
return {range, content};
|
||||
return { range, content };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR, Content,
|
||||
import { BufferLine } from 'common/buffer//BufferLine';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { CharData, IBufferLine } from '../Types';
|
||||
import { assert, expect } from 'chai';
|
||||
import { assert } from 'chai';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
|
||||
|
||||
@@ -151,20 +151,20 @@ describe('CellData', () => {
|
||||
describe('BufferLine', function(): void {
|
||||
it('ctor', function(): void {
|
||||
let line: IBufferLine = new TestBufferLine(0);
|
||||
expect(line.length).equals(0);
|
||||
expect(line.isWrapped).equals(false);
|
||||
assert.equal(line.length, 0);
|
||||
assert.equal(line.isWrapped, false);
|
||||
line = new TestBufferLine(10);
|
||||
expect(line.length).equals(10);
|
||||
expect(line.loadCell(0, new CellData()).getAsCharData()).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
expect(line.isWrapped).equals(false);
|
||||
assert.equal(line.length, 10);
|
||||
assert.deepEqual(line.loadCell(0, new CellData()).getAsCharData(), [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
assert.equal(line.isWrapped, false);
|
||||
line = new TestBufferLine(10, undefined, true);
|
||||
expect(line.length).equals(10);
|
||||
expect(line.loadCell(0, new CellData()).getAsCharData()).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
expect(line.isWrapped).equals(true);
|
||||
assert.equal(line.length, 10);
|
||||
assert.deepEqual(line.loadCell(0, new CellData()).getAsCharData(), [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
assert.equal(line.isWrapped, true);
|
||||
line = new TestBufferLine(10, CellData.fromCharData([123, 'a', 456, 'a'.charCodeAt(0)]), true);
|
||||
expect(line.length).equals(10);
|
||||
expect(line.loadCell(0, new CellData()).getAsCharData()).eql([123, 'a', 456, 'a'.charCodeAt(0)]);
|
||||
expect(line.isWrapped).equals(true);
|
||||
assert.equal(line.length, 10);
|
||||
assert.deepEqual(line.loadCell(0, new CellData()).getAsCharData(), [123, 'a', 456, 'a'.charCodeAt(0)]);
|
||||
assert.equal(line.isWrapped, true);
|
||||
});
|
||||
it('insertCells', function(): void {
|
||||
const line = new TestBufferLine(3);
|
||||
@@ -172,7 +172,7 @@ describe('BufferLine', function(): void {
|
||||
line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
line.insertCells(1, 3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
|
||||
expect(line.toArray()).eql([
|
||||
assert.deepEqual(line.toArray(), [
|
||||
[1, 'a', 0, 'a'.charCodeAt(0)],
|
||||
[4, 'd', 0, 'd'.charCodeAt(0)],
|
||||
[4, 'd', 0, 'd'.charCodeAt(0)]
|
||||
@@ -186,7 +186,7 @@ describe('BufferLine', function(): void {
|
||||
line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
|
||||
line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)]));
|
||||
line.deleteCells(1, 2, CellData.fromCharData([6, 'f', 0, 'f'.charCodeAt(0)]));
|
||||
expect(line.toArray()).eql([
|
||||
assert.deepEqual(line.toArray(), [
|
||||
[1, 'a', 0, 'a'.charCodeAt(0)],
|
||||
[4, 'd', 0, 'd'.charCodeAt(0)],
|
||||
[5, 'e', 0, 'e'.charCodeAt(0)],
|
||||
@@ -202,7 +202,7 @@ describe('BufferLine', function(): void {
|
||||
line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
|
||||
line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)]));
|
||||
line.replaceCells(2, 4, CellData.fromCharData([6, 'f', 0, 'f'.charCodeAt(0)]));
|
||||
expect(line.toArray()).eql([
|
||||
assert.deepEqual(line.toArray(), [
|
||||
[1, 'a', 0, 'a'.charCodeAt(0)],
|
||||
[2, 'b', 0, 'b'.charCodeAt(0)],
|
||||
[6, 'f', 0, 'f'.charCodeAt(0)],
|
||||
@@ -218,7 +218,7 @@ describe('BufferLine', function(): void {
|
||||
line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
|
||||
line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)]));
|
||||
line.fill(CellData.fromCharData([123, 'z', 0, 'z'.charCodeAt(0)]));
|
||||
expect(line.toArray()).eql([
|
||||
assert.deepEqual(line.toArray(), [
|
||||
[123, 'z', 0, 'z'.charCodeAt(0)],
|
||||
[123, 'z', 0, 'z'.charCodeAt(0)],
|
||||
[123, 'z', 0, 'z'.charCodeAt(0)],
|
||||
@@ -234,9 +234,9 @@ describe('BufferLine', function(): void {
|
||||
line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
|
||||
line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)]));
|
||||
const line2 = line.clone();
|
||||
expect(TestBufferLine.prototype.toArray.apply(line2)).eql(line.toArray());
|
||||
expect(line2.length).equals(line.length);
|
||||
expect(line2.isWrapped).equals(line.isWrapped);
|
||||
assert.deepEqual(TestBufferLine.prototype.toArray.apply(line2), line.toArray());
|
||||
assert.equal(line2.length, line.length);
|
||||
assert.equal(line2.isWrapped, line.isWrapped);
|
||||
});
|
||||
it('copyFrom', function(): void {
|
||||
const line = new TestBufferLine(5);
|
||||
@@ -247,92 +247,92 @@ describe('BufferLine', function(): void {
|
||||
line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)]));
|
||||
const line2 = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), true);
|
||||
line2.copyFrom(line);
|
||||
expect(line2.toArray()).eql(line.toArray());
|
||||
expect(line2.length).equals(line.length);
|
||||
expect(line2.isWrapped).equals(line.isWrapped);
|
||||
assert.deepEqual(line2.toArray(), line.toArray());
|
||||
assert.equal(line2.length, line.length);
|
||||
assert.equal(line2.isWrapped, line.isWrapped);
|
||||
});
|
||||
it('should support combining chars', function(): void {
|
||||
// CHAR_DATA_CODE_INDEX resembles current behavior in InputHandler.print
|
||||
// --> set code to the last charCodeAt value of the string
|
||||
// Note: needs to be fixed once the string pointer is in place
|
||||
const line = new TestBufferLine(2, CellData.fromCharData([1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]));
|
||||
expect(line.toArray()).eql([[1, 'e\u0301', 0, '\u0301'.charCodeAt(0)], [1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]]);
|
||||
assert.deepEqual(line.toArray(), [[1, 'e\u0301', 0, '\u0301'.charCodeAt(0)], [1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]]);
|
||||
const line2 = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, '\u0301'.charCodeAt(0)]), true);
|
||||
line2.copyFrom(line);
|
||||
expect(line2.toArray()).eql(line.toArray());
|
||||
assert.deepEqual(line2.toArray(), line.toArray());
|
||||
const line3 = line.clone();
|
||||
expect(TestBufferLine.prototype.toArray.apply(line3)).eql(line.toArray());
|
||||
assert.deepEqual(TestBufferLine.prototype.toArray.apply(line3), line.toArray());
|
||||
});
|
||||
describe('resize', function(): void {
|
||||
it('enlarge(false)', function(): void {
|
||||
const line = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false);
|
||||
line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
expect(line.toArray()).eql((Array(10) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
assert.deepEqual(line.toArray(), (Array(10) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
it('enlarge(true)', function(): void {
|
||||
const line = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false);
|
||||
line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
expect(line.toArray()).eql((Array(10) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
assert.deepEqual(line.toArray(), (Array(10) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
it('shrink(true) - should apply new size', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false);
|
||||
line.resize(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
expect(line.toArray()).eql((Array(5) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
assert.deepEqual(line.toArray(), (Array(5) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
it('shrink to 0 length', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false);
|
||||
line.resize(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
expect(line.toArray()).eql((Array(0) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
assert.deepEqual(line.toArray(), (Array(0) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
it('should remove combining data on replaced cells after shrinking then enlarging', () => {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false);
|
||||
line.set(2, [ 0, '😁', 1, '😁'.charCodeAt(0) ]);
|
||||
line.set(9, [ 0, '😁', 1, '😁'.charCodeAt(0) ]);
|
||||
expect(line.translateToString()).eql('aa😁aaaaaa😁');
|
||||
expect(Object.keys(line.combined).length).eql(2);
|
||||
assert.equal(line.translateToString(), 'aa😁aaaaaa😁');
|
||||
assert.equal(Object.keys(line.combined).length, 2);
|
||||
line.resize(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
expect(line.translateToString()).eql('aa😁aa');
|
||||
assert.equal(line.translateToString(), 'aa😁aa');
|
||||
line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
expect(line.translateToString()).eql('aa😁aaaaaaa');
|
||||
expect(Object.keys(line.combined).length).eql(1);
|
||||
assert.equal(line.translateToString(), 'aa😁aaaaaaa');
|
||||
assert.equal(Object.keys(line.combined).length, 1);
|
||||
});
|
||||
});
|
||||
describe('getTrimLength', function(): void {
|
||||
it('empty line', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
expect(line.getTrimmedLength()).equal(0);
|
||||
assert.equal(line.getTrimmedLength(), 0);
|
||||
});
|
||||
it('ASCII', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
expect(line.getTrimmedLength()).equal(3);
|
||||
assert.equal(line.getTrimmedLength(), 3);
|
||||
});
|
||||
it('surrogate', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)]));
|
||||
expect(line.getTrimmedLength()).equal(3);
|
||||
assert.equal(line.getTrimmedLength(), 3);
|
||||
});
|
||||
it('combining', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]));
|
||||
expect(line.getTrimmedLength()).equal(3);
|
||||
assert.equal(line.getTrimmedLength(), 3);
|
||||
});
|
||||
it('fullwidth', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)]));
|
||||
line.setCell(3, CellData.fromCharData([0, '', 0, 0]));
|
||||
expect(line.getTrimmedLength()).equal(4); // also counts null cell after fullwidth
|
||||
assert.equal(line.getTrimmedLength(), 4); // also counts null cell after fullwidth
|
||||
});
|
||||
});
|
||||
describe('translateToString with and w\'o trimming', function(): void {
|
||||
it('empty line', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
expect(line.translateToString(false)).equal(' ');
|
||||
expect(line.translateToString(true)).equal('');
|
||||
assert.equal(line.translateToString(false), ' ');
|
||||
assert.equal(line.translateToString(true), '');
|
||||
});
|
||||
it('ASCII', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
@@ -340,14 +340,14 @@ describe('BufferLine', function(): void {
|
||||
line.setCell(2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(4, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(5, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
expect(line.translateToString(false)).equal('a a aa ');
|
||||
expect(line.translateToString(true)).equal('a a aa');
|
||||
expect(line.translateToString(false, 0, 5)).equal('a a a');
|
||||
expect(line.translateToString(false, 0, 4)).equal('a a ');
|
||||
expect(line.translateToString(false, 0, 3)).equal('a a');
|
||||
expect(line.translateToString(true, 0, 5)).equal('a a a');
|
||||
expect(line.translateToString(true, 0, 4)).equal('a a ');
|
||||
expect(line.translateToString(true, 0, 3)).equal('a a');
|
||||
assert.equal(line.translateToString(false), 'a a aa ');
|
||||
assert.equal(line.translateToString(true), 'a a aa');
|
||||
assert.equal(line.translateToString(false, 0, 5), 'a a a');
|
||||
assert.equal(line.translateToString(false, 0, 4), 'a a ');
|
||||
assert.equal(line.translateToString(false, 0, 3), 'a a');
|
||||
assert.equal(line.translateToString(true, 0, 5), 'a a a');
|
||||
assert.equal(line.translateToString(true, 0, 4), 'a a ');
|
||||
assert.equal(line.translateToString(true, 0, 3), 'a a');
|
||||
|
||||
});
|
||||
it('surrogate', function(): void {
|
||||
@@ -356,14 +356,14 @@ describe('BufferLine', function(): void {
|
||||
line.setCell(2, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)]));
|
||||
line.setCell(4, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)]));
|
||||
line.setCell(5, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)]));
|
||||
expect(line.translateToString(false)).equal('a 𝄞 𝄞𝄞 ');
|
||||
expect(line.translateToString(true)).equal('a 𝄞 𝄞𝄞');
|
||||
expect(line.translateToString(false, 0, 5)).equal('a 𝄞 𝄞');
|
||||
expect(line.translateToString(false, 0, 4)).equal('a 𝄞 ');
|
||||
expect(line.translateToString(false, 0, 3)).equal('a 𝄞');
|
||||
expect(line.translateToString(true, 0, 5)).equal('a 𝄞 𝄞');
|
||||
expect(line.translateToString(true, 0, 4)).equal('a 𝄞 ');
|
||||
expect(line.translateToString(true, 0, 3)).equal('a 𝄞');
|
||||
assert.equal(line.translateToString(false), 'a 𝄞 𝄞𝄞 ');
|
||||
assert.equal(line.translateToString(true), 'a 𝄞 𝄞𝄞');
|
||||
assert.equal(line.translateToString(false, 0, 5), 'a 𝄞 𝄞');
|
||||
assert.equal(line.translateToString(false, 0, 4), 'a 𝄞 ');
|
||||
assert.equal(line.translateToString(false, 0, 3), 'a 𝄞');
|
||||
assert.equal(line.translateToString(true, 0, 5), 'a 𝄞 𝄞');
|
||||
assert.equal(line.translateToString(true, 0, 4), 'a 𝄞 ');
|
||||
assert.equal(line.translateToString(true, 0, 3), 'a 𝄞');
|
||||
});
|
||||
it('combining', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
@@ -371,14 +371,14 @@ describe('BufferLine', function(): void {
|
||||
line.setCell(2, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]));
|
||||
line.setCell(4, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]));
|
||||
line.setCell(5, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]));
|
||||
expect(line.translateToString(false)).equal('a e\u0301 e\u0301e\u0301 ');
|
||||
expect(line.translateToString(true)).equal('a e\u0301 e\u0301e\u0301');
|
||||
expect(line.translateToString(false, 0, 5)).equal('a e\u0301 e\u0301');
|
||||
expect(line.translateToString(false, 0, 4)).equal('a e\u0301 ');
|
||||
expect(line.translateToString(false, 0, 3)).equal('a e\u0301');
|
||||
expect(line.translateToString(true, 0, 5)).equal('a e\u0301 e\u0301');
|
||||
expect(line.translateToString(true, 0, 4)).equal('a e\u0301 ');
|
||||
expect(line.translateToString(true, 0, 3)).equal('a e\u0301');
|
||||
assert.equal(line.translateToString(false), 'a e\u0301 e\u0301e\u0301 ');
|
||||
assert.equal(line.translateToString(true), 'a e\u0301 e\u0301e\u0301');
|
||||
assert.equal(line.translateToString(false, 0, 5), 'a e\u0301 e\u0301');
|
||||
assert.equal(line.translateToString(false, 0, 4), 'a e\u0301 ');
|
||||
assert.equal(line.translateToString(false, 0, 3), 'a e\u0301');
|
||||
assert.equal(line.translateToString(true, 0, 5), 'a e\u0301 e\u0301');
|
||||
assert.equal(line.translateToString(true, 0, 4), 'a e\u0301 ');
|
||||
assert.equal(line.translateToString(true, 0, 3), 'a e\u0301');
|
||||
});
|
||||
it('fullwidth', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
@@ -389,20 +389,20 @@ describe('BufferLine', function(): void {
|
||||
line.setCell(6, CellData.fromCharData([0, '', 0, 0]));
|
||||
line.setCell(7, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)]));
|
||||
line.setCell(8, CellData.fromCharData([0, '', 0, 0]));
|
||||
expect(line.translateToString(false)).equal('a 1 11 ');
|
||||
expect(line.translateToString(true)).equal('a 1 11');
|
||||
expect(line.translateToString(false, 0, 7)).equal('a 1 1');
|
||||
expect(line.translateToString(false, 0, 6)).equal('a 1 1');
|
||||
expect(line.translateToString(false, 0, 5)).equal('a 1 ');
|
||||
expect(line.translateToString(false, 0, 4)).equal('a 1');
|
||||
expect(line.translateToString(false, 0, 3)).equal('a 1');
|
||||
expect(line.translateToString(false, 0, 2)).equal('a ');
|
||||
expect(line.translateToString(true, 0, 7)).equal('a 1 1');
|
||||
expect(line.translateToString(true, 0, 6)).equal('a 1 1');
|
||||
expect(line.translateToString(true, 0, 5)).equal('a 1 ');
|
||||
expect(line.translateToString(true, 0, 4)).equal('a 1');
|
||||
expect(line.translateToString(true, 0, 3)).equal('a 1');
|
||||
expect(line.translateToString(true, 0, 2)).equal('a ');
|
||||
assert.equal(line.translateToString(false), 'a 1 11 ');
|
||||
assert.equal(line.translateToString(true), 'a 1 11');
|
||||
assert.equal(line.translateToString(false, 0, 7), 'a 1 1');
|
||||
assert.equal(line.translateToString(false, 0, 6), 'a 1 1');
|
||||
assert.equal(line.translateToString(false, 0, 5), 'a 1 ');
|
||||
assert.equal(line.translateToString(false, 0, 4), 'a 1');
|
||||
assert.equal(line.translateToString(false, 0, 3), 'a 1');
|
||||
assert.equal(line.translateToString(false, 0, 2), 'a ');
|
||||
assert.equal(line.translateToString(true, 0, 7), 'a 1 1');
|
||||
assert.equal(line.translateToString(true, 0, 6), 'a 1 1');
|
||||
assert.equal(line.translateToString(true, 0, 5), 'a 1 ');
|
||||
assert.equal(line.translateToString(true, 0, 4), 'a 1');
|
||||
assert.equal(line.translateToString(true, 0, 3), 'a 1');
|
||||
assert.equal(line.translateToString(true, 0, 2), 'a ');
|
||||
});
|
||||
it('space at end', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
@@ -411,21 +411,21 @@ describe('BufferLine', function(): void {
|
||||
line.setCell(4, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(5, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(6, CellData.fromCharData([1, ' ', 1, ' '.charCodeAt(0)]));
|
||||
expect(line.translateToString(false)).equal('a a aa ');
|
||||
expect(line.translateToString(true)).equal('a a aa ');
|
||||
assert.equal(line.translateToString(false), 'a a aa ');
|
||||
assert.equal(line.translateToString(true), 'a a aa ');
|
||||
});
|
||||
it('should always return some sane value', function(): void {
|
||||
// sanity check - broken line with invalid out of bound null width cells
|
||||
// this can atm happen with deleting/inserting chars in inputhandler by "breaking"
|
||||
// fullwidth pairs --> needs to be fixed after settling BufferLine impl
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
|
||||
expect(line.translateToString(false)).equal(' ');
|
||||
expect(line.translateToString(true)).equal('');
|
||||
assert.equal(line.translateToString(false), ' ');
|
||||
assert.equal(line.translateToString(true), '');
|
||||
});
|
||||
it('should work with endCol=0', () => {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
expect(line.translateToString(true, 0, 0)).equal('');
|
||||
assert.equal(line.translateToString(true, 0, 0), '');
|
||||
});
|
||||
});
|
||||
describe('addCharToCell', () => {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2021 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { getRangeLength } from 'common/buffer/BufferRange';
|
||||
import { IBufferRange } from 'xterm';
|
||||
|
||||
describe('BufferRange', () => {
|
||||
describe('getRangeLength', () => {
|
||||
it('should get range for single line', () => {
|
||||
assert.equal(getRangeLength(createRange(1, 1, 4, 1), 0), 4);
|
||||
});
|
||||
it('should throw for invalid range', () => {
|
||||
assert.throws(() => getRangeLength(createRange(1, 3, 1, 1), 0));
|
||||
});
|
||||
it('should get range multiple lines', () => {
|
||||
assert.equal(getRangeLength(createRange(1, 1, 4, 5), 5), 24);
|
||||
});
|
||||
it('should get range for end line right after start line', () => {
|
||||
assert.equal(getRangeLength(createRange(1, 1, 7, 2), 5), 12);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createRange(x1: number, y1: number, x2: number, y2: number): IBufferRange {
|
||||
return {
|
||||
start: { x: x1, y: y1 },
|
||||
end: { x: x2, y: y2 }
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) 2021 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IBufferRange } from 'xterm';
|
||||
|
||||
export function getRangeLength(range: IBufferRange, bufferCols: number): number {
|
||||
if (range.start.y > range.end.y) {
|
||||
throw new Error(`Buffer range end (${range.end.x}, ${range.end.y}) cannot be before start (${range.start.x}, ${range.start.y})`);
|
||||
}
|
||||
return bufferCols * (range.end.y - range.start.y) + (range.end.x - range.start.x + 1);
|
||||
}
|
||||
@@ -110,13 +110,14 @@ export const enum Attributes {
|
||||
|
||||
export const enum FgFlags {
|
||||
/**
|
||||
* bit 27..31 (32th bit unused)
|
||||
* bit 27..32
|
||||
*/
|
||||
INVERSE = 0x4000000,
|
||||
BOLD = 0x8000000,
|
||||
UNDERLINE = 0x10000000,
|
||||
BLINK = 0x20000000,
|
||||
INVISIBLE = 0x40000000
|
||||
INVISIBLE = 0x40000000,
|
||||
STRIKETHROUGH = 0x80000000,
|
||||
}
|
||||
|
||||
export const enum BgFlags {
|
||||
|
||||
@@ -356,7 +356,7 @@ export function evaluateKeyboardEvent(
|
||||
const keyCode = ev.ctrlKey ? ev.keyCode - 64 : ev.keyCode + 32;
|
||||
result.key = C0.ESC + String.fromCharCode(keyCode);
|
||||
}
|
||||
} else if (isMac && !ev.altKey && !ev.ctrlKey && ev.metaKey) {
|
||||
} else if (isMac && !ev.altKey && !ev.ctrlKey && !ev.shiftKey && ev.metaKey) {
|
||||
if (ev.keyCode === 65) { // cmd + a
|
||||
result.type = KeyboardResultType.SELECT_ALL;
|
||||
}
|
||||
|
||||
@@ -58,8 +58,8 @@ describe('text encodings', () => {
|
||||
const decoder = new StringToUtf32();
|
||||
const target = new Uint32Array(5);
|
||||
for (let i = 0; i < 65536; ++i) {
|
||||
// skip surrogate pairs
|
||||
if (i >= 0xD800 && i <= 0xDFFF) {
|
||||
// skip surrogate pairs and a BOM
|
||||
if ((i >= 0xD800 && i <= 0xDFFF) || i === 0xFEFF) {
|
||||
continue;
|
||||
}
|
||||
const length = decoder.decode(String.fromCharCode(i), target);
|
||||
@@ -84,6 +84,14 @@ describe('text encodings', () => {
|
||||
decoder.clear();
|
||||
}
|
||||
});
|
||||
|
||||
it('0xFEFF(BOM)', () => {
|
||||
const decoder = new StringToUtf32();
|
||||
const target = new Uint32Array(5);
|
||||
const length = decoder.decode(String.fromCharCode(0xFEFF), target);
|
||||
assert.equal(length, 0);
|
||||
decoder.clear();
|
||||
});
|
||||
});
|
||||
|
||||
it('test strings', () => {
|
||||
@@ -118,8 +126,8 @@ describe('text encodings', () => {
|
||||
const decoder = new Utf8ToUtf32();
|
||||
const target = new Uint32Array(5);
|
||||
for (let i = 0; i < 65536; ++i) {
|
||||
// skip surrogate pairs
|
||||
if (i >= 0xD800 && i <= 0xDFFF) {
|
||||
// skip surrogate pairs and a BOM
|
||||
if ((i >= 0xD800 && i <= 0xDFFF) || i === 0xFEFF) {
|
||||
continue;
|
||||
}
|
||||
const utf8Data = fromByteString(encode(String.fromCharCode(i)));
|
||||
@@ -142,6 +150,15 @@ describe('text encodings', () => {
|
||||
decoder.clear();
|
||||
}
|
||||
});
|
||||
|
||||
it('0xFEFF(BOM)', () => {
|
||||
const decoder = new Utf8ToUtf32();
|
||||
const target = new Uint32Array(5);
|
||||
const utf8Data = fromByteString(encode(String.fromCharCode(0xFEFF)));
|
||||
const length = decoder.decode(utf8Data, target);
|
||||
assert.equal(length, 0);
|
||||
decoder.clear();
|
||||
});
|
||||
});
|
||||
|
||||
it('test strings', () => {
|
||||
@@ -215,6 +232,19 @@ describe('text encodings', () => {
|
||||
}
|
||||
assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€');
|
||||
});
|
||||
|
||||
it('BOMs (3 byte sequences) - advance by 2', () => {
|
||||
const decoder = new Utf8ToUtf32();
|
||||
const target = new Uint32Array(5);
|
||||
const utf8Data = fromByteString('\xef\xbb\xbf\xef\xbb\xbf');
|
||||
let decoded = '';
|
||||
for (let i = 0; i < utf8Data.length; i += 2) {
|
||||
const written = decoder.decode(utf8Data.slice(i, i + 2), target);
|
||||
decoded += toString(target, written);
|
||||
}
|
||||
assert.equal(decoded, '');
|
||||
});
|
||||
|
||||
it('test break after 3 bytes - issue #2495', () => {
|
||||
const decoder = new Utf8ToUtf32();
|
||||
const target = new Uint32Array(5);
|
||||
|
||||
@@ -105,6 +105,10 @@ export class StringToUtf32 {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (code === 0xFEFF) {
|
||||
// BOM
|
||||
continue;
|
||||
}
|
||||
target[size++] = code;
|
||||
}
|
||||
return size;
|
||||
@@ -188,8 +192,8 @@ export class Utf8ToUtf32 {
|
||||
target[size++] = cp;
|
||||
}
|
||||
} else if (type === 3) {
|
||||
if (cp < 0x0800 || (cp >= 0xD800 && cp <= 0xDFFF)) {
|
||||
// illegal codepoint
|
||||
if (cp < 0x0800 || (cp >= 0xD800 && cp <= 0xDFFF) || cp === 0xFEFF) {
|
||||
// illegal codepoint or BOM
|
||||
} else {
|
||||
target[size++] = cp;
|
||||
}
|
||||
@@ -286,8 +290,8 @@ export class Utf8ToUtf32 {
|
||||
continue;
|
||||
}
|
||||
codepoint = (byte1 & 0x0F) << 12 | (byte2 & 0x3F) << 6 | (byte3 & 0x3F);
|
||||
if (codepoint < 0x0800 || (codepoint >= 0xD800 && codepoint <= 0xDFFF)) {
|
||||
// illegal codepoint, no i-- here
|
||||
if (codepoint < 0x0800 || (codepoint >= 0xD800 && codepoint <= 0xDFFF) || codepoint === 0xFEFF) {
|
||||
// illegal codepoint or BOM, no i-- here
|
||||
continue;
|
||||
}
|
||||
target[size++] = codepoint;
|
||||
|
||||
@@ -85,5 +85,26 @@ describe('WriteBuffer', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('writeSync called from action does not overflow callstack - issue #3265', () => {
|
||||
wb = new WriteBuffer(data => {
|
||||
const num = parseInt(data as string);
|
||||
if (num < 1000000) {
|
||||
wb.writeSync('' + (num + 1));
|
||||
}
|
||||
});
|
||||
wb.writeSync('1');
|
||||
});
|
||||
it('writeSync maxSubsequentCalls argument', () => {
|
||||
let last: string = '';
|
||||
wb = new WriteBuffer(data => {
|
||||
last = data as string;
|
||||
const num = parseInt(data as string);
|
||||
if (num < 1000000) {
|
||||
wb.writeSync('' + (num + 1), 10);
|
||||
}
|
||||
});
|
||||
wb.writeSync('1', 10);
|
||||
assert.equal(last, '11'); // 1 + 10 sub calls = 11
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+140
-26
@@ -31,33 +31,66 @@ const WRITE_TIMEOUT_MS = 12;
|
||||
*/
|
||||
const WRITE_BUFFER_LENGTH_THRESHOLD = 50;
|
||||
|
||||
// queueMicrotask polyfill for nodejs < v11
|
||||
const qmt: (cb: () => void) => void = (typeof queueMicrotask === 'undefined')
|
||||
? (cb: () => void) => { Promise.resolve().then(cb); }
|
||||
: queueMicrotask;
|
||||
|
||||
|
||||
export class WriteBuffer {
|
||||
private _writeBuffer: (string | Uint8Array)[] = [];
|
||||
private _callbacks: ((() => void) | undefined)[] = [];
|
||||
private _pendingData = 0;
|
||||
private _bufferOffset = 0;
|
||||
private _isSyncWriting = false;
|
||||
private _syncCalls = 0;
|
||||
|
||||
constructor(private _action: (data: string | Uint8Array) => void) { }
|
||||
constructor(private _action: (data: string | Uint8Array, promiseResult?: boolean) => void | Promise<boolean>) { }
|
||||
|
||||
/**
|
||||
* @deprecated Unreliable, to be removed soon.
|
||||
*/
|
||||
public writeSync(data: string | Uint8Array, maxSubsequentCalls?: number): void {
|
||||
// stop writeSync recursions with maxSubsequentCalls argument
|
||||
// This is dangerous to use as it will lose the current data chunk
|
||||
// and return immediately.
|
||||
if (maxSubsequentCalls !== undefined && this._syncCalls > maxSubsequentCalls) {
|
||||
// comment next line if a whole loop block should only contain x `writeSync` calls
|
||||
// (total flat vs. deep nested limit)
|
||||
this._syncCalls = 0;
|
||||
return;
|
||||
}
|
||||
// append chunk to buffer
|
||||
this._pendingData += data.length;
|
||||
this._writeBuffer.push(data);
|
||||
this._callbacks.push(undefined);
|
||||
|
||||
// increase recursion counter
|
||||
this._syncCalls++;
|
||||
// exit early if another writeSync loop is active
|
||||
if (this._isSyncWriting) {
|
||||
return;
|
||||
}
|
||||
this._isSyncWriting = true;
|
||||
|
||||
public writeSync(data: string | Uint8Array): void {
|
||||
// force sync processing on pending data chunks to avoid in-band data scrambling
|
||||
// does the same as innerWrite but without event loop
|
||||
if (this._writeBuffer.length) {
|
||||
for (let i = this._bufferOffset; i < this._writeBuffer.length; ++i) {
|
||||
const data = this._writeBuffer[i];
|
||||
const cb = this._callbacks[i];
|
||||
this._action(data);
|
||||
if (cb) cb();
|
||||
}
|
||||
// reset all to avoid reprocessing of chunks with scheduled innerWrite call
|
||||
this._writeBuffer = [];
|
||||
this._callbacks = [];
|
||||
this._pendingData = 0;
|
||||
// stop scheduled innerWrite by offset > length condition
|
||||
this._bufferOffset = 0x7FFFFFFF;
|
||||
// we have to do it here as single loop steps to not corrupt loop subject
|
||||
// by another writeSync call triggered from _action
|
||||
let chunk: string | Uint8Array | undefined;
|
||||
while (chunk = this._writeBuffer.shift()) {
|
||||
this._action(chunk);
|
||||
const cb = this._callbacks.shift();
|
||||
if (cb) cb();
|
||||
}
|
||||
// handle current data chunk
|
||||
this._action(data);
|
||||
// reset to avoid reprocessing of chunks with scheduled innerWrite call
|
||||
// stopping scheduled innerWrite by offset > length condition
|
||||
this._pendingData = 0;
|
||||
this._bufferOffset = 0x7FFFFFFF;
|
||||
|
||||
// allow another writeSync to loop
|
||||
this._isSyncWriting = false;
|
||||
this._syncCalls = 0;
|
||||
}
|
||||
|
||||
public write(data: string | Uint8Array, callback?: () => void): void {
|
||||
@@ -76,16 +109,97 @@ export class WriteBuffer {
|
||||
this._callbacks.push(callback);
|
||||
}
|
||||
|
||||
protected _innerWrite(): void {
|
||||
const startTime = Date.now();
|
||||
/**
|
||||
* Inner write call, that enters the sliced chunk processing by timing.
|
||||
*
|
||||
* `lastTime` indicates, when the last _innerWrite call had started.
|
||||
* It is used to aggregate async handler execution under a timeout constraint
|
||||
* effectively lowering the redrawing needs, schematically:
|
||||
*
|
||||
* macroTask _innerWrite:
|
||||
* if (Date.now() - (lastTime | 0) < WRITE_TIMEOUT_MS):
|
||||
* schedule microTask _innerWrite(lastTime)
|
||||
* else:
|
||||
* schedule macroTask _innerWrite(0)
|
||||
*
|
||||
* overall execution order on task queues:
|
||||
*
|
||||
* macrotasks: [...] --> _innerWrite(0) --> [...] --> screenUpdate --> [...]
|
||||
* m t: |
|
||||
* i a: [...]
|
||||
* c s: |
|
||||
* r k: while < timeout:
|
||||
* o s: _innerWrite(timeout)
|
||||
*
|
||||
* `promiseResult` depicts the promise resolve value of an async handler.
|
||||
* This value gets carried forward through all saved stack states of the
|
||||
* paused parser for proper continuation.
|
||||
*
|
||||
* Note, for pure sync code `lastTime` and `promiseResult` have no meaning.
|
||||
*/
|
||||
protected _innerWrite(lastTime: number = 0, promiseResult: boolean = true): void {
|
||||
const startTime = lastTime || Date.now();
|
||||
while (this._writeBuffer.length > this._bufferOffset) {
|
||||
const data = this._writeBuffer[this._bufferOffset];
|
||||
const cb = this._callbacks[this._bufferOffset];
|
||||
this._bufferOffset++;
|
||||
const result = this._action(data, promiseResult);
|
||||
if (result) {
|
||||
/**
|
||||
* If we get a promise as return value, we re-schedule the continuation
|
||||
* as thenable on the promise and exit right away.
|
||||
*
|
||||
* The exit here means, that we block input processing at the current active chunk,
|
||||
* the exact execution position within the chunk is preserved by the saved
|
||||
* stack content in InputHandler and EscapeSequenceParser.
|
||||
*
|
||||
* Resuming happens automatically from that saved stack state.
|
||||
* Also the resolved promise value is passed along the callstack to
|
||||
* `EscapeSequenceParser.parse` to correctly resume the stopped handler loop.
|
||||
*
|
||||
* Exceptions on async handlers will be logged to console async, but do not interrupt
|
||||
* the input processing (continues with next handler at the current input position).
|
||||
*/
|
||||
|
||||
this._action(data);
|
||||
this._pendingData -= data.length;
|
||||
/**
|
||||
* If a promise takes long to resolve, we should schedule continuation behind setTimeout.
|
||||
* This might already be too late, if our .then enters really late (executor + prev thens took very long).
|
||||
* This cannot be solved here for the handler itself (it is the handlers responsibility to slice hard work),
|
||||
* but we can at least schedule a screen update as we gain control.
|
||||
*/
|
||||
const continuation: (r: boolean) => void = (r: boolean) => Date.now() - startTime >= WRITE_TIMEOUT_MS
|
||||
? setTimeout(() => this._innerWrite(0, r))
|
||||
: this._innerWrite(startTime, r);
|
||||
|
||||
/**
|
||||
* Optimization considerations:
|
||||
* The continuation above favors FPS over throughput by eval'ing `startTime` on resolve.
|
||||
* This might schedule too many screen updates with bad throughput drops (in case a slow
|
||||
* resolving handler sliced its work properly behind setTimeout calls). We cannot spot
|
||||
* this condition here, also the renderer has no way to spot nonsense updates either.
|
||||
* FIXME: A proper fix for this would track the FPS at the renderer entry level separately.
|
||||
*
|
||||
* If favoring of FPS shows bad throughtput impact, use the following instead. It favors
|
||||
* throughput by eval'ing `startTime` upfront pulling at least one more chunk into the
|
||||
* current microtask queue (executed before setTimeout).
|
||||
*/
|
||||
// const continuation: (r: boolean) => void = Date.now() - startTime >= WRITE_TIMEOUT_MS
|
||||
// ? r => setTimeout(() => this._innerWrite(0, r))
|
||||
// : r => this._innerWrite(startTime, r);
|
||||
|
||||
// Handle exceptions synchronously to current band position, idea:
|
||||
// 1. spawn a single microtask which we allow to throw hard
|
||||
// 2. spawn a promise immediately resolving to `true`
|
||||
// (executed on the same queue, thus properly aligned before continuation happens)
|
||||
result.catch(err => {
|
||||
qmt(() => {throw err;});
|
||||
return Promise.resolve(false);
|
||||
}).then(continuation);
|
||||
return;
|
||||
}
|
||||
|
||||
const cb = this._callbacks[this._bufferOffset];
|
||||
if (cb) cb();
|
||||
this._bufferOffset++;
|
||||
this._pendingData -= data.length;
|
||||
|
||||
if (Date.now() - startTime >= WRITE_TIMEOUT_MS) {
|
||||
break;
|
||||
@@ -99,10 +213,10 @@ export class WriteBuffer {
|
||||
this._callbacks = this._callbacks.slice(this._bufferOffset);
|
||||
this._bufferOffset = 0;
|
||||
}
|
||||
setTimeout(() => this._innerWrite(), 0);
|
||||
setTimeout(() => this._innerWrite());
|
||||
} else {
|
||||
this._writeBuffer = [];
|
||||
this._callbacks = [];
|
||||
this._writeBuffer.length = 0;
|
||||
this._callbacks.length = 0;
|
||||
this._pendingData = 0;
|
||||
this._bufferOffset = 0;
|
||||
}
|
||||
|
||||
@@ -61,11 +61,12 @@ class TestHandler implements IDcsHandler {
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
this.output.push([this.msg, 'PUT', utf32ToString(data, start, end)]);
|
||||
}
|
||||
public unhook(success: boolean): void | boolean {
|
||||
public unhook(success: boolean): boolean {
|
||||
this.output.push([this.msg, 'UNHOOK', success]);
|
||||
if (this.returnFalse) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +85,7 @@ describe('DcsParser', () => {
|
||||
});
|
||||
describe('handler registration', () => {
|
||||
it('setDcsHandler', () => {
|
||||
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th'));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th'));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -100,7 +101,7 @@ describe('DcsParser', () => {
|
||||
]);
|
||||
});
|
||||
it('clearDcsHandler', () => {
|
||||
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th'));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th'));
|
||||
parser.clearHandler(identifier({intermediates: '+', final: 'p'}));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
@@ -117,8 +118,8 @@ describe('DcsParser', () => {
|
||||
]);
|
||||
});
|
||||
it('addDcsHandler', () => {
|
||||
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1'));
|
||||
parser.addHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2'));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1'));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2'));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -137,8 +138,8 @@ describe('DcsParser', () => {
|
||||
]);
|
||||
});
|
||||
it('addDcsHandler with return false', () => {
|
||||
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1'));
|
||||
parser.addHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1'));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -157,8 +158,8 @@ describe('DcsParser', () => {
|
||||
]);
|
||||
});
|
||||
it('dispose handlers', () => {
|
||||
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1'));
|
||||
const dispo = parser.addHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1'));
|
||||
const dispo = parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true));
|
||||
dispo.dispose();
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
@@ -176,7 +177,7 @@ describe('DcsParser', () => {
|
||||
});
|
||||
describe('DcsHandlerFactory', () => {
|
||||
it('should be called once on end(true)', () => {
|
||||
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data])));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push([params.toArray(), data]); return true; }));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -186,7 +187,7 @@ describe('DcsParser', () => {
|
||||
assert.deepEqual(reports, [[[1, 2, 3], 'Here comes the mouse!']]);
|
||||
});
|
||||
it('should not be called on end(false)', () => {
|
||||
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data])));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push([params.toArray(), data]); return true; }));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -196,8 +197,8 @@ describe('DcsParser', () => {
|
||||
assert.deepEqual(reports, []);
|
||||
});
|
||||
it('should be disposable', () => {
|
||||
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push(['one', params.toArray(), data])));
|
||||
const dispo = parser.addHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push(['two', params.toArray(), data])));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push(['one', params.toArray(), data]); return true; }));
|
||||
const dispo = parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push(['two', params.toArray(), data]); return true; }));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -215,8 +216,8 @@ describe('DcsParser', () => {
|
||||
assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'some other data']]);
|
||||
});
|
||||
it('should respect return false', () => {
|
||||
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push(['one', params.toArray(), data])));
|
||||
parser.addHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push(['two', params.toArray(), data]); return false; }));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push(['one', params.toArray(), data]); return true; }));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push(['two', params.toArray(), data]); return false; }));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -227,7 +228,7 @@ describe('DcsParser', () => {
|
||||
});
|
||||
it('should work up to payload limit', function(): void {
|
||||
this.timeout(10000);
|
||||
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data])));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push([params.toArray(), data]); return true; }));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
const data = toUtf32('A'.repeat(1000));
|
||||
for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) {
|
||||
@@ -238,7 +239,7 @@ describe('DcsParser', () => {
|
||||
});
|
||||
it('should abort for payload limit +1', function(): void {
|
||||
this.timeout(10000);
|
||||
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data])));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push([params.toArray(), data]); return true; }));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('A'.repeat(1000));
|
||||
for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) {
|
||||
@@ -251,3 +252,208 @@ describe('DcsParser', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
class TestHandlerAsync implements IDcsHandler {
|
||||
constructor(public output: any[], public msg: string, public returnFalse: boolean = false) {}
|
||||
public hook(params: IParams): void {
|
||||
this.output.push([this.msg, 'HOOK', params.toArray()]);
|
||||
}
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
this.output.push([this.msg, 'PUT', utf32ToString(data, start, end)]);
|
||||
}
|
||||
public async unhook(success: boolean): Promise<boolean> {
|
||||
// simple sleep to check in tests whether ordering gets messed up
|
||||
await new Promise(res => setTimeout(res, 20));
|
||||
this.output.push([this.msg, 'UNHOOK', success]);
|
||||
if (this.returnFalse) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
async function unhookP(parser: DcsParser, success: boolean): Promise<void> {
|
||||
let result: void | Promise<boolean>;
|
||||
let prev: boolean | undefined;
|
||||
while (result = parser.unhook(success, prev)) {
|
||||
prev = await result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
describe('DcsParser - async tests', () => {
|
||||
let parser: DcsParser;
|
||||
let reports: any[] = [];
|
||||
beforeEach(() => {
|
||||
reports = [];
|
||||
parser = new DcsParser();
|
||||
parser.setHandlerFallback((id, action, data) => {
|
||||
if (action === 'HOOK') {
|
||||
data = data.toArray();
|
||||
}
|
||||
reports.push([id, action, data]);
|
||||
});
|
||||
});
|
||||
describe('sync and async mixed', () => {
|
||||
describe('sync | async | sync', () => {
|
||||
it('first should run, cleanup action for others', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', false));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', false));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's2', false));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await unhookP(parser, true);
|
||||
assert.deepEqual(reports, [
|
||||
// messages from TestHandler
|
||||
['s2', 'HOOK', [1, 2, 3]],
|
||||
['a1', 'HOOK', [1, 2, 3]],
|
||||
['s1', 'HOOK', [1, 2, 3]],
|
||||
['s2', 'PUT', 'Here comes'],
|
||||
['a1', 'PUT', 'Here comes'],
|
||||
['s1', 'PUT', 'Here comes'],
|
||||
['s2', 'PUT', 'the mouse!'],
|
||||
['a1', 'PUT', 'the mouse!'],
|
||||
['s1', 'PUT', 'the mouse!'],
|
||||
['s2', 'UNHOOK', true],
|
||||
['a1', 'UNHOOK', false], // important: a1 before s1
|
||||
['s1', 'UNHOOK', false]
|
||||
]);
|
||||
});
|
||||
it('all should run', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', true));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', true));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's2', true));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await unhookP(parser, true);
|
||||
assert.deepEqual(reports, [
|
||||
// messages from TestHandler
|
||||
['s2', 'HOOK', [1, 2, 3]],
|
||||
['a1', 'HOOK', [1, 2, 3]],
|
||||
['s1', 'HOOK', [1, 2, 3]],
|
||||
['s2', 'PUT', 'Here comes'],
|
||||
['a1', 'PUT', 'Here comes'],
|
||||
['s1', 'PUT', 'Here comes'],
|
||||
['s2', 'PUT', 'the mouse!'],
|
||||
['a1', 'PUT', 'the mouse!'],
|
||||
['s1', 'PUT', 'the mouse!'],
|
||||
['s2', 'UNHOOK', true],
|
||||
['a1', 'UNHOOK', true], // important: a1 before s1
|
||||
['s1', 'UNHOOK', true]
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('async | sync | async', () => {
|
||||
it('first should run, cleanup action for others', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', false));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', false));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a2', false));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await unhookP(parser, true);
|
||||
assert.deepEqual(reports, [
|
||||
// messages from TestHandler
|
||||
['a2', 'HOOK', [1, 2, 3]],
|
||||
['s1', 'HOOK', [1, 2, 3]],
|
||||
['a1', 'HOOK', [1, 2, 3]],
|
||||
['a2', 'PUT', 'Here comes'],
|
||||
['s1', 'PUT', 'Here comes'],
|
||||
['a1', 'PUT', 'Here comes'],
|
||||
['a2', 'PUT', 'the mouse!'],
|
||||
['s1', 'PUT', 'the mouse!'],
|
||||
['a1', 'PUT', 'the mouse!'],
|
||||
['a2', 'UNHOOK', true],
|
||||
['s1', 'UNHOOK', false], // important: s1 between a2 .. a1
|
||||
['a1', 'UNHOOK', false]
|
||||
]);
|
||||
});
|
||||
it('all should run', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', true));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', true));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a2', true));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await unhookP(parser, true);
|
||||
assert.deepEqual(reports, [
|
||||
// messages from TestHandler
|
||||
['a2', 'HOOK', [1, 2, 3]],
|
||||
['s1', 'HOOK', [1, 2, 3]],
|
||||
['a1', 'HOOK', [1, 2, 3]],
|
||||
['a2', 'PUT', 'Here comes'],
|
||||
['s1', 'PUT', 'Here comes'],
|
||||
['a1', 'PUT', 'Here comes'],
|
||||
['a2', 'PUT', 'the mouse!'],
|
||||
['s1', 'PUT', 'the mouse!'],
|
||||
['a1', 'PUT', 'the mouse!'],
|
||||
['a2', 'UNHOOK', true],
|
||||
['s1', 'UNHOOK', true], // important: s1 between a2 .. a1
|
||||
['a1', 'UNHOOK', true]
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('DcsHandlerFactory', () => {
|
||||
it('should be called once on end(true)', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push([params.toArray(), data]); return true; }));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await unhookP(parser, true);
|
||||
assert.deepEqual(reports, [[[1, 2, 3], 'Here comes the mouse!']]);
|
||||
});
|
||||
it('should not be called on end(false)', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push([params.toArray(), data]); return true; }));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await unhookP(parser, false);
|
||||
assert.deepEqual(reports, []);
|
||||
});
|
||||
it('should be disposable', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['one', params.toArray(), data]); return true; }));
|
||||
const dispo = parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['two', params.toArray(), data]); return true; }));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await unhookP(parser, true);
|
||||
assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!']]);
|
||||
dispo.dispose();
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
data = toUtf32('some other');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' data');
|
||||
parser.put(data, 0, data.length);
|
||||
await unhookP(parser, true);
|
||||
assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'some other data']]);
|
||||
});
|
||||
it('should respect return false', async () => {
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['one', params.toArray(), data]); return true; }));
|
||||
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['two', params.toArray(), data]); return false; }));
|
||||
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
|
||||
let data = toUtf32('Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await unhookP(parser, true);
|
||||
assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'Here comes the mouse!']]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { IDisposable } from 'common/Types';
|
||||
import { IDcsHandler, IParams, IHandlerCollection, IDcsParser, DcsFallbackHandlerType } from 'common/parser/Types';
|
||||
import { IDcsHandler, IParams, IHandlerCollection, IDcsParser, DcsFallbackHandlerType, ISubParserStackState } from 'common/parser/Types';
|
||||
import { utf32ToString } from 'common/input/TextDecoder';
|
||||
import { Params } from 'common/parser/Params';
|
||||
import { PAYLOAD_LIMIT } from 'common/parser/Constants';
|
||||
@@ -15,14 +15,20 @@ export class DcsParser implements IDcsParser {
|
||||
private _handlers: IHandlerCollection<IDcsHandler> = Object.create(null);
|
||||
private _active: IDcsHandler[] = EMPTY_HANDLERS;
|
||||
private _ident: number = 0;
|
||||
private _handlerFb: DcsFallbackHandlerType = () => {};
|
||||
private _handlerFb: DcsFallbackHandlerType = () => { };
|
||||
private _stack: ISubParserStackState = {
|
||||
paused: false,
|
||||
loopPosition: 0,
|
||||
fallThrough: false
|
||||
};
|
||||
|
||||
public dispose(): void {
|
||||
this._handlers = Object.create(null);
|
||||
this._handlerFb = () => {};
|
||||
this._handlerFb = () => { };
|
||||
this._active = EMPTY_HANDLERS;
|
||||
}
|
||||
|
||||
public addHandler(ident: number, handler: IDcsHandler): IDisposable {
|
||||
public registerHandler(ident: number, handler: IDcsHandler): IDisposable {
|
||||
if (this._handlers[ident] === undefined) {
|
||||
this._handlers[ident] = [];
|
||||
}
|
||||
@@ -38,10 +44,6 @@ export class DcsParser implements IDcsParser {
|
||||
};
|
||||
}
|
||||
|
||||
public setHandler(ident: number, handler: IDcsHandler): void {
|
||||
this._handlers[ident] = [handler];
|
||||
}
|
||||
|
||||
public clearHandler(ident: number): void {
|
||||
if (this._handlers[ident]) delete this._handlers[ident];
|
||||
}
|
||||
@@ -51,9 +53,13 @@ export class DcsParser implements IDcsParser {
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
// force cleanup leftover handlers
|
||||
if (this._active.length) {
|
||||
this.unhook(false);
|
||||
for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) {
|
||||
this._active[j].unhook(false);
|
||||
}
|
||||
}
|
||||
this._stack.paused = false;
|
||||
this._active = EMPTY_HANDLERS;
|
||||
this._ident = 0;
|
||||
}
|
||||
@@ -82,20 +88,42 @@ export class DcsParser implements IDcsParser {
|
||||
}
|
||||
}
|
||||
|
||||
public unhook(success: boolean): void {
|
||||
public unhook(success: boolean, promiseResult: boolean = true): void | Promise<boolean> {
|
||||
if (!this._active.length) {
|
||||
this._handlerFb(this._ident, 'UNHOOK', success);
|
||||
} else {
|
||||
let handlerResult: boolean | Promise<boolean> = false;
|
||||
let j = this._active.length - 1;
|
||||
for (; j >= 0; j--) {
|
||||
if (this._active[j].unhook(success) !== false) {
|
||||
break;
|
||||
}
|
||||
let fallThrough = false;
|
||||
if (this._stack.paused) {
|
||||
j = this._stack.loopPosition - 1;
|
||||
handlerResult = promiseResult;
|
||||
fallThrough = this._stack.fallThrough;
|
||||
this._stack.paused = false;
|
||||
}
|
||||
j--;
|
||||
// cleanup left over handlers
|
||||
if (!fallThrough && handlerResult === false) {
|
||||
for (; j >= 0; j--) {
|
||||
handlerResult = this._active[j].unhook(success);
|
||||
if (handlerResult === true) {
|
||||
break;
|
||||
} else if (handlerResult instanceof Promise) {
|
||||
this._stack.paused = true;
|
||||
this._stack.loopPosition = j;
|
||||
this._stack.fallThrough = false;
|
||||
return handlerResult;
|
||||
}
|
||||
}
|
||||
j--;
|
||||
}
|
||||
// cleanup left over handlers (fallThrough for async)
|
||||
for (; j >= 0; j--) {
|
||||
this._active[j].unhook(false);
|
||||
handlerResult = this._active[j].unhook(false);
|
||||
if (handlerResult instanceof Promise) {
|
||||
this._stack.paused = true;
|
||||
this._stack.loopPosition = j;
|
||||
this._stack.fallThrough = true;
|
||||
return handlerResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
this._active = EMPTY_HANDLERS;
|
||||
@@ -103,19 +131,27 @@ export class DcsParser implements IDcsParser {
|
||||
}
|
||||
}
|
||||
|
||||
// predefine empty params as [0] (ZDM)
|
||||
const EMPTY_PARAMS = new Params();
|
||||
EMPTY_PARAMS.addParam(0);
|
||||
|
||||
/**
|
||||
* Convenient class to create a DCS handler from a single callback function.
|
||||
* Note: The payload is currently limited to 50 MB (hardcoded).
|
||||
*/
|
||||
export class DcsHandler implements IDcsHandler {
|
||||
private _data = '';
|
||||
private _params: IParams | undefined;
|
||||
private _params: IParams = EMPTY_PARAMS;
|
||||
private _hitLimit: boolean = false;
|
||||
|
||||
constructor(private _handler: (data: string, params: IParams) => any) {}
|
||||
constructor(private _handler: (data: string, params: IParams) => boolean | Promise<boolean>) { }
|
||||
|
||||
public hook(params: IParams): void {
|
||||
this._params = params.clone();
|
||||
// since we need to preserve params until `unhook`, we have to clone it
|
||||
// (only borrowed from parser and spans multiple parser states)
|
||||
// perf optimization:
|
||||
// clone only, if we have non empty params, otherwise stick with default
|
||||
this._params = (params.length > 1 || params.params[0]) ? params.clone() : EMPTY_PARAMS;
|
||||
this._data = '';
|
||||
this._hitLimit = false;
|
||||
}
|
||||
@@ -131,14 +167,24 @@ export class DcsHandler implements IDcsHandler {
|
||||
}
|
||||
}
|
||||
|
||||
public unhook(success: boolean): any {
|
||||
let ret;
|
||||
public unhook(success: boolean): boolean | Promise<boolean> {
|
||||
let ret: boolean | Promise<boolean> = false;
|
||||
if (this._hitLimit) {
|
||||
ret = false;
|
||||
} else if (success) {
|
||||
ret = this._handler(this._data, this._params || new Params());
|
||||
ret = this._handler(this._data, this._params);
|
||||
if (ret instanceof Promise) {
|
||||
// need to hold data and params until `ret` got resolved
|
||||
// dont care for errors, data will be freed anyway on next start
|
||||
return ret.then(res => {
|
||||
this._params = EMPTY_PARAMS;
|
||||
this._data = '';
|
||||
this._hitLimit = false;
|
||||
return res;
|
||||
});
|
||||
}
|
||||
}
|
||||
this._params = undefined;
|
||||
this._params = EMPTY_PARAMS;
|
||||
this._data = '';
|
||||
this._hitLimit = false;
|
||||
return ret;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, IOscParser, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType } from 'common/parser/Types';
|
||||
import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, IOscParser, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType, IParserStackState, ParserStackType, ResumableHandlersType } from 'common/parser/Types';
|
||||
import { ParserState, ParserAction } from 'common/parser/Constants';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { IDisposable } from 'common/Types';
|
||||
@@ -239,7 +239,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
|
||||
// handler lookup containers
|
||||
protected _printHandler: PrintHandlerType;
|
||||
protected _executeHandlers: {[flag: number]: ExecuteHandlerType};
|
||||
protected _executeHandlers: { [flag: number]: ExecuteHandlerType };
|
||||
protected _csiHandlers: IHandlerCollection<CsiHandlerType>;
|
||||
protected _escHandlers: IHandlerCollection<EscHandlerType>;
|
||||
protected _oscParser: IOscParser;
|
||||
@@ -253,6 +253,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
protected _escHandlerFb: EscFallbackHandlerType;
|
||||
protected _errorHandlerFb: (state: IParsingState) => IParsingState;
|
||||
|
||||
// parser stack save for async handler support
|
||||
protected _parseStack: IParserStackState = {
|
||||
state: ParserStackType.NONE,
|
||||
handlers: [],
|
||||
handlerPos: 0,
|
||||
transition: 0,
|
||||
chunkPos: 0
|
||||
};
|
||||
|
||||
constructor(
|
||||
protected readonly _transitions: TransitionTable = VT500_TRANSITION_TABLE
|
||||
) {
|
||||
@@ -280,7 +289,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
this._errorHandler = this._errorHandlerFb;
|
||||
|
||||
// swallow 7bit ST (ESC+\)
|
||||
this.setEscHandler({final: '\\'}, () => {});
|
||||
this.registerEscHandler({ final: '\\' }, () => true);
|
||||
}
|
||||
|
||||
protected _identifier(id: IFunctionIdentifier, finalRange: number[] = [0x40, 0x7e]): number {
|
||||
@@ -344,7 +353,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
this._printHandler = this._printHandlerFb;
|
||||
}
|
||||
|
||||
public addEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): IDisposable {
|
||||
public registerEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): IDisposable {
|
||||
const ident = this._identifier(id, [0x30, 0x7e]);
|
||||
if (this._escHandlers[ident] === undefined) {
|
||||
this._escHandlers[ident] = [];
|
||||
@@ -360,9 +369,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
}
|
||||
};
|
||||
}
|
||||
public setEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): void {
|
||||
this._escHandlers[this._identifier(id, [0x30, 0x7e])] = [handler];
|
||||
}
|
||||
public clearEscHandler(id: IFunctionIdentifier): void {
|
||||
if (this._escHandlers[this._identifier(id, [0x30, 0x7e])]) delete this._escHandlers[this._identifier(id, [0x30, 0x7e])];
|
||||
}
|
||||
@@ -380,7 +386,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
this._executeHandlerFb = handler;
|
||||
}
|
||||
|
||||
public addCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): IDisposable {
|
||||
public registerCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): IDisposable {
|
||||
const ident = this._identifier(id);
|
||||
if (this._csiHandlers[ident] === undefined) {
|
||||
this._csiHandlers[ident] = [];
|
||||
@@ -396,9 +402,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
}
|
||||
};
|
||||
}
|
||||
public setCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): void {
|
||||
this._csiHandlers[this._identifier(id)] = [handler];
|
||||
}
|
||||
public clearCsiHandler(id: IFunctionIdentifier): void {
|
||||
if (this._csiHandlers[this._identifier(id)]) delete this._csiHandlers[this._identifier(id)];
|
||||
}
|
||||
@@ -406,11 +409,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
this._csiHandlerFb = callback;
|
||||
}
|
||||
|
||||
public addDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable {
|
||||
return this._dcsParser.addHandler(this._identifier(id), handler);
|
||||
}
|
||||
public setDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): void {
|
||||
this._dcsParser.setHandler(this._identifier(id), handler);
|
||||
public registerDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable {
|
||||
return this._dcsParser.registerHandler(this._identifier(id), handler);
|
||||
}
|
||||
public clearDcsHandler(id: IFunctionIdentifier): void {
|
||||
this._dcsParser.clearHandler(this._identifier(id));
|
||||
@@ -419,11 +419,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
this._dcsParser.setHandlerFallback(handler);
|
||||
}
|
||||
|
||||
public addOscHandler(ident: number, handler: IOscHandler): IDisposable {
|
||||
return this._oscParser.addHandler(ident, handler);
|
||||
}
|
||||
public setOscHandler(ident: number, handler: IOscHandler): void {
|
||||
this._oscParser.setHandler(ident, handler);
|
||||
public registerOscHandler(ident: number, handler: IOscHandler): IDisposable {
|
||||
return this._oscParser.registerHandler(ident, handler);
|
||||
}
|
||||
public clearOscHandler(ident: number): void {
|
||||
this._oscParser.clearHandler(ident);
|
||||
@@ -439,6 +436,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
this._errorHandler = this._errorHandlerFb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset parser to initial values.
|
||||
*
|
||||
* This can also be used to lift the improper continuation error condition
|
||||
* when dealing with async handlers. Use this only as a last resort to silence
|
||||
* that error when the terminal has no pending data to be processed. Note that
|
||||
* the interrupted async handler might continue its work in the future messing
|
||||
* up the terminal state even further.
|
||||
*/
|
||||
public reset(): void {
|
||||
this.currentState = this.initialState;
|
||||
this._oscParser.reset();
|
||||
@@ -447,9 +453,31 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
this._params.addParam(0); // ZDM
|
||||
this._collect = 0;
|
||||
this.precedingCodepoint = 0;
|
||||
// abort pending continuation from async handler
|
||||
// Here the RESET type indicates, that the next parse call will
|
||||
// ignore any saved stack, instead continues sync with next codepoint from GROUND
|
||||
if (this._parseStack.state !== ParserStackType.NONE) {
|
||||
this._parseStack.state = ParserStackType.RESET;
|
||||
this._parseStack.handlers = []; // also release handlers ref
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Async parse support.
|
||||
*/
|
||||
protected _preserveStack(
|
||||
state: ParserStackType,
|
||||
handlers: ResumableHandlersType,
|
||||
handlerPos: number,
|
||||
transition: number,
|
||||
chunkPos: number
|
||||
): void {
|
||||
this._parseStack.state = state;
|
||||
this._parseStack.handlers = handlers;
|
||||
this._parseStack.handlerPos = handlerPos;
|
||||
this._parseStack.transition = transition;
|
||||
this._parseStack.chunkPos = chunkPos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse UTF32 codepoints in `data` up to `length`.
|
||||
@@ -464,23 +492,141 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
* - DCS_PARAM:PARAM
|
||||
* - OSC_STRING:OSC_PUT
|
||||
* - DCS_PASSTHROUGH:DCS_PUT
|
||||
*
|
||||
* Note on asynchronous handler support:
|
||||
* Any handler returning a promise will be treated as asynchronous.
|
||||
* To keep the in-band blocking working for async handlers, `parse` pauses execution,
|
||||
* creates a stack save and returns the promise to the caller.
|
||||
* For proper continuation of the paused state it is important
|
||||
* to await the promise resolving. On resolve the parse must be repeated
|
||||
* with the same chunk of data and the resolved value in `promiseResult`
|
||||
* until no promise is returned.
|
||||
*
|
||||
* Important: With only sync handlers defined, parsing is completely synchronous as well.
|
||||
* As soon as an async handler is involved, synchronous parsing is not possible anymore.
|
||||
*
|
||||
* Boilerplate for proper parsing of multiple chunks with async handlers:
|
||||
*
|
||||
* ```typescript
|
||||
* async function parseMultipleChunks(chunks: Uint32Array[]): Promise<void> {
|
||||
* for (const chunk of chunks) {
|
||||
* let result: void | Promise<boolean>;
|
||||
* let prev: boolean | undefined;
|
||||
* while (result = parser.parse(chunk, chunk.length, prev)) {
|
||||
* prev = await result;
|
||||
* }
|
||||
* }
|
||||
* // finished parsing all chunks...
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public parse(data: Uint32Array, length: number): void {
|
||||
public parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise<boolean> {
|
||||
let code = 0;
|
||||
let transition = 0;
|
||||
let currentState = this.currentState;
|
||||
const osc = this._oscParser;
|
||||
const dcs = this._dcsParser;
|
||||
let collect = this._collect;
|
||||
const params = this._params;
|
||||
const table: Uint8Array = this._transitions.table;
|
||||
let start = 0;
|
||||
let handlerResult: void | boolean | Promise<boolean>;
|
||||
|
||||
// resume from async handler
|
||||
if (this._parseStack.state) {
|
||||
// allow sync parser reset even in continuation mode
|
||||
// Note: can be used to recover parser from improper continuation error below
|
||||
if (this._parseStack.state === ParserStackType.RESET) {
|
||||
this._parseStack.state = ParserStackType.NONE;
|
||||
start = this._parseStack.chunkPos + 1; // continue with next codepoint in GROUND
|
||||
} else {
|
||||
if (promiseResult === undefined || this._parseStack.state === ParserStackType.FAIL) {
|
||||
/**
|
||||
* Reject further parsing on improper continuation after pausing.
|
||||
* This is a really bad condition with screwed up execution order and prolly messed up
|
||||
* terminal state, therefore we exit hard with an exception and reject any further parsing.
|
||||
*
|
||||
* Note: With `Terminal.write` usage this exception should never occur, as the top level
|
||||
* calls are guaranteed to handle async conditions properly. If you ever encounter this
|
||||
* exception in your terminal integration it indicates, that you injected data chunks to
|
||||
* `InputHandler.parse` or `EscapeSequenceParser.parse` synchronously without waiting for
|
||||
* continuation of a running async handler.
|
||||
*
|
||||
* It is possible to get rid of this error by calling `reset`. But dont rely on that,
|
||||
* as the pending async handler still might mess up the terminal later. Instead fix the faulty
|
||||
* async handling, so this error will not be thrown anymore.
|
||||
*/
|
||||
this._parseStack.state = ParserStackType.FAIL;
|
||||
throw new Error('improper continuation due to previous async handler, giving up parsing');
|
||||
}
|
||||
|
||||
// we have to resume the old handler loop if:
|
||||
// - return value of the promise was `false`
|
||||
// - handlers are not exhausted yet
|
||||
const handlers = this._parseStack.handlers;
|
||||
let handlerPos = this._parseStack.handlerPos - 1;
|
||||
switch (this._parseStack.state) {
|
||||
case ParserStackType.CSI:
|
||||
if (promiseResult === false && handlerPos > -1) {
|
||||
for (; handlerPos >= 0; handlerPos--) {
|
||||
handlerResult = (handlers as CsiHandlerType[])[handlerPos](this._params);
|
||||
if (handlerResult === true) {
|
||||
break;
|
||||
} else if (handlerResult instanceof Promise) {
|
||||
this._parseStack.handlerPos = handlerPos;
|
||||
return handlerResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
this._parseStack.handlers = [];
|
||||
break;
|
||||
case ParserStackType.ESC:
|
||||
if (promiseResult === false && handlerPos > -1) {
|
||||
for (; handlerPos >= 0; handlerPos--) {
|
||||
handlerResult = (handlers as EscHandlerType[])[handlerPos]();
|
||||
if (handlerResult === true) {
|
||||
break;
|
||||
} else if (handlerResult instanceof Promise) {
|
||||
this._parseStack.handlerPos = handlerPos;
|
||||
return handlerResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
this._parseStack.handlers = [];
|
||||
break;
|
||||
case ParserStackType.DCS:
|
||||
code = data[this._parseStack.chunkPos];
|
||||
handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a, promiseResult);
|
||||
if (handlerResult) {
|
||||
return handlerResult;
|
||||
}
|
||||
if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;
|
||||
this._params.reset();
|
||||
this._params.addParam(0); // ZDM
|
||||
this._collect = 0;
|
||||
break;
|
||||
case ParserStackType.OSC:
|
||||
code = data[this._parseStack.chunkPos];
|
||||
handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a, promiseResult);
|
||||
if (handlerResult) {
|
||||
return handlerResult;
|
||||
}
|
||||
if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;
|
||||
this._params.reset();
|
||||
this._params.addParam(0); // ZDM
|
||||
this._collect = 0;
|
||||
break;
|
||||
}
|
||||
// cleanup before continuing with the main sync loop
|
||||
this._parseStack.state = ParserStackType.NONE;
|
||||
start = this._parseStack.chunkPos + 1;
|
||||
this.precedingCodepoint = 0;
|
||||
this.currentState = this._parseStack.transition & TableAccess.TRANSITION_STATE_MASK;
|
||||
}
|
||||
}
|
||||
|
||||
// continue with main sync loop
|
||||
|
||||
// process input string
|
||||
for (let i = 0; i < length; ++i) {
|
||||
for (let i = start; i < length; ++i) {
|
||||
code = data[i];
|
||||
|
||||
// normal transition & action lookup
|
||||
transition = table[currentState << TableAccess.INDEX_STATE_SHIFT | (code < 0xa0 ? code : NON_ASCII_PRINTABLE)];
|
||||
transition = this._transitions.table[this.currentState << TableAccess.INDEX_STATE_SHIFT | (code < 0xa0 ? code : NON_ASCII_PRINTABLE)];
|
||||
switch (transition >> TableAccess.TRANSITION_ACTION_SHIFT) {
|
||||
case ParserAction.PRINT:
|
||||
// read ahead with loop unrolling
|
||||
@@ -520,9 +666,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
{
|
||||
position: i,
|
||||
code,
|
||||
currentState,
|
||||
collect,
|
||||
params,
|
||||
currentState: this.currentState,
|
||||
collect: this._collect,
|
||||
params: this._params,
|
||||
abort: false
|
||||
});
|
||||
if (inject.abort) return;
|
||||
@@ -530,16 +676,21 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
break;
|
||||
case ParserAction.CSI_DISPATCH:
|
||||
// Trigger CSI Handler
|
||||
const handlers = this._csiHandlers[collect << 8 | code];
|
||||
const handlers = this._csiHandlers[this._collect << 8 | code];
|
||||
let j = handlers ? handlers.length - 1 : -1;
|
||||
for (; j >= 0; j--) {
|
||||
// undefined or true means success and to stop bubbling
|
||||
if (handlers[j](params) !== false) {
|
||||
// true means success and to stop bubbling
|
||||
// a promise indicates an async handler that needs to finish before progressing
|
||||
handlerResult = handlers[j](this._params);
|
||||
if (handlerResult === true) {
|
||||
break;
|
||||
} else if (handlerResult instanceof Promise) {
|
||||
this._preserveStack(ParserStackType.CSI, handlers, j, transition, i);
|
||||
return handlerResult;
|
||||
}
|
||||
}
|
||||
if (j < 0) {
|
||||
this._csiHandlerFb(collect << 8 | code, params);
|
||||
this._csiHandlerFb(this._collect << 8 | code, this._params);
|
||||
}
|
||||
this.precedingCodepoint = 0;
|
||||
break;
|
||||
@@ -548,91 +699,98 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
do {
|
||||
switch (code) {
|
||||
case 0x3b:
|
||||
params.addParam(0); // ZDM
|
||||
this._params.addParam(0); // ZDM
|
||||
break;
|
||||
case 0x3a:
|
||||
params.addSubParam(-1);
|
||||
this._params.addSubParam(-1);
|
||||
break;
|
||||
default: // 0x30 - 0x39
|
||||
params.addDigit(code - 48);
|
||||
this._params.addDigit(code - 48);
|
||||
}
|
||||
} while (++i < length && (code = data[i]) > 0x2f && code < 0x3c);
|
||||
i--;
|
||||
break;
|
||||
case ParserAction.COLLECT:
|
||||
collect <<= 8;
|
||||
collect |= code;
|
||||
this._collect <<= 8;
|
||||
this._collect |= code;
|
||||
break;
|
||||
case ParserAction.ESC_DISPATCH:
|
||||
const handlersEsc = this._escHandlers[collect << 8 | code];
|
||||
const handlersEsc = this._escHandlers[this._collect << 8 | code];
|
||||
let jj = handlersEsc ? handlersEsc.length - 1 : -1;
|
||||
for (; jj >= 0; jj--) {
|
||||
// undefined or true means success and to stop bubbling
|
||||
if (handlersEsc[jj]() !== false) {
|
||||
// true means success and to stop bubbling
|
||||
// a promise indicates an async handler that needs to finish before progressing
|
||||
handlerResult = handlersEsc[jj]();
|
||||
if (handlerResult === true) {
|
||||
break;
|
||||
} else if (handlerResult instanceof Promise) {
|
||||
this._preserveStack(ParserStackType.ESC, handlersEsc, jj, transition, i);
|
||||
return handlerResult;
|
||||
}
|
||||
}
|
||||
if (jj < 0) {
|
||||
this._escHandlerFb(collect << 8 | code);
|
||||
this._escHandlerFb(this._collect << 8 | code);
|
||||
}
|
||||
this.precedingCodepoint = 0;
|
||||
break;
|
||||
case ParserAction.CLEAR:
|
||||
params.reset();
|
||||
params.addParam(0); // ZDM
|
||||
collect = 0;
|
||||
this._params.reset();
|
||||
this._params.addParam(0); // ZDM
|
||||
this._collect = 0;
|
||||
break;
|
||||
case ParserAction.DCS_HOOK:
|
||||
dcs.hook(collect << 8 | code, params);
|
||||
this._dcsParser.hook(this._collect << 8 | code, this._params);
|
||||
break;
|
||||
case ParserAction.DCS_PUT:
|
||||
// inner loop - exit DCS_PUT: 0x18, 0x1a, 0x1b, 0x7f, 0x80 - 0x9f
|
||||
// unhook triggered by: 0x1b, 0x9c (success) and 0x18, 0x1a (abort)
|
||||
for (let j = i + 1; ; ++j) {
|
||||
if (j >= length || (code = data[j]) === 0x18 || code === 0x1a || code === 0x1b || (code > 0x7f && code < NON_ASCII_PRINTABLE)) {
|
||||
dcs.put(data, i, j);
|
||||
this._dcsParser.put(data, i, j);
|
||||
i = j - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ParserAction.DCS_UNHOOK:
|
||||
dcs.unhook(code !== 0x18 && code !== 0x1a);
|
||||
handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a);
|
||||
if (handlerResult) {
|
||||
this._preserveStack(ParserStackType.DCS, [], 0, transition, i);
|
||||
return handlerResult;
|
||||
}
|
||||
if (code === 0x1b) transition |= ParserState.ESCAPE;
|
||||
params.reset();
|
||||
params.addParam(0); // ZDM
|
||||
collect = 0;
|
||||
this._params.reset();
|
||||
this._params.addParam(0); // ZDM
|
||||
this._collect = 0;
|
||||
this.precedingCodepoint = 0;
|
||||
break;
|
||||
case ParserAction.OSC_START:
|
||||
osc.start();
|
||||
this._oscParser.start();
|
||||
break;
|
||||
case ParserAction.OSC_PUT:
|
||||
// inner loop: 0x20 (SP) included, 0x7F (DEL) included
|
||||
for (let j = i + 1; ; j++) {
|
||||
if (j >= length || (code = data[j]) < 0x20 || (code > 0x7f && code <= 0x9f)) {
|
||||
osc.put(data, i, j);
|
||||
if (j >= length || (code = data[j]) < 0x20 || (code > 0x7f && code < NON_ASCII_PRINTABLE)) {
|
||||
this._oscParser.put(data, i, j);
|
||||
i = j - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ParserAction.OSC_END:
|
||||
osc.end(code !== 0x18 && code !== 0x1a);
|
||||
handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a);
|
||||
if (handlerResult) {
|
||||
this._preserveStack(ParserStackType.OSC, [], 0, transition, i);
|
||||
return handlerResult;
|
||||
}
|
||||
if (code === 0x1b) transition |= ParserState.ESCAPE;
|
||||
params.reset();
|
||||
params.addParam(0); // ZDM
|
||||
collect = 0;
|
||||
this._params.reset();
|
||||
this._params.addParam(0); // ZDM
|
||||
this._collect = 0;
|
||||
this.precedingCodepoint = 0;
|
||||
break;
|
||||
}
|
||||
currentState = transition & TableAccess.TRANSITION_STATE_MASK;
|
||||
this.currentState = transition & TableAccess.TRANSITION_STATE_MASK;
|
||||
}
|
||||
|
||||
// save collected intermediates
|
||||
this._collect = collect;
|
||||
|
||||
// save state
|
||||
this.currentState = currentState;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +23,12 @@ class TestHandler implements IOscHandler {
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
this.output.push([this.msg, this.id, 'PUT', utf32ToString(data, start, end)]);
|
||||
}
|
||||
public end(success: boolean): void | boolean {
|
||||
public end(success: boolean): boolean {
|
||||
this.output.push([this.msg, this.id, 'END', success]);
|
||||
if (this.returnFalse) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +79,7 @@ describe('OscParser', () => {
|
||||
});
|
||||
describe('handler registration', () => {
|
||||
it('setOscHandler', () => {
|
||||
parser.setHandler(1234, new TestHandler(1234, reports, 'th'));
|
||||
parser.registerHandler(1234, new TestHandler(1234, reports, 'th'));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -94,7 +95,7 @@ describe('OscParser', () => {
|
||||
]);
|
||||
});
|
||||
it('clearOscHandler', () => {
|
||||
parser.setHandler(1234, new TestHandler(1234, reports, 'th'));
|
||||
parser.registerHandler(1234, new TestHandler(1234, reports, 'th'));
|
||||
parser.clearHandler(1234);
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
@@ -111,8 +112,8 @@ describe('OscParser', () => {
|
||||
]);
|
||||
});
|
||||
it('addOscHandler', () => {
|
||||
parser.setHandler(1234, new TestHandler(1234, reports, 'th1'));
|
||||
parser.addHandler(1234, new TestHandler(1234, reports, 'th2'));
|
||||
parser.registerHandler(1234, new TestHandler(1234, reports, 'th1'));
|
||||
parser.registerHandler(1234, new TestHandler(1234, reports, 'th2'));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -131,8 +132,8 @@ describe('OscParser', () => {
|
||||
]);
|
||||
});
|
||||
it('addOscHandler with return false', () => {
|
||||
parser.setHandler(1234, new TestHandler(1234, reports, 'th1'));
|
||||
parser.addHandler(1234, new TestHandler(1234, reports, 'th2', true));
|
||||
parser.registerHandler(1234, new TestHandler(1234, reports, 'th1'));
|
||||
parser.registerHandler(1234, new TestHandler(1234, reports, 'th2', true));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -151,8 +152,8 @@ describe('OscParser', () => {
|
||||
]);
|
||||
});
|
||||
it('dispose handlers', () => {
|
||||
parser.setHandler(1234, new TestHandler(1234, reports, 'th1'));
|
||||
const dispo = parser.addHandler(1234, new TestHandler(1234, reports, 'th2', true));
|
||||
parser.registerHandler(1234, new TestHandler(1234, reports, 'th1'));
|
||||
const dispo = parser.registerHandler(1234, new TestHandler(1234, reports, 'th2', true));
|
||||
dispo.dispose();
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
@@ -170,7 +171,7 @@ describe('OscParser', () => {
|
||||
});
|
||||
describe('OscHandlerFactory', () => {
|
||||
it('should be called once on end(true)', () => {
|
||||
parser.setHandler(1234, new OscHandler(data => reports.push([1234, data])));
|
||||
parser.registerHandler(1234, new OscHandler(data => { reports.push([1234, data]); return true; }));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -180,7 +181,7 @@ describe('OscParser', () => {
|
||||
assert.deepEqual(reports, [[1234, 'Here comes the mouse!']]);
|
||||
});
|
||||
it('should not be called on end(false)', () => {
|
||||
parser.setHandler(1234, new OscHandler(data => reports.push([1234, data])));
|
||||
parser.registerHandler(1234, new OscHandler(data => { reports.push([1234, data]); return true; }));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -190,8 +191,8 @@ describe('OscParser', () => {
|
||||
assert.deepEqual(reports, []);
|
||||
});
|
||||
it('should be disposable', () => {
|
||||
parser.setHandler(1234, new OscHandler(data => reports.push(['one', data])));
|
||||
const dispo = parser.addHandler(1234, new OscHandler(data => reports.push(['two', data])));
|
||||
parser.registerHandler(1234, new OscHandler(data => { reports.push(['one', data]); return true; }));
|
||||
const dispo = parser.registerHandler(1234, new OscHandler(data => { reports.push(['two', data]); return true; }));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -209,8 +210,8 @@ describe('OscParser', () => {
|
||||
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'some other data']]);
|
||||
});
|
||||
it('should respect return false', () => {
|
||||
parser.setHandler(1234, new OscHandler(data => reports.push(['one', data])));
|
||||
parser.addHandler(1234, new OscHandler(data => { reports.push(['two', data]); return false; }));
|
||||
parser.registerHandler(1234, new OscHandler(data => { reports.push(['one', data]); return true; }));
|
||||
parser.registerHandler(1234, new OscHandler(data => { reports.push(['two', data]); return false; }));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -221,7 +222,7 @@ describe('OscParser', () => {
|
||||
});
|
||||
it('should work up to payload limit', function(): void {
|
||||
this.timeout(10000);
|
||||
parser.setHandler(1234, new OscHandler(data => reports.push([1234, data])));
|
||||
parser.registerHandler(1234, new OscHandler(data => { reports.push([1234, data]); return true; }));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -234,7 +235,7 @@ describe('OscParser', () => {
|
||||
});
|
||||
it('should abort for payload limit +1', function(): void {
|
||||
this.timeout(10000);
|
||||
parser.setHandler(1234, new OscHandler(data => reports.push([1234, data])));
|
||||
parser.registerHandler(1234, new OscHandler(data => { reports.push([1234, data]); return true; }));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;');
|
||||
parser.put(data, 0, data.length);
|
||||
@@ -249,3 +250,204 @@ describe('OscParser', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
class TestHandlerAsync implements IOscHandler {
|
||||
constructor(public id: number, public output: any[], public msg: string, public returnFalse: boolean = false) {}
|
||||
public start(): void {
|
||||
this.output.push([this.msg, this.id, 'START']);
|
||||
}
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
this.output.push([this.msg, this.id, 'PUT', utf32ToString(data, start, end)]);
|
||||
}
|
||||
public async end(success: boolean): Promise<boolean> {
|
||||
await new Promise(res => setTimeout(res, 20));
|
||||
this.output.push([this.msg, this.id, 'END', success]);
|
||||
if (this.returnFalse) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
async function endP(parser: OscParser, success: boolean): Promise<void> {
|
||||
let result: void | Promise<boolean>;
|
||||
let prev: boolean | undefined;
|
||||
while (result = parser.end(success, prev)) {
|
||||
prev = await result;
|
||||
}
|
||||
}
|
||||
|
||||
describe('OscParser - async tests', () => {
|
||||
let parser: OscParser;
|
||||
let reports: any[] = [];
|
||||
beforeEach(() => {
|
||||
reports = [];
|
||||
parser = new OscParser();
|
||||
parser.setHandlerFallback((id, action, data) => {
|
||||
reports.push([id, action, data]);
|
||||
});
|
||||
});
|
||||
describe('sync and async mixed', () => {
|
||||
describe('sync | async | sync', () => {
|
||||
it('first should run, cleanup action for others', async () => {
|
||||
parser.registerHandler(1234, new TestHandler(1234, reports, 's1'));
|
||||
parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 'a1'));
|
||||
parser.registerHandler(1234, new TestHandler(1234, reports, 's2'));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await endP(parser, true);
|
||||
assert.deepEqual(reports, [
|
||||
// messages from TestHandler
|
||||
['s2', 1234, 'START'],
|
||||
['a1', 1234, 'START'],
|
||||
['s1', 1234, 'START'],
|
||||
['s2', 1234, 'PUT', 'Here comes'],
|
||||
['a1', 1234, 'PUT', 'Here comes'],
|
||||
['s1', 1234, 'PUT', 'Here comes'],
|
||||
['s2', 1234, 'PUT', 'the mouse!'],
|
||||
['a1', 1234, 'PUT', 'the mouse!'],
|
||||
['s1', 1234, 'PUT', 'the mouse!'],
|
||||
['s2', 1234, 'END', true],
|
||||
['a1', 1234, 'END', false],
|
||||
['s1', 1234, 'END', false]
|
||||
]);
|
||||
});
|
||||
it('all should run', async () => {
|
||||
parser.registerHandler(1234, new TestHandler(1234, reports, 's1', true));
|
||||
parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 'a1', true));
|
||||
parser.registerHandler(1234, new TestHandler(1234, reports, 's2', true));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await endP(parser, true);
|
||||
assert.deepEqual(reports, [
|
||||
// messages from TestHandler
|
||||
['s2', 1234, 'START'],
|
||||
['a1', 1234, 'START'],
|
||||
['s1', 1234, 'START'],
|
||||
['s2', 1234, 'PUT', 'Here comes'],
|
||||
['a1', 1234, 'PUT', 'Here comes'],
|
||||
['s1', 1234, 'PUT', 'Here comes'],
|
||||
['s2', 1234, 'PUT', 'the mouse!'],
|
||||
['a1', 1234, 'PUT', 'the mouse!'],
|
||||
['s1', 1234, 'PUT', 'the mouse!'],
|
||||
['s2', 1234, 'END', true],
|
||||
['a1', 1234, 'END', true],
|
||||
['s1', 1234, 'END', true]
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('async | sync | async', () => {
|
||||
it('first should run, cleanup action for others', async () => {
|
||||
parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's1'));
|
||||
parser.registerHandler(1234, new TestHandler(1234, reports, 'a1'));
|
||||
parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's2'));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await endP(parser, true);
|
||||
assert.deepEqual(reports, [
|
||||
// messages from TestHandler
|
||||
['s2', 1234, 'START'],
|
||||
['a1', 1234, 'START'],
|
||||
['s1', 1234, 'START'],
|
||||
['s2', 1234, 'PUT', 'Here comes'],
|
||||
['a1', 1234, 'PUT', 'Here comes'],
|
||||
['s1', 1234, 'PUT', 'Here comes'],
|
||||
['s2', 1234, 'PUT', 'the mouse!'],
|
||||
['a1', 1234, 'PUT', 'the mouse!'],
|
||||
['s1', 1234, 'PUT', 'the mouse!'],
|
||||
['s2', 1234, 'END', true],
|
||||
['a1', 1234, 'END', false],
|
||||
['s1', 1234, 'END', false]
|
||||
]);
|
||||
});
|
||||
it('all should run', async () => {
|
||||
parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's1', true));
|
||||
parser.registerHandler(1234, new TestHandler(1234, reports, 'a1', true));
|
||||
parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's2', true));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32('the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await endP(parser, true);
|
||||
assert.deepEqual(reports, [
|
||||
// messages from TestHandler
|
||||
['s2', 1234, 'START'],
|
||||
['a1', 1234, 'START'],
|
||||
['s1', 1234, 'START'],
|
||||
['s2', 1234, 'PUT', 'Here comes'],
|
||||
['a1', 1234, 'PUT', 'Here comes'],
|
||||
['s1', 1234, 'PUT', 'Here comes'],
|
||||
['s2', 1234, 'PUT', 'the mouse!'],
|
||||
['a1', 1234, 'PUT', 'the mouse!'],
|
||||
['s1', 1234, 'PUT', 'the mouse!'],
|
||||
['s2', 1234, 'END', true],
|
||||
['a1', 1234, 'END', true],
|
||||
['s1', 1234, 'END', true]
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('OscHandlerFactory', () => {
|
||||
it('should be called once on end(true)', async () => {
|
||||
parser.registerHandler(1234, new OscHandler(async data => { reports.push([1234, data]); return true; }));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
await endP(parser, true);
|
||||
assert.deepEqual(reports, [[1234, 'Here comes the mouse!']]);
|
||||
});
|
||||
it('should not be called on end(false)', async () => {
|
||||
parser.registerHandler(1234, new OscHandler(async data => { reports.push([1234, data]); return true; }));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await endP(parser, false);
|
||||
assert.deepEqual(reports, []);
|
||||
});
|
||||
it('should be disposable', async () => {
|
||||
parser.registerHandler(1234, new OscHandler(async data => { reports.push(['one', data]); return true; }));
|
||||
const dispo = parser.registerHandler(1234, new OscHandler(async data => { reports.push(['two', data]); return true; }));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await endP(parser, true);
|
||||
assert.deepEqual(reports, [['two', 'Here comes the mouse!']]);
|
||||
dispo.dispose();
|
||||
parser.start();
|
||||
data = toUtf32('1234;some other');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' data');
|
||||
parser.put(data, 0, data.length);
|
||||
await endP(parser, true);
|
||||
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'some other data']]);
|
||||
});
|
||||
it('should respect return false', async () => {
|
||||
parser.registerHandler(1234, new OscHandler(async data => { reports.push(['one', data]); return true; }));
|
||||
parser.registerHandler(1234, new OscHandler(async data => { reports.push(['two', data]); return false; }));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;Here comes');
|
||||
parser.put(data, 0, data.length);
|
||||
data = toUtf32(' the mouse!');
|
||||
parser.put(data, 0, data.length);
|
||||
await endP(parser, true);
|
||||
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'Here comes the mouse!']]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,19 +3,26 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IOscHandler, IHandlerCollection, OscFallbackHandlerType, IOscParser } from 'common/parser/Types';
|
||||
import { IOscHandler, IHandlerCollection, OscFallbackHandlerType, IOscParser, ISubParserStackState } from 'common/parser/Types';
|
||||
import { OscState, PAYLOAD_LIMIT } from 'common/parser/Constants';
|
||||
import { utf32ToString } from 'common/input/TextDecoder';
|
||||
import { IDisposable } from 'common/Types';
|
||||
|
||||
const EMPTY_HANDLERS: IOscHandler[] = [];
|
||||
|
||||
export class OscParser implements IOscParser {
|
||||
private _state = OscState.START;
|
||||
private _active = EMPTY_HANDLERS;
|
||||
private _id = -1;
|
||||
private _handlers: IHandlerCollection<IOscHandler> = Object.create(null);
|
||||
private _handlerFb: OscFallbackHandlerType = () => { };
|
||||
private _stack: ISubParserStackState = {
|
||||
paused: false,
|
||||
loopPosition: 0,
|
||||
fallThrough: false
|
||||
};
|
||||
|
||||
public addHandler(ident: number, handler: IOscHandler): IDisposable {
|
||||
public registerHandler(ident: number, handler: IOscHandler): IDisposable {
|
||||
if (this._handlers[ident] === undefined) {
|
||||
this._handlers[ident] = [];
|
||||
}
|
||||
@@ -30,9 +37,6 @@ export class OscParser implements IOscParser {
|
||||
}
|
||||
};
|
||||
}
|
||||
public setHandler(ident: number, handler: IOscHandler): void {
|
||||
this._handlers[ident] = [handler];
|
||||
}
|
||||
public clearHandler(ident: number): void {
|
||||
if (this._handlers[ident]) delete this._handlers[ident];
|
||||
}
|
||||
@@ -42,58 +46,40 @@ export class OscParser implements IOscParser {
|
||||
|
||||
public dispose(): void {
|
||||
this._handlers = Object.create(null);
|
||||
this._handlerFb = () => {};
|
||||
this._handlerFb = () => { };
|
||||
this._active = EMPTY_HANDLERS;
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
// cleanup handlers if payload was already sent
|
||||
// force cleanup handlers if payload was already sent
|
||||
if (this._state === OscState.PAYLOAD) {
|
||||
this.end(false);
|
||||
for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) {
|
||||
this._active[j].end(false);
|
||||
}
|
||||
}
|
||||
this._stack.paused = false;
|
||||
this._active = EMPTY_HANDLERS;
|
||||
this._id = -1;
|
||||
this._state = OscState.START;
|
||||
}
|
||||
|
||||
private _start(): void {
|
||||
const handlers = this._handlers[this._id];
|
||||
if (!handlers) {
|
||||
this._active = this._handlers[this._id] || EMPTY_HANDLERS;
|
||||
if (!this._active.length) {
|
||||
this._handlerFb(this._id, 'START');
|
||||
} else {
|
||||
for (let j = handlers.length - 1; j >= 0; j--) {
|
||||
handlers[j].start();
|
||||
for (let j = this._active.length - 1; j >= 0; j--) {
|
||||
this._active[j].start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _put(data: Uint32Array, start: number, end: number): void {
|
||||
const handlers = this._handlers[this._id];
|
||||
if (!handlers) {
|
||||
if (!this._active.length) {
|
||||
this._handlerFb(this._id, 'PUT', utf32ToString(data, start, end));
|
||||
} else {
|
||||
for (let j = handlers.length - 1; j >= 0; j--) {
|
||||
handlers[j].put(data, start, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _end(success: boolean): void {
|
||||
// other than the old code we always have to call .end
|
||||
// to keep the bubbling we use `success` to indicate
|
||||
// whether a handler should execute
|
||||
const handlers = this._handlers[this._id];
|
||||
if (!handlers) {
|
||||
this._handlerFb(this._id, 'END', success);
|
||||
} else {
|
||||
let j = handlers.length - 1;
|
||||
for (; j >= 0; j--) {
|
||||
if (handlers[j].end(success) !== false) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
j--;
|
||||
// cleanup left over handlers
|
||||
for (; j >= 0; j--) {
|
||||
handlers[j].end(false);
|
||||
for (let j = this._active.length - 1; j >= 0; j--) {
|
||||
this._active[j].put(data, start, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,7 +87,6 @@ export class OscParser implements IOscParser {
|
||||
public start(): void {
|
||||
// always reset leftover handlers
|
||||
this.reset();
|
||||
this._id = -1;
|
||||
this._state = OscState.ID;
|
||||
}
|
||||
|
||||
@@ -144,7 +129,7 @@ export class OscParser implements IOscParser {
|
||||
* Whether the OSC got aborted or finished normally
|
||||
* is indicated by `success`.
|
||||
*/
|
||||
public end(success: boolean): void {
|
||||
public end(success: boolean, promiseResult: boolean = true): void | Promise<boolean> {
|
||||
if (this._state === OscState.START) {
|
||||
return;
|
||||
}
|
||||
@@ -156,8 +141,49 @@ export class OscParser implements IOscParser {
|
||||
if (this._state === OscState.ID) {
|
||||
this._start();
|
||||
}
|
||||
this._end(success);
|
||||
|
||||
if (!this._active.length) {
|
||||
this._handlerFb(this._id, 'END', success);
|
||||
} else {
|
||||
let handlerResult: boolean | Promise<boolean> = false;
|
||||
let j = this._active.length - 1;
|
||||
let fallThrough = false;
|
||||
if (this._stack.paused) {
|
||||
j = this._stack.loopPosition - 1;
|
||||
handlerResult = promiseResult;
|
||||
fallThrough = this._stack.fallThrough;
|
||||
this._stack.paused = false;
|
||||
}
|
||||
if (!fallThrough && handlerResult === false) {
|
||||
for (; j >= 0; j--) {
|
||||
handlerResult = this._active[j].end(success);
|
||||
if (handlerResult === true) {
|
||||
break;
|
||||
} else if (handlerResult instanceof Promise) {
|
||||
this._stack.paused = true;
|
||||
this._stack.loopPosition = j;
|
||||
this._stack.fallThrough = false;
|
||||
return handlerResult;
|
||||
}
|
||||
}
|
||||
j--;
|
||||
}
|
||||
// cleanup left over handlers
|
||||
// we always have to call .end for proper cleanup,
|
||||
// here we use `success` to indicate whether a handler should execute
|
||||
for (; j >= 0; j--) {
|
||||
handlerResult = this._active[j].end(false);
|
||||
if (handlerResult instanceof Promise) {
|
||||
this._stack.paused = true;
|
||||
this._stack.loopPosition = j;
|
||||
this._stack.fallThrough = true;
|
||||
return handlerResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
this._active = EMPTY_HANDLERS;
|
||||
this._id = -1;
|
||||
this._state = OscState.START;
|
||||
}
|
||||
@@ -171,7 +197,7 @@ export class OscHandler implements IOscHandler {
|
||||
private _data = '';
|
||||
private _hitLimit: boolean = false;
|
||||
|
||||
constructor(private _handler: (data: string) => any) {}
|
||||
constructor(private _handler: (data: string) => boolean | Promise<boolean>) { }
|
||||
|
||||
public start(): void {
|
||||
this._data = '';
|
||||
@@ -189,12 +215,21 @@ export class OscHandler implements IOscHandler {
|
||||
}
|
||||
}
|
||||
|
||||
public end(success: boolean): any {
|
||||
let ret;
|
||||
public end(success: boolean): boolean | Promise<boolean> {
|
||||
let ret: boolean | Promise<boolean> = false;
|
||||
if (this._hitLimit) {
|
||||
ret = false;
|
||||
} else if (success) {
|
||||
ret = this._handler(this._data);
|
||||
if (ret instanceof Promise) {
|
||||
// need to hold data until `ret` got resolved
|
||||
// dont care for errors, data will be freed anyway on next start
|
||||
return ret.then(res => {
|
||||
this._data = '';
|
||||
this._hitLimit = false;
|
||||
return res;
|
||||
});
|
||||
}
|
||||
}
|
||||
this._data = '';
|
||||
this._hitLimit = false;
|
||||
|
||||
Vendored
+48
-18
@@ -6,6 +6,7 @@
|
||||
import { IDisposable } from 'common/Types';
|
||||
import { ParserState } from 'common/parser/Constants';
|
||||
|
||||
|
||||
/** sequence params serialized to js arrays */
|
||||
export type ParamsArray = (number | number[])[];
|
||||
|
||||
@@ -69,7 +70,7 @@ export interface IParsingState {
|
||||
* CSI handler types.
|
||||
* Note: `params` is borrowed.
|
||||
*/
|
||||
export type CsiHandlerType = (params: IParams) => boolean | void;
|
||||
export type CsiHandlerType = (params: IParams) => boolean | Promise<boolean>;
|
||||
export type CsiFallbackHandlerType = (ident: number, params: IParams) => void;
|
||||
|
||||
/**
|
||||
@@ -93,20 +94,20 @@ export interface IDcsHandler {
|
||||
* execution of the command should depend on `success`.
|
||||
* To save memory also cleanup data structures here.
|
||||
*/
|
||||
unhook(success: boolean): void | boolean;
|
||||
unhook(success: boolean): boolean | Promise<boolean>;
|
||||
}
|
||||
export type DcsFallbackHandlerType = (ident: number, action: 'HOOK' | 'PUT' | 'UNHOOK', payload?: any) => void;
|
||||
|
||||
/**
|
||||
* ESC handler types.
|
||||
*/
|
||||
export type EscHandlerType = () => boolean | void;
|
||||
export type EscHandlerType = () => boolean | Promise<boolean>;
|
||||
export type EscFallbackHandlerType = (identifier: number) => void;
|
||||
|
||||
/**
|
||||
* EXECUTE handler types.
|
||||
*/
|
||||
export type ExecuteHandlerType = () => boolean | void;
|
||||
export type ExecuteHandlerType = () => boolean;
|
||||
export type ExecuteFallbackHandlerType = (ident: number) => void;
|
||||
|
||||
/**
|
||||
@@ -129,7 +130,7 @@ export interface IOscHandler {
|
||||
* execution of the command should depend on `success`.
|
||||
* To save memory also cleanup data structures here.
|
||||
*/
|
||||
end(success: boolean): void | boolean;
|
||||
end(success: boolean): boolean | Promise<boolean>;
|
||||
}
|
||||
export type OscFallbackHandlerType = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void;
|
||||
|
||||
@@ -160,7 +161,7 @@ export interface IEscapeSequenceParser extends IDisposable {
|
||||
* Parse UTF32 codepoints in `data` up to `length`.
|
||||
* @param data The data to parse.
|
||||
*/
|
||||
parse(data: Uint32Array, length: number): void;
|
||||
parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Get string from numercial function identifier `ident`.
|
||||
@@ -174,29 +175,25 @@ export interface IEscapeSequenceParser extends IDisposable {
|
||||
setPrintHandler(handler: PrintHandlerType): void;
|
||||
clearPrintHandler(): void;
|
||||
|
||||
setEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): void;
|
||||
registerEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): IDisposable;
|
||||
clearEscHandler(id: IFunctionIdentifier): void;
|
||||
setEscHandlerFallback(handler: EscFallbackHandlerType): void;
|
||||
addEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): IDisposable;
|
||||
|
||||
setExecuteHandler(flag: string, handler: ExecuteHandlerType): void;
|
||||
clearExecuteHandler(flag: string): void;
|
||||
setExecuteHandlerFallback(handler: ExecuteFallbackHandlerType): void;
|
||||
|
||||
setCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): void;
|
||||
registerCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): IDisposable;
|
||||
clearCsiHandler(id: IFunctionIdentifier): void;
|
||||
setCsiHandlerFallback(callback: CsiFallbackHandlerType): void;
|
||||
addCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): IDisposable;
|
||||
|
||||
setDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): void;
|
||||
registerDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable;
|
||||
clearDcsHandler(id: IFunctionIdentifier): void;
|
||||
setDcsHandlerFallback(handler: DcsFallbackHandlerType): void;
|
||||
addDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable;
|
||||
|
||||
setOscHandler(ident: number, handler: IOscHandler): void;
|
||||
registerOscHandler(ident: number, handler: IOscHandler): IDisposable;
|
||||
clearOscHandler(ident: number): void;
|
||||
setOscHandlerFallback(handler: OscFallbackHandlerType): void;
|
||||
addOscHandler(ident: number, handler: IOscHandler): IDisposable;
|
||||
|
||||
setErrorHandler(handler: (state: IParsingState) => IParsingState): void;
|
||||
clearErrorHandler(): void;
|
||||
@@ -209,8 +206,7 @@ export interface IEscapeSequenceParser extends IDisposable {
|
||||
*/
|
||||
export interface ISubParser<T, U> extends IDisposable {
|
||||
reset(): void;
|
||||
addHandler(ident: number, handler: T): IDisposable;
|
||||
setHandler(ident: number, handler: T): void;
|
||||
registerHandler(ident: number, handler: T): IDisposable;
|
||||
clearHandler(ident: number): void;
|
||||
setHandlerFallback(handler: U): void;
|
||||
put(data: Uint32Array, start: number, end: number): void;
|
||||
@@ -218,12 +214,12 @@ export interface ISubParser<T, U> extends IDisposable {
|
||||
|
||||
export interface IOscParser extends ISubParser<IOscHandler, OscFallbackHandlerType> {
|
||||
start(): void;
|
||||
end(success: boolean): void;
|
||||
end(success: boolean, promiseResult?: boolean): void | Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface IDcsParser extends ISubParser<IDcsHandler, DcsFallbackHandlerType> {
|
||||
hook(ident: number, params: IParams): void;
|
||||
unhook(success: boolean): void;
|
||||
unhook(success: boolean, promiseResult?: boolean): void | Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -242,3 +238,37 @@ export interface IFunctionIdentifier {
|
||||
export interface IHandlerCollection<T> {
|
||||
[key: string]: T[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Types for async parser support.
|
||||
*/
|
||||
|
||||
// type of saved stack state in parser
|
||||
export const enum ParserStackType {
|
||||
NONE = 0,
|
||||
FAIL,
|
||||
RESET,
|
||||
CSI,
|
||||
ESC,
|
||||
OSC,
|
||||
DCS
|
||||
}
|
||||
|
||||
// aggregate of resumable handler lists
|
||||
export type ResumableHandlersType = CsiHandlerType[] | EscHandlerType[];
|
||||
|
||||
// saved stack state of the parser
|
||||
export interface IParserStackState {
|
||||
state: ParserStackType;
|
||||
handlers: ResumableHandlersType;
|
||||
handlerPos: number;
|
||||
transition: number;
|
||||
chunkPos: number;
|
||||
}
|
||||
|
||||
// saved stack state of subparser (OSC and DCS)
|
||||
export interface ISubParserStackState {
|
||||
paused: boolean;
|
||||
loopPosition: number;
|
||||
fallThrough: boolean;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2021 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi } from 'xterm';
|
||||
import { IBuffer } from 'common/buffer/Types';
|
||||
import { BufferLineApiView } from 'common/public/BufferLineApiView';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
|
||||
export class BufferApiView implements IBufferApi {
|
||||
constructor(
|
||||
private _buffer: IBuffer,
|
||||
public readonly type: 'normal' | 'alternate'
|
||||
) { }
|
||||
|
||||
public init(buffer: IBuffer): BufferApiView {
|
||||
this._buffer = buffer;
|
||||
return this;
|
||||
}
|
||||
|
||||
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 | undefined {
|
||||
const line = this._buffer.lines.get(y);
|
||||
if (!line) {
|
||||
return undefined;
|
||||
}
|
||||
return new BufferLineApiView(line);
|
||||
}
|
||||
public getNullCell(): IBufferCellApi { return new CellData(); }
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2021 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IBufferLine, ICellData } from 'common/Types';
|
||||
import { IBufferCell as IBufferCellApi, IBufferLine as IBufferLineApi } from 'xterm';
|
||||
|
||||
export class BufferLineApiView implements IBufferLineApi {
|
||||
constructor(private _line: IBufferLine) { }
|
||||
|
||||
public get isWrapped(): boolean { return this._line.isWrapped; }
|
||||
public get length(): number { return this._line.length; }
|
||||
public getCell(x: number, cell?: IBufferCellApi): IBufferCellApi | undefined {
|
||||
if (x < 0 || x >= this._line.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (cell) {
|
||||
this._line.loadCell(x, cell as ICellData);
|
||||
return cell;
|
||||
}
|
||||
return this._line.loadCell(x, new CellData());
|
||||
}
|
||||
public translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string {
|
||||
return this._line.translateToString(trimRight, startColumn, endColumn);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2021 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IBuffer as IBufferApi, IBufferNamespace as IBufferNamespaceApi } from 'xterm';
|
||||
import { BufferApiView } from 'common/public/BufferApiView';
|
||||
import { IEvent, EventEmitter } from 'common/EventEmitter';
|
||||
import { ICoreTerminal } from 'common/Types';
|
||||
|
||||
export class BufferNamespaceApi implements IBufferNamespaceApi {
|
||||
private _normal: BufferApiView;
|
||||
private _alternate: BufferApiView;
|
||||
private _onBufferChange = new EventEmitter<IBufferApi>();
|
||||
public get onBufferChange(): IEvent<IBufferApi> { return this._onBufferChange.event; }
|
||||
|
||||
constructor(private _core: ICoreTerminal) {
|
||||
this._normal = new BufferApiView(this._core.buffers.normal, 'normal');
|
||||
this._alternate = new BufferApiView(this._core.buffers.alt, 'alternate');
|
||||
this._core.buffers.onBufferActivate(() => this._onBufferChange.fire(this.active));
|
||||
}
|
||||
public get active(): IBufferApi {
|
||||
if (this._core.buffers.active === this._core.buffers.normal) { return this.normal; }
|
||||
if (this._core.buffers.active === this._core.buffers.alt) { return this.alternate; }
|
||||
throw new Error('Active buffer is neither normal nor alternate');
|
||||
}
|
||||
public get normal(): IBufferApi {
|
||||
return this._normal.init(this._core.buffers.normal);
|
||||
}
|
||||
public get alternate(): IBufferApi {
|
||||
return this._alternate.init(this._core.buffers.alt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2021 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IParams } from 'common/parser/Types';
|
||||
import { IDisposable, IFunctionIdentifier, IParser } from 'xterm';
|
||||
import { ICoreTerminal } from 'common/Types';
|
||||
|
||||
export class ParserApi implements IParser {
|
||||
constructor(private _core: ICoreTerminal) { }
|
||||
|
||||
public registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean | Promise<boolean>): IDisposable {
|
||||
return this._core.registerCsiHandler(id, (params: IParams) => callback(params.toArray()));
|
||||
}
|
||||
public addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean | Promise<boolean>): IDisposable {
|
||||
return this.registerCsiHandler(id, callback);
|
||||
}
|
||||
public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean | Promise<boolean>): IDisposable {
|
||||
return this._core.registerDcsHandler(id, (data: string, params: IParams) => callback(data, params.toArray()));
|
||||
}
|
||||
public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean | Promise<boolean>): IDisposable {
|
||||
return this.registerDcsHandler(id, callback);
|
||||
}
|
||||
public registerEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise<boolean>): IDisposable {
|
||||
return this._core.registerEscHandler(id, handler);
|
||||
}
|
||||
public addEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise<boolean>): IDisposable {
|
||||
return this.registerEscHandler(id, handler);
|
||||
}
|
||||
public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
|
||||
return this._core.registerOscHandler(ident, callback);
|
||||
}
|
||||
public addOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
|
||||
return this.registerOscHandler(ident, callback);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) 2021 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ICoreTerminal } from 'common/Types';
|
||||
import { IUnicodeHandling, IUnicodeVersionProvider } from 'xterm';
|
||||
|
||||
export class UnicodeApi implements IUnicodeHandling {
|
||||
constructor(private _core: ICoreTerminal) { }
|
||||
|
||||
public register(provider: IUnicodeVersionProvider): void {
|
||||
this._core.unicodeService.register(provider);
|
||||
}
|
||||
|
||||
public get versions(): string[] {
|
||||
return this._core.unicodeService.versions;
|
||||
}
|
||||
|
||||
public get activeVersion(): string {
|
||||
return this._core.unicodeService.activeVersion;
|
||||
}
|
||||
|
||||
public set activeVersion(version: string) {
|
||||
this._core.unicodeService.activeVersion = version;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { BufferSet } from 'common/buffer/BufferSet';
|
||||
import { IBufferSet, IBuffer } from 'common/buffer/Types';
|
||||
import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { IAttributeData, IBufferLine, ScrollSource } from 'common/Types';
|
||||
|
||||
export const MINIMUM_COLS = 2; // Less than 2 can mess with wide chars
|
||||
export const MINIMUM_ROWS = 1;
|
||||
@@ -23,9 +24,14 @@ export class BufferService extends Disposable implements IBufferService {
|
||||
|
||||
private _onResize = new EventEmitter<{ cols: number, rows: number }>();
|
||||
public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; }
|
||||
private _onScroll = new EventEmitter<number>();
|
||||
public get onScroll(): IEvent<number> { return this._onScroll.event; }
|
||||
|
||||
public get buffer(): IBuffer { return this.buffers.active; }
|
||||
|
||||
/** An IBufferline to clone/copy from for new blank lines */
|
||||
private _cachedBlankLine: IBufferLine | undefined;
|
||||
|
||||
constructor(
|
||||
@IOptionsService private _optionsService: IOptionsService
|
||||
) {
|
||||
@@ -52,4 +58,128 @@ export class BufferService extends Disposable implements IBufferService {
|
||||
this.buffers.reset();
|
||||
this.isUserScrolling = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll the terminal down 1 row, creating a blank line.
|
||||
* @param isWrapped Whether the new line is wrapped from the previous line.
|
||||
*/
|
||||
public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void {
|
||||
const buffer = this.buffer;
|
||||
|
||||
let newLine: IBufferLine | undefined;
|
||||
newLine = this._cachedBlankLine;
|
||||
if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) {
|
||||
newLine = buffer.getBlankLine(eraseAttr, isWrapped);
|
||||
this._cachedBlankLine = newLine;
|
||||
}
|
||||
newLine.isWrapped = isWrapped;
|
||||
|
||||
const topRow = buffer.ybase + buffer.scrollTop;
|
||||
const bottomRow = buffer.ybase + buffer.scrollBottom;
|
||||
|
||||
if (buffer.scrollTop === 0) {
|
||||
// Determine whether the buffer is going to be trimmed after insertion.
|
||||
const willBufferBeTrimmed = buffer.lines.isFull;
|
||||
|
||||
// Insert the line using the fastest method
|
||||
if (bottomRow === buffer.lines.length - 1) {
|
||||
if (willBufferBeTrimmed) {
|
||||
buffer.lines.recycle().copyFrom(newLine);
|
||||
} else {
|
||||
buffer.lines.push(newLine.clone());
|
||||
}
|
||||
} else {
|
||||
buffer.lines.splice(bottomRow + 1, 0, newLine.clone());
|
||||
}
|
||||
|
||||
// Only adjust ybase and ydisp when the buffer is not trimmed
|
||||
if (!willBufferBeTrimmed) {
|
||||
buffer.ybase++;
|
||||
// Only scroll the ydisp with ybase if the user has not scrolled up
|
||||
if (!this.isUserScrolling) {
|
||||
buffer.ydisp++;
|
||||
}
|
||||
} else {
|
||||
// When the buffer is full and the user has scrolled up, keep the text
|
||||
// stable unless ydisp is right at the top
|
||||
if (this.isUserScrolling) {
|
||||
buffer.ydisp = Math.max(buffer.ydisp - 1, 0);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// scrollTop is non-zero which means no line will be going to the
|
||||
// scrollback, instead we can just shift them in-place.
|
||||
const scrollRegionHeight = bottomRow - topRow + 1 /* as it's zero-based */;
|
||||
buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1);
|
||||
buffer.lines.set(bottomRow, newLine.clone());
|
||||
}
|
||||
|
||||
// Move the viewport to the bottom of the buffer unless the user is
|
||||
// scrolling.
|
||||
if (!this.isUserScrolling) {
|
||||
buffer.ydisp = buffer.ybase;
|
||||
}
|
||||
|
||||
this._onScroll.fire(buffer.ydisp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll the display of the terminal
|
||||
* @param disp The number of lines to scroll down (negative scroll up).
|
||||
* @param suppressScrollEvent Don't emit the scroll event as scrollLines. This is used
|
||||
* to avoid unwanted events being handled by the viewport when the event was triggered from the
|
||||
* viewport originally.
|
||||
*/
|
||||
public scrollLines(disp: number, suppressScrollEvent?: boolean, source?: ScrollSource): void {
|
||||
const buffer = this.buffer;
|
||||
if (disp < 0) {
|
||||
if (buffer.ydisp === 0) {
|
||||
return;
|
||||
}
|
||||
this.isUserScrolling = true;
|
||||
} else if (disp + buffer.ydisp >= buffer.ybase) {
|
||||
this.isUserScrolling = false;
|
||||
}
|
||||
|
||||
const oldYdisp = buffer.ydisp;
|
||||
buffer.ydisp = Math.max(Math.min(buffer.ydisp + disp, buffer.ybase), 0);
|
||||
|
||||
// No change occurred, don't trigger scroll/refresh
|
||||
if (oldYdisp === buffer.ydisp) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!suppressScrollEvent) {
|
||||
this._onScroll.fire(buffer.ydisp);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scroll the display of the terminal by a number of pages.
|
||||
* @param pageCount The number of pages to scroll (negative scrolls up).
|
||||
*/
|
||||
public scrollPages(pageCount: number): void {
|
||||
this.scrollLines(pageCount * (this.rows - 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls the display of the terminal to the top.
|
||||
*/
|
||||
public scrollToTop(): void {
|
||||
this.scrollLines(-this.buffer.ydisp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls the display of the terminal to the bottom.
|
||||
*/
|
||||
public scrollToBottom(): void {
|
||||
this.scrollLines(this.buffer.ybase - this.buffer.ydisp);
|
||||
}
|
||||
|
||||
public scrollToLine(line: number): void {
|
||||
const scrollAmount = line - this.buffer.ydisp;
|
||||
if (scrollAmount !== 0) {
|
||||
this.scrollLines(scrollAmount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ export class ServiceCollection {
|
||||
}
|
||||
|
||||
export class InstantiationService implements IInstantiationService {
|
||||
public serviceBrand: undefined;
|
||||
|
||||
private readonly _services: ServiceCollection = new ServiceCollection();
|
||||
|
||||
constructor() {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ILogService, IOptionsService } from 'common/services/Services';
|
||||
import { ILogService, IOptionsService, LogLevelEnum } from 'common/services/Services';
|
||||
|
||||
type LogType = (message?: any, ...optionalParams: any[]) => void;
|
||||
|
||||
@@ -19,21 +19,12 @@ interface IConsole {
|
||||
// module doesn't depend on them so we need to explicitly declare it.
|
||||
declare const console: IConsole;
|
||||
|
||||
|
||||
export enum LogLevel {
|
||||
DEBUG = 0,
|
||||
INFO = 1,
|
||||
WARN = 2,
|
||||
ERROR = 3,
|
||||
OFF = 4
|
||||
}
|
||||
|
||||
const optionsKeyToLogLevel: { [key: string]: LogLevel } = {
|
||||
debug: LogLevel.DEBUG,
|
||||
info: LogLevel.INFO,
|
||||
warn: LogLevel.WARN,
|
||||
error: LogLevel.ERROR,
|
||||
off: LogLevel.OFF
|
||||
const optionsKeyToLogLevel: { [key: string]: LogLevelEnum } = {
|
||||
debug: LogLevelEnum.DEBUG,
|
||||
info: LogLevelEnum.INFO,
|
||||
warn: LogLevelEnum.WARN,
|
||||
error: LogLevelEnum.ERROR,
|
||||
off: LogLevelEnum.OFF
|
||||
};
|
||||
|
||||
const LOG_PREFIX = 'xterm.js: ';
|
||||
@@ -41,7 +32,7 @@ const LOG_PREFIX = 'xterm.js: ';
|
||||
export class LogService implements ILogService {
|
||||
public serviceBrand: any;
|
||||
|
||||
private _logLevel!: LogLevel;
|
||||
public logLevel: LogLevelEnum = LogLevelEnum.OFF;
|
||||
|
||||
constructor(
|
||||
@IOptionsService private readonly _optionsService: IOptionsService
|
||||
@@ -55,7 +46,7 @@ export class LogService implements ILogService {
|
||||
}
|
||||
|
||||
private _updateLogLevel(): void {
|
||||
this._logLevel = optionsKeyToLogLevel[this._optionsService.options.logLevel];
|
||||
this.logLevel = optionsKeyToLogLevel[this._optionsService.options.logLevel];
|
||||
}
|
||||
|
||||
private _evalLazyOptionalParams(optionalParams: any[]): void {
|
||||
@@ -72,25 +63,25 @@ export class LogService implements ILogService {
|
||||
}
|
||||
|
||||
public debug(message: string, ...optionalParams: any[]): void {
|
||||
if (this._logLevel <= LogLevel.DEBUG) {
|
||||
if (this.logLevel <= LogLevelEnum.DEBUG) {
|
||||
this._log(console.log, message, optionalParams);
|
||||
}
|
||||
}
|
||||
|
||||
public info(message: string, ...optionalParams: any[]): void {
|
||||
if (this._logLevel <= LogLevel.INFO) {
|
||||
if (this.logLevel <= LogLevelEnum.INFO) {
|
||||
this._log(console.info, message, optionalParams);
|
||||
}
|
||||
}
|
||||
|
||||
public warn(message: string, ...optionalParams: any[]): void {
|
||||
if (this._logLevel <= LogLevel.WARN) {
|
||||
if (this.logLevel <= LogLevelEnum.WARN) {
|
||||
this._log(console.warn, message, optionalParams);
|
||||
}
|
||||
}
|
||||
|
||||
public error(message: string, ...optionalParams: any[]): void {
|
||||
if (this._logLevel <= LogLevel.ERROR) {
|
||||
if (this.logLevel <= LogLevelEnum.ERROR) {
|
||||
this._log(console.error, message, optionalParams);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { IEvent } from 'common/EventEmitter';
|
||||
import { IBuffer, IBufferSet } from 'common/buffer/Types';
|
||||
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes } from 'common/Types';
|
||||
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource } from 'common/Types';
|
||||
import { createDecorator } from 'common/services/ServiceRegistry';
|
||||
|
||||
export const IBufferService = createDecorator<IBufferService>('BufferService');
|
||||
@@ -17,9 +17,14 @@ export interface IBufferService {
|
||||
readonly buffer: IBuffer;
|
||||
readonly buffers: IBufferSet;
|
||||
isUserScrolling: boolean;
|
||||
|
||||
onResize: IEvent<{ cols: number, rows: number }>;
|
||||
|
||||
onScroll: IEvent<number>;
|
||||
scroll(eraseAttr: IAttributeData, isWrapped?: boolean): void;
|
||||
scrollToBottom(): void;
|
||||
scrollToTop(): void;
|
||||
scrollToLine(line: number): void;
|
||||
scrollLines(disp: number, suppressScrollEvent?: boolean, source?: ScrollSource): void;
|
||||
scrollPages(pageCount: number): void;
|
||||
resize(cols: number, rows: number): void;
|
||||
reset(): void;
|
||||
}
|
||||
@@ -152,6 +157,8 @@ type GetLeadingNonServiceArgs<Args> =
|
||||
|
||||
export const IInstantiationService = createDecorator<IInstantiationService>('InstantiationService');
|
||||
export interface IInstantiationService {
|
||||
serviceBrand: undefined;
|
||||
|
||||
setService<T>(id: IServiceIdentifier<T>, instance: T): void;
|
||||
getService<T>(id: IServiceIdentifier<T>): T | undefined;
|
||||
createInstance<Ctor extends new (...args: any[]) => any, R extends InstanceType<Ctor>>(t: Ctor, ...args: GetLeadingNonServiceArgs<ConstructorParameters<Ctor>>): R;
|
||||
@@ -161,6 +168,8 @@ export const ILogService = createDecorator<ILogService>('LogService');
|
||||
export interface ILogService {
|
||||
serviceBrand: undefined;
|
||||
|
||||
logLevel: LogLevelEnum;
|
||||
|
||||
debug(message: any, ...optionalParams: any[]): void;
|
||||
info(message: any, ...optionalParams: any[]): void;
|
||||
warn(message: any, ...optionalParams: any[]): void;
|
||||
@@ -181,6 +190,13 @@ export interface IOptionsService {
|
||||
|
||||
export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' | number;
|
||||
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'off';
|
||||
export enum LogLevelEnum {
|
||||
DEBUG = 0,
|
||||
INFO = 1,
|
||||
WARN = 2,
|
||||
ERROR = 3,
|
||||
OFF = 4
|
||||
}
|
||||
export type RendererType = 'dom' | 'canvas';
|
||||
|
||||
export interface IPartialTerminalOptions {
|
||||
|
||||
Reference in New Issue
Block a user