From 62e2b00bd5bbb6cb29583ce2577469a3aac6a580 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 21 Jun 2019 19:54:11 -0700 Subject: [PATCH 01/12] Remove terminal dep in clipboard --- src/Clipboard.ts | 51 ++++++++++++++++++++++++------------------------ src/Terminal.ts | 10 +++++----- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/src/Clipboard.ts b/src/Clipboard.ts index 9461afa1..75b0da8e 100644 --- a/src/Clipboard.ts +++ b/src/Clipboard.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ITerminal, ISelectionManager } from './Types'; +import { ISelectionManager } from './Types'; /** * Prepares text to be pasted into the terminal by normalizing the line endings @@ -28,7 +28,7 @@ export function bracketTextForPaste(text: string, bracketedPasteMode: boolean): * Binds copy functionality to the given terminal. * @param ev The original copy event to be handled */ -export function copyHandler(ev: ClipboardEvent, term: ITerminal, selectionManager: ISelectionManager): void { +export function copyHandler(ev: ClipboardEvent, selectionManager: ISelectionManager): void { ev.clipboardData.setData('text/plain', selectionManager.selectionText); // Prevent or the original text will be copied. ev.preventDefault(); @@ -39,17 +39,16 @@ export function copyHandler(ev: ClipboardEvent, term: ITerminal, selectionManage * @param ev The original paste event to be handled * @param term The terminal on which to apply the handled paste event */ -export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void { +export function pasteHandler(ev: ClipboardEvent, textarea: HTMLTextAreaElement, bracketedPasteMode: boolean, triggerUserInput: (data: string) => void): void { ev.stopPropagation(); let text: string; const dispatchPaste = function(text: string): void { text = prepareTextForTerminal(text); - text = bracketTextForPaste(text, term.bracketedPasteMode); - term.handler(text); - term.textarea.value = ''; - term.cancel(ev); + text = bracketTextForPaste(text, bracketedPasteMode); + triggerUserInput(text); + textarea.value = ''; }; if (ev.clipboardData) { @@ -63,32 +62,32 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void { * @param ev The original right click event to be handled. * @param textarea The terminal's textarea. */ -export function moveTextAreaUnderMouseCursor(ev: MouseEvent, term: ITerminal): void { +export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextAreaElement, screenElement: HTMLElement): void { // Calculate textarea position relative to the screen element - const pos = term.screenElement.getBoundingClientRect(); + const pos = screenElement.getBoundingClientRect(); const left = ev.clientX - pos.left - 10; const top = ev.clientY - pos.top - 10; // Bring textarea at the cursor position - term.textarea.style.position = 'absolute'; - term.textarea.style.width = '20px'; - term.textarea.style.height = '20px'; - term.textarea.style.left = `${left}px`; - term.textarea.style.top = `${top}px`; - term.textarea.style.zIndex = '1000'; + textarea.style.position = 'absolute'; + textarea.style.width = '20px'; + textarea.style.height = '20px'; + textarea.style.left = `${left}px`; + textarea.style.top = `${top}px`; + textarea.style.zIndex = '1000'; - term.textarea.focus(); + textarea.focus(); // Reset the terminal textarea's styling // Timeout needs to be long enough for click event to be handled. setTimeout(() => { - term.textarea.style.position = null; - term.textarea.style.width = null; - term.textarea.style.height = null; - term.textarea.style.left = null; - term.textarea.style.top = null; - term.textarea.style.zIndex = null; + textarea.style.position = null; + textarea.style.width = null; + textarea.style.height = null; + textarea.style.left = null; + textarea.style.top = null; + textarea.style.zIndex = null; }, 200); } @@ -99,14 +98,14 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, term: ITerminal): v * @param selectionManager The terminal's selection manager. * @param shouldSelectWord If true and there is no selection the current word will be selected */ -export function rightClickHandler(ev: MouseEvent, term: ITerminal, selectionManager: ISelectionManager, shouldSelectWord: boolean): void { - moveTextAreaUnderMouseCursor(ev, term); +export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, screenElement: HTMLElement, selectionManager: ISelectionManager, shouldSelectWord: boolean): void { + moveTextAreaUnderMouseCursor(ev, textarea, screenElement); if (shouldSelectWord && !selectionManager.isClickInSelection(ev)) { selectionManager.selectWordAtCursor(ev); } // Get textarea ready to copy from the context menu - term.textarea.value = selectionManager.selectionText; - term.textarea.select(); + textarea.value = selectionManager.selectionText; + textarea.select(); } diff --git a/src/Terminal.ts b/src/Terminal.ts index b28cfcfa..310aebe9 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -478,9 +478,9 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp if (!this.hasSelection()) { return; } - copyHandler(event, this, this.selectionManager); + copyHandler(event, this.selectionManager); })); - const pasteHandlerWrapper = (event: ClipboardEvent) => pasteHandler(event, this); + const pasteHandlerWrapper = (event: ClipboardEvent) => pasteHandler(event, this.textarea, this.bracketedPasteMode, e => this.handler(e)); this.register(addDisposableDomListener(this.textarea, 'paste', pasteHandlerWrapper)); this.register(addDisposableDomListener(this.element, 'paste', pasteHandlerWrapper)); @@ -489,12 +489,12 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // Firefox doesn't appear to fire the contextmenu event on right click this.register(addDisposableDomListener(this.element, 'mousedown', (event: MouseEvent) => { if (event.button === 2) { - rightClickHandler(event, this, this.selectionManager, this.options.rightClickSelectsWord); + rightClickHandler(event, this.textarea, this.screenElement, this.selectionManager, this.options.rightClickSelectsWord); } })); } else { this.register(addDisposableDomListener(this.element, 'contextmenu', (event: MouseEvent) => { - rightClickHandler(event, this, this.selectionManager, this.options.rightClickSelectsWord); + rightClickHandler(event, this.textarea, this.screenElement, this.selectionManager, this.options.rightClickSelectsWord); })); } @@ -506,7 +506,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // that the regular click event doesn't fire for the middle mouse button. this.register(addDisposableDomListener(this.element, 'auxclick', (event: MouseEvent) => { if (event.button === 1) { - moveTextAreaUnderMouseCursor(event, this); + moveTextAreaUnderMouseCursor(event, this.textarea, this.screenElement); } })); } From 72f615da14a0e0d627db4cff81172b0e9ef98285 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 21 Jun 2019 20:08:58 -0700 Subject: [PATCH 02/12] Remove some of terminal dependency in SelectionManager --- src/SelectionManager.ts | 82 ++++++++++++++++++++--------------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index fb2caba8..4bd62cdc 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -114,13 +114,13 @@ export class SelectionManager implements ISelectionManager { constructor( private readonly _terminal: ITerminal, private readonly _charSizeService: ICharSizeService, - readonly bufferService: IBufferService, + private readonly _bufferService: IBufferService, private readonly _mouseService: IMouseService ) { this._initListeners(); this.enable(); - this._model = new SelectionModel(bufferService); + this._model = new SelectionModel(this._bufferService); this._activeSelectionMode = SelectionMode.NORMAL; } @@ -128,10 +128,6 @@ export class SelectionManager implements ISelectionManager { this._removeMouseDownListeners(); } - private get _buffer(): IBuffer { - return this._terminal.buffers.active; - } - /** * Initializes listener variables. */ @@ -143,8 +139,8 @@ export class SelectionManager implements ISelectionManager { } public initBuffersListeners(): void { - this._trimListener = this._terminal.buffer.lines.onTrim(amount => this._onTrim(amount)); - this._terminal.buffers.onBufferActivate(e => this._onBufferActivate(e)); + this._trimListener = this._bufferService.buffer.lines.onTrim(amount => this._onTrim(amount)); + this._bufferService.buffers.onBufferActivate(e => this._onBufferActivate(e)); } /** @@ -188,6 +184,7 @@ export class SelectionManager implements ISelectionManager { return ''; } + const buffer = this._bufferService.buffer; const result: string[] = []; if (this._activeSelectionMode === SelectionMode.COLUMN) { @@ -197,18 +194,18 @@ export class SelectionManager implements ISelectionManager { } for (let i = start[1]; i <= end[1]; i++) { - const lineText = this._buffer.translateBufferLineToString(i, true, start[0], end[0]); + const lineText = buffer.translateBufferLineToString(i, true, start[0], end[0]); result.push(lineText); } } else { // Get first row const startRowEndCol = start[1] === end[1] ? end[0] : undefined; - result.push(this._buffer.translateBufferLineToString(start[1], true, start[0], startRowEndCol)); + result.push(buffer.translateBufferLineToString(start[1], true, start[0], startRowEndCol)); // Get middle rows for (let i = start[1] + 1; i <= end[1] - 1; i++) { - const bufferLine = this._buffer.lines.get(i); - const lineText = this._buffer.translateBufferLineToString(i, true); + const bufferLine = buffer.lines.get(i); + const lineText = buffer.translateBufferLineToString(i, true); if (bufferLine.isWrapped) { result[result.length - 1] += lineText; } else { @@ -218,8 +215,8 @@ export class SelectionManager implements ISelectionManager { // Get final row if (start[1] !== end[1]) { - const bufferLine = this._buffer.lines.get(end[1]); - const lineText = this._buffer.translateBufferLineToString(end[1], true, 0, end[0]); + const bufferLine = buffer.lines.get(end[1]); + const lineText = buffer.translateBufferLineToString(end[1], true, 0, end[0]); if (bufferLine.isWrapped) { result[result.length - 1] += lineText; } else { @@ -329,9 +326,9 @@ export class SelectionManager implements ISelectionManager { public selectLines(start: number, end: number): void { this._model.clearSelection(); start = Math.max(start, 0); - end = Math.min(end, this._terminal.buffer.lines.length - 1); + end = Math.min(end, this._bufferService.buffer.lines.length - 1); this._model.selectionStart = [0, start]; - this._model.selectionEnd = [this._terminal.cols, end]; + this._model.selectionEnd = [this._bufferService.cols, end]; this.refresh(); this._onSelectionChange.fire(); } @@ -362,7 +359,7 @@ export class SelectionManager implements ISelectionManager { coords[1]--; // Convert viewport coords to buffer coords - coords[1] += this._terminal.buffer.ydisp; + coords[1] += this._bufferService.buffer.ydisp; return coords; } @@ -499,7 +496,7 @@ export class SelectionManager implements ISelectionManager { this._model.selectionEnd = null; // Ensure the line exists - const line = this._buffer.lines.get(this._model.selectionStart[1]); + const line = this._bufferService.buffer.lines.get(this._model.selectionStart[1]); if (!line) { return; } @@ -576,7 +573,7 @@ export class SelectionManager implements ISelectionManager { if (this._model.selectionEnd[1] < this._model.selectionStart[1]) { this._model.selectionEnd[0] = 0; } else { - this._model.selectionEnd[0] = this._terminal.cols; + this._model.selectionEnd[0] = this._bufferService.cols; } } else if (this._activeSelectionMode === SelectionMode.WORD) { this._selectToWordAt(this._model.selectionEnd); @@ -590,7 +587,7 @@ export class SelectionManager implements ISelectionManager { // NOT in column select mode. if (this._activeSelectionMode !== SelectionMode.COLUMN) { if (this._dragScrollAmount > 0) { - this._model.selectionEnd[0] = this._terminal.cols; + this._model.selectionEnd[0] = this._bufferService.cols; } else if (this._dragScrollAmount < 0) { this._model.selectionEnd[0] = 0; } @@ -599,8 +596,9 @@ export class SelectionManager implements ISelectionManager { // If the character is a wide character include the cell to the right in the // selection. Note that selections at the very end of the line will never // have a character. - if (this._model.selectionEnd[1] < this._buffer.lines.length) { - if (this._buffer.lines.get(this._model.selectionEnd[1]).hasWidth(this._model.selectionEnd[0]) === 0) { + 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) { this._model.selectionEnd[0]++; } } @@ -624,16 +622,17 @@ export class SelectionManager implements ISelectionManager { // If the cursor was above or below the viewport, make sure it's at the // start or end of the viewport respectively. This should only happen when // NOT in column select mode. + const buffer = this._bufferService.buffer; if (this._dragScrollAmount > 0) { if (this._activeSelectionMode !== SelectionMode.COLUMN) { - this._model.selectionEnd[0] = this._terminal.cols; + this._model.selectionEnd[0] = this._bufferService.cols; } - this._model.selectionEnd[1] = Math.min(this._terminal.buffer.ydisp + this._terminal.rows, this._terminal.buffer.lines.length - 1); + this._model.selectionEnd[1] = Math.min(buffer.ydisp + this._bufferService.rows, buffer.lines.length - 1); } else { if (this._activeSelectionMode !== SelectionMode.COLUMN) { this._model.selectionEnd[0] = 0; } - this._model.selectionEnd[1] = this._terminal.buffer.ydisp; + this._model.selectionEnd[1] = buffer.ydisp; } this.refresh(); } @@ -704,16 +703,17 @@ export class SelectionManager implements ISelectionManager { */ private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean, followWrappedLinesAbove: boolean = true, followWrappedLinesBelow: boolean = true): IWordPosition { // Ensure coords are within viewport (eg. not within scroll bar) - if (coords[0] >= this._terminal.cols) { + if (coords[0] >= this._bufferService.cols) { return null; } - const bufferLine = this._buffer.lines.get(coords[1]); + const buffer = this._bufferService.buffer; + const bufferLine = buffer.lines.get(coords[1]); if (!bufferLine) { return null; } - const line = this._buffer.translateBufferLineToString(coords[1], false); + const line = buffer.translateBufferLineToString(coords[1], false); // Get actual index, taking into consideration wide characters let startIndex = this._convertViewportColToCharacterIndex(bufferLine, coords); @@ -808,7 +808,7 @@ export class SelectionManager implements ISelectionManager { // Calculate the length in _columns_, converting the the string indexes back // to column coordinates. - let length = Math.min(this._terminal.cols, // Disallow lengths larger than the terminal cols + let length = Math.min(this._bufferService.cols, // Disallow lengths larger than the terminal cols endIndex // The index of the selection's end char in the line string - startIndex // The index of the selection's start char in the line string + leftWideCharCount // The number of wide chars left of the initial char @@ -823,11 +823,11 @@ export class SelectionManager implements ISelectionManager { // Recurse upwards if the line is wrapped and the word wraps to the above line if (followWrappedLinesAbove) { if (start === 0 && bufferLine.getCodePoint(0) !== 32 /*' '*/) { - const previousBufferLine = this._buffer.lines.get(coords[1] - 1); - if (previousBufferLine && bufferLine.isWrapped && previousBufferLine.getCodePoint(this._terminal.cols - 1) !== 32 /*' '*/) { - const previousLineWordPosition = this._getWordAt([this._terminal.cols - 1, coords[1] - 1], false, true, false); + const previousBufferLine = buffer.lines.get(coords[1] - 1); + if (previousBufferLine && bufferLine.isWrapped && previousBufferLine.getCodePoint(this._bufferService.cols - 1) !== 32 /*' '*/) { + const previousLineWordPosition = this._getWordAt([this._bufferService.cols - 1, coords[1] - 1], false, true, false); if (previousLineWordPosition) { - const offset = this._terminal.cols - previousLineWordPosition.start; + const offset = this._bufferService.cols - previousLineWordPosition.start; start -= offset; length += offset; } @@ -837,8 +837,8 @@ export class SelectionManager implements ISelectionManager { // Recurse downwards if the line is wrapped and the word wraps to the next line if (followWrappedLinesBelow) { - if (start + length === this._terminal.cols && bufferLine.getCodePoint(this._terminal.cols - 1) !== 32 /*' '*/) { - const nextBufferLine = this._buffer.lines.get(coords[1] + 1); + if (start + length === this._bufferService.cols && bufferLine.getCodePoint(this._bufferService.cols - 1) !== 32 /*' '*/) { + const nextBufferLine = buffer.lines.get(coords[1] + 1); if (nextBufferLine && nextBufferLine.isWrapped && nextBufferLine.getCodePoint(0) !== 32 /*' '*/) { const nextLineWordPosition = this._getWordAt([0, coords[1] + 1], false, false, true); if (nextLineWordPosition) { @@ -861,7 +861,7 @@ export class SelectionManager implements ISelectionManager { if (wordPosition) { // Adjust negative start value while (wordPosition.start < 0) { - wordPosition.start += this._terminal.cols; + wordPosition.start += this._bufferService.cols; coords[1]--; } this._model.selectionStart = [wordPosition.start, coords[1]]; @@ -880,15 +880,15 @@ export class SelectionManager implements ISelectionManager { // Adjust negative start value while (wordPosition.start < 0) { - wordPosition.start += this._terminal.cols; + wordPosition.start += this._bufferService.cols; endRow--; } // Adjust wrapped length value, this only needs to happen when values are reversed as in that // case we're interested in the start of the word, not the end if (!this._model.areSelectionValuesReversed()) { - while (wordPosition.start + wordPosition.length > this._terminal.cols) { - wordPosition.length -= this._terminal.cols; + while (wordPosition.start + wordPosition.length > this._bufferService.cols) { + wordPosition.length -= this._bufferService.cols; endRow++; } } @@ -916,9 +916,9 @@ export class SelectionManager implements ISelectionManager { * @param line The line index. */ protected _selectLineAt(line: number): void { - const wrappedRange = this._buffer.getWrappedRangeForLine(line); + const wrappedRange = this._bufferService.buffer.getWrappedRangeForLine(line); this._model.selectionStart = [0, wrappedRange.first]; - this._model.selectionEnd = [this._terminal.cols, wrappedRange.last]; + this._model.selectionEnd = [this._bufferService.cols, wrappedRange.last]; this._model.selectionStartLength = 0; } } From 0ba90a934951b08c46bc953c82b912bf76a40125 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 21 Jun 2019 20:12:17 -0700 Subject: [PATCH 03/12] Adopt options service in selection manager --- src/SelectionManager.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 4bd62cdc..060c0366 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -13,7 +13,7 @@ import { CellData } from 'common/buffer/CellData'; import { IDisposable } from 'xterm'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { ICharSizeService, IMouseService } from 'browser/services/Services'; -import { IBufferService } from 'common/services/Services'; +import { IBufferService, IOptionsService } from 'common/services/Services'; import { getCoordsRelativeToElement } from 'browser/input/Mouse'; /** @@ -115,7 +115,8 @@ export class SelectionManager implements ISelectionManager { private readonly _terminal: ITerminal, private readonly _charSizeService: ICharSizeService, private readonly _bufferService: IBufferService, - private readonly _mouseService: IMouseService + private readonly _mouseService: IMouseService, + private readonly _optionsService: IOptionsService ) { this._initListeners(); this.enable(); @@ -349,7 +350,7 @@ export class SelectionManager implements ISelectionManager { * @param event The mouse event. */ private _getMouseBufferCoords(event: MouseEvent): [number, number] { - const coords = this._mouseService.getCoords(event, this._terminal.screenElement, this._terminal.cols, this._terminal.rows, true); + const coords = this._mouseService.getCoords(event, this._terminal.screenElement, this._bufferService.cols, this._bufferService.rows, true); if (!coords) { return null; } @@ -370,7 +371,7 @@ export class SelectionManager implements ISelectionManager { */ private _getMouseEventScrollAmount(event: MouseEvent): number { let offset = getCoordsRelativeToElement(event, this._terminal.screenElement)[1]; - const terminalHeight = this._terminal.rows * Math.ceil(this._charSizeService.height * this._terminal.options.lineHeight); + const terminalHeight = this._bufferService.rows * Math.ceil(this._charSizeService.height * this._optionsService.options.lineHeight); if (offset >= 0 && offset <= terminalHeight) { return 0; } @@ -390,7 +391,7 @@ export class SelectionManager implements ISelectionManager { */ public shouldForceSelection(event: MouseEvent): boolean { if (Browser.isMac) { - return event.altKey && this._terminal.options.macOptionClickForcesSelection; + return event.altKey && this._optionsService.options.macOptionClickForcesSelection; } return event.shiftKey; @@ -543,7 +544,7 @@ export class SelectionManager implements ISelectionManager { * @param event the mouse or keyboard event */ public shouldColumnSelect(event: KeyboardEvent | MouseEvent): boolean { - return event.altKey && !(Browser.isMac && this._terminal.options.macOptionClickForcesSelection); + return event.altKey && !(Browser.isMac && this._optionsService.options.macOptionClickForcesSelection); } /** @@ -908,7 +909,7 @@ export class SelectionManager implements ISelectionManager { if (cell.getWidth() === 0) { return false; } - return this._terminal.optionsService.options.wordSeparator.indexOf(cell.getChars()) >= 0; + return this._optionsService.options.wordSeparator.indexOf(cell.getChars()) >= 0; } /** From 9174f60d94fe097f11da7b5f28e03ebb26e7c711 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 21 Jun 2019 20:20:48 -0700 Subject: [PATCH 04/12] Move screen element into ctor --- src/SelectionManager.test.ts | 15 +++++++++------ src/SelectionManager.ts | 15 ++++++++------- src/Terminal.ts | 2 +- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index 0b82466d..814cb5fe 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -10,9 +10,9 @@ import { ITerminal } from './Types'; import { IBuffer } from 'common/buffer/Types'; import { IBufferLine } from 'common/Types'; import { MockTerminal } from './TestUtils.test'; -import { MockBufferService } from 'common/TestUtils.test'; +import { MockBufferService, MockOptionsService } from 'common/TestUtils.test'; import { BufferLine } from 'common/buffer/BufferLine'; -import { IBufferService } from 'common/services/Services'; +import { IBufferService, IOptionsService } from 'common/services/Services'; import { MockCharSizeService, MockMouseService } from 'browser/TestUtils.test'; import { CellData } from 'common/buffer/CellData'; @@ -23,9 +23,10 @@ class TestMockTerminal extends MockTerminal { class TestSelectionManager extends SelectionManager { constructor( terminal: ITerminal, - bufferService: IBufferService + bufferService: IBufferService, + optionsService: IOptionsService ) { - super(terminal, new MockCharSizeService(10, 10), bufferService, new MockMouseService()); + super(terminal, null, new MockCharSizeService(10, 10), bufferService, new MockMouseService(), optionsService); } public get model(): SelectionModel { return this._model; } @@ -46,17 +47,19 @@ describe('SelectionManager', () => { let terminal: ITerminal; let buffer: IBuffer; let bufferService: IBufferService; + let optionsService: IOptionsService; let selectionManager: TestSelectionManager; beforeEach(() => { terminal = new TestMockTerminal(); - bufferService = new MockBufferService(20, 20); + optionsService = new MockOptionsService(); + bufferService = new MockBufferService(20, 20, optionsService); terminal.buffers = bufferService.buffers; terminal.cols = 20; terminal.rows = 20; terminal.buffer = terminal.buffers.active; buffer = terminal.buffer; - selectionManager = new TestSelectionManager(terminal, bufferService); + selectionManager = new TestSelectionManager(terminal, bufferService, optionsService); }); function stringToRow(text: string): IBufferLine { diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 060c0366..767cf156 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -113,6 +113,7 @@ export class SelectionManager implements ISelectionManager { constructor( private readonly _terminal: ITerminal, + private readonly _screenElement: HTMLElement, private readonly _charSizeService: ICharSizeService, private readonly _bufferService: IBufferService, private readonly _mouseService: IMouseService, @@ -350,7 +351,7 @@ export class SelectionManager implements ISelectionManager { * @param event The mouse event. */ private _getMouseBufferCoords(event: MouseEvent): [number, number] { - const coords = this._mouseService.getCoords(event, this._terminal.screenElement, this._bufferService.cols, this._bufferService.rows, true); + const coords = this._mouseService.getCoords(event, this._screenElement, this._bufferService.cols, this._bufferService.rows, true); if (!coords) { return null; } @@ -370,7 +371,7 @@ export class SelectionManager implements ISelectionManager { * @param event The mouse event. */ private _getMouseEventScrollAmount(event: MouseEvent): number { - let offset = getCoordsRelativeToElement(event, this._terminal.screenElement)[1]; + let offset = getCoordsRelativeToElement(event, this._screenElement)[1]; const terminalHeight = this._bufferService.rows * Math.ceil(this._charSizeService.height * this._optionsService.options.lineHeight); if (offset >= 0 && offset <= terminalHeight) { return 0; @@ -451,8 +452,8 @@ export class SelectionManager implements ISelectionManager { */ private _addMouseDownListeners(): void { // Listen on the document so that dragging outside of viewport works - this._terminal.element.ownerDocument.addEventListener('mousemove', this._mouseMoveListener); - this._terminal.element.ownerDocument.addEventListener('mouseup', this._mouseUpListener); + this._screenElement.ownerDocument.addEventListener('mousemove', this._mouseMoveListener); + this._screenElement.ownerDocument.addEventListener('mouseup', this._mouseUpListener); this._dragScrollIntervalTimer = setInterval(() => this._dragScroll(), DRAG_SCROLL_INTERVAL); } @@ -460,9 +461,9 @@ export class SelectionManager implements ISelectionManager { * Removes the listeners that are registered when mousedown is triggered. */ private _removeMouseDownListeners(): void { - if (this._terminal.element.ownerDocument) { - this._terminal.element.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener); - this._terminal.element.ownerDocument.removeEventListener('mouseup', this._mouseUpListener); + if (this._screenElement.ownerDocument) { + this._screenElement.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener); + this._screenElement.ownerDocument.removeEventListener('mouseup', this._mouseUpListener); } clearInterval(this._dragScrollIntervalTimer); this._dragScrollIntervalTimer = null; diff --git a/src/Terminal.ts b/src/Terminal.ts index 310aebe9..72cecb19 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -640,7 +640,7 @@ 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.selectionManager = new SelectionManager(this, this._charSizeService, this._bufferService, this._mouseService); + this.selectionManager = new SelectionManager(this, this.screenElement, this._charSizeService, this._bufferService, this._mouseService, this.optionsService); this.register(this.selectionManager.onSelectionChange(() => this._onSelectionChange.fire())); this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e))); this.register(this.selectionManager.onRedrawRequest(e => this._renderService.onSelectionChanged(e.start, e.end, e.columnSelectMode))); From 00969508d9352da6156e7d924f2f1b2ce7f3959b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 22 Jun 2019 09:40:11 -0700 Subject: [PATCH 05/12] Remove some private member deps in AltClickHandler --- src/SelectionManager.ts | 2 +- src/handlers/AltClickHandler.ts | 172 ++++++++++++++++---------------- 2 files changed, 89 insertions(+), 85 deletions(-) diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 767cf156..0a5daed2 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -650,7 +650,7 @@ export class SelectionManager implements ISelectionManager { this._removeMouseDownListeners(); if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME) { - (new AltClickHandler(event, this._terminal, this._mouseService)).move(); + (new AltClickHandler(event, this._terminal, this._mouseService)).move(this._bufferService, this._terminal.applicationCursor); } else if (this.hasSelection) { this._onSelectionChange.fire(); } diff --git a/src/handlers/AltClickHandler.ts b/src/handlers/AltClickHandler.ts index 334b78d8..7b46d745 100644 --- a/src/handlers/AltClickHandler.ts +++ b/src/handlers/AltClickHandler.ts @@ -7,6 +7,7 @@ import { ITerminal } from '../Types'; import { IBufferLine, ICircularList } from 'common/Types'; import { C0 } from 'common/data/EscapeSequences'; import { IMouseService } from 'browser/services/Services'; +import { IBufferService } from 'common/services/Services'; const enum Direction { UP = 'A', @@ -49,9 +50,9 @@ export class AltClickHandler { /** * Writes the escape sequences of arrows to the terminal */ - public move(): void { + public move(bufferService: IBufferService, applicationCursor: boolean): void { if (this._mouseEvent.altKey && this._endCol !== undefined && this._endRow !== undefined) { - this._terminal.handler(this._arrowSequences()); + this._terminal.handler(this._arrowSequences(bufferService, applicationCursor)); } } @@ -60,14 +61,16 @@ export class AltClickHandler { * Resets the starting row to an unwrapped row, moves to the requested row, * then moves to requested col. */ - private _arrowSequences(): string { + private _arrowSequences(bufferService: IBufferService, applicationCursor: boolean): string { // The alt buffer should try to navigate between rows - if (!this._terminal.buffer.hasScrollback) { - return this._resetStartingRow() + this._moveToRequestedRow() + this._moveToRequestedCol(); + if (!bufferService.buffer.hasScrollback) { + return this._resetStartingRow(bufferService, applicationCursor) + + this._moveToRequestedRow(bufferService, applicationCursor) + + this._moveToRequestedCol(bufferService, applicationCursor); } // Only move horizontally for the normal buffer - return this._moveHorizontallyOnly(); + return this._moveHorizontallyOnly(bufferService, applicationCursor); } /** @@ -75,52 +78,52 @@ export class AltClickHandler { * cursor up to the first row that is not wrapped to have accurate vertical * positioning. */ - private _resetStartingRow(): string { - if (this._moveToRequestedRow().length === 0) { + private _resetStartingRow(bufferService: IBufferService, applicationCursor: boolean): string { + if (this._moveToRequestedRow(bufferService, applicationCursor).length === 0) { return ''; } - return repeat(this._bufferLine( + return repeat(bufferLine( this._startCol, this._startRow, this._startCol, - this._startRow - this._wrappedRowsForRow(this._startRow), false - ).length, this._sequence(Direction.LEFT)); + this._startRow - this._wrappedRowsForRow(bufferService, this._startRow), false, bufferService + ).length, sequence(Direction.LEFT, applicationCursor)); } /** * Using the reset starting and ending row, move to the requested row, * ignoring wrapped rows */ - private _moveToRequestedRow(): string { - const startRow = this._startRow - this._wrappedRowsForRow(this._startRow); - const endRow = this._endRow - this._wrappedRowsForRow(this._endRow); + private _moveToRequestedRow(bufferService: IBufferService, applicationCursor: boolean): string { + const startRow = this._startRow - this._wrappedRowsForRow(bufferService, this._startRow); + const endRow = this._endRow - this._wrappedRowsForRow(bufferService, this._endRow); - const rowsToMove = Math.abs(startRow - endRow) - this._wrappedRowsCount(); + const rowsToMove = Math.abs(startRow - endRow) - this._wrappedRowsCount(bufferService); - return repeat(rowsToMove, this._sequence(this._verticalDirection())); + return repeat(rowsToMove, sequence(this._verticalDirection(), applicationCursor)); } /** * Move to the requested col on the ending row */ - private _moveToRequestedCol(): string { + private _moveToRequestedCol(bufferService: IBufferService, applicationCursor: boolean): string { let startRow; - if (this._moveToRequestedRow().length > 0) { - startRow = this._endRow - this._wrappedRowsForRow(this._endRow); + if (this._moveToRequestedRow(bufferService, applicationCursor).length > 0) { + startRow = this._endRow - this._wrappedRowsForRow(bufferService, this._endRow); } else { startRow = this._startRow; } const endRow = this._endRow; - const direction = this._horizontalDirection(); + const direction = this._horizontalDirection(bufferService, applicationCursor); - return repeat(this._bufferLine( + return repeat(bufferLine( this._startCol, startRow, this._endCol, endRow, - direction === Direction.RIGHT - ).length, this._sequence(direction)); + direction === Direction.RIGHT, bufferService + ).length, sequence(direction, applicationCursor)); } - private _moveHorizontallyOnly(): string { - const direction = this._horizontalDirection(); - return repeat(Math.abs(this._startCol - this._endCol), this._sequence(direction)); + private _moveHorizontallyOnly(bufferService: IBufferService, applicationCursor: boolean): string { + const direction = this._horizontalDirection(bufferService, applicationCursor); + return repeat(Math.abs(this._startCol - this._endCol), sequence(direction, applicationCursor)); } /** @@ -131,10 +134,10 @@ export class AltClickHandler { * Calculates the number of wrapped rows between the unwrapped starting and * ending rows. These rows need to ignored since the cursor skips over them. */ - private _wrappedRowsCount(): number { + private _wrappedRowsCount(bufferService: IBufferService): number { let wrappedRows = 0; - const startRow = this._startRow - this._wrappedRowsForRow(this._startRow); - const endRow = this._endRow - this._wrappedRowsForRow(this._endRow); + const startRow = this._startRow - this._wrappedRowsForRow(bufferService, this._startRow); + const endRow = this._endRow - this._wrappedRowsForRow(bufferService, this._endRow); for (let i = 0; i < Math.abs(startRow - endRow); i++) { const direction = this._verticalDirection() === Direction.UP ? -1 : 1; @@ -151,14 +154,14 @@ export class AltClickHandler { * Calculates the number of wrapped rows that make up a given row. * @param currentRow The row to determine how many wrapped rows make it up */ - private _wrappedRowsForRow(currentRow: number): number { + private _wrappedRowsForRow(bufferService: IBufferService, currentRow: number): number { let rowCount = 0; - let lineWraps = this._lines.get(currentRow).isWrapped; + let lineWraps = bufferService.buffer.lines.get(currentRow).isWrapped; - while (lineWraps && currentRow >= 0 && currentRow < this._terminal.rows) { + while (lineWraps && currentRow >= 0 && currentRow < bufferService.rows) { rowCount++; currentRow--; - lineWraps = this._lines.get(currentRow).isWrapped; + lineWraps = bufferService.buffer.lines.get(currentRow).isWrapped; } return rowCount; @@ -171,10 +174,10 @@ export class AltClickHandler { /** * Determines if the right or left arrow is needed */ - private _horizontalDirection(): Direction { + private _horizontalDirection(bufferService: IBufferService, applicationCursor: boolean): Direction { let startRow; - if (this._moveToRequestedRow().length > 0) { - startRow = this._endRow - this._wrappedRowsForRow(this._endRow); + if (this._moveToRequestedRow(bufferService, applicationCursor).length > 0) { + startRow = this._endRow - this._wrappedRowsForRow(bufferService, this._endRow); } else { startRow = this._startRow; } @@ -197,60 +200,61 @@ export class AltClickHandler { } return Direction.DOWN; } +} - /** - * Constructs the string of chars in the buffer from a starting row and col - * to an ending row and col - * @param startCol The starting column position - * @param startRow The starting row position - * @param endCol The ending column position - * @param endRow The ending row position - * @param forward Direction to move - */ - private _bufferLine( - startCol: number, - startRow: number, - endCol: number, - endRow: number, - forward: boolean - ): string { - let currentCol = startCol; - let currentRow = startRow; - let bufferStr = ''; +/** + * Constructs the string of chars in the buffer from a starting row and col + * to an ending row and col + * @param startCol The starting column position + * @param startRow The starting row position + * @param endCol The ending column position + * @param endRow The ending row position + * @param forward Direction to move + */ +function bufferLine( + startCol: number, + startRow: number, + endCol: number, + endRow: number, + forward: boolean, + bufferService: IBufferService +): string { + let currentCol = startCol; + let currentRow = startRow; + let bufferStr = ''; - while (currentCol !== endCol || currentRow !== endRow) { - currentCol += forward ? 1 : -1; + while (currentCol !== endCol || currentRow !== endRow) { + currentCol += forward ? 1 : -1; - if (forward && currentCol > this._terminal.cols - 1) { - bufferStr += this._terminal.buffer.translateBufferLineToString( - currentRow, false, startCol, currentCol - ); - currentCol = 0; - startCol = 0; - currentRow++; - } else if (!forward && currentCol < 0) { - bufferStr += this._terminal.buffer.translateBufferLineToString( - currentRow, false, 0, startCol + 1 - ); - currentCol = this._terminal.cols - 1; - startCol = currentCol; - currentRow--; - } + if (forward && currentCol > bufferService.cols - 1) { + bufferStr += bufferService.buffer.translateBufferLineToString( + currentRow, false, startCol, currentCol + ); + currentCol = 0; + startCol = 0; + currentRow++; + } else if (!forward && currentCol < 0) { + bufferStr += bufferService.buffer.translateBufferLineToString( + currentRow, false, 0, startCol + 1 + ); + currentCol = bufferService.cols - 1; + startCol = currentCol; + currentRow--; } - - return bufferStr + this._terminal.buffer.translateBufferLineToString( - currentRow, false, startCol, currentCol - ); } - /** - * Constructs the escape sequence for clicking an arrow - * @param direction The direction to move - */ - private _sequence(direction: Direction): string { - const mod = this._terminal.applicationCursor ? 'O' : '['; - return C0.ESC + mod + direction; - } + return bufferStr + bufferService.buffer.translateBufferLineToString( + currentRow, false, startCol, currentCol + ); +} + +/** + * Constructs the escape sequence for clicking an arrow + * @param direction The direction to move + */ +function sequence(direction: Direction, applicationCursor: boolean): string { + const mod = applicationCursor ? 'O' : '['; + return C0.ESC + mod + direction; } /** From da1484ac47d7e40e2ba42f654346903112dea2b8 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 22 Jun 2019 09:44:25 -0700 Subject: [PATCH 06/12] Return a sequence from AltClickHandler --- src/SelectionManager.ts | 15 ++++++++++++++- src/handlers/AltClickHandler.ts | 27 +++++---------------------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 0a5daed2..483ea7cb 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -650,7 +650,20 @@ export class SelectionManager implements ISelectionManager { this._removeMouseDownListeners(); if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME) { - (new AltClickHandler(event, this._terminal, this._mouseService)).move(this._bufferService, this._terminal.applicationCursor); + if (event.altKey) { + const coordinates = this._mouseService.getCoords( + event, + this._terminal.element, + this._bufferService.cols, + this._bufferService.rows, + false + ); + if (coordinates && coordinates[0] !== undefined && coordinates[1] !== undefined) { + this._terminal.handler( + (new AltClickHandler(this._terminal)).move(coordinates[0] - 1, coordinates[1] - 1, this._bufferService, this._terminal.applicationCursor) + ); + } + } } else if (this.hasSelection) { this._onSelectionChange.fire(); } diff --git a/src/handlers/AltClickHandler.ts b/src/handlers/AltClickHandler.ts index 7b46d745..918ab60d 100644 --- a/src/handlers/AltClickHandler.ts +++ b/src/handlers/AltClickHandler.ts @@ -6,7 +6,6 @@ import { ITerminal } from '../Types'; import { IBufferLine, ICircularList } from 'common/Types'; import { C0 } from 'common/data/EscapeSequences'; -import { IMouseService } from 'browser/services/Services'; import { IBufferService } from 'common/services/Services'; const enum Direction { @@ -24,36 +23,20 @@ export class AltClickHandler { private _lines: ICircularList; constructor( - private _mouseEvent: MouseEvent, - private _terminal: ITerminal, - private readonly _mouseService: IMouseService + private _terminal: ITerminal ) { this._lines = this._terminal.buffer.lines; this._startCol = this._terminal.buffer.x; this._startRow = this._terminal.buffer.y; - - const coordinates = this._mouseService.getCoords( - this._mouseEvent, - this._terminal.element, - this._terminal.cols, - this._terminal.rows, - false - ); - - if (coordinates) { - [this._endCol, this._endRow] = coordinates.map((coordinate: number) => { - return coordinate - 1; - }); - } } /** * Writes the escape sequences of arrows to the terminal */ - public move(bufferService: IBufferService, applicationCursor: boolean): void { - if (this._mouseEvent.altKey && this._endCol !== undefined && this._endRow !== undefined) { - this._terminal.handler(this._arrowSequences(bufferService, applicationCursor)); - } + public move(targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string { + this._endCol = targetX; + this._endRow = targetY; + return this._arrowSequences(bufferService, applicationCursor); } /** From 7454701c0b34459ed8f920bf76be775c5b686e06 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 22 Jun 2019 17:53:11 -0700 Subject: [PATCH 07/12] Remove AltHandler member usage --- src/SelectionManager.ts | 2 +- src/handlers/AltClickHandler.ts | 99 ++++++++++++++------------------- 2 files changed, 44 insertions(+), 57 deletions(-) diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 483ea7cb..344978cf 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -660,7 +660,7 @@ export class SelectionManager implements ISelectionManager { ); if (coordinates && coordinates[0] !== undefined && coordinates[1] !== undefined) { this._terminal.handler( - (new AltClickHandler(this._terminal)).move(coordinates[0] - 1, coordinates[1] - 1, this._bufferService, this._terminal.applicationCursor) + (new AltClickHandler()).move(coordinates[0] - 1, coordinates[1] - 1, this._bufferService, this._terminal.applicationCursor) ); } } diff --git a/src/handlers/AltClickHandler.ts b/src/handlers/AltClickHandler.ts index 918ab60d..8f8288a6 100644 --- a/src/handlers/AltClickHandler.ts +++ b/src/handlers/AltClickHandler.ts @@ -3,8 +3,6 @@ * @license MIT */ -import { ITerminal } from '../Types'; -import { IBufferLine, ICircularList } from 'common/Types'; import { C0 } from 'common/data/EscapeSequences'; import { IBufferService } from 'common/services/Services'; @@ -16,27 +14,16 @@ const enum Direction { } export class AltClickHandler { - private _startRow: number; - private _startCol: number; - private _endRow: number; - private _endCol: number; - private _lines: ICircularList; constructor( - private _terminal: ITerminal ) { - this._lines = this._terminal.buffer.lines; - this._startCol = this._terminal.buffer.x; - this._startRow = this._terminal.buffer.y; } /** * Writes the escape sequences of arrows to the terminal */ public move(targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string { - this._endCol = targetX; - this._endRow = targetY; - return this._arrowSequences(bufferService, applicationCursor); + return this._arrowSequences(targetX, targetY, bufferService, applicationCursor); } /** @@ -44,16 +31,19 @@ export class AltClickHandler { * Resets the starting row to an unwrapped row, moves to the requested row, * then moves to requested col. */ - private _arrowSequences(bufferService: IBufferService, applicationCursor: boolean): string { + private _arrowSequences(targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string { + const startX = bufferService.buffer.x; + const startY = bufferService.buffer.y; + // The alt buffer should try to navigate between rows if (!bufferService.buffer.hasScrollback) { - return this._resetStartingRow(bufferService, applicationCursor) + - this._moveToRequestedRow(bufferService, applicationCursor) + - this._moveToRequestedCol(bufferService, applicationCursor); + return this._resetStartingRow(startX, startY, targetX, targetY, bufferService, applicationCursor) + + this._moveToRequestedRow(startY, targetY, bufferService, applicationCursor) + + this._moveToRequestedCol(startX, startY, targetX, targetY, bufferService, applicationCursor); } // Only move horizontally for the normal buffer - return this._moveHorizontallyOnly(bufferService, applicationCursor); + return this._moveHorizontallyOnly(startX, startY, targetX, targetY, bufferService, applicationCursor); } /** @@ -61,13 +51,13 @@ export class AltClickHandler { * cursor up to the first row that is not wrapped to have accurate vertical * positioning. */ - private _resetStartingRow(bufferService: IBufferService, applicationCursor: boolean): string { - if (this._moveToRequestedRow(bufferService, applicationCursor).length === 0) { + private _resetStartingRow(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string { + if (this._moveToRequestedRow(startY, targetY, bufferService, applicationCursor).length === 0) { return ''; } return repeat(bufferLine( - this._startCol, this._startRow, this._startCol, - this._startRow - this._wrappedRowsForRow(bufferService, this._startRow), false, bufferService + startX, startY, startX, + startY - this._wrappedRowsForRow(bufferService, startY), false, bufferService ).length, sequence(Direction.LEFT, applicationCursor)); } @@ -75,38 +65,38 @@ export class AltClickHandler { * Using the reset starting and ending row, move to the requested row, * ignoring wrapped rows */ - private _moveToRequestedRow(bufferService: IBufferService, applicationCursor: boolean): string { - const startRow = this._startRow - this._wrappedRowsForRow(bufferService, this._startRow); - const endRow = this._endRow - this._wrappedRowsForRow(bufferService, this._endRow); + private _moveToRequestedRow(startY: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string { + const startRow = startY - this._wrappedRowsForRow(bufferService, startY); + const endRow = targetY - this._wrappedRowsForRow(bufferService, targetY); - const rowsToMove = Math.abs(startRow - endRow) - this._wrappedRowsCount(bufferService); + const rowsToMove = Math.abs(startRow - endRow) - this._wrappedRowsCount(startY, targetY, bufferService); - return repeat(rowsToMove, sequence(this._verticalDirection(), applicationCursor)); + return repeat(rowsToMove, sequence(this._verticalDirection(startY, targetY), applicationCursor)); } /** * Move to the requested col on the ending row */ - private _moveToRequestedCol(bufferService: IBufferService, applicationCursor: boolean): string { + private _moveToRequestedCol(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string { let startRow; - if (this._moveToRequestedRow(bufferService, applicationCursor).length > 0) { - startRow = this._endRow - this._wrappedRowsForRow(bufferService, this._endRow); + if (this._moveToRequestedRow(startY, targetY, bufferService, applicationCursor).length > 0) { + startRow = targetY - this._wrappedRowsForRow(bufferService, targetY); } else { - startRow = this._startRow; + startRow = startY; } - const endRow = this._endRow; - const direction = this._horizontalDirection(bufferService, applicationCursor); + const endRow = targetY; + const direction = this._horizontalDirection(startX, startY, targetX, targetY, bufferService, applicationCursor); return repeat(bufferLine( - this._startCol, startRow, this._endCol, endRow, + startX, startRow, targetX, endRow, direction === Direction.RIGHT, bufferService ).length, sequence(direction, applicationCursor)); } - private _moveHorizontallyOnly(bufferService: IBufferService, applicationCursor: boolean): string { - const direction = this._horizontalDirection(bufferService, applicationCursor); - return repeat(Math.abs(this._startCol - this._endCol), sequence(direction, applicationCursor)); + private _moveHorizontallyOnly(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string { + const direction = this._horizontalDirection(startX, startY, targetX, targetY, bufferService, applicationCursor); + return repeat(Math.abs(startX - targetX), sequence(direction, applicationCursor)); } /** @@ -117,15 +107,15 @@ export class AltClickHandler { * Calculates the number of wrapped rows between the unwrapped starting and * ending rows. These rows need to ignored since the cursor skips over them. */ - private _wrappedRowsCount(bufferService: IBufferService): number { + private _wrappedRowsCount(startY: number, targetY: number, bufferService: IBufferService): number { let wrappedRows = 0; - const startRow = this._startRow - this._wrappedRowsForRow(bufferService, this._startRow); - const endRow = this._endRow - this._wrappedRowsForRow(bufferService, this._endRow); + const startRow = startY - this._wrappedRowsForRow(bufferService, startY); + const endRow = targetY - this._wrappedRowsForRow(bufferService, targetY); for (let i = 0; i < Math.abs(startRow - endRow); i++) { - const direction = this._verticalDirection() === Direction.UP ? -1 : 1; + const direction = this._verticalDirection(startY, targetY) === Direction.UP ? -1 : 1; - if (this._lines.get(startRow + (direction * i)).isWrapped) { + if (bufferService.buffer.lines.get(startRow + (direction * i)).isWrapped) { wrappedRows++; } } @@ -157,18 +147,18 @@ export class AltClickHandler { /** * Determines if the right or left arrow is needed */ - private _horizontalDirection(bufferService: IBufferService, applicationCursor: boolean): Direction { + private _horizontalDirection(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): Direction { let startRow; - if (this._moveToRequestedRow(bufferService, applicationCursor).length > 0) { - startRow = this._endRow - this._wrappedRowsForRow(bufferService, this._endRow); + if (this._moveToRequestedRow(targetX, targetY, bufferService, applicationCursor).length > 0) { + startRow = targetY - this._wrappedRowsForRow(bufferService, targetY); } else { - startRow = this._startRow; + startRow = startY; } - if ((this._startCol < this._endCol && - startRow <= this._endRow) || // down/right or same y/right - (this._startCol >= this._endCol && - startRow < this._endRow)) { // down/left or same y/left + if ((startX < targetX && + startRow <= targetY) || // down/right or same y/right + (startX >= targetX && + startRow < targetY)) { // down/left or same y/left return Direction.RIGHT; } return Direction.LEFT; @@ -177,11 +167,8 @@ export class AltClickHandler { /** * Determines if the up or down arrow is needed */ - private _verticalDirection(): Direction { - if (this._startRow > this._endRow) { - return Direction.UP; - } - return Direction.DOWN; + private _verticalDirection(startY: number, targetY: number): Direction { + return startY > targetY ? Direction.UP : Direction.DOWN; } } From 13a63efdab01e404e45b5302d045dd4fb1eda168 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 22 Jun 2019 17:55:59 -0700 Subject: [PATCH 08/12] Change AltClickHandler to be functional --- src/SelectionManager.ts | 7 +- src/handlers/AltClickHandler.ts | 266 +++++++++++++++----------------- 2 files changed, 129 insertions(+), 144 deletions(-) diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 344978cf..48a2fec8 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -8,13 +8,13 @@ import { IBuffer } from 'common/buffer/Types'; import { IBufferLine } from 'common/Types'; import * as Browser from 'common/Platform'; import { SelectionModel } from 'browser/selection/SelectionModel'; -import { AltClickHandler } from './handlers/AltClickHandler'; import { CellData } from 'common/buffer/CellData'; import { IDisposable } from 'xterm'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { ICharSizeService, IMouseService } from 'browser/services/Services'; import { IBufferService, IOptionsService } from 'common/services/Services'; import { getCoordsRelativeToElement } from 'browser/input/Mouse'; +import { moveToCellSequence } from 'handlers/AltClickHandler'; /** * The number of pixels the mouse needs to be above or below the viewport in @@ -659,9 +659,8 @@ export class SelectionManager implements ISelectionManager { false ); if (coordinates && coordinates[0] !== undefined && coordinates[1] !== undefined) { - this._terminal.handler( - (new AltClickHandler()).move(coordinates[0] - 1, coordinates[1] - 1, this._bufferService, this._terminal.applicationCursor) - ); + const sequence = moveToCellSequence(coordinates[0] - 1, coordinates[1] - 1, this._bufferService, this._terminal.applicationCursor); + this._terminal.handler(sequence); } } } else if (this.hasSelection) { diff --git a/src/handlers/AltClickHandler.ts b/src/handlers/AltClickHandler.ts index 8f8288a6..73e9b729 100644 --- a/src/handlers/AltClickHandler.ts +++ b/src/handlers/AltClickHandler.ts @@ -13,163 +13,149 @@ const enum Direction { LEFT = 'D' } -export class AltClickHandler { +/** + * Concatenates all the arrow sequences together. + * Resets the starting row to an unwrapped row, moves to the requested row, + * then moves to requested col. + */ +export function moveToCellSequence(targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string { + const startX = bufferService.buffer.x; + const startY = bufferService.buffer.y; - constructor( - ) { + // The alt buffer should try to navigate between rows + if (!bufferService.buffer.hasScrollback) { + return resetStartingRow(startX, startY, targetX, targetY, bufferService, applicationCursor) + + moveToRequestedRow(startY, targetY, bufferService, applicationCursor) + + moveToRequestedCol(startX, startY, targetX, targetY, bufferService, applicationCursor); } - /** - * Writes the escape sequences of arrows to the terminal - */ - public move(targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string { - return this._arrowSequences(targetX, targetY, bufferService, applicationCursor); + // Only move horizontally for the normal buffer + return moveHorizontallyOnly(startX, startY, targetX, targetY, bufferService, applicationCursor); +} + +/** + * If the initial position of the cursor is on a row that is wrapped, move the + * cursor up to the first row that is not wrapped to have accurate vertical + * positioning. + */ +function resetStartingRow(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string { + if (moveToRequestedRow(startY, targetY, bufferService, applicationCursor).length === 0) { + return ''; + } + return repeat(bufferLine( + startX, startY, startX, + startY - wrappedRowsForRow(bufferService, startY), false, bufferService + ).length, sequence(Direction.LEFT, applicationCursor)); +} + +/** + * Using the reset starting and ending row, move to the requested row, + * ignoring wrapped rows + */ +function moveToRequestedRow(startY: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string { + const startRow = startY - wrappedRowsForRow(bufferService, startY); + const endRow = targetY - wrappedRowsForRow(bufferService, targetY); + + const rowsToMove = Math.abs(startRow - endRow) - wrappedRowsCount(startY, targetY, bufferService); + + return repeat(rowsToMove, sequence(verticalDirection(startY, targetY), applicationCursor)); +} + +/** + * Move to the requested col on the ending row + */ +function moveToRequestedCol(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string { + let startRow; + if (moveToRequestedRow(startY, targetY, bufferService, applicationCursor).length > 0) { + startRow = targetY - wrappedRowsForRow(bufferService, targetY); + } else { + startRow = startY; } - /** - * Concatenates all the arrow sequences together. - * Resets the starting row to an unwrapped row, moves to the requested row, - * then moves to requested col. - */ - private _arrowSequences(targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string { - const startX = bufferService.buffer.x; - const startY = bufferService.buffer.y; + const endRow = targetY; + const direction = horizontalDirection(startX, startY, targetX, targetY, bufferService, applicationCursor); - // The alt buffer should try to navigate between rows - if (!bufferService.buffer.hasScrollback) { - return this._resetStartingRow(startX, startY, targetX, targetY, bufferService, applicationCursor) + - this._moveToRequestedRow(startY, targetY, bufferService, applicationCursor) + - this._moveToRequestedCol(startX, startY, targetX, targetY, bufferService, applicationCursor); + return repeat(bufferLine( + startX, startRow, targetX, endRow, + direction === Direction.RIGHT, bufferService + ).length, sequence(direction, applicationCursor)); +} + +function moveHorizontallyOnly(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string { + const direction = horizontalDirection(startX, startY, targetX, targetY, bufferService, applicationCursor); + return repeat(Math.abs(startX - targetX), sequence(direction, applicationCursor)); +} + +/** + * Utility functions + */ + +/** + * Calculates the number of wrapped rows between the unwrapped starting and + * ending rows. These rows need to ignored since the cursor skips over them. + */ +function wrappedRowsCount(startY: number, targetY: number, bufferService: IBufferService): number { + let wrappedRows = 0; + const startRow = startY - wrappedRowsForRow(bufferService, startY); + const endRow = targetY - wrappedRowsForRow(bufferService, targetY); + + for (let i = 0; i < Math.abs(startRow - endRow); i++) { + const direction = verticalDirection(startY, targetY) === Direction.UP ? -1 : 1; + + if (bufferService.buffer.lines.get(startRow + (direction * i)).isWrapped) { + wrappedRows++; } - - // Only move horizontally for the normal buffer - return this._moveHorizontallyOnly(startX, startY, targetX, targetY, bufferService, applicationCursor); } - /** - * If the initial position of the cursor is on a row that is wrapped, move the - * cursor up to the first row that is not wrapped to have accurate vertical - * positioning. - */ - private _resetStartingRow(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string { - if (this._moveToRequestedRow(startY, targetY, bufferService, applicationCursor).length === 0) { - return ''; - } - return repeat(bufferLine( - startX, startY, startX, - startY - this._wrappedRowsForRow(bufferService, startY), false, bufferService - ).length, sequence(Direction.LEFT, applicationCursor)); + return wrappedRows; +} + +/** + * Calculates the number of wrapped rows that make up a given row. + * @param currentRow The row to determine how many wrapped rows make it up + */ +function wrappedRowsForRow(bufferService: IBufferService, currentRow: number): number { + let rowCount = 0; + let lineWraps = bufferService.buffer.lines.get(currentRow).isWrapped; + + while (lineWraps && currentRow >= 0 && currentRow < bufferService.rows) { + rowCount++; + currentRow--; + lineWraps = bufferService.buffer.lines.get(currentRow).isWrapped; } - /** - * Using the reset starting and ending row, move to the requested row, - * ignoring wrapped rows - */ - private _moveToRequestedRow(startY: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string { - const startRow = startY - this._wrappedRowsForRow(bufferService, startY); - const endRow = targetY - this._wrappedRowsForRow(bufferService, targetY); + return rowCount; +} - const rowsToMove = Math.abs(startRow - endRow) - this._wrappedRowsCount(startY, targetY, bufferService); +/** + * Direction determiners + */ - return repeat(rowsToMove, sequence(this._verticalDirection(startY, targetY), applicationCursor)); +/** + * Determines if the right or left arrow is needed + */ +function horizontalDirection(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): Direction { + let startRow; + if (moveToRequestedRow(targetX, targetY, bufferService, applicationCursor).length > 0) { + startRow = targetY - wrappedRowsForRow(bufferService, targetY); + } else { + startRow = startY; } - /** - * Move to the requested col on the ending row - */ - private _moveToRequestedCol(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string { - let startRow; - if (this._moveToRequestedRow(startY, targetY, bufferService, applicationCursor).length > 0) { - startRow = targetY - this._wrappedRowsForRow(bufferService, targetY); - } else { - startRow = startY; - } - - const endRow = targetY; - const direction = this._horizontalDirection(startX, startY, targetX, targetY, bufferService, applicationCursor); - - return repeat(bufferLine( - startX, startRow, targetX, endRow, - direction === Direction.RIGHT, bufferService - ).length, sequence(direction, applicationCursor)); + if ((startX < targetX && + startRow <= targetY) || // down/right or same y/right + (startX >= targetX && + startRow < targetY)) { // down/left or same y/left + return Direction.RIGHT; } + return Direction.LEFT; +} - private _moveHorizontallyOnly(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string { - const direction = this._horizontalDirection(startX, startY, targetX, targetY, bufferService, applicationCursor); - return repeat(Math.abs(startX - targetX), sequence(direction, applicationCursor)); - } - - /** - * Utility functions - */ - - /** - * Calculates the number of wrapped rows between the unwrapped starting and - * ending rows. These rows need to ignored since the cursor skips over them. - */ - private _wrappedRowsCount(startY: number, targetY: number, bufferService: IBufferService): number { - let wrappedRows = 0; - const startRow = startY - this._wrappedRowsForRow(bufferService, startY); - const endRow = targetY - this._wrappedRowsForRow(bufferService, targetY); - - for (let i = 0; i < Math.abs(startRow - endRow); i++) { - const direction = this._verticalDirection(startY, targetY) === Direction.UP ? -1 : 1; - - if (bufferService.buffer.lines.get(startRow + (direction * i)).isWrapped) { - wrappedRows++; - } - } - - return wrappedRows; - } - - /** - * Calculates the number of wrapped rows that make up a given row. - * @param currentRow The row to determine how many wrapped rows make it up - */ - private _wrappedRowsForRow(bufferService: IBufferService, currentRow: number): number { - let rowCount = 0; - let lineWraps = bufferService.buffer.lines.get(currentRow).isWrapped; - - while (lineWraps && currentRow >= 0 && currentRow < bufferService.rows) { - rowCount++; - currentRow--; - lineWraps = bufferService.buffer.lines.get(currentRow).isWrapped; - } - - return rowCount; - } - - /** - * Direction determiners - */ - - /** - * Determines if the right or left arrow is needed - */ - private _horizontalDirection(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): Direction { - let startRow; - if (this._moveToRequestedRow(targetX, targetY, bufferService, applicationCursor).length > 0) { - startRow = targetY - this._wrappedRowsForRow(bufferService, targetY); - } else { - startRow = startY; - } - - if ((startX < targetX && - startRow <= targetY) || // down/right or same y/right - (startX >= targetX && - startRow < targetY)) { // down/left or same y/left - return Direction.RIGHT; - } - return Direction.LEFT; - } - - /** - * Determines if the up or down arrow is needed - */ - private _verticalDirection(startY: number, targetY: number): Direction { - return startY > targetY ? Direction.UP : Direction.DOWN; - } +/** + * Determines if the up or down arrow is needed + */ +function verticalDirection(startY: number, targetY: number): Direction { + return startY > targetY ? Direction.UP : Direction.DOWN; } /** From 22a7e48f04ab014d79fc8d8e1e5aabdca0a1bfbc Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 22 Jun 2019 18:00:03 -0700 Subject: [PATCH 09/12] Move alt click into browser --- src/SelectionManager.ts | 2 +- .../input/MoveToCell.ts} | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) rename src/{handlers/AltClickHandler.ts => browser/input/MoveToCell.ts} (96%) diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 48a2fec8..56d32b56 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -14,7 +14,7 @@ import { EventEmitter, IEvent } from 'common/EventEmitter'; import { ICharSizeService, IMouseService } from 'browser/services/Services'; import { IBufferService, IOptionsService } from 'common/services/Services'; import { getCoordsRelativeToElement } from 'browser/input/Mouse'; -import { moveToCellSequence } from 'handlers/AltClickHandler'; +import { moveToCellSequence } from 'browser/input/MoveToCell'; /** * The number of pixels the mouse needs to be above or below the viewport in diff --git a/src/handlers/AltClickHandler.ts b/src/browser/input/MoveToCell.ts similarity index 96% rename from src/handlers/AltClickHandler.ts rename to src/browser/input/MoveToCell.ts index 73e9b729..406ec807 100644 --- a/src/handlers/AltClickHandler.ts +++ b/src/browser/input/MoveToCell.ts @@ -101,8 +101,8 @@ function wrappedRowsCount(startY: number, targetY: number, bufferService: IBuffe for (let i = 0; i < Math.abs(startRow - endRow); i++) { const direction = verticalDirection(startY, targetY) === Direction.UP ? -1 : 1; - - if (bufferService.buffer.lines.get(startRow + (direction * i)).isWrapped) { + const line = bufferService.buffer.lines.get(startRow + (direction * i)); + if (line && line.isWrapped) { wrappedRows++; } } @@ -116,12 +116,13 @@ function wrappedRowsCount(startY: number, targetY: number, bufferService: IBuffe */ function wrappedRowsForRow(bufferService: IBufferService, currentRow: number): number { let rowCount = 0; - let lineWraps = bufferService.buffer.lines.get(currentRow).isWrapped; + let line = bufferService.buffer.lines.get(currentRow); + let lineWraps = line && line.isWrapped; while (lineWraps && currentRow >= 0 && currentRow < bufferService.rows) { rowCount++; - currentRow--; - lineWraps = bufferService.buffer.lines.get(currentRow).isWrapped; + line = bufferService.buffer.lines.get(--currentRow); + lineWraps = line && line.isWrapped; } return rowCount; From f903dbde4508c8c79f751b10667ee7b51fe8362d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 22 Jun 2019 18:16:33 -0700 Subject: [PATCH 10/12] Add some tests for MoveToCell --- src/browser/input/MoveToCell.test.ts | 49 ++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/browser/input/MoveToCell.test.ts diff --git a/src/browser/input/MoveToCell.test.ts b/src/browser/input/MoveToCell.test.ts new file mode 100644 index 00000000..bc4012c0 --- /dev/null +++ b/src/browser/input/MoveToCell.test.ts @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; +import { IBufferService } from 'common/services/Services'; +import { MockBufferService } from 'common/TestUtils.test'; +import { moveToCellSequence } from './MoveToCell'; + +describe('MoveToCell', () => { + let bufferService: IBufferService; + + beforeEach(() => { + bufferService = new MockBufferService(5, 5); + bufferService.buffer.x = 3; + bufferService.buffer.y = 3; + }); + + describe('normal buffer', () => { + it('should use the right directional escape sequences', () => { + assert.equal(moveToCellSequence(2, 3, bufferService, false), '\x1b[D'); + assert.equal(moveToCellSequence(4, 3, bufferService, false), '\x1b[C'); + }); + it('should ignore the Y value', () => { + assert.equal(moveToCellSequence(1, 1, bufferService, false), '\x1b[D\x1b[D'); + assert.equal(moveToCellSequence(1, 2, bufferService, false), '\x1b[D\x1b[D'); + assert.equal(moveToCellSequence(1, 3, bufferService, false), '\x1b[D\x1b[D'); + assert.equal(moveToCellSequence(1, 4, bufferService, false), '\x1b[D\x1b[D'); + assert.equal(moveToCellSequence(1, 5, bufferService, false), '\x1b[D\x1b[D'); + }); + it('should use the correct character for application cursor', () => { + assert.equal(moveToCellSequence(2, 1, bufferService, false), '\x1b[D'); + assert.equal(moveToCellSequence(2, 1, bufferService, true), '\x1bOD'); + }); + }); + + describe('alt buffer', () => { + beforeEach(() => { + bufferService.buffers.activateAltBuffer(); + bufferService.buffer.x = 3; + bufferService.buffer.y = 3; + }); + + it('should move the cursor across rows', () => { + assert.equal(moveToCellSequence(4, 4, bufferService, false), '\x1b[B\x1b[C'); + }); + }); +}); From 87897a7ee540215a9730496c7c59b4bd2720e0c5 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 22 Jun 2019 18:27:25 -0700 Subject: [PATCH 11/12] Move selection manager types into browser --- src/Clipboard.ts | 2 +- src/SelectionManager.ts | 3 ++- src/TestUtils.test.ts | 3 ++- src/Types.d.ts | 19 +------------------ src/browser/selection/Types.d.ts | 22 ++++++++++++++++++++++ 5 files changed, 28 insertions(+), 21 deletions(-) create mode 100644 src/browser/selection/Types.d.ts diff --git a/src/Clipboard.ts b/src/Clipboard.ts index 75b0da8e..1ee232ee 100644 --- a/src/Clipboard.ts +++ b/src/Clipboard.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ISelectionManager } from './Types'; +import { ISelectionManager } from 'browser/selection/Types'; /** * Prepares text to be pasted into the terminal by normalizing the line endings diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 56d32b56..4133fccc 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -3,7 +3,8 @@ * @license MIT */ -import { ITerminal, ISelectionManager, ISelectionRedrawRequestEvent } from './Types'; +import { ITerminal } from './Types'; +import { ISelectionManager, ISelectionRedrawRequestEvent } from 'browser/selection/Types'; import { IBuffer } from 'common/buffer/Types'; import { IBufferLine } from 'common/Types'; import * as Browser from 'common/Platform'; diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index 07f91705..8394f5a7 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -4,7 +4,7 @@ */ import { IRenderer, IRenderDimensions, CharacterJoinerHandler } from 'browser/renderer/Types'; -import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBrowser, ISelectionManager, ITerminalOptions, ILinkifier, ILinkMatcherOptions } from './Types'; +import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBrowser, ITerminalOptions, ILinkifier, ILinkMatcherOptions } from './Types'; import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types'; import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener } from 'common/Types'; import { Buffer } from 'common/buffer/Buffer'; @@ -15,6 +15,7 @@ import { AttributeData } from 'common/buffer/AttributeData'; import { IColorManager, IColorSet } from 'browser/Types'; import { IOptionsService } from 'common/services/Services'; import { EventEmitter } from 'common/EventEmitter'; +import { ISelectionManager } from 'browser/selection/Types'; export class TestTerminal extends Terminal { writeSync(data: string): void { diff --git a/src/Types.d.ts b/src/Types.d.ts index 5987d605..c3957ab7 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -9,6 +9,7 @@ import { IEvent, IEventEmitter } from 'common/EventEmitter'; import { IColorSet } from 'browser/Types'; import { IOptionsService } from 'common/services/Services'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; +import { ISelectionManager } from 'browser/selection/Types'; export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; @@ -296,24 +297,6 @@ export interface ITerminalOptions extends IPublicTerminalOptions { useFlowControl?: boolean; } -export interface ISelectionManager { - selectionText: string; - selectionStart: [number, number]; - selectionEnd: [number, number]; - - disable(): void; - enable(): void; - setSelection(row: number, col: number, length: number): void; - isClickInSelection(event: MouseEvent): boolean; - selectWordAtCursor(event: MouseEvent): void; -} - -export interface ISelectionRedrawRequestEvent { - start: [number, number]; - end: [number, number]; - columnSelectMode: boolean; -} - export interface ILinkifier { onLinkHover: IEvent; onLinkLeave: IEvent; diff --git a/src/browser/selection/Types.d.ts b/src/browser/selection/Types.d.ts new file mode 100644 index 00000000..241731f1 --- /dev/null +++ b/src/browser/selection/Types.d.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +export interface ISelectionManager { + selectionText: string; + selectionStart: [number, number]; + selectionEnd: [number, number]; + + disable(): void; + enable(): void; + setSelection(row: number, col: number, length: number): void; + isClickInSelection(event: MouseEvent): boolean; + selectWordAtCursor(event: MouseEvent): void; +} + +export interface ISelectionRedrawRequestEvent { + start: [number, number]; + end: [number, number]; + columnSelectMode: boolean; +} From bfa7a2df766ccec168c214f24a6350eabb27f985 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 23 Jun 2019 10:10:17 -0700 Subject: [PATCH 12/12] Move handler into CoreService and adopt This also fine tunes some of the data events to not clear selection and scroll to the bottom of the viewport anymore. Part of #1507 Fixes #2112 --- src/CompositionHelper.test.ts | 3 +- src/CompositionHelper.ts | 16 +++--- src/InputHandler.test.ts | 19 +++---- src/InputHandler.ts | 22 ++++----- src/SelectionManager.test.ts | 4 +- src/SelectionManager.ts | 11 +++-- src/Terminal.test.ts | 11 ++--- src/Terminal.ts | 79 ++++++++++++++++-------------- src/Types.d.ts | 2 - src/common/TestUtils.test.ts | 8 ++- src/common/services/CoreService.ts | 43 ++++++++++++++++ src/common/services/Services.d.ts | 15 ++++++ 12 files changed, 153 insertions(+), 80 deletions(-) create mode 100644 src/common/services/CoreService.ts diff --git a/src/CompositionHelper.test.ts b/src/CompositionHelper.test.ts index 2d28f55d..6cf615fe 100644 --- a/src/CompositionHelper.test.ts +++ b/src/CompositionHelper.test.ts @@ -7,6 +7,7 @@ import { assert } from 'chai'; import { CompositionHelper } from './CompositionHelper'; import { ITerminal } from './Types'; import { MockCharSizeService } from 'browser/TestUtils.test'; +import { MockCoreService } from '../out/common/TestUtils.test'; describe('CompositionHelper', () => { let terminal: ITerminal; @@ -54,7 +55,7 @@ describe('CompositionHelper', () => { } } as any; handledText = ''; - compositionHelper = new CompositionHelper(textarea, compositionView, terminal, new MockCharSizeService(10, 10)); + compositionHelper = new CompositionHelper(textarea, compositionView, terminal, new MockCharSizeService(10, 10), new MockCoreService()); }); describe('Input', () => { diff --git a/src/CompositionHelper.ts b/src/CompositionHelper.ts index 2b2d3042..010585f8 100644 --- a/src/CompositionHelper.ts +++ b/src/CompositionHelper.ts @@ -5,6 +5,7 @@ import { ITerminal } from './Types'; import { ICharSizeService } from 'browser/services/Services'; +import { ICoreService } from 'common/services/Services'; interface IPosition { start: number; @@ -41,10 +42,11 @@ export class CompositionHelper { * @param _terminal The Terminal to forward the finished composition to. */ constructor( - private _textarea: HTMLTextAreaElement, - private _compositionView: HTMLElement, - private _terminal: ITerminal, - private _charSizeService: ICharSizeService + private readonly _textarea: HTMLTextAreaElement, + private readonly _compositionView: HTMLElement, + private readonly _terminal: ITerminal, + private readonly _charSizeService: ICharSizeService, + private readonly _coreService: ICoreService ) { this._isComposing = false; this._isSendingComposition = false; @@ -127,7 +129,7 @@ export class CompositionHelper { // Cancel any delayed composition send requests and send the input immediately. this._isSendingComposition = false; const input = this._textarea.value.substring(this._compositionPosition.start, this._compositionPosition.end); - this._terminal.handler(input); + this._coreService.triggerDataEvent(input, true); } else { // Make a deep copy of the composition position here as a new compositionstart event may // fire before the setTimeout executes. @@ -159,7 +161,7 @@ export class CompositionHelper { // (eg. 2) after a composition character. input = this._textarea.value.substring(currentCompositionPosition.start); } - this._terminal.handler(input); + this._coreService.triggerDataEvent(input, true); } }, 0); } @@ -179,7 +181,7 @@ export class CompositionHelper { const newValue = this._textarea.value; const diff = newValue.replace(oldValue, ''); if (diff.length > 0) { - this._terminal.handler(diff); + this._coreService.triggerDataEvent(diff, true); } } }, 0); diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index 19d5f6a2..e93fc945 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -12,6 +12,7 @@ import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { CellData } from 'common/buffer/CellData'; import { Attributes } from 'common/buffer/Constants'; import { AttributeData } from 'common/buffer/AttributeData'; +import { MockCoreService } from 'common/TestUtils.test'; describe('InputHandler', () => { describe('save and restore cursor', () => { @@ -20,7 +21,7 @@ describe('InputHandler', () => { terminal.buffer.y = 2; terminal.buffer.ybase = 0; terminal.curAttrData.fg = 3; - const inputHandler = new InputHandler(terminal); + const inputHandler = new InputHandler(terminal, new MockCoreService()); // Save cursor position inputHandler.saveCursor([]); assert.equal(terminal.buffer.x, 1); @@ -39,7 +40,7 @@ describe('InputHandler', () => { describe('setCursorStyle', () => { it('should call Terminal.setOption with correct params', () => { const terminal = new MockInputHandlingTerminal(); - const inputHandler = new InputHandler(terminal); + const inputHandler = new InputHandler(terminal, new MockCoreService()); const collect = ' '; inputHandler.setCursorStyle([0], collect); @@ -82,7 +83,7 @@ describe('InputHandler', () => { const terminal = new MockInputHandlingTerminal(); const collect = '?'; terminal.bracketedPasteMode = false; - const inputHandler = new InputHandler(terminal); + const inputHandler = new InputHandler(terminal, new MockCoreService()); // Set bracketed paste mode inputHandler.setMode([2004], collect); assert.equal(terminal.bracketedPasteMode, true); @@ -100,7 +101,7 @@ describe('InputHandler', () => { it('insertChars', function(): void { const term = new Terminal(); - const inputHandler = new InputHandler(term); + const inputHandler = new InputHandler(term, new MockCoreService()); // insert some data in first and second line inputHandler.parse(Array(term.cols - 9).join('a')); @@ -137,7 +138,7 @@ describe('InputHandler', () => { }); it('deleteChars', function(): void { const term = new Terminal(); - const inputHandler = new InputHandler(term); + const inputHandler = new InputHandler(term, new MockCoreService()); // insert some data in first and second line inputHandler.parse(Array(term.cols - 9).join('a')); @@ -177,7 +178,7 @@ describe('InputHandler', () => { }); it('eraseInLine', function(): void { const term = new Terminal(); - const inputHandler = new InputHandler(term); + const inputHandler = new InputHandler(term, new MockCoreService()); // fill 6 lines to test 3 different states inputHandler.parse(Array(term.cols + 1).join('a')); @@ -205,7 +206,7 @@ describe('InputHandler', () => { }); it('eraseInDisplay', function(): void { const term = new Terminal({cols: 80, rows: 7}); - const inputHandler = new InputHandler(term); + const inputHandler = new InputHandler(term, new MockCoreService()); // fill display with a's for (let i = 0; i < term.rows; ++i) inputHandler.parse(Array(term.cols + 1).join('a')); @@ -340,7 +341,7 @@ describe('InputHandler', () => { describe('print', () => { it('should not cause an infinite loop (regression test)', () => { const term = new Terminal(); - const inputHandler = new InputHandler(term); + const inputHandler = new InputHandler(term, new MockCoreService()); const container = new Uint32Array(10); container[0] = 0x200B; inputHandler.print(container, 0, 1); @@ -353,7 +354,7 @@ describe('InputHandler', () => { beforeEach(() => { term = new Terminal(); - handler = new InputHandler(term); + handler = new InputHandler(term, new MockCoreService()); }); 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 a2ae4e08..194d2e84 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -19,6 +19,7 @@ import { IParsingState, IDcsHandler, IEscapeSequenceParser } from 'common/parser import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; import { AttributeData } from 'common/buffer/AttributeData'; +import { ICoreService } from 'common/services/Services'; /** * Map collect to glevel. Used in `selectCharset`. @@ -113,8 +114,6 @@ export class InputHandler extends Disposable implements IInputHandler { private _onCursorMove = new EventEmitter(); public get onCursorMove(): IEvent { return this._onCursorMove.event; } - private _onData = new EventEmitter(); - public get onData(): IEvent { return this._onData.event; } private _onLineFeed = new EventEmitter(); public get onLineFeed(): IEvent { return this._onLineFeed.event; } private _onScroll = new EventEmitter(); @@ -122,6 +121,7 @@ export class InputHandler extends Disposable implements IInputHandler { constructor( protected _terminal: IInputHandlingTerminal, + private _coreService: ICoreService, private _parser: IEscapeSequenceParser = new EscapeSequenceParser()) { super(); @@ -1098,24 +1098,24 @@ export class InputHandler extends Disposable implements IInputHandler { if (!collect) { if (this._terminal.is('xterm') || this._terminal.is('rxvt-unicode') || this._terminal.is('screen')) { - this._terminal.handler(C0.ESC + '[?1;2c'); + this._coreService.triggerDataEvent(C0.ESC + '[?1;2c'); } else if (this._terminal.is('linux')) { - this._terminal.handler(C0.ESC + '[?6c'); + this._coreService.triggerDataEvent(C0.ESC + '[?6c'); } } else if (collect === '>') { // xterm and urxvt // seem to spit this // out around ~370 times (?). if (this._terminal.is('xterm')) { - this._terminal.handler(C0.ESC + '[>0;276;0c'); + this._coreService.triggerDataEvent(C0.ESC + '[>0;276;0c'); } else if (this._terminal.is('rxvt-unicode')) { - this._terminal.handler(C0.ESC + '[>85;95;0c'); + this._coreService.triggerDataEvent(C0.ESC + '[>85;95;0c'); } else if (this._terminal.is('linux')) { // not supported by linux console. // linux console echoes parameters. - this._terminal.handler(params[0] + 'c'); + this._coreService.triggerDataEvent(params[0] + 'c'); } else if (this._terminal.is('screen')) { - this._terminal.handler(C0.ESC + '[>83;40003;0c'); + this._coreService.triggerDataEvent(C0.ESC + '[>83;40003;0c'); } } } @@ -1799,13 +1799,13 @@ export class InputHandler extends Disposable implements IInputHandler { switch (params[0]) { case 5: // status report - this._onData.fire(`${C0.ESC}[0n`); + this._coreService.triggerDataEvent(`${C0.ESC}[0n`); break; case 6: // cursor position const y = this._terminal.buffer.y + 1; const x = this._terminal.buffer.x + 1; - this._onData.fire(`${C0.ESC}[${y};${x}R`); + this._coreService.triggerDataEvent(`${C0.ESC}[${y};${x}R`); break; } } else if (collect === '?') { @@ -1816,7 +1816,7 @@ export class InputHandler extends Disposable implements IInputHandler { // cursor position const y = this._terminal.buffer.y + 1; const x = this._terminal.buffer.x + 1; - this._onData.fire(`${C0.ESC}[?${y};${x}R`); + this._coreService.triggerDataEvent(`${C0.ESC}[?${y};${x}R`); break; case 15: // no printer diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index 814cb5fe..6b11698f 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -10,7 +10,7 @@ import { ITerminal } from './Types'; import { IBuffer } from 'common/buffer/Types'; import { IBufferLine } from 'common/Types'; import { MockTerminal } from './TestUtils.test'; -import { MockBufferService, MockOptionsService } from 'common/TestUtils.test'; +import { MockBufferService, MockOptionsService, MockCoreService } from 'common/TestUtils.test'; import { BufferLine } from 'common/buffer/BufferLine'; import { IBufferService, IOptionsService } from 'common/services/Services'; import { MockCharSizeService, MockMouseService } from 'browser/TestUtils.test'; @@ -26,7 +26,7 @@ class TestSelectionManager extends SelectionManager { bufferService: IBufferService, optionsService: IOptionsService ) { - super(terminal, null, new MockCharSizeService(10, 10), bufferService, new MockMouseService(), optionsService); + super(terminal, null, new MockCharSizeService(10, 10), bufferService, new MockCoreService(), new MockMouseService(), optionsService); } public get model(): SelectionModel { return this._model; } diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 4133fccc..70023836 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -13,7 +13,7 @@ import { CellData } from 'common/buffer/CellData'; import { IDisposable } from 'xterm'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { ICharSizeService, IMouseService } from 'browser/services/Services'; -import { IBufferService, IOptionsService } from 'common/services/Services'; +import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services'; import { getCoordsRelativeToElement } from 'browser/input/Mouse'; import { moveToCellSequence } from 'browser/input/MoveToCell'; @@ -117,6 +117,7 @@ export class SelectionManager implements ISelectionManager { private readonly _screenElement: HTMLElement, private readonly _charSizeService: ICharSizeService, private readonly _bufferService: IBufferService, + private readonly _coreService: ICoreService, private readonly _mouseService: IMouseService, private readonly _optionsService: IOptionsService ) { @@ -137,7 +138,11 @@ export class SelectionManager implements ISelectionManager { 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(); } @@ -661,7 +666,7 @@ export class SelectionManager implements ISelectionManager { ); if (coordinates && coordinates[0] !== undefined && coordinates[1] !== undefined) { const sequence = moveToCellSequence(coordinates[0] - 1, coordinates[1] - 1, this._bufferService, this._terminal.applicationCursor); - this._terminal.handler(sequence); + this._coreService.triggerDataEvent(sequence, true); } } } else if (this.hasSelection) { diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index f3f7fc58..f2f02eec 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -53,10 +53,11 @@ describe('Terminal', () => { }); describe('events', () => { - it('should fire the onData evnet', (done) => { - term.onData(() => done()); - term.handler('fake'); - }); + // TODO: Add an onData test back + // it('should fire the onData evnet', (done) => { + // term.onData(() => done()); + // term.handler('fake'); + // }); it('should fire the onCursorMove event', (done) => { term.onCursorMove(() => done()); term.write('foo'); @@ -142,7 +143,6 @@ describe('Terminal', () => { }; beforeEach(() => { - term.handler = () => { }; term.showCursor = () => { }; term.clearSelection = () => { }; }); @@ -520,7 +520,6 @@ describe('Terminal', () => { let evKeyPress: any; beforeEach(() => { - term.handler = () => { }; term.showCursor = () => { }; term.clearSelection = () => { }; // term.compositionHelper = { diff --git a/src/Terminal.ts b/src/Terminal.ts index 72cecb19..a9f11b4b 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -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 } from 'common/services/Services'; +import { IOptionsService, IBufferService, ICoreService } from 'common/services/Services'; import { OptionsService } from 'common/services/OptionsService'; import { ICharSizeService, IRenderService, IMouseService } from 'browser/services/Services'; import { CharSizeService } from 'browser/services/CharSizeService'; @@ -56,6 +56,7 @@ import { Disposable } from 'common/Lifecycle'; import { IBufferSet, IBuffer } from 'common/buffer/Types'; import { Attributes } from 'common/buffer/Constants'; import { MouseService } from 'browser/services/MouseService'; +import { CoreService } from 'common/services/CoreService'; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -107,6 +108,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // common services private _bufferService: IBufferService; + private _coreService: ICoreService; public optionsService: IOptionsService; // browser services @@ -237,6 +239,9 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // Setup and initialize common services this.optionsService = new OptionsService(options); this._bufferService = new BufferService(this.optionsService); + this._coreService = new CoreService(() => this.scrollToBottom(), this._bufferService, this.optionsService); + this._coreService.onData(e => this._onData.fire(e)); + this._setupOptionsListeners(); this._setup(); } @@ -249,7 +254,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } this._customKeyEventHandler = null; removeTerminalFromCache(this); - this.handler = () => {}; this.write = () => {}; if (this.element && this.element.parentNode) { this.element.parentNode.removeChild(this.element); @@ -294,10 +298,9 @@ 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._inputHandler = new InputHandler(this, this._coreService); this._inputHandler.onCursorMove(() => this._onCursorMove.fire()); this._inputHandler.onLineFeed(() => this._onLineFeed.fire()); - this._inputHandler.onData(e => this._onData.fire(e)); this.register(this._inputHandler); this.selectionManager = this.selectionManager || null; @@ -434,7 +437,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp */ private _onTextAreaFocus(ev: KeyboardEvent): void { if (this.sendFocus) { - this.handler(C0.ESC + '[I'); + this._coreService.triggerDataEvent(C0.ESC + '[I'); } this.updateCursorStyle(ev); this.element.classList.add('focus'); @@ -459,7 +462,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.textarea.value = ''; this.refresh(this.buffer.y, this.buffer.y); if (this.sendFocus) { - this.handler(C0.ESC + '[O'); + this._coreService.triggerDataEvent(C0.ESC + '[O'); } this.element.classList.remove('focus'); this._onBlur.fire(); @@ -480,7 +483,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } copyHandler(event, this.selectionManager); })); - const pasteHandlerWrapper = (event: ClipboardEvent) => pasteHandler(event, this.textarea, this.bracketedPasteMode, e => this.handler(e)); + const pasteHandlerWrapper = (event: ClipboardEvent) => pasteHandler(event, this.textarea, this.bracketedPasteMode, e => this._coreService.triggerDataEvent(e, true)); this.register(addDisposableDomListener(this.textarea, 'paste', pasteHandlerWrapper)); this.register(addDisposableDomListener(this.element, 'paste', pasteHandlerWrapper)); @@ -607,7 +610,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._compositionView = document.createElement('div'); this._compositionView.classList.add('composition-view'); - this._compositionHelper = new CompositionHelper(this.textarea, this._compositionView, this, this._charSizeService); + this._compositionHelper = new CompositionHelper(this.textarea, this._compositionView, this, this._charSizeService, this._coreService); this._helperContainer.appendChild(this._compositionView); // Performance: Add viewport and helper elements from the fragment @@ -640,7 +643,7 @@ 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.selectionManager = new SelectionManager(this, this.screenElement, this._charSizeService, this._bufferService, this._mouseService, this.optionsService); + this.selectionManager = new SelectionManager(this, this.screenElement, this._charSizeService, this._bufferService, this._coreService, this._mouseService, this.optionsService); this.register(this.selectionManager.onSelectionChange(() => this._onSelectionChange.fire())); this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e))); this.register(this.selectionManager.onRedrawRequest(e => this._renderService.onSelectionChanged(e.start, e.end, e.columnSelectMode))); @@ -814,7 +817,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp else if (button === 3) return; else data += '0'; data += '~[' + pos.x + ',' + pos.y + ']\r'; - self.handler(data); + self._coreService.triggerDataEvent(data, true); return; } @@ -827,7 +830,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp else if (button === 1) button = 4; else if (button === 2) button = 6; else if (button === 3) button = 3; - self.handler(C0.ESC + '[' + self._coreService.triggerDataEvent(C0.ESC + '[' + button + ';' + (button === 3 ? 4 : 0) @@ -838,7 +841,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp + ';' // Not sure what page is meant to be + (pos).page || 0 - + '&w'); + + '&w', true); return; } @@ -847,20 +850,20 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp pos.y -= 32; pos.x++; pos.y++; - self.handler(C0.ESC + '[' + button + ';' + pos.x + ';' + pos.y + 'M'); + self._coreService.triggerDataEvent(C0.ESC + '[' + button + ';' + pos.x + ';' + pos.y + 'M', true); return; } if (self.sgrMouse) { pos.x -= 32; pos.y -= 32; - self.handler(C0.ESC + '[<' + self._coreService.triggerDataEvent(C0.ESC + '[<' + (((button & 3) === 3 ? button & ~3 : button) - 32) + ';' + pos.x + ';' + pos.y - + ((button & 3) === 3 ? 'm' : 'M')); + + ((button & 3) === 3 ? 'm' : 'M'), true); return; } @@ -870,7 +873,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp encode(data, pos.x); encode(data, pos.y); - self.handler(C0.ESC + '[M' + String.fromCharCode.apply(String, data)); + self._coreService.triggerDataEvent(C0.ESC + '[M' + String.fromCharCode.apply(String, data), true); } function getButton(ev: MouseEvent): number { @@ -1015,7 +1018,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp for (let i = 0; i < Math.abs(amount); i++) { data += sequence; } - this.handler(data); + this._coreService.triggerDataEvent(data, true); } return; } @@ -1240,7 +1243,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp if (this.options.useFlowControl && !this._xoffSentToCatchUp && this.writeBufferUtf8.length >= WRITE_BUFFER_PAUSE_THRESHOLD) { // XOFF - stop pty pipe // XON will be triggered by emulator before processing data chunk - this.handler(C0.DC3); + this._coreService.triggerDataEvent(C0.DC3); this._xoffSentToCatchUp = true; } @@ -1268,7 +1271,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // If XOFF was sent in order to catch up with the pty process, resume it if // we reached the end of the writeBuffer to allow more data to come in. if (this._xoffSentToCatchUp && this.writeBufferUtf8.length === bufferOffset) { - this.handler(C0.DC1); + this._coreService.triggerDataEvent(C0.DC1); this._xoffSentToCatchUp = false; } @@ -1327,7 +1330,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp if (this.options.useFlowControl && !this._xoffSentToCatchUp && this.writeBuffer.length >= WRITE_BUFFER_PAUSE_THRESHOLD) { // XOFF - stop pty pipe // XON will be triggered by emulator before processing data chunk - this.handler(C0.DC3); + this._coreService.triggerDataEvent(C0.DC3); this._xoffSentToCatchUp = true; } @@ -1355,7 +1358,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // If XOFF was sent in order to catch up with the pty process, resume it if // we reached the end of the writeBuffer to allow more data to come in. if (this._xoffSentToCatchUp && this.writeBuffer.length === bufferOffset) { - this.handler(C0.DC1); + this._coreService.triggerDataEvent(C0.DC1); this._xoffSentToCatchUp = false; } @@ -1587,7 +1590,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._onKey.fire({ key: result.key, domEvent: event }); this.showCursor(); - this.handler(result.key); + this._coreService.triggerDataEvent(result.key, true); return this.cancel(event, true); } @@ -1665,7 +1668,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._onKey.fire({ key, domEvent: ev }); this.showCursor(); - this.handler(key); + this._coreService.triggerDataEvent(key, true); return true; } @@ -1796,23 +1799,23 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp * Emit the data event and populate the given data. * @param data The data to populate in the event. */ - public handler(data: string): void { - // Prevents all events to pty process if stdin is disabled - if (this.options.disableStdin) { - return; - } + // public handler(data: string): void { + // // Prevents all events to pty process if stdin is disabled + // if (this.options.disableStdin) { + // return; + // } - // Clear the selection if the selection manager is available and has an active selection - if (this.selectionManager && this.selectionManager.hasSelection) { - this.selectionManager.clearSelection(); - } + // // Clear the selection if the selection manager is available and has an active selection + // if (this.selectionManager && this.selectionManager.hasSelection) { + // this.selectionManager.clearSelection(); + // } - // Input is being sent to the terminal, the terminal should focus the prompt. - if (this.buffer.ybase !== this.buffer.ydisp) { - this.scrollToBottom(); - } - this._onData.fire(data); - } + // // Input is being sent to the terminal, the terminal should focus the prompt. + // if (this.buffer.ybase !== this.buffer.ydisp) { + // this.scrollToBottom(); + // } + // this._onData.fire(data); + // } /** * Emit the 'title' event and populate the given title. diff --git a/src/Types.d.ts b/src/Types.d.ts index c3957ab7..20e6c082 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -73,7 +73,6 @@ export interface IInputHandlingTerminal { refresh(start: number, end: number): void; error(text: string, data?: any): void; tabSet(): void; - handler(data: string): void; handleTitle(title: string): void; index(): void; reverseIndex(): void; @@ -217,7 +216,6 @@ export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAcc onA11yChar: IEvent; onA11yTab: IEvent; - handler(data: string): void; scrollLines(disp: number, suppressScrollEvent?: boolean): void; cancel(ev: Event, force?: boolean): boolean | void; log(text: string): void; diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index c6402fb1..86098d33 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IBufferService, IOptionsService, ITerminalOptions, IPartialTerminalOptions } from 'common/services/Services'; +import { IBufferService, ICoreService, IOptionsService, ITerminalOptions, IPartialTerminalOptions } from 'common/services/Services'; import { IEvent, EventEmitter } from 'common/EventEmitter'; import { clone } from 'common/Clone'; import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; @@ -27,6 +27,12 @@ export class MockBufferService implements IBufferService { reset(): void {} } +export class MockCoreService implements ICoreService { + onData: IEvent = new EventEmitter().event; + onUserInput: IEvent = new EventEmitter().event; + triggerDataEvent(data: string, wasUserInput?: boolean): void {} +} + export class MockOptionsService implements IOptionsService { options: ITerminalOptions = clone(DEFAULT_OPTIONS); onOptionChange: IEvent = new EventEmitter().event; diff --git a/src/common/services/CoreService.ts b/src/common/services/CoreService.ts new file mode 100644 index 00000000..bd731e8d --- /dev/null +++ b/src/common/services/CoreService.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ICoreService, IOptionsService, IBufferService } from 'common/services/Services'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; + +export class CoreService implements ICoreService { + private _onData = new EventEmitter(); + public get onData(): IEvent { return this._onData.event; } + private _onUserInput = new EventEmitter(); + public get onUserInput(): IEvent { return this._onUserInput.event; } + + constructor( + // TODO: Move this into a service + private readonly _scrollToBottom: () => void, + private readonly _bufferService: IBufferService, + private readonly _optionsService: IOptionsService + ) { + } + + public triggerDataEvent(data: string, wasUserInput: boolean = false): void { + // Prevents all events to pty process if stdin is disabled + if (this._optionsService.options.disableStdin) { + return; + } + + // Input is being sent to the terminal, the terminal should focus the prompt. + const buffer = this._bufferService.buffer; + if (buffer.ybase !== buffer.ydisp) { + this._scrollToBottom(); + } + + // Fire onUserInput so listeners can react as well (eg. clear selection) + if (wasUserInput) { + this._onUserInput.fire(); + } + + // Fire onData API + this._onData.fire(data); + } +} diff --git a/src/common/services/Services.d.ts b/src/common/services/Services.d.ts index b8276f51..d9903e70 100644 --- a/src/common/services/Services.d.ts +++ b/src/common/services/Services.d.ts @@ -18,6 +18,21 @@ export interface IBufferService { reset(): void; } +export interface ICoreService { + readonly onData: IEvent; + readonly onUserInput: IEvent; + + /** + * Triggers the onData event in the public API. + * @param data The data that is being emitted. + * @param wasFromUser Whether the data originated from the user (as opposed to + * resulting from parsing incoming data). When true this will also: + * - Scroll to the bottom of the buffer.s + * - Fire the `onUserInput` event (so selection can be cleared). + */ + triggerDataEvent(data: string, wasUserInput?: boolean): void; +} + export interface IOptionsService { readonly options: ITerminalOptions;