diff --git a/package.json b/package.json index f26c010f..08357583 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "prepackage": "npm run build", "package": "webpack", "start": "node demo/start", - "lint": "tslint 'src/**/*.ts' './demo/**/*.ts' './addons/**/*.ts'", + "lint": "tslint 'src/**/*.ts' 'addons/**/*.ts'", "test": "npm run test-unit", "posttest": "npm run lint", "test-api": "mocha \"**/*.api.js\"", @@ -45,7 +45,7 @@ "puppeteer": "^1.15.0", "source-map-loader": "^0.2.4", "ts-loader": "^4.5.0", - "tslint": "^5.9.1", + "tslint": "^5.18.0", "tslint-consistent-codestyle": "^1.13.0", "typescript": "3.5", "utf8": "^3.0.0", diff --git a/src/AccessibilityManager.ts b/src/AccessibilityManager.ts index 915a86d6..75393ea4 100644 --- a/src/AccessibilityManager.ts +++ b/src/AccessibilityManager.ts @@ -11,7 +11,7 @@ import { RenderDebouncer } from 'browser/RenderDebouncer'; import { addDisposableDomListener } from 'browser/Lifecycle'; import { Disposable } from 'common/Lifecycle'; import { ScreenDprMonitor } from 'browser/ScreenDprMonitor'; -import { IRenderDimensions } from 'browser/renderer/Types'; +import { IRenderService } from 'browser/services/Services'; const MAX_ROWS_TO_READ = 20; @@ -47,8 +47,8 @@ export class AccessibilityManager extends Disposable { private _charsToAnnounce: string = ''; constructor( - private _terminal: ITerminal, - private _dimensions: IRenderDimensions + private readonly _terminal: ITerminal, + private readonly _renderService: IRenderService ) { super(); this._accessibilityTreeRoot = document.createElement('div'); @@ -90,6 +90,7 @@ export class AccessibilityManager extends Disposable { this.register(this._terminal.onA11yTab(spaceCount => this._onTab(spaceCount))); this.register(this._terminal.onKey(e => this._onKey(e.key))); this.register(this._terminal.onBlur(() => this._clearLiveRegion())); + this.register(this._renderService.onDimensionsChange(() => this._refreshRowsDimensions())); this._screenDprMonitor = new ScreenDprMonitor(); this.register(this._screenDprMonitor); @@ -271,7 +272,7 @@ export class AccessibilityManager extends Disposable { } private _refreshRowsDimensions(): void { - if (!this._dimensions.actualCellHeight) { + if (!this._renderService.dimensions.actualCellHeight) { return; } if (this._rowElements.length !== this._terminal.rows) { @@ -282,13 +283,8 @@ export class AccessibilityManager extends Disposable { } } - public setDimensions(dimensions: IRenderDimensions): void { - this._dimensions = dimensions; - this._refreshRowsDimensions(); - } - private _refreshRowDimensions(element: HTMLElement): void { - element.style.height = `${this._dimensions.actualCellHeight}px`; + element.style.height = `${this._renderService.dimensions.actualCellHeight}px`; } private _announceCharacters(): void { diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index 53d0bc49..38645a0b 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -13,8 +13,10 @@ import { CellData } from 'common/buffer/CellData'; import { Attributes } from 'common/buffer/Constants'; import { AttributeData } from 'common/buffer/AttributeData'; import { Params } from 'common/parser/Params'; -import { MockCoreService, MockBufferService, MockOptionsService, MockLogService } from 'common/TestUtils.test'; +import { MockCoreService, MockBufferService, MockDirtyRowService, MockOptionsService, MockLogService } from 'common/TestUtils.test'; import { IBufferService } from 'common/services/Services'; +import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; +import { clone } from 'common/Clone'; function getCursor(term: TestTerminal): number[] { return [ @@ -31,7 +33,7 @@ describe('InputHandler', () => { bufferService.buffer.x = 1; bufferService.buffer.y = 2; bufferService.buffer.ybase = 0; - const inputHandler = new InputHandler(terminal, bufferService, new MockCoreService(), new MockLogService(), new MockOptionsService()); + const inputHandler = new InputHandler(terminal, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService()); // Save cursor position inputHandler.saveCursor(); assert.equal(bufferService.buffer.x, 1); @@ -49,43 +51,43 @@ describe('InputHandler', () => { }); describe('setCursorStyle', () => { it('should call Terminal.setOption with correct params', () => { - const terminal = new MockInputHandlingTerminal(); - const inputHandler = new InputHandler(terminal, new MockBufferService(80, 30), new MockCoreService(), new MockLogService(), new MockOptionsService()); + const optionsService = new MockOptionsService(); + const inputHandler = new InputHandler(new MockInputHandlingTerminal(), new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), optionsService); const collect = ' '; inputHandler.setCursorStyle(Params.fromArray([0]), collect); - assert.equal(terminal.options['cursorStyle'], 'block'); - assert.equal(terminal.options['cursorBlink'], true); + assert.equal(optionsService.options['cursorStyle'], 'block'); + assert.equal(optionsService.options['cursorBlink'], true); - terminal.options = {}; + optionsService.options = clone(DEFAULT_OPTIONS); inputHandler.setCursorStyle(Params.fromArray([1]), collect); - assert.equal(terminal.options['cursorStyle'], 'block'); - assert.equal(terminal.options['cursorBlink'], true); + assert.equal(optionsService.options['cursorStyle'], 'block'); + assert.equal(optionsService.options['cursorBlink'], true); - terminal.options = {}; + optionsService.options = clone(DEFAULT_OPTIONS); inputHandler.setCursorStyle(Params.fromArray([2]), collect); - assert.equal(terminal.options['cursorStyle'], 'block'); - assert.equal(terminal.options['cursorBlink'], false); + assert.equal(optionsService.options['cursorStyle'], 'block'); + assert.equal(optionsService.options['cursorBlink'], false); - terminal.options = {}; + optionsService.options = clone(DEFAULT_OPTIONS); inputHandler.setCursorStyle(Params.fromArray([3]), collect); - assert.equal(terminal.options['cursorStyle'], 'underline'); - assert.equal(terminal.options['cursorBlink'], true); + assert.equal(optionsService.options['cursorStyle'], 'underline'); + assert.equal(optionsService.options['cursorBlink'], true); - terminal.options = {}; + optionsService.options = clone(DEFAULT_OPTIONS); inputHandler.setCursorStyle(Params.fromArray([4]), collect); - assert.equal(terminal.options['cursorStyle'], 'underline'); - assert.equal(terminal.options['cursorBlink'], false); + assert.equal(optionsService.options['cursorStyle'], 'underline'); + assert.equal(optionsService.options['cursorBlink'], false); - terminal.options = {}; + optionsService.options = clone(DEFAULT_OPTIONS); inputHandler.setCursorStyle(Params.fromArray([5]), collect); - assert.equal(terminal.options['cursorStyle'], 'bar'); - assert.equal(terminal.options['cursorBlink'], true); + assert.equal(optionsService.options['cursorStyle'], 'bar'); + assert.equal(optionsService.options['cursorBlink'], true); - terminal.options = {}; + optionsService.options = clone(DEFAULT_OPTIONS); inputHandler.setCursorStyle(Params.fromArray([6]), collect); - assert.equal(terminal.options['cursorStyle'], 'bar'); - assert.equal(terminal.options['cursorBlink'], false); + assert.equal(optionsService.options['cursorStyle'], 'bar'); + assert.equal(optionsService.options['cursorBlink'], false); }); }); describe('setMode', () => { @@ -93,7 +95,7 @@ describe('InputHandler', () => { const terminal = new MockInputHandlingTerminal(); const collect = '?'; terminal.bracketedPasteMode = false; - const inputHandler = new InputHandler(terminal, new MockBufferService(80, 30), new MockCoreService(), new MockLogService(), new MockOptionsService()); + const inputHandler = new InputHandler(terminal, new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService()); // Set bracketed paste mode inputHandler.setMode(Params.fromArray([2004]), collect); assert.equal(terminal.bracketedPasteMode, true); @@ -112,7 +114,7 @@ describe('InputHandler', () => { it('insertChars', function(): void { const term = new Terminal(); const bufferService = new MockBufferService(80, 30); - const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockLogService(), new MockOptionsService()); + const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService()); // insert some data in first and second line inputHandler.parse(Array(bufferService.cols - 9).join('a')); @@ -150,7 +152,7 @@ describe('InputHandler', () => { it('deleteChars', function(): void { const term = new Terminal(); const bufferService = new MockBufferService(80, 30); - const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockLogService(), new MockOptionsService()); + const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService()); // insert some data in first and second line inputHandler.parse(Array(bufferService.cols - 9).join('a')); @@ -191,7 +193,7 @@ describe('InputHandler', () => { it('eraseInLine', function(): void { const term = new Terminal(); const bufferService = new MockBufferService(80, 30); - const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockLogService(), new MockOptionsService()); + const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService()); // fill 6 lines to test 3 different states inputHandler.parse(Array(bufferService.cols + 1).join('a')); @@ -220,7 +222,7 @@ describe('InputHandler', () => { it('eraseInDisplay', function(): void { const term = new Terminal({cols: 80, rows: 7}); const bufferService = new MockBufferService(80, 7); - const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockLogService(), new MockOptionsService()); + const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService()); // fill display with a's for (let i = 0; i < bufferService.rows; ++i) inputHandler.parse(Array(bufferService.cols + 1).join('a')); @@ -355,7 +357,7 @@ describe('InputHandler', () => { describe('print', () => { it('should not cause an infinite loop (regression test)', () => { const term = new Terminal(); - const inputHandler = new InputHandler(term, new MockBufferService(80, 30), new MockCoreService(), new MockLogService(), new MockOptionsService()); + const inputHandler = new InputHandler(term, new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService()); const container = new Uint32Array(10); container[0] = 0x200B; inputHandler.print(container, 0, 1); @@ -370,7 +372,7 @@ describe('InputHandler', () => { beforeEach(() => { term = new Terminal(); bufferService = new MockBufferService(80, 30); - handler = new InputHandler(term, bufferService, new MockCoreService(), new MockLogService(), new MockOptionsService()); + handler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService()); }); it('should handle DECSET/DECRST 47 (alt screen buffer)', () => { handler.parse('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST'); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 0f4da041..cc767a16 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -19,7 +19,7 @@ import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content import { CellData } from 'common/buffer/CellData'; import { AttributeData } from 'common/buffer/AttributeData'; import { IAttributeData, IDisposable } from 'common/Types'; -import { ICoreService, IBufferService, IOptionsService, ILogService } from 'common/services/Services'; +import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService } from 'common/services/Services'; import { ISelectionService } from 'browser/services/Services'; /** @@ -128,12 +128,13 @@ export class InputHandler extends Disposable implements IInputHandler { public get onScroll(): IEvent { return this._onScroll.event; } constructor( - protected _terminal: IInputHandlingTerminal, - private _bufferService: IBufferService, - private _coreService: ICoreService, - private _logService: ILogService, - private _optionsService: IOptionsService, - private _parser: IEscapeSequenceParser = new EscapeSequenceParser()) + protected _terminal: IInputHandlingTerminal, + private readonly _bufferService: IBufferService, + private readonly _coreService: ICoreService, + private readonly _dirtyRowService: IDirtyRowService, + private readonly _logService: ILogService, + private readonly _optionsService: IOptionsService, + private readonly _parser: IEscapeSequenceParser = new EscapeSequenceParser()) { super(); @@ -306,7 +307,6 @@ export class InputHandler extends Disposable implements IInputHandler { public dispose(): void { super.dispose(); - this._terminal = null; } // TODO: When InputHandler moves into common, browser dependencies need to move out @@ -315,11 +315,6 @@ export class InputHandler extends Disposable implements IInputHandler { } public parse(data: string): void { - // Ensure the terminal is not disposed - if (!this._terminal) { - return; - } - let buffer = this._bufferService.buffer; const cursorStartX = buffer.x; const cursorStartY = buffer.y; @@ -338,11 +333,6 @@ export class InputHandler extends Disposable implements IInputHandler { } public parseUtf8(data: Uint8Array): void { - // Ensure the terminal is not disposed - if (!this._terminal) { - return; - } - let buffer = this._bufferService.buffer; const cursorStartX = buffer.x; const cursorStartY = buffer.y; @@ -365,14 +355,14 @@ export class InputHandler extends Disposable implements IInputHandler { let chWidth: number; const buffer = this._bufferService.buffer; const charset = this._terminal.charset; - const screenReaderMode = this._terminal.options.screenReaderMode; + const screenReaderMode = this._optionsService.options.screenReaderMode; const cols = this._bufferService.cols; const wraparoundMode = this._terminal.wraparoundMode; const insertMode = this._terminal.insertMode; const curAttr = this._terminal.curAttrData; let bufferRow = buffer.lines.get(buffer.y + buffer.ybase); - this._terminal.updateRange(buffer.y); + this._dirtyRowService.markDirty(buffer.y); for (let pos = start; pos < end; ++pos) { code = data[pos]; @@ -481,7 +471,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._parser.precedingCodepoint = this._workCell.content; } } - this._terminal.updateRange(buffer.y); + this._dirtyRowService.markDirty(buffer.y); } /** @@ -514,7 +504,7 @@ export class InputHandler extends Disposable implements IInputHandler { // make buffer local for faster access const buffer = this._bufferService.buffer; - if (this._terminal.options.convertEol) { + if (this._optionsService.options.convertEol) { buffer.x = 0; } buffer.y++; @@ -559,7 +549,7 @@ export class InputHandler extends Disposable implements IInputHandler { } const originalX = this._bufferService.buffer.x; this._bufferService.buffer.x = this._bufferService.buffer.nextStop(); - if (this._terminal.options.screenReaderMode) { + if (this._optionsService.options.screenReaderMode) { this._terminal.onA11yTabEmitter.fire(this._bufferService.buffer.x - originalX); } } @@ -830,16 +820,16 @@ export class InputHandler extends Disposable implements IInputHandler { switch (params.params[0]) { case 0: j = this._bufferService.buffer.y; - this._terminal.updateRange(j); + this._dirtyRowService.markDirty(j); this._eraseInBufferLine(j++, this._bufferService.buffer.x, this._bufferService.cols, this._bufferService.buffer.x === 0); for (; j < this._bufferService.rows; j++) { this._resetBufferLine(j); } - this._terminal.updateRange(j); + this._dirtyRowService.markDirty(j); break; case 1: j = this._bufferService.buffer.y; - this._terminal.updateRange(j); + this._dirtyRowService.markDirty(j); // Deleted front part of line and everything before. This line will no longer be wrapped. this._eraseInBufferLine(j, 0, this._bufferService.buffer.x + 1, true); if (this._bufferService.buffer.x + 1 >= this._bufferService.cols) { @@ -849,15 +839,15 @@ export class InputHandler extends Disposable implements IInputHandler { while (j--) { this._resetBufferLine(j); } - this._terminal.updateRange(0); + this._dirtyRowService.markDirty(0); break; case 2: j = this._bufferService.rows; - this._terminal.updateRange(j - 1); + this._dirtyRowService.markDirty(j - 1); while (j--) { this._resetBufferLine(j); } - this._terminal.updateRange(0); + this._dirtyRowService.markDirty(0); break; case 3: // Clear scrollback (everything not in viewport) @@ -897,7 +887,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._eraseInBufferLine(this._bufferService.buffer.y, 0, this._bufferService.cols); break; } - this._terminal.updateRange(this._bufferService.buffer.y); + this._dirtyRowService.markDirty(this._bufferService.buffer.y); } /** @@ -926,9 +916,7 @@ export class InputHandler extends Disposable implements IInputHandler { buffer.lines.splice(row, 0, buffer.getBlankLine(this._terminal.eraseAttrData())); } - // this.maxRange(); - this._terminal.updateRange(buffer.y); - this._terminal.updateRange(buffer.scrollBottom); + this._dirtyRowService.markRangeDirty(buffer.y, buffer.scrollBottom); buffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only? } @@ -959,9 +947,7 @@ export class InputHandler extends Disposable implements IInputHandler { buffer.lines.splice(j, 0, buffer.getBlankLine(this._terminal.eraseAttrData())); } - // this.maxRange(); - this._terminal.updateRange(buffer.y); - this._terminal.updateRange(buffer.scrollBottom); + this._dirtyRowService.markRangeDirty(buffer.y, buffer.scrollBottom); buffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only? } @@ -978,7 +964,7 @@ export class InputHandler extends Disposable implements IInputHandler { params.params[0] || 1, this._bufferService.buffer.getNullCell(this._terminal.eraseAttrData()) ); - this._terminal.updateRange(this._bufferService.buffer.y); + this._dirtyRowService.markDirty(this._bufferService.buffer.y); } } @@ -995,7 +981,7 @@ export class InputHandler extends Disposable implements IInputHandler { params.params[0] || 1, this._bufferService.buffer.getNullCell(this._terminal.eraseAttrData()) ); - this._terminal.updateRange(this._bufferService.buffer.y); + this._dirtyRowService.markDirty(this._bufferService.buffer.y); } } @@ -1012,9 +998,7 @@ export class InputHandler extends Disposable implements IInputHandler { buffer.lines.splice(buffer.ybase + buffer.scrollTop, 1); buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); } - // this.maxRange(); - this._terminal.updateRange(buffer.scrollTop); - this._terminal.updateRange(buffer.scrollBottom); + this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); } /** @@ -1031,9 +1015,7 @@ export class InputHandler extends Disposable implements IInputHandler { buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1); buffer.lines.splice(buffer.ybase + buffer.scrollTop, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); } - // this.maxRange(); - this._terminal.updateRange(buffer.scrollTop); - this._terminal.updateRange(buffer.scrollBottom); + this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); } } @@ -1050,7 +1032,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._bufferService.buffer.x + (params.params[0] || 1), this._bufferService.buffer.getNullCell(this._terminal.eraseAttrData()) ); - this._terminal.updateRange(this._bufferService.buffer.y); + this._dirtyRowService.markDirty(this._bufferService.buffer.y); } } @@ -1893,19 +1875,19 @@ export class InputHandler extends Disposable implements IInputHandler { switch (param) { case 1: case 2: - this._terminal.options.cursorStyle = 'block'; + this._optionsService.options.cursorStyle = 'block'; break; case 3: case 4: - this._terminal.options.cursorStyle = 'underline'; + this._optionsService.options.cursorStyle = 'underline'; break; case 5: case 6: - this._terminal.options.cursorStyle = 'bar'; + this._optionsService.options.cursorStyle = 'bar'; break; } const isBlinking = param % 2 === 1; - this._terminal.options.cursorBlink = isBlinking; + this._optionsService.options.cursorBlink = isBlinking; } } @@ -2097,8 +2079,7 @@ export class InputHandler extends Disposable implements IInputHandler { const scrollRegionHeight = buffer.scrollBottom - buffer.scrollTop; buffer.lines.shiftElements(buffer.y + buffer.ybase, scrollRegionHeight, 1); buffer.lines.set(buffer.y + buffer.ybase, buffer.getBlankLine(this._terminal.eraseAttrData())); - this._terminal.updateRange(buffer.scrollTop); - this._terminal.updateRange(buffer.scrollBottom); + this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); } else { buffer.y--; this._restrictCursor(); // quickfix to not run out of bounds @@ -2152,8 +2133,7 @@ export class InputHandler extends Disposable implements IInputHandler { buffer.lines.get(row).fill(cell); buffer.lines.get(row).isWrapped = false; } - this._terminal.updateRange(0); - this._terminal.updateRange(this._bufferService.rows); + this._dirtyRowService.markAllDirty(); this._setCursor(0, 0); } } diff --git a/src/Terminal.ts b/src/Terminal.ts index e1d0a4d7..7a068c84 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -30,7 +30,7 @@ import { C0 } from 'common/data/EscapeSequences'; import { InputHandler } from './InputHandler'; import { Renderer } from './renderer/Renderer'; import { Linkifier } from 'browser/Linkifier'; -import { SelectionService } from './browser/services/SelectionService'; +import { SelectionService } from 'browser/services/SelectionService'; import * as Browser from 'common/Platform'; import { addDisposableDomListener } from 'browser/Lifecycle'; import * as Strings from 'browser/LocalizableStrings'; @@ -47,7 +47,7 @@ import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { applyWindowsMode } from './WindowsMode'; import { ColorManager } from 'browser/ColorManager'; import { RenderService } from 'browser/services/RenderService'; -import { IOptionsService, IBufferService, ICoreService, ILogService } from 'common/services/Services'; +import { IOptionsService, IBufferService, ICoreService, ILogService, IDirtyRowService, IInstantiationService } from 'common/services/Services'; import { OptionsService } from 'common/services/OptionsService'; import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService } from 'browser/services/Services'; import { CharSizeService } from 'browser/services/CharSizeService'; @@ -60,6 +60,8 @@ import { IParams } from 'common/parser/Types'; import { CoreService } from 'common/services/CoreService'; import { LogService } from 'common/services/LogService'; import { ILinkifier, IMouseZoneManager, LinkMatcherHandler, ILinkMatcherOptions, IViewport } from 'browser/Types'; +import { DirtyRowService } from 'common/services/DirtyRowService'; +import { InstantiationService } from 'common/services/InstantiationService'; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -111,6 +113,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // common services private _bufferService: IBufferService; private _coreService: ICoreService; + private _dirtyRowService: IDirtyRowService; + private _instantiationService: IInstantiationService; private _logService: ILogService; public optionsService: IOptionsService; @@ -148,8 +152,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp public urxvtMouse: boolean; // misc - private _refreshStart: number; - private _refreshEnd: number; public savedCols: number; public curAttrData: IAttributeData; @@ -239,11 +241,18 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp super(); // Setup and initialize common services + this._instantiationService = new InstantiationService(); this.optionsService = new OptionsService(options); - this._bufferService = new BufferService(this.optionsService); - this._coreService = new CoreService(() => this.scrollToBottom(), this._bufferService, this.optionsService); + this._instantiationService.setService(IOptionsService, this.optionsService); + this._bufferService = this._instantiationService.createInstance(BufferService); + this._instantiationService.setService(IBufferService, this._bufferService); + this._coreService = this._instantiationService.createInstance(CoreService, () => this.scrollToBottom()); + this._instantiationService.setService(ICoreService, this._coreService); this._coreService.onData(e => this._onData.fire(e)); - this._logService = new LogService(this.optionsService); + this._dirtyRowService = this._instantiationService.createInstance(DirtyRowService); + this._instantiationService.setService(IDirtyRowService, this._dirtyRowService); + this._logService = this._instantiationService.createInstance(LogService); + this._instantiationService.setService(ILogService, this._logService); this._setupOptionsListeners(); this._setup(); @@ -300,7 +309,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._userScrolling = false; // Register input handler and refire/handle events - this._inputHandler = new InputHandler(this, this._bufferService, this._coreService, this._logService, this.optionsService); + this._inputHandler = new InputHandler(this, this._bufferService, this._coreService, this._dirtyRowService, this._logService, this.optionsService); this._inputHandler.onCursorMove(() => this._onCursorMove.fire()); this._inputHandler.onLineFeed(() => this._onLineFeed.fire()); this.register(this._inputHandler); @@ -385,7 +394,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp case 'screenReaderMode': if (this.optionsService.options.screenReaderMode) { if (!this._accessibilityManager && this._renderService) { - this._accessibilityManager = new AccessibilityManager(this, this._renderService.dimensions); + this._accessibilityManager = new AccessibilityManager(this, this._renderService); } } else { if (this._accessibilityManager) { @@ -576,11 +585,12 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.register(addDisposableDomListener(this.textarea, 'blur', () => this._onTextAreaBlur())); this._helperContainer.appendChild(this.textarea); - this._charSizeService = new CharSizeService(this._document, this._helperContainer, this.optionsService); + 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 = new CompositionHelper(this.textarea, this._compositionView, this._bufferService, this.optionsService, this._charSizeService, this._coreService); + this._compositionHelper = this._instantiationService.createInstance(CompositionHelper, this.textarea, this._compositionView); this._helperContainer.appendChild(this._compositionView); // Performance: Add viewport and helper elements from the fragment @@ -592,20 +602,20 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._colorManager.setTheme(this._theme); const renderer = this._createRenderer(); - this._renderService = new RenderService(renderer, this.rows, this.screenElement, this.optionsService, this._charSizeService); + this._renderService = this._instantiationService.createInstance(RenderService, renderer, this.rows, this.screenElement); + this._instantiationService.setService(IRenderService, this._renderService); this._renderService.onRender(e => this._onRender.fire(e)); this.onResize(e => this._renderService.resize(e.cols, e.rows)); - this._soundService = new SoundService(this.optionsService); - this._mouseService = new MouseService(this._renderService, this._charSizeService); + this._soundService = this._instantiationService.createInstance(SoundService); + this._instantiationService.setService(ISoundService, this._soundService); + this._mouseService = this._instantiationService.createInstance(MouseService); + this._instantiationService.setService(IMouseService, this._mouseService); - this.viewport = new Viewport( + this.viewport = this._instantiationService.createInstance(Viewport, (amount: number, suppressEvent: boolean) => this.scrollLines(amount, suppressEvent), this._viewportElement, - this._viewportScrollArea, - this._bufferService, - this._charSizeService, - this._renderService + this._viewportScrollArea ); this.viewport.onThemeChange(this._colorManager.colors); this.register(this.viewport); @@ -616,11 +626,11 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.register(this.onFocus(() => this._renderService.onFocus())); this.register(this._renderService.onDimensionsChange(() => this.viewport.syncScrollArea())); - this._selectionService = new SelectionService( + this._selectionService = this._instantiationService.createInstance(SelectionService, (amount: number, suppressEvent: boolean) => this.scrollLines(amount, suppressEvent), - this.element, this.screenElement, this._charSizeService, this._bufferService, this._coreService, - this._mouseService, this.optionsService - ); + this.element, + this.screenElement); + this._instantiationService.setService(ISelectionService, this._selectionService); this.register(this._selectionService.onSelectionChange(() => this._onSelectionChange.fire())); this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService.onMouseDown(e))); this.register(this._selectionService.onRedrawRequest(e => this._renderService.onSelectionChanged(e.start, e.end, e.columnSelectMode))); @@ -638,7 +648,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp })); this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this._selectionService.refresh())); - this._mouseZoneManager = new MouseZoneManager(this.element, this.screenElement, this._bufferService, this._mouseService, this._selectionService); + this._mouseZoneManager = this._instantiationService.createInstance(MouseZoneManager, this.element, this.screenElement); this.register(this._mouseZoneManager); this.register(this.onScroll(() => this._mouseZoneManager.clearAll())); this.linkifier.attachToDom(this.element, this._mouseZoneManager); @@ -655,8 +665,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp if (this.options.screenReaderMode) { // Note that this must be done *after* the renderer is created in order to // ensure the correct order of the dprchange event - this._accessibilityManager = new AccessibilityManager(this, this._renderService.dimensions); - this._accessibilityManager.register(this._renderService.onDimensionsChange(e => this._accessibilityManager.setDimensions(e))); + this._accessibilityManager = new AccessibilityManager(this, this._renderService); } // Measure the character size @@ -676,8 +685,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp private _createRenderer(): IRenderer { switch (this.options.rendererType) { - case 'canvas': return new Renderer(this, this._colorManager.colors, this._charSizeService); break; - case 'dom': return new DomRenderer(this, this._colorManager.colors, this._charSizeService, this.optionsService); break; + case 'canvas': return new Renderer(this._colorManager.colors, this, this._bufferService, this._charSizeService); + case 'dom': return new DomRenderer(this, this._colorManager.colors, this._charSizeService, this.optionsService); default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`); } } @@ -1136,8 +1145,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } // Flag rows that need updating - this.updateRange(this.buffer.scrollTop); - this.updateRange(this.buffer.scrollBottom); + this._dirtyRowService.markRangeDirty(this.buffer.scrollTop, this.buffer.scrollBottom); this._onScroll.fire(this.buffer.ydisp); } @@ -1258,19 +1266,9 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._xoffSentToCatchUp = false; } - this._refreshStart = this.buffer.y; - this._refreshEnd = this.buffer.y; - - // HACK: Set the parser state based on it's state at the time of return. - // This works around the bug #662 which saw the parser state reset in the - // middle of parsing escape sequence in two chunks. For some reason the - // state of the parser resets to 0 after exiting parser.parse. This change - // just sets the state back based on the correct return statement. - this._inputHandler.parseUtf8(data); - this.updateRange(this.buffer.y); - this.refresh(this._refreshStart, this._refreshEnd); + this.refresh(this._dirtyRowService.start, this._dirtyRowService.end); if (Date.now() - startTime >= WRITE_TIMEOUT_MS) { break; @@ -1345,19 +1343,9 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._xoffSentToCatchUp = false; } - this._refreshStart = this.buffer.y; - this._refreshEnd = this.buffer.y; - - // HACK: Set the parser state based on it's state at the time of return. - // This works around the bug #662 which saw the parser state reset in the - // middle of parsing escape sequence in two chunks. For some reason the - // state of the parser resets to 0 after exiting parser.parse. This change - // just sets the state back based on the correct return statement. - this._inputHandler.parse(data); - this.updateRange(this.buffer.y); - this.refresh(this._refreshStart, this._refreshEnd); + this.refresh(this._dirtyRowService.start, this._dirtyRowService.end); if (Date.now() - startTime >= WRITE_TIMEOUT_MS) { break; @@ -1717,29 +1705,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._onResize.fire({ cols: x, rows: y }); } - /** - * Updates the range of rows to refresh - * @param y The number of rows to refresh next. - */ - public updateRange(y: number): void { - if (y < this._refreshStart) this._refreshStart = y; - if (y > this._refreshEnd) this._refreshEnd = y; - // if (y > this.refreshEnd) { - // this.refreshEnd = y; - // if (y > this.rows - 1) { - // this.refreshEnd = this.rows - 1; - // } - // } - } - - /** - * Set the range of refreshing to the maximum value - */ - public maxRange(): void { - this._refreshStart = 0; - this._refreshEnd = this.rows - 1; - } - /** * Clear the entire buffer, making the prompt line the new first line. */ diff --git a/src/Types.d.ts b/src/Types.d.ts index 124aa428..f96ba07d 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -55,7 +55,6 @@ export interface IInputHandlingTerminal { bell(): void; focus(): void; - updateRange(y: number): void; scroll(isWrapped?: boolean): void; setgLevel(g: number): void; eraseAttrData(): IAttributeData; diff --git a/src/browser/MouseZoneManager.ts b/src/browser/MouseZoneManager.ts index 589428f8..7eb7c5f8 100644 --- a/src/browser/MouseZoneManager.ts +++ b/src/browser/MouseZoneManager.ts @@ -35,9 +35,9 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { constructor( private readonly _element: HTMLElement, private readonly _screenElement: HTMLElement, - private readonly _bufferService: IBufferService, - private readonly _mouseService: IMouseService, - private readonly _selectionService: ISelectionService + @IBufferService private readonly _bufferService: IBufferService, + @IMouseService private readonly _mouseService: IMouseService, + @ISelectionService private readonly _selectionService: ISelectionService ) { super(); diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index d89dc391..60861112 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -7,6 +7,7 @@ import { IEvent, EventEmitter } from 'common/EventEmitter'; import { ICharSizeService, IMouseService } from 'browser/services/Services'; export class MockCharSizeService implements ICharSizeService { + serviceBrand: any; get hasValidSize(): boolean { return this.width > 0 && this.height > 0; } onCharSizeChange: IEvent = new EventEmitter().event; constructor(public width: number, public height: number) {} @@ -14,6 +15,7 @@ export class MockCharSizeService implements ICharSizeService { } export class MockMouseService implements IMouseService { + serviceBrand: any; public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined { throw new Error('Not implemented'); } diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index 270d4c67..9625588d 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -36,9 +36,9 @@ export class Viewport extends Disposable implements IViewport { private readonly _scrollLines: (amount: number, suppressEvent: boolean) => void, private readonly _viewportElement: HTMLElement, private readonly _scrollArea: HTMLElement, - private readonly _bufferService: IBufferService, - private readonly _charSizeService: ICharSizeService, - private readonly _renderService: IRenderService + @IBufferService private readonly _bufferService: IBufferService, + @ICharSizeService private readonly _charSizeService: ICharSizeService, + @IRenderService private readonly _renderService: IRenderService ) { super(); diff --git a/src/browser/input/CompositionHelper.ts b/src/browser/input/CompositionHelper.ts index a8c8bb14..e6994ea3 100644 --- a/src/browser/input/CompositionHelper.ts +++ b/src/browser/input/CompositionHelper.ts @@ -37,10 +37,10 @@ export class CompositionHelper { constructor( private readonly _textarea: HTMLTextAreaElement, private readonly _compositionView: HTMLElement, - private readonly _bufferService: IBufferService, - private readonly _optionsService: IOptionsService, - private readonly _charSizeService: ICharSizeService, - private readonly _coreService: ICoreService + @IBufferService private readonly _bufferService: IBufferService, + @IOptionsService private readonly _optionsService: IOptionsService, + @ICharSizeService private readonly _charSizeService: ICharSizeService, + @ICoreService private readonly _coreService: ICoreService ) { this._isComposing = false; this._isSendingComposition = false; diff --git a/src/browser/services/CharSizeService.ts b/src/browser/services/CharSizeService.ts index 42920bfa..0749a420 100644 --- a/src/browser/services/CharSizeService.ts +++ b/src/browser/services/CharSizeService.ts @@ -8,6 +8,8 @@ import { IEvent, EventEmitter } from 'common/EventEmitter'; import { ICharSizeService } from 'browser/services/Services'; export class CharSizeService implements ICharSizeService { + serviceBrand: any; + public width: number = 0; public height: number = 0; private _measureStrategy: IMeasureStrategy; @@ -18,9 +20,9 @@ export class CharSizeService implements ICharSizeService { public get onCharSizeChange(): IEvent { return this._onCharSizeChange.event; } constructor( - document: Document, - parentElement: HTMLElement, - private _optionsService: IOptionsService + readonly document: Document, + readonly parentElement: HTMLElement, + @IOptionsService private readonly _optionsService: IOptionsService ) { this._measureStrategy = new DomMeasureStrategy(document, parentElement, this._optionsService); } diff --git a/src/browser/services/MouseService.ts b/src/browser/services/MouseService.ts index 76968698..b0f8c358 100644 --- a/src/browser/services/MouseService.ts +++ b/src/browser/services/MouseService.ts @@ -7,9 +7,11 @@ import { ICharSizeService, IRenderService, IMouseService } from './Services'; import { getCoords, getRawByteCoords } from 'browser/input/Mouse'; export class MouseService implements IMouseService { + serviceBrand: any; + constructor( - private readonly _renderService: IRenderService, - private readonly _charSizeService: ICharSizeService + @IRenderService private readonly _renderService: IRenderService, + @ICharSizeService private readonly _charSizeService: ICharSizeService ) { } diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index b41f1ebc..ecf20e28 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -14,6 +14,8 @@ import { IOptionsService } from 'common/services/Services'; import { ICharSizeService, IRenderService } from 'browser/services/Services'; export class RenderService extends Disposable implements IRenderService { + serviceBrand: any; + private _renderDebouncer: RenderDebouncer; private _screenDprMonitor: ScreenDprMonitor; @@ -34,9 +36,9 @@ export class RenderService extends Disposable implements IRenderService { constructor( private _renderer: IRenderer, private _rowCount: number, - screenElement: HTMLElement, - optionsService: IOptionsService, - charSizeService: ICharSizeService + readonly screenElement: HTMLElement, + @IOptionsService readonly optionsService: IOptionsService, + @ICharSizeService readonly charSizeService: ICharSizeService ) { super(); this._renderDebouncer = new RenderDebouncer((start, end) => this._renderRows(start, end)); diff --git a/src/browser/services/SelectionService.ts b/src/browser/services/SelectionService.ts index 6a9498a1..e4c703eb 100644 --- a/src/browser/services/SelectionService.ts +++ b/src/browser/services/SelectionService.ts @@ -67,6 +67,8 @@ export const enum SelectionMode { * when the selection is ready to be redrawn (on an animation frame). */ export class SelectionService implements ISelectionService { + serviceBrand: any; + protected _model: SelectionModel; /** @@ -114,11 +116,11 @@ export class SelectionService implements ISelectionService { private readonly _scrollLines: (amount: number, suppressEvent: boolean) => void, private readonly _element: HTMLElement, private readonly _screenElement: HTMLElement, - private readonly _charSizeService: ICharSizeService, - private readonly _bufferService: IBufferService, - private readonly _coreService: ICoreService, - private readonly _mouseService: IMouseService, - private readonly _optionsService: IOptionsService + @ICharSizeService private readonly _charSizeService: ICharSizeService, + @IBufferService private readonly _bufferService: IBufferService, + @ICoreService private readonly _coreService: ICoreService, + @IMouseService private readonly _mouseService: IMouseService, + @IOptionsService private readonly _optionsService: IOptionsService ) { // Init listeners this._mouseMoveListener = event => this._onMouseMove(event); diff --git a/src/browser/services/Services.d.ts b/src/browser/services/Services.ts similarity index 82% rename from src/browser/services/Services.d.ts rename to src/browser/services/Services.ts index 603d4109..863f3a9e 100644 --- a/src/browser/services/Services.d.ts +++ b/src/browser/services/Services.ts @@ -7,8 +7,12 @@ import { IEvent } from 'common/EventEmitter'; import { IRenderDimensions, IRenderer, CharacterJoinerHandler } from 'browser/renderer/Types'; import { IColorSet } from 'browser/Types'; import { ISelectionRedrawRequestEvent } from 'browser/selection/Types'; +import { createDecorator } from 'common/services/ServiceRegistry'; +export const ICharSizeService = createDecorator('CharSizeService'); export interface ICharSizeService { + serviceBrand: any; + readonly width: number; readonly height: number; readonly hasValidSize: boolean; @@ -18,12 +22,18 @@ export interface ICharSizeService { measure(): void; } +export const IMouseService = createDecorator('MouseService'); export interface IMouseService { + serviceBrand: any; + getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined; getRawByteCoords(event: MouseEvent, element: HTMLElement, colCount: number, rowCount: number): { x: number, y: number } | undefined; } +export const IRenderService = createDecorator('RenderService'); export interface IRenderService { + serviceBrand: any; + onDimensionsChange: IEvent; onRender: IEvent<{ start: number, end: number }>; onRefreshRequest: IEvent<{ start: number, end: number }>; @@ -48,7 +58,10 @@ export interface IRenderService { deregisterCharacterJoiner(joinerId: number): boolean; } +export const ISelectionService = createDecorator('SelectionService'); export interface ISelectionService { + serviceBrand: any; + readonly selectionText: string; readonly hasSelection: boolean; readonly selectionStart: [number, number] | undefined; @@ -73,6 +86,9 @@ export interface ISelectionService { onMouseDown(event: MouseEvent): void; } +export const ISoundService = createDecorator('SoundService'); export interface ISoundService { + serviceBrand: any; + playBellSound(): void; } diff --git a/src/browser/services/SoundService.ts b/src/browser/services/SoundService.ts index 31380031..1772c750 100644 --- a/src/browser/services/SoundService.ts +++ b/src/browser/services/SoundService.ts @@ -7,6 +7,8 @@ import { IOptionsService } from 'common/services/Services'; import { ISoundService } from 'browser/services/Services'; export class SoundService implements ISoundService { + serviceBrand: any; + private static _audioContext: AudioContext; static get audioContext(): AudioContext | null { @@ -22,7 +24,7 @@ export class SoundService implements ISoundService { } constructor( - private _optionsService: IOptionsService + @IOptionsService private _optionsService: IOptionsService ) { } diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index c786a06d..ec36be2f 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IPartialTerminalOptions } from 'common/services/Services'; +import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IPartialTerminalOptions, IDirtyRowService } from 'common/services/Services'; import { IEvent, EventEmitter } from 'common/EventEmitter'; import { clone } from 'common/Clone'; import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; @@ -12,6 +12,7 @@ import { BufferSet } from 'common/buffer/BufferSet'; import { IDecPrivateModes } from 'common/Types'; export class MockBufferService implements IBufferService { + serviceBrand: any; public get buffer(): IBuffer { return this.buffers.active; } public buffers: IBufferSet = {} as any; constructor( @@ -29,6 +30,7 @@ export class MockBufferService implements IBufferService { } export class MockCoreService implements ICoreService { + serviceBrand: any; decPrivateModes: IDecPrivateModes = {} as any; onData: IEvent = new EventEmitter().event; onUserInput: IEvent = new EventEmitter().event; @@ -36,7 +38,18 @@ export class MockCoreService implements ICoreService { triggerDataEvent(data: string, wasUserInput?: boolean): void {} } +export class MockDirtyRowService implements IDirtyRowService { + serviceBrand: any; + start: number = 0; + end: number = 0; + clearRange(): void {} + markDirty(y: number): void {} + markRangeDirty(y1: number, y2: number): void {} + markAllDirty(): void {} +} + export class MockLogService implements ILogService { + serviceBrand: any; debug(message: any, ...optionalParams: any[]): void {} info(message: any, ...optionalParams: any[]): void {} warn(message: any, ...optionalParams: any[]): void {} @@ -44,6 +57,7 @@ export class MockLogService implements ILogService { } export class MockOptionsService implements IOptionsService { + serviceBrand: any; options: ITerminalOptions = clone(DEFAULT_OPTIONS); onOptionChange: IEvent = new EventEmitter().event; constructor(testOptions?: IPartialTerminalOptions) { diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 320aaeb8..b25b50a5 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -153,3 +153,8 @@ export interface IMarker extends IDisposable { export interface IDecPrivateModes { applicationCursorKeys: boolean; } + +export interface IRowRange { + start: number; + end: number; +} diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts index 6ff08061..c7b6afce 100644 --- a/src/common/services/BufferService.ts +++ b/src/common/services/BufferService.ts @@ -11,6 +11,8 @@ export const MINIMUM_COLS = 2; // Less than 2 can mess with wide chars export const MINIMUM_ROWS = 1; export class BufferService implements IBufferService { + serviceBrand: any; + public cols: number; public rows: number; public buffers: IBufferSet; @@ -18,7 +20,7 @@ export class BufferService implements IBufferService { public get buffer(): IBuffer { return this.buffers.active; } constructor( - private _optionsService: IOptionsService + @IOptionsService private _optionsService: IOptionsService ) { this.cols = Math.max(_optionsService.options.cols, MINIMUM_COLS); this.rows = Math.max(_optionsService.options.rows, MINIMUM_ROWS); diff --git a/src/common/services/CoreService.ts b/src/common/services/CoreService.ts index 295b5a08..674cfedb 100644 --- a/src/common/services/CoreService.ts +++ b/src/common/services/CoreService.ts @@ -13,6 +13,8 @@ const DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({ }); export class CoreService implements ICoreService { + serviceBrand: any; + public decPrivateModes: IDecPrivateModes; private _onData = new EventEmitter(); @@ -23,8 +25,8 @@ export class CoreService implements ICoreService { constructor( // TODO: Move this into a service private readonly _scrollToBottom: () => void, - private readonly _bufferService: IBufferService, - private readonly _optionsService: IOptionsService + @IBufferService private readonly _bufferService: IBufferService, + @IOptionsService private readonly _optionsService: IOptionsService ) { this.decPrivateModes = clone(DEFAULT_DEC_PRIVATE_MODES); } diff --git a/src/common/services/DirtyRowService.ts b/src/common/services/DirtyRowService.ts new file mode 100644 index 00000000..0f2f14f5 --- /dev/null +++ b/src/common/services/DirtyRowService.ts @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IBufferService, IDirtyRowService } from 'common/services/Services'; + +export class DirtyRowService implements IDirtyRowService { + 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); + } +} diff --git a/src/common/services/InstantiationService.ts b/src/common/services/InstantiationService.ts new file mode 100644 index 00000000..abce8ac5 --- /dev/null +++ b/src/common/services/InstantiationService.ts @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + * + * This was heavily inspired from microsoft/vscode's dependency injection system (MIT). + */ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IInstantiationService, IServiceIdentifier } from 'common/services/Services'; +import { getServiceDependencies } from 'common/services/ServiceRegistry'; + +export class ServiceCollection { + + private _entries = new Map, any>(); + + constructor(...entries: [IServiceIdentifier, any][]) { + for (const [id, service] of entries) { + this.set(id, service); + } + } + + set(id: IServiceIdentifier, instance: T): T { + const result = this._entries.get(id); + this._entries.set(id, instance); + return result; + } + + forEach(callback: (id: IServiceIdentifier, instance: any) => any): void { + this._entries.forEach((value, key) => callback(key, value)); + } + + has(id: IServiceIdentifier): boolean { + return this._entries.has(id); + } + + get(id: IServiceIdentifier): T { + return this._entries.get(id); + } +} + +export class InstantiationService implements IInstantiationService { + private readonly _services: ServiceCollection = new ServiceCollection(); + + constructor() { + this._services.set(IInstantiationService, this); + } + + public setService(id: IServiceIdentifier, instance: T): void { + this._services.set(id, instance); + } + + public createInstance(ctor: any, ...args: any[]): any { + const serviceDependencies = getServiceDependencies(ctor).sort((a, b) => a.index - b.index); + + const serviceArgs: any[] = []; + for (const dependency of serviceDependencies) { + const service = this._services.get(dependency.id); + if (!service) { + throw new Error(`[createInstance] ${ctor.name} depends on UNKNOWN service ${dependency.id}.`); + } + serviceArgs.push(service); + } + + const firstServiceArgPos = serviceDependencies.length > 0 ? serviceDependencies[0].index : args.length; + + // check for argument mismatches, adjust static args if needed + if (args.length !== firstServiceArgPos) { + throw new Error(`[createInstance] First service dependency of ${ctor.name} at position ${firstServiceArgPos + 1} conflicts with ${args.length} static arguments`); + } + + // now create the instance + return new ctor(...[...args, ...serviceArgs]); + } +} diff --git a/src/common/services/LogService.ts b/src/common/services/LogService.ts index f7480078..6740ad4a 100644 --- a/src/common/services/LogService.ts +++ b/src/common/services/LogService.ts @@ -37,10 +37,12 @@ const optionsKeyToLogLevel: { [key: string]: LogLevel } = { const LOG_PREFIX = 'xterm.js: '; export class LogService implements ILogService { + serviceBrand: any; + private _logLevel!: LogLevel; constructor( - private readonly _optionsService: IOptionsService + @IOptionsService private readonly _optionsService: IOptionsService ) { this._updateLogLevel(); this._optionsService.onOptionChange(key => { diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index 7396491e..9a5d2151 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -56,6 +56,8 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({ const CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows']; export class OptionsService implements IOptionsService { + serviceBrand: any; + public options: ITerminalOptions; private _onOptionChange = new EventEmitter(); diff --git a/src/common/services/ServiceRegistry.ts b/src/common/services/ServiceRegistry.ts new file mode 100644 index 00000000..450af492 --- /dev/null +++ b/src/common/services/ServiceRegistry.ts @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + * + * This was heavily inspired from microsoft/vscode's dependency injection system (MIT). + */ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IServiceIdentifier } from 'common/services/Services'; + +const DI_TARGET = 'di$target'; +const DI_DEPENDENCIES = 'di$dependencies'; + +export const serviceRegistry: Map> = new Map(); + +export function getServiceDependencies(ctor: any): { id: IServiceIdentifier, index: number, optional: boolean }[] { + return ctor[DI_DEPENDENCIES] || []; +} + +export function createDecorator(id: string): IServiceIdentifier { + if (serviceRegistry.has(id)) { + return serviceRegistry.get(id)!; + } + + const decorator = function (target: Function, key: string, index: number): any { + if (arguments.length !== 3) { + throw new Error('@IServiceName-decorator can only be used to decorate a parameter'); + } + + storeServiceDependency(decorator, target, index); + }; + + decorator.toString = () => id; + + serviceRegistry.set(id, decorator); + return decorator; +} + +function storeServiceDependency(id: Function, target: Function, index: number): void { + if ((target as any)[DI_TARGET] === target) { + (target as any)[DI_DEPENDENCIES].push({ id, index }); + } else { + (target as any)[DI_DEPENDENCIES] = [{ id, index }]; + (target as any)[DI_TARGET] = target; + } +} diff --git a/src/common/services/Services.d.ts b/src/common/services/Services.ts similarity index 50% rename from src/common/services/Services.d.ts rename to src/common/services/Services.ts index 9a98ca92..1af55e9a 100644 --- a/src/common/services/Services.d.ts +++ b/src/common/services/Services.ts @@ -6,8 +6,12 @@ import { IEvent } from 'common/EventEmitter'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; import { IDecPrivateModes } from 'common/Types'; +import { createDecorator } from 'common/services/ServiceRegistry'; +export const IBufferService = createDecorator('BufferService'); export interface IBufferService { + serviceBrand: any; + readonly cols: number; readonly rows: number; readonly buffer: IBuffer; @@ -19,7 +23,10 @@ export interface IBufferService { reset(): void; } +export const ICoreService = createDecorator('CoreService'); export interface ICoreService { + serviceBrand: any; + readonly decPrivateModes: IDecPrivateModes; readonly onData: IEvent; @@ -35,17 +42,92 @@ export interface ICoreService { * - Scroll to the bottom of the buffer.s * - Fire the `onUserInput` event (so selection can be cleared). */ - triggerDataEvent(data: string, wasUserInput?: boolean): void; + triggerDataEvent(data: string, wasUserInput?: boolean): void; } +export const IDirtyRowService = createDecorator('DirtyRowService'); +export interface IDirtyRowService { + serviceBrand: any; + + readonly start: number; + readonly end: number; + + clearRange(): void; + markDirty(y: number): void; + markRangeDirty(y1: number, y2: number): void; + markAllDirty(): void; +} + +export interface IServiceIdentifier { + (...args: any[]): void; + type: T; +} + +export interface IConstructorSignature0 { + new(...services: { serviceBrand: any; }[]): T; +} + +export interface IConstructorSignature1 { + new(first: A1, ...services: { serviceBrand: any; }[]): T; +} + +export interface IConstructorSignature2 { + new(first: A1, second: A2, ...services: { serviceBrand: any; }[]): T; +} + +export interface IConstructorSignature3 { + new(first: A1, second: A2, third: A3, ...services: { serviceBrand: any; }[]): T; +} + +export interface IConstructorSignature4 { + new(first: A1, second: A2, third: A3, fourth: A4, ...services: { serviceBrand: any; }[]): T; +} + +export interface IConstructorSignature5 { + new(first: A1, second: A2, third: A3, fourth: A4, fifth: A5, ...services: { serviceBrand: any; }[]): T; +} + +export interface IConstructorSignature6 { + new(first: A1, second: A2, third: A3, fourth: A4, fifth: A5, sixth: A6, ...services: { serviceBrand: any; }[]): T; +} + +export interface IConstructorSignature7 { + new(first: A1, second: A2, third: A3, fourth: A4, fifth: A5, sixth: A6, seventh: A7, ...services: { serviceBrand: any; }[]): T; +} + +export interface IConstructorSignature8 { + new(first: A1, second: A2, third: A3, fourth: A4, fifth: A5, sixth: A6, seventh: A7, eigth: A8, ...services: { serviceBrand: any; }[]): T; +} + +export const IInstantiationService = createDecorator('InstantiationService'); +export interface IInstantiationService { + setService(id: IServiceIdentifier, instance: T): void; + + createInstance(ctor: IConstructorSignature0): T; + createInstance(ctor: IConstructorSignature1, first: A1): T; + createInstance(ctor: IConstructorSignature2, first: A1, second: A2): T; + createInstance(ctor: IConstructorSignature3, first: A1, second: A2, third: A3): T; + createInstance(ctor: IConstructorSignature4, first: A1, second: A2, third: A3, fourth: A4): T; + createInstance(ctor: IConstructorSignature5, first: A1, second: A2, third: A3, fourth: A4, fifth: A5): T; + createInstance(ctor: IConstructorSignature6, first: A1, second: A2, third: A3, fourth: A4, fifth: A5, sixth: A6): T; + createInstance(ctor: IConstructorSignature7, first: A1, second: A2, third: A3, fourth: A4, fifth: A5, sixth: A6, seventh: A7): T; + createInstance(ctor: IConstructorSignature8, first: A1, second: A2, third: A3, fourth: A4, fifth: A5, sixth: A6, seventh: A7, eigth: A8): T; +} + +export const ILogService = createDecorator('LogService'); export interface ILogService { + serviceBrand: any; + debug(message: any, ...optionalParams: any[]): void; info(message: any, ...optionalParams: any[]): void; warn(message: any, ...optionalParams: any[]): void; error(message: any, ...optionalParams: any[]): void; } +export const IOptionsService = createDecorator('OptionsService'); export interface IOptionsService { + serviceBrand: any; + readonly options: ITerminalOptions; readonly onOptionChange: IEvent; diff --git a/src/common/tsconfig.json b/src/common/tsconfig.json index dca04f9a..59050a0b 100644 --- a/src/common/tsconfig.json +++ b/src/common/tsconfig.json @@ -1,6 +1,9 @@ { "extends": "../tsconfig-library-base", "compilerOptions": { + "lib": [ + "es2015" + ], "outDir": "../../out", "types": [ "../../node_modules/@types/mocha" diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index a6e8fca7..2ed61b76 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -14,6 +14,7 @@ import { CharacterJoinerRegistry } from 'browser/renderer/CharacterJoinerRegistr import { Disposable } from 'common/Lifecycle'; import { IColorSet } from 'browser/Types'; import { ICharSizeService } from 'browser/services/Services'; +import { IBufferService } from 'common/services/Services'; export class Renderer extends Disposable implements IRenderer { private _renderLayers: IRenderLayer[]; @@ -23,13 +24,14 @@ export class Renderer extends Disposable implements IRenderer { public dimensions: IRenderDimensions; constructor( - private _terminal: ITerminal, private _colors: IColorSet, - private _charSizeService: ICharSizeService + private readonly _terminal: ITerminal, + readonly bufferService: IBufferService, + private readonly _charSizeService: ICharSizeService ) { super(); const allowTransparency = this._terminal.options.allowTransparency; - this._characterJoinerRegistry = new CharacterJoinerRegistry(_terminal); + this._characterJoinerRegistry = new CharacterJoinerRegistry(bufferService); this._renderLayers = [ new TextRenderLayer(this._terminal.screenElement, 0, this._colors, this._characterJoinerRegistry, allowTransparency), diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index 60bf50da..22b50349 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -21,9 +21,6 @@ const SELECTION_CLASS = 'xterm-selection'; let nextTerminalId = 1; -// TODO: Pull into an addon when TS composite projects allow easier sharing of code (not just -// interfaces) between core and addons - /** * A fallback renderer for when canvas is slow. This is not meant to be * particularly fast or feature complete, more just stable and usable for when diff --git a/src/tsconfig-library-base.json b/src/tsconfig-library-base.json index 7a9eedf3..e08695d7 100644 --- a/src/tsconfig-library-base.json +++ b/src/tsconfig-library-base.json @@ -3,6 +3,7 @@ "compilerOptions": { "composite": true, "strict": true, - "declarationMap": true + "declarationMap": true, + "experimentalDecorators": true } } diff --git a/tslint.json b/tslint.json index a18111bf..cd790c14 100644 --- a/tslint.json +++ b/tslint.json @@ -20,6 +20,10 @@ true, "spaces" ], + "import-blacklist": [ + true, + [".*\\/out\\/.*"] + ], "interface-name": [ true, "always-prefix" @@ -71,12 +75,6 @@ "variable-declaration": "nospace" } ], - "variable-name": [ - true, - "ban-keywords", - "check-format", - "allow-leading-underscore" - ], "whitespace": [ true, "check-branch", @@ -99,6 +97,7 @@ {"type": "member", "modifiers": ["protected"], "format": "camelCase", "leadingUnderscore": "require"}, {"type": "member", "modifiers": ["private"], "format": "camelCase", "leadingUnderscore": "require"}, {"type": "variable", "modifiers": ["const"], "format": ["camelCase", "UPPER_CASE"]}, + {"type": "variable", "modifiers": ["const", "export"], "filter": "^I.+Service$", "format": "PascalCase", "prefix": "I"}, {"type": "interface", "prefix": "I"} ], "no-else-after-return": { diff --git a/yarn.lock b/yarn.lock index 8861d1f6..46d21413 100644 --- a/yarn.lock +++ b/yarn.lock @@ -419,11 +419,6 @@ ansi-regex@^4.1.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= - ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -574,15 +569,6 @@ aws4@^1.6.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.7.0.tgz#d4d0e9b9dbfca77bf08eeb0a8a471550fe39e289" integrity sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w== -babel-code-frame@^6.22.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" - integrity sha1-Y/1D99weO7fONZR9uP42mj9Yx0s= - dependencies: - chalk "^1.1.3" - esutils "^2.0.2" - js-tokens "^3.0.2" - balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" @@ -818,17 +804,6 @@ chai@3.5.0: deep-eql "^0.1.3" type-detect "^1.0.0" -chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" @@ -1574,7 +1549,7 @@ escape-latex@1.2.0: resolved "https://registry.yarnpkg.com/escape-latex/-/escape-latex-1.2.0.tgz#07c03818cf7dac250cce517f4fda1b001ef2bca1" integrity sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw== -escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: +escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= @@ -2078,13 +2053,6 @@ har-validator@~5.0.3: ajv "^5.1.0" har-schema "^2.0.0" -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= - dependencies: - ansi-regex "^2.0.0" - has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -2527,17 +2495,12 @@ javascript-natural-sort@0.7.1: resolved "https://registry.yarnpkg.com/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz#f9e2303d4507f6d74355a73664d1440fb5a0ef59" integrity sha1-+eIwPUUH9tdDVac2ZNFED7Wg71k= -js-tokens@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" - integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= - js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@3.13.1, js-yaml@^3.13.1, js-yaml@^3.7.0: +js-yaml@3.13.1, js-yaml@^3.13.1: version "3.13.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== @@ -4318,11 +4281,6 @@ supports-color@6.0.0: dependencies: has-flag "^3.0.0" -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= - supports-color@^5.3.0: version "5.4.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" @@ -4498,25 +4456,26 @@ tslint@^5.17.0: tslib "^1.8.0" tsutils "^2.29.0" -tslint@^5.9.1: - version "5.10.0" - resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.10.0.tgz#11e26bccb88afa02dd0d9956cae3d4540b5f54c3" - integrity sha1-EeJrzLiK+gLdDZlWyuPUVAtfVMM= +tslint@^5.18.0: + version "5.18.0" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.18.0.tgz#f61a6ddcf372344ac5e41708095bbf043a147ac6" + integrity sha512-Q3kXkuDEijQ37nXZZLKErssQVnwCV/+23gFEMROi8IlbaBG6tXqLPQJ5Wjcyt/yHPKBC+hD5SzuGaMora+ZS6w== dependencies: - babel-code-frame "^6.22.0" + "@babel/code-frame" "^7.0.0" builtin-modules "^1.1.1" chalk "^2.3.0" commander "^2.12.1" diff "^3.2.0" glob "^7.1.1" - js-yaml "^3.7.0" + js-yaml "^3.13.1" minimatch "^3.0.4" + mkdirp "^0.5.1" resolve "^1.3.2" semver "^5.3.0" tslib "^1.8.0" - tsutils "^2.12.1" + tsutils "^2.29.0" -tsutils@^2.12.1, tsutils@^2.24.0, tsutils@^2.27.0: +tsutils@^2.24.0, tsutils@^2.27.0: version "2.27.2" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.27.2.tgz#60ba88a23d6f785ec4b89c6e8179cac9b431f1c7" integrity sha512-qf6rmT84TFMuxAKez2pIfR8UCai49iQsfB7YWVjV1bKpy/d0PWT5rEOSM6La9PiHZ0k1RRZQiwVdVJfQ3BPHgg==