diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index f2f02eec..f454f538 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -548,10 +548,12 @@ describe('Terminal', () => { }); describe('with macOptionIsMeta', () => { + let originalIsMac: boolean; beforeEach(() => { - term.browser.isMac = true; + originalIsMac = term.browser.isMac; term.options.macOptionIsMeta = true; }); + afterEach(() => term.browser.isMac = originalIsMac); it('should interfere with the alt key on keyDown', () => { evKeyDown.altKey = true; @@ -564,9 +566,12 @@ describe('Terminal', () => { }); describe('On Mac OS', () => { + let originalIsMac: boolean; beforeEach(() => { + originalIsMac = term.browser.isMac; term.browser.isMac = true; }); + afterEach(() => term.browser.isMac = originalIsMac); it('should not interfere with the alt key on keyDown', () => { evKeyDown.altKey = true; @@ -627,9 +632,12 @@ describe('Terminal', () => { }); describe('On MS Windows', () => { + let originalIsWindows: boolean; beforeEach(() => { + originalIsWindows = term.browser.isMSWindows; term.browser.isMSWindows = true; }); + afterEach(() => term.browser.isMSWindows = originalIsWindows); it('should not interfere with the alt + ctrl key on keyDown', () => { evKeyPress.altKey = true; diff --git a/src/Terminal.ts b/src/Terminal.ts index 79395206..0cf104dd 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 './Linkifier'; -import { SelectionService } from './SelectionService'; +import { SelectionService } from './browser/services/SelectionService'; import * as Browser from 'common/Platform'; import { addDisposableDomListener } from 'browser/Lifecycle'; import * as Strings from './browser/LocalizableStrings'; diff --git a/src/browser/selection/Types.d.ts b/src/browser/selection/Types.d.ts index 2551be43..d5a03ecc 100644 --- a/src/browser/selection/Types.d.ts +++ b/src/browser/selection/Types.d.ts @@ -4,7 +4,7 @@ */ export interface ISelectionRedrawRequestEvent { - start: [number, number]; - end: [number, number]; + start: [number, number] | undefined; + end: [number, number] | undefined; columnSelectMode: boolean; } diff --git a/src/SelectionService.test.ts b/src/browser/services/SelectionService.test.ts similarity index 95% rename from src/SelectionService.test.ts rename to src/browser/services/SelectionService.test.ts index f59c54bf..8a880794 100644 --- a/src/SelectionService.test.ts +++ b/src/browser/services/SelectionService.test.ts @@ -13,13 +13,14 @@ import { IBufferService, IOptionsService } from 'common/services/Services'; import { MockCharSizeService, MockMouseService } from 'browser/TestUtils.test'; import { CellData } from 'common/buffer/CellData'; import { IBuffer } from 'common/buffer/Types'; +import { isMSWindows } from '../../../out/common/Platform'; class TestSelectionService extends SelectionService { constructor( bufferService: IBufferService, optionsService: IOptionsService ) { - super(() => {}, null, null, new MockCharSizeService(10, 10), bufferService, new MockCoreService(), new MockMouseService(), optionsService); + super(() => {}, null!, null!, new MockCharSizeService(10, 10), bufferService, new MockCoreService(), new MockMouseService(), optionsService); } public get model(): SelectionModel { return this._model; } @@ -97,21 +98,21 @@ describe('SelectionService', () => { it('should expand selection for wide characters', () => { // Wide characters use a special format const data: [number, string, number, number][] = [ - [null, '中', 2, '中'.charCodeAt(0)], - [null, '', 0, null], - [null, '文', 2, '文'.charCodeAt(0)], - [null, '', 0, null], - [null, ' ', 1, ' '.charCodeAt(0)], - [null, 'a', 1, 'a'.charCodeAt(0)], - [null, '中', 2, '中'.charCodeAt(0)], - [null, '', 0, null], - [null, '文', 2, '文'.charCodeAt(0)], - [null, '', 0, ''.charCodeAt(0)], - [null, 'b', 1, 'b'.charCodeAt(0)], - [null, ' ', 1, ' '.charCodeAt(0)], - [null, 'f', 1, 'f'.charCodeAt(0)], - [null, 'o', 1, 'o'.charCodeAt(0)], - [null, 'o', 1, 'o'.charCodeAt(0)] + [0, '中', 2, '中'.charCodeAt(0)], + [0, '', 0, 0], + [0, '文', 2, '文'.charCodeAt(0)], + [0, '', 0, 0], + [0, ' ', 1, ' '.charCodeAt(0)], + [0, 'a', 1, 'a'.charCodeAt(0)], + [0, '中', 2, '中'.charCodeAt(0)], + [0, '', 0, 0], + [0, '文', 2, '文'.charCodeAt(0)], + [0, '', 0, ''.charCodeAt(0)], + [0, 'b', 1, 'b'.charCodeAt(0)], + [0, ' ', 1, ' '.charCodeAt(0)], + [0, 'f', 1, 'f'.charCodeAt(0)], + [0, 'o', 1, 'o'.charCodeAt(0)], + [0, 'o', 1, 'o'.charCodeAt(0)] ]; const line = new BufferLine(data.length); for (let i = 0; i < data.length; ++i) line.setCell(i, CellData.fromCharData(data[i])); @@ -188,7 +189,7 @@ describe('SelectionService', () => { it('should expand upwards or downards for wrapped lines', () => { buffer.lines.set(0, stringToRow(' foo')); buffer.lines.set(1, stringToRow('bar ')); - buffer.lines.get(1).isWrapped = true; + buffer.lines.get(1)!.isWrapped = true; selectionService.selectWordAt([1, 1]); assert.equal(selectionService.selectionText, 'foobar'); selectionService.model.clearSelection(); @@ -202,10 +203,10 @@ describe('SelectionService', () => { buffer.lines.set(2, stringToRow('bbbbbbbbbbbbbbbbbbbb')); buffer.lines.set(3, stringToRow('cccccccccccccccccccc')); buffer.lines.set(4, stringToRow('bar ')); - buffer.lines.get(1).isWrapped = true; - buffer.lines.get(2).isWrapped = true; - buffer.lines.get(3).isWrapped = true; - buffer.lines.get(4).isWrapped = true; + buffer.lines.get(1)!.isWrapped = true; + buffer.lines.get(2)!.isWrapped = true; + buffer.lines.get(3)!.isWrapped = true; + buffer.lines.get(4)!.isWrapped = true; selectionService.selectWordAt([18, 0]); assert.equal(selectionService.selectionText, expectedText); selectionService.model.clearSelection(); @@ -359,6 +360,8 @@ describe('SelectionService', () => { buffer.lines.set(3, stringToRow('4')); buffer.lines.set(4, stringToRow('5')); selectionService.selectAll(); + console.log(selectionService.selectionText.length); + console.log(isMSWindows); assert.equal(selectionService.selectionText, '1\n2\n3\n4\n5'); }); }); diff --git a/src/SelectionService.ts b/src/browser/services/SelectionService.ts similarity index 95% rename from src/SelectionService.ts rename to src/browser/services/SelectionService.ts index 2eeb92e0..cfea0cc8 100644 --- a/src/SelectionService.ts +++ b/src/browser/services/SelectionService.ts @@ -73,7 +73,7 @@ export class SelectionService implements ISelectionService { * The amount to scroll every drag scroll update (depends on how far the mouse * drag is above or below the terminal). */ - private _dragScrollAmount: number; + private _dragScrollAmount: number = 0; /** * The current selection mode. @@ -84,12 +84,12 @@ export class SelectionService implements ISelectionService { * A setInterval timer that is active while the mouse is down whose callback * scrolls the viewport when necessary. */ - private _dragScrollIntervalTimer: NodeJS.Timer; + private _dragScrollIntervalTimer: number | undefined; /** * The animation frame ID used for refreshing the selection. */ - private _refreshAnimationFrame: number; + private _refreshAnimationFrame: number | undefined; /** * Whether selection is enabled. @@ -101,7 +101,7 @@ export class SelectionService implements ISelectionService { private _trimListener: IDisposable; private _workCell: CellData = new CellData(); - private _mouseDownTimeStamp: number; + private _mouseDownTimeStamp: number = 0; private _onLinuxMouseSelection = new EventEmitter(); public get onLinuxMouseSelection(): IEvent { return this._onLinuxMouseSelection.event; } @@ -120,7 +120,17 @@ export class SelectionService implements ISelectionService { private readonly _mouseService: IMouseService, private readonly _optionsService: IOptionsService ) { - this._initListeners(); + // Init listeners + this._mouseMoveListener = event => this._onMouseMove(event); + this._mouseUpListener = event => this._onMouseUp(event); + this._coreService.onUserInput(() => { + if (this.hasSelection) { + this.clearSelection(); + } + }); + this._trimListener = this._bufferService.buffer.lines.onTrim(amount => this._onTrim(amount)); + this._bufferService.buffers.onBufferActivate(e => this._onBufferActivate(e)); + this.enable(); this._model = new SelectionModel(this._bufferService); @@ -131,29 +141,10 @@ export class SelectionService implements ISelectionService { this._removeMouseDownListeners(); } - /** - * Initializes listener variables. - */ - private _initListeners(): void { - this._mouseMoveListener = event => this._onMouseMove(event); - this._mouseUpListener = event => this._onMouseUp(event); - this._coreService.onUserInput(() => { - if (this.hasSelection) { - this.clearSelection(); - } - }); - this.initBuffersListeners(); - } - public reset(): void { this.clearSelection(); } - public initBuffersListeners(): void { - this._trimListener = this._bufferService.buffer.lines.onTrim(amount => this._onTrim(amount)); - this._bufferService.buffers.onBufferActivate(e => this._onBufferActivate(e)); - } - /** * Disables the selection manager. This is useful for when terminal mouse * are enabled. @@ -170,8 +161,8 @@ export class SelectionService implements ISelectionService { this._enabled = true; } - public get selectionStart(): [number, number] { return this._model.finalSelectionStart; } - public get selectionEnd(): [number, number] { return this._model.finalSelectionEnd; } + public get selectionStart(): [number, number] | undefined { return this._model.finalSelectionStart; } + public get selectionEnd(): [number, number] | undefined { return this._model.finalSelectionEnd; } /** * Gets whether there is an active text selection. @@ -217,7 +208,7 @@ export class SelectionService implements ISelectionService { for (let i = start[1] + 1; i <= end[1] - 1; i++) { const bufferLine = buffer.lines.get(i); const lineText = buffer.translateBufferLineToString(i, true); - if (bufferLine.isWrapped) { + if (bufferLine!.isWrapped) { result[result.length - 1] += lineText; } else { result.push(lineText); @@ -228,7 +219,7 @@ export class SelectionService implements ISelectionService { if (start[1] !== end[1]) { const bufferLine = buffer.lines.get(end[1]); const lineText = buffer.translateBufferLineToString(end[1], true, 0, end[0]); - if (bufferLine.isWrapped) { + if (bufferLine!.isWrapped) { result[result.length - 1] += lineText; } else { result.push(lineText); @@ -281,7 +272,7 @@ export class SelectionService implements ISelectionService { * selection state. */ private _refresh(): void { - this._refreshAnimationFrame = null; + this._refreshAnimationFrame = undefined; this._onRedrawRequest.fire({ start: this._model.finalSelectionStart, end: this._model.finalSelectionEnd, @@ -298,7 +289,7 @@ export class SelectionService implements ISelectionService { const start = this._model.finalSelectionStart; const end = this._model.finalSelectionEnd; - if (!start || !end) { + if (!start || !end || !coords) { return false; } @@ -320,7 +311,7 @@ export class SelectionService implements ISelectionService { const coords = this._getMouseBufferCoords(event); if (coords) { this._selectWordAt(coords, false); - this._model.selectionEnd = null; + this._model.selectionEnd = undefined; this.refresh(true); } } @@ -359,10 +350,10 @@ export class SelectionService implements ISelectionService { * Gets the 0-based [x, y] buffer coordinates of the current mouse event. * @param event The mouse event. */ - private _getMouseBufferCoords(event: MouseEvent): [number, number] { + private _getMouseBufferCoords(event: MouseEvent): [number, number] | undefined { const coords = this._mouseService.getCoords(event, this._screenElement, this._bufferService.cols, this._bufferService.rows, true); if (!coords) { - return null; + return undefined; } // Convert to 0-based @@ -461,9 +452,11 @@ export class SelectionService implements ISelectionService { */ private _addMouseDownListeners(): void { // Listen on the document so that dragging outside of viewport works - this._screenElement.ownerDocument.addEventListener('mousemove', this._mouseMoveListener); - this._screenElement.ownerDocument.addEventListener('mouseup', this._mouseUpListener); - this._dragScrollIntervalTimer = setInterval(() => this._dragScroll(), DRAG_SCROLL_INTERVAL); + if (this._screenElement.ownerDocument) { + this._screenElement.ownerDocument.addEventListener('mousemove', this._mouseMoveListener); + this._screenElement.ownerDocument.addEventListener('mouseup', this._mouseUpListener); + } + this._dragScrollIntervalTimer = window.setInterval(() => this._dragScroll(), DRAG_SCROLL_INTERVAL); } /** @@ -475,7 +468,7 @@ export class SelectionService implements ISelectionService { this._screenElement.ownerDocument.removeEventListener('mouseup', this._mouseUpListener); } clearInterval(this._dragScrollIntervalTimer); - this._dragScrollIntervalTimer = null; + this._dragScrollIntervalTimer = undefined; } /** @@ -504,7 +497,7 @@ export class SelectionService implements ISelectionService { if (!this._model.selectionStart) { return; } - this._model.selectionEnd = null; + this._model.selectionEnd = undefined; // Ensure the line exists const line = this._bufferService.buffer.lines.get(this._model.selectionStart[1]); @@ -568,6 +561,11 @@ export class SelectionService implements ISelectionService { // to be sent to the pty. event.stopImmediatePropagation(); + // Something went wrong + if (!this._model.selectionStart) { + throw new Error('Selection start position was not set before mousemove event'); + } + // Record the previous position so we know whether to redraw the selection // at the end. const previousSelectionEnd = this._model.selectionEnd ? [this._model.selectionEnd[0], this._model.selectionEnd[1]] : null; @@ -609,7 +607,8 @@ export class SelectionService implements ISelectionService { // have a character. const buffer = this._bufferService.buffer; if (this._model.selectionEnd[1] < buffer.lines.length) { - if (buffer.lines.get(this._model.selectionEnd[1]).hasWidth(this._model.selectionEnd[0]) === 0) { + const line = buffer.lines.get(this._model.selectionEnd[1]); + if (line && line.hasWidth(this._model.selectionEnd[0]) === 0) { this._model.selectionEnd[0]++; } } @@ -627,6 +626,9 @@ export class SelectionService implements ISelectionService { * scrolling of the viewport. */ private _dragScroll(): void { + if (!this._model.selectionEnd || !this._model.selectionStart) { + return; + } if (this._dragScrollAmount) { this._scrollLines(this._dragScrollAmount, false); // Re-evaluate selection @@ -724,16 +726,16 @@ export class SelectionService implements ISelectionService { * Gets positional information for the word at the coordinated specified. * @param coords The coordinates to get the word at. */ - private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean, followWrappedLinesAbove: boolean = true, followWrappedLinesBelow: boolean = true): IWordPosition { + private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean, followWrappedLinesAbove: boolean = true, followWrappedLinesBelow: boolean = true): IWordPosition | undefined { // Ensure coords are within viewport (eg. not within scroll bar) if (coords[0] >= this._bufferService.cols) { - return null; + return undefined; } const buffer = this._bufferService.buffer; const bufferLine = buffer.lines.get(coords[1]); if (!bufferLine) { - return null; + return undefined; } const line = buffer.translateBufferLineToString(coords[1], false); @@ -840,7 +842,7 @@ export class SelectionService implements ISelectionService { - rightLongCharOffset); // The number of additional chars right of the initial char (inclusive) added by columns with strings longer than 1 (emojis) if (!allowWhitespaceOnlySelection && line.slice(startIndex, endIndex).trim() === '') { - return null; + return undefined; } // Recurse upwards if the line is wrapped and the word wraps to the above line diff --git a/src/browser/services/Services.d.ts b/src/browser/services/Services.d.ts index 6426eeea..b22a0b37 100644 --- a/src/browser/services/Services.d.ts +++ b/src/browser/services/Services.d.ts @@ -51,11 +51,11 @@ export interface IRenderService { export interface ISelectionService { readonly selectionText: string; readonly hasSelection: boolean; - readonly selectionStart: [number, number]; - readonly selectionEnd: [number, number]; + readonly selectionStart: [number, number] | undefined; + readonly selectionEnd: [number, number] | undefined; readonly onLinuxMouseSelection: IEvent; - readonly onRedrawRequest: IEvent + readonly onRedrawRequest: IEvent; readonly onSelectionChange: IEvent; disable(): void; diff --git a/src/common/services/Services.d.ts b/src/common/services/Services.d.ts index 08263b49..f46f3d76 100644 --- a/src/common/services/Services.d.ts +++ b/src/common/services/Services.d.ts @@ -104,6 +104,7 @@ export interface ITerminalOptions { tabStopWidth: number; theme: ITheme; windowsMode: boolean; + wordSeparator: string; [key: string]: any; cancelEvents: boolean; @@ -112,7 +113,6 @@ export interface ITerminalOptions { screenKeys: boolean; termName: string; useFlowControl: boolean; - wordSeparator?: string; } export interface ITheme {