Merge remote-tracking branch 'origin/master' into 998_mouse_off

This commit is contained in:
Daniel Imms
2018-07-02 13:22:28 -07:00
42 changed files with 644 additions and 241 deletions
+1
View File
@@ -32,6 +32,7 @@
</div>
<hr/>
<p><strong>Attention:</strong> The demo is a barebones implementation and is designed for the development and evaluation of xterm.js only. Exposing the demo to the public as is would introduce security risks for the host.</p>
<button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button>
<script src="dist/bundle.js" defer ></script>
</body>
</html>
+44 -22
View File
@@ -33,23 +33,25 @@ function setPadding() {
term.fit();
}
paddingElement.addEventListener('change', setPadding);
actionElements.findNext.addEventListener('keypress', function (e) {
if (e.key === "Enter") {
e.preventDefault();
term.findNext(actionElements.findNext.value);
}
});
actionElements.findPrevious.addEventListener('keypress', function (e) {
if (e.key === "Enter") {
e.preventDefault();
term.findPrevious(actionElements.findPrevious.value);
}
});
createTerminal();
const disposeRecreateButtonHandler = () => {
// If the terminal exists dispose of it, otherwise recreate it
if (term) {
term.dispose();
term = null;
window.term = null;
socket = null;
document.getElementById('dispose').innerHTML = 'Recreate Terminal';
}
else {
createTerminal();
document.getElementById('dispose').innerHTML = 'Dispose terminal';
}
};
document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
function createTerminal() {
// Clean terminal
while (terminalContainer.children.length) {
@@ -76,6 +78,21 @@ function createTerminal() {
term.fit();
term.focus();
addDomListener(paddingElement, 'change', setPadding);
addDomListener(actionElements.findNext, 'keypress', function (e) {
if (e.key === "Enter") {
e.preventDefault();
term.findNext(actionElements.findNext.value);
}
});
addDomListener(actionElements.findPrevious, 'keypress', function (e) {
if (e.key === "Enter") {
e.preventDefault();
term.findPrevious(actionElements.findPrevious.value);
}
});
// fit is called within a setTimeout, cols and rows need this.
setTimeout(function () {
initOptions(term);
@@ -124,7 +141,7 @@ function runFakeTerminal() {
term.writeln('');
term.prompt();
term.on('key', function (key, ev) {
term._core.register(term.addDisposableListener('key', function (key, ev) {
var printable = (
!ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey
);
@@ -139,11 +156,11 @@ function runFakeTerminal() {
} else if (printable) {
term.write(key);
}
});
}));
term.on('paste', function (data, ev) {
term._core.register(term.addDisposableListener('paste', function (data, ev) {
term.write(data);
});
}));
}
function initOptions(term) {
@@ -213,14 +230,14 @@ function initOptions(term) {
// Attach listeners
booleanOptions.forEach(o => {
var input = document.getElementById(`opt-${o}`);
input.addEventListener('change', () => {
addDomListener(input, 'change', () => {
console.log('change', o, input.checked);
term.setOption(o, input.checked);
});
});
numberOptions.forEach(o => {
var input = document.getElementById(`opt-${o}`);
input.addEventListener('change', () => {
addDomListener(input, 'change', () => {
console.log('change', o, input.value);
if (o === 'cols' || o === 'rows') {
updateTerminalSize();
@@ -231,13 +248,18 @@ function initOptions(term) {
});
Object.keys(stringOptions).forEach(o => {
var input = document.getElementById(`opt-${o}`);
input.addEventListener('change', () => {
addDomListener(input, 'change', () => {
console.log('change', o, input.value);
term.setOption(o, input.value);
});
});
}
function addDomListener(element, type, handler) {
element.addEventListener(type, handler);
term._core.register({ dispose: () => element.removeEventListener(type, handler) });
}
function updateTerminalSize() {
var cols = parseInt(document.getElementById(`opt-cols`).value, 10);
var rows = parseInt(document.getElementById(`opt-rows`).value, 10);
+1 -1
View File
@@ -30,7 +30,7 @@
"jsdoc": "3.4.3",
"jsdom": "^11.11.0",
"merge-stream": "^1.0.1",
"node-pty": "^0.7.2",
"node-pty": "0.7.6",
"nodemon": "1.10.2",
"npm-run-all": "^4.1.2",
"nyc": "^11.8.0",
+18 -20
View File
@@ -6,9 +6,9 @@
import * as Strings from './Strings';
import { ITerminal, IBuffer } from './Types';
import { isMac } from './shared/utils/Browser';
import { RenderDebouncer } from './utils/RenderDebouncer';
import { addDisposableListener } from './utils/Dom';
import { IDisposable } from 'xterm';
import { RenderDebouncer } from './ui/RenderDebouncer';
import { addDisposableDomListener } from './ui/Lifecycle';
import { Disposable } from './common/Lifecycle';
const MAX_ROWS_TO_READ = 20;
@@ -17,7 +17,7 @@ const enum BoundaryPosition {
BOTTOM
}
export class AccessibilityManager implements IDisposable {
export class AccessibilityManager extends Disposable {
private _accessibilityTreeRoot: HTMLElement;
private _rowContainer: HTMLElement;
private _rowElements: HTMLElement[];
@@ -29,8 +29,6 @@ export class AccessibilityManager implements IDisposable {
private _topBoundaryFocusListener: (e: FocusEvent) => void;
private _bottomBoundaryFocusListener: (e: FocusEvent) => void;
private _disposables: IDisposable[] = [];
/**
* This queue has a character pushed to it for keys that are pressed, if the
* next character added to the terminal is equal to the key char then it is
@@ -43,6 +41,7 @@ export class AccessibilityManager implements IDisposable {
private _charsToConsume: string[] = [];
constructor(private _terminal: ITerminal) {
super();
this._accessibilityTreeRoot = document.createElement('div');
this._accessibilityTreeRoot.classList.add('xterm-accessibility');
@@ -72,29 +71,28 @@ export class AccessibilityManager implements IDisposable {
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);
this._disposables.push(this._renderRowsDebouncer);
this._disposables.push(this._terminal.addDisposableListener('resize', data => this._onResize(data.cols, data.rows)));
this._disposables.push(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end)));
this._disposables.push(this._terminal.addDisposableListener('scroll', data => this._refreshRows()));
this.register(this._renderRowsDebouncer);
this.register(this._terminal.addDisposableListener('resize', data => this._onResize(data.cols, data.rows)));
this.register(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end)));
this.register(this._terminal.addDisposableListener('scroll', data => this._refreshRows()));
// Line feed is an issue as the prompt won't be read out after a command is run
this._disposables.push(this._terminal.addDisposableListener('a11y.char', (char) => this._onChar(char)));
this._disposables.push(this._terminal.addDisposableListener('linefeed', () => this._onChar('\n')));
this._disposables.push(this._terminal.addDisposableListener('a11y.tab', spaceCount => this._onTab(spaceCount)));
this._disposables.push(this._terminal.addDisposableListener('key', keyChar => this._onKey(keyChar)));
this._disposables.push(this._terminal.addDisposableListener('blur', () => this._clearLiveRegion()));
this.register(this._terminal.addDisposableListener('a11y.char', (char) => this._onChar(char)));
this.register(this._terminal.addDisposableListener('linefeed', () => this._onChar('\n')));
this.register(this._terminal.addDisposableListener('a11y.tab', spaceCount => this._onTab(spaceCount)));
this.register(this._terminal.addDisposableListener('key', keyChar => this._onKey(keyChar)));
this.register(this._terminal.addDisposableListener('blur', () => this._clearLiveRegion()));
// TODO: Maybe renderer should fire an event on terminal when the characters change and that
// should be listened to instead? That would mean that the order of events are always
// guarenteed
this._disposables.push(this._terminal.addDisposableListener('dprchange', () => this._refreshRowsDimensions()));
this._disposables.push(this._terminal.renderer.addDisposableListener('resize', () => this._refreshRowsDimensions()));
this.register(this._terminal.addDisposableListener('dprchange', () => this._refreshRowsDimensions()));
this.register(this._terminal.renderer.addDisposableListener('resize', () => this._refreshRowsDimensions()));
// This shouldn't be needed on modern browsers but is present in case the
// media query that drives the dprchange event isn't supported
this._disposables.push(addDisposableListener(window, 'resize', () => this._refreshRowsDimensions()));
this.register(addDisposableDomListener(window, 'resize', () => this._refreshRowsDimensions()));
}
public dispose(): void {
this._disposables.forEach(d => d.dispose());
this._disposables.length = 0;
super.dispose();
this._terminal.element.removeChild(this._accessibilityTreeRoot);
this._rowElements.length = 0;
}
+78 -1
View File
@@ -6,7 +6,7 @@
import { assert } from 'chai';
import { ITerminal } from './Types';
import { Buffer } from './Buffer';
import { CircularList } from './utils/CircularList';
import { CircularList } from './common/CircularList';
import { MockTerminal } from './utils/TestUtils.test';
const INIT_COLS = 80;
@@ -269,4 +269,81 @@ describe('Buffer', () => {
assert.equal(buffer.markers.length, 0);
});
});
describe ('translateBufferLineToString', () => {
it('should handle selecting a section of ascii text', () => {
buffer.lines.set(0, [
[ null, 'a', 1, 'a'.charCodeAt(0)],
[ null, 'b', 1, 'b'.charCodeAt(0)],
[ null, 'c', 1, 'c'.charCodeAt(0)],
[ null, 'd', 1, 'd'.charCodeAt(0)]
]);
const str = buffer.translateBufferLineToString(0, true, 0, 2);
assert.equal(str, 'ab');
});
it('should handle a cut-off double width character by including it', () => {
buffer.lines.set(0, [
[ null, '語', 2, 35486 ],
[ null, '', 0, null],
[ null, 'a', 1, 'a'.charCodeAt(0)]
]);
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
assert.equal(str1, '語');
});
it('should handle a zero width character in the middle of the string by not including it', () => {
buffer.lines.set(0, [
[ null, '語', 2, '語'.charCodeAt(0) ],
[ null, '', 0, null],
[ null, 'a', 1, 'a'.charCodeAt(0)]
]);
const str0 = buffer.translateBufferLineToString(0, true, 0, 1);
assert.equal(str0, '語');
const str1 = buffer.translateBufferLineToString(0, true, 0, 2);
assert.equal(str1, '語');
const str2 = buffer.translateBufferLineToString(0, true, 0, 3);
assert.equal(str2, '語a');
});
it('should handle single width emojis', () => {
buffer.lines.set(0, [
[ null, '😁', 1, '😁'.charCodeAt(0) ],
[ null, 'a', 1, 'a'.charCodeAt(0)]
]);
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
assert.equal(str1, '😁');
const str2 = buffer.translateBufferLineToString(0, true, 0, 2);
assert.equal(str2, '😁a');
});
it('should handle double width emojis', () => {
buffer.lines.set(0, [
[ null, '😁', 2, '😁'.charCodeAt(0) ],
[ null, '', 0, null]
]);
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
assert.equal(str1, '😁');
const str2 = buffer.translateBufferLineToString(0, true, 0, 2);
assert.equal(str2, '😁');
buffer.lines.set(0, [
[ null, '😁', 2, '😁'.charCodeAt(0) ],
[ null, '', 0, null],
[ null, 'a', 1, 'a'.charCodeAt(0)]
]);
const str3 = buffer.translateBufferLineToString(0, true, 0, 3);
assert.equal(str3, '😁a');
});
});
});
+7 -8
View File
@@ -3,10 +3,10 @@
* @license MIT
*/
import { CircularList } from './utils/CircularList';
import { CircularList } from './common/CircularList';
import { LineData, CharData, ITerminal, IBuffer } from './Types';
import { EventEmitter } from './EventEmitter';
import { IDisposable, IMarker } from 'xterm';
import { IMarker } from 'xterm';
export const DEFAULT_ATTR = (0 << 18) | (257 << 9) | (256 << 0);
export const CHAR_DATA_ATTR_INDEX = 0;
@@ -227,7 +227,7 @@ export class Buffer implements IBuffer {
if (startCol >= i) {
startIndex--;
}
if (endCol >= i) {
if (endCol > i) {
endIndex--;
}
} else {
@@ -320,14 +320,14 @@ export class Buffer implements IBuffer {
public addMarker(y: number): Marker {
const marker = new Marker(y);
this.markers.push(marker);
marker.disposables.push(this.lines.addDisposableListener('trim', amount => {
marker.register(this.lines.addDisposableListener('trim', amount => {
marker.line -= amount;
// The marker should be disposed when the line is trimmed from the buffer
if (marker.line < 0) {
marker.dispose();
}
}));
marker.on('dispose', () => this._removeMarker(marker));
marker.register(marker.addDisposableListener('dispose', () => this._removeMarker(marker)));
return marker;
}
@@ -342,7 +342,6 @@ export class Marker extends EventEmitter implements IMarker {
private _id: number = Marker._nextId++;
public isDisposed: boolean = false;
public disposables: IDisposable[] = [];
public get id(): number { return this._id; }
@@ -357,8 +356,8 @@ export class Marker extends EventEmitter implements IMarker {
return;
}
this.isDisposed = true;
this.disposables.forEach(d => d.dispose());
this.disposables.length = 0;
// Emit before super.dispose such that dispose listeners get a change to react
this.emit('dispose');
super.dispose();
}
}
+22 -1
View File
@@ -4,6 +4,7 @@
*/
import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from './Types';
import { Disposable } from './common/Lifecycle';
/**
* Returns an array filled with numbers between the low and high parameters (right exclusive).
@@ -207,7 +208,7 @@ class DcsDummy implements IDcsHandler {
* NOTE: The parameter element notation is currently not supported.
* TODO: implement error recovery hook via error handler return values
*/
export class EscapeSequenceParser implements IEscapeSequenceParser {
export class EscapeSequenceParser extends Disposable implements IEscapeSequenceParser {
public initialState: number;
public currentState: number;
@@ -236,6 +237,8 @@ export class EscapeSequenceParser implements IEscapeSequenceParser {
protected _errorHandlerFb: (state: IParsingState) => IParsingState;
constructor(readonly TRANSITIONS: TransitionTable = VT500_TRANSITION_TABLE) {
super();
this.initialState = ParserState.GROUND;
this.currentState = this.initialState;
this._osc = '';
@@ -260,6 +263,24 @@ export class EscapeSequenceParser implements IEscapeSequenceParser {
this._errorHandler = this._errorHandlerFb;
}
public dispose(): void {
this._printHandlerFb = null;
this._executeHandlerFb = null;
this._csiHandlerFb = null;
this._escHandlerFb = null;
this._oscHandlerFb = null;
this._dcsHandlerFb = null;
this._errorHandlerFb = null;
this._printHandler = null;
this._executeHandlers = null;
this._csiHandlers = null;
this._escHandlers = null;
this._oscHandlers = null;
this._dcsHandlers = null;
this._activeDcsHandler = null;
this._errorHandler = null;
}
setPrintHandler(callback: (data: string, start: number, end: number) => void): void {
this._printHandler = callback;
}
+5 -1
View File
@@ -5,11 +5,13 @@
import { XtermListener } from './Types';
import { IEventEmitter, IDisposable } from 'xterm';
import { Disposable } from './common/Lifecycle';
export class EventEmitter implements IEventEmitter, IDisposable {
export class EventEmitter extends Disposable implements IEventEmitter, IDisposable {
private _events: {[type: string]: XtermListener[]};
constructor() {
super();
// Restore the previous events if available, this will happen if the
// constructor is called multiple times on the same object (terminal reset).
this._events = this._events || {};
@@ -26,6 +28,7 @@ export class EventEmitter implements IEventEmitter, IDisposable {
* @param handler The handler for the listener.
*/
public addDisposableListener(type: string, handler: XtermListener): IDisposable {
// TODO: Rename addDisposableEventListener to more easily disambiguate from Dom listener
this.on(type, handler);
return {
dispose: () => {
@@ -76,6 +79,7 @@ export class EventEmitter implements IEventEmitter, IDisposable {
}
public dispose(): void {
super.dispose();
this._events = {};
}
}
+4
View File
@@ -12,18 +12,22 @@ describe('InputHandler', () => {
const terminal = new MockInputHandlingTerminal();
terminal.buffer.x = 1;
terminal.buffer.y = 2;
terminal.curAttr = 3;
const inputHandler = new InputHandler(terminal);
// Save cursor position
inputHandler.saveCursor([]);
assert.equal(terminal.buffer.x, 1);
assert.equal(terminal.buffer.y, 2);
assert.equal(terminal.curAttr, 3);
// Change cursor position
terminal.buffer.x = 10;
terminal.buffer.y = 20;
terminal.curAttr = 30;
// Restore cursor position
inputHandler.restoreCursor([]);
assert.equal(terminal.buffer.x, 1);
assert.equal(terminal.buffer.y, 2);
assert.equal(terminal.curAttr, 3);
});
describe('setCursorStyle', () => {
it('should call Terminal.setOption with correct params', () => {
+13 -1
View File
@@ -12,6 +12,7 @@ import { FLAGS } from './renderer/Types';
import { wcwidth } from './CharWidth';
import { EscapeSequenceParser } from './EscapeSequenceParser';
import { ICharset } from './core/Types';
import { Disposable } from './common/Lifecycle';
/**
* Map collect to glevel. Used in `selectCharset`.
@@ -111,13 +112,17 @@ class DECRQSS implements IDcsHandler {
* Refer to http://invisible-island.net/xterm/ctlseqs/ctlseqs.html to understand
* each function's header comment.
*/
export class InputHandler implements IInputHandler {
export class InputHandler extends Disposable implements IInputHandler {
private _surrogateHigh: string;
constructor(
private _terminal: any, // TODO: reestablish IInputHandlingTerminal here
private _parser: IEscapeSequenceParser = new EscapeSequenceParser())
{
super();
this.register(this._parser);
this._surrogateHigh = '';
/**
@@ -285,6 +290,11 @@ export class InputHandler implements IInputHandler {
this._parser.setDcsHandler('+q', new RequestTerminfo(this._terminal));
}
public dispose(): void {
super.dispose();
this._terminal = null;
}
public parse(data: string): void {
let buffer = this._terminal.buffer;
const cursorStartX = buffer.x;
@@ -1823,6 +1833,7 @@ export class InputHandler implements IInputHandler {
public saveCursor(params: number[]): void {
this._terminal.buffer.savedX = this._terminal.buffer.x;
this._terminal.buffer.savedY = this._terminal.buffer.y;
this._terminal.savedCurAttr = this._terminal.curAttr;
}
@@ -1834,6 +1845,7 @@ export class InputHandler implements IInputHandler {
public restoreCursor(params: number[]): void {
this._terminal.buffer.x = this._terminal.buffer.savedX || 0;
this._terminal.buffer.y = this._terminal.buffer.savedY || 0;
this._terminal.curAttr = this._terminal.savedCurAttr || DEFAULT_ATTR;
}
+4 -2
View File
@@ -4,11 +4,11 @@
*/
import { assert } from 'chai';
import { IMouseZoneManager, IMouseZone } from './input/Types';
import { IMouseZoneManager, IMouseZone } from './ui/Types';
import { ILinkMatcher, LineData, ITerminal } from './Types';
import { Linkifier } from './Linkifier';
import { MockBuffer, MockTerminal } from './utils/TestUtils.test';
import { CircularList } from './utils/CircularList';
import { CircularList } from './common/CircularList';
class TestLinkifier extends Linkifier {
constructor(terminal: ITerminal) {
@@ -21,6 +21,8 @@ class TestLinkifier extends Linkifier {
}
class TestMouseZoneManager implements IMouseZoneManager {
dispose(): void {
}
public clears: number = 0;
public zones: IMouseZone[] = [];
add(zone: IMouseZone): void {
+2 -2
View File
@@ -3,9 +3,9 @@
* @license MIT
*/
import { IMouseZoneManager } from './input/Types';
import { IMouseZoneManager } from './ui/Types';
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal } from './Types';
import { MouseZone } from './input/MouseZoneManager';
import { MouseZone } from './ui/MouseZoneManager';
import { EventEmitter } from './EventEmitter';
/**
+61 -2
View File
@@ -4,8 +4,8 @@
*/
import { assert } from 'chai';
import { CharMeasure } from './utils/CharMeasure';
import { SelectionManager } from './SelectionManager';
import { CharMeasure } from './ui/CharMeasure';
import { SelectionManager, SelectionMode } from './SelectionManager';
import { SelectionModel } from './SelectionModel';
import { BufferSet } from './BufferSet';
import { LineData, CharData, ITerminal, IBuffer } from './Types';
@@ -25,6 +25,8 @@ class TestSelectionManager extends SelectionManager {
public get model(): SelectionModel { return this._model; }
public set selectionMode(mode: SelectionMode) { this._activeSelectionMode = mode; }
public selectLineAt(line: number): void { this._selectLineAt(line); }
public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords, true); }
@@ -378,4 +380,61 @@ describe('SelectionManager', () => {
assert.equal(selectionManager.hasSelection, true);
});
});
describe('column selection', () => {
it('should select a column of text', () => {
buffer.lines.length = 3;
buffer.lines.set(0, stringToRow('abcdefghij'));
buffer.lines.set(1, stringToRow('klmnopqrst'));
buffer.lines.set(2, stringToRow('uvwxyz'));
selectionManager.selectionMode = SelectionMode.COLUMN;
selectionManager.model.selectionStart = [2, 0];
selectionManager.model.selectionEnd = [4, 2];
assert.equal(selectionManager.selectionText, 'cd\nmn\nwx');
});
it('should select a column of text without chopping up double width characters', () => {
buffer.lines.length = 3;
buffer.lines.set(0, stringToRow('a'));
buffer.lines.set(1, stringToRow('語'));
buffer.lines.set(2, stringToRow('b'));
selectionManager.selectionMode = SelectionMode.COLUMN;
selectionManager.model.selectionStart = [0, 0];
selectionManager.model.selectionEnd = [1, 2];
assert.equal(selectionManager.selectionText, 'a\n語\nb');
});
it('should select a column of text with single character emojis', () => {
buffer.lines.length = 3;
buffer.lines.set(0, stringToRow('a'));
buffer.lines.set(1, stringToRow('☃'));
buffer.lines.set(2, stringToRow('c'));
selectionManager.selectionMode = SelectionMode.COLUMN;
selectionManager.model.selectionStart = [0, 0];
selectionManager.model.selectionEnd = [1, 2];
assert.equal(selectionManager.selectionText, 'a\n☃\nc');
});
it('should select a column of text with double character emojis', () => {
// TODO the case this is testing works for me in the demo webapp,
// but doing it programmatically fails.
buffer.lines.length = 3;
buffer.lines.set(0, stringToRow('a '));
buffer.lines.set(1, stringArrayToRow(['😁', ' ']));
buffer.lines.set(2, stringToRow('c '));
selectionManager.selectionMode = SelectionMode.COLUMN;
selectionManager.model.selectionStart = [0, 0];
selectionManager.model.selectionEnd = [1, 2];
assert.equal(selectionManager.selectionText, 'a\n😁\nc');
});
});
});
+61 -26
View File
@@ -6,7 +6,7 @@
import { ITerminal, ISelectionManager, IBuffer, CharData, XtermListener } from './Types';
import { MouseHelper } from './utils/MouseHelper';
import * as Browser from './shared/utils/Browser';
import { CharMeasure } from './utils/CharMeasure';
import { CharMeasure } from './ui/CharMeasure';
import { EventEmitter } from './EventEmitter';
import { SelectionModel } from './SelectionModel';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
@@ -54,10 +54,11 @@ interface IWordPosition {
/**
* A selection mode, this drives how the selection behaves on mouse move.
*/
const enum SelectionMode {
export const enum SelectionMode {
NORMAL,
WORD,
LINE
LINE,
COLUMN
}
/**
@@ -80,7 +81,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
/**
* The current selection mode.
*/
private _activeSelectionMode: SelectionMode;
protected _activeSelectionMode: SelectionMode;
/**
* A setInterval timer that is active while the mouse is down whose callback
@@ -116,6 +117,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
this._activeSelectionMode = SelectionMode.NORMAL;
}
public dispose(): void {
super.dispose();
this._removeMouseDownListeners();
}
private get _buffer(): IBuffer {
return this._terminal.buffers.active;
}
@@ -177,30 +183,43 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
return '';
}
// Get first row
const startRowEndCol = start[1] === end[1] ? end[0] : null;
const result: string[] = [];
result.push(this._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);
if ((<any>bufferLine).isWrapped) {
result[result.length - 1] += lineText;
} else {
if (this._activeSelectionMode === SelectionMode.COLUMN) {
// Ignore zero width selections
if (start[0] === end[0]) {
return '';
}
for (let i = start[1]; i <= end[1]; i++) {
const lineText = this._buffer.translateBufferLineToString(i, true, start[0], end[0]);
result.push(lineText);
}
}
} else {
// Get first row
const startRowEndCol = start[1] === end[1] ? end[0] : null;
result.push(this._buffer.translateBufferLineToString(start[1], true, start[0], startRowEndCol));
// 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]);
if ((<any>bufferLine).isWrapped) {
result[result.length - 1] += lineText;
} else {
result.push(lineText);
// 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);
if ((<any>bufferLine).isWrapped) {
result[result.length - 1] += lineText;
} else {
result.push(lineText);
}
}
// 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]);
if ((<any>bufferLine).isWrapped) {
result[result.length - 1] += lineText;
} else {
result.push(lineText);
}
}
}
@@ -249,7 +268,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
*/
private _refresh(): void {
this._refreshAnimationFrame = null;
this.emit('refresh', { start: this._model.finalSelectionStart, end: this._model.finalSelectionEnd });
this.emit('refresh', {
start: this._model.finalSelectionStart,
end: this._model.finalSelectionEnd,
columnSelectMode: this._activeSelectionMode === SelectionMode.COLUMN
});
}
/**
@@ -358,7 +381,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param event The mouse event.
*/
public shouldForceSelection(event: MouseEvent): boolean {
return Browser.isMac ? event.altKey : event.shiftKey;
if (Browser.isMac) {
return event.altKey && this._terminal.options.macOptionClickForcesSelection;
}
return event.shiftKey;
}
/**
@@ -449,7 +476,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
private _onSingleClick(event: MouseEvent): void {
this._model.selectionStartLength = 0;
this._model.isSelectAllActive = false;
this._activeSelectionMode = SelectionMode.NORMAL;
this._activeSelectionMode = this.shouldColumnSelect(event) ? SelectionMode.COLUMN : SelectionMode.NORMAL;
// Initialize the new selection
this._model.selectionStart = this._getMouseBufferCoords(event);
@@ -502,6 +529,14 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
}
}
/**
* Returns whether the selection manager should operate in column select mode
* @param event the mouse or keyboard event
*/
public shouldColumnSelect(event: KeyboardEvent | MouseEvent): boolean {
return event.altKey && !(Browser.isMac && this._terminal.options.macOptionClickForcesSelection);
}
/**
* Handles the mousemove event when the mouse button is down, recording the
* end of the selection and refreshing the selection.
+1 -1
View File
@@ -88,7 +88,7 @@ if (os.platform() !== 'win32') {
/** some helpers for pty interaction */
// we need a pty in between to get the termios decorations
// for the basic test cases a raw pty device is enough
primitivePty = pty.native.open(cols, rows);
primitivePty = (<any>pty).native.open(cols, rows);
/** tests */
describe('xterm output comparison', () => {
+11
View File
@@ -120,6 +120,17 @@ describe('term.js addons', () => {
});
});
describe('reset', () => {
it('should not affect cursorState', () => {
term.cursorState = 1;
term.reset();
assert.equal(term.cursorState, 1);
term.cursorState = 0;
term.reset();
assert.equal(term.cursorState, 0);
});
});
describe('clear', () => {
it('should clear a buffer equal to rows', () => {
const promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y);
+92 -81
View File
@@ -22,7 +22,7 @@
*/
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, LineData } from './Types';
import { IMouseZoneManager } from './input/Types';
import { IMouseZoneManager } from './ui/Types';
import { IRenderer } from './renderer/Types';
import { BufferSet } from './BufferSet';
import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR } from './Buffer';
@@ -36,17 +36,17 @@ import { InputHandler } from './InputHandler';
import { Renderer } from './renderer/Renderer';
import { Linkifier } from './Linkifier';
import { SelectionManager } from './SelectionManager';
import { CharMeasure } from './utils/CharMeasure';
import { CharMeasure } from './ui/CharMeasure';
import * as Browser from './shared/utils/Browser';
import * as Dom from './utils/Dom';
import { addDisposableDomListener } from './ui/Lifecycle';
import * as Strings from './Strings';
import { MouseHelper } from './utils/MouseHelper';
import { clone } from './utils/Clone';
import { DEFAULT_BELL_SOUND, SoundManager } from './SoundManager';
import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager';
import { MouseZoneManager } from './input/MouseZoneManager';
import { MouseZoneManager } from './ui/MouseZoneManager';
import { AccessibilityManager } from './AccessibilityManager';
import { ScreenDprMonitor } from './utils/ScreenDprMonitor';
import { ScreenDprMonitor } from './ui/ScreenDprMonitor';
import { ITheme, IMarker, IDisposable } from 'xterm';
import { removeTerminalFromCache } from './renderer/atlas/CharAtlasCache';
import { DomRenderer } from './renderer/dom/DomRenderer';
@@ -98,6 +98,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = {
screenReaderMode: false,
debug: false,
macOptionIsMeta: false,
macOptionClickForcesSelection: false,
cancelEvents: false,
disableStdin: false,
useFlowControl: false,
@@ -113,8 +114,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
public element: HTMLElement;
public screenElement: HTMLElement;
private _disposables: IDisposable[];
/**
* The HTMLElement that the terminal is created in, set by Terminal.open.
*/
@@ -173,6 +172,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
public savedCols: number;
public curAttr: number;
public savedCurAttr: number;
public params: (string | number)[];
public currentParam: string | number;
@@ -234,8 +234,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
public dispose(): void {
super.dispose();
this._disposables.forEach(d => d.dispose());
this._disposables.length = 0;
this._customKeyEventHandler = null;
removeTerminalFromCache(this);
this.handler = () => {};
this.write = () => {};
@@ -252,8 +251,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
}
private _setup(): void {
this._disposables = [];
Object.keys(DEFAULT_OPTIONS).forEach((key) => {
if (this.options[key] == null) {
this.options[key] = DEFAULT_OPTIONS[key];
@@ -306,6 +303,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this._userScrolling = false;
this._inputHandler = new InputHandler(this);
this.register(this._inputHandler);
// Reuse renderer if the Terminal is being recreated via a reset call.
this.renderer = this.renderer || null;
this.selectionManager = this.selectionManager || null;
@@ -445,6 +443,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.charMeasure.measure(this.options);
}
break;
case 'drawBoldTextInBrightColors':
case 'experimentalCharAtlas':
case 'enableBold':
case 'letterSpacing':
@@ -525,30 +524,30 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this._bindKeys();
// Bind clipboard functionality
on(this.element, 'copy', (event: ClipboardEvent) => {
this.register(addDisposableDomListener(this.element, 'copy', (event: ClipboardEvent) => {
// If mouse events are active it means the selection manager is disabled and
// copy should be handled by the host program.
if (!this.hasSelection()) {
return;
}
copyHandler(event, this, this.selectionManager);
});
}));
const pasteHandlerWrapper = (event: ClipboardEvent) => pasteHandler(event, this);
on(this.textarea, 'paste', pasteHandlerWrapper);
on(this.element, 'paste', pasteHandlerWrapper);
this.register(addDisposableDomListener(this.textarea, 'paste', pasteHandlerWrapper));
this.register(addDisposableDomListener(this.element, 'paste', pasteHandlerWrapper));
// Handle right click context menus
if (Browser.isFirefox) {
// Firefox doesn't appear to fire the contextmenu event on right click
on(this.element, 'mousedown', (event: MouseEvent) => {
this.register(addDisposableDomListener(this.element, 'mousedown', (event: MouseEvent) => {
if (event.button === 2) {
rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord);
}
});
}));
} else {
on(this.element, 'contextmenu', (event: MouseEvent) => {
this.register(addDisposableDomListener(this.element, 'contextmenu', (event: MouseEvent) => {
rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord);
});
}));
}
// Move the textarea under the cursor when middle clicking on Linux to ensure
@@ -557,11 +556,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
if (Browser.isLinux) {
// Use auxclick event over mousedown the latter doesn't seem to work. Note
// that the regular click event doesn't fire for the middle mouse button.
on(this.element, 'auxclick', (event: MouseEvent) => {
this.register(addDisposableDomListener(this.element, 'auxclick', (event: MouseEvent) => {
if (event.button === 1) {
moveTextAreaUnderMouseCursor(event, this.textarea);
}
});
}));
}
}
@@ -570,33 +569,35 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
*/
private _bindKeys(): void {
const self = this;
on(this.element, 'keydown', function (ev: KeyboardEvent): void {
this.register(addDisposableDomListener(this.element, 'keydown', function (ev: KeyboardEvent): void {
if (document.activeElement !== this) {
return;
}
self._keyDown(ev);
}, true);
}, true));
on(this.element, 'keypress', function (ev: KeyboardEvent): void {
this.register(addDisposableDomListener(this.element, 'keypress', function (ev: KeyboardEvent): void {
if (document.activeElement !== this) {
return;
}
self._keyPress(ev);
}, true);
}, true));
on(this.element, 'keyup', (ev: KeyboardEvent) => {
this.register(addDisposableDomListener(this.element, 'keyup', (ev: KeyboardEvent) => {
if (!wasMondifierKeyOnlyEvent(ev)) {
this.focus();
}
}, true);
on(this.textarea, 'keydown', (ev: KeyboardEvent) => this._keyDown(ev), true);
on(this.textarea, 'keypress', (ev: KeyboardEvent) => this._keyPress(ev), true);
on(this.textarea, 'compositionstart', () => this._compositionHelper.compositionstart());
on(this.textarea, 'compositionupdate', (e: CompositionEvent) => this._compositionHelper.compositionupdate(e));
on(this.textarea, 'compositionend', () => this._compositionHelper.compositionend());
this.on('refresh', () => this._compositionHelper.updateCompositionElements());
this.on('refresh', (data) => this._queueLinkification(data.start, data.end));
self._keyUp(ev);
}, true));
this.register(addDisposableDomListener(this.textarea, 'keydown', (ev: KeyboardEvent) => this._keyDown(ev), true));
this.register(addDisposableDomListener(this.textarea, 'keypress', (ev: KeyboardEvent) => this._keyPress(ev), true));
this.register(addDisposableDomListener(this.textarea, 'compositionstart', () => this._compositionHelper.compositionstart()));
this.register(addDisposableDomListener(this.textarea, 'compositionupdate', (e: CompositionEvent) => this._compositionHelper.compositionupdate(e)));
this.register(addDisposableDomListener(this.textarea, 'compositionend', () => this._compositionHelper.compositionend()));
this.register(this.addDisposableListener('refresh', () => this._compositionHelper.updateCompositionElements()));
this.register(this.addDisposableListener('refresh', (data) => this._queueLinkification(data.start, data.end)));
}
/**
@@ -617,6 +618,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this._screenDprMonitor = new ScreenDprMonitor();
this._screenDprMonitor.setListener(() => this.emit('dprchange', window.devicePixelRatio));
this.register(this._screenDprMonitor);
// Create main element container
this.element = this._document.createElement('div');
@@ -646,7 +648,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
fragment.appendChild(this.screenElement);
this._mouseZoneManager = new MouseZoneManager(this);
this.on('scroll', () => this._mouseZoneManager.clearAll());
this.register(this._mouseZoneManager);
this.register(this.addDisposableListener('scroll', () => this._mouseZoneManager.clearAll()));
this.linkifier.attachToDom(this._mouseZoneManager);
this.textarea = document.createElement('textarea');
@@ -658,8 +661,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.textarea.setAttribute('autocapitalize', 'off');
this.textarea.setAttribute('spellcheck', 'false');
this.textarea.tabIndex = 0;
this.textarea.addEventListener('focus', () => this._onTextAreaFocus());
this.textarea.addEventListener('blur', () => this._onTextAreaBlur());
this.register(addDisposableDomListener(this.textarea, 'focus', () => this._onTextAreaFocus()));
this.register(addDisposableDomListener(this.textarea, 'blur', () => this._onTextAreaBlur()));
this._helperContainer.appendChild(this.textarea);
this._compositionView = document.createElement('div');
@@ -677,37 +680,39 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
case 'dom': this.renderer = new DomRenderer(this, this.options.theme); break;
default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`);
}
this.register(this.renderer);
this.options.theme = null;
this.viewport = new Viewport(this, this._viewportElement, this._viewportScrollArea, this.charMeasure);
this.viewport.onThemeChanged(this.renderer.colorManager.colors);
this.register(this.viewport);
this.on('cursormove', () => this.renderer.onCursorMove());
this.on('resize', () => this.renderer.onResize(this.cols, this.rows));
this.on('blur', () => this.renderer.onBlur());
this.on('focus', () => this.renderer.onFocus());
this.on('dprchange', () => this.renderer.onWindowResize(window.devicePixelRatio));
this.register(this.addDisposableListener('cursormove', () => this.renderer.onCursorMove()));
this.register(this.addDisposableListener('resize', () => this.renderer.onResize(this.cols, this.rows)));
this.register(this.addDisposableListener('blur', () => this.renderer.onBlur()));
this.register(this.addDisposableListener('focus', () => this.renderer.onFocus()));
this.register(this.addDisposableListener('dprchange', () => this.renderer.onWindowResize(window.devicePixelRatio)));
// dprchange should handle this case, we need this as well for browsers that don't support the
// matchMedia query.
this._disposables.push(Dom.addDisposableListener(window, 'resize', () => this.renderer.onWindowResize(window.devicePixelRatio)));
this.charMeasure.on('charsizechanged', () => this.renderer.onCharSizeChanged());
this.renderer.on('resize', (dimensions) => this.viewport.syncScrollArea());
this.register(addDisposableDomListener(window, 'resize', () => this.renderer.onWindowResize(window.devicePixelRatio)));
this.register(this.charMeasure.addDisposableListener('charsizechanged', () => this.renderer.onCharSizeChanged()));
this.register(this.renderer.addDisposableListener('resize', (dimensions) => this.viewport.syncScrollArea()));
this.selectionManager = new SelectionManager(this, this.charMeasure);
this.element.addEventListener('mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e));
this.selectionManager.on('refresh', data => this.renderer.onSelectionChanged(data.start, data.end));
this.selectionManager.on('newselection', text => {
this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e)));
this.register(this.selectionManager.addDisposableListener('refresh', data => this.renderer.onSelectionChanged(data.start, data.end, data.columnSelectMode)));
this.register(this.selectionManager.addDisposableListener('newselection', text => {
// If there's a new selection, put it into the textarea, focus and select it
// in order to register it as a selection on the OS. This event is fired
// only on Linux to enable middle click to paste selection.
this.textarea.value = text;
this.textarea.focus();
this.textarea.select();
});
this.on('scroll', () => {
}));
this.register(this.addDisposableListener('scroll', () => {
this.viewport.syncScrollArea();
this.selectionManager.refresh();
});
this._viewportElement.addEventListener('scroll', () => this.selectionManager.refresh());
}));
this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this.selectionManager.refresh()));
this.mouseHelper = new MouseHelper(this.renderer);
@@ -977,7 +982,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
return button;
}
on(el, 'mousedown', (ev: MouseEvent) => {
this.register(addDisposableDomListener(el, 'mousedown', (ev: MouseEvent) => {
// Prevent the focus on the textarea from getting lost
// and make sure we get focused on mousedown
@@ -1015,7 +1020,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
}
sendMove(event);
};
on(this._document, 'mousemove', moveHandler);
// TODO: these event listeners should be managed by the disposable, the Terminal reference may
// be kept aroud if Terminal.dispose is fired when the mouse is down
this._document.addEventListener('mousemove', moveHandler);
}
// x10 compatibility mode can't send button releases
@@ -1026,22 +1033,22 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
if (moveHandler) {
// Even though this should only be attached when this.normalMouse is true, holding the
// mouse button down when normalMouse changes can happen. Just always try to remove it.
off(this._document, 'mousemove', moveHandler);
this._document.removeEventListener('mousemove', moveHandler);
moveHandler = null;
}
off(this._document, 'mouseup', handler);
this._document.removeEventListener('mouseup', handler);
return this.cancel(ev);
};
on(this._document, 'mouseup', handler);
this._document.addEventListener('mouseup', handler);
return this.cancel(ev);
});
}));
// if (this.normalMouse) {
// on(this.document, 'mousemove', sendMove);
// }
on(el, 'wheel', (ev: WheelEvent) => {
this.register(addDisposableDomListener(el, 'wheel', (ev: WheelEvent) => {
if (!this.mouseEvents) {
// Convert wheel events into up/down events when the buffer does not have scrollback, this
// enables scrolling in apps hosted in the alt buffer such as vim or tmux.
@@ -1066,27 +1073,27 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
if (this.x10Mouse || this._vt300Mouse || this._decLocator) return;
sendButton(ev);
ev.preventDefault();
});
}));
// allow wheel scrolling in
// the shell for example
on(el, 'wheel', (ev: WheelEvent) => {
this.register(addDisposableDomListener(el, 'wheel', (ev: WheelEvent) => {
if (this.mouseEvents) return;
this.viewport.onWheel(ev);
return this.cancel(ev);
});
}));
on(el, 'touchstart', (ev: TouchEvent) => {
this.register(addDisposableDomListener(el, 'touchstart', (ev: TouchEvent) => {
if (this.mouseEvents) return;
this.viewport.onTouchStart(ev);
return this.cancel(ev);
});
}));
on(el, 'touchmove', (ev: TouchEvent) => {
this.register(addDisposableDomListener(el, 'touchmove', (ev: TouchEvent) => {
if (this.mouseEvents) return;
this.viewport.onTouchMove(ev);
return this.cancel(ev);
});
}));
}
/**
@@ -1112,6 +1119,17 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
}
}
/**
* Change the cursor style for different selection modes
*/
public updateCursorStyle(ev: KeyboardEvent): void {
if (this.selectionManager && this.selectionManager.shouldColumnSelect(ev)) {
this.element.classList.add('xterm-cursor-crosshair');
} else {
this.element.classList.remove('xterm-cursor-crosshair');
}
}
/**
* Display the cursor element
*/
@@ -1429,6 +1447,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
const result = evaluateKeyboardEvent(event, this.applicationCursor, this.browser.isMac, this.options.macOptionIsMeta);
this.updateCursorStyle(event);
// if (result.key === C0.DC3) { // XOFF
// this._writeStopped = true;
// } else if (result.key === C0.DC1) { // XON
@@ -1500,6 +1520,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
}
}
protected _keyUp(ev: KeyboardEvent): void {
this.updateCursorStyle(ev);
}
/**
* Handle a keypress event.
* Key Resources:
@@ -1844,9 +1868,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.options.cols = this.cols;
const customKeyEventHandler = this._customKeyEventHandler;
const inputHandler = this._inputHandler;
const cursorState = this.cursorState;
this._setup();
this._customKeyEventHandler = customKeyEventHandler;
this._inputHandler = inputHandler;
this.cursorState = cursorState;
this.refresh(0, this.rows - 1);
if (this.viewport) {
this.viewport.syncScrollArea();
@@ -1928,21 +1954,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
* Helpers
*/
function globalOn(el: any, type: string, handler: (event: Event) => any, capture?: boolean, passive?: boolean): void {
if (!Array.isArray(el)) {
el = [el];
}
el.forEach((element: HTMLElement) => {
element.addEventListener(type, handler, { capture: capture || false, passive: passive || false });
});
}
// TODO: Remove once everything is typed
const on = globalOn;
function off(el: any, type: string, handler: (event: Event) => any, capture: boolean = false): void {
el.removeEventListener(type, handler, capture);
}
function wasMondifierKeyOnlyEvent(ev: KeyboardEvent): boolean {
return ev.keyCode === 16 || // Shift
ev.keyCode === 17 || // Ctrl
+4 -4
View File
@@ -3,9 +3,9 @@
* @license MIT
*/
import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter } from 'xterm';
import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter, IDisposable } from 'xterm';
import { IColorSet, IRenderer } from './renderer/Types';
import { IMouseZoneManager } from './input/Types';
import { IMouseZoneManager } from './ui/Types';
import { ICharset } from './core/Types';
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
@@ -86,7 +86,7 @@ export interface IInputHandlingTerminal extends IEventEmitter {
tabSet(): void;
}
export interface IViewport {
export interface IViewport extends IDisposable {
scrollBarWidth: number;
syncScrollArea(): void;
getLinesScrolled(ev: WheelEvent): number;
@@ -468,7 +468,7 @@ export interface IDcsHandler {
/**
* EscapeSequenceParser interface.
*/
export interface IEscapeSequenceParser {
export interface IEscapeSequenceParser extends IDisposable {
/**
* Reset the parser to its initial state (handlers are kept).
*/
+7 -3
View File
@@ -5,7 +5,9 @@
import { IColorSet } from './renderer/Types';
import { ITerminal, IViewport } from './Types';
import { CharMeasure } from './utils/CharMeasure';
import { CharMeasure } from './ui/CharMeasure';
import { Disposable } from './common/Lifecycle';
import { addDisposableDomListener } from './ui/Lifecycle';
const FALLBACK_SCROLL_BAR_WIDTH = 15;
@@ -13,7 +15,7 @@ const FALLBACK_SCROLL_BAR_WIDTH = 15;
* Represents the viewport of a terminal, the visible area within the larger buffer of output.
* Logic for the virtual scroll bar is included in this object.
*/
export class Viewport implements IViewport {
export class Viewport extends Disposable implements IViewport {
public scrollBarWidth: number = 0;
private _currentRowHeight: number = 0;
private _lastRecordedBufferLength: number = 0;
@@ -39,11 +41,13 @@ export class Viewport implements IViewport {
private _scrollArea: HTMLElement,
private _charMeasure: CharMeasure
) {
super();
// Measure the width of the scrollbar. If it is 0 we can assume it's an OSX overlay scrollbar.
// Unfortunately the overlay scrollbar would be hidden underneath the screen element in that case,
// therefore we account for a standard amount to make it visible
this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH;
this._viewportElement.addEventListener('scroll', this._onScroll.bind(this));
this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._onScroll.bind(this)));
// Perform this async to ensure the CharMeasure is ready.
setTimeout(() => this.syncScrollArea(), 0);
+5 -1
View File
@@ -5,9 +5,13 @@
* Implements the attach method, that attaches the terminal to a WebSocket stream.
*/
import { Terminal } from 'xterm';
import { Terminal, IDisposable } from 'xterm';
export interface IAttachAddonTerminal extends Terminal {
_core: {
register<T extends IDisposable>(d: T): void;
};
__socket?: WebSocket;
__attachSocketBuffer?: string;
+19 -5
View File
@@ -5,7 +5,7 @@
* Implements the attach method, that attaches the terminal to a WebSocket stream.
*/
import { Terminal } from 'xterm';
import { Terminal, IDisposable } from 'xterm';
import { IAttachAddonTerminal } from './Interfaces';
/**
@@ -87,14 +87,28 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean
socket.send(data);
};
socket.addEventListener('message', addonTerminal.__getMessage);
addonTerminal._core.register(addSocketListener(socket, 'message', addonTerminal.__getMessage));
if (bidirectional) {
addonTerminal.on('data', addonTerminal.__sendData);
addonTerminal._core.register(addonTerminal.addDisposableListener('data', addonTerminal.__sendData));
}
socket.addEventListener('close', () => detach(addonTerminal, socket));
socket.addEventListener('error', () => detach(addonTerminal, socket));
addonTerminal._core.register(addSocketListener(socket, 'close', () => detach(addonTerminal, socket)));
addonTerminal._core.register(addSocketListener(socket, 'error', () => detach(addonTerminal, socket)));
}
function addSocketListener(socket: WebSocket, type: string, handler: (this: WebSocket, ev: Event) => any): IDisposable {
socket.addEventListener(type, handler);
return {
dispose: () => {
if (!handler) {
// Already disposed
return;
}
socket.removeEventListener(type, handler);
handler = null;
}
};
}
/**
+33
View File
@@ -0,0 +1,33 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IDisposable } from 'xterm';
/**
* A base class that can be extended to provide convenience methods for managing the lifecycle of an
* object and its components.
*/
export abstract class Disposable implements IDisposable {
protected _disposables: IDisposable[] = [];
constructor() {
}
/**
* Disposes the object, triggering the `dispose` method on all registered IDisposables.
*/
public dispose(): void {
this._disposables.forEach(d => d.dispose());
this._disposables.length = 0;
}
/**
* Registers a disposable object.
* @param d The disposable to register.
*/
public register<T extends IDisposable>(d: T): void {
this._disposables.push(d);
}
}
+1 -1
View File
@@ -45,7 +45,7 @@ export class Terminal implements ITerminalApi {
this._core.emit(type, data);
}
public addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable {
return this.addDisposableListener(type, handler);
return this._core.addDisposableListener(type, handler);
}
public resize(columns: number, rows: number): void {
this._core.resize(columns, rows);
+1 -1
View File
@@ -49,7 +49,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
public onFocus(terminal: ITerminal): void {}
public onCursorMove(terminal: ITerminal): void {}
public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void {}
public onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void {}
public onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number], columnSelectMode: boolean = false): void {}
public onThemeChanged(terminal: ITerminal, colorSet: IColorSet): void {
this._refreshCharAtlas(terminal, colorSet);
+6 -4
View File
@@ -11,8 +11,8 @@ import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Types';
import { ITerminal } from '../Types';
import { LinkRenderLayer } from './LinkRenderLayer';
import { EventEmitter } from '../EventEmitter';
import { RenderDebouncer } from '../utils/RenderDebouncer';
import { ScreenDprMonitor } from '../utils/ScreenDprMonitor';
import { RenderDebouncer } from '../ui/RenderDebouncer';
import { ScreenDprMonitor } from '../ui/ScreenDprMonitor';
import { ITheme } from 'xterm';
export class Renderer extends EventEmitter implements IRenderer {
@@ -62,12 +62,14 @@ export class Renderer extends EventEmitter implements IRenderer {
this._renderDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this));
this._screenDprMonitor = new ScreenDprMonitor();
this._screenDprMonitor.setListener(() => this.onWindowResize(window.devicePixelRatio));
this.register(this._screenDprMonitor);
// Detect whether IntersectionObserver is detected and enable renderer pause
// and resume based on terminal visibility if so
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver(e => this.onIntersectionChange(e[0]), {threshold: 0});
observer.observe(this._terminal.element);
this.register({ dispose: () => observer.disconnect() });
}
}
@@ -141,8 +143,8 @@ export class Renderer extends EventEmitter implements IRenderer {
this._runOperation(l => l.onFocus(this._terminal));
}
public onSelectionChanged(start: [number, number], end: [number, number]): void {
this._runOperation(l => l.onSelectionChanged(this._terminal, start, end));
public onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean = false): void {
this._runOperation(l => l.onSelectionChanged(this._terminal, start, end, columnSelectMode));
}
public onCursorMove(): void {
+44 -18
View File
@@ -8,13 +8,14 @@ import { IColorSet, IRenderDimensions } from './Types';
import { BaseRenderLayer } from './BaseRenderLayer';
export class SelectionRenderLayer extends BaseRenderLayer {
private _state: {start: [number, number], end: [number, number]};
private _state: { start: [number, number], end: [number, number], columnSelectMode: boolean };
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
super(container, 'selection', zIndex, true, colors);
this._state = {
start: null,
end: null
end: null,
columnSelectMode: null
};
}
@@ -23,7 +24,8 @@ export class SelectionRenderLayer extends BaseRenderLayer {
// Resizing the canvas discards the contents of the canvas so clear state
this._state = {
start: null,
end: null
end: null,
columnSelectMode: null
};
}
@@ -31,15 +33,16 @@ export class SelectionRenderLayer extends BaseRenderLayer {
if (this._state.start && this._state.end) {
this._state = {
start: null,
end: null
end: null,
columnSelectMode: null
};
this.clearAll();
}
}
public onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void {
public onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number], columnSelectMode: boolean): void {
// Selection has not changed
if (this._state.start === start || this._state.end === end) {
if (!this._didStateChange(start, end, columnSelectMode)) {
return;
}
@@ -62,25 +65,48 @@ export class SelectionRenderLayer extends BaseRenderLayer {
return;
}
// Draw first row
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
const startRowEndCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : terminal.cols;
this._ctx.fillStyle = this._colors.selection.css;
this.fillCells(startCol, viewportCappedStartRow, startRowEndCol - startCol, 1);
// Draw middle rows
const middleRowsCount = Math.max(viewportCappedEndRow - viewportCappedStartRow - 1, 0);
this.fillCells(0, viewportCappedStartRow + 1, terminal.cols, middleRowsCount);
if (columnSelectMode) {
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
const width = end[0] - startCol;
const height = viewportCappedEndRow - viewportCappedStartRow + 1;
this.fillCells(startCol, viewportCappedStartRow, width, height);
} else {
// Draw first row
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
const startRowEndCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : terminal.cols;
this.fillCells(startCol, viewportCappedStartRow, startRowEndCol - startCol, 1);
// Draw final row
if (viewportCappedStartRow !== viewportCappedEndRow) {
// Only draw viewportEndRow if it's not the same as viewportStartRow
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : terminal.cols;
this.fillCells(0, viewportCappedEndRow, endCol, 1);
// Draw middle rows
const middleRowsCount = Math.max(viewportCappedEndRow - viewportCappedStartRow - 1, 0);
this.fillCells(0, viewportCappedStartRow + 1, terminal.cols, middleRowsCount);
// Draw final row
if (viewportCappedStartRow !== viewportCappedEndRow) {
// Only draw viewportEndRow if it's not the same as viewportStartRow
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : terminal.cols;
this.fillCells(0, viewportCappedEndRow, endCol, 1);
}
}
// Save state for next render
this._state.start = [start[0], start[1]];
this._state.end = [end[0], end[1]];
this._state.columnSelectMode = columnSelectMode;
}
private _didStateChange(start: [number, number], end: [number, number], columnSelectMode: boolean): boolean {
return !this._areCoordinatesEqual(start, this._state.start) ||
!this._areCoordinatesEqual(end, this._state.end) ||
columnSelectMode !== this._state.columnSelectMode;
}
private _areCoordinatesEqual(coord1: [number, number], coord2: [number, number]): boolean {
if (!coord1 || !coord2) {
return false;
}
return coord1[0] === coord2[0] && coord1[1] === coord2[1];
}
}
+5 -4
View File
@@ -4,7 +4,7 @@
*/
import { ITerminal } from '../Types';
import { IEventEmitter, ITheme } from 'xterm';
import { IEventEmitter, ITheme, IDisposable } from 'xterm';
import { IColorSet } from '../shared/Types';
/**
@@ -24,17 +24,18 @@ export const enum FLAGS {
* Note that IRenderer implementations should emit the refresh event after
* rendering rows to the screen.
*/
export interface IRenderer extends IEventEmitter {
export interface IRenderer extends IEventEmitter, IDisposable {
dimensions: IRenderDimensions;
colorManager: IColorManager;
dispose(): void;
setTheme(theme: ITheme): IColorSet;
onWindowResize(devicePixelRatio: number): void;
onResize(cols: number, rows: number): void;
onCharSizeChanged(): void;
onBlur(): void;
onFocus(): void;
onSelectionChanged(start: [number, number], end: [number, number]): void;
onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void;
onCursorMove(): void;
onOptionsChanged(): void;
clear(): void;
@@ -98,7 +99,7 @@ export interface IRenderLayer {
/**
* Calls when the selection changes.
*/
onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void;
onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number], columnSelectMode: boolean): void;
/**
* Resize the render layer.
+21 -14
View File
@@ -8,7 +8,7 @@ import { ITerminal } from '../../Types';
import { ITheme } from 'xterm';
import { EventEmitter } from '../../EventEmitter';
import { ColorManager } from '../ColorManager';
import { RenderDebouncer } from '../../utils/RenderDebouncer';
import { RenderDebouncer } from '../../ui/RenderDebouncer';
import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, DomRendererRowFactory } from './DomRendererRowFactory';
const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-';
@@ -217,7 +217,7 @@ export class DomRenderer extends EventEmitter implements IRenderer {
this._rowContainer.classList.add(FOCUS_CLASS);
}
public onSelectionChanged(start: [number, number], end: [number, number]): void {
public onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void {
// Remove all selections
while (this._selectionContainer.children.length) {
this._selectionContainer.removeChild(this._selectionContainer.children[0]);
@@ -241,18 +241,25 @@ export class DomRenderer extends EventEmitter implements IRenderer {
// Create the selections
const documentFragment = document.createDocumentFragment();
// Draw first row
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol));
// Draw middle rows
const middleRowsCount = viewportCappedEndRow - viewportCappedStartRow - 1;
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._terminal.cols, middleRowsCount));
// Draw final row
if (viewportCappedStartRow !== viewportCappedEndRow) {
// Only draw viewportEndRow if it's not the same as viewporttartRow
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, endCol));
if (columnSelectMode) {
documentFragment.appendChild(
this._createSelectionElement(viewportCappedStartRow, startCol, end[0], viewportCappedEndRow - viewportStartRow + 1)
);
} else {
// Draw first row
const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol));
// Draw middle rows
const middleRowsCount = viewportCappedEndRow - viewportCappedStartRow - 1;
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._terminal.cols, middleRowsCount));
// Draw final row
if (viewportCappedStartRow !== viewportCappedEndRow) {
// Only draw viewportEndRow if it's not the same as viewporttartRow
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, endCol));
}
}
this._selectionContainer.appendChild(documentFragment);
}
@@ -303,7 +310,7 @@ export class DomRenderer extends EventEmitter implements IRenderer {
const row = y + terminal.buffer.ydisp;
const lineData = terminal.buffer.lines.get(row);
rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorX, terminal.charMeasure.width));
rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorX, terminal.charMeasure.width, terminal.cols));
}
this._terminal.emit('refresh', {start, end});
+20 -11
View File
@@ -23,7 +23,7 @@ describe('DomRendererRowFactory', () => {
describe('createRow', () => {
it('should create an element for every character in the row', () => {
const fragment = rowFactory.createRow(lineData, false, 0, 5);
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span> </span>' +
'<span> </span>'
@@ -34,24 +34,33 @@ describe('DomRendererRowFactory', () => {
lineData[0] = [DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)];
// There should be no element for the following "empty" cell
lineData[1] = [DEFAULT_ATTR, '', 0, undefined];
const fragment = rowFactory.createRow(lineData, false, 0, 5);
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span style="width: 10px;">語</span>'
);
});
it('should add class for cursor', () => {
const fragment = rowFactory.createRow(lineData, true, 0, 5);
const fragment = rowFactory.createRow(lineData, true, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-cursor"> </span>' +
'<span> </span>'
);
});
it('should not render cells that go beyond the terminal\'s columns', () => {
lineData[0] = [DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)];
lineData[1] = [DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5, 1);
assert.equal(getFragmentHtml(fragment),
'<span>a</span>'
);
});
describe('attributes', () => {
it('should add class for bold', () => {
lineData[0] = [DEFAULT_ATTR | (FLAGS.BOLD << 18), 'a', 1, 'a'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5);
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-bold">a</span>' +
'<span> </span>'
@@ -60,7 +69,7 @@ describe('DomRendererRowFactory', () => {
it('should add class for italic', () => {
lineData[0] = [DEFAULT_ATTR | (FLAGS.ITALIC << 18), 'a', 1, 'a'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5);
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-italic">a</span>' +
'<span> </span>'
@@ -71,7 +80,7 @@ describe('DomRendererRowFactory', () => {
const defaultAttrNoFgColor = (0 << 9) | (256 << 0);
for (let i = 0; i < 256; i++) {
lineData[0] = [defaultAttrNoFgColor | (i << 9), 'a', 1, 'a'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5);
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-fg-${i}">a</span>` +
'<span> </span>'
@@ -83,7 +92,7 @@ describe('DomRendererRowFactory', () => {
const defaultAttrNoBgColor = (257 << 9) | (0 << 0);
for (let i = 0; i < 256; i++) {
lineData[0] = [defaultAttrNoBgColor | (i << 0), 'a', 1, 'a'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5);
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-bg-${i}">a</span>` +
'<span> </span>'
@@ -93,7 +102,7 @@ describe('DomRendererRowFactory', () => {
it('should correctly invert colors', () => {
lineData[0] = [(FLAGS.INVERSE << 18) | (2 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5);
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-fg-1 xterm-bg-2">a</span>' +
'<span> </span>'
@@ -102,7 +111,7 @@ describe('DomRendererRowFactory', () => {
it('should correctly invert default fg color', () => {
lineData[0] = [(FLAGS.INVERSE << 18) | (257 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5);
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-fg-1 xterm-bg-15">a</span>' +
'<span> </span>'
@@ -111,7 +120,7 @@ describe('DomRendererRowFactory', () => {
it('should correctly invert default bg color', () => {
lineData[0] = [(FLAGS.INVERSE << 18) | (1 << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5);
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-fg-0 xterm-bg-1">a</span>' +
'<span> </span>'
@@ -121,7 +130,7 @@ describe('DomRendererRowFactory', () => {
it('should turn bold fg text bright', () => {
for (let i = 0; i < 8; i++) {
lineData[0] = [(FLAGS.BOLD << 18) | (i << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5);
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-bold xterm-fg-${i + 8}">a</span>` +
'<span> </span>'
+9 -1
View File
@@ -17,9 +17,16 @@ export class DomRendererRowFactory {
) {
}
public createRow(lineData: LineData, isCursorRow: boolean, cursorX: number, cellWidth: number): DocumentFragment {
public createRow(lineData: LineData, isCursorRow: boolean, cursorX: number, cellWidth: number, cols: number): DocumentFragment {
const fragment = this._document.createDocumentFragment();
let colCount = 0;
for (let x = 0; x < lineData.length; x++) {
// Don't allow any buffer to the right to be displayed
if (colCount >= cols) {
continue;
}
const charData = lineData[x];
const char: string = charData[CHAR_DATA_CHAR_INDEX];
const attr: number = charData[CHAR_DATA_ATTR_INDEX];
@@ -76,6 +83,7 @@ export class DomRendererRowFactory {
charElement.classList.add(`xterm-bg-${bg}`);
}
fragment.appendChild(charElement);
colCount += width;
}
return fragment;
}
+1 -1
View File
@@ -10,7 +10,7 @@ import { IDisposable } from 'xterm';
* @param type The event type.
* @param handler The handler for the listener.
*/
export function addDisposableListener(
export function addDisposableDomListener(
node: Element | Window | Document,
type: string,
handler: (e: any) => void,
@@ -5,6 +5,8 @@
import { ITerminal } from '../Types';
import { IMouseZoneManager, IMouseZone } from './Types';
import { Disposable } from '../common/Lifecycle';
import { addDisposableDomListener } from './Lifecycle';
const HOVER_DURATION = 500;
@@ -16,7 +18,7 @@ const HOVER_DURATION = 500;
* needed to support was single-line links which never overlap. Improvements can
* be made in the future.
*/
export class MouseZoneManager implements IMouseZoneManager {
export class MouseZoneManager extends Disposable implements IMouseZoneManager {
private _zones: IMouseZone[] = [];
private _areZonesActive: boolean = false;
@@ -30,13 +32,20 @@ export class MouseZoneManager implements IMouseZoneManager {
constructor(
private _terminal: ITerminal
) {
this._terminal.element.addEventListener('mousedown', e => this._onMouseDown(e));
super();
this.register(addDisposableDomListener(this._terminal.element, 'mousedown', e => this._onMouseDown(e)));
// These events are expensive, only listen to it when mouse zones are active
this._mouseMoveListener = e => this._onMouseMove(e);
this._clickListener = e => this._onClick(e);
}
public dispose(): void {
super.dispose();
this._deactivate();
}
public add(zone: IMouseZone): void {
this._zones.push(zone);
if (this._zones.length === 1) {
@@ -3,6 +3,8 @@
* @license MIT
*/
import { Disposable } from '../common/Lifecycle';
export type ScreenDprListener = (newDevicePixelRatio?: number, oldDevicePixelRatio?: number) => void;
/**
@@ -15,7 +17,7 @@ export type ScreenDprListener = (newDevicePixelRatio?: number, oldDevicePixelRat
* The listener should fire on both window zoom changes and switching to a
* monitor with a different DPI.
*/
export class ScreenDprMonitor {
export class ScreenDprMonitor extends Disposable {
private _currentDevicePixelRatio: number;
private _outerListener: MediaQueryListListener;
private _listener: ScreenDprListener;
@@ -33,6 +35,11 @@ export class ScreenDprMonitor {
this._updateDpr();
}
public dispose(): void {
super.dispose();
this.clearListener();
}
private _updateDpr(): void {
// Clear listeners for old DPR
if (this._resolutionMediaMatchList) {
+3 -1
View File
@@ -3,7 +3,9 @@
* @license MIT
*/
export interface IMouseZoneManager {
import { IDisposable } from 'xterm';
export interface IMouseZoneManager extends IDisposable {
add(zone: IMouseZone): void;
clearAll(start?: number, end?: number): void;
}
+7
View File
@@ -182,6 +182,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
wraparoundMode: boolean;
bracketedPasteMode: boolean;
curAttr: number;
savedCurAttr: number;
savedCols: number;
x10Mouse: boolean;
vt200Mouse: boolean;
@@ -315,6 +316,9 @@ export class MockBuffer implements IBuffer {
}
export class MockRenderer implements IRenderer {
dispose(): void {
throw new Error('Method not implemented.');
}
colorManager: IColorManager;
on(type: string, listener: XtermListener): void {
throw new Error('Method not implemented.');
@@ -343,6 +347,9 @@ export class MockRenderer implements IRenderer {
}
export class MockViewport implements IViewport {
dispose(): void {
throw new Error('Method not implemented.');
}
scrollBarWidth: number = 0;
onThemeChanged(colors: IColorSet): void {
throw new Error('Method not implemented.');
+5
View File
@@ -139,6 +139,11 @@
cursor: pointer;
}
.xterm.xterm-cursor-crosshair {
/* Column selection mode */
cursor: crosshair;
}
.xterm .xterm-accessibility,
.xterm .xterm-message {
position: absolute;
+9
View File
@@ -124,6 +124,15 @@ declare module 'xterm' {
*/
macOptionIsMeta?: boolean;
/**
* Whether holding a modifier key will force normal selection behavior,
* regardless of whether the terminal is in mouse events mode. This will
* also prevent mouse events from being emitted by the terminal. For example,
* this allows you to use xterm.js' regular selection inside tmux with
* mouse mode enabled.
*/
macOptionClickForcesSelection?: boolean;
/**
* (EXPERIMENTAL) The type of renderer to use, this allows using the
* fallback DOM renderer when canvas is too slow for the environment. The