Merge remote-tracking branch 'ups/master' into pr/schrej/3205

This commit is contained in:
Daniel Imms
2021-04-01 08:36:14 -07:00
64 changed files with 4666 additions and 2205 deletions
+2 -2
View File
@@ -89,8 +89,8 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA
export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, screenElement: HTMLElement, selectionService: ISelectionService, shouldSelectWord: boolean): void {
moveTextAreaUnderMouseCursor(ev, textarea, screenElement);
if (shouldSelectWord && !selectionService.isClickInSelection(ev)) {
selectionService.selectWordAtCursor(ev);
if (shouldSelectWord) {
selectionService.rightClickSelect(ev);
}
// Get textarea ready to copy from the context menu
+2 -11
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { ILinkifier2, ILinkProvider, IBufferCellPosition, ILink, ILinkifierEvent, ILinkDecorations } from 'browser/Types';
import { ILinkifier2, ILinkProvider, IBufferCellPosition, ILink, ILinkifierEvent, ILinkDecorations, ILinkWithState } from 'browser/Types';
import { IDisposable } from 'common/Types';
import { IMouseService, IRenderService } from './services/Services';
import { IBufferService } from 'common/services/Services';
@@ -11,21 +11,12 @@ import { EventEmitter, IEvent } from 'common/EventEmitter';
import { Disposable, getDisposeArrayDisposable, disposeArray } from 'common/Lifecycle';
import { addDisposableDomListener } from 'browser/Lifecycle';
interface ILinkState {
decorations: ILinkDecorations;
isHovered: boolean;
}
interface ILinkWithState {
link: ILink;
state?: ILinkState;
}
export class Linkifier2 extends Disposable implements ILinkifier2 {
private _element: HTMLElement | undefined;
private _mouseService: IMouseService | undefined;
private _renderService: IRenderService | undefined;
private _linkProviders: ILinkProvider[] = [];
public get currentLink(): ILinkWithState | undefined { return this._currentLink; }
protected _currentLink: ILinkWithState | undefined;
private _lastMouseEvent: MouseEvent | undefined;
private _linkCacheDisposables: IDisposable[] = [];
+1 -3
View File
@@ -156,9 +156,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
private _onTooltip(e: MouseEvent): void {
this._tooltipTimeout = undefined;
const zone = this._findZoneEventAt(e);
if (zone && zone.tooltipCallback) {
zone.tooltipCallback(e);
}
zone?.tooltipCallback(e);
}
private _onMouseDown(e: MouseEvent): void {
File diff suppressed because it is too large Load Diff
+28 -19
View File
@@ -162,11 +162,11 @@ export class Terminal extends CoreTerminal implements ITerminal {
private _changeAnsiColor(event: IAnsiColorChangeEvent): void {
if (!this._colorManager) { return; }
event.colors.forEach(ansiColor => {
for (const ansiColor of event.colors) {
const color = rgba.toColor(ansiColor.red, ansiColor.green, ansiColor.blue);
this._colorManager!.colors.ansi[ansiColor.colorIndex] = color;
});
}
this._renderService?.setColors(this._colorManager!.colors);
this.viewport?.onThemeChange(this._colorManager!.colors);
@@ -297,19 +297,26 @@ export class Terminal extends CoreTerminal implements ITerminal {
}
private _syncTextArea(): void {
if (!this.textarea || !this.buffer.isCursorInViewport || this._compositionHelper!.isComposing) {
if (!this.textarea || !this.buffer.isCursorInViewport || this._compositionHelper!.isComposing || !this._renderService) {
return;
}
const cellHeight = Math.ceil(this._charSizeService!.height * this.optionsService.options.lineHeight);
const cursorTop = this._bufferService.buffer.y * cellHeight;
const cursorLeft = this._bufferService.buffer.x * this._charSizeService!.width;
const cursorY = this.buffer.ybase + this.buffer.y;
const bufferLine = this.buffer.lines.get(cursorY);
if (!bufferLine) {
return;
}
const cursorX = Math.min(this.buffer.x, this.cols - 1);
const cellHeight = this._renderService.dimensions.actualCellHeight;
const width = bufferLine.getWidth(cursorX);
const cellWidth = this._renderService.dimensions.actualCellWidth * width;
const cursorTop = this.buffer.y * this._renderService.dimensions.actualCellHeight;
const cursorLeft = cursorX * this._renderService.dimensions.actualCellWidth;
// Sync the textarea to the exact position of the composition view so the IME knows where the
// text is.
this.textarea.style.left = cursorLeft + 'px';
this.textarea.style.top = cursorTop + 'px';
this.textarea.style.width = this._charSizeService!.width + 'px';
this.textarea.style.width = cellWidth + 'px';
this.textarea.style.height = cellHeight + 'px';
this.textarea.style.lineHeight = cellHeight + 'px';
this.textarea.style.zIndex = '-5';
@@ -386,7 +393,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
throw new Error('Terminal requires a parent element.');
}
if (!document.body.contains(parent)) {
if (!parent.isConnected) {
this._logService.debug('Terminal.open was called on an element that was not attached to the DOM');
}
@@ -438,14 +445,6 @@ export class Terminal extends CoreTerminal implements ITerminal {
this._charSizeService = this._instantiationService.createInstance(CharSizeService, this._document, this._helperContainer);
this._instantiationService.setService(ICharSizeService, this._charSizeService);
this._compositionView = document.createElement('div');
this._compositionView.classList.add('composition-view');
this._compositionHelper = this._instantiationService.createInstance(CompositionHelper, this.textarea, this._compositionView);
this._helperContainer.appendChild(this._compositionView);
// Performance: Add viewport and helper elements from the fragment
this.element.appendChild(fragment);
this._theme = this.options.theme || this._theme;
this._colorManager = new ColorManager(document, this.options.allowTransparency);
this.register(this.optionsService.onOptionChange(e => this._colorManager!.onOptionsChange(e)));
@@ -457,6 +456,14 @@ export class Terminal extends CoreTerminal implements ITerminal {
this.register(this._renderService.onRenderedBufferChange(e => this._onRender.fire(e)));
this.onResize(e => this._renderService!.resize(e.cols, e.rows));
this._compositionView = document.createElement('div');
this._compositionView.classList.add('composition-view');
this._compositionHelper = this._instantiationService.createInstance(CompositionHelper, this.textarea, this._compositionView);
this._helperContainer.appendChild(this._compositionView);
// Performance: Add viewport and helper elements from the fragment
this.element.appendChild(fragment);
this._soundService = this._instantiationService.createInstance(SoundService);
this._instantiationService.setService(ISoundService, this._soundService);
this._mouseService = this._instantiationService.createInstance(MouseService);
@@ -482,7 +489,9 @@ export class Terminal extends CoreTerminal implements ITerminal {
this._selectionService = this.register(this._instantiationService.createInstance(SelectionService,
this.element,
this.screenElement));
this.screenElement,
this.linkifier2
));
this._instantiationService.setService(ISelectionService, this._selectionService);
this.register(this._selectionService.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent)));
this.register(this._selectionService.onSelectionChange(() => this._onSelectionChange.fire()));
@@ -836,7 +845,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
* Change the cursor style for different selection modes
*/
public updateCursorStyle(ev: KeyboardEvent): void {
if (this._selectionService && this._selectionService.shouldColumnSelect(ev)) {
if (this._selectionService?.shouldColumnSelect(ev)) {
this.element!.classList.add('column-select');
} else {
this.element!.classList.remove('column-select');
+1 -1
View File
@@ -74,7 +74,7 @@ describe('Escape Sequence Files', function(): void {
let content = '';
const OSC_CODE = 12345;
await new Promise(resolve => {
customHandler = term.addOscHandler(OSC_CODE, () => {
customHandler = term.registerOscHandler(OSC_CODE, () => {
// grab terminal viewport content
content = terminalToString(term);
resolve();
+7 -4
View File
@@ -21,6 +21,9 @@ export class TestTerminal extends Terminal {
public get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; }
public keyDown(ev: any): boolean | undefined { return this._keyDown(ev); }
public keyPress(ev: any): boolean { return this._keyPress(ev); }
public writeP(data: string | Uint8Array): Promise<void> {
return new Promise(r => this.write(data, r));
}
}
export class MockTerminal implements ITerminal {
@@ -75,16 +78,16 @@ export class MockTerminal implements ITerminal {
public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
throw new Error('Method not implemented.');
}
public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable {
public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise<boolean>): IDisposable {
throw new Error('Method not implemented.');
}
public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable {
public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable {
throw new Error('Method not implemented.');
}
public addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable {
public registerEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise<boolean>): IDisposable {
throw new Error('Method not implemented.');
}
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
throw new Error('Method not implemented.');
}
public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number {
+14 -4
View File
@@ -52,10 +52,10 @@ export interface IPublicTerminal extends IDisposable {
resize(columns: number, rows: number): void;
open(parent: HTMLElement): void;
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void;
addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable;
addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable;
addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable;
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
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;
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number;
deregisterLinkMatcher(matcherId: number): void;
registerLinkProvider(linkProvider: ILinkProvider): IDisposable;
@@ -205,9 +205,19 @@ export interface ILinkifier {
deregisterLinkMatcher(matcherId: number): boolean;
}
interface ILinkState {
decorations: ILinkDecorations;
isHovered: boolean;
}
export interface ILinkWithState {
link: ILink;
state?: ILinkState;
}
export interface ILinkifier2 {
onShowLinkUnderline: IEvent<ILinkifierEvent>;
onHideLinkUnderline: IEvent<ILinkifierEvent>;
readonly currentLink: ILinkWithState | undefined;
attachToDom(element: HTMLElement, mouseService: IMouseService, renderService: IRenderService): void;
registerLinkProvider(linkProvider: ILinkProvider): IDisposable;
+2 -2
View File
@@ -5,7 +5,7 @@
import { assert } from 'chai';
import { CompositionHelper } from 'browser/input/CompositionHelper';
import { MockCharSizeService } from 'browser/TestUtils.test';
import { MockRenderService } from 'browser/TestUtils.test';
import { MockCoreService, MockBufferService, MockOptionsService } from 'common/TestUtils.test';
describe('CompositionHelper', () => {
@@ -42,7 +42,7 @@ describe('CompositionHelper', () => {
};
handledText = '';
const bufferService = new MockBufferService(10, 5);
compositionHelper = new CompositionHelper(textarea, compositionView, bufferService, new MockOptionsService(), new MockCharSizeService(10, 10), coreService);
compositionHelper = new CompositionHelper(textarea, compositionView, bufferService, new MockOptionsService(), coreService, new MockRenderService());
});
describe('Input', () => {
+11 -8
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { ICharSizeService } from 'browser/services/Services';
import { IRenderService } from 'browser/services/Services';
import { IBufferService, ICoreService, IOptionsService } from 'common/services/Services';
interface IPosition {
@@ -45,8 +45,8 @@ export class CompositionHelper {
private readonly _compositionView: HTMLElement,
@IBufferService private readonly _bufferService: IBufferService,
@IOptionsService private readonly _optionsService: IOptionsService,
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@ICoreService private readonly _coreService: ICoreService
@ICoreService private readonly _coreService: ICoreService,
@IRenderService private readonly _renderService: IRenderService
) {
this._isComposing = false;
this._isSendingComposition = false;
@@ -207,9 +207,11 @@ export class CompositionHelper {
}
if (this._bufferService.buffer.isCursorInViewport) {
const cellHeight = Math.ceil(this._charSizeService.height * this._optionsService.options.lineHeight);
const cursorTop = this._bufferService.buffer.y * cellHeight;
const cursorLeft = this._bufferService.buffer.x * this._charSizeService.width;
const cursorX = Math.min(this._bufferService.buffer.x, this._bufferService.cols - 1);
const cellHeight = this._renderService.dimensions.actualCellHeight;
const cursorTop = this._bufferService.buffer.y * this._renderService.dimensions.actualCellHeight;
const cursorLeft = cursorX * this._renderService.dimensions.actualCellWidth;
this._compositionView.style.left = cursorLeft + 'px';
this._compositionView.style.top = cursorTop + 'px';
@@ -222,8 +224,9 @@ export class CompositionHelper {
const compositionViewBounds = this._compositionView.getBoundingClientRect();
this._textarea.style.left = cursorLeft + 'px';
this._textarea.style.top = cursorTop + 'px';
this._textarea.style.width = compositionViewBounds.width + 'px';
this._textarea.style.height = compositionViewBounds.height + 'px';
// Ensure the text area is at least 1x1, otherwise certain IMEs may break
this._textarea.style.width = Math.max(compositionViewBounds.width, 1) + 'px';
this._textarea.style.height = Math.max(compositionViewBounds.height, 1) + 'px';
this._textarea.style.lineHeight = compositionViewBounds.height + 'px';
}
+12 -12
View File
@@ -292,28 +292,28 @@ class BufferLineApiView implements IBufferLineApi {
class ParserApi implements IParser {
constructor(private _core: ITerminal) { }
public registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable {
return this._core.addCsiHandler(id, (params: IParams) => callback(params.toArray()));
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): IDisposable {
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): IDisposable {
return this._core.addDcsHandler(id, (data: string, params: IParams) => callback(data, params.toArray()));
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): IDisposable {
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): IDisposable {
return this._core.addEscHandler(id, handler);
public registerEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise<boolean>): IDisposable {
return this._core.registerEscHandler(id, handler);
}
public addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable {
public addEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise<boolean>): IDisposable {
return this.registerEscHandler(id, handler);
}
public registerOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
return this._core.addOscHandler(ident, callback);
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): IDisposable {
public addOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
return this.registerOscHandler(ident, callback);
}
}
+4
View File
@@ -174,6 +174,10 @@ export class RenderService extends Disposable implements IRenderService {
}
public onDevicePixelRatioChange(): void {
// Force char size measurement as DomMeasureStrategy(getBoundingClientRect) is not stable
// when devicePixelRatio changes
this._charSizeService.measure();
this._renderer.onDevicePixelRatioChange();
this.refreshRows(0, this._rowCount - 1);
}
@@ -21,7 +21,7 @@ class TestSelectionService extends SelectionService {
optionsService: IOptionsService,
renderService: IRenderService
) {
super(null!, null!, bufferService, new MockCoreService(), new MockMouseService(), optionsService, renderService);
super(null!, null!, null!, bufferService, new MockCoreService(), new MockMouseService(), optionsService, renderService);
}
public get model(): SelectionModel { return this._model; }
+63 -10
View File
@@ -11,6 +11,7 @@ import { SelectionModel } from 'browser/selection/SelectionModel';
import { CellData } from 'common/buffer/CellData';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { ICharSizeService, IMouseService, ISelectionService, IRenderService } from 'browser/services/Services';
import { ILinkifier2 } from 'browser/Types';
import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services';
import { getCoordsRelativeToElement } from 'browser/input/Mouse';
import { moveToCellSequence } from 'browser/input/MoveToCell';
@@ -105,6 +106,9 @@ export class SelectionService extends Disposable implements ISelectionService {
private _workCell: CellData = new CellData();
private _mouseDownTimeStamp: number = 0;
private _oldHasSelection: boolean = false;
private _oldSelectionStart: [number, number] | undefined = undefined;
private _oldSelectionEnd: [number, number] | undefined = undefined;
private _onLinuxMouseSelection = this.register(new EventEmitter<string>());
public get onLinuxMouseSelection(): IEvent<string> { return this._onLinuxMouseSelection.event; }
@@ -118,6 +122,7 @@ export class SelectionService extends Disposable implements ISelectionService {
constructor(
private readonly _element: HTMLElement,
private readonly _screenElement: HTMLElement,
private readonly _linkifier: ILinkifier2,
@IBufferService private readonly _bufferService: IBufferService,
@ICoreService private readonly _coreService: ICoreService,
@IMouseService private readonly _mouseService: IMouseService,
@@ -290,7 +295,7 @@ export class SelectionService extends Disposable implements ISelectionService {
* Checks if the current click was inside the current selection
* @param event The mouse event
*/
public isClickInSelection(event: MouseEvent): boolean {
private _isClickInSelection(event: MouseEvent): boolean {
const coords = this._getMouseBufferCoords(event);
const start = this._model.finalSelectionStart;
const end = this._model.finalSelectionEnd;
@@ -313,13 +318,22 @@ export class SelectionService extends Disposable implements ISelectionService {
* Selects word at the current mouse event coordinates.
* @param event The mouse event.
*/
public selectWordAtCursor(event: MouseEvent): void {
private _selectWordAtCursor(event: MouseEvent, allowWhitespaceOnlySelection: boolean): boolean {
// Check if there is a link under the cursor first and select that if so
const range = this._linkifier.currentLink?.link?.range;
if (range) {
this._model.selectionStart = [range.start.x - 1, range.start.y - 1];
this._model.selectionEnd = [range.end.x, range.end.y - 1];
return true;
}
const coords = this._getMouseBufferCoords(event);
if (coords) {
this._selectWordAt(coords, false);
this._selectWordAt(coords, allowWhitespaceOnlySelection);
this._model.selectionEnd = undefined;
this.refresh(true);
return true;
}
return false;
}
/**
@@ -524,14 +538,12 @@ export class SelectionService extends Disposable implements ISelectionService {
}
/**
* Performs a double click, selecting the current work.
* Performs a double click, selecting the current word.
* @param event The mouse event.
*/
private _onDoubleClick(event: MouseEvent): void {
const coords = this._getMouseBufferCoords(event);
if (coords) {
if (this._selectWordAtCursor(event, true)) {
this._activeSelectionMode = SelectionMode.WORD;
this._selectWordAt(coords, true);
}
}
@@ -681,11 +693,43 @@ export class SelectionService extends Disposable implements ISelectionService {
this._coreService.triggerDataEvent(sequence, true);
}
}
} else if (this.hasSelection) {
this._onSelectionChange.fire();
} else {
this._fireEventIfSelectionChanged();
}
}
private _fireEventIfSelectionChanged(): void {
const start = this._model.finalSelectionStart;
const end = this._model.finalSelectionEnd;
const hasSelection = !!start && !!end && (start[0] !== end[0] || start[1] !== end[1]);
if (!hasSelection) {
if (this._oldHasSelection) {
this._fireOnSelectionChange(start, end, hasSelection);
}
return;
}
// Sanity check, these should not be undefined as there is a selection
if (!start || !end) {
return;
}
if (!this._oldSelectionStart || !this._oldSelectionEnd || (
start[0] !== this._oldSelectionStart[0] || start[1] !== this._oldSelectionStart[1] ||
end[0] !== this._oldSelectionEnd[0] || end[1] !== this._oldSelectionEnd[1])) {
this._fireOnSelectionChange(start, end, hasSelection);
}
}
private _fireOnSelectionChange(start: [number, number] | undefined, end: [number, number] | undefined, hasSelection: boolean): void {
this._oldSelectionStart = start;
this._oldSelectionEnd = end;
this._oldHasSelection = hasSelection;
this._onSelectionChange.fire();
}
private _onBufferActivate(e: {activeBuffer: IBuffer, inactiveBuffer: IBuffer}): void {
this.clearSelection();
// Only adjust the selection on trim, shiftElements is rarely used (only in
@@ -727,6 +771,15 @@ export class SelectionService extends Disposable implements ISelectionService {
this.refresh();
}
public rightClickSelect(ev: MouseEvent): void {
if (!this._isClickInSelection(ev)) {
if (this._selectWordAtCursor(ev, false)) {
this.refresh(true);
}
this._fireEventIfSelectionChanged();
}
}
/**
* Gets positional information for the word at the coordinated specified.
* @param coords The coordinates to get the word at.
+1 -2
View File
@@ -91,8 +91,7 @@ export interface ISelectionService {
selectAll(): void;
selectLines(start: number, end: number): void;
clearSelection(): void;
isClickInSelection(event: MouseEvent): boolean;
selectWordAtCursor(event: MouseEvent): void;
rightClickSelect(event: MouseEvent): void;
shouldColumnSelect(event: KeyboardEvent | MouseEvent): boolean;
shouldForceSelection(event: MouseEvent): boolean;
refresh(isLinuxMouseSelection?: boolean): void;
+2 -2
View File
@@ -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));
});
});
+29 -13
View File
@@ -22,7 +22,7 @@
*/
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';
@@ -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;
@@ -113,7 +116,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
this.register(this.optionsService.onOptionChange(key => this._updateOptions(key)));
// 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 {
@@ -129,8 +132,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 {
@@ -282,23 +298,23 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
}
/** 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 {
@@ -336,7 +352,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;
}));
File diff suppressed because it is too large Load Diff
+344 -194
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -3,7 +3,7 @@
* @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';
@@ -92,6 +92,7 @@ export class MockDirtyRowService implements IDirtyRowService {
export class MockLogService implements ILogService {
public serviceBrand: any;
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 {}
+14 -2
View File
@@ -3,7 +3,7 @@
* @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';
@@ -359,8 +359,12 @@ 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;
@@ -437,3 +441,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;
}
+84 -84
View File
@@ -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', () => {
+34 -4
View File
@@ -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);
+8 -4
View File
@@ -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;
+21
View File
@@ -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
View File
@@ -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;
}
+223 -17
View File
@@ -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!']]);
});
});
});
});
+70 -24
View File
@@ -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
+228 -70
View File
@@ -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;
}
}
+219 -17
View File
@@ -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!']]);
});
});
});
});
+79 -44
View File
@@ -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;
+48 -18
View File
@@ -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;
}
+13 -22
View File
@@ -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);
}
}
+9
View File
@@ -161,6 +161,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 +183,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 {