Merge remote-tracking branch 'ups/master' into 687_macos_ci
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
||||
* text=auto eol=lf
|
||||
* text=auto
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
List of xterm.js contributors. Updated before every release.
|
||||
|
||||
Aleksandr Andrienko <aandrienko@codenvy.com>
|
||||
Aleksandr Andriienko <oandriie@redhat.com>
|
||||
Alessandro Nadalin <alessandro.nadalin@gmail.com>
|
||||
Alexander Olsson <noseglid@gmail.com>
|
||||
Alexey Kontsevoy <alexey@gravitational.com>
|
||||
Anish Athalye <me@anishathalye.com>
|
||||
Antonis Kalipetis <akalipetis@sourcelair.com>
|
||||
Anton Skshidlevsky <meefik@gmail.com>
|
||||
Anton Yurovskykh <anton.yurovskykh@gmail.com>
|
||||
Artem Arbatskiy <artyom.arbatskiy@gmail.com>
|
||||
Austin Robertson <austinrobertson@gmail.com>
|
||||
ayapi <colors.aya@gmail.com>
|
||||
Ben Hall <ben@benhall.me.uk>
|
||||
@@ -15,6 +19,7 @@ Bob Reid <bobreid@Bobs-MacBook-Pro.local>
|
||||
bottleofwater <nison.mael+bottleofwater@gmail.com>
|
||||
Brian Mock <brian@mockbrian.com>
|
||||
Carson Anderson <carson@betterservers.com>
|
||||
CHaBou <chabup@delean.fr>
|
||||
Christian Budde Christensen <budde377@gmail.com>
|
||||
Christopher Jeffrey <chjjeffrey@gmail.com>
|
||||
coderaiser <mnemonic.enemy@gmail.com>
|
||||
@@ -29,6 +34,7 @@ hiro-su <h.sugipon@gmail.com>
|
||||
Ian Lewis <ianlewis@google.com>
|
||||
imoses <ido@twiggle.com>
|
||||
InDieTasten <indietasten@gmail.com>
|
||||
irokas <akasidiari@inf.uth.gr>
|
||||
Jean Bruenn <himself@jeanbruenn.info>
|
||||
Jeremy Danyow <jedanyow@microsoft.com>
|
||||
Jörg Breitbart <jerch@rockborn.de>
|
||||
@@ -40,6 +46,7 @@ Martin Wang <jiahaow@ca.ibm.com>
|
||||
Michael Irwin <mikesir87@gmail.com>
|
||||
Mikko Karvonen <mikko.karvonen@arm.com>
|
||||
Nicolas Ramz <nicolas.ramz@gmail.com>
|
||||
Oleksandr Andriienko <oandriie@redhat.com>
|
||||
Paris Kasidiaris <pariskasidiaris@gmail.com>
|
||||
Paris Kasidiaris <paris@sourcelair.com>
|
||||
runarberg <runar@greenqloud.com>
|
||||
|
||||
@@ -36,6 +36,8 @@ Xterm.js is used in several world-class applications to provide great terminal e
|
||||
- [**RStudio**](https://www.rstudio.com/products/RStudio "RStudio"): RStudio is an integrated development environment (IDE) for R.
|
||||
- [**Terminal for Atom**](https://github.com/jsmecham/atom-terminal-tab): A simple terminal for the Atom text editor.
|
||||
- [**Eclipse Orion**](https://orionhub.org): A modern, open source software development environment that runs in the cloud. Code, deploy and run in the cloud.
|
||||
- [**Gravitational Teleport**](https://github.com/gravitational/teleport): Gravitational Teleport is a modern SSH server for remotely accessing clusters of Linux servers via SSH or HTTPS.
|
||||
- [**Hexlet**](https://en.hexlet.io): Practical programming courses (JavaScript, PHP, Unix, databases, functional programming). A steady path from the first line of code to the first job.
|
||||
|
||||
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.
|
||||
|
||||
@@ -133,6 +135,6 @@ To contribute either code, documentation or issues to xterm.js please read the [
|
||||
|
||||
If you contribute code to this project, you are implicitly allowing your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work.
|
||||
|
||||
Copyright (c) 2014-2016, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)
|
||||
Copyright (c) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)
|
||||
|
||||
Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xterm.js",
|
||||
"version": "2.7.0",
|
||||
"version": "2.8.1",
|
||||
"ignore": ["demo", "test", ".gitignore"],
|
||||
"main": [
|
||||
"dist/xterm.js",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<link rel="stylesheet" href="/build/xterm.css" />
|
||||
<link rel="stylesheet" href="/build/addons/fullscreen/fullscreen.css" />
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js"></script>
|
||||
<script src="/build/xterm.js" ></script>
|
||||
<script src="/build/addons/attach/attach.js" ></script>
|
||||
|
||||
Vendored
+22
@@ -41,6 +41,9 @@
|
||||
font-family: courier-new, courier, monospace;
|
||||
font-feature-settings: "liga" 0;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.terminal.focus,
|
||||
@@ -180,6 +183,25 @@
|
||||
left: -9999em;
|
||||
}
|
||||
|
||||
.terminal.enable-mouse-events {
|
||||
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.terminal .xterm-selection {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.terminal .xterm-selection div {
|
||||
position: absolute;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/*
|
||||
* Determine default colors for xterm.js
|
||||
*/
|
||||
|
||||
Vendored
+758
-118
File diff suppressed because it is too large
Load Diff
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -50,7 +50,7 @@ gulp.task('browserify', ['tsc'], function() {
|
||||
let browserifyOptions = {
|
||||
basedir: buildDir,
|
||||
debug: true,
|
||||
entries: [`../${outDir}/xterm.js`],
|
||||
entries: [`${outDir}/xterm.js`],
|
||||
standalone: 'Terminal',
|
||||
cache: {},
|
||||
packageCache: {}
|
||||
|
||||
Generated
+11892
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "xterm",
|
||||
"description": "Full xterm terminal, in your browser",
|
||||
"version": "2.7.0",
|
||||
"version": "2.8.1",
|
||||
"ignore": [
|
||||
"demo",
|
||||
"test",
|
||||
@@ -36,7 +36,7 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/chai": "^3.4.34",
|
||||
"@types/jsdom": "^2.0.30",
|
||||
"@types/jsdom": "^11.0.1",
|
||||
"@types/mocha": "^2.2.33",
|
||||
"@types/node": "^6.0.41",
|
||||
"browserify": "^13.1.0",
|
||||
@@ -54,7 +54,7 @@
|
||||
"gulp-sourcemaps": "1.9.1",
|
||||
"gulp-typescript": "^3.1.3",
|
||||
"jsdoc": "3.4.3",
|
||||
"jsdom": "^9.11.0",
|
||||
"jsdom": "^11.1.0",
|
||||
"merge-stream": "^1.0.1",
|
||||
"node-pty": "^0.4.1",
|
||||
"nodemon": "1.10.2",
|
||||
|
||||
+6
-1
@@ -56,8 +56,13 @@ export class InputHandler implements IInputHandler {
|
||||
this._terminal.x = 0;
|
||||
this._terminal.y++;
|
||||
if (this._terminal.y > this._terminal.scrollBottom) {
|
||||
// Insert a new line, scroll and mark as a wrapped line
|
||||
this._terminal.y--;
|
||||
this._terminal.scroll();
|
||||
this._terminal.scroll(true);
|
||||
} else {
|
||||
// The line already exists (eg. the initial viewport), mark it as a
|
||||
// wrapped line
|
||||
this._terminal.lines.get(this._terminal.y).isWrapped = true;
|
||||
}
|
||||
} else {
|
||||
if (ch_width === 2) // FIXME: check for xterm behavior
|
||||
|
||||
+12
-7
@@ -17,6 +17,7 @@ class TestLinkifier extends Linkifier {
|
||||
}
|
||||
|
||||
describe('Linkifier', () => {
|
||||
let dom: jsdom.JSDOM;
|
||||
let window: Window;
|
||||
let document: Document;
|
||||
|
||||
@@ -24,13 +25,11 @@ describe('Linkifier', () => {
|
||||
let rows: HTMLElement[];
|
||||
let linkifier: TestLinkifier;
|
||||
|
||||
beforeEach(done => {
|
||||
jsdom.env('', (err, w) => {
|
||||
window = w;
|
||||
document = window.document;
|
||||
linkifier = new TestLinkifier();
|
||||
done();
|
||||
});
|
||||
beforeEach(() => {
|
||||
dom = new jsdom.JSDOM('');
|
||||
window = dom.window;
|
||||
document = window.document;
|
||||
linkifier = new TestLinkifier();
|
||||
});
|
||||
|
||||
function addRow(html: string) {
|
||||
@@ -119,6 +118,12 @@ describe('Linkifier', () => {
|
||||
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);
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validationCallback', () => {
|
||||
|
||||
+24
-10
@@ -200,7 +200,7 @@ export class Linkifier {
|
||||
* Linkifies a row given a specific handler.
|
||||
* @param {HTMLElement} row The row to linkify.
|
||||
* @param {LinkMatcher} matcher The link matcher for this line.
|
||||
* @return The link element if it was added, otherwise undefined.
|
||||
* @return The link element(s) that were added.
|
||||
*/
|
||||
private _doLinkifyRow(row: HTMLElement, matcher: LinkMatcher): HTMLElement[] {
|
||||
// Iterate over nodes as we want to consider text nodes
|
||||
@@ -235,8 +235,21 @@ export class Linkifier {
|
||||
element.innerHTML = '';
|
||||
element.appendChild(linkElement);
|
||||
}
|
||||
} else if (node.childNodes.length > 1) {
|
||||
// Matches part of string in an element with multiple child nodes
|
||||
for (let j = 0; j < node.childNodes.length; j++) {
|
||||
const childNode = node.childNodes[j];
|
||||
const childSearchIndex = childNode.textContent.indexOf(uri);
|
||||
if (childSearchIndex !== -1) {
|
||||
// Match found in currentNode
|
||||
this._replaceNodeSubstringWithNode(childNode, linkElement, uri, childSearchIndex);
|
||||
// Don't need to count nodesAdded by replacing the node as this
|
||||
// is a child node, not a top-level node.
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Matches part of string
|
||||
// Matches part of string in a single text node
|
||||
const nodesAdded = this._replaceNodeSubstringWithNode(node, linkElement, uri, searchIndex);
|
||||
// No need to consider the new nodes
|
||||
i += nodesAdded;
|
||||
@@ -308,25 +321,26 @@ export class Linkifier {
|
||||
* @return The number of nodes to skip when searching for the next uri.
|
||||
*/
|
||||
private _replaceNodeSubstringWithNode(targetNode: Node, newNode: Node, substring: string, substringIndex: number): number {
|
||||
let node = targetNode;
|
||||
if (node.nodeType !== 3/*Node.TEXT_NODE*/) {
|
||||
node = node.childNodes[0];
|
||||
// If the targetNode is a non-text node with a single child, make the child
|
||||
// the new targetNode.
|
||||
if (targetNode.childNodes.length === 1) {
|
||||
targetNode = targetNode.childNodes[0];
|
||||
}
|
||||
|
||||
// The targetNode will be either a text node or a <span>. The text node
|
||||
// (targetNode or its only-child) needs to be replaced with newNode plus new
|
||||
// text nodes potentially on either side.
|
||||
if (node.childNodes.length === 0 && node.nodeType !== 3/*Node.TEXT_NODE*/) {
|
||||
if (targetNode.nodeType !== 3/*Node.TEXT_NODE*/) {
|
||||
throw new Error('targetNode must be a text node or only contain a single text node');
|
||||
}
|
||||
|
||||
const fullText = node.textContent;
|
||||
const fullText = targetNode.textContent;
|
||||
|
||||
if (substringIndex === 0) {
|
||||
// Replace with <newNode><textnode>
|
||||
const rightText = fullText.substring(substring.length);
|
||||
const rightTextNode = this._document.createTextNode(rightText);
|
||||
this._replaceNode(node, newNode, rightTextNode);
|
||||
this._replaceNode(targetNode, newNode, rightTextNode);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -334,7 +348,7 @@ export class Linkifier {
|
||||
// Replace with <textnode><newNode>
|
||||
const leftText = fullText.substring(0, substringIndex);
|
||||
const leftTextNode = this._document.createTextNode(leftText);
|
||||
this._replaceNode(node, leftTextNode, newNode);
|
||||
this._replaceNode(targetNode, leftTextNode, newNode);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -343,7 +357,7 @@ export class Linkifier {
|
||||
const leftTextNode = this._document.createTextNode(leftText);
|
||||
const rightText = fullText.substring(substringIndex + substring.length);
|
||||
const rightTextNode = this._document.createTextNode(rightText);
|
||||
this._replaceNode(node, leftTextNode, newNode, rightTextNode);
|
||||
this._replaceNode(targetNode, leftTextNode, newNode, rightTextNode);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -200,7 +200,8 @@ export class Renderer {
|
||||
}
|
||||
currentElement = this._spanElementObjectPool.acquire();
|
||||
if (data === -1) {
|
||||
currentElement.classList.add('reverse-video', 'terminal-cursor');
|
||||
currentElement.classList.add('reverse-video');
|
||||
currentElement.classList.add('terminal-cursor');
|
||||
} else {
|
||||
let bg = data & 0x1ff;
|
||||
let fg = (data >> 9) & 0x1ff;
|
||||
@@ -353,9 +354,8 @@ export class Renderer {
|
||||
const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
|
||||
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol));
|
||||
// Draw middle rows
|
||||
for (let i = viewportCappedStartRow + 1; i < viewportCappedEndRow; i++) {
|
||||
documentFragment.appendChild(this._createSelectionElement(i, 0, this._terminal.cols));
|
||||
}
|
||||
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
|
||||
@@ -371,9 +371,9 @@ export class Renderer {
|
||||
* @param colStart The start column.
|
||||
* @param colEnd The end columns.
|
||||
*/
|
||||
private _createSelectionElement(row: number, colStart: number, colEnd: number): HTMLElement {
|
||||
private _createSelectionElement(row: number, colStart: number, colEnd: number, rowCount: number = 1): HTMLElement {
|
||||
const element = document.createElement('div');
|
||||
element.style.height = `${this._terminal.charMeasure.height}px`;
|
||||
element.style.height = `${rowCount * this._terminal.charMeasure.height}px`;
|
||||
element.style.top = `${row * this._terminal.charMeasure.height}px`;
|
||||
element.style.left = `${colStart * this._terminal.charMeasure.width}px`;
|
||||
element.style.width = `${this._terminal.charMeasure.width * (colEnd - colStart)}px`;
|
||||
|
||||
@@ -31,6 +31,7 @@ class TestSelectionManager extends SelectionManager {
|
||||
}
|
||||
|
||||
describe('SelectionManager', () => {
|
||||
let dom: jsdom.JSDOM;
|
||||
let window: Window;
|
||||
let document: Document;
|
||||
|
||||
@@ -39,15 +40,13 @@ describe('SelectionManager', () => {
|
||||
let rowContainer: HTMLElement;
|
||||
let selectionManager: TestSelectionManager;
|
||||
|
||||
beforeEach(done => {
|
||||
jsdom.env('', (err, w) => {
|
||||
window = w;
|
||||
document = window.document;
|
||||
buffer = new CircularList<any>(100);
|
||||
terminal = <any>{ cols: 80, rows: 2 };
|
||||
selectionManager = new TestSelectionManager(terminal, buffer, rowContainer, null);
|
||||
done();
|
||||
});
|
||||
beforeEach(() => {
|
||||
dom = new jsdom.JSDOM('');
|
||||
window = dom.window;
|
||||
document = window.document;
|
||||
buffer = new CircularList<any>(100);
|
||||
terminal = <any>{ cols: 80, rows: 2 };
|
||||
selectionManager = new TestSelectionManager(terminal, buffer, rowContainer, null);
|
||||
});
|
||||
|
||||
function stringToRow(text: string): [number, string, number][] {
|
||||
@@ -142,6 +141,41 @@ describe('SelectionManager', () => {
|
||||
selectionManager.selectWordAt([14, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'foo');
|
||||
});
|
||||
it('should select up to non-path characters that are commonly adjacent to paths', () => {
|
||||
buffer.push(stringToRow('(cd)[ef]{gh}\'ij"'));
|
||||
selectionManager.selectWordAt([0, 0]);
|
||||
assert.equal(selectionManager.selectionText, '(cd');
|
||||
selectionManager.selectWordAt([1, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'cd');
|
||||
selectionManager.selectWordAt([2, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'cd');
|
||||
selectionManager.selectWordAt([3, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'cd)');
|
||||
selectionManager.selectWordAt([4, 0]);
|
||||
assert.equal(selectionManager.selectionText, '[ef');
|
||||
selectionManager.selectWordAt([5, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'ef');
|
||||
selectionManager.selectWordAt([6, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'ef');
|
||||
selectionManager.selectWordAt([7, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'ef]');
|
||||
selectionManager.selectWordAt([8, 0]);
|
||||
assert.equal(selectionManager.selectionText, '{gh');
|
||||
selectionManager.selectWordAt([9, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'gh');
|
||||
selectionManager.selectWordAt([10, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'gh');
|
||||
selectionManager.selectWordAt([11, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'gh}');
|
||||
selectionManager.selectWordAt([12, 0]);
|
||||
assert.equal(selectionManager.selectionText, '\'ij');
|
||||
selectionManager.selectWordAt([13, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'ij');
|
||||
selectionManager.selectWordAt([14, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'ij');
|
||||
selectionManager.selectWordAt([15, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'ij"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('_selectLineAt', () => {
|
||||
@@ -166,4 +200,20 @@ describe('SelectionManager', () => {
|
||||
assert.equal(selectionManager.selectionText, '1\n2\n3\n4\n5');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasSelection', () => {
|
||||
it('should return whether there is a selection', () => {
|
||||
selectionManager.model.selectionStart = [0, 0];
|
||||
selectionManager.model.selectionStartLength = 0;
|
||||
assert.equal(selectionManager.hasSelection, false);
|
||||
selectionManager.model.selectionEnd = [0, 0];
|
||||
assert.equal(selectionManager.hasSelection, false);
|
||||
selectionManager.model.selectionEnd = [1, 0];
|
||||
assert.equal(selectionManager.hasSelection, true);
|
||||
selectionManager.model.selectionEnd = [0, 1];
|
||||
assert.equal(selectionManager.hasSelection, true);
|
||||
selectionManager.model.selectionEnd = [1, 1];
|
||||
assert.equal(selectionManager.hasSelection, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+110
-23
@@ -2,10 +2,11 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import * as Mouse from './utils/Mouse';
|
||||
import * as Browser from './utils/Browser';
|
||||
import { CharMeasure } from './utils/CharMeasure';
|
||||
import { CircularList } from './utils/CircularList';
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
import * as Mouse from './utils/Mouse';
|
||||
import { ITerminal } from './Interfaces';
|
||||
import { SelectionModel } from './SelectionModel';
|
||||
|
||||
@@ -37,6 +38,12 @@ const CLEAR_MOUSE_DOWN_TIME = 400;
|
||||
*/
|
||||
const CLEAR_MOUSE_DISTANCE = 10;
|
||||
|
||||
/**
|
||||
* A string containing all characters that are considered word separated by the
|
||||
* double click to select work logic.
|
||||
*/
|
||||
const WORD_SEPARATORS = ' ()[]{}\'"';
|
||||
|
||||
// TODO: Move these constants elsewhere, they belong in a buffer or buffer
|
||||
// data/line class.
|
||||
const LINE_DATA_CHAR_INDEX = 1;
|
||||
@@ -45,6 +52,23 @@ const LINE_DATA_WIDTH_INDEX = 2;
|
||||
const NON_BREAKING_SPACE_CHAR = String.fromCharCode(160);
|
||||
const ALL_NON_BREAKING_SPACE_REGEX = new RegExp(NON_BREAKING_SPACE_CHAR, 'g');
|
||||
|
||||
/**
|
||||
* Represents a position of a word on a line.
|
||||
*/
|
||||
interface IWordPosition {
|
||||
start: number;
|
||||
length: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A selection mode, this drives how the selection behaves on mouse move.
|
||||
*/
|
||||
enum SelectionMode {
|
||||
NORMAL,
|
||||
WORD,
|
||||
LINE
|
||||
}
|
||||
|
||||
/**
|
||||
* A class that manages the selection of the terminal. With help from
|
||||
* SelectionModel, SelectionManager handles with all logic associated with
|
||||
@@ -80,9 +104,9 @@ export class SelectionManager extends EventEmitter {
|
||||
private _clickCount: number;
|
||||
|
||||
/**
|
||||
* Whether line select mode is active, this occurs after a triple click.
|
||||
* The current selection mode.
|
||||
*/
|
||||
private _isLineSelectModeActive: boolean;
|
||||
private _activeSelectionMode: SelectionMode;
|
||||
|
||||
/**
|
||||
* A setInterval timer that is active while the mouse is down whose callback
|
||||
@@ -112,7 +136,7 @@ export class SelectionManager extends EventEmitter {
|
||||
|
||||
this._model = new SelectionModel(_terminal);
|
||||
this._lastMouseDownTime = 0;
|
||||
this._isLineSelectModeActive = false;
|
||||
this._activeSelectionMode = SelectionMode.NORMAL;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,13 +178,19 @@ export class SelectionManager extends EventEmitter {
|
||||
*/
|
||||
public setBuffer(buffer: CircularList<any>): void {
|
||||
this._buffer = buffer;
|
||||
this.clearSelection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether there is an active text selection.
|
||||
*/
|
||||
public get hasSelection(): boolean {
|
||||
return !!this._model.finalSelectionStart && !!this._model.finalSelectionEnd;
|
||||
const start = this._model.finalSelectionStart;
|
||||
const end = this._model.finalSelectionEnd;
|
||||
if (!start || !end) {
|
||||
return false;
|
||||
}
|
||||
return start[0] !== end[0] || start[1] !== end[1];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -180,19 +210,31 @@ export class SelectionManager extends EventEmitter {
|
||||
|
||||
// Get middle rows
|
||||
for (let i = start[1] + 1; i <= end[1] - 1; i++) {
|
||||
result.push(this._translateBufferLineToString(this._buffer.get(i), true));
|
||||
const bufferLine = this._buffer.get(i);
|
||||
const lineText = this._translateBufferLineToString(bufferLine, true);
|
||||
if (bufferLine.isWrapped) {
|
||||
result[result.length - 1] += lineText;
|
||||
} else {
|
||||
result.push(lineText);
|
||||
}
|
||||
}
|
||||
|
||||
// Get final row
|
||||
if (start[1] !== end[1]) {
|
||||
result.push(this._translateBufferLineToString(this._buffer.get(end[1]), true, 0, end[0]));
|
||||
const bufferLine = this._buffer.get(end[1]);
|
||||
const lineText = this._translateBufferLineToString(bufferLine, true, 0, end[0]);
|
||||
if (bufferLine.isWrapped) {
|
||||
result[result.length - 1] += lineText;
|
||||
} else {
|
||||
result.push(lineText);
|
||||
}
|
||||
}
|
||||
|
||||
// Format string by replacing non-breaking space chars with regular spaces
|
||||
// and joining the array into a multi-line string.
|
||||
const formattedResult = result.map(line => {
|
||||
return line.replace(ALL_NON_BREAKING_SPACE_REGEX, ' ');
|
||||
}).join('\n');
|
||||
}).join(Browser.isMSWindows ? '\r\n' : '\n');
|
||||
|
||||
return formattedResult;
|
||||
}
|
||||
@@ -257,11 +299,23 @@ export class SelectionManager extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Queues a refresh, redrawing the selection on the next opportunity.
|
||||
* @param isNewSelection Whether the selection should be registered as a new
|
||||
* selection on Linux.
|
||||
*/
|
||||
public refresh(): void {
|
||||
public refresh(isNewSelection?: boolean): void {
|
||||
// Queue the refresh for the renderer
|
||||
if (!this._refreshAnimationFrame) {
|
||||
this._refreshAnimationFrame = window.requestAnimationFrame(() => this._refresh());
|
||||
}
|
||||
|
||||
// If the platform is Linux and the refresh call comes from a mouse event,
|
||||
// we need to update the selection for middle click to paste selection.
|
||||
if (Browser.isLinux && isNewSelection) {
|
||||
const selectionText = this.selectionText;
|
||||
if (selectionText.length) {
|
||||
this.emit('newselection', this.selectionText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -297,7 +351,7 @@ export class SelectionManager extends EventEmitter {
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _getMouseBufferCoords(event: MouseEvent): [number, number] {
|
||||
const coords = Mouse.getCoords(event, this._rowContainer, this._charMeasure, this._terminal.cols, this._terminal.rows);
|
||||
const coords = Mouse.getCoords(event, this._rowContainer, this._charMeasure, this._terminal.cols, this._terminal.rows, true);
|
||||
// Convert to 0-based
|
||||
coords[0]--;
|
||||
coords[1]--;
|
||||
@@ -336,6 +390,9 @@ export class SelectionManager extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
// Tell the browser not to start a regular selection
|
||||
event.preventDefault();
|
||||
|
||||
// Reset drag scroll state
|
||||
this._dragScrollAmount = 0;
|
||||
|
||||
@@ -354,7 +411,7 @@ export class SelectionManager extends EventEmitter {
|
||||
}
|
||||
|
||||
this._addMouseDownListeners();
|
||||
this.refresh();
|
||||
this.refresh(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -396,7 +453,7 @@ export class SelectionManager extends EventEmitter {
|
||||
private _onSingleClick(event: MouseEvent): void {
|
||||
this._model.selectionStartLength = 0;
|
||||
this._model.isSelectAllActive = false;
|
||||
this._isLineSelectModeActive = false;
|
||||
this._activeSelectionMode = SelectionMode.NORMAL;
|
||||
this._model.selectionStart = this._getMouseBufferCoords(event);
|
||||
if (this._model.selectionStart) {
|
||||
this._model.selectionEnd = null;
|
||||
@@ -416,6 +473,7 @@ export class SelectionManager extends EventEmitter {
|
||||
private _onDoubleClick(event: MouseEvent): void {
|
||||
const coords = this._getMouseBufferCoords(event);
|
||||
if (coords) {
|
||||
this._activeSelectionMode = SelectionMode.WORD;
|
||||
this._selectWordAt(coords);
|
||||
}
|
||||
}
|
||||
@@ -428,7 +486,7 @@ export class SelectionManager extends EventEmitter {
|
||||
private _onTripleClick(event: MouseEvent): void {
|
||||
const coords = this._getMouseBufferCoords(event);
|
||||
if (coords) {
|
||||
this._isLineSelectModeActive = true;
|
||||
this._activeSelectionMode = SelectionMode.LINE;
|
||||
this._selectLineAt(coords[1]);
|
||||
}
|
||||
}
|
||||
@@ -473,12 +531,14 @@ export class SelectionManager extends EventEmitter {
|
||||
this._model.selectionEnd = this._getMouseBufferCoords(event);
|
||||
|
||||
// Select the entire line if line select mode is active.
|
||||
if (this._isLineSelectModeActive) {
|
||||
if (this._activeSelectionMode === SelectionMode.LINE) {
|
||||
if (this._model.selectionEnd[1] < this._model.selectionStart[1]) {
|
||||
this._model.selectionEnd[0] = 0;
|
||||
} else {
|
||||
this._model.selectionEnd[0] = this._terminal.cols;
|
||||
}
|
||||
} else if (this._activeSelectionMode === SelectionMode.WORD) {
|
||||
this._selectToWordAt(this._model.selectionEnd);
|
||||
}
|
||||
|
||||
// Determine the amount of scrolling that will happen.
|
||||
@@ -506,7 +566,7 @@ export class SelectionManager extends EventEmitter {
|
||||
if (!previousSelectionEnd ||
|
||||
previousSelectionEnd[0] !== this._model.selectionEnd[0] ||
|
||||
previousSelectionEnd[1] !== this._model.selectionEnd[1]) {
|
||||
this.refresh();
|
||||
this.refresh(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -552,11 +612,10 @@ export class SelectionManager extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the word at the coordinates specified. Words are defined as all
|
||||
* non-whitespace characters.
|
||||
* Gets positional information for the word at the coordinated specified.
|
||||
* @param coords The coordinates to get the word at.
|
||||
*/
|
||||
protected _selectWordAt(coords: [number, number]): void {
|
||||
private _getWordAt(coords: [number, number]): IWordPosition {
|
||||
const bufferLine = this._buffer.get(coords[1]);
|
||||
const line = this._translateBufferLineToString(bufferLine, false);
|
||||
|
||||
@@ -595,7 +654,7 @@ export class SelectionManager extends EventEmitter {
|
||||
endCol++;
|
||||
}
|
||||
// Expand the string in both directions until a space is hit
|
||||
while (startIndex > 0 && line.charAt(startIndex - 1) !== ' ') {
|
||||
while (startIndex > 0 && !this._isCharWordSeparator(line.charAt(startIndex - 1))) {
|
||||
if (bufferLine[startCol - 1][LINE_DATA_WIDTH_INDEX] === 0) {
|
||||
// If the next character is a wide char, record it and skip the column
|
||||
leftWideCharCount++;
|
||||
@@ -604,7 +663,7 @@ export class SelectionManager extends EventEmitter {
|
||||
startIndex--;
|
||||
startCol--;
|
||||
}
|
||||
while (endIndex + 1 < line.length && line.charAt(endIndex + 1) !== ' ') {
|
||||
while (endIndex + 1 < line.length && !this._isCharWordSeparator(line.charAt(endIndex + 1))) {
|
||||
if (bufferLine[endCol + 1][LINE_DATA_WIDTH_INDEX] === 2) {
|
||||
// If the next character is a wide char, record it and skip the column
|
||||
rightWideCharCount++;
|
||||
@@ -615,9 +674,37 @@ export class SelectionManager extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
// Record the resulting selection
|
||||
this._model.selectionStart = [startIndex + charOffset - leftWideCharCount, coords[1]];
|
||||
this._model.selectionStartLength = Math.min(endIndex - startIndex + leftWideCharCount + rightWideCharCount + 1/*include endIndex char*/, this._terminal.cols);
|
||||
const start = startIndex + charOffset - leftWideCharCount;
|
||||
const length = Math.min(endIndex - startIndex + leftWideCharCount + rightWideCharCount + 1/*include endIndex char*/, this._terminal.cols);
|
||||
return {start, length};
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the word at the coordinates specified.
|
||||
* @param coords The coordinates to get the word at.
|
||||
*/
|
||||
protected _selectWordAt(coords: [number, number]): void {
|
||||
const wordPosition = this._getWordAt(coords);
|
||||
this._model.selectionStart = [wordPosition.start, coords[1]];
|
||||
this._model.selectionStartLength = wordPosition.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the selection end to the word at the coordinated specified.
|
||||
* @param coords The coordinates to get the word at.
|
||||
*/
|
||||
private _selectToWordAt(coords: [number, number]): void {
|
||||
const wordPosition = this._getWordAt(coords);
|
||||
this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : (wordPosition.start + wordPosition.length), coords[1]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether the character is considered a word separator by the select
|
||||
* word logic.
|
||||
* @param char The character to check.
|
||||
*/
|
||||
private _isCharWordSeparator(char: string): boolean {
|
||||
return WORD_SEPARATORS.indexOf(char) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,8 +11,6 @@ class TestSelectionModel extends SelectionModel {
|
||||
) {
|
||||
super(terminal);
|
||||
}
|
||||
|
||||
public areSelectionValuesReversed(): boolean { return this._areSelectionValuesReversed(); }
|
||||
}
|
||||
|
||||
describe('SelectionManager', () => {
|
||||
@@ -39,7 +37,7 @@ describe('SelectionManager', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('_areSelectionValuesReversed', () => {
|
||||
describe('areSelectionValuesReversed', () => {
|
||||
it('should return true when the selection end is before selection start', () => {
|
||||
model.selectionStart = [1, 0];
|
||||
model.selectionEnd = [0, 0];
|
||||
|
||||
@@ -59,7 +59,7 @@ export class SelectionModel {
|
||||
return this.selectionStart;
|
||||
}
|
||||
|
||||
return this._areSelectionValuesReversed() ? this.selectionEnd : this.selectionStart;
|
||||
return this.areSelectionValuesReversed() ? this.selectionEnd : this.selectionStart;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,7 +76,7 @@ export class SelectionModel {
|
||||
}
|
||||
|
||||
// Use the selection start if the end doesn't exist or they're reversed
|
||||
if (!this.selectionEnd || this._areSelectionValuesReversed()) {
|
||||
if (!this.selectionEnd || this.areSelectionValuesReversed()) {
|
||||
return [this.selectionStart[0] + this.selectionStartLength, this.selectionStart[1]];
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ export class SelectionModel {
|
||||
/**
|
||||
* Returns whether the selection start and end are reversed.
|
||||
*/
|
||||
protected _areSelectionValuesReversed(): boolean {
|
||||
public areSelectionValuesReversed(): boolean {
|
||||
const start = this.selectionStart;
|
||||
const end = this.selectionEnd;
|
||||
return start[1] > end[1] || (start[1] === end[1] && start[0] > end[0]);
|
||||
|
||||
@@ -13,6 +13,7 @@ export class Viewport {
|
||||
private currentRowHeight: number;
|
||||
private lastRecordedBufferLength: number;
|
||||
private lastRecordedViewportHeight: number;
|
||||
private lastTouchY: number;
|
||||
|
||||
/**
|
||||
* Creates a new Viewport.
|
||||
@@ -121,4 +122,26 @@ export class Viewport {
|
||||
// Prevent the page from scrolling when the terminal scrolls
|
||||
ev.preventDefault();
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the touchstart event, recording the touch occurred.
|
||||
* @param ev The touch event.
|
||||
*/
|
||||
public onTouchStart(ev: TouchEvent) {
|
||||
this.lastTouchY = ev.touches[0].pageY;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the touchmove event, scrolling the viewport if the position shifted.
|
||||
* @param ev The touch event.
|
||||
*/
|
||||
public onTouchMove(ev: TouchEvent) {
|
||||
let deltaY = this.lastTouchY - ev.touches[0].pageY;
|
||||
this.lastTouchY = ev.touches[0].pageY;
|
||||
if (deltaY === 0) {
|
||||
return;
|
||||
}
|
||||
this.viewportElement.scrollTop += deltaY;
|
||||
ev.preventDefault();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,6 +9,6 @@ describe('evaluatePastedTextProcessing', function () {
|
||||
windowsProcessedText = Clipboard.prepareTextForTerminal(pastedText, true);
|
||||
|
||||
assert.equal(processedText, 'foo\r\nbar\r\n');
|
||||
assert.equal(windowsProcessedText, 'foo\nbar\n');
|
||||
assert.equal(windowsProcessedText, 'foo\rbar\r');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@ declare var window: IWindow;
|
||||
*/
|
||||
export function prepareTextForTerminal(text: string, isMSWindows: boolean): string {
|
||||
if (isMSWindows) {
|
||||
return text.replace(/\r?\n/g, '\n');
|
||||
return text.replace(/\r?\n/g, '\r');
|
||||
}
|
||||
return text;
|
||||
}
|
||||
@@ -75,12 +75,11 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind to right-click event and allow right-click copy and paste.
|
||||
* @param ev The original right click event to be handled
|
||||
* @param term The terminal on which to apply the handled paste event
|
||||
* @param selectionManager The terminal's selection manager.
|
||||
* Moves the textarea under the mouse cursor and focuses it.
|
||||
* @param ev The original right click event to be handled.
|
||||
* @param textarea The terminal's textarea.
|
||||
*/
|
||||
export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager) {
|
||||
export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextAreaElement) {
|
||||
// Bring textarea at the cursor position
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.width = '20px';
|
||||
@@ -89,10 +88,7 @@ export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement,
|
||||
textarea.style.top = (ev.clientY - 10) + 'px';
|
||||
textarea.style.zIndex = '1000';
|
||||
|
||||
// Get textarea ready to copy from the context menu
|
||||
textarea.value = selectionManager.selectionText;
|
||||
textarea.focus();
|
||||
textarea.select();
|
||||
|
||||
// Reset the terminal textarea's styling
|
||||
setTimeout(function () {
|
||||
@@ -104,3 +100,17 @@ export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement,
|
||||
textarea.style.zIndex = null;
|
||||
}, 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind to right-click event and allow right-click copy and paste.
|
||||
* @param ev The original right click event to be handled.
|
||||
* @param textarea The terminal's textarea.
|
||||
* @param selectionManager The terminal's selection manager.
|
||||
*/
|
||||
export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager) {
|
||||
moveTextAreaUnderMouseCursor(ev, textarea);
|
||||
|
||||
// Get textarea ready to copy from the context menu
|
||||
textarea.value = selectionManager.selectionText;
|
||||
textarea.select();
|
||||
}
|
||||
|
||||
+25
-5
@@ -55,6 +55,11 @@ describe('xterm.js', function() {
|
||||
it('should throw when setting a non-existant option', function() {
|
||||
assert.throws(xterm.setOption.bind(xterm, 'fake', true));
|
||||
});
|
||||
it('should not allow scrollback less than number of rows', function() {
|
||||
let setOptionCall = xterm.setOption.bind(xterm, 'scrollback', xterm.rows - 1);
|
||||
|
||||
assert.equal(setOptionCall(), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clear', function() {
|
||||
@@ -371,12 +376,17 @@ describe('xterm.js', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('attachCustomEventHandler', function () {
|
||||
describe('attachCustomKeyEventHandler', function () {
|
||||
var evKeyDown = {
|
||||
preventDefault: function() {},
|
||||
stopPropagation: function() {},
|
||||
type: 'keydown'
|
||||
}
|
||||
var evKeyPress = {
|
||||
preventDefault: function() {},
|
||||
stopPropagation: function() {},
|
||||
type: 'keypress'
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
xterm.handler = function() {};
|
||||
@@ -387,29 +397,39 @@ describe('xterm.js', function() {
|
||||
bind: function() {
|
||||
return function () { return true; }
|
||||
}
|
||||
},
|
||||
keypress: {
|
||||
bind: function() {
|
||||
return function () { return true; }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should process the keydown event based on what the handler returns', function () {
|
||||
it('should process the keydown/keypress event based on what the handler returns', function () {
|
||||
assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), true);
|
||||
xterm.attachCustomKeydownHandler(function (ev) {
|
||||
assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), true);
|
||||
xterm.attachCustomKeyEventHandler(function (ev) {
|
||||
return ev.keyCode === 77;
|
||||
});
|
||||
assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), true);
|
||||
xterm.attachCustomKeydownHandler(function (ev) {
|
||||
assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), true);
|
||||
xterm.attachCustomKeyEventHandler(function (ev) {
|
||||
return ev.keyCode !== 77;
|
||||
});
|
||||
assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), false);
|
||||
assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), false);
|
||||
});
|
||||
|
||||
it('should alive after reset(ESC c Full Reset (RIS))', function () {
|
||||
xterm.attachCustomKeydownHandler(function (ev) {
|
||||
xterm.attachCustomKeyEventHandler(function (ev) {
|
||||
return ev.keyCode !== 77;
|
||||
});
|
||||
assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), false);
|
||||
assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), false);
|
||||
xterm.reset();
|
||||
assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), false);
|
||||
assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -20,3 +20,4 @@ export const isMac = contains(['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'], pla
|
||||
export const isIpad = platform === 'iPad';
|
||||
export const isIphone = platform === 'iPhone';
|
||||
export const isMSWindows = contains(['Windows', 'Win16', 'Win32', 'WinCE'], platform);
|
||||
export const isLinux = platform.indexOf('Linux') >= 0;
|
||||
|
||||
@@ -7,21 +7,19 @@ import { ICharMeasure, ITerminal } from '../Interfaces';
|
||||
import { CharMeasure } from './CharMeasure';
|
||||
|
||||
describe('CharMeasure', () => {
|
||||
let dom: jsdom.JSDOM;
|
||||
let window: Window;
|
||||
let document: Document;
|
||||
|
||||
let container: HTMLElement;
|
||||
let charMeasure: ICharMeasure;
|
||||
|
||||
beforeEach(done => {
|
||||
jsdom.env('', (err, w) => {
|
||||
window = w;
|
||||
document = window.document;
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
charMeasure = new CharMeasure(document, container);
|
||||
done();
|
||||
});
|
||||
beforeEach(() => {
|
||||
dom = new jsdom.JSDOM('');
|
||||
window = dom.window;
|
||||
document = window.document;
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
charMeasure = new CharMeasure(document, container);
|
||||
});
|
||||
|
||||
describe('measure', () => {
|
||||
|
||||
+8
-3
@@ -30,12 +30,17 @@ export function getCoordsRelativeToElement(event: MouseEvent, element: HTMLEleme
|
||||
* @param event The mouse event.
|
||||
* @param rowContainer The terminal's row container.
|
||||
* @param charMeasure The char measure object used to determine character sizes.
|
||||
* @param colCount The number of columns in the terminal.
|
||||
* @param rowCount The number of rows n the terminal.
|
||||
* @param isSelection Whether the request is for the selection or not. This will
|
||||
* apply an offset to the x value such that the left half of the cell will
|
||||
* select that cell and the right half will select the next cell.
|
||||
*/
|
||||
export function getCoords(event: MouseEvent, rowContainer: HTMLElement, charMeasure: CharMeasure, colCount: number, rowCount: number): [number, number] {
|
||||
export function getCoords(event: MouseEvent, rowContainer: HTMLElement, charMeasure: CharMeasure, colCount: number, rowCount: number, isSelection?: boolean): [number, number] {
|
||||
const coords = getCoordsRelativeToElement(event, rowContainer);
|
||||
|
||||
// Convert to cols/rows
|
||||
coords[0] = Math.ceil(coords[0] / charMeasure.width);
|
||||
// Convert to cols/rows.
|
||||
coords[0] = Math.ceil((coords[0] + (isSelection ? charMeasure.width / 2 : 0)) / charMeasure.width);
|
||||
coords[1] = Math.ceil(coords[1] / charMeasure.height);
|
||||
|
||||
// Ensure coordinates are within the terminal viewport.
|
||||
|
||||
+4
-1
@@ -192,11 +192,14 @@
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.terminal .xterm-selection div {
|
||||
position: absolute;
|
||||
background-color: #555;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
+115
-41
@@ -13,7 +13,7 @@
|
||||
import { CompositionHelper } from './CompositionHelper';
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
import { Viewport } from './Viewport';
|
||||
import { rightClickHandler, pasteHandler, copyHandler } from './handlers/Clipboard';
|
||||
import { rightClickHandler, moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from './handlers/Clipboard';
|
||||
import { CircularList } from './utils/CircularList';
|
||||
import { C0 } from './EscapeSequences';
|
||||
import { InputHandler } from './InputHandler';
|
||||
@@ -167,7 +167,7 @@ function Terminal(options) {
|
||||
this.queue = '';
|
||||
this.scrollTop = 0;
|
||||
this.scrollBottom = this.rows - 1;
|
||||
this.customKeydownHandler = null;
|
||||
this.customKeyEventHandler = null;
|
||||
this.cursorBlinkInterval = null;
|
||||
|
||||
// modes
|
||||
@@ -419,6 +419,15 @@ Terminal.prototype.setOption = function(key, value) {
|
||||
}
|
||||
switch (key) {
|
||||
case 'scrollback':
|
||||
if (value < this.rows) {
|
||||
let msg = 'Setting the scrollback value less than the number of rows ';
|
||||
|
||||
msg += `(${this.rows}) is not allowed.`;
|
||||
|
||||
console.warn(msg);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.options[key] !== value) {
|
||||
if (this.lines.length > value) {
|
||||
const amountToTrim = this.lines.length - value;
|
||||
@@ -537,9 +546,11 @@ Terminal.prototype.initGlobal = function() {
|
||||
on(this.textarea, 'paste', pasteHandlerWrapper);
|
||||
on(this.element, 'paste', pasteHandlerWrapper);
|
||||
|
||||
// Handle right click context menus
|
||||
if (term.browser.isFirefox) {
|
||||
// Firefox doesn't appear to fire the contextmenu event on right click
|
||||
on(this.element, 'mousedown', event => {
|
||||
if (ev.button == 2) {
|
||||
if (event.button == 2) {
|
||||
rightClickHandler(event, this.textarea, this.selectionManager);
|
||||
}
|
||||
});
|
||||
@@ -548,6 +559,19 @@ Terminal.prototype.initGlobal = function() {
|
||||
rightClickHandler(event, this.textarea, this.selectionManager);
|
||||
});
|
||||
}
|
||||
|
||||
// Move the textarea under the cursor when middle clicking on Linux to ensure
|
||||
// middle click to paste selection works. This only appears to work in Chrome
|
||||
// at the time is writing.
|
||||
if (term.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 => {
|
||||
if (event.button === 1) {
|
||||
moveTextAreaUnderMouseCursor(event, this.textarea, this.selectionManager);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -637,7 +661,6 @@ Terminal.prototype.open = function(parent, focus) {
|
||||
this.element.classList.add('xterm-theme-' + this.theme);
|
||||
this.setCursorBlinking(this.options.cursorBlink);
|
||||
|
||||
this.element.style.height;
|
||||
this.element.setAttribute('tabindex', 0);
|
||||
|
||||
this.viewportElement = document.createElement('div');
|
||||
@@ -647,8 +670,7 @@ Terminal.prototype.open = function(parent, focus) {
|
||||
this.viewportScrollArea.classList.add('xterm-scroll-area');
|
||||
this.viewportElement.appendChild(this.viewportScrollArea);
|
||||
|
||||
// Create the selection container. This needs to be added before the
|
||||
// rowContainer as the selection must be below the text.
|
||||
// Create the selection container.
|
||||
this.selectionContainer = document.createElement('div');
|
||||
this.selectionContainer.classList.add('xterm-selection');
|
||||
this.element.appendChild(this.selectionContainer);
|
||||
@@ -703,7 +725,17 @@ Terminal.prototype.open = function(parent, focus) {
|
||||
this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure);
|
||||
this.renderer = new Renderer(this);
|
||||
this.selectionManager = new SelectionManager(this, this.lines, this.rowContainer, this.charMeasure);
|
||||
this.selectionManager.on('refresh', data => this.renderer.refreshSelection(data.start, data.end));
|
||||
this.selectionManager.on('refresh', data => {
|
||||
this.renderer.refreshSelection(data.start, data.end);
|
||||
});
|
||||
this.selectionManager.on('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.selectionManager.refresh());
|
||||
this.viewportElement.addEventListener('scroll', () => this.selectionManager.refresh());
|
||||
|
||||
@@ -779,7 +811,8 @@ Terminal.loadAddon = function(addon, callback) {
|
||||
Terminal.prototype.updateCharSizeStyles = function() {
|
||||
this.charSizeStyleElement.textContent =
|
||||
`.xterm-wide-char{width:${this.charMeasure.width * 2}px;}` +
|
||||
`.xterm-normal-char{width:${this.charMeasure.width}px;}`;
|
||||
`.xterm-normal-char{width:${this.charMeasure.width}px;}` +
|
||||
`.xterm-rows > div{height:${this.charMeasure.height}px;}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1062,6 +1095,18 @@ Terminal.prototype.bindMouse = function() {
|
||||
self.viewport.onWheel(ev);
|
||||
return self.cancel(ev);
|
||||
});
|
||||
|
||||
on(el, 'touchstart', function(ev) {
|
||||
if (self.mouseEvents) return;
|
||||
self.viewport.onTouchStart(ev);
|
||||
return self.cancel(ev);
|
||||
});
|
||||
|
||||
on(el, 'touchmove', function(ev) {
|
||||
if (self.mouseEvents) return;
|
||||
self.viewport.onTouchMove(ev);
|
||||
return self.cancel(ev);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1102,7 +1147,7 @@ Terminal.prototype.queueLinkification = function(start, end) {
|
||||
this.linkifier.linkifyRow(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Display the cursor element
|
||||
@@ -1116,8 +1161,10 @@ Terminal.prototype.showCursor = function() {
|
||||
|
||||
/**
|
||||
* Scroll the terminal down 1 row, creating a blank line.
|
||||
* @param {boolean} isWrapped Whether the new line is wrapped from the previous
|
||||
* line.
|
||||
*/
|
||||
Terminal.prototype.scroll = function() {
|
||||
Terminal.prototype.scroll = function(isWrapped) {
|
||||
var row;
|
||||
|
||||
// Make room for the new row in lines
|
||||
@@ -1144,10 +1191,10 @@ Terminal.prototype.scroll = function() {
|
||||
|
||||
if (row === this.lines.length) {
|
||||
// Optimization: pushing is faster than splicing when they amount to the same behavior
|
||||
this.lines.push(this.blankLine());
|
||||
this.lines.push(this.blankLine(undefined, isWrapped));
|
||||
} else {
|
||||
// add our new line
|
||||
this.lines.splice(row, 0, this.blankLine());
|
||||
this.lines.splice(row, 0, this.blankLine(undefined, isWrapped));
|
||||
}
|
||||
|
||||
if (this.scrollTop !== 0) {
|
||||
@@ -1211,25 +1258,25 @@ Terminal.prototype.scrollDisp = function(disp, suppressScrollEvent) {
|
||||
*/
|
||||
Terminal.prototype.scrollPages = function(pageCount) {
|
||||
this.scrollDisp(pageCount * (this.rows - 1));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Scrolls the display of the terminal to the top.
|
||||
*/
|
||||
Terminal.prototype.scrollToTop = function() {
|
||||
this.scrollDisp(-this.ydisp);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Scrolls the display of the terminal to the bottom.
|
||||
*/
|
||||
Terminal.prototype.scrollToBottom = function() {
|
||||
this.scrollDisp(this.ybase - this.ydisp);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Writes text to the terminal.
|
||||
* @param {string} text The text to write to the terminal.
|
||||
* @param {string} data The text to write to the terminal.
|
||||
*/
|
||||
Terminal.prototype.write = function(data) {
|
||||
this.writeBuffer.push(data);
|
||||
@@ -1253,7 +1300,7 @@ Terminal.prototype.write = function(data) {
|
||||
self.innerWrite();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Terminal.prototype.innerWrite = function() {
|
||||
var writeBatch = this.writeBuffer.splice(0, WRITE_BATCH_SIZE);
|
||||
@@ -1295,29 +1342,41 @@ Terminal.prototype.innerWrite = function() {
|
||||
|
||||
/**
|
||||
* Writes text to the terminal, followed by a break line character (\n).
|
||||
* @param {string} text The text to write to the terminal.
|
||||
* @param {string} data The text to write to the terminal.
|
||||
*/
|
||||
Terminal.prototype.writeln = function(data) {
|
||||
this.write(data + '\r\n');
|
||||
};
|
||||
|
||||
/**
|
||||
* Attaches a custom keydown handler which is run before keys are processed, giving consumers of
|
||||
* xterm.js ultimate control as to what keys should be processed by the terminal and what keys
|
||||
* should not.
|
||||
* DEPRECATED: only for backward compatibility. Please use attachCustomKeyEventHandler() instead.
|
||||
* @param {function} customKeydownHandler The custom KeyboardEvent handler to attach. This is a
|
||||
* function that takes a KeyboardEvent, allowing consumers to stop propogation and/or prevent
|
||||
* the default action. The function returns whether the event should be processed by xterm.js.
|
||||
*/
|
||||
Terminal.prototype.attachCustomKeydownHandler = function(customKeydownHandler) {
|
||||
this.customKeydownHandler = customKeydownHandler;
|
||||
}
|
||||
let message = 'attachCustomKeydownHandler() is DEPRECATED and will be removed soon. Please use attachCustomKeyEventHandler() instead.';
|
||||
console.warn(message);
|
||||
this.attachCustomKeyEventHandler(customKeydownHandler);
|
||||
};
|
||||
|
||||
/**
|
||||
* Attaches a custom key event handler which is run before keys are processed, giving consumers of
|
||||
* xterm.js ultimate control as to what keys should be processed by the terminal and what keys
|
||||
* should not.
|
||||
* @param {function} customKeyEventHandler The custom KeyboardEvent handler to attach. This is a
|
||||
* function that takes a KeyboardEvent, allowing consumers to stop propogation and/or prevent
|
||||
* the default action. The function returns whether the event should be processed by xterm.js.
|
||||
*/
|
||||
Terminal.prototype.attachCustomKeyEventHandler = function(customKeyEventHandler) {
|
||||
this.customKeyEventHandler = customKeyEventHandler;
|
||||
};
|
||||
|
||||
/**
|
||||
* Attaches a http(s) link handler, forcing web links to behave differently to
|
||||
* regular <a> tags. This will trigger a refresh as links potentially need to be
|
||||
* reconstructed. Calling this with null will remove the handler.
|
||||
* @param {LinkHandler} handler The handler callback function.
|
||||
* @param {LinkMatcherHandler} handler The handler callback function.
|
||||
*/
|
||||
Terminal.prototype.setHypertextLinkHandler = function(handler) {
|
||||
if (!this.linkifier) {
|
||||
@@ -1326,7 +1385,7 @@ Terminal.prototype.setHypertextLinkHandler = function(handler) {
|
||||
this.linkifier.setHypertextLinkHandler(handler);
|
||||
// Refresh to force links to refresh
|
||||
this.refresh(0, this.rows - 1);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Attaches a validation callback for hypertext links. This is useful to use
|
||||
@@ -1334,14 +1393,14 @@ Terminal.prototype.setHypertextLinkHandler = function(handler) {
|
||||
* @param {LinkMatcherValidationCallback} callback The callback to use, this can
|
||||
* be cleared with null.
|
||||
*/
|
||||
Terminal.prototype.setHypertextValidationCallback = function(handler) {
|
||||
Terminal.prototype.setHypertextValidationCallback = function(callback) {
|
||||
if (!this.linkifier) {
|
||||
throw new Error('Cannot attach a hypertext validation callback before Terminal.open is called');
|
||||
}
|
||||
this.linkifier.setHypertextValidationCallback(handler);
|
||||
this.linkifier.setHypertextValidationCallback(callback);
|
||||
// Refresh to force links to refresh
|
||||
this.refresh(0, this.rows - 1);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers a link matcher, allowing custom link patterns to be matched and
|
||||
@@ -1349,7 +1408,7 @@ Terminal.prototype.setHypertextValidationCallback = function(handler) {
|
||||
* @param {RegExp} regex The regular expression to search for, specifically
|
||||
* this searches the textContent of the rows. You will want to use \s to match
|
||||
* a space ' ' character for example.
|
||||
* @param {LinkHandler} handler The callback when the link is called.
|
||||
* @param {LinkMatcherHandler} handler The callback when the link is called.
|
||||
* @param {LinkMatcherOptions} [options] Options for the link matcher.
|
||||
* @return {number} The ID of the new matcher, this can be used to deregister.
|
||||
*/
|
||||
@@ -1359,7 +1418,7 @@ Terminal.prototype.registerLinkMatcher = function(regex, handler, options) {
|
||||
this.refresh(0, this.rows - 1);
|
||||
return matcherId;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Deregisters a link matcher if it has been registered.
|
||||
@@ -1371,14 +1430,14 @@ Terminal.prototype.deregisterLinkMatcher = function(matcherId) {
|
||||
this.refresh(0, this.rows - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets whether the terminal has an active selection.
|
||||
*/
|
||||
Terminal.prototype.hasSelection = function() {
|
||||
return this.selectionManager.hasSelection;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the terminal's current selection, this is useful for implementing copy
|
||||
@@ -1386,21 +1445,21 @@ Terminal.prototype.hasSelection = function() {
|
||||
*/
|
||||
Terminal.prototype.getSelection = function() {
|
||||
return this.selectionManager.selectionText;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clears the current terminal selection.
|
||||
*/
|
||||
Terminal.prototype.clearSelection = function() {
|
||||
this.selectionManager.clearSelection();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Selects all text within the terminal.
|
||||
*/
|
||||
Terminal.prototype.selectAll = function() {
|
||||
this.selectionManager.selectAll();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle a keydown event
|
||||
@@ -1409,7 +1468,7 @@ Terminal.prototype.selectAll = function() {
|
||||
* @param {KeyboardEvent} ev The keydown event to be handled.
|
||||
*/
|
||||
Terminal.prototype.keyDown = function(ev) {
|
||||
if (this.customKeydownHandler && this.customKeydownHandler(ev) === false) {
|
||||
if (this.customKeyEventHandler && this.customKeyEventHandler(ev) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1774,6 +1833,10 @@ Terminal.prototype.setgCharset = function(g, charset) {
|
||||
Terminal.prototype.keyPress = function(ev) {
|
||||
var key;
|
||||
|
||||
if (this.customKeyEventHandler && this.customKeyEventHandler(ev) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.cancel(ev);
|
||||
|
||||
if (ev.charCode) {
|
||||
@@ -1799,7 +1862,7 @@ Terminal.prototype.keyPress = function(ev) {
|
||||
this.showCursor();
|
||||
this.handler(key);
|
||||
|
||||
return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1864,6 +1927,10 @@ Terminal.prototype.resize = function(x, y) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (y > this.getOption('scrollback')) {
|
||||
this.setOption('scrollback', y)
|
||||
}
|
||||
|
||||
var line
|
||||
, el
|
||||
, i
|
||||
@@ -1905,7 +1972,7 @@ Terminal.prototype.resize = function(x, y) {
|
||||
// There is room above the buffer and there are no empty elements below the line,
|
||||
// scroll up
|
||||
this.ybase--;
|
||||
addToY++
|
||||
addToY++;
|
||||
if (this.ydisp > 0) {
|
||||
// Viewport is at the top of the buffer, must increase downwards
|
||||
this.ydisp--;
|
||||
@@ -2106,8 +2173,9 @@ Terminal.prototype.eraseLine = function(y) {
|
||||
/**
|
||||
* Return the data array of a blank line
|
||||
* @param {number} cur First bunch of data for each "blank" character.
|
||||
* @param {boolean} isWrapped Whether the new line is wrapped from the previous line.
|
||||
*/
|
||||
Terminal.prototype.blankLine = function(cur) {
|
||||
Terminal.prototype.blankLine = function(cur, isWrapped) {
|
||||
var attr = cur
|
||||
? this.eraseAttr()
|
||||
: this.defAttr;
|
||||
@@ -2116,6 +2184,12 @@ Terminal.prototype.blankLine = function(cur) {
|
||||
, line = []
|
||||
, i = 0;
|
||||
|
||||
// TODO: It is not ideal that this is a property on an array, a buffer line
|
||||
// class should be added that will hold this data and other useful functions.
|
||||
if (isWrapped) {
|
||||
line.isWrapped = isWrapped;
|
||||
}
|
||||
|
||||
for (; i < this.cols; i++) {
|
||||
line[i] = ch;
|
||||
}
|
||||
@@ -2225,10 +2299,10 @@ Terminal.prototype.reverseIndex = function() {
|
||||
Terminal.prototype.reset = function() {
|
||||
this.options.rows = this.rows;
|
||||
this.options.cols = this.cols;
|
||||
var customKeydownHandler = this.customKeydownHandler;
|
||||
var customKeyEventHandler = this.customKeyEventHandler;
|
||||
var cursorBlinkInterval = this.cursorBlinkInterval;
|
||||
Terminal.call(this, this.options);
|
||||
this.customKeydownHandler = customKeydownHandler;
|
||||
this.customKeyEventHandler = customKeyEventHandler;
|
||||
this.cursorBlinkInterval = cursorBlinkInterval;
|
||||
this.refresh(0, this.rows - 1);
|
||||
this.viewport.syncScrollArea();
|
||||
|
||||
Reference in New Issue
Block a user