Merge branch 'master' into webgl2

This commit is contained in:
Daniel Imms
2019-06-23 13:01:42 -07:00
committed by GitHub
18 changed files with 563 additions and 457 deletions
+25 -26
View File
@@ -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();
}
+2 -1
View File
@@ -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', () => {
+9 -7
View File
@@ -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);
+10 -9
View File
@@ -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');
+11 -11
View File
@@ -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<void>();
public get onCursorMove(): IEvent<void> { return this._onCursorMove.event; }
private _onData = new EventEmitter<string>();
public get onData(): IEvent<string> { return this._onData.event; }
private _onLineFeed = new EventEmitter<void>();
public get onLineFeed(): IEvent<void> { return this._onLineFeed.event; }
private _onScroll = new EventEmitter<number>();
@@ -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
+9 -6
View File
@@ -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 {
+78 -58
View File
@@ -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(<MouseEvent>event);
this._mouseUpListener = event => this._onMouseUp(<MouseEvent>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;
}
}
+5 -6
View File
@@ -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 = {
+45 -42
View File
@@ -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
+ (<any>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.
+2 -1
View File
@@ -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 {
+1 -20
View File
@@ -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<string>;
onA11yTab: IEvent<number>;
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<ILinkifierEvent>;
onLinkLeave: IEvent<ILinkifierEvent>;
+49
View File
@@ -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');
});
});
});
+230
View File
@@ -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;
}
+22
View File
@@ -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;
}
+7 -1
View File
@@ -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<string> = new EventEmitter<string>().event;
onUserInput: IEvent<void> = new EventEmitter<void>().event;
triggerDataEvent(data: string, wasUserInput?: boolean): void {}
}
export class MockOptionsService implements IOptionsService {
options: ITerminalOptions = clone(DEFAULT_OPTIONS);
onOptionChange: IEvent<string> = new EventEmitter<string>().event;
+43
View File
@@ -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<string>();
public get onData(): IEvent<string> { return this._onData.event; }
private _onUserInput = new EventEmitter<void>();
public get onUserInput(): IEvent<void> { 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);
}
}
+15
View File
@@ -18,6 +18,21 @@ export interface IBufferService {
reset(): void;
}
export interface ICoreService {
readonly onData: IEvent<string>;
readonly onUserInput: IEvent<void>;
/**
* 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;
-269
View File
@@ -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<IBufferLine>;
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;
}