diff --git a/src/Clipboard.ts b/src/Clipboard.ts index 9461afa1..1ee232ee 100644 --- a/src/Clipboard.ts +++ b/src/Clipboard.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ITerminal, ISelectionManager } from './Types'; +import { ISelectionManager } from 'browser/selection/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/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 0b82466d..6b11698f 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, MockCoreService } 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 MockCoreService(), 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 fb2caba8..70023836 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -3,18 +3,19 @@ * @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'; 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 } from 'common/services/Services'; +import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services'; import { getCoordsRelativeToElement } from 'browser/input/Mouse'; +import { moveToCellSequence } from 'browser/input/MoveToCell'; /** * The number of pixels the mouse needs to be above or below the viewport in @@ -113,14 +114,17 @@ export class SelectionManager implements ISelectionManager { constructor( private readonly _terminal: ITerminal, + private readonly _screenElement: HTMLElement, private readonly _charSizeService: ICharSizeService, - readonly bufferService: IBufferService, - private readonly _mouseService: IMouseService + private readonly _bufferService: IBufferService, + private readonly _coreService: ICoreService, + private readonly _mouseService: IMouseService, + private readonly _optionsService: IOptionsService ) { this._initListeners(); this.enable(); - this._model = new SelectionModel(bufferService); + this._model = new SelectionModel(this._bufferService); this._activeSelectionMode = SelectionMode.NORMAL; } @@ -128,23 +132,23 @@ export class SelectionManager implements ISelectionManager { this._removeMouseDownListeners(); } - private get _buffer(): IBuffer { - return this._terminal.buffers.active; - } - /** * Initializes listener variables. */ private _initListeners(): void { this._mouseMoveListener = event => this._onMouseMove(event); this._mouseUpListener = event => this._onMouseUp(event); - + this._coreService.onUserInput(() => { + if (this.hasSelection) { + this.clearSelection(); + } + }); this.initBuffersListeners(); } public 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 +192,7 @@ export class SelectionManager implements ISelectionManager { return ''; } + const buffer = this._bufferService.buffer; const result: string[] = []; if (this._activeSelectionMode === SelectionMode.COLUMN) { @@ -197,18 +202,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 +223,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 +334,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(); } @@ -352,7 +357,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._screenElement, this._bufferService.cols, this._bufferService.rows, true); if (!coords) { return null; } @@ -362,7 +367,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; } @@ -372,8 +377,8 @@ export class SelectionManager implements ISelectionManager { * @param event The mouse event. */ 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); + 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; } @@ -393,7 +398,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; @@ -453,8 +458,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); } @@ -462,9 +467,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; @@ -499,7 +504,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; } @@ -546,7 +551,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); } /** @@ -576,7 +581,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 +595,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 +604,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 +630,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(); } @@ -649,7 +656,19 @@ 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(); + 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) { + const sequence = moveToCellSequence(coordinates[0] - 1, coordinates[1] - 1, this._bufferService, this._terminal.applicationCursor); + this._coreService.triggerDataEvent(sequence, true); + } + } } else if (this.hasSelection) { this._onSelectionChange.fire(); } @@ -704,16 +723,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 +828,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 +843,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 +857,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 +881,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 +900,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++; } } @@ -908,7 +928,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; } /** @@ -916,9 +936,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; } } 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 b28cfcfa..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(); @@ -478,9 +481,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._coreService.triggerDataEvent(e, true)); this.register(addDisposableDomListener(this.textarea, 'paste', pasteHandlerWrapper)); this.register(addDisposableDomListener(this.element, 'paste', pasteHandlerWrapper)); @@ -489,12 +492,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 +509,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); } })); } @@ -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._charSizeService, this._bufferService, this._mouseService); + 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/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..20e6c082 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; @@ -72,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; @@ -216,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; @@ -296,24 +295,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/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'); + }); + }); +}); diff --git a/src/browser/input/MoveToCell.ts b/src/browser/input/MoveToCell.ts new file mode 100644 index 00000000..406ec807 --- /dev/null +++ b/src/browser/input/MoveToCell.ts @@ -0,0 +1,230 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { C0 } from 'common/data/EscapeSequences'; +import { IBufferService } from 'common/services/Services'; + +const enum Direction { + UP = 'A', + DOWN = 'B', + RIGHT = 'C', + LEFT = 'D' +} + +/** + * 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; + + // 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); + } + + // 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; + } + + const endRow = targetY; + const direction = horizontalDirection(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; + const line = bufferService.buffer.lines.get(startRow + (direction * i)); + if (line && line.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 + */ +function wrappedRowsForRow(bufferService: IBufferService, currentRow: number): number { + let rowCount = 0; + let line = bufferService.buffer.lines.get(currentRow); + let lineWraps = line && line.isWrapped; + + while (lineWraps && currentRow >= 0 && currentRow < bufferService.rows) { + rowCount++; + line = bufferService.buffer.lines.get(--currentRow); + lineWraps = line && line.isWrapped; + } + + return rowCount; +} + +/** + * Direction determiners + */ + +/** + * 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; + } + + 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 + */ +function verticalDirection(startY: number, targetY: number): Direction { + return startY > targetY ? Direction.UP : 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 + */ +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; + + 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 + 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; +} + +/** + * Returns a string repeated a given number of times + * Polyfill from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat + * @param count The number of times to repeat the string + * @param string The string that is to be repeated + */ +function repeat(count: number, str: string): string { + count = Math.floor(count); + let rpt = ''; + for (let i = 0; i < count; i++) { + rpt += str; + } + return rpt; +} 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; +} 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; diff --git a/src/handlers/AltClickHandler.ts b/src/handlers/AltClickHandler.ts deleted file mode 100644 index 334b78d8..00000000 --- a/src/handlers/AltClickHandler.ts +++ /dev/null @@ -1,269 +0,0 @@ -/** - * Copyright (c) 2018 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { ITerminal } from '../Types'; -import { IBufferLine, ICircularList } from 'common/Types'; -import { C0 } from 'common/data/EscapeSequences'; -import { IMouseService } from 'browser/services/Services'; - -const enum Direction { - UP = 'A', - DOWN = 'B', - RIGHT = 'C', - LEFT = 'D' -} - -export class AltClickHandler { - private _startRow: number; - private _startCol: number; - private _endRow: number; - private _endCol: number; - private _lines: ICircularList; - - constructor( - private _mouseEvent: MouseEvent, - private _terminal: ITerminal, - private readonly _mouseService: IMouseService - ) { - 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(): void { - if (this._mouseEvent.altKey && this._endCol !== undefined && this._endRow !== undefined) { - this._terminal.handler(this._arrowSequences()); - } - } - - /** - * 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(): string { - // The alt buffer should try to navigate between rows - if (!this._terminal.buffer.hasScrollback) { - return this._resetStartingRow() + this._moveToRequestedRow() + this._moveToRequestedCol(); - } - - // Only move horizontally for the normal buffer - return this._moveHorizontallyOnly(); - } - - /** - * 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(): string { - if (this._moveToRequestedRow().length === 0) { - return ''; - } - return repeat(this._bufferLine( - this._startCol, this._startRow, this._startCol, - this._startRow - this._wrappedRowsForRow(this._startRow), false - ).length, this._sequence(Direction.LEFT)); - } - - /** - * 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); - - const rowsToMove = Math.abs(startRow - endRow) - this._wrappedRowsCount(); - - return repeat(rowsToMove, this._sequence(this._verticalDirection())); - } - - /** - * Move to the requested col on the ending row - */ - private _moveToRequestedCol(): string { - let startRow; - if (this._moveToRequestedRow().length > 0) { - startRow = this._endRow - this._wrappedRowsForRow(this._endRow); - } else { - startRow = this._startRow; - } - - const endRow = this._endRow; - const direction = this._horizontalDirection(); - - return repeat(this._bufferLine( - this._startCol, startRow, this._endCol, endRow, - direction === Direction.RIGHT - ).length, this._sequence(direction)); - } - - private _moveHorizontallyOnly(): string { - const direction = this._horizontalDirection(); - return repeat(Math.abs(this._startCol - this._endCol), this._sequence(direction)); - } - - /** - * 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(): number { - let wrappedRows = 0; - const startRow = this._startRow - this._wrappedRowsForRow(this._startRow); - const endRow = this._endRow - this._wrappedRowsForRow(this._endRow); - - for (let i = 0; i < Math.abs(startRow - endRow); i++) { - const direction = this._verticalDirection() === Direction.UP ? -1 : 1; - - if (this._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(currentRow: number): number { - let rowCount = 0; - let lineWraps = this._lines.get(currentRow).isWrapped; - - while (lineWraps && currentRow >= 0 && currentRow < this._terminal.rows) { - rowCount++; - currentRow--; - lineWraps = this._lines.get(currentRow).isWrapped; - } - - return rowCount; - } - - /** - * Direction determiners - */ - - /** - * Determines if the right or left arrow is needed - */ - private _horizontalDirection(): Direction { - let startRow; - if (this._moveToRequestedRow().length > 0) { - startRow = this._endRow - this._wrappedRowsForRow(this._endRow); - } else { - startRow = this._startRow; - } - - 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 - return Direction.RIGHT; - } - return Direction.LEFT; - } - - /** - * Determines if the up or down arrow is needed - */ - private _verticalDirection(): Direction { - if (this._startRow > this._endRow) { - return Direction.UP; - } - 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 = ''; - - 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--; - } - } - - 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; - } -} - -/** - * Returns a string repeated a given number of times - * Polyfill from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat - * @param count The number of times to repeat the string - * @param string The string that is to be repeated - */ -function repeat(count: number, str: string): string { - count = Math.floor(count); - let rpt = ''; - for (let i = 0; i < count; i++) { - rpt += str; - } - return rpt; -}