Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
479fb0e782 | ||
|
|
8ba11a2bd0 | ||
|
|
abc3f3b94d | ||
|
|
67bc0456e0 | ||
|
|
3e8ddd1966 | ||
|
|
6223342890 | ||
|
|
9ddabb3fee | ||
|
|
af0030b580 | ||
|
|
b93271d9cd | ||
|
|
934ca6508f | ||
|
|
7e210c7e6b | ||
|
|
e4970266d8 | ||
|
|
0a5d7a7bd2 | ||
|
|
31aab8ea6f | ||
|
|
857d9bcf2a | ||
|
|
7463166aec | ||
|
|
f80159fedc | ||
|
|
3180a6f6ec | ||
|
|
35d26f1d27 | ||
|
|
ca03f8928d | ||
|
|
0b3d1e50a8 | ||
|
|
401886c7c3 | ||
|
|
4eac7661c2 | ||
|
|
b8163cfbbf | ||
|
|
a9f82907ee | ||
|
|
57f4ae5320 | ||
|
|
d1712424c8 | ||
|
|
16f865987d | ||
|
|
f071531494 | ||
|
|
618768c7ed | ||
|
|
cfe22e6fd2 | ||
|
|
3961dd5a44 | ||
|
|
471e332036 | ||
|
|
467e8c563a | ||
|
|
5971630f64 | ||
|
|
f7cc1058c7 | ||
|
|
645a5920da | ||
|
|
f95bc64971 | ||
|
|
0c7c307de3 | ||
|
|
1c84881d00 | ||
|
|
f1252abbbe | ||
|
|
11d6ca2aa1 | ||
|
|
eb59b90790 | ||
|
|
4c3aa97917 | ||
|
|
ba54f25b8c | ||
|
|
af268f4968 | ||
|
|
63d63c27e7 | ||
|
|
ef1e2ab9e2 | ||
|
|
7b46940772 | ||
|
|
6f5f68df1a | ||
|
|
dc165175cf | ||
|
|
32f1782c84 | ||
|
|
8811d96a8b | ||
|
|
d11d74688e | ||
|
|
8e79d53d78 | ||
|
|
6dbc476cdc | ||
|
|
57921e3fe0 | ||
|
|
75b74ac4ff | ||
|
|
1c659841fa | ||
|
|
9573357778 | ||
|
|
f11362a80d | ||
|
|
a01fbb636a | ||
|
|
320fb55ddc | ||
|
|
4b170ca43f | ||
|
|
c6e7fb74ab | ||
|
|
b05814e6a5 | ||
|
|
07882c42dc | ||
|
|
a59b2f8433 | ||
|
|
7f2e94c30c | ||
|
|
95aed8fcb0 | ||
|
|
663d3b24cd | ||
|
|
a87303f375 | ||
|
|
259858fac2 | ||
|
|
a4514ee98a | ||
|
|
d27b7bfa7a | ||
|
|
ce61795c38 | ||
|
|
9cc878cea7 | ||
|
|
087acf8a9a | ||
|
|
7612180276 | ||
|
|
201e3e4dab | ||
|
|
55f51ad169 | ||
|
|
405d1ee52d | ||
|
|
5f9ad5c19e | ||
|
|
c5cfa409b1 | ||
|
|
2a0f44b0fb | ||
|
|
7d0e585c44 | ||
|
|
a5ced5d909 | ||
|
|
0a99f65f01 | ||
|
|
c33814a7a6 | ||
|
|
0950369a5c | ||
|
|
346b5177bc | ||
|
|
9246d524cb | ||
|
|
a889fef752 | ||
|
|
6075498c24 | ||
|
|
ee1da73a69 | ||
|
|
29ac6a153e | ||
|
|
264757e964 | ||
|
|
e88a9ffd89 | ||
|
|
d56a611a19 | ||
|
|
28ed7772c6 | ||
|
|
a047359f81 | ||
|
|
b81c165bea | ||
|
|
4405f5e163 | ||
|
|
2012c8c91d | ||
|
|
f719a4e91b | ||
|
|
5d33727ae4 | ||
|
|
1343b83fc3 | ||
|
|
9e47ec9af2 | ||
|
|
9c459aacc7 | ||
|
|
72724abac8 | ||
|
|
9a86eeb333 | ||
|
|
46f6fd18b0 | ||
|
|
603f22a7bb | ||
|
|
13c401cbaa | ||
|
|
b812991072 | ||
|
|
2b2431824f | ||
|
|
7147787441 | ||
|
|
ec3bf1139d | ||
|
|
d0b603d033 | ||
|
|
2621be819e | ||
|
|
f380153f42 | ||
|
|
fd91c5e1a8 | ||
|
|
f61d852f3c | ||
|
|
5bc1112151 | ||
|
|
59cc0b675e | ||
|
|
db8ded2a02 | ||
|
|
d3865ad23f | ||
|
|
8b1067d180 | ||
|
|
a53a0acd65 | ||
|
|
11cec31da6 | ||
|
|
d9991b25a4 | ||
|
|
cb6533f3a6 | ||
|
|
54e7f65d4e | ||
|
|
ec61f3acf0 | ||
|
|
910aa1ad7f | ||
|
|
24bed01fa4 | ||
|
|
f7d6ab5f2a | ||
|
|
0716cffd7d | ||
|
|
e29ab29441 | ||
|
|
9f271de803 | ||
|
|
43c796a781 | ||
|
|
25152e4441 | ||
|
|
3846fe0a7d | ||
|
|
b3b2bd1faf | ||
|
|
0dc3dd0336 | ||
|
|
c4762e7228 | ||
|
|
ab40908fa4 | ||
|
|
a5c48c891e | ||
|
|
e63fdf580b | ||
|
|
597c6939c8 | ||
|
|
293ae18a11 | ||
|
|
32b34cbe4c | ||
|
|
2f6d05f99d | ||
|
|
ad3ae67e70 | ||
|
|
b594407cd5 | ||
|
|
207c4cf961 | ||
|
|
fe11837a61 | ||
|
|
fee912023f | ||
|
|
65256c8751 | ||
|
|
b36d8780fd | ||
|
|
70fda99415 |
@@ -0,0 +1 @@
|
||||
* 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.
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xterm.js",
|
||||
"version": "2.7.0",
|
||||
"version": "2.8.0",
|
||||
"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
+755
-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
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "xterm",
|
||||
"description": "Full xterm terminal, in your browser",
|
||||
"version": "2.7.0",
|
||||
"version": "2.8.0",
|
||||
"ignore": [
|
||||
"demo",
|
||||
"test",
|
||||
|
||||
+9
-4
@@ -57,7 +57,7 @@ export class InputHandler implements IInputHandler {
|
||||
this._terminal.y++;
|
||||
if (this._terminal.y > this._terminal.scrollBottom) {
|
||||
this._terminal.y--;
|
||||
this._terminal.scroll();
|
||||
this._terminal.scroll(true);
|
||||
}
|
||||
} else {
|
||||
if (ch_width === 2) // FIXME: check for xterm behavior
|
||||
@@ -75,8 +75,9 @@ export class InputHandler implements IInputHandler {
|
||||
const removed = this._terminal.lines.get(this._terminal.y + this._terminal.ybase).pop();
|
||||
if (removed[2] === 0
|
||||
&& this._terminal.lines.get(row)[this._terminal.cols - 2]
|
||||
&& this._terminal.lines.get(row)[this._terminal.cols - 2][2] === 2)
|
||||
&& this._terminal.lines.get(row)[this._terminal.cols - 2][2] === 2) {
|
||||
this._terminal.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1];
|
||||
}
|
||||
|
||||
// insert empty cell at cursor
|
||||
this._terminal.lines.get(row).splice(this._terminal.x, 0, [this._terminal.curAttr, ' ', 1]);
|
||||
@@ -903,7 +904,8 @@ export class InputHandler implements IInputHandler {
|
||||
this._terminal.vt200Mouse = params[0] === 1000;
|
||||
this._terminal.normalMouse = params[0] > 1000;
|
||||
this._terminal.mouseEvents = true;
|
||||
this._terminal.element.style.cursor = 'default';
|
||||
this._terminal.element.classList.add('enable-mouse-events');
|
||||
this._terminal.selectionManager.disable();
|
||||
this._terminal.log('Binding to mouse events.');
|
||||
break;
|
||||
case 1004: // send focusin/focusout events
|
||||
@@ -1096,7 +1098,8 @@ export class InputHandler implements IInputHandler {
|
||||
this._terminal.vt200Mouse = false;
|
||||
this._terminal.normalMouse = false;
|
||||
this._terminal.mouseEvents = false;
|
||||
this._terminal.element.style.cursor = '';
|
||||
this._terminal.element.classList.remove('enable-mouse-events');
|
||||
this._terminal.selectionManager.enable();
|
||||
break;
|
||||
case 1004: // send focusin/focusout events
|
||||
this._terminal.sendFocus = false;
|
||||
@@ -1127,6 +1130,8 @@ export class InputHandler implements IInputHandler {
|
||||
this._terminal.scrollBottom = this._terminal.normal.scrollBottom;
|
||||
this._terminal.tabs = this._terminal.normal.tabs;
|
||||
this._terminal.normal = null;
|
||||
// Ensure the selection manager has the correct buffer
|
||||
this._terminal.selectionManager.setBuffer(this._terminal.lines);
|
||||
// if (params === 1049) {
|
||||
// this.x = this.savedX;
|
||||
// this.y = this.savedY;
|
||||
|
||||
@@ -20,6 +20,8 @@ export interface IBrowser {
|
||||
export interface ITerminal {
|
||||
element: HTMLElement;
|
||||
rowContainer: HTMLElement;
|
||||
selectionContainer: HTMLElement;
|
||||
charMeasure: ICharMeasure;
|
||||
textarea: HTMLTextAreaElement;
|
||||
ybase: number;
|
||||
ydisp: number;
|
||||
@@ -47,6 +49,10 @@ export interface ITerminal {
|
||||
emit(event: string, data: any);
|
||||
}
|
||||
|
||||
export interface ISelectionManager {
|
||||
selectionText: string;
|
||||
}
|
||||
|
||||
export interface ICharMeasure {
|
||||
width: number;
|
||||
height: number;
|
||||
|
||||
@@ -119,6 +119,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;
|
||||
}
|
||||
}
|
||||
|
||||
+62
-1
@@ -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;
|
||||
@@ -318,6 +319,66 @@ export class Renderer {
|
||||
|
||||
this._terminal.emit('refresh', {element: this._terminal.element, start: start, end: end});
|
||||
};
|
||||
|
||||
/**
|
||||
* Refreshes the selection in the DOM.
|
||||
* @param start The selection start.
|
||||
* @param end The selection end.
|
||||
*/
|
||||
public refreshSelection(start: [number, number], end: [number, number]) {
|
||||
// Remove all selections
|
||||
while (this._terminal.selectionContainer.children.length) {
|
||||
this._terminal.selectionContainer.removeChild(this._terminal.selectionContainer.children[0]);
|
||||
}
|
||||
|
||||
// Selection does not exist
|
||||
if (!start || !end) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Translate from buffer position to viewport position
|
||||
const viewportStartRow = start[1] - this._terminal.ydisp;
|
||||
const viewportEndRow = end[1] - this._terminal.ydisp;
|
||||
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
|
||||
const viewportCappedEndRow = Math.min(viewportEndRow, this._terminal.rows - 1);
|
||||
|
||||
// No need to draw the selection
|
||||
if (viewportCappedStartRow >= this._terminal.rows || viewportCappedEndRow < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the selections
|
||||
const documentFragment = document.createDocumentFragment();
|
||||
// Draw first row
|
||||
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
|
||||
const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
|
||||
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol));
|
||||
// Draw middle rows
|
||||
const middleRowsCount = viewportCappedEndRow - viewportCappedStartRow - 1;
|
||||
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._terminal.cols, middleRowsCount));
|
||||
// Draw final row
|
||||
if (viewportCappedStartRow !== viewportCappedEndRow) {
|
||||
// Only draw viewportEndRow if it's not the same as viewporttartRow
|
||||
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
|
||||
documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, endCol));
|
||||
}
|
||||
this._terminal.selectionContainer.appendChild(documentFragment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a selection element at the specified position.
|
||||
* @param row The row of the selection.
|
||||
* @param colStart The start column.
|
||||
* @param colEnd The end columns.
|
||||
*/
|
||||
private _createSelectionElement(row: number, colStart: number, colEnd: number, rowCount: number = 1): HTMLElement {
|
||||
const element = document.createElement('div');
|
||||
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`;
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* @license MIT
|
||||
*/
|
||||
import jsdom = require('jsdom');
|
||||
import { assert } from 'chai';
|
||||
import { ITerminal } from './Interfaces';
|
||||
import { CharMeasure } from './utils/CharMeasure';
|
||||
import { CircularList } from './utils/CircularList';
|
||||
import { SelectionManager } from './SelectionManager';
|
||||
import { SelectionModel } from './SelectionModel';
|
||||
|
||||
class TestSelectionManager extends SelectionManager {
|
||||
constructor(
|
||||
terminal: ITerminal,
|
||||
buffer: CircularList<any>,
|
||||
rowContainer: HTMLElement,
|
||||
charMeasure: CharMeasure
|
||||
) {
|
||||
super(terminal, buffer, rowContainer, charMeasure);
|
||||
}
|
||||
|
||||
public get model(): SelectionModel { return this._model; }
|
||||
|
||||
public selectLineAt(line: number): void { this._selectLineAt(line); }
|
||||
public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords); }
|
||||
|
||||
// Disable DOM interaction
|
||||
public enable(): void {}
|
||||
public disable(): void {}
|
||||
public refresh(): void {}
|
||||
}
|
||||
|
||||
describe('SelectionManager', () => {
|
||||
let window: Window;
|
||||
let document: Document;
|
||||
|
||||
let terminal: ITerminal;
|
||||
let buffer: CircularList<any>;
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
function stringToRow(text: string): [number, string, number][] {
|
||||
let result: [number, string, number][] = [];
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
result.push([0, text.charAt(i), 1]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
describe('_selectWordAt', () => {
|
||||
it('should expand selection for normal width chars', () => {
|
||||
buffer.push(stringToRow('foo bar'));
|
||||
selectionManager.selectWordAt([0, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'foo');
|
||||
selectionManager.selectWordAt([1, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'foo');
|
||||
selectionManager.selectWordAt([2, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'foo');
|
||||
selectionManager.selectWordAt([3, 0]);
|
||||
assert.equal(selectionManager.selectionText, ' ');
|
||||
selectionManager.selectWordAt([4, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'bar');
|
||||
selectionManager.selectWordAt([5, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'bar');
|
||||
selectionManager.selectWordAt([6, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'bar');
|
||||
});
|
||||
it('should expand selection for whitespace', () => {
|
||||
buffer.push(stringToRow('a b'));
|
||||
selectionManager.selectWordAt([0, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'a');
|
||||
selectionManager.selectWordAt([1, 0]);
|
||||
assert.equal(selectionManager.selectionText, ' ');
|
||||
selectionManager.selectWordAt([2, 0]);
|
||||
assert.equal(selectionManager.selectionText, ' ');
|
||||
selectionManager.selectWordAt([3, 0]);
|
||||
assert.equal(selectionManager.selectionText, ' ');
|
||||
selectionManager.selectWordAt([4, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'b');
|
||||
});
|
||||
it('should expand selection for wide characters', () => {
|
||||
// Wide characters use a special format
|
||||
buffer.push([
|
||||
[null, '中', 2],
|
||||
[null, '', 0],
|
||||
[null, '文', 2],
|
||||
[null, '', 0],
|
||||
[null, ' ', 1],
|
||||
[null, 'a', 1],
|
||||
[null, '中', 2],
|
||||
[null, '', 0],
|
||||
[null, '文', 2],
|
||||
[null, '', 0],
|
||||
[null, 'b', 1],
|
||||
[null, ' ', 1],
|
||||
[null, 'f', 1],
|
||||
[null, 'o', 1],
|
||||
[null, 'o', 1]
|
||||
]);
|
||||
// Ensure wide characters take up 2 columns
|
||||
selectionManager.selectWordAt([0, 0]);
|
||||
assert.equal(selectionManager.selectionText, '中文');
|
||||
selectionManager.selectWordAt([1, 0]);
|
||||
assert.equal(selectionManager.selectionText, '中文');
|
||||
selectionManager.selectWordAt([2, 0]);
|
||||
assert.equal(selectionManager.selectionText, '中文');
|
||||
selectionManager.selectWordAt([3, 0]);
|
||||
assert.equal(selectionManager.selectionText, '中文');
|
||||
selectionManager.selectWordAt([4, 0]);
|
||||
assert.equal(selectionManager.selectionText, ' ');
|
||||
// Ensure wide characters work when wrapped in normal width characters
|
||||
selectionManager.selectWordAt([5, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'a中文b');
|
||||
selectionManager.selectWordAt([6, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'a中文b');
|
||||
selectionManager.selectWordAt([7, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'a中文b');
|
||||
selectionManager.selectWordAt([8, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'a中文b');
|
||||
selectionManager.selectWordAt([9, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'a中文b');
|
||||
selectionManager.selectWordAt([10, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'a中文b');
|
||||
selectionManager.selectWordAt([11, 0]);
|
||||
assert.equal(selectionManager.selectionText, ' ');
|
||||
// Ensure normal width characters work fine in a line containing wide characters
|
||||
selectionManager.selectWordAt([12, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'foo');
|
||||
selectionManager.selectWordAt([13, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'foo');
|
||||
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', () => {
|
||||
it('should select the entire line', () => {
|
||||
buffer.push(stringToRow('foo bar'));
|
||||
selectionManager.selectLineAt(0);
|
||||
assert.equal(selectionManager.selectionText, 'foo bar', 'The selected text is correct');
|
||||
assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]);
|
||||
assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 0], 'The actual selection spans the entire column');
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectAll', () => {
|
||||
it('should select the entire buffer, beyond the viewport', () => {
|
||||
buffer.push(stringToRow('1'));
|
||||
buffer.push(stringToRow('2'));
|
||||
buffer.push(stringToRow('3'));
|
||||
buffer.push(stringToRow('4'));
|
||||
buffer.push(stringToRow('5'));
|
||||
selectionManager.selectAll();
|
||||
terminal.ybase = buffer.length - terminal.rows;
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,718 @@
|
||||
/**
|
||||
* @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 { ITerminal } from './Interfaces';
|
||||
import { SelectionModel } from './SelectionModel';
|
||||
|
||||
/**
|
||||
* The number of pixels the mouse needs to be above or below the viewport in
|
||||
* order to scroll at the maximum speed.
|
||||
*/
|
||||
const DRAG_SCROLL_MAX_THRESHOLD = 50;
|
||||
|
||||
/**
|
||||
* The maximum scrolling speed
|
||||
*/
|
||||
const DRAG_SCROLL_MAX_SPEED = 15;
|
||||
|
||||
/**
|
||||
* The number of milliseconds between drag scroll updates.
|
||||
*/
|
||||
const DRAG_SCROLL_INTERVAL = 50;
|
||||
|
||||
/**
|
||||
* The amount of time before mousedown events are no longer stacked to create
|
||||
* double/triple click events.
|
||||
*/
|
||||
const CLEAR_MOUSE_DOWN_TIME = 400;
|
||||
|
||||
/**
|
||||
* The number of pixels in each direction that the mouse must move before
|
||||
* mousedown events are no longer stacked to create double/triple click events.
|
||||
*/
|
||||
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;
|
||||
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
|
||||
* dealing with the selection, including handling mouse interaction, wide
|
||||
* characters and fetching the actual text within the selection. Rendering is
|
||||
* not handled by the SelectionManager but a 'refresh' event is fired when the
|
||||
* selection is ready to be redrawn.
|
||||
*/
|
||||
export class SelectionManager extends EventEmitter {
|
||||
protected _model: SelectionModel;
|
||||
|
||||
/**
|
||||
* The amount to scroll every drag scroll update (depends on how far the mouse
|
||||
* drag is above or below the terminal).
|
||||
*/
|
||||
private _dragScrollAmount: number;
|
||||
|
||||
/**
|
||||
* The last time the mousedown event fired, this is used to track double and
|
||||
* triple clicks.
|
||||
*/
|
||||
private _lastMouseDownTime: number;
|
||||
|
||||
/**
|
||||
* The last position the mouse was clicked [x, y].
|
||||
*/
|
||||
private _lastMousePosition: [number, number];
|
||||
|
||||
/**
|
||||
* The number of clicks of the mousedown event. This is used to keep track of
|
||||
* double and triple clicks.
|
||||
*/
|
||||
private _clickCount: number;
|
||||
|
||||
/**
|
||||
* The current selection mode.
|
||||
*/
|
||||
private _activeSelectionMode: SelectionMode;
|
||||
|
||||
/**
|
||||
* A setInterval timer that is active while the mouse is down whose callback
|
||||
* scrolls the viewport when necessary.
|
||||
*/
|
||||
private _dragScrollIntervalTimer: NodeJS.Timer;
|
||||
|
||||
/**
|
||||
* The animation frame ID used for refreshing the selection.
|
||||
*/
|
||||
private _refreshAnimationFrame: number;
|
||||
|
||||
private _bufferTrimListener: any;
|
||||
private _mouseMoveListener: EventListener;
|
||||
private _mouseDownListener: EventListener;
|
||||
private _mouseUpListener: EventListener;
|
||||
|
||||
constructor(
|
||||
private _terminal: ITerminal,
|
||||
private _buffer: CircularList<any>,
|
||||
private _rowContainer: HTMLElement,
|
||||
private _charMeasure: CharMeasure
|
||||
) {
|
||||
super();
|
||||
this._initListeners();
|
||||
this.enable();
|
||||
|
||||
this._model = new SelectionModel(_terminal);
|
||||
this._lastMouseDownTime = 0;
|
||||
this._activeSelectionMode = SelectionMode.NORMAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes listener variables.
|
||||
*/
|
||||
private _initListeners() {
|
||||
this._bufferTrimListener = (amount: number) => this._onTrim(amount);
|
||||
this._mouseMoveListener = event => this._onMouseMove(<MouseEvent>event);
|
||||
this._mouseDownListener = event => this._onMouseDown(<MouseEvent>event);
|
||||
this._mouseUpListener = event => this._onMouseUp(<MouseEvent>event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the selection manager. This is useful for when terminal mouse
|
||||
* are enabled.
|
||||
*/
|
||||
public disable() {
|
||||
this.clearSelection();
|
||||
this._buffer.off('trim', this._bufferTrimListener);
|
||||
this._rowContainer.removeEventListener('mousedown', this._mouseDownListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the selection manager.
|
||||
*/
|
||||
public enable() {
|
||||
// Only adjust the selection on trim, shiftElements is rarely used (only in
|
||||
// reverseIndex) and delete in a splice is only ever used when the same
|
||||
// number of elements was just added. Given this is could actually be
|
||||
// beneficial to leave the selection as is for these cases.
|
||||
this._buffer.on('trim', this._bufferTrimListener);
|
||||
this._rowContainer.addEventListener('mousedown', this._mouseDownListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the active buffer, this should be called when the alt buffer is
|
||||
* switched in or out.
|
||||
* @param buffer The active buffer.
|
||||
*/
|
||||
public setBuffer(buffer: CircularList<any>): void {
|
||||
this._buffer = buffer;
|
||||
this.clearSelection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether there is an active text selection.
|
||||
*/
|
||||
public get hasSelection(): boolean {
|
||||
const start = this._model.finalSelectionStart;
|
||||
const end = this._model.finalSelectionEnd;
|
||||
if (!start || !end) {
|
||||
return false;
|
||||
}
|
||||
return start[0] !== end[0] || start[1] !== end[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the text currently selected.
|
||||
*/
|
||||
public get selectionText(): string {
|
||||
const start = this._model.finalSelectionStart;
|
||||
const end = this._model.finalSelectionEnd;
|
||||
if (!start || !end) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Get first row
|
||||
const startRowEndCol = start[1] === end[1] ? end[0] : null;
|
||||
let result: string[] = [];
|
||||
result.push(this._translateBufferLineToString(this._buffer.get(start[1]), true, start[0], startRowEndCol));
|
||||
|
||||
// Get middle rows
|
||||
for (let i = start[1] + 1; i <= end[1] - 1; i++) {
|
||||
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]) {
|
||||
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(Browser.isMSWindows ? '\r\n' : '\n');
|
||||
|
||||
return formattedResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the current terminal selection.
|
||||
*/
|
||||
public clearSelection(): void {
|
||||
this._model.clearSelection();
|
||||
this._removeMouseDownListeners();
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a buffer line to a string, with optional start and end columns.
|
||||
* Wide characters will count as two columns in the resulting string. This
|
||||
* function is useful for getting the actual text underneath the raw selection
|
||||
* position.
|
||||
* @param line The line being translated.
|
||||
* @param trimRight Whether to trim whitespace to the right.
|
||||
* @param startCol The column to start at.
|
||||
* @param endCol The column to end at.
|
||||
*/
|
||||
private _translateBufferLineToString(line: any, trimRight: boolean, startCol: number = 0, endCol: number = null): string {
|
||||
// TODO: This function should live in a buffer or buffer line class
|
||||
|
||||
// Get full line
|
||||
let lineString = '';
|
||||
let widthAdjustedStartCol = startCol;
|
||||
let widthAdjustedEndCol = endCol;
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const char = line[i];
|
||||
lineString += char[LINE_DATA_CHAR_INDEX];
|
||||
// Adjust start and end cols for wide characters if they affect their
|
||||
// column indexes
|
||||
if (char[LINE_DATA_WIDTH_INDEX] === 0) {
|
||||
if (startCol >= i) {
|
||||
widthAdjustedStartCol--;
|
||||
}
|
||||
if (endCol >= i) {
|
||||
widthAdjustedEndCol--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the final end col by trimming whitespace on the right of the
|
||||
// line if needed.
|
||||
let finalEndCol = widthAdjustedEndCol || line.length;
|
||||
if (trimRight) {
|
||||
const rightWhitespaceIndex = lineString.search(/\s+$/);
|
||||
if (rightWhitespaceIndex !== -1) {
|
||||
finalEndCol = Math.min(finalEndCol, rightWhitespaceIndex);
|
||||
}
|
||||
// Return the empty string if only trimmed whitespace is selected
|
||||
if (finalEndCol <= widthAdjustedStartCol) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
return lineString.substring(widthAdjustedStartCol, finalEndCol);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires the refresh event, causing consumers to pick it up and redraw the
|
||||
* selection state.
|
||||
*/
|
||||
private _refresh(): void {
|
||||
this._refreshAnimationFrame = null;
|
||||
this.emit('refresh', { start: this._model.finalSelectionStart, end: this._model.finalSelectionEnd });
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects all text within the terminal.
|
||||
*/
|
||||
public selectAll(): void {
|
||||
this._model.isSelectAllActive = true;
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the buffer being trimmed, adjust the selection position.
|
||||
* @param amount The amount the buffer is being trimmed.
|
||||
*/
|
||||
private _onTrim(amount: number) {
|
||||
const needsRefresh = this._model.onTrim(amount);
|
||||
if (needsRefresh) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the 0-based [x, y] buffer coordinates of the current mouse event.
|
||||
* @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, true);
|
||||
// Convert to 0-based
|
||||
coords[0]--;
|
||||
coords[1]--;
|
||||
// Convert viewport coords to buffer coords
|
||||
coords[1] += this._terminal.ydisp;
|
||||
return coords;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the amount the viewport should be scrolled based on how far out of the
|
||||
* terminal the mouse is.
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _getMouseEventScrollAmount(event: MouseEvent): number {
|
||||
let offset = Mouse.getCoordsRelativeToElement(event, this._rowContainer)[1];
|
||||
const terminalHeight = this._terminal.rows * this._charMeasure.height;
|
||||
if (offset >= 0 && offset <= terminalHeight) {
|
||||
return 0;
|
||||
}
|
||||
if (offset > terminalHeight) {
|
||||
offset -= terminalHeight;
|
||||
}
|
||||
|
||||
offset = Math.min(Math.max(offset, -DRAG_SCROLL_MAX_THRESHOLD), DRAG_SCROLL_MAX_THRESHOLD);
|
||||
offset /= DRAG_SCROLL_MAX_THRESHOLD;
|
||||
return (offset / Math.abs(offset)) + Math.round(offset * (DRAG_SCROLL_MAX_SPEED - 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles te mousedown event, setting up for a new selection.
|
||||
* @param event The mousedown event.
|
||||
*/
|
||||
private _onMouseDown(event: MouseEvent) {
|
||||
// Only action the primary button
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Tell the browser not to start a regular selection
|
||||
event.preventDefault();
|
||||
|
||||
// Reset drag scroll state
|
||||
this._dragScrollAmount = 0;
|
||||
|
||||
this._setMouseClickCount(event);
|
||||
|
||||
if (event.shiftKey) {
|
||||
this._onShiftClick(event);
|
||||
} else {
|
||||
if (this._clickCount === 1) {
|
||||
this._onSingleClick(event);
|
||||
} else if (this._clickCount === 2) {
|
||||
this._onDoubleClick(event);
|
||||
} else if (this._clickCount === 3) {
|
||||
this._onTripleClick(event);
|
||||
}
|
||||
}
|
||||
|
||||
this._addMouseDownListeners();
|
||||
this.refresh(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds listeners when mousedown is triggered.
|
||||
*/
|
||||
private _addMouseDownListeners(): void {
|
||||
// Listen on the document so that dragging outside of viewport works
|
||||
this._rowContainer.ownerDocument.addEventListener('mousemove', this._mouseMoveListener);
|
||||
this._rowContainer.ownerDocument.addEventListener('mouseup', this._mouseUpListener);
|
||||
this._dragScrollIntervalTimer = setInterval(() => this._dragScroll(), DRAG_SCROLL_INTERVAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the listeners that are registered when mousedown is triggered.
|
||||
*/
|
||||
private _removeMouseDownListeners(): void {
|
||||
this._rowContainer.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener);
|
||||
this._rowContainer.ownerDocument.removeEventListener('mouseup', this._mouseUpListener);
|
||||
clearInterval(this._dragScrollIntervalTimer);
|
||||
this._dragScrollIntervalTimer = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a shift click, setting the selection end position to the mouse
|
||||
* position.
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _onShiftClick(event: MouseEvent): void {
|
||||
if (this._model.selectionStart) {
|
||||
this._model.selectionEnd = this._getMouseBufferCoords(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a single click, resetting relevant state and setting the selection
|
||||
* start position.
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _onSingleClick(event: MouseEvent): void {
|
||||
this._model.selectionStartLength = 0;
|
||||
this._model.isSelectAllActive = false;
|
||||
this._activeSelectionMode = SelectionMode.NORMAL;
|
||||
this._model.selectionStart = this._getMouseBufferCoords(event);
|
||||
if (this._model.selectionStart) {
|
||||
this._model.selectionEnd = null;
|
||||
// If the mouse is over the second half of a wide character, adjust the
|
||||
// selection to cover the whole character
|
||||
const char = this._buffer.get(this._model.selectionStart[1])[this._model.selectionStart[0]];
|
||||
if (char[LINE_DATA_WIDTH_INDEX] === 0) {
|
||||
this._model.selectionStart[0]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a double click, selecting the current work.
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _onDoubleClick(event: MouseEvent): void {
|
||||
const coords = this._getMouseBufferCoords(event);
|
||||
if (coords) {
|
||||
this._activeSelectionMode = SelectionMode.WORD;
|
||||
this._selectWordAt(coords);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a triple click, selecting the current line and activating line
|
||||
* select mode.
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _onTripleClick(event: MouseEvent): void {
|
||||
const coords = this._getMouseBufferCoords(event);
|
||||
if (coords) {
|
||||
this._activeSelectionMode = SelectionMode.LINE;
|
||||
this._selectLineAt(coords[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the number of clicks for the current mousedown event based on the time
|
||||
* and position of the last mousedown event.
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _setMouseClickCount(event: MouseEvent): void {
|
||||
let currentTime = (new Date()).getTime();
|
||||
if (currentTime - this._lastMouseDownTime > CLEAR_MOUSE_DOWN_TIME || this._distanceFromLastMousePosition(event) > CLEAR_MOUSE_DISTANCE) {
|
||||
this._clickCount = 0;
|
||||
}
|
||||
this._lastMouseDownTime = currentTime;
|
||||
this._lastMousePosition = [event.pageX, event.pageY];
|
||||
this._clickCount++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum number of pixels in each direction the mouse has moved.
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _distanceFromLastMousePosition(event: MouseEvent): number {
|
||||
const result = Math.max(
|
||||
Math.abs(this._lastMousePosition[0] - event.pageX),
|
||||
Math.abs(this._lastMousePosition[1] - event.pageY));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the mousemove event when the mouse button is down, recording the
|
||||
* end of the selection and refreshing the selection.
|
||||
* @param event The mousemove event.
|
||||
*/
|
||||
private _onMouseMove(event: MouseEvent) {
|
||||
// Record the previous position so we know whether to redraw the selection
|
||||
// at the end.
|
||||
const previousSelectionEnd = this._model.selectionEnd ? [this._model.selectionEnd[0], this._model.selectionEnd[1]] : null;
|
||||
|
||||
// Set the initial selection end based on the mouse coordinates
|
||||
this._model.selectionEnd = this._getMouseBufferCoords(event);
|
||||
|
||||
// Select the entire line if line select mode is active.
|
||||
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.
|
||||
this._dragScrollAmount = this._getMouseEventScrollAmount(event);
|
||||
|
||||
// If the cursor was above or below the viewport, make sure it's at the
|
||||
// start or end of the viewport respectively.
|
||||
if (this._dragScrollAmount > 0) {
|
||||
this._model.selectionEnd[0] = this._terminal.cols - 1;
|
||||
} else if (this._dragScrollAmount < 0) {
|
||||
this._model.selectionEnd[0] = 0;
|
||||
}
|
||||
|
||||
// If the character is a wide character include the cell to the right in the
|
||||
// selection. Note that selections at the very end of the line will never
|
||||
// have a character.
|
||||
if (this._model.selectionEnd[1] < this._buffer.length) {
|
||||
const char = this._buffer.get(this._model.selectionEnd[1])[this._model.selectionEnd[0]];
|
||||
if (char && char[2] === 0) {
|
||||
this._model.selectionEnd[0]++;
|
||||
}
|
||||
}
|
||||
|
||||
// Only draw here if the selection changes.
|
||||
if (!previousSelectionEnd ||
|
||||
previousSelectionEnd[0] !== this._model.selectionEnd[0] ||
|
||||
previousSelectionEnd[1] !== this._model.selectionEnd[1]) {
|
||||
this.refresh(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The callback that occurs every DRAG_SCROLL_INTERVAL ms that does the
|
||||
* scrolling of the viewport.
|
||||
*/
|
||||
private _dragScroll() {
|
||||
if (this._dragScrollAmount) {
|
||||
this._terminal.scrollDisp(this._dragScrollAmount, false);
|
||||
// Re-evaluate selection
|
||||
if (this._dragScrollAmount > 0) {
|
||||
this._model.selectionEnd = [this._terminal.cols - 1, this._terminal.ydisp + this._terminal.rows];
|
||||
} else {
|
||||
this._model.selectionEnd = [0, this._terminal.ydisp];
|
||||
}
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the mouseup event, removing the mousedown listeners.
|
||||
* @param event The mouseup event.
|
||||
*/
|
||||
private _onMouseUp(event: MouseEvent) {
|
||||
this._removeMouseDownListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a viewport column to the character index on the buffer line, the
|
||||
* latter takes into account wide characters.
|
||||
* @param coords The coordinates to find the 2 index for.
|
||||
*/
|
||||
private _convertViewportColToCharacterIndex(bufferLine: any, coords: [number, number]): number {
|
||||
let charIndex = coords[0];
|
||||
for (let i = 0; coords[0] >= i; i++) {
|
||||
const char = bufferLine[i];
|
||||
if (char[LINE_DATA_WIDTH_INDEX] === 0) {
|
||||
charIndex--;
|
||||
}
|
||||
}
|
||||
return charIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets positional information for the word at the coordinated specified.
|
||||
* @param coords The coordinates to get the word at.
|
||||
*/
|
||||
private _getWordAt(coords: [number, number]): IWordPosition {
|
||||
const bufferLine = this._buffer.get(coords[1]);
|
||||
const line = this._translateBufferLineToString(bufferLine, false);
|
||||
|
||||
// Get actual index, taking into consideration wide characters
|
||||
let endIndex = this._convertViewportColToCharacterIndex(bufferLine, coords);
|
||||
let startIndex = endIndex;
|
||||
|
||||
// Record offset to be used later
|
||||
const charOffset = coords[0] - startIndex;
|
||||
let leftWideCharCount = 0;
|
||||
let rightWideCharCount = 0;
|
||||
|
||||
if (line.charAt(startIndex) === ' ') {
|
||||
// Expand until non-whitespace is hit
|
||||
while (startIndex > 0 && line.charAt(startIndex - 1) === ' ') {
|
||||
startIndex--;
|
||||
}
|
||||
while (endIndex < line.length && line.charAt(endIndex + 1) === ' ') {
|
||||
endIndex++;
|
||||
}
|
||||
} else {
|
||||
// Expand until whitespace is hit. This algorithm works by scanning left
|
||||
// and right from the starting position, keeping both the index format
|
||||
// (line) and the column format (bufferLine) in sync. When a wide
|
||||
// character is hit, it is recorded and the column index is adjusted.
|
||||
let startCol = coords[0];
|
||||
let endCol = coords[0];
|
||||
// Consider the initial position, skip it and increment the wide char
|
||||
// variable
|
||||
if (bufferLine[startCol][LINE_DATA_WIDTH_INDEX] === 0) {
|
||||
leftWideCharCount++;
|
||||
startCol--;
|
||||
}
|
||||
if (bufferLine[endCol][LINE_DATA_WIDTH_INDEX] === 2) {
|
||||
rightWideCharCount++;
|
||||
endCol++;
|
||||
}
|
||||
// Expand the string in both directions until a space is hit
|
||||
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++;
|
||||
startCol--;
|
||||
}
|
||||
startIndex--;
|
||||
startCol--;
|
||||
}
|
||||
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++;
|
||||
endCol++;
|
||||
}
|
||||
endIndex++;
|
||||
endCol++;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the line specified.
|
||||
* @param line The line index.
|
||||
*/
|
||||
protected _selectLineAt(line: number): void {
|
||||
this._model.selectionStart = [0, line];
|
||||
this._model.selectionStartLength = this._terminal.cols;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* @license MIT
|
||||
*/
|
||||
import { assert } from 'chai';
|
||||
import { ITerminal } from './Interfaces';
|
||||
import { SelectionModel } from './SelectionModel';
|
||||
|
||||
class TestSelectionModel extends SelectionModel {
|
||||
constructor(
|
||||
terminal: ITerminal
|
||||
) {
|
||||
super(terminal);
|
||||
}
|
||||
}
|
||||
|
||||
describe('SelectionManager', () => {
|
||||
let window: Window;
|
||||
let document: Document;
|
||||
|
||||
let terminal: ITerminal;
|
||||
let model: TestSelectionModel;
|
||||
|
||||
beforeEach(() => {
|
||||
terminal = <any>{ cols: 80, rows: 2, ybase: 0 };
|
||||
model = new TestSelectionModel(terminal);
|
||||
});
|
||||
|
||||
describe('clearSelection', () => {
|
||||
it('should clear the final selection', () => {
|
||||
model.selectionStart = [0, 0];
|
||||
model.selectionEnd = [10, 2];
|
||||
assert.deepEqual(model.finalSelectionStart, [0, 0]);
|
||||
assert.deepEqual(model.finalSelectionEnd, [10, 2]);
|
||||
model.clearSelection();
|
||||
assert.deepEqual(model.finalSelectionStart, null);
|
||||
assert.deepEqual(model.finalSelectionEnd, null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('areSelectionValuesReversed', () => {
|
||||
it('should return true when the selection end is before selection start', () => {
|
||||
model.selectionStart = [1, 0];
|
||||
model.selectionEnd = [0, 0];
|
||||
assert.equal(model.areSelectionValuesReversed(), true);
|
||||
model.selectionStart = [10, 2];
|
||||
model.selectionEnd = [0, 0];
|
||||
assert.equal(model.areSelectionValuesReversed(), true);
|
||||
});
|
||||
it('should return false when the selection end is after selection start', () => {
|
||||
model.selectionStart = [0, 0];
|
||||
model.selectionEnd = [1, 0];
|
||||
assert.equal(model.areSelectionValuesReversed(), false);
|
||||
model.selectionStart = [0, 0];
|
||||
model.selectionEnd = [10, 2];
|
||||
assert.equal(model.areSelectionValuesReversed(), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onTrim', () => {
|
||||
it('should trim a portion of the selection when a part of it is trimmed', () => {
|
||||
model.selectionStart = [0, 0];
|
||||
model.selectionEnd = [10, 2];
|
||||
model.onTrim(1);
|
||||
assert.deepEqual(model.finalSelectionStart, [0, 0]);
|
||||
assert.deepEqual(model.finalSelectionEnd, [10, 1]);
|
||||
model.onTrim(1);
|
||||
assert.deepEqual(model.finalSelectionStart, [0, 0]);
|
||||
assert.deepEqual(model.finalSelectionEnd, [10, 0]);
|
||||
});
|
||||
it('should clear selection when it is trimmed in its entirety', () => {
|
||||
model.selectionStart = [0, 0];
|
||||
model.selectionEnd = [10, 0];
|
||||
model.onTrim(1);
|
||||
assert.deepEqual(model.finalSelectionStart, null);
|
||||
assert.deepEqual(model.finalSelectionEnd, null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('finalSelectionStart', () => {
|
||||
it('should return the start of the buffer if select all is active', () => {
|
||||
model.isSelectAllActive = true;
|
||||
assert.deepEqual(model.finalSelectionStart, [0, 0]);
|
||||
});
|
||||
it('should return selection start if there is no selection end', () => {
|
||||
model.selectionStart = [2, 2];
|
||||
assert.deepEqual(model.finalSelectionStart, [2, 2]);
|
||||
});
|
||||
it('should return selection end if values are reversed', () => {
|
||||
model.selectionStart = [2, 2];
|
||||
model.selectionEnd = [3, 2];
|
||||
assert.deepEqual(model.finalSelectionStart, [2, 2]);
|
||||
model.selectionEnd = [1, 2];
|
||||
assert.deepEqual(model.finalSelectionStart, [1, 2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('finalSelectionEnd', () => {
|
||||
it('should return the end of the buffer if select all is active', () => {
|
||||
model.isSelectAllActive = true;
|
||||
assert.deepEqual(model.finalSelectionEnd, [80, 1]);
|
||||
});
|
||||
it('should return null if there is no selection start', () => {
|
||||
assert.equal(model.finalSelectionEnd, null);
|
||||
model.selectionEnd = [1, 2];
|
||||
assert.equal(model.finalSelectionEnd, null);
|
||||
});
|
||||
it('should return selection start + length if there is no selection end', () => {
|
||||
model.selectionStart = [2, 2];
|
||||
model.selectionStartLength = 2;
|
||||
assert.deepEqual(model.finalSelectionEnd, [4, 2]);
|
||||
});
|
||||
it('should return selection start + length if values are reversed', () => {
|
||||
model.selectionStart = [2, 2];
|
||||
model.selectionStartLength = 2;
|
||||
model.selectionEnd = [2, 1];
|
||||
assert.deepEqual(model.finalSelectionEnd, [4, 2]);
|
||||
});
|
||||
it('should return selection start + length if selection end is inside the start selection', () => {
|
||||
model.selectionStart = [2, 2];
|
||||
model.selectionStartLength = 2;
|
||||
model.selectionEnd = [3, 2];
|
||||
assert.deepEqual(model.finalSelectionEnd, [4, 2]);
|
||||
});
|
||||
it('should return selection end if selection end is after selection start + length', () => {
|
||||
model.selectionStart = [2, 2];
|
||||
model.selectionStartLength = 2;
|
||||
model.selectionEnd = [5, 2];
|
||||
assert.deepEqual(model.finalSelectionEnd, [5, 2]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal } from './Interfaces';
|
||||
|
||||
/**
|
||||
* Represents a selection within the buffer. This model only cares about column
|
||||
* and row coordinates, not wide characters.
|
||||
*/
|
||||
export class SelectionModel {
|
||||
/**
|
||||
* Whether select all is currently active.
|
||||
*/
|
||||
public isSelectAllActive: boolean;
|
||||
|
||||
/**
|
||||
* The [x, y] position the selection starts at.
|
||||
*/
|
||||
public selectionStart: [number, number];
|
||||
|
||||
/**
|
||||
* The minimal length of the selection from the start position. When double
|
||||
* clicking on a word, the word will be selected which makes the selection
|
||||
* start at the start of the word and makes this variable the length.
|
||||
*/
|
||||
public selectionStartLength: number;
|
||||
|
||||
/**
|
||||
* The [x, y] position the selection ends at.
|
||||
*/
|
||||
public selectionEnd: [number, number];
|
||||
|
||||
constructor(
|
||||
private _terminal: ITerminal
|
||||
) {
|
||||
this.clearSelection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the current selection.
|
||||
*/
|
||||
public clearSelection(): void {
|
||||
this.selectionStart = null;
|
||||
this.selectionEnd = null;
|
||||
this.isSelectAllActive = false;
|
||||
this.selectionStartLength = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* The final selection start, taking into consideration select all.
|
||||
*/
|
||||
public get finalSelectionStart(): [number, number] {
|
||||
if (this.isSelectAllActive) {
|
||||
return [0, 0];
|
||||
}
|
||||
|
||||
if (!this.selectionEnd || !this.selectionStart) {
|
||||
return this.selectionStart;
|
||||
}
|
||||
|
||||
return this.areSelectionValuesReversed() ? this.selectionEnd : this.selectionStart;
|
||||
}
|
||||
|
||||
/**
|
||||
* The final selection end, taking into consideration select all, double click
|
||||
* word selection and triple click line selection.
|
||||
*/
|
||||
public get finalSelectionEnd(): [number, number] {
|
||||
if (this.isSelectAllActive) {
|
||||
return [this._terminal.cols, this._terminal.ybase + this._terminal.rows - 1];
|
||||
}
|
||||
|
||||
if (!this.selectionStart) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Use the selection start if the end doesn't exist or they're reversed
|
||||
if (!this.selectionEnd || this.areSelectionValuesReversed()) {
|
||||
return [this.selectionStart[0] + this.selectionStartLength, this.selectionStart[1]];
|
||||
}
|
||||
|
||||
// Ensure the the word/line is selected after a double/triple click
|
||||
if (this.selectionStartLength) {
|
||||
// Select the larger of the two when start and end are on the same line
|
||||
if (this.selectionEnd[1] === this.selectionStart[1]) {
|
||||
return [Math.max(this.selectionStart[0] + this.selectionStartLength, this.selectionEnd[0]), this.selectionEnd[1]];
|
||||
}
|
||||
}
|
||||
return this.selectionEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the selection start and end are reversed.
|
||||
*/
|
||||
public areSelectionValuesReversed(): boolean {
|
||||
const start = this.selectionStart;
|
||||
const end = this.selectionEnd;
|
||||
return start[1] > end[1] || (start[1] === end[1] && start[0] > end[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the buffer being trimmed, adjust the selection position.
|
||||
* @param amount The amount the buffer is being trimmed.
|
||||
* @return Whether a refresh is necessary.
|
||||
*/
|
||||
public onTrim(amount: number): boolean {
|
||||
// Adjust the selection position based on the trimmed amount.
|
||||
if (this.selectionStart) {
|
||||
this.selectionStart[1] -= amount;
|
||||
}
|
||||
if (this.selectionEnd) {
|
||||
this.selectionEnd[1] -= amount;
|
||||
}
|
||||
|
||||
// The selection has moved off the buffer, clear it.
|
||||
if (this.selectionEnd && this.selectionEnd[1] < 0) {
|
||||
this.clearSelection();
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the selection start is trimmed, ensure the start column is 0.
|
||||
if (this.selectionStart && this.selectionStart[1] < 0) {
|
||||
this.selectionStart[1] = 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { Viewport } from './Viewport';
|
||||
describe('Viewport', () => {
|
||||
let terminal;
|
||||
let viewportElement;
|
||||
let selectionContainer;
|
||||
let charMeasure;
|
||||
let viewport;
|
||||
let scrollAreaElement;
|
||||
@@ -20,6 +21,11 @@ describe('Viewport', () => {
|
||||
style: {
|
||||
lineHeight: 0
|
||||
}
|
||||
},
|
||||
selectionContainer: {
|
||||
style: {
|
||||
height: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
viewportElement = {
|
||||
|
||||
@@ -13,6 +13,7 @@ export class Viewport {
|
||||
private currentRowHeight: number;
|
||||
private lastRecordedBufferLength: number;
|
||||
private lastRecordedViewportHeight: number;
|
||||
private lastTouchY: number;
|
||||
|
||||
/**
|
||||
* Creates a new Viewport.
|
||||
@@ -57,6 +58,7 @@ export class Viewport {
|
||||
if (rowHeightChanged || viewportHeightChanged) {
|
||||
this.lastRecordedViewportHeight = this.terminal.rows;
|
||||
this.viewportElement.style.height = this.charMeasure.height * this.terminal.rows + 'px';
|
||||
this.terminal.selectionContainer.style.height = this.viewportElement.style.height;
|
||||
}
|
||||
this.scrollArea.style.height = (this.charMeasure.height * this.lastRecordedBufferLength) + 'px';
|
||||
}
|
||||
@@ -120,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();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,21 +2,6 @@ import { assert } from 'chai';
|
||||
import * as Terminal from '../xterm';
|
||||
import * as Clipboard from './Clipboard';
|
||||
|
||||
|
||||
describe('evaluateCopiedTextProcessing', function () {
|
||||
it('should strip trailing whitespaces and replace nbsps with spaces', function () {
|
||||
let nonBreakingSpace = String.fromCharCode(160),
|
||||
copiedText = 'echo' + nonBreakingSpace + 'hello' + nonBreakingSpace,
|
||||
processedText = Clipboard.prepareTextForClipboard(copiedText);
|
||||
|
||||
// No trailing spaces
|
||||
assert.equal(processedText.match(/\s+$/), null);
|
||||
|
||||
// No non-breaking space
|
||||
assert.equal(processedText.indexOf(nonBreakingSpace), -1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('evaluatePastedTextProcessing', function () {
|
||||
it('should replace carriage return + line feed with line feed on windows', function () {
|
||||
const pastedText = 'foo\r\nbar\r\n',
|
||||
@@ -24,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');
|
||||
});
|
||||
});
|
||||
|
||||
+42
-94
@@ -5,7 +5,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal } from '../Interfaces';
|
||||
import { ITerminal, ISelectionManager } from '../Interfaces';
|
||||
|
||||
interface IWindow extends Window {
|
||||
clipboardData?: {
|
||||
@@ -16,35 +16,13 @@ interface IWindow extends Window {
|
||||
|
||||
declare var window: IWindow;
|
||||
|
||||
/**
|
||||
* Prepares text copied from terminal selection, to be saved in the clipboard by:
|
||||
* 1. stripping all trailing white spaces
|
||||
* 2. converting all non-breaking spaces to regular spaces
|
||||
* @param {string} text The copied text that needs processing for storing in clipboard
|
||||
* @returns {string}
|
||||
*/
|
||||
export function prepareTextForClipboard(text: string): string {
|
||||
let space = String.fromCharCode(32),
|
||||
nonBreakingSpace = String.fromCharCode(160),
|
||||
allNonBreakingSpaces = new RegExp(nonBreakingSpace, 'g'),
|
||||
processedText = text.split('\n').map(function (line) {
|
||||
// Strip all trailing white spaces and convert all non-breaking spaces
|
||||
// to regular spaces.
|
||||
let processedLine = line.replace(/\s+$/g, '').replace(allNonBreakingSpaces, space);
|
||||
|
||||
return processedLine;
|
||||
}).join('\n');
|
||||
|
||||
return processedText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares text to be pasted into the terminal by normalizing the line endings
|
||||
* @param text The pasted text that needs processing before inserting into the terminal
|
||||
*/
|
||||
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;
|
||||
}
|
||||
@@ -53,19 +31,15 @@ export function prepareTextForTerminal(text: string, isMSWindows: boolean): stri
|
||||
* Binds copy functionality to the given terminal.
|
||||
* @param {ClipboardEvent} ev The original copy event to be handled
|
||||
*/
|
||||
export function copyHandler(ev: ClipboardEvent, term: ITerminal) {
|
||||
// We cast `window` to `any` type, because TypeScript has not declared the `clipboardData`
|
||||
// property that we use below for Internet Explorer.
|
||||
let copiedText = window.getSelection().toString(),
|
||||
text = prepareTextForClipboard(copiedText);
|
||||
|
||||
export function copyHandler(ev: ClipboardEvent, term: ITerminal, selectionManager: ISelectionManager) {
|
||||
if (term.browser.isMSIE) {
|
||||
window.clipboardData.setData('Text', text);
|
||||
window.clipboardData.setData('Text', selectionManager.selectionText);
|
||||
} else {
|
||||
ev.clipboardData.setData('text/plain', text);
|
||||
ev.clipboardData.setData('text/plain', selectionManager.selectionText);
|
||||
}
|
||||
|
||||
ev.preventDefault(); // Prevent or the original text will be copied.
|
||||
// Prevent or the original text will be copied.
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,68 +75,42 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind to right-click event and allow right-click copy and paste.
|
||||
*
|
||||
* **Logic**
|
||||
* If text is selected and right-click happens on selected text, then
|
||||
* do nothing to allow seamless copying.
|
||||
* If no text is selected or right-click is outside of the selection
|
||||
* area, then bring the terminal's input below the cursor, in order to
|
||||
* trigger the event on the textarea and allow-right click paste, without
|
||||
* caring about disappearing selection.
|
||||
* @param {MouseEvent} ev The original right click event to be handled
|
||||
* @param {Terminal} term The terminal on which to apply the handled paste event
|
||||
* 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, term: ITerminal) {
|
||||
let s = document.getSelection(),
|
||||
selectedText = prepareTextForClipboard(s.toString()),
|
||||
clickIsOnSelection = false,
|
||||
x = ev.clientX,
|
||||
y = ev.clientY;
|
||||
|
||||
if (s.rangeCount) {
|
||||
let r = s.getRangeAt(0),
|
||||
cr = r.getClientRects();
|
||||
|
||||
for (let i = 0; i < cr.length; i++) {
|
||||
let rect = cr[i];
|
||||
|
||||
clickIsOnSelection = (
|
||||
(x > rect.left) && (x < rect.right) &&
|
||||
(y > rect.top) && (y < rect.bottom)
|
||||
);
|
||||
|
||||
if (clickIsOnSelection) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If we clicked on selection and selection is not a single space,
|
||||
// then mark the right click as copy-only. We check for the single
|
||||
// space selection, as this can happen when clicking on an
|
||||
// and there is not much pointing in copying a single space.
|
||||
if (selectedText.match(/^\s$/) || !selectedText.length) {
|
||||
clickIsOnSelection = false;
|
||||
}
|
||||
}
|
||||
|
||||
export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextAreaElement) {
|
||||
// Bring textarea at the cursor position
|
||||
if (!clickIsOnSelection) {
|
||||
term.textarea.style.position = 'fixed';
|
||||
term.textarea.style.width = '20px';
|
||||
term.textarea.style.height = '20px';
|
||||
term.textarea.style.left = (x - 10) + 'px';
|
||||
term.textarea.style.top = (y - 10) + 'px';
|
||||
term.textarea.style.zIndex = '1000';
|
||||
term.textarea.focus();
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.width = '20px';
|
||||
textarea.style.height = '20px';
|
||||
textarea.style.left = (ev.clientX - 10) + 'px';
|
||||
textarea.style.top = (ev.clientY - 10) + 'px';
|
||||
textarea.style.zIndex = '1000';
|
||||
|
||||
// Reset the terminal textarea's styling
|
||||
setTimeout(function () {
|
||||
term.textarea.style.position = null;
|
||||
term.textarea.style.width = null;
|
||||
term.textarea.style.height = null;
|
||||
term.textarea.style.left = null;
|
||||
term.textarea.style.top = null;
|
||||
term.textarea.style.zIndex = null;
|
||||
}, 4);
|
||||
}
|
||||
textarea.focus();
|
||||
|
||||
// Reset the terminal textarea's styling
|
||||
setTimeout(function () {
|
||||
textarea.style.position = null;
|
||||
textarea.style.width = null;
|
||||
textarea.style.height = null;
|
||||
textarea.style.left = null;
|
||||
textarea.style.top = null;
|
||||
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;
|
||||
|
||||
@@ -4,12 +4,15 @@
|
||||
* @module xterm/utils/CircularList
|
||||
* @license MIT
|
||||
*/
|
||||
export class CircularList<T> {
|
||||
import { EventEmitter } from '../EventEmitter';
|
||||
|
||||
export class CircularList<T> extends EventEmitter {
|
||||
private _array: T[];
|
||||
private _startIndex: number;
|
||||
private _length: number;
|
||||
|
||||
constructor(maxLength: number) {
|
||||
super();
|
||||
this._array = new Array<T>(maxLength);
|
||||
this._startIndex = 0;
|
||||
this._length = 0;
|
||||
@@ -43,8 +46,14 @@ export class CircularList<T> {
|
||||
this._length = newLength;
|
||||
}
|
||||
|
||||
public get forEach(): (callbackfn: (value: T, index: number, array: T[]) => void) => void {
|
||||
return this._array.forEach;
|
||||
public get forEach(): (callbackfn: (value: T, index: number) => void) => void {
|
||||
return (callbackfn: (value: T, index: number) => void) => {
|
||||
let i = 0;
|
||||
let length = this.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
callbackfn(this.get(i), i);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,6 +92,7 @@ export class CircularList<T> {
|
||||
if (this._startIndex === this.maxLength) {
|
||||
this._startIndex = 0;
|
||||
}
|
||||
this.emit('trim', 1);
|
||||
} else {
|
||||
this._length++;
|
||||
}
|
||||
@@ -106,13 +116,16 @@ export class CircularList<T> {
|
||||
* @param items The items to insert.
|
||||
*/
|
||||
public splice(start: number, deleteCount: number, ...items: T[]): void {
|
||||
// Delete items
|
||||
if (deleteCount) {
|
||||
for (let i = start; i < this._length - deleteCount; i++) {
|
||||
this._array[this._getCyclicIndex(i)] = this._array[this._getCyclicIndex(i + deleteCount)];
|
||||
}
|
||||
this._length -= deleteCount;
|
||||
}
|
||||
|
||||
if (items && items.length) {
|
||||
// Add items
|
||||
for (let i = this._length - 1; i >= start; i--) {
|
||||
this._array[this._getCyclicIndex(i + items.length)] = this._array[this._getCyclicIndex(i)];
|
||||
}
|
||||
@@ -120,9 +133,12 @@ export class CircularList<T> {
|
||||
this._array[this._getCyclicIndex(start + i)] = items[i];
|
||||
}
|
||||
|
||||
// Adjust length as needed
|
||||
if (this._length + items.length > this.maxLength) {
|
||||
this._startIndex += (this._length + items.length) - this.maxLength;
|
||||
const countToTrim = (this._length + items.length) - this.maxLength;
|
||||
this._startIndex += countToTrim;
|
||||
this._length = this.maxLength;
|
||||
this.emit('trim', countToTrim);
|
||||
} else {
|
||||
this._length += items.length;
|
||||
}
|
||||
@@ -139,6 +155,7 @@ export class CircularList<T> {
|
||||
}
|
||||
this._startIndex += count;
|
||||
this._length -= count;
|
||||
this.emit('trim', count);
|
||||
}
|
||||
|
||||
public shiftElements(start: number, count: number, offset: number): void {
|
||||
@@ -162,6 +179,7 @@ export class CircularList<T> {
|
||||
while (this._length > this.maxLength) {
|
||||
this._length--;
|
||||
this._startIndex++;
|
||||
this.emit('trim', 1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
+34
-25
@@ -4,6 +4,25 @@
|
||||
|
||||
import { CharMeasure } from './CharMeasure';
|
||||
|
||||
export function getCoordsRelativeToElement(event: MouseEvent, element: HTMLElement): [number, number] {
|
||||
// Ignore browsers that don't support MouseEvent.pageX
|
||||
if (event.pageX == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let x = event.pageX;
|
||||
let y = event.pageY;
|
||||
|
||||
// Converts the coordinates from being relative to the document to being
|
||||
// relative to the terminal.
|
||||
while (element && element !== self.document.documentElement) {
|
||||
x -= element.offsetLeft;
|
||||
y -= element.offsetTop;
|
||||
element = 'offsetParent' in element ? <HTMLElement>element.offsetParent : <HTMLElement>element.parentElement;
|
||||
}
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets coordinates within the terminal for a particular mouse event. The result
|
||||
* is returned as an array in the form [x, y] instead of an object as it's a
|
||||
@@ -11,30 +30,24 @@ import { CharMeasure } from './CharMeasure';
|
||||
* @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): [number, number] {
|
||||
// Ignore browsers that don't support MouseEvent.pageX
|
||||
if (event.pageX == null) {
|
||||
return null;
|
||||
}
|
||||
export function getCoords(event: MouseEvent, rowContainer: HTMLElement, charMeasure: CharMeasure, colCount: number, rowCount: number, isSelection?: boolean): [number, number] {
|
||||
const coords = getCoordsRelativeToElement(event, rowContainer);
|
||||
|
||||
let x = event.pageX;
|
||||
let y = event.pageY;
|
||||
let el = rowContainer;
|
||||
// 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);
|
||||
|
||||
// Converts the coordinates from being relative to the document to being
|
||||
// relative to the terminal.
|
||||
while (el && el !== self.document.documentElement) {
|
||||
x -= el.offsetLeft;
|
||||
y -= el.offsetTop;
|
||||
el = 'offsetParent' in el ? <HTMLElement>el.offsetParent : <HTMLElement>el.parentElement;
|
||||
}
|
||||
// Ensure coordinates are within the terminal viewport.
|
||||
coords[0] = Math.min(Math.max(coords[0], 1), colCount + 1);
|
||||
coords[1] = Math.min(Math.max(coords[1], 1), rowCount + 1);
|
||||
|
||||
// Convert to cols/rows
|
||||
x = Math.ceil(x / charMeasure.width);
|
||||
y = Math.ceil(y / charMeasure.height);
|
||||
|
||||
return [x, y];
|
||||
return coords;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,14 +61,10 @@ export function getCoords(event: MouseEvent, rowContainer: HTMLElement, charMeas
|
||||
* @param rowCount The number of rows in the terminal.
|
||||
*/
|
||||
export function getRawByteCoords(event: MouseEvent, rowContainer: HTMLElement, charMeasure: CharMeasure, colCount: number, rowCount: number): { x: number, y: number } {
|
||||
const coords = getCoords(event, rowContainer, charMeasure);
|
||||
const coords = getCoords(event, rowContainer, charMeasure, colCount, rowCount);
|
||||
let x = coords[0];
|
||||
let y = coords[1];
|
||||
|
||||
// Ensure coordinates are within the terminal viewport.
|
||||
x = Math.min(Math.max(x, 0), colCount);
|
||||
y = Math.min(Math.max(y, 0), rowCount);
|
||||
|
||||
// xterm sends raw bytes and starts at 32 (SP) for each.
|
||||
x += 32;
|
||||
y += 32;
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
+178
-52
@@ -13,16 +13,17 @@
|
||||
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';
|
||||
import { Parser } from './Parser';
|
||||
import { Renderer } from './Renderer';
|
||||
import { Linkifier } from './Linkifier';
|
||||
import { SelectionManager } from './SelectionManager';
|
||||
import { CharMeasure } from './utils/CharMeasure';
|
||||
import * as Browser from './utils/Browser';
|
||||
import * as Keyboard from './utils/Keyboard';
|
||||
import * as Mouse from './utils/Mouse';
|
||||
import { CHARSETS } from './Charsets';
|
||||
import { getRawByteCoords } from './utils/Mouse';
|
||||
|
||||
@@ -166,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
|
||||
@@ -220,6 +221,7 @@ function Terminal(options) {
|
||||
this.parser = new Parser(this.inputHandler, this);
|
||||
// Reuse renderer if the Terminal is being recreated via a Terminal.reset call.
|
||||
this.renderer = this.renderer || null;
|
||||
this.selectionManager = this.selectionManager || null;
|
||||
this.linkifier = this.linkifier || new Linkifier();
|
||||
|
||||
// user input states
|
||||
@@ -249,6 +251,10 @@ function Terminal(options) {
|
||||
while (i--) {
|
||||
this.lines.push(this.blankLine());
|
||||
}
|
||||
// Ensure the selection manager has the correct buffer
|
||||
if (this.selectionManager) {
|
||||
this.selectionManager.setBuffer(this.lines);
|
||||
}
|
||||
|
||||
this.tabs;
|
||||
this.setupStops();
|
||||
@@ -413,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;
|
||||
@@ -519,28 +534,43 @@ Terminal.prototype.initGlobal = function() {
|
||||
Terminal.bindBlur(this);
|
||||
|
||||
// Bind clipboard functionality
|
||||
on(this.element, 'copy', function (ev) {
|
||||
copyHandler.call(this, ev, term);
|
||||
});
|
||||
on(this.textarea, 'paste', function (ev) {
|
||||
pasteHandler.call(this, ev, term);
|
||||
});
|
||||
on(this.element, 'paste', function (ev) {
|
||||
pasteHandler.call(this, ev, term);
|
||||
on(this.element, 'copy', event => {
|
||||
// If mouse events are active it means the selection manager is disabled and
|
||||
// copy should be handled by the host program.
|
||||
if (this.mouseEvents) {
|
||||
return;
|
||||
}
|
||||
copyHandler(event, term, this.selectionManager);
|
||||
});
|
||||
const pasteHandlerWrapper = event => pasteHandler(event, term);
|
||||
on(this.textarea, 'paste', pasteHandlerWrapper);
|
||||
on(this.element, 'paste', pasteHandlerWrapper);
|
||||
|
||||
function rightClickHandlerWrapper (ev) {
|
||||
rightClickHandler.call(this, ev, term);
|
||||
}
|
||||
|
||||
// Handle right click context menus
|
||||
if (term.browser.isFirefox) {
|
||||
on(this.element, 'mousedown', function (ev) {
|
||||
if (ev.button == 2) {
|
||||
rightClickHandlerWrapper(ev);
|
||||
// Firefox doesn't appear to fire the contextmenu event on right click
|
||||
on(this.element, 'mousedown', event => {
|
||||
if (event.button == 2) {
|
||||
rightClickHandler(event, this.textarea, this.selectionManager);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
on(this.element, 'contextmenu', rightClickHandlerWrapper);
|
||||
on(this.element, 'contextmenu', event => {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -631,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');
|
||||
@@ -641,6 +670,11 @@ Terminal.prototype.open = function(parent, focus) {
|
||||
this.viewportScrollArea.classList.add('xterm-scroll-area');
|
||||
this.viewportElement.appendChild(this.viewportScrollArea);
|
||||
|
||||
// Create the selection container.
|
||||
this.selectionContainer = document.createElement('div');
|
||||
this.selectionContainer.classList.add('xterm-selection');
|
||||
this.element.appendChild(this.selectionContainer);
|
||||
|
||||
// Create the container that will hold the lines of the terminal and then
|
||||
// produce the lines the lines.
|
||||
this.rowContainer = document.createElement('div');
|
||||
@@ -684,12 +718,26 @@ Terminal.prototype.open = function(parent, focus) {
|
||||
|
||||
this.charMeasure = new CharMeasure(document, this.helperContainer);
|
||||
this.charMeasure.on('charsizechanged', function () {
|
||||
self.updateCharSizeCSS();
|
||||
self.updateCharSizeStyles();
|
||||
});
|
||||
this.charMeasure.measure();
|
||||
|
||||
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('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());
|
||||
|
||||
// Setup loop that draws to screen
|
||||
this.refresh(0, this.rows - 1);
|
||||
@@ -760,10 +808,11 @@ Terminal.loadAddon = function(addon, callback) {
|
||||
* Updates the helper CSS class with any changes necessary after the terminal's
|
||||
* character width has been changed.
|
||||
*/
|
||||
Terminal.prototype.updateCharSizeCSS = function() {
|
||||
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;}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1046,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);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1086,7 +1147,7 @@ Terminal.prototype.queueLinkification = function(start, end) {
|
||||
this.linkifier.linkifyRow(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Display the cursor element
|
||||
@@ -1100,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
|
||||
@@ -1128,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) {
|
||||
@@ -1166,6 +1229,9 @@ Terminal.prototype.scroll = function() {
|
||||
*/
|
||||
Terminal.prototype.scrollDisp = function(disp, suppressScrollEvent) {
|
||||
if (disp < 0) {
|
||||
if (this.ydisp === 0) {
|
||||
return;
|
||||
}
|
||||
this.userScrolling = true;
|
||||
} else if (disp + this.ydisp >= this.ybase) {
|
||||
this.userScrolling = false;
|
||||
@@ -1192,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);
|
||||
@@ -1234,7 +1300,7 @@ Terminal.prototype.write = function(data) {
|
||||
self.innerWrite();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Terminal.prototype.innerWrite = function() {
|
||||
var writeBatch = this.writeBuffer.splice(0, WRITE_BATCH_SIZE);
|
||||
@@ -1276,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) {
|
||||
@@ -1307,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
|
||||
@@ -1315,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
|
||||
@@ -1330,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.
|
||||
*/
|
||||
@@ -1340,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.
|
||||
@@ -1352,7 +1430,36 @@ 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
|
||||
* behavior outside of xterm.js.
|
||||
*/
|
||||
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
|
||||
@@ -1361,7 +1468,7 @@ Terminal.prototype.deregisterLinkMatcher = function(matcherId) {
|
||||
* @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;
|
||||
}
|
||||
|
||||
@@ -1685,6 +1792,10 @@ Terminal.prototype.evaluateKeyEscapeSequence = function(ev) {
|
||||
} else if (ev.keyCode >= 48 && ev.keyCode <= 57) {
|
||||
result.key = C0.ESC + (ev.keyCode - 48);
|
||||
}
|
||||
} else if (this.browser.isMac && !ev.altKey && !ev.ctrlKey && ev.metaKey) {
|
||||
if (ev.keyCode === 65) { // cmd + a
|
||||
this.selectAll();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -1722,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) {
|
||||
@@ -1747,7 +1862,7 @@ Terminal.prototype.keyPress = function(ev) {
|
||||
this.showCursor();
|
||||
this.handler(key);
|
||||
|
||||
return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1812,6 +1927,10 @@ Terminal.prototype.resize = function(x, y) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (y > this.getOption('scrollback')) {
|
||||
this.setOption('scrollback', y)
|
||||
}
|
||||
|
||||
var line
|
||||
, el
|
||||
, i
|
||||
@@ -1853,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--;
|
||||
@@ -2054,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;
|
||||
@@ -2064,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;
|
||||
}
|
||||
@@ -2173,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