Merge branch 'master' into buffer_optimizations

This commit is contained in:
Jörg Breitbart
2022-10-09 21:41:06 +02:00
103 changed files with 2785 additions and 3664 deletions
+9 -7
View File
@@ -5,6 +5,7 @@
import { ICircularList } from 'common/Types';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { Disposable } from 'common/Lifecycle';
export interface IInsertEvent {
index: number;
@@ -20,21 +21,22 @@ export interface IDeleteEvent {
* Represents a circular list; a list with a maximum size that wraps around when push is called,
* overriding values at the start of the list.
*/
export class CircularList<T> implements ICircularList<T> {
export class CircularList<T> extends Disposable implements ICircularList<T> {
protected _array: (T | undefined)[];
private _startIndex: number;
private _length: number;
public onDeleteEmitter = new EventEmitter<IDeleteEvent>();
public get onDelete(): IEvent<IDeleteEvent> { return this.onDeleteEmitter.event; }
public onInsertEmitter = new EventEmitter<IInsertEvent>();
public get onInsert(): IEvent<IInsertEvent> { return this.onInsertEmitter.event; }
public onTrimEmitter = new EventEmitter<number>();
public get onTrim(): IEvent<number> { return this.onTrimEmitter.event; }
public readonly onDeleteEmitter = this.register(new EventEmitter<IDeleteEvent>());
public readonly onDelete = this.onDeleteEmitter.event;
public readonly onInsertEmitter = this.register(new EventEmitter<IInsertEvent>());
public readonly onInsert = this.onInsertEmitter.event;
public readonly onTrimEmitter = this.register(new EventEmitter<number>());
public readonly onTrim = this.onTrimEmitter.event;
constructor(
private _maxLength: number
) {
super();
this._array = new Array<T>(this._maxLength);
this._startIndex = 0;
this._length = 0;
+6 -1
View File
@@ -11,6 +11,11 @@ let $g = 0;
let $b = 0;
let $a = 0;
export const NULL_COLOR: IColor = {
css: '#00000000',
rgba: 0
};
/**
* Helper functions where the source type is "channels" (individual color channels as numbers).
*/
@@ -129,7 +134,7 @@ export namespace css {
*/
export function toColor(css: string): IColor {
// Formats: #rgb[a] and #rrggbb[aa]
if (css.match(/#[0-9a-f]{3,8}/i)) {
if (css.match(/#[\da-f]{3,8}/i)) {
switch (css.length) {
case 4: { // #rgb
$r = parseInt(css.slice(1, 2).repeat(2), 16);
+37 -51
View File
@@ -21,8 +21,8 @@
* http://linux.die.net/man/7/urxvt
*/
import { Disposable } from 'common/Lifecycle';
import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, IDirtyRowService, LogLevelEnum, ITerminalOptions, IOscLinkService } from 'common/services/Services';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, LogLevelEnum, ITerminalOptions, IOscLinkService } 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';
@@ -31,7 +31,6 @@ import { IDisposable, IAttributeData, ICoreTerminal, IScrollEvent, ScrollSource
import { CoreService } from 'common/services/CoreService';
import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter';
import { CoreMouseService } from 'common/services/CoreMouseService';
import { DirtyRowService } from 'common/services/DirtyRowService';
import { UnicodeService } from 'common/services/UnicodeService';
import { CharsetService } from 'common/services/CharsetService';
import { updateWindowsModeWrappedState } from 'common/WindowsMode';
@@ -49,7 +48,6 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
protected readonly _bufferService: IBufferService;
protected readonly _logService: ILogService;
protected readonly _charsetService: ICharsetService;
protected readonly _dirtyRowService: IDirtyRowService;
protected readonly _oscLinkService: IOscLinkService;
public readonly coreMouseService: ICoreMouseService;
@@ -61,28 +59,29 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
private _writeBuffer: WriteBuffer;
private _windowsMode: IDisposable | undefined;
private _onBinary = new EventEmitter<string>();
public get onBinary(): IEvent<string> { return this._onBinary.event; }
private _onData = new EventEmitter<string>();
public get onData(): IEvent<string> { return this._onData.event; }
protected _onLineFeed = new EventEmitter<void>();
public get onLineFeed(): IEvent<void> { return this._onLineFeed.event; }
private _onResize = new EventEmitter<{ cols: number, rows: number }>();
public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; }
protected _onScroll = new EventEmitter<IScrollEvent, void>();
public get onWriteParsed(): IEvent<void> { return this._onWriteParsed.event; }
protected _onWriteParsed = new EventEmitter<void>();
private readonly _onBinary = this.register(new EventEmitter<string>());
public readonly onBinary = this._onBinary.event;
private readonly _onData = this.register(new EventEmitter<string>());
public readonly onData = this._onData.event;
protected _onLineFeed = this.register(new EventEmitter<void>());
public readonly onLineFeed = this._onLineFeed.event;
private readonly _onResize = this.register(new EventEmitter<{ cols: number, rows: number }>());
public readonly onResize = this._onResize.event;
protected readonly _onWriteParsed = this.register(new EventEmitter<void>());
public readonly onWriteParsed = this._onWriteParsed.event;
/**
* Internally we track the source of the scroll but this is meaningless outside the library so
* it's filtered out.
*/
protected _onScrollApi?: EventEmitter<number, void>;
protected _onScroll = this.register(new EventEmitter<IScrollEvent, void>());
public get onScroll(): IEvent<number, void> {
if (!this._onScrollApi) {
this._onScrollApi = new EventEmitter<number, void>();
this.register(this._onScroll.event(ev => {
this._onScrollApi = this.register(new EventEmitter<number, void>());
this._onScroll.event(ev => {
this._onScrollApi?.fire(ev.position);
}));
});
}
return this._onScrollApi.event;
}
@@ -104,19 +103,17 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
// Setup and initialize services
this._instantiationService = new InstantiationService();
this.optionsService = new OptionsService(options);
this.optionsService = this.register(new OptionsService(options));
this._instantiationService.setService(IOptionsService, this.optionsService);
this._bufferService = this.register(this._instantiationService.createInstance(BufferService));
this._instantiationService.setService(IBufferService, this._bufferService);
this._logService = this._instantiationService.createInstance(LogService);
this._logService = this.register(this._instantiationService.createInstance(LogService));
this._instantiationService.setService(ILogService, this._logService);
this.coreService = this.register(this._instantiationService.createInstance(CoreService, () => this.scrollToBottom()));
this._instantiationService.setService(ICoreService, this.coreService);
this.coreMouseService = this._instantiationService.createInstance(CoreMouseService);
this.coreMouseService = this.register(this._instantiationService.createInstance(CoreMouseService));
this._instantiationService.setService(ICoreMouseService, this.coreMouseService);
this._dirtyRowService = this._instantiationService.createInstance(DirtyRowService);
this._instantiationService.setService(IDirtyRowService, this._dirtyRowService);
this.unicodeService = this._instantiationService.createInstance(UnicodeService);
this.unicodeService = this.register(this._instantiationService.createInstance(UnicodeService));
this._instantiationService.setService(IUnicodeService, this.unicodeService);
this._charsetService = this._instantiationService.createInstance(CharsetService);
this._instantiationService.setService(ICharsetService, this._charsetService);
@@ -124,7 +121,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
this._instantiationService.setService(IOscLinkService, this._oscLinkService);
// Register input handler and handle/forward events
this._inputHandler = new InputHandler(this._bufferService, this._charsetService, this.coreService, this._dirtyRowService, this._logService, this.optionsService, this._oscLinkService, this.coreMouseService, this.unicodeService);
this._inputHandler = this.register(new InputHandler(this._bufferService, this._charsetService, this.coreService, this._logService, this.optionsService, this._oscLinkService, this.coreMouseService, this.unicodeService));
this.register(forwardEvent(this._inputHandler.onLineFeed, this._onLineFeed));
this.register(this._inputHandler);
@@ -132,28 +129,25 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
this.register(forwardEvent(this._bufferService.onResize, this._onResize));
this.register(forwardEvent(this.coreService.onData, this._onData));
this.register(forwardEvent(this.coreService.onBinary, this._onBinary));
this.register(this.optionsService.onOptionChange(key => this._updateOptions(key)));
this.register(this.coreService.onUserInput(() => this._writeBuffer.handleUserInput()));
this.register(this.optionsService.onSpecificOptionChange('windowsMode', e => this._handleWindowsModeOptionChange(e)));
this.register(this._bufferService.onScroll(event => {
this._onScroll.fire({ position: this._bufferService.buffer.ydisp, source: ScrollSource.TERMINAL });
this._dirtyRowService.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);
this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);
}));
this.register(this._inputHandler.onScroll(event => {
this._onScroll.fire({ position: this._bufferService.buffer.ydisp, source: ScrollSource.TERMINAL });
this._dirtyRowService.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);
this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom);
}));
// Setup WriteBuffer
this._writeBuffer = new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult));
this._writeBuffer = this.register(new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult)));
this.register(forwardEvent(this._writeBuffer.onWriteParsed, this._onWriteParsed));
}
public dispose(): void {
if (this._isDisposed) {
return;
}
super.dispose();
this._windowsMode?.dispose();
this._windowsMode = undefined;
this.register(toDisposable(() => {
this._windowsMode?.dispose();
this._windowsMode = undefined;
}));
}
public write(data: string | Uint8Array, callback?: () => void): void {
@@ -267,20 +261,12 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
this.coreMouseService.reset();
}
protected _updateOptions(key: string): void {
// TODO: These listeners should be owned by individual components
switch (key) {
case 'scrollback':
this.buffers.resize(this.cols, this.rows);
break;
case 'windowsMode':
if (this.optionsService.rawOptions.windowsMode) {
this._enableWindowsMode();
} else {
this._windowsMode?.dispose();
this._windowsMode = undefined;
}
break;
private _handleWindowsModeOptionChange(value: boolean): void {
if (value) {
this._enableWindowsMode();
} else {
this._windowsMode?.dispose();
this._windowsMode = undefined;
}
}
+6 -12
View File
@@ -11,7 +11,7 @@ import { CellData } from 'common/buffer/CellData';
import { Attributes, UnderlineStyle } from 'common/buffer/Constants';
import { AttributeData } from 'common/buffer/AttributeData';
import { Params } from 'common/parser/Params';
import { MockCoreService, MockBufferService, MockDirtyRowService, MockOptionsService, MockLogService, MockCoreMouseService, MockCharsetService, MockUnicodeService, MockOscLinkService } from 'common/TestUtils.test';
import { MockCoreService, MockBufferService, MockOptionsService, MockLogService, MockCoreMouseService, MockCharsetService, MockUnicodeService, MockOscLinkService } from 'common/TestUtils.test';
import { IBufferService, ICoreService } from 'common/services/Services';
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
import { clone } from 'common/Clone';
@@ -67,7 +67,7 @@ describe('InputHandler', () => {
bufferService.resize(80, 30);
coreService = new CoreService(() => { }, bufferService, new MockLogService(), optionsService);
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
});
describe('SL/SR/DECIC/DECDC', () => {
@@ -236,7 +236,7 @@ describe('InputHandler', () => {
describe('setMode', () => {
it('should toggle bracketedPasteMode', () => {
const coreService = new MockCoreService();
const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
const inputHandler = new TestInputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
// Set bracketed paste mode
inputHandler.setModePrivate(Params.fromArray([2004]));
assert.equal(coreService.decPrivateModes.bracketedPasteMode, true);
@@ -258,7 +258,6 @@ describe('InputHandler', () => {
bufferService,
new MockCharsetService(),
new MockCoreService(),
new MockDirtyRowService(),
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
@@ -305,7 +304,6 @@ describe('InputHandler', () => {
bufferService,
new MockCharsetService(),
new MockCoreService(),
new MockDirtyRowService(),
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
@@ -356,7 +354,6 @@ describe('InputHandler', () => {
bufferService,
new MockCharsetService(),
new MockCoreService(),
new MockDirtyRowService(),
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
@@ -394,7 +391,6 @@ describe('InputHandler', () => {
bufferService,
new MockCharsetService(),
new MockCoreService(),
new MockDirtyRowService(),
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
@@ -445,7 +441,6 @@ describe('InputHandler', () => {
bufferService,
new MockCharsetService(),
new MockCoreService(),
new MockDirtyRowService(),
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
@@ -572,7 +567,6 @@ describe('InputHandler', () => {
new MockBufferService(80, 30),
new MockCharsetService(),
new MockCoreService(),
new MockDirtyRowService(),
new MockLogService(),
new MockOptionsService(),
new MockOscLinkService(),
@@ -599,7 +593,7 @@ describe('InputHandler', () => {
beforeEach(() => {
bufferService = new MockBufferService(80, 30);
handler = new TestInputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
handler = new TestInputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockLogService(), new MockOptionsService(), new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
});
it('should handle DECSET/DECRST 47 (alt screen buffer)', async () => {
await handler.parseP('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST');
@@ -796,7 +790,7 @@ describe('InputHandler', () => {
describe('colon notation', () => {
let inputHandler2: TestInputHandler;
beforeEach(() => {
inputHandler2 = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
inputHandler2 = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
});
describe('should equal to semicolon', () => {
it('CSI 38:2::50:100:150 m', async () => {
@@ -2278,7 +2272,7 @@ describe('InputHandler - async handlers', () => {
coreService = new CoreService(() => { }, bufferService, new MockLogService(), optionsService);
coreService.onData(data => { console.log(data); });
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockLogService(), optionsService, new MockOscLinkService(), new MockCoreMouseService(), new MockUnicodeService());
});
it('async CUP with CPR check', async () => {
+117 -62
View File
@@ -12,11 +12,11 @@ import { Disposable } from 'common/Lifecycle';
import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32 } from 'common/input/TextDecoder';
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IFunctionIdentifier } from 'common/parser/Types';
import { IParsingState, IEscapeSequenceParser, IParams, IFunctionIdentifier } from 'common/parser/Types';
import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content, UnderlineStyle } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
import { AttributeData } from 'common/buffer/AttributeData';
import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, LogLevelEnum, IOscLinkService } from 'common/services/Services';
import { ICoreService, IBufferService, IOptionsService, ILogService, ICoreMouseService, ICharsetService, IUnicodeService, LogLevelEnum, IOscLinkService } from 'common/services/Services';
import { OscHandler } from 'common/parser/OscParser';
import { DcsHandler } from 'common/parser/DcsParser';
import { IBuffer } from 'common/buffer/Types';
@@ -104,6 +104,8 @@ export enum WindowsOptionsReportType {
// create a warning log if an async handler takes longer than the limit (in ms)
const SLOW_ASYNC_LIMIT = 5000;
// Work variables to avoid garbage collection
let $temp = 0;
/**
* The terminal's standard implementation of IInputHandler, this handles all
@@ -120,6 +122,7 @@ export class InputHandler extends Disposable implements IInputHandler {
private _windowTitle = '';
private _iconName = '';
private _currentLinkId?: number;
private _dirtyRowTracker: IDirtyRowTracker;
protected _windowTitleStack: string[] = [];
protected _iconNameStack: string[] = [];
@@ -129,33 +132,33 @@ export class InputHandler extends Disposable implements IInputHandler {
private _activeBuffer: IBuffer;
private _onRequestBell = new EventEmitter<void>();
public get onRequestBell(): IEvent<void> { return this._onRequestBell.event; }
private _onRequestRefreshRows = new EventEmitter<number, number>();
public get onRequestRefreshRows(): IEvent<number, number> { return this._onRequestRefreshRows.event; }
private _onRequestReset = new EventEmitter<void>();
public get onRequestReset(): IEvent<void> { return this._onRequestReset.event; }
private _onRequestSendFocus = new EventEmitter<void>();
public get onRequestSendFocus(): IEvent<void> { return this._onRequestSendFocus.event; }
private _onRequestSyncScrollBar = new EventEmitter<void>();
public get onRequestSyncScrollBar(): IEvent<void> { return this._onRequestSyncScrollBar.event; }
private _onRequestWindowsOptionsReport = new EventEmitter<WindowsOptionsReportType>();
public get onRequestWindowsOptionsReport(): IEvent<WindowsOptionsReportType> { return this._onRequestWindowsOptionsReport.event; }
private readonly _onRequestBell = this.register(new EventEmitter<void>());
public readonly onRequestBell = this._onRequestBell.event;
private readonly _onRequestRefreshRows = this.register(new EventEmitter<number, number>());
public readonly onRequestRefreshRows = this._onRequestRefreshRows.event;
private readonly _onRequestReset = this.register(new EventEmitter<void>());
public readonly onRequestReset = this._onRequestReset.event;
private readonly _onRequestSendFocus = this.register(new EventEmitter<void>());
public readonly onRequestSendFocus = this._onRequestSendFocus.event;
private readonly _onRequestSyncScrollBar = this.register(new EventEmitter<void>());
public readonly onRequestSyncScrollBar = this._onRequestSyncScrollBar.event;
private readonly _onRequestWindowsOptionsReport = this.register(new EventEmitter<WindowsOptionsReportType>());
public readonly onRequestWindowsOptionsReport = this._onRequestWindowsOptionsReport.event;
private _onA11yChar = new EventEmitter<string>();
public get onA11yChar(): IEvent<string> { return this._onA11yChar.event; }
private _onA11yTab = new EventEmitter<number>();
public get onA11yTab(): IEvent<number> { return this._onA11yTab.event; }
private _onCursorMove = new EventEmitter<void>();
public get onCursorMove(): IEvent<void> { return this._onCursorMove.event; }
private _onLineFeed = new EventEmitter<void>();
public get onLineFeed(): IEvent<void> { return this._onLineFeed.event; }
private _onScroll = new EventEmitter<number>();
public get onScroll(): IEvent<number> { return this._onScroll.event; }
private _onTitleChange = new EventEmitter<string>();
public get onTitleChange(): IEvent<string> { return this._onTitleChange.event; }
private _onColor = new EventEmitter<IColorEvent>();
public get onColor(): IEvent<IColorEvent> { return this._onColor.event; }
private readonly _onA11yChar = this.register(new EventEmitter<string>());
public readonly onA11yChar = this._onA11yChar.event;
private readonly _onA11yTab = this.register(new EventEmitter<number>());
public readonly onA11yTab = this._onA11yTab.event;
private readonly _onCursorMove = this.register(new EventEmitter<void>());
public readonly onCursorMove = this._onCursorMove.event;
private readonly _onLineFeed = this.register(new EventEmitter<void>());
public readonly onLineFeed = this._onLineFeed.event;
private readonly _onScroll = this.register(new EventEmitter<number>());
public readonly onScroll = this._onScroll.event;
private readonly _onTitleChange = this.register(new EventEmitter<string>());
public readonly onTitleChange = this._onTitleChange.event;
private readonly _onColor = this.register(new EventEmitter<IColorEvent>());
public readonly onColor = this._onColor.event;
private _parseStack: IParseStack = {
paused: false,
@@ -169,7 +172,6 @@ export class InputHandler extends Disposable implements IInputHandler {
private readonly _bufferService: IBufferService,
private readonly _charsetService: ICharsetService,
private readonly _coreService: ICoreService,
private readonly _dirtyRowService: IDirtyRowService,
private readonly _logService: ILogService,
private readonly _optionsService: IOptionsService,
private readonly _oscLinkService: IOscLinkService,
@@ -179,6 +181,7 @@ export class InputHandler extends Disposable implements IInputHandler {
) {
super();
this.register(this._parser);
this._dirtyRowTracker = new DirtyRowTracker(this._bufferService);
// Track properties used in performance critical code manually to avoid using slow getters
this._activeBuffer = this._bufferService.buffer;
@@ -379,10 +382,6 @@ export class InputHandler extends Disposable implements IInputHandler {
this._parser.registerDcsHandler({ intermediates: '$', final: 'q' }, new DcsHandler((data, params) => this.requestStatusString(data, params)));
}
public dispose(): void {
super.dispose();
}
/**
* Async parse support.
*/
@@ -459,7 +458,7 @@ export class InputHandler extends Disposable implements IInputHandler {
// Clear the dirty row service so we know which lines changed as a result of parsing
// Important: do not clear between async calls, otherwise we lost pending update information.
if (!wasPaused) {
this._dirtyRowService.clearRange();
this._dirtyRowTracker.clearRange();
}
// process big data in smaller chunks
@@ -493,7 +492,7 @@ export class InputHandler extends Disposable implements IInputHandler {
}
// Refresh any dirty rows accumulated as part of parsing
this._onRequestRefreshRows.fire(this._dirtyRowService.start, this._dirtyRowService.end);
this._onRequestRefreshRows.fire(this._dirtyRowTracker.start, this._dirtyRowTracker.end);
}
public print(data: Uint32Array, start: number, end: number): void {
@@ -507,7 +506,7 @@ export class InputHandler extends Disposable implements IInputHandler {
const curAttr = this._curAttrData;
let bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!;
this._dirtyRowService.markDirty(this._activeBuffer.y);
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
// handle wide chars: reset start_cell-1 if we would overwrite the second cell of a wide char
if (this._activeBuffer.x && end - start > 0 && bufferRow.getWidth(this._activeBuffer.x - 1) === 2) {
@@ -635,7 +634,7 @@ export class InputHandler extends Disposable implements IInputHandler {
bufferRow.setCellFromCodePoint(this._activeBuffer.x, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended);
}
this._dirtyRowService.markDirty(this._activeBuffer.y);
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
}
/**
@@ -699,7 +698,7 @@ export class InputHandler extends Disposable implements IInputHandler {
* @vt: #Y C0 FF "Form Feed" "\f, \x0C" "Treated as LF."
*/
public lineFeed(): boolean {
this._dirtyRowService.markDirty(this._activeBuffer.y);
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
if (this._optionsService.rawOptions.convertEol) {
this._activeBuffer.x = 0;
}
@@ -714,7 +713,7 @@ export class InputHandler extends Disposable implements IInputHandler {
if (this._activeBuffer.x >= this._bufferService.cols) {
this._activeBuffer.x--;
}
this._dirtyRowService.markDirty(this._activeBuffer.y);
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
this._onLineFeed.fire();
return true;
@@ -842,14 +841,14 @@ export class InputHandler extends Disposable implements IInputHandler {
this._activeBuffer.y = this._coreService.decPrivateModes.origin
? Math.min(this._activeBuffer.scrollBottom, Math.max(this._activeBuffer.scrollTop, this._activeBuffer.y))
: Math.min(this._bufferService.rows - 1, Math.max(0, this._activeBuffer.y));
this._dirtyRowService.markDirty(this._activeBuffer.y);
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
}
/**
* Set absolute cursor position.
*/
private _setCursor(x: number, y: number): void {
this._dirtyRowService.markDirty(this._activeBuffer.y);
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
if (this._coreService.decPrivateModes.origin) {
this._activeBuffer.x = x;
this._activeBuffer.y = this._activeBuffer.scrollTop + y;
@@ -858,7 +857,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._activeBuffer.y = y;
}
this._restrictCursor();
this._dirtyRowService.markDirty(this._activeBuffer.y);
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
}
/**
@@ -1178,16 +1177,16 @@ export class InputHandler extends Disposable implements IInputHandler {
switch (params.params[0]) {
case 0:
j = this._activeBuffer.y;
this._dirtyRowService.markDirty(j);
this._dirtyRowTracker.markDirty(j);
this._eraseInBufferLine(j++, this._activeBuffer.x, this._bufferService.cols, this._activeBuffer.x === 0, respectProtect);
for (; j < this._bufferService.rows; j++) {
this._resetBufferLine(j, respectProtect);
}
this._dirtyRowService.markDirty(j);
this._dirtyRowTracker.markDirty(j);
break;
case 1:
j = this._activeBuffer.y;
this._dirtyRowService.markDirty(j);
this._dirtyRowTracker.markDirty(j);
// Deleted front part of line and everything before. This line will no longer be wrapped.
this._eraseInBufferLine(j, 0, this._activeBuffer.x + 1, true, respectProtect);
if (this._activeBuffer.x + 1 >= this._bufferService.cols) {
@@ -1197,15 +1196,15 @@ export class InputHandler extends Disposable implements IInputHandler {
while (j--) {
this._resetBufferLine(j, respectProtect);
}
this._dirtyRowService.markDirty(0);
this._dirtyRowTracker.markDirty(0);
break;
case 2:
j = this._bufferService.rows;
this._dirtyRowService.markDirty(j - 1);
this._dirtyRowTracker.markDirty(j - 1);
while (j--) {
this._resetBufferLine(j, respectProtect);
}
this._dirtyRowService.markDirty(0);
this._dirtyRowTracker.markDirty(0);
break;
case 3:
// Clear scrollback (everything not in viewport)
@@ -1257,7 +1256,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._eraseInBufferLine(this._activeBuffer.y, 0, this._bufferService.cols, true, respectProtect);
break;
}
this._dirtyRowService.markDirty(this._activeBuffer.y);
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
return true;
}
@@ -1289,7 +1288,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._activeBuffer.lines.splice(row, 0, this._activeBuffer.getBlankLine(this._eraseAttrData()));
}
this._dirtyRowService.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom);
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom);
this._activeBuffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only?
return true;
}
@@ -1323,7 +1322,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._activeBuffer.lines.splice(j, 0, this._activeBuffer.getBlankLine(this._eraseAttrData()));
}
this._dirtyRowService.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom);
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom);
this._activeBuffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only?
return true;
}
@@ -1349,7 +1348,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._activeBuffer.getNullCell(this._eraseAttrData()),
this._eraseAttrData()
);
this._dirtyRowService.markDirty(this._activeBuffer.y);
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
}
return true;
}
@@ -1375,7 +1374,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._activeBuffer.getNullCell(this._eraseAttrData()),
this._eraseAttrData()
);
this._dirtyRowService.markDirty(this._activeBuffer.y);
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
}
return true;
}
@@ -1395,7 +1394,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollTop, 1);
this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollBottom, 0, this._activeBuffer.getBlankLine(this._eraseAttrData()));
}
this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
return true;
}
@@ -1411,7 +1410,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollBottom, 1);
this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollTop, 0, this._activeBuffer.getBlankLine(DEFAULT_ATTR_DATA));
}
this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
return true;
}
@@ -1443,7 +1442,7 @@ export class InputHandler extends Disposable implements IInputHandler {
line.deleteCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData());
line.isWrapped = false;
}
this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
return true;
}
@@ -1476,7 +1475,7 @@ export class InputHandler extends Disposable implements IInputHandler {
line.insertCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData());
line.isWrapped = false;
}
this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
return true;
}
@@ -1499,7 +1498,7 @@ export class InputHandler extends Disposable implements IInputHandler {
line.insertCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData());
line.isWrapped = false;
}
this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
return true;
}
@@ -1522,7 +1521,7 @@ export class InputHandler extends Disposable implements IInputHandler {
line.deleteCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData());
line.isWrapped = false;
}
this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
return true;
}
@@ -1544,7 +1543,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._activeBuffer.getNullCell(this._eraseAttrData()),
this._eraseAttrData()
);
this._dirtyRowService.markDirty(this._activeBuffer.y);
this._dirtyRowTracker.markDirty(this._activeBuffer.y);
}
return true;
}
@@ -3220,7 +3219,7 @@ export class InputHandler extends Disposable implements IInputHandler {
const scrollRegionHeight = this._activeBuffer.scrollBottom - this._activeBuffer.scrollTop;
this._activeBuffer.lines.shiftElements(this._activeBuffer.ybase + this._activeBuffer.y, scrollRegionHeight, 1);
this._activeBuffer.lines.set(this._activeBuffer.ybase + this._activeBuffer.y, this._activeBuffer.getBlankLine(this._eraseAttrData()));
this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
} else {
this._activeBuffer.y--;
this._restrictCursor(); // quickfix to not run out of bounds
@@ -3293,7 +3292,7 @@ export class InputHandler extends Disposable implements IInputHandler {
line.isWrapped = false;
}
}
this._dirtyRowService.markAllDirty();
this._dirtyRowTracker.markAllDirty();
this._setCursor(0, 0);
return true;
}
@@ -3344,4 +3343,60 @@ export class InputHandler extends Disposable implements IInputHandler {
if (data === ' q') return f(`P1$r${STYLES[opts.cursorStyle] - (opts.cursorBlink ? 1 : 0)} q`);
return f(`P0$r`);
}
public markRangeDirty(y1: number, y2: number): void {
this._dirtyRowTracker.markRangeDirty(y1, y2);
}
}
export interface IDirtyRowTracker {
readonly start: number;
readonly end: number;
clearRange(): void;
markDirty(y: number): void;
markRangeDirty(y1: number, y2: number): void;
markAllDirty(): void;
}
class DirtyRowTracker implements IDirtyRowTracker {
public start!: number;
public end!: number;
constructor(
@IBufferService private readonly _bufferService: IBufferService
) {
this.clearRange();
}
public clearRange(): void {
this.start = this._bufferService.buffer.y;
this.end = this._bufferService.buffer.y;
}
public markDirty(y: number): void {
if (y < this.start) {
this.start = y;
} else if (y > this.end) {
this.end = y;
}
}
public markRangeDirty(y1: number, y2: number): void {
if (y1 > y2) {
$temp = y1;
y1 = y2;
y2 = $temp;
}
if (y1 < this.start) {
this.start = y1;
}
if (y2 > this.end) {
this.end = y2;
}
}
public markAllDirty(): void {
this.markRangeDirty(0, this._bufferService.rows - 1);
}
}
+6 -3
View File
@@ -17,15 +17,18 @@ export abstract class Disposable implements IDisposable {
}
/**
* Disposes the object, triggering the `dispose` method on all registered IDisposables.
* Disposes the object, triggering the `dispose` method on all registered IDisposables. This is a
* readonly property instead of a method to prevent subclasses overriding it which is an easy
* mistake that can introduce memory leaks. If a class extends Disposable, all dispose calls
* should be done via {@link register}.
*/
public dispose(): void {
public readonly dispose = (): void => {
this._isDisposed = true;
for (const d of this._disposables) {
d.dispose();
}
this._disposables.length = 0;
}
};
/**
* Registers a disposable object.
+19 -13
View File
@@ -3,13 +3,13 @@
* @license MIT
*/
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum, IDecorationService, IInternalDecoration, IOscLinkService } from 'common/services/Services';
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum, IDecorationService, IInternalDecoration, IOscLinkService } from 'common/services/Services';
import { IEvent, EventEmitter } from 'common/EventEmitter';
import { clone } from 'common/Clone';
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
import { IBufferSet, IBuffer } from 'common/buffer/Types';
import { BufferSet } from 'common/buffer/BufferSet';
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes, IAttributeData, IOscLinkData } from 'common/Types';
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes, IAttributeData, IOscLinkData, IDisposable } from 'common/Types';
import { UnicodeV6 } from 'common/input/UnicodeV6';
import { IDecorationOptions, IDecoration } from 'xterm';
@@ -100,16 +100,6 @@ export class MockCoreService implements ICoreService {
public triggerBinaryEvent(data: string): void { }
}
export class MockDirtyRowService implements IDirtyRowService {
public serviceBrand: any;
public start: number = 0;
public end: number = 0;
public clearRange(): void { }
public markDirty(y: number): void { }
public markRangeDirty(y1: number, y2: number): void { }
public markAllDirty(): void { }
}
export class MockLogService implements ILogService {
public serviceBrand: any;
public logLevel = LogLevelEnum.DEBUG;
@@ -123,7 +113,7 @@ export class MockOptionsService implements IOptionsService {
public serviceBrand: any;
public readonly rawOptions: Required<ITerminalOptions> = clone(DEFAULT_OPTIONS);
public options: Required<ITerminalOptions> = this.rawOptions;
public onOptionChange: IEvent<string> = new EventEmitter<string>().event;
public onOptionChange: IEvent<keyof ITerminalOptions> = new EventEmitter<keyof ITerminalOptions>().event;
constructor(testOptions?: Partial<ITerminalOptions>) {
if (testOptions) {
for (const key of Object.keys(testOptions)) {
@@ -131,6 +121,22 @@ export class MockOptionsService implements IOptionsService {
}
}
}
// eslint-disable-next-line @typescript-eslint/naming-convention
public onSpecificOptionChange<T extends keyof ITerminalOptions>(key: T, listener: (arg1: ITerminalOptions[T]) => any): IDisposable {
return this.onOptionChange(eventKey => {
if (eventKey === key) {
listener(this.rawOptions[key]);
}
});
}
// eslint-disable-next-line @typescript-eslint/naming-convention
public onMultipleOptionChange(keys: (keyof ITerminalOptions)[], listener: () => any): IDisposable {
return this.onOptionChange(eventKey => {
if (keys.indexOf(eventKey) !== -1) {
listener();
}
});
}
public setOptions(options: ITerminalOptions): void {
for (const key of Object.keys(options)) {
this.options[key] = options[key];
+2
View File
@@ -168,6 +168,7 @@ export class Buffer implements IBuffer {
// Deal with columns increasing (reducing needs to happen after reflow)
if (this._cols < newCols) {
for (let i = 0; i < this.lines.length; i++) {
// +boolean for fast 0 or 1 conversion
needsCleanup |= +this.lines.get(i)!.resize(newCols, nullCell);
}
}
@@ -247,6 +248,7 @@ export class Buffer implements IBuffer {
// Trim the end of the line off if cols shrunk
if (this._cols > newCols) {
for (let i = 0; i < this.lines.length; i++) {
// +boolean for fast 0 or 1 conversion
needsCleanup |= +this.lines.get(i)!.resize(newCols, nullCell);
}
}
+6 -8
View File
@@ -37,10 +37,8 @@ const enum Cell {
export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData());
/** Work variables to avoid garbage collection. */
const w: { startIndex: number } = {
startIndex: 0
};
// Work variables to avoid garbage collection
let $startIndex = 0;
/** Factor when to cleanup underlying array buffer after shrinking. */
const CLEANUP_THRESHOLD = 2;
@@ -181,10 +179,10 @@ export class BufferLine implements IBufferLine {
* to GC as it significantly reduced the amount of new objects/references needed.
*/
public loadCell(index: number, cell: ICellData): ICellData {
w.startIndex = index * CELL_SIZE;
cell.content = this._data[w.startIndex + Cell.CONTENT];
cell.fg = this._data[w.startIndex + Cell.FG];
cell.bg = this._data[w.startIndex + Cell.BG];
$startIndex = index * CELL_SIZE;
cell.content = this._data[$startIndex + Cell.CONTENT];
cell.fg = this._data[$startIndex + Cell.FG];
cell.bg = this._data[$startIndex + Cell.BG];
if (cell.content & Content.IS_COMBINED_MASK) {
cell.combinedData = this._combined[index];
}
+5 -2
View File
@@ -19,8 +19,8 @@ export class BufferSet extends Disposable implements IBufferSet {
private _alt!: Buffer;
private _activeBuffer!: Buffer;
private _onBufferActivate = this.register(new EventEmitter<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}>());
public get onBufferActivate(): IEvent<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}> { return this._onBufferActivate.event; }
private readonly _onBufferActivate = this.register(new EventEmitter<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}>());
public readonly onBufferActivate = this._onBufferActivate.event;
/**
* Create a new BufferSet for the given terminal.
@@ -32,6 +32,8 @@ export class BufferSet extends Disposable implements IBufferSet {
) {
super();
this.reset();
this.register(this._optionsService.onSpecificOptionChange('scrollback', () => this.resize(this._bufferService.cols, this._bufferService.rows)));
this.register(this._optionsService.onSpecificOptionChange('tabStopWidth', () => this.setupTabStops()));
}
public reset(): void {
@@ -119,6 +121,7 @@ export class BufferSet extends Disposable implements IBufferSet {
public resize(newCols: number, newRows: number): void {
this._normal.resize(newCols, newRows);
this._alt.resize(newCols, newRows);
this.setupTabStops(newCols);
}
/**
+15 -9
View File
@@ -3,25 +3,25 @@
* @license MIT
*/
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { Disposable } from 'common/Lifecycle';
import { IMarker } from 'common/Types';
import { EventEmitter } from 'common/EventEmitter';
import { disposeArray } from 'common/Lifecycle';
import { IDisposable, IMarker } from 'common/Types';
export class Marker extends Disposable implements IMarker {
export class Marker implements IMarker {
private static _nextId = 1;
private _id: number = Marker._nextId++;
public isDisposed: boolean = false;
private _disposables: IDisposable[] = [];
private _id: number = Marker._nextId++;
public get id(): number { return this._id; }
private _onDispose = new EventEmitter<void>();
public get onDispose(): IEvent<void> { return this._onDispose.event; }
private readonly _onDispose = this.register(new EventEmitter<void>());
public readonly onDispose = this._onDispose.event;
constructor(
public line: number
) {
super();
}
public dispose(): void {
@@ -32,6 +32,12 @@ export class Marker extends Disposable implements IMarker {
this.line = -1;
// Emit before super.dispose such that dispose listeners get a change to react
this._onDispose.fire();
super.dispose();
disposeArray(this._disposables);
this._disposables.length = 0;
}
public register<T extends IDisposable>(disposable: T): T {
this._disposables.push(disposable);
return disposable;
}
}
+35 -28
View File
@@ -121,35 +121,42 @@ describe('text encodings', () => {
describe('Utf8ToUtf32 decoder', () => {
describe('full codepoint test', () => {
it('0..65535 (1/2/3 byte sequences)', () => {
const decoder = new Utf8ToUtf32();
const target = new Uint32Array(5);
for (let i = 0; i < 65536; ++i) {
// skip surrogate pairs and a BOM
if ((i >= 0xD800 && i <= 0xDFFF) || i === 0xFEFF) {
continue;
function formatRange(min: number, max: number): string {
return `${min}..${max} (0x${min.toString(16).toUpperCase()}..0x${max.toString(16).toUpperCase()})`;
}
for (let min = 0; min < 65535; min += 10000) {
const max = Math.min(min + 10000, 65536);
it(`${formatRange(min, max)} (1/2/3 byte sequences)`, () => {
const decoder = new Utf8ToUtf32();
const target = new Uint32Array(5);
for (let i = min; i < max; ++i) {
// skip surrogate pairs and a BOM
if ((i >= 0xD800 && i <= 0xDFFF) || i === 0xFEFF) {
continue;
}
const utf8Data = fromByteString(encode(String.fromCharCode(i)));
const length = decoder.decode(utf8Data, target);
assert.equal(length, 1);
assert.equal(toString(target, length), String.fromCharCode(i));
decoder.clear();
}
const utf8Data = fromByteString(encode(String.fromCharCode(i)));
const length = decoder.decode(utf8Data, target);
assert.equal(length, 1);
assert.equal(toString(target, length), String.fromCharCode(i));
decoder.clear();
}
});
it('65536..0x10FFFF (4 byte sequences)', function (): void {
this.timeout(20000);
const decoder = new Utf8ToUtf32();
const target = new Uint32Array(5);
for (let i = 65536; i < 0x10FFFF; ++i) {
const utf8Data = fromByteString(encode(stringFromCodePoint(i)));
const length = decoder.decode(utf8Data, target);
assert.equal(length, 1);
assert.equal(target[0], i);
decoder.clear();
}
});
});
}
for (let minRaw = 60000; minRaw < 0x10FFFF; minRaw += 10000) {
const min = Math.max(minRaw, 65536);
const max = Math.min(minRaw + 10000, 0x10FFFF);
it(`${formatRange(min, max)} (4 byte sequences)`, function (): void {
const decoder = new Utf8ToUtf32();
const target = new Uint32Array(5);
for (let i = min; i < max; ++i) {
const utf8Data = fromByteString(encode(stringFromCodePoint(i)));
const length = decoder.decode(utf8Data, target);
assert.equal(length, 1);
assert.equal(target[0], i);
decoder.clear();
}
});
}
it('0xFEFF(BOM)', () => {
const decoder = new Utf8ToUtf32();
+27 -5
View File
@@ -5,6 +5,7 @@
*/
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { Disposable } from 'common/Lifecycle';
declare const setTimeout: (handler: () => void, timeout?: number) => void;
@@ -33,17 +34,25 @@ const WRITE_TIMEOUT_MS = 12;
*/
const WRITE_BUFFER_LENGTH_THRESHOLD = 50;
export class WriteBuffer {
export class WriteBuffer extends Disposable {
private _writeBuffer: (string | Uint8Array)[] = [];
private _callbacks: ((() => void) | undefined)[] = [];
private _pendingData = 0;
private _bufferOffset = 0;
private _isSyncWriting = false;
private _syncCalls = 0;
public get onWriteParsed(): IEvent<void> { return this._onWriteParsed.event; }
private _onWriteParsed = new EventEmitter<void>();
private _didUserInput = false;
constructor(private _action: (data: string | Uint8Array, promiseResult?: boolean) => void | Promise<boolean>) { }
private readonly _onWriteParsed = this.register(new EventEmitter<void>());
public readonly onWriteParsed = this._onWriteParsed.event;
constructor(private _action: (data: string | Uint8Array, promiseResult?: boolean) => void | Promise<boolean>) {
super();
}
public handleUserInput(): void {
this._didUserInput = true;
}
/**
* @deprecated Unreliable, to be removed soon.
@@ -99,7 +108,20 @@ export class WriteBuffer {
// schedule chunk processing for next event loop run
if (!this._writeBuffer.length) {
this._bufferOffset = 0;
queueMicrotask(() => this._innerWrite());
// If this is the first write call after the user has done some input,
// parse it immediately to minimize input latency,
// otherwise schedule for the next event
if (this._didUserInput) {
this._didUserInput = false;
this._pendingData += data.length;
this._writeBuffer.push(data);
this._callbacks.push(callback);
this._innerWrite();
return;
}
setTimeout(() => this._innerWrite());
}
this._pendingData += data.length;
@@ -87,7 +87,7 @@ class TestEscapeSequenceParser extends EscapeSequenceParser {
}
}
public mockOscParser(): void {
this._oscParser = oscPutParser;
(this as any)._oscParser = oscPutParser;
}
public identifier(id: IFunctionIdentifier): number {
return this._identifier(id);
+10 -13
View File
@@ -5,7 +5,7 @@
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 { Disposable, toDisposable } from 'common/Lifecycle';
import { IDisposable } from 'common/Types';
import { fill } from 'common/TypedArrayUtils';
import { Params } from 'common/parser/Params';
@@ -242,8 +242,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
protected _executeHandlers: { [flag: number]: ExecuteHandlerType };
protected _csiHandlers: IHandlerCollection<CsiHandlerType>;
protected _escHandlers: IHandlerCollection<EscHandlerType>;
protected _oscParser: IOscParser;
protected _dcsParser: IDcsParser;
protected readonly _oscParser: IOscParser;
protected readonly _dcsParser: IDcsParser;
protected _errorHandler: (state: IParsingState) => IParsingState;
// fallback handlers
@@ -284,8 +284,13 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._executeHandlers = Object.create(null);
this._csiHandlers = Object.create(null);
this._escHandlers = Object.create(null);
this._oscParser = new OscParser();
this._dcsParser = new DcsParser();
this.register(toDisposable(() => {
this._csiHandlers = Object.create(null);
this._executeHandlers = Object.create(null);
this._escHandlers = Object.create(null);
}));
this._oscParser = this.register(new OscParser());
this._dcsParser = this.register(new DcsParser());
this._errorHandler = this._errorHandlerFb;
// swallow 7bit ST (ESC+\)
@@ -338,14 +343,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
return res.reverse().join('');
}
public dispose(): void {
this._csiHandlers = Object.create(null);
this._executeHandlers = Object.create(null);
this._escHandlers = Object.create(null);
this._oscParser.dispose();
this._dcsParser.dispose();
}
public setPrintHandler(handler: PrintHandlerType): void {
this._printHandler = handler;
}
+4 -3
View File
@@ -5,14 +5,15 @@
import { IBuffer as IBufferApi, IBufferNamespace as IBufferNamespaceApi } from 'xterm';
import { BufferApiView } from 'common/public/BufferApiView';
import { IEvent, EventEmitter } from 'common/EventEmitter';
import { EventEmitter } from 'common/EventEmitter';
import { ICoreTerminal } from 'common/Types';
export class BufferNamespaceApi implements IBufferNamespaceApi {
private _normal: BufferApiView;
private _alternate: BufferApiView;
private _onBufferChange = new EventEmitter<IBufferApi>();
public get onBufferChange(): IEvent<IBufferApi> { return this._onBufferChange.event; }
private readonly _onBufferChange = new EventEmitter<IBufferApi>();
public readonly onBufferChange = this._onBufferChange.event;
constructor(private _core: ICoreTerminal) {
this._normal = new BufferApiView(this._core.buffers.normal, 'normal');
+7 -12
View File
@@ -6,7 +6,7 @@
import { IBufferService, IOptionsService } from 'common/services/Services';
import { BufferSet } from 'common/buffer/BufferSet';
import { IBufferSet, IBuffer } from 'common/buffer/Types';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { EventEmitter, IEventEmitter, IEvent } from 'common/EventEmitter';
import { Disposable } from 'common/Lifecycle';
import { IAttributeData, IBufferLine, ScrollSource } from 'common/Types';
@@ -22,10 +22,10 @@ export class BufferService extends Disposable implements IBufferService {
/** Whether the user is scrolling (locks the scroll position) */
public isUserScrolling: boolean = false;
private _onResize = new EventEmitter<{ cols: number, rows: number }>();
public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; }
private _onScroll = new EventEmitter<number>();
public get onScroll(): IEvent<number> { return this._onScroll.event; }
private readonly _onResize = this.register(new EventEmitter<{ cols: number, rows: number }>());
public readonly onResize = this._onResize.event;
private readonly _onScroll = this.register(new EventEmitter<number>());
public readonly onScroll = this._onScroll.event;
public get buffer(): IBuffer { return this.buffers.active; }
@@ -36,19 +36,14 @@ export class BufferService extends Disposable implements IBufferService {
super();
this.cols = Math.max(optionsService.rawOptions.cols || 0, MINIMUM_COLS);
this.rows = Math.max(optionsService.rawOptions.rows || 0, MINIMUM_ROWS);
this.buffers = new BufferSet(optionsService, this);
}
public dispose(): void {
super.dispose();
this.buffers.dispose();
this.buffers = this.register(new BufferSet(optionsService, this));
}
public resize(cols: number, rows: number): void {
this.cols = cols;
this.rows = rows;
this.buffers.resize(cols, rows);
this.buffers.setupTabStops(this.cols);
// TODO: This doesn't fire when scrollback changes - add a resize event to BufferSet and forward event
this._onResize.fire({ cols, rows });
}
+6 -9
View File
@@ -5,6 +5,7 @@
import { IBufferService, ICoreService, ICoreMouseService } from 'common/services/Services';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { ICoreMouseProtocol, ICoreMouseEvent, CoreMouseEncoding, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types';
import { Disposable } from 'common/Lifecycle';
/**
* Supported default protocols.
@@ -165,18 +166,21 @@ const DEFAULT_ENCODINGS: { [key: string]: CoreMouseEncoding } = {
* a tracking report to the backend based on protocol and encoding limitations.
* To send a mouse event call `triggerMouseEvent`.
*/
export class CoreMouseService implements ICoreMouseService {
export class CoreMouseService extends Disposable implements ICoreMouseService {
private _protocols: { [name: string]: ICoreMouseProtocol } = {};
private _encodings: { [name: string]: CoreMouseEncoding } = {};
private _activeProtocol: string = '';
private _activeEncoding: string = '';
private _onProtocolChange = new EventEmitter<CoreMouseEventType>();
private _lastEvent: ICoreMouseEvent | null = null;
private readonly _onProtocolChange = this.register(new EventEmitter<CoreMouseEventType>());
public readonly onProtocolChange = this._onProtocolChange.event;
constructor(
@IBufferService private readonly _bufferService: IBufferService,
@ICoreService private readonly _coreService: ICoreService
) {
super();
// register default protocols and encodings
for (const name of Object.keys(DEFAULT_PROTOCOLS)) this.addProtocol(name, DEFAULT_PROTOCOLS[name]);
for (const name of Object.keys(DEFAULT_ENCODINGS)) this.addEncoding(name, DEFAULT_ENCODINGS[name]);
@@ -225,13 +229,6 @@ export class CoreMouseService implements ICoreMouseService {
this._lastEvent = null;
}
/**
* Event to announce changes in mouse tracking.
*/
public get onProtocolChange(): IEvent<CoreMouseEventType> {
return this._onProtocolChange.event;
}
/**
* Triggers a mouse event to be sent.
*
+6 -6
View File
@@ -34,12 +34,12 @@ export class CoreService extends Disposable implements ICoreService {
// Circular dependency, this must be unset or memory will leak after Terminal.dispose
private _scrollToBottom: (() => void) | undefined;
private _onData = this.register(new EventEmitter<string>());
public get onData(): IEvent<string> { return this._onData.event; }
private _onUserInput = this.register(new EventEmitter<void>());
public get onUserInput(): IEvent<void> { return this._onUserInput.event; }
private _onBinary = this.register(new EventEmitter<string>());
public get onBinary(): IEvent<string> { return this._onBinary.event; }
private readonly _onData = this.register(new EventEmitter<string>());
public readonly onData = this._onData.event;
private readonly _onUserInput = this.register(new EventEmitter<void>());
public readonly onUserInput = this._onUserInput.event;
private readonly _onBinary = this.register(new EventEmitter<string>());
public readonly onBinary = this._onBinary.event;
constructor(
// TODO: Move this into a service
+30 -30
View File
@@ -5,17 +5,15 @@
import { css } from 'common/Color';
import { EventEmitter } from 'common/EventEmitter';
import { Disposable } from 'common/Lifecycle';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IDecorationService, IInternalDecoration } from 'common/services/Services';
import { SortedList } from 'common/SortedList';
import { IColor } from 'common/Types';
import { IDecorationOptions, IDecoration, IMarker, IEvent } from 'xterm';
/** Work variables to avoid garbage collection. */
const w = {
xmin: 0,
xmax: 0
};
// Work variables to avoid garbage collection
let $xmin = 0;
let $xmax = 0;
export class DecorationService extends Disposable implements IDecorationService {
public serviceBrand: any;
@@ -27,13 +25,23 @@ export class DecorationService extends Disposable implements IDecorationService
*/
private readonly _decorations: SortedList<IInternalDecoration> = new SortedList(e => e?.marker.line);
private _onDecorationRegistered = this.register(new EventEmitter<IInternalDecoration>());
public get onDecorationRegistered(): IEvent<IInternalDecoration> { return this._onDecorationRegistered.event; }
private _onDecorationRemoved = this.register(new EventEmitter<IInternalDecoration>());
public get onDecorationRemoved(): IEvent<IInternalDecoration> { return this._onDecorationRemoved.event; }
private readonly _onDecorationRegistered = this.register(new EventEmitter<IInternalDecoration>());
public readonly onDecorationRegistered = this._onDecorationRegistered.event;
private readonly _onDecorationRemoved = this.register(new EventEmitter<IInternalDecoration>());
public readonly onDecorationRemoved = this._onDecorationRemoved.event;
public get decorations(): IterableIterator<IInternalDecoration> { return this._decorations.values(); }
constructor() {
super();
this.register(toDisposable(() => {
for (const d of this._decorations.values()) {
this._onDecorationRemoved.fire(d);
}
this.reset();
}));
}
public registerDecoration(options: IDecorationOptions): IDecoration | undefined {
if (options.marker.isDisposed) {
return undefined;
@@ -76,32 +84,26 @@ export class DecorationService extends Disposable implements IDecorationService
public forEachDecorationAtCell(x: number, line: number, layer: 'bottom' | 'top' | undefined, callback: (decoration: IInternalDecoration) => void): void {
this._decorations.forEachByKey(line, d => {
w.xmin = d.options.x ?? 0;
w.xmax = w.xmin + (d.options.width ?? 1);
if (x >= w.xmin && x < w.xmax && (!layer || (d.options.layer ?? 'bottom') === layer)) {
$xmin = d.options.x ?? 0;
$xmax = $xmin + (d.options.width ?? 1);
if (x >= $xmin && x < $xmax && (!layer || (d.options.layer ?? 'bottom') === layer)) {
callback(d);
}
});
}
public dispose(): void {
for (const d of this._decorations.values()) {
this._onDecorationRemoved.fire(d);
}
this.reset();
}
}
class Decoration extends Disposable implements IInternalDecoration {
public readonly marker: IMarker;
public element: HTMLElement | undefined;
public isDisposed: boolean = false;
public readonly onRenderEmitter = this.register(new EventEmitter<HTMLElement>());
public readonly onRender = this.onRenderEmitter.event;
private _onDispose = this.register(new EventEmitter<void>());
private readonly _onDispose = this.register(new EventEmitter<void>());
public readonly onDispose = this._onDispose.event;
public get isDisposed(): boolean { return this._isDisposed; }
private _cachedBg: IColor | undefined | null = null;
public get backgroundColorRGB(): IColor | undefined {
if (this._cachedBg === null) {
@@ -134,14 +136,12 @@ class Decoration extends Disposable implements IInternalDecoration {
if (this.options.overviewRulerOptions && !this.options.overviewRulerOptions.position) {
this.options.overviewRulerOptions.position = 'full';
}
}
public override dispose(): void {
if (this._isDisposed) {
return;
}
this._isDisposed = true;
this._onDispose.fire();
super.dispose();
this.register(toDisposable(() => {
if (this._isDisposed) {
return;
}
this._onDispose.fire();
}));
}
}
-53
View File
@@ -1,53 +0,0 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IBufferService, IDirtyRowService } from 'common/services/Services';
export class DirtyRowService implements IDirtyRowService {
public serviceBrand: any;
private _start!: number;
private _end!: number;
public get start(): number { return this._start; }
public get end(): number { return this._end; }
constructor(
@IBufferService private readonly _bufferService: IBufferService
) {
this.clearRange();
}
public clearRange(): void {
this._start = this._bufferService.buffer.y;
this._end = this._bufferService.buffer.y;
}
public markDirty(y: number): void {
if (y < this._start) {
this._start = y;
} else if (y > this._end) {
this._end = y;
}
}
public markRangeDirty(y1: number, y2: number): void {
if (y1 > y2) {
const temp = y1;
y1 = y2;
y2 = temp;
}
if (y1 < this._start) {
this._start = y1;
}
if (y2 > this._end) {
this._end = y2;
}
}
public markAllDirty(): void {
this.markRangeDirty(0, this._bufferService.rows - 1);
}
}
+3 -1
View File
@@ -29,7 +29,9 @@ export class ServiceCollection {
}
public forEach(callback: (id: IServiceIdentifier<any>, instance: any) => any): void {
this._entries.forEach((value, key) => callback(key, value));
for (const [key, value] of this._entries.entries()) {
callback(key, value);
}
}
public has(id: IServiceIdentifier<any>): boolean {
+4 -6
View File
@@ -3,6 +3,7 @@
* @license MIT
*/
import { Disposable } from 'common/Lifecycle';
import { ILogService, IOptionsService, LogLevelEnum } from 'common/services/Services';
type LogType = (message?: any, ...optionalParams: any[]) => void;
@@ -29,7 +30,7 @@ const optionsKeyToLogLevel: { [key: string]: LogLevelEnum } = {
const LOG_PREFIX = 'xterm.js: ';
export class LogService implements ILogService {
export class LogService extends Disposable implements ILogService {
public serviceBrand: any;
public logLevel: LogLevelEnum = LogLevelEnum.OFF;
@@ -37,12 +38,9 @@ export class LogService implements ILogService {
constructor(
@IOptionsService private readonly _optionsService: IOptionsService
) {
super();
this._updateLogLevel();
this._optionsService.onOptionChange(key => {
if (key === 'logLevel') {
this._updateLogLevel();
}
});
this.register(this._optionsService.onSpecificOptionChange('logLevel', () => this._updateLogLevel()));
}
private _updateLogLevel(): void {
+77 -1
View File
@@ -5,6 +5,7 @@
import { assert } from 'chai';
import { OptionsService, DEFAULT_OPTIONS } from 'common/services/OptionsService';
import { IDisposable } from 'common/Types';
describe('OptionsService', () => {
describe('constructor', () => {
@@ -59,7 +60,7 @@ describe('OptionsService', () => {
});
it('normalizes invalid fontWeight option values', () => {
service.options.fontWeight = 350;
assert.doesNotThrow(() => service.options.fontWeight = 10000), 'fontWeight should be normalized instead of throwing';
assert.doesNotThrow(() => service.options.fontWeight = 10000, 'fontWeight should be normalized instead of throwing');
assert.equal(service.options.fontWeight, DEFAULT_OPTIONS.fontWeight, 'Values greater than 1000 should be reset to default');
service.options.fontWeight = 350;
@@ -71,4 +72,79 @@ describe('OptionsService', () => {
assert.equal(service.options.fontWeight, DEFAULT_OPTIONS.fontWeight, 'Wrong string literals should be reset to default');
});
});
describe('onOptionChange', () => {
let service: OptionsService;
beforeEach(() => {
service = new OptionsService({});
});
it('should fire on any option change', async () => {
let disposable: IDisposable;
await new Promise<void>(r => {
disposable = service.onOptionChange(e => {
assert.strictEqual(e, 'cursorWidth');
r();
});
service.options.cursorWidth = 10;
});
disposable!.dispose();
await new Promise<void>(r => {
service.onOptionChange(e => {
assert.strictEqual(e, 'scrollback');
r();
});
service.options.scrollback = 20;
});
});
});
describe('onSpecificOptionChange', () => {
let service: OptionsService;
beforeEach(() => {
service = new OptionsService({});
});
it('should fire only on a specific option change', async () => {
await new Promise<void>(r => {
service.onSpecificOptionChange('scrollback', e => {
assert.strictEqual(e, 20);
r();
});
service.options.cursorWidth = 10;
service.options.scrollback = 20;
});
});
});
describe('onSpecificOptionChange', () => {
let service: OptionsService;
beforeEach(() => {
service = new OptionsService({});
});
it('should fire only on a specific option change', async () => {
await new Promise<void>(r => {
service.onSpecificOptionChange('scrollback', e => {
assert.strictEqual(e, 20);
r();
});
service.options.cursorWidth = 10;
service.options.scrollback = 20;
});
});
});
describe('onMultipleOptionChange', () => {
let service: OptionsService;
beforeEach(() => {
service = new OptionsService({});
});
it('should fire only for specific options', async () => {
await new Promise<void>(r => {
let called = false;
service.onMultipleOptionChange(['scrollback'], () => {
called = true;
});
service.options.cursorWidth = 10;
assert.notOk(called);
service.options.scrollback = 20;
assert.ok(called);
r();
});
});
});
});
+24 -4
View File
@@ -6,7 +6,8 @@
import { IOptionsService, ITerminalOptions, FontWeight } from 'common/services/Services';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { isMac } from 'common/Platform';
import { CursorStyle } from 'common/Types';
import { CursorStyle, IDisposable } from 'common/Types';
import { Disposable } from 'common/Lifecycle';
export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
cols: 80,
@@ -51,16 +52,17 @@ export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
const FONT_WEIGHT_OPTIONS: Extract<FontWeight, string>[] = ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'];
export class OptionsService implements IOptionsService {
export class OptionsService extends Disposable implements IOptionsService {
public serviceBrand: any;
public readonly rawOptions: Required<ITerminalOptions>;
public options: Required<ITerminalOptions>;
private _onOptionChange = new EventEmitter<string>();
public get onOptionChange(): IEvent<string> { return this._onOptionChange.event; }
private readonly _onOptionChange = this.register(new EventEmitter<keyof ITerminalOptions>());
public readonly onOptionChange = this._onOptionChange.event;
constructor(options: Partial<ITerminalOptions>) {
super();
// set the default value of each option
const defaultOptions = { ...DEFAULT_OPTIONS };
for (const key in options) {
@@ -80,6 +82,24 @@ export class OptionsService implements IOptionsService {
this._setupOptions();
}
// eslint-disable-next-line @typescript-eslint/naming-convention
public onSpecificOptionChange<T extends keyof ITerminalOptions>(key: T, listener: (value: ITerminalOptions[T]) => any): IDisposable {
return this.onOptionChange(eventKey => {
if (eventKey === key) {
listener(this.rawOptions[key]);
}
});
}
// eslint-disable-next-line @typescript-eslint/naming-convention
public onMultipleOptionChange(keys: (keyof ITerminalOptions)[], listener: () => any): IDisposable {
return this.onOptionChange(eventKey => {
if (keys.indexOf(eventKey) !== -1) {
listener();
}
});
}
private _setupOptions(): void {
const getter = (propName: string): any => {
if (!(propName in DEFAULT_OPTIONS)) {
+25 -14
View File
@@ -122,19 +122,6 @@ export interface ICharsetService {
setgCharset(g: number, charset: ICharset | undefined): void;
}
export const IDirtyRowService = createDecorator<IDirtyRowService>('DirtyRowService');
export interface IDirtyRowService {
serviceBrand: undefined;
readonly start: number;
readonly end: number;
clearRange(): void;
markDirty(y: number): void;
markRangeDirty(y1: number, y2: number): void;
markAllDirty(): void;
}
export interface IServiceIdentifier<T> {
(...args: any[]): void;
type: T;
@@ -195,9 +182,33 @@ export interface IOptionsService {
* internally.
*/
readonly rawOptions: Required<ITerminalOptions>;
/**
* Options as exposed through the public API, this property uses getters and setters with
* validation which makes it safer but slower. {@link rawOptions} should be used for pretty much
* all internal usage for performance reasons.
*/
readonly options: Required<ITerminalOptions>;
readonly onOptionChange: IEvent<string>;
/**
* Adds an event listener for when any option changes.
*/
readonly onOptionChange: IEvent<keyof ITerminalOptions>;
/**
* Adds an event listener for when a specific option changes, this is a convenience method that is
* preferred over {@link onOptionChange} when only a single option is being listened to.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
onSpecificOptionChange<T extends keyof ITerminalOptions>(key: T, listener: (arg1: Required<ITerminalOptions>[T]) => any): IDisposable;
/**
* Adds an event listener for when a set of specific options change, this is a convenience method
* that is preferred over {@link onOptionChange} when multiple options are being listened to and
* handled the same way.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
onMultipleOptionChange(keys: (keyof ITerminalOptions)[], listener: () => any): IDisposable;
}
export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' | number;
+7 -3
View File
@@ -6,15 +6,15 @@ import { IUnicodeService, IUnicodeVersionProvider } from 'common/services/Servic
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { UnicodeV6 } from 'common/input/UnicodeV6';
export class UnicodeService implements IUnicodeService {
public serviceBrand: any;
private _providers: {[key: string]: IUnicodeVersionProvider} = Object.create(null);
private _active: string = '';
private _activeProvider: IUnicodeVersionProvider;
private _onChange = new EventEmitter<string>();
public get onChange(): IEvent<string> { return this._onChange.event; }
private readonly _onChange = new EventEmitter<string>();
public readonly onChange = this._onChange.event;
constructor() {
const defaultProvider = new UnicodeV6();
@@ -23,6 +23,10 @@ export class UnicodeService implements IUnicodeService {
this._activeProvider = defaultProvider;
}
public dispose(): void {
this._onChange.dispose();
}
public get versions(): string[] {
return Object.keys(this._providers);
}