Merge remote-tracking branch 'ups/v3' into v3

This commit is contained in:
Daniel Imms
2017-10-02 11:00:24 -07:00
72 changed files with 3687 additions and 3838 deletions
+6 -6
View File
@@ -97,12 +97,10 @@ xterm.fit();
Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Here is a list of the versions we aim to support:
- Chrome 48+
- Edge 13+
- Firefox 44+
- Internet Explorer 11+
- Opera 35+
- Safari 8+
- Chrome latest
- Edge latest
- Firefox latest
- Safari latest
Xterm.js works seamlessly in Electron apps and may even work on earlier versions of the browsers but these are the browsers we strive to keep working.
@@ -130,6 +128,8 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising `xterm.js`, SJCL & websockets.
- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible
computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages.
- [**Theia**](https://github.com/theia-ide/theia): Theia is a cloud & desktop IDE framework implemented in TypeScript.
- [**DevOps Helper**](https://github.com/ricktbaker/devops_helper) DevOps Helper tool to make life easier working with AWS instances across multiple organizations.
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list.
+24 -25
View File
@@ -2,9 +2,7 @@ var term,
protocol,
socketURL,
socket,
pid,
charWidth,
charHeight;
pid;
var terminalContainer = document.getElementById('terminal-container'),
actionElements = {
@@ -21,11 +19,13 @@ var terminalContainer = document.getElementById('terminal-container'),
colsElement = document.getElementById('cols'),
rowsElement = document.getElementById('rows');
function setTerminalSize () {
var cols = parseInt(colsElement.value, 10),
rows = parseInt(rowsElement.value, 10),
width = (cols * charWidth).toString() + 'px',
height = (rows * charHeight).toString() + 'px';
function setTerminalSize() {
var cols = parseInt(colsElement.value, 10);
var rows = parseInt(rowsElement.value, 10);
var viewportElement = document.querySelector('.xterm-viewport');
var scrollBarWidth = viewportElement.offsetWidth - viewportElement.clientWidth;
var width = (cols * term.charMeasure.width + 20 /*room for scrollbar*/).toString() + 'px';
var height = (rows * term.charMeasure.height).toString() + 'px';
terminalContainer.style.width = width;
terminalContainer.style.height = height;
@@ -92,27 +92,26 @@ function createTerminal() {
term.open(terminalContainer);
term.fit();
var initialGeometry = term.proposeGeometry(),
cols = initialGeometry.cols,
rows = initialGeometry.rows;
// fit is called within a setTimeout, cols and rows need this.
setTimeout(() => {
colsElement.value = term.cols;
rowsElement.value = term.rows;
colsElement.value = cols;
rowsElement.value = rows;
// Set terminal size again to set the specific dimensions on the demo
setTerminalSize();
fetch('/terminals?cols=' + cols + '&rows=' + rows, {method: 'POST'}).then(function (res) {
fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then(function (res) {
charWidth = Math.ceil(term.element.offsetWidth / cols);
charHeight = Math.ceil(term.element.offsetHeight / rows);
res.text().then(function (pid) {
window.pid = pid;
socketURL += pid;
socket = new WebSocket(socketURL);
socket.onopen = runRealTerminal;
socket.onclose = runFakeTerminal;
socket.onerror = runFakeTerminal;
res.text().then(function (pid) {
window.pid = pid;
socketURL += pid;
socket = new WebSocket(socketURL);
socket.onopen = runRealTerminal;
socket.onclose = runFakeTerminal;
socket.onerror = runFakeTerminal;
});
});
});
}, 0);
}
function runRealTerminal() {
-6
View File
@@ -14,9 +14,3 @@ h1 {
margin: 0 auto;
padding: 2px;
}
#terminal-container .terminal {
background-color: #111;
color: #fafafa;
padding: 2px;
}
+11 -3
View File
@@ -168,6 +168,10 @@ namespace methods_core {
t.setOption('bellStyle', 'visual');
t.setOption('bellStyle', 'sound');
t.setOption('bellStyle', 'both');
t.setOption('fontSize', 1);
t.setOption('lineHeight', 1);
t.setOption('fontFamily', 'foo');
t.setOption('theme', {background: '#ff0000'});
}
}
namespace scrolling {
@@ -210,9 +214,13 @@ namespace methods_experimental {
t.registerLinkMatcher(/foo/, () => true, {
matchIndex: 1,
priority: 1,
validationCallback: (uri: string, element: HTMLElement, callback: (isValid: boolean) => void) => {
console.log(uri, element, callback);
}
validationCallback: (uri: string, callback: (isValid: boolean) => void) => {
console.log(uri, callback);
},
tooltipCallback: (e: MouseEvent, uri: string) => {
console.log(e, uri);
},
leaveCallback: () => {}
});
t.deregisterLinkMatcher(1);
}
+2
View File
@@ -7,6 +7,7 @@ const buffer = require('vinyl-buffer');
const coveralls = require('gulp-coveralls');
const fs = require('fs-extra');
const gulp = require('gulp');
const path = require('path');
const istanbul = require('gulp-istanbul');
const merge = require('merge-stream');
const mocha = require('gulp-mocha');
@@ -96,6 +97,7 @@ gulp.task('browserify-addons', ['tsc'], function() {
packageCache: {}
};
let searchBundle = browserify(searchOptions)
.external(path.join(outDir, 'Terminal.js'))
.bundle()
.pipe(source('./addons/search/search.js'))
.pipe(buffer())
+1 -1
View File
@@ -64,7 +64,7 @@
"nodemon": "1.10.2",
"sorcery": "^0.10.0",
"tslint": "^4.0.2",
"typescript": "~2.2.0",
"typescript": "~2.4.0",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0"
},
+1
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
+132 -74
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
@@ -6,8 +7,10 @@ import { ITerminal, IBuffer } from './Interfaces';
import { CircularList } from './utils/CircularList';
import { LineData, CharData } from './Types';
export const CHAR_DATA_ATTR_INDEX = 0;
export const CHAR_DATA_CHAR_INDEX = 1;
export const CHAR_DATA_WIDTH_INDEX = 2;
export const CHAR_DATA_CODE_INDEX = 3;
/**
* This class represents a terminal buffer (an internal state of the terminal), where the
@@ -32,8 +35,8 @@ export class Buffer implements IBuffer {
/**
* Create a new Buffer.
* @param _terminal The terminal the Buffer will belong to.
* @param _hasScrollback Whether the buffer should respecr the scrollback of
* the terminal..
* @param _hasScrollback Whether the buffer should respect the scrollback of
* the terminal.
*/
constructor(
private _terminal: ITerminal,
@@ -46,6 +49,16 @@ export class Buffer implements IBuffer {
return this._lines;
}
public get hasScrollback(): boolean {
return this._hasScrollback && this.lines.maxLength > this._terminal.rows;
}
public get isCursorInViewport(): boolean {
const absoluteY = this.ybase + this.y;
const relativeY = absoluteY - this.ydisp;
return (relativeY >= 0 && relativeY < this._terminal.rows);
}
/**
* Gets the correct buffer length based on the rows provided, the terminal's
* scrollback and whether this buffer is flagged to have scrollback or not.
@@ -78,11 +91,10 @@ export class Buffer implements IBuffer {
this.ybase = 0;
this.y = 0;
this.x = 0;
this.scrollBottom = 0;
this.scrollTop = 0;
this.tabs = {};
this._lines = new CircularList<LineData>(this._getCorrectBufferLength(this._terminal.rows));
this.scrollTop = 0;
this.scrollBottom = this._terminal.rows - 1;
this.setupTabStops();
}
/**
@@ -91,11 +103,6 @@ export class Buffer implements IBuffer {
* @param newRows The new number of rows.
*/
public resize(newCols: number, newRows: number): void {
// Don't resize the buffer if it's empty and hasn't been used yet.
if (this._lines.length === 0) {
return;
}
// Increase max length if needed before adjustments to allow space to fill
// as required.
const newMaxLength = this._getCorrectBufferLength(newRows);
@@ -103,83 +110,88 @@ export class Buffer implements IBuffer {
this._lines.maxLength = newMaxLength;
}
// Deal with columns increasing (we don't do anything when columns reduce)
if (this._terminal.cols < newCols) {
const ch: CharData = [this._terminal.defAttr, ' ', 1]; // does xterm use the default attr?
for (let i = 0; i < this._lines.length; i++) {
// TODO: This should be removed, with tests setup for the case that was
// causing the underlying bug, see https://github.com/sourcelair/xterm.js/issues/824
if (this._lines.get(i) === undefined) {
this._lines.set(i, this._terminal.blankLine(undefined, undefined, newCols));
}
while (this._lines.get(i).length < newCols) {
this._lines.get(i).push(ch);
// The following adjustments should only happen if the buffer has been
// initialized/filled.
if (this._lines.length > 0) {
// Deal with columns increasing (we don't do anything when columns reduce)
if (this._terminal.cols < newCols) {
const ch: CharData = [this._terminal.defAttr, ' ', 1, 32]; // does xterm use the default attr?
for (let i = 0; i < this._lines.length; i++) {
// TODO: This should be removed, with tests setup for the case that was
// causing the underlying bug, see https://github.com/sourcelair/xterm.js/issues/824
if (this._lines.get(i) === undefined) {
this._lines.set(i, this._terminal.blankLine(undefined, undefined, newCols));
}
while (this._lines.get(i).length < newCols) {
this._lines.get(i).push(ch);
}
}
}
}
// Resize rows in both directions as needed
let addToY = 0;
if (this._terminal.rows < newRows) {
for (let y = this._terminal.rows; y < newRows; y++) {
if (this._lines.length < newRows + this.ybase) {
if (this.ybase > 0 && this._lines.length <= this.ybase + this.y + addToY + 1) {
// There is room above the buffer and there are no empty elements below the line,
// scroll up
this.ybase--;
addToY++;
if (this.ydisp > 0) {
// Viewport is at the top of the buffer, must increase downwards
this.ydisp--;
// Resize rows in both directions as needed
let addToY = 0;
if (this._terminal.rows < newRows) {
for (let y = this._terminal.rows; y < newRows; y++) {
if (this._lines.length < newRows + this.ybase) {
if (this.ybase > 0 && this._lines.length <= this.ybase + this.y + addToY + 1) {
// There is room above the buffer and there are no empty elements below the line,
// scroll up
this.ybase--;
addToY++;
if (this.ydisp > 0) {
// Viewport is at the top of the buffer, must increase downwards
this.ydisp--;
}
} else {
// Add a blank line if there is no buffer left at the top to scroll to, or if there
// are blank lines after the cursor
this._lines.push(this._terminal.blankLine(undefined, undefined, newCols));
}
}
}
} else { // (this._terminal.rows >= newRows)
for (let y = this._terminal.rows; y > newRows; y--) {
if (this._lines.length > newRows + this.ybase) {
if (this._lines.length > this.ybase + this.y + 1) {
// The line is a blank line below the cursor, remove it
this._lines.pop();
} else {
// The line is the cursor, scroll down
this.ybase++;
this.ydisp++;
}
} else {
// Add a blank line if there is no buffer left at the top to scroll to, or if there
// are blank lines after the cursor
this._lines.push(this._terminal.blankLine(undefined, undefined, newCols));
}
}
}
} else { // (this._terminal.rows >= newRows)
for (let y = this._terminal.rows; y > newRows; y--) {
if (this._lines.length > newRows + this.ybase) {
if (this._lines.length > this.ybase + this.y + 1) {
// The line is a blank line below the cursor, remove it
this._lines.pop();
} else {
// The line is the cursor, scroll down
this.ybase++;
this.ydisp++;
}
// Reduce max length if needed after adjustments, this is done after as it
// would otherwise cut data from the bottom of the buffer.
if (newMaxLength < this._lines.maxLength) {
// Trim from the top of the buffer and adjust ybase and ydisp.
const amountToTrim = this._lines.length - newMaxLength;
if (amountToTrim > 0) {
this._lines.trimStart(amountToTrim);
this.ybase = Math.max(this.ybase - amountToTrim, 0);
this.ydisp = Math.max(this.ydisp - amountToTrim, 0);
}
this._lines.maxLength = newMaxLength;
}
}
// Reduce max length if needed after adjustments, this is done after as it
// would otherwise cut data from the bottom of the buffer.
if (newMaxLength < this._lines.maxLength) {
// Trim from the top of the buffer and adjust ybase and ydisp.
const amountToTrim = this._lines.length - newMaxLength;
if (amountToTrim > 0) {
this._lines.trimStart(amountToTrim);
this.ybase = Math.max(this.ybase - amountToTrim, 0);
this.ydisp = Math.max(this.ydisp - amountToTrim, 0);
// Make sure that the cursor stays on screen
if (this.y >= newRows) {
this.y = newRows - 1;
}
this._lines.maxLength = newMaxLength;
if (addToY) {
this.y += addToY;
}
if (this.x >= newCols) {
this.x = newCols - 1;
}
this.scrollTop = 0;
}
// Make sure that the cursor stays on screen
if (this.y >= newRows) {
this.y = newRows - 1;
}
if (addToY) {
this.y += addToY;
}
if (this.x >= newCols) {
this.x = newCols - 1;
}
this.scrollTop = 0;
this.scrollBottom = newRows - 1;
}
@@ -199,6 +211,9 @@ export class Buffer implements IBuffer {
let widthAdjustedStartCol = startCol;
let widthAdjustedEndCol = endCol;
const line = this.lines.get(lineIndex);
if (!line) {
return '';
}
for (let i = 0; i < line.length; i++) {
const char = line[i];
lineString += char[CHAR_DATA_CHAR_INDEX];
@@ -230,4 +245,47 @@ export class Buffer implements IBuffer {
return lineString.substring(widthAdjustedStartCol, finalEndCol);
}
/**
* Setup the tab stops.
* @param i The index to start setting up tab stops from.
*/
public setupTabStops(i?: number): void {
if (i != null) {
if (!this.tabs[i]) {
i = this.prevStop(i);
}
} else {
this.tabs = {};
i = 0;
}
for (; i < this._terminal.cols; i += this._terminal.options.tabStopWidth) {
this.tabs[i] = true;
}
}
/**
* Move the cursor to the previous tab stop from the given position (default is current).
* @param x The position to move the cursor to the previous tab stop.
*/
public prevStop(x?: number): number {
if (x == null) {
x = this.x;
}
while (!this.tabs[--x] && x > 0);
return x >= this._terminal.cols ? this._terminal.cols - 1 : x < 0 ? 0 : x;
}
/**
* Move the cursor one tab stop forward from the given position (default is current).
* @param x The position to move the cursor one tab stop forward.
*/
public nextStop(x?: number): number {
if (x == null) {
x = this.x;
}
while (!this.tabs[++x] && x < this._terminal.cols);
return x >= this._terminal.cols ? this._terminal.cols - 1 : x < 0 ? 0 : x;
}
}
+1
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
+12
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
@@ -28,6 +29,8 @@ export class BufferSet extends EventEmitter implements IBufferSet {
// See http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer
this._alt = new Buffer(this._terminal, false);
this._activeBuffer = this._normal;
this.setupTabStops();
}
/**
@@ -87,4 +90,13 @@ export class BufferSet extends EventEmitter implements IBufferSet {
this._normal.resize(newCols, newRows);
this._alt.resize(newCols, newRows);
}
/**
* Setup the tab stops.
* @param i The index to start setting up tab stops from.
*/
public setupTabStops(i?: number): void {
this._normal.setupTabStops(i);
this._alt.setupTabStops(i);
}
}
+1
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*/
+11
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*/
@@ -42,6 +43,16 @@ describe('CompositionHelper', () => {
},
handler: (text: string) => {
handledText += text;
},
buffer: {
isCursorInViewport: true
},
charMeasure: {
height: 10,
width: 10
},
options: {
lineHeight: 1
}
};
handledText = '';
+11 -10
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*/
@@ -193,26 +194,26 @@ export class CompositionHelper {
if (!this.isComposing) {
return;
}
const cursor = <HTMLElement>this.terminal.element.querySelector('.terminal-cursor');
if (cursor) {
// Take .xterm-rows offsetTop into account as well in case it's positioned absolutely within
// the .xterm element.
const xtermRows = <HTMLElement>this.terminal.element.querySelector('.xterm-rows');
const cursorTop = xtermRows.offsetTop + cursor.offsetTop;
this.compositionView.style.left = cursor.offsetLeft + 'px';
if (this.terminal.buffer.isCursorInViewport) {
const cellHeight = Math.ceil(this.terminal.charMeasure.height * this.terminal.options.lineHeight);
const cursorTop = this.terminal.buffer.y * cellHeight;
const cursorLeft = this.terminal.buffer.x * this.terminal.charMeasure.width;
this.compositionView.style.left = cursorLeft + 'px';
this.compositionView.style.top = cursorTop + 'px';
this.compositionView.style.height = cursor.offsetHeight + 'px';
this.compositionView.style.lineHeight = cursor.offsetHeight + 'px';
this.compositionView.style.height = cellHeight + 'px';
this.compositionView.style.lineHeight = cellHeight + 'px';
// Sync the textarea to the exact position of the composition view so the IME knows where the
// text is.
const compositionViewBounds = this.compositionView.getBoundingClientRect();
this.textarea.style.left = cursor.offsetLeft + 'px';
this.textarea.style.left = cursorLeft + 'px';
this.textarea.style.top = cursorTop + 'px';
this.textarea.style.width = compositionViewBounds.width + 'px';
this.textarea.style.height = compositionViewBounds.height + 'px';
this.textarea.style.lineHeight = compositionViewBounds.height + 'px';
}
if (!dontRecurse) {
setTimeout(() => this.updateCompositionElements(true), 0);
}
+1
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
+1
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
+1
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
+1
View File
@@ -1,4 +1,5 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
+18 -15
View File
@@ -1,4 +1,6 @@
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
* @license MIT
*/
@@ -36,13 +38,14 @@ export class InputHandler implements IInputHandler {
// dont overflow left
if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1]) {
if (!this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) {
// found empty cell after fullwidth, need to go 2 cells back
if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2])
if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2]) {
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char;
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2][3] = char.charCodeAt(0);
}
} else {
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char;
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][3] = char.charCodeAt(0);
}
this._terminal.updateRange(this._terminal.buffer.y);
}
@@ -81,21 +84,21 @@ export class InputHandler implements IInputHandler {
if (removed[CHAR_DATA_WIDTH_INDEX] === 0
&& this._terminal.buffer.lines.get(row)[this._terminal.cols - 2]
&& this._terminal.buffer.lines.get(row)[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) {
this._terminal.buffer.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1];
this._terminal.buffer.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)];
}
// insert empty cell at cursor
this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 0, [this._terminal.curAttr, ' ', 1]);
this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 0, [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]);
}
}
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, char, ch_width];
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, char, ch_width, char.charCodeAt(0)];
this._terminal.buffer.x++;
this._terminal.updateRange(this._terminal.buffer.y);
// fullwidth char - set next cell width to zero and advance cursor
if (ch_width === 2) {
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, '', 0];
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, '', 0, undefined];
this._terminal.buffer.x++;
}
}
@@ -157,7 +160,7 @@ export class InputHandler implements IInputHandler {
* Horizontal Tab (HT) (Ctrl-I).
*/
public tab(): void {
this._terminal.buffer.x = this._terminal.nextStop();
this._terminal.buffer.x = this._terminal.buffer.nextStop();
}
/**
@@ -188,7 +191,7 @@ export class InputHandler implements IInputHandler {
const row = this._terminal.buffer.y + this._terminal.buffer.ybase;
let j = this._terminal.buffer.x;
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1]; // xterm
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm
while (param-- && j < this._terminal.cols) {
this._terminal.buffer.lines.get(row).splice(j++, 0, ch);
@@ -349,7 +352,7 @@ export class InputHandler implements IInputHandler {
public cursorForwardTab(params: number[]): void {
let param = params[0] || 1;
while (param--) {
this._terminal.buffer.x = this._terminal.nextStop();
this._terminal.buffer.x = this._terminal.buffer.nextStop();
}
}
@@ -467,7 +470,7 @@ export class InputHandler implements IInputHandler {
while (param--) {
// test: echo -e '\e[44m\e[1M\e[0m'
// blankLine(true) - xterm/linux behavior
this._terminal.buffer.lines.splice(row - 1, 1);
this._terminal.buffer.lines.splice(row, 1);
this._terminal.buffer.lines.splice(j, 0, this._terminal.blankLine(true));
}
@@ -487,7 +490,7 @@ export class InputHandler implements IInputHandler {
}
const row = this._terminal.buffer.y + this._terminal.buffer.ybase;
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1]; // xterm
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm
while (param--) {
this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 1);
@@ -535,7 +538,7 @@ export class InputHandler implements IInputHandler {
const row = this._terminal.buffer.y + this._terminal.buffer.ybase;
let j = this._terminal.buffer.x;
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1]; // xterm
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm
while (param-- && j < this._terminal.cols) {
this._terminal.buffer.lines.get(row)[j++] = ch;
@@ -548,7 +551,7 @@ export class InputHandler implements IInputHandler {
public cursorBackwardTab(params: number[]): void {
let param = params[0] || 1;
while (param--) {
this._terminal.buffer.x = this._terminal.prevStop();
this._terminal.buffer.x = this._terminal.buffer.prevStop();
}
}
@@ -589,7 +592,7 @@ export class InputHandler implements IInputHandler {
public repeatPrecedingCharacter(params: number[]): void {
let param = params[0] || 1;
const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + this._terminal.buffer.y);
const ch = line[this._terminal.buffer.x - 1] || [this._terminal.defAttr, ' ', 1];
const ch = line[this._terminal.buffer.x - 1] || [this._terminal.defAttr, ' ', 1, 32];
while (param--) {
line[this._terminal.buffer.x++] = ch;
+70 -12
View File
@@ -1,9 +1,12 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ILinkMatcherOptions } from './Interfaces';
import { LinkMatcherHandler, LinkMatcherValidationCallback, Charset, LineData } from './Types';
import { IColorSet, IRenderer } from './renderer/Interfaces';
import { IMouseZoneManager } from './input/Interfaces';
export interface IBrowser {
isNode: boolean;
@@ -17,24 +20,34 @@ export interface IBrowser {
isMSWindows: boolean;
}
export interface ITerminal extends IEventEmitter {
export interface IBufferAccessor {
buffer: IBuffer;
}
export interface IElementAccessor {
element: HTMLElement;
rowContainer: HTMLElement;
selectionContainer: HTMLElement;
}
export interface ILinkifierAccessor {
linkifier: ILinkifier;
}
export interface ITerminal extends ILinkifierAccessor, IBufferAccessor, IElementAccessor, IEventEmitter {
selectionManager: ISelectionManager;
charMeasure: ICharMeasure;
textarea: HTMLTextAreaElement;
renderer: IRenderer;
rows: number;
cols: number;
browser: IBrowser;
writeBuffer: string[];
children: HTMLElement[];
cursorHidden: boolean;
cursorState: number;
defAttr: number;
options: ITerminalOptions;
buffers: IBufferSet;
buffer: IBuffer;
isFocused: boolean;
mouseHelper: IMouseHelper;
/**
* Emit the 'data' event and populate the given data.
@@ -47,6 +60,7 @@ export interface ITerminal extends IEventEmitter {
reset(): void;
showCursor(): void;
blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData;
refresh(start: number, end: number): void;
}
/**
@@ -92,14 +106,12 @@ export interface IInputHandlingTerminal extends IEventEmitter {
convertEol: boolean;
updateRange(y: number): void;
scroll(isWrapped?: boolean): void;
nextStop(x?: number): number;
setgLevel(g: number): void;
eraseAttr(): number;
eraseRight(x: number, y: number): void;
eraseLine(y: number): void;
eraseLeft(x: number, y: number): void;
blankLine(cur?: boolean, isWrapped?: boolean): LineData;
prevStop(x?: number): number;
is(term: string): boolean;
send(data: string): void;
setgCharset(g: number, charset: Charset): void;
@@ -117,20 +129,23 @@ export interface ITerminalOptions {
bellSound?: string;
bellStyle?: string;
cancelEvents?: boolean;
colors?: string[];
cols?: number;
convertEol?: boolean;
cursorBlink?: boolean;
cursorStyle?: string;
debug?: boolean;
disableStdin?: boolean;
fontSize?: number;
fontFamily?: string;
geometry?: [number, number];
handler?: (data: string) => void;
lineHeight?: number;
rows?: number;
screenKeys?: boolean;
scrollback?: number;
tabStopWidth?: number;
termName?: string;
theme?: ITheme;
useFlowControl?: boolean;
}
@@ -145,7 +160,10 @@ export interface IBuffer {
scrollTop: number;
savedY: number;
savedX: number;
isCursorInViewport: boolean;
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string;
nextStop(x?: number): number;
prevStop(x?: number): number;
}
export interface IBufferSet {
@@ -157,11 +175,17 @@ export interface IBufferSet {
activateAltBuffer(): void;
}
export interface IMouseHelper {
getCoords(event: {pageX: number, pageY: number}, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number];
getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number };
}
export interface IViewport {
syncScrollArea(): void;
onWheel(ev: WheelEvent): void;
onTouchStart(ev: TouchEvent): void;
onTouchMove(ev: TouchEvent): void;
onThemeChanged(colors: IColorSet): void;
}
export interface ISelectionManager {
@@ -186,12 +210,14 @@ export interface ICompositionHelper {
export interface ICharMeasure {
width: number;
height: number;
measure(): void;
measure(options: ITerminalOptions): void;
}
export interface ILinkifier {
linkifyRow(rowIndex: number): void;
attachHypertextLinkHandler(handler: LinkMatcherHandler): void;
export interface ILinkifier extends IEventEmitter {
attachToDom(mouseZoneManager: IMouseZoneManager): void;
linkifyRows(start: number, end: number): void;
setHypertextLinkHandler(handler: LinkMatcherHandler): void;
setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void;
registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number;
deregisterLinkMatcher(matcherId: number): boolean;
}
@@ -232,6 +258,14 @@ export interface ILinkMatcherOptions {
* false if invalid.
*/
validationCallback?: LinkMatcherValidationCallback;
/**
* A callback that fires when the mouse hovers over a link.
*/
tooltipCallback?: LinkMatcherHandler;
/**
* A callback that fires when the mouse leaves a link that was hovered.
*/
leaveCallback?: () => void;
/**
* The priority of the link matcher, this defines the order in which the link
* matcher is evaluated relative to others, from highest to lowest. The
@@ -291,3 +325,27 @@ export interface IInputHandler {
/** CSI s */ saveCursor(params?: number[]): void;
/** CSI u */ restoreCursor(params?: number[]): void;
}
export interface ITheme {
foreground?: string;
background?: string;
cursor?: string;
cursorAccent?: string;
selection?: string;
black?: string;
red?: string;
green?: string;
yellow?: string;
blue?: string;
magenta?: string;
cyan?: string;
white?: string;
brightBlack?: string;
brightRed?: string;
brightGreen?: string;
brightYellow?: string;
brightBlue?: string;
brightMagenta?: string;
brightCyan?: string;
brightWhite?: string;
}
+106 -79
View File
@@ -1,43 +1,90 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import jsdom = require('jsdom');
import { assert } from 'chai';
import { ITerminal, ILinkifier } from './Interfaces';
import { ITerminal, ILinkifier, IBuffer, IBufferAccessor, IElementAccessor } from './Interfaces';
import { Linkifier } from './Linkifier';
import { LinkMatcher } from './Types';
import { LinkMatcher, LineData } from './Types';
import { IMouseZoneManager, IMouseZone } from './input/Interfaces';
import { MockBuffer } from './utils/TestUtils.test';
import { CircularList } from './utils/CircularList';
class TestLinkifier extends Linkifier {
constructor() {
constructor(_terminal: IBufferAccessor & IElementAccessor) {
super(_terminal);
Linkifier.TIME_BEFORE_LINKIFY = 0;
super();
}
public get linkMatchers(): LinkMatcher[] { return this._linkMatchers; }
public linkifyRows(): void { super.linkifyRows(0, this._terminal.buffer.lines.length - 1); }
}
class TestMouseZoneManager implements IMouseZoneManager {
public clears: number = 0;
public zones: IMouseZone[] = [];
add(zone: IMouseZone): void {
this.zones.push(zone);
}
clearAll(): void {
this.clears++;
}
}
describe('Linkifier', () => {
let dom: jsdom.JSDOM;
let window: Window;
let document: Document;
let container: HTMLElement;
let rows: HTMLElement[];
let terminal: IBufferAccessor & IElementAccessor;
let linkifier: TestLinkifier;
let mouseZoneManager: TestMouseZoneManager;
beforeEach(() => {
dom = new jsdom.JSDOM('');
window = dom.window;
document = window.document;
linkifier = new TestLinkifier();
terminal = {
buffer: new MockBuffer(),
element: <HTMLElement>{}
};
terminal.buffer.lines = new CircularList<LineData>(20);
terminal.buffer.ydisp = 0;
linkifier = new TestLinkifier(terminal);
mouseZoneManager = new TestMouseZoneManager();
});
function addRow(html: string): void {
const element = document.createElement('div');
element.innerHTML = html;
container.appendChild(element);
rows.push(element);
function stringToRow(text: string): LineData {
let result: LineData = [];
for (let i = 0; i < text.length; i++) {
result.push([0, text.charAt(i), 1, text.charCodeAt(i)]);
}
return result;
}
function addRow(text: string): void {
terminal.buffer.lines.push(stringToRow(text));
}
function assertLinkifiesEntireRow(uri: string, done: MochaDone): void {
addRow(uri);
linkifier.linkifyRows();
setTimeout(() => {
assert.equal(mouseZoneManager.zones[0].x1, 1);
assert.equal(mouseZoneManager.zones[0].x2, uri.length + 1);
assert.equal(mouseZoneManager.zones[0].y, terminal.buffer.lines.length);
done();
}, 0);
}
function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, links: {x: number, length: number}[], done: MochaDone): void {
addRow(rowText);
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
linkifier.linkifyRows();
// Allow linkify to happen
setTimeout(() => {
assert.equal(mouseZoneManager.zones.length, links.length);
links.forEach((l, i) => {
assert.equal(mouseZoneManager.zones[i].x1, l.x + 1);
assert.equal(mouseZoneManager.zones[i].x2, l.x + l.length + 1);
assert.equal(mouseZoneManager.zones[i].y, terminal.buffer.lines.length);
});
done();
}, 0);
}
describe('before attachToDom', () => {
@@ -52,78 +99,42 @@ describe('Linkifier', () => {
describe('after attachToDom', () => {
beforeEach(() => {
rows = [];
linkifier.attachToDom(document, rows);
container = document.createElement('div');
document.body.appendChild(container);
linkifier.attachToDom(mouseZoneManager);
});
function clickElement(element: Node): void {
const event = document.createEvent('MouseEvent');
event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
element.dispatchEvent(event);
}
function assertLinkifiesEntireRow(uri: string, done: MochaDone): void {
addRow(uri);
linkifier.linkifyRow(0);
setTimeout(() => {
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
assert.equal((<HTMLElement>rows[0].firstChild).textContent, uri);
done();
}, 0);
}
describe('http links', () => {
function assertLinkifiesEntireRow(uri: string, done: MochaDone): void {
addRow(uri);
linkifier.linkifyRow(0);
setTimeout(() => {
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
assert.equal((<HTMLElement>rows[0].firstChild).textContent, uri);
done();
}, 0);
}
it('should allow ~ character in URI path', done => assertLinkifiesEntireRow('http://foo.com/a~b#c~d?e~f', done));
it('should allow ~ character in URI path', (done) => {
assertLinkifiesEntireRow('http://foo.com/a~b#c~d?e~f', done);
});
});
describe('link matcher', () => {
function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, expectedHtml: string, done: MochaDone): void {
addRow(rowText);
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
linkifier.linkifyRow(0);
// Allow linkify to happen
setTimeout(() => {
assert.equal(rows[0].innerHTML, expectedHtml);
done();
}, 0);
}
it('should match a single link', done => {
assertLinkifiesRow('foo', /foo/, '<a>foo</a>', done);
assertLinkifiesRow('foo', /foo/, [{x: 0, length: 3}], done);
});
it('should match a single link at the start of a text node', done => {
assertLinkifiesRow('foo bar', /foo/, '<a>foo</a> bar', done);
assertLinkifiesRow('foo bar', /foo/, [{x: 0, length: 3}], done);
});
it('should match a single link in the middle of a text node', done => {
assertLinkifiesRow('foo bar baz', /bar/, 'foo <a>bar</a> baz', done);
assertLinkifiesRow('foo bar baz', /bar/, [{x: 4, length: 3}], done);
});
it('should match a single link at the end of a text node', done => {
assertLinkifiesRow('foo bar', /bar/, 'foo <a>bar</a>', done);
assertLinkifiesRow('foo bar', /bar/, [{x: 4, length: 3}], done);
});
it('should match a link after a link at the start of a text node', done => {
assertLinkifiesRow('foo bar', /foo|bar/, '<a>foo</a> <a>bar</a>', done);
assertLinkifiesRow('foo bar', /foo|bar/, [{x: 0, length: 3}, {x: 4, length: 3}], done);
});
it('should match a link after a link in the middle of a text node', done => {
assertLinkifiesRow('foo bar baz', /bar|baz/, 'foo <a>bar</a> <a>baz</a>', done);
assertLinkifiesRow('foo bar baz', /bar|baz/, [{x: 4, length: 3}, {x: 8, length: 3}], done);
});
it('should match a link immediately after a link at the end of a text node', done => {
assertLinkifiesRow('<span>foo bar</span>baz', /bar|baz/, '<span>foo <a>bar</a></span><a>baz</a>', done);
assertLinkifiesRow('foo barbaz', /bar|baz/, [{x: 4, length: 3}, {x: 7, length: 3}], done);
});
it('should not duplicate text after a unicode character (wrapped in a span)', done => {
// This is a regression test for an issue that came about when using
// an oh-my-zsh theme that added the large blue diamond unicode
// character (U+1F537) which caused the path to be duplicated. See #642.
assertLinkifiesRow('echo \'<span class="xterm-normal-char">🔷</span>foo\'', /foo/, 'echo \'<span class="xterm-normal-char">🔷</span><a>foo</a>\'', done);
assertLinkifiesRow('echo \'🔷foo\'', /foo/, [{x: 8, length: 3}], done);
});
});
@@ -131,26 +142,42 @@ describe('Linkifier', () => {
it('should enable link if true', done => {
addRow('test');
linkifier.registerLinkMatcher(/test/, () => done(), {
validationCallback: (url, element, cb) => {
validationCallback: (url, cb) => {
assert.equal(mouseZoneManager.zones.length, 0);
cb(true);
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
setTimeout(() => clickElement(rows[0].firstChild), 0);
assert.equal(mouseZoneManager.zones.length, 1);
assert.equal(mouseZoneManager.zones[0].x1, 1);
assert.equal(mouseZoneManager.zones[0].x2, 5);
assert.equal(mouseZoneManager.zones[0].y, 1);
// Fires done()
mouseZoneManager.zones[0].clickCallback(<any>{});
}
});
linkifier.linkifyRow(0);
linkifier.linkifyRows();
});
it('should validate the uri, not the row', done => {
addRow('abc test abc');
linkifier.registerLinkMatcher(/test/, () => done(), {
validationCallback: (uri, cb) => {
assert.equal(uri, 'test');
done();
}
});
linkifier.linkifyRows();
});
it('should disable link if false', done => {
addRow('test');
linkifier.registerLinkMatcher(/test/, () => assert.fail(), {
validationCallback: (url, element, cb) => {
validationCallback: (url, cb) => {
assert.equal(mouseZoneManager.zones.length, 0);
cb(false);
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
setTimeout(() => clickElement(rows[0].firstChild), 0);
assert.equal(mouseZoneManager.zones.length, 0);
}
});
linkifier.linkifyRow(0);
// Allow time for the click to be performed
linkifier.linkifyRows();
// Allow time for the validation callback to be performed
setTimeout(() => done(), 10);
});
@@ -158,7 +185,7 @@ describe('Linkifier', () => {
addRow('test test');
let count = 0;
linkifier.registerLinkMatcher(/test/, () => assert.fail(), {
validationCallback: (url, element, cb) => {
validationCallback: (url, cb) => {
count += 1;
if (count === 2) {
done();
@@ -166,7 +193,7 @@ describe('Linkifier', () => {
cb(false);
}
});
linkifier.linkifyRow(0);
linkifier.linkifyRows();
});
});

Some files were not shown because too many files have changed in this diff Show More