Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae61292b10 | ||
|
|
8fd4bdd678 | ||
|
|
a7b051c52f | ||
|
|
b92bbf578b | ||
|
|
d4ec50e7de | ||
|
|
5a1c71c1d5 | ||
|
|
bbf36fe091 | ||
|
|
990bf5c973 | ||
|
|
b28bba56df | ||
|
|
21f3534668 | ||
|
|
fefc6c6a34 | ||
|
|
57a6fe5b87 | ||
|
|
9e446a9a0e | ||
|
|
16c0ad858a | ||
|
|
750ec3870c | ||
|
|
e9d54bf011 | ||
|
|
408403e2a0 | ||
|
|
a30bb22c7a | ||
|
|
a531b4dd9b | ||
|
|
10687b138d | ||
|
|
1108d0746e | ||
|
|
005785b84d | ||
|
|
2e8e64eb4d | ||
|
|
f44d57b2e8 | ||
|
|
4941d5a19e | ||
|
|
1f2d223e64 | ||
|
|
d1991396e7 | ||
|
|
478d824fa7 | ||
|
|
39a7b18065 | ||
|
|
2c62c3a8c3 | ||
|
|
4463f8d810 | ||
|
|
c05b6bd885 | ||
|
|
290c95ad71 | ||
|
|
e736f1223f | ||
|
|
cbe7b21090 | ||
|
|
d9ec47ab7a | ||
|
|
94b20d40bf | ||
|
|
202734f10c | ||
|
|
4656bd433c | ||
|
|
f6d5abf7b3 | ||
|
|
6ea56cd85e | ||
|
|
4d7ce4da46 | ||
|
|
39047767e4 | ||
|
|
8d4b5cdc55 | ||
|
|
00af63b271 | ||
|
|
608d78b6ac | ||
|
|
ac3595e8b4 | ||
|
|
5284a5a742 | ||
|
|
842ee6c164 | ||
|
|
d0dc387921 | ||
|
|
7762ab96a5 | ||
|
|
6528ba84ff | ||
|
|
0bcf30a21b | ||
|
|
86c78e726d | ||
|
|
cf9cb80cb4 | ||
|
|
04f2d52f26 | ||
|
|
192f751862 | ||
|
|
de143b552d | ||
|
|
d37a8ce539 | ||
|
|
2f66ec452c | ||
|
|
4b68976dfb | ||
|
|
3143ad7f02 | ||
|
|
631e331077 | ||
|
|
70be55df75 | ||
|
|
14c20c5f32 | ||
|
|
57bbbda7e8 | ||
|
|
8664f54a76 | ||
|
|
9514bfd31c | ||
|
|
9ce2894353 | ||
|
|
5c873c1ce2 | ||
|
|
4d4d9914a8 | ||
|
|
a19673d835 | ||
|
|
f7dbadc79b | ||
|
|
50d600877c | ||
|
|
cd2ac929c3 | ||
|
|
7401567856 | ||
|
|
a8d9c92ae1 | ||
|
|
bd68853575 | ||
|
|
69a6c04620 | ||
|
|
68766d540f | ||
|
|
a09e6a6238 | ||
|
|
30f74bf5f3 | ||
|
|
2708a76501 | ||
|
|
c8c22e3ddf | ||
|
|
7fc1fd947f | ||
|
|
7e69c5d0db | ||
|
|
0068c477b9 | ||
|
|
74474da7f3 | ||
|
|
e6e079bfa1 | ||
|
|
7dc8118367 | ||
|
|
5aa6656971 | ||
|
|
8979eea810 | ||
|
|
7d2bdacf77 | ||
|
|
1edc732c55 | ||
|
|
e03c93fb81 | ||
|
|
d23c65d1db | ||
|
|
07b5bd0b64 | ||
|
|
35483b3bf4 | ||
|
|
6ebf17bf04 | ||
|
|
5d37a17383 | ||
|
|
dade9ba050 | ||
|
|
5909885d43 | ||
|
|
0446cf3aee | ||
|
|
3a97dbecde | ||
|
|
c88bc62ff9 | ||
|
|
a2bfbeaa74 | ||
|
|
ade3e0943c | ||
|
|
16de86f5ea | ||
|
|
fed579238a | ||
|
|
11426bb0d0 | ||
|
|
5e004b21a3 | ||
|
|
b2ba281916 | ||
|
|
bcf223e235 | ||
|
|
d5c17a0cc9 | ||
|
|
54ed988fd1 | ||
|
|
1460a34796 | ||
|
|
bc156294aa | ||
|
|
09028d4d86 | ||
|
|
c35306aaa6 | ||
|
|
3289eed2a2 | ||
|
|
a76d4a6055 | ||
|
|
12e38404a2 | ||
|
|
08af72be1a | ||
|
|
daa6fa0a66 | ||
|
|
4aa8e402a4 | ||
|
|
a494e80a9f | ||
|
|
a8fe4107a3 | ||
|
|
f2049694c8 | ||
|
|
e59fef87cc | ||
|
|
d943a5c890 | ||
|
|
715c078b6d | ||
|
|
f9717bc635 | ||
|
|
7ef18cca1e | ||
|
|
c291ab1058 | ||
|
|
25e0e97ef2 | ||
|
|
0f28ccb093 | ||
|
|
f4bb4a22d4 | ||
|
|
4eede85ef6 | ||
|
|
bdb6647ae0 | ||
|
|
0d89fcdeeb | ||
|
|
4ffc076f84 | ||
|
|
2ed3997aaa | ||
|
|
a0f9768fe8 | ||
|
|
8b334b01a6 | ||
|
|
c6029765a4 | ||
|
|
520dacac27 | ||
|
|
dcc1b7a3e3 | ||
|
|
27c3663470 | ||
|
|
599271bde4 | ||
|
|
2716413e66 | ||
|
|
e5465841f8 | ||
|
|
03e54cce7d | ||
|
|
88205511e6 | ||
|
|
374a8b70eb | ||
|
|
48cf73c152 | ||
|
|
1dc88d2e29 | ||
|
|
7c1b1e2eb8 | ||
|
|
fb72f25067 | ||
|
|
3618c6495b | ||
|
|
443b2dbc28 | ||
|
|
53e995a2d8 | ||
|
|
53193d0a47 | ||
|
|
92341c3e7b | ||
|
|
57b8e064a8 | ||
|
|
3a310f1597 | ||
|
|
4d0217f816 | ||
|
|
559e932a2e | ||
|
|
aa427010e1 | ||
|
|
1aec57e8c0 | ||
|
|
cbbf6bd654 | ||
|
|
33b65bbfaa | ||
|
|
8e39650b5f | ||
|
|
f148238117 | ||
|
|
6057586ff3 | ||
|
|
e2267fce92 | ||
|
|
784a142dac | ||
|
|
71aa7027bd | ||
|
|
ca76ebc396 | ||
|
|
54187b3597 | ||
|
|
d58f5cac01 | ||
|
|
b3e8980270 | ||
|
|
4d14e00034 | ||
|
|
e75578440e | ||
|
|
70afb13c9b | ||
|
|
6a2c4b9823 | ||
|
|
e2cfd94005 | ||
|
|
9fe8ea8de2 | ||
|
|
46311b87a2 | ||
|
|
692ca1c9a3 | ||
|
|
bf5c1f0f78 | ||
|
|
467a93c8d9 | ||
|
|
3841edadb5 | ||
|
|
dc332480cc | ||
|
|
a194263183 | ||
|
|
dd7081f948 | ||
|
|
999fce4e52 | ||
|
|
0d49d1f64d | ||
|
|
11df1e1d80 | ||
|
|
a74ca25b09 | ||
|
|
32231dcd41 | ||
|
|
04a847f0e2 | ||
|
|
276e65fa0b | ||
|
|
a1421b151e | ||
|
|
71f5965f8e | ||
|
|
59fc4af9a0 | ||
|
|
381e6c1740 | ||
|
|
9a54cf639b | ||
|
|
01a4ec21da | ||
|
|
a65bd1a77c | ||
|
|
6439f6e856 | ||
|
|
d24caf14a4 | ||
|
|
52f132a570 | ||
|
|
424d3257d1 | ||
|
|
f47f095b89 | ||
|
|
de26be56c1 | ||
|
|
f964a34b34 | ||
|
|
6576d513b0 | ||
|
|
4af2d1ed41 | ||
|
|
9e27c231e5 | ||
|
|
ae3684c897 | ||
|
|
d4c621a182 | ||
|
|
9c68eb3b11 | ||
|
|
77cb697990 | ||
|
|
cee432a66a | ||
|
|
a553415216 | ||
|
|
cd8477a942 | ||
|
|
811b3cc326 | ||
|
|
a99cac45f7 | ||
|
|
3d34881c4e | ||
|
|
0340d91150 | ||
|
|
5c3315251b | ||
|
|
e769e787a8 | ||
|
|
96ed32698b | ||
|
|
e66928da20 | ||
|
|
c762885c4b | ||
|
|
63ced6f302 | ||
|
|
82f6547f10 | ||
|
|
841a0e010d | ||
|
|
85f25e77f5 | ||
|
|
2adaed87d1 | ||
|
|
4c93d8ddaa | ||
|
|
b35fa9576e | ||
|
|
091c42f7e6 | ||
|
|
6601133912 | ||
|
|
9273e8175e | ||
|
|
cd75d6fb68 | ||
|
|
01bfa1df4b | ||
|
|
123e074c16 | ||
|
|
929150a8e1 | ||
|
|
e3eb1b81b3 | ||
|
|
04c4448f97 | ||
|
|
0e45909c7e | ||
|
|
44366ffd3c | ||
|
|
49cf1a2cea | ||
|
|
14d75364eb | ||
|
|
2127128ef5 | ||
|
|
95ebba6fdb |
+2
-1
@@ -23,7 +23,7 @@ opening an issue, read these pointers.
|
|||||||
- Mention precisely what went wrong. What did you expect to happen? What happened instead? Describe the
|
- Mention precisely what went wrong. What did you expect to happen? What happened instead? Describe the
|
||||||
exact steps a maintainer has to take to make the problem occur.
|
exact steps a maintainer has to take to make the problem occur.
|
||||||
|
|
||||||
- If the problem can not be reproduced in the [demo of xterm.js](README.md#demo), please provide an HTML document that demonstrates the problem.
|
- If the problem can not be reproduced in the [demo of xterm.js](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo), please provide an HTML document that demonstrates the problem.
|
||||||
|
|
||||||
- Be polite. Issues with an indignant or belligerent tone tend to be moved to the
|
- Be polite. Issues with an indignant or belligerent tone tend to be moved to the
|
||||||
bottom of the pile.
|
bottom of the pile.
|
||||||
@@ -33,6 +33,7 @@ opening an issue, read these pointers.
|
|||||||
- Make sure you have a [GitHub account](https://github.com/join)
|
- Make sure you have a [GitHub account](https://github.com/join)
|
||||||
- Fork [xterm.js](https://github.com/sourcelair/xterm.js/)
|
- Fork [xterm.js](https://github.com/sourcelair/xterm.js/)
|
||||||
([how to fork a repo](https://help.github.com/articles/fork-a-repo))
|
([how to fork a repo](https://help.github.com/articles/fork-a-repo))
|
||||||
|
- Get the [xterm.js demo](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo) running
|
||||||
- Make your changes
|
- Make your changes
|
||||||
- If your changes are easy to test or likely to regress, add tests. Tests go into `test`, directory.
|
- If your changes are easy to test or likely to regress, add tests. Tests go into `test`, directory.
|
||||||
- Follow the general code style of the rest of the project (see below).
|
- Follow the general code style of the rest of the project (see below).
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
Copyright (c) 2017-2018, The xterm.js authors (https://github.com/xtermjs/xterm.js)
|
||||||
Copyright (c) 2014-2016, SourceLair Private Company (https://www.sourcelair.com)
|
Copyright (c) 2014-2016, SourceLair Private Company (https://www.sourcelair.com)
|
||||||
Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/)
|
Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# [](https://xtermjs.org)
|
# [](https://xtermjs.org)
|
||||||
|
|
||||||
[](https://xtermjs.visualstudio.com/xterm.js/_build/index?definitionId=1)
|
[](https://dev.azure.com/xtermjs/xterm.js/_build/latest?definitionId=3)
|
||||||
[](https://coveralls.io/github/xtermjs/xterm.js?branch=master)
|
[](https://coveralls.io/github/xtermjs/xterm.js?branch=master)
|
||||||
[](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
[](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||||
[](https://www.jsdelivr.com/package/npm/xterm)
|
[](https://www.jsdelivr.com/package/npm/xterm)
|
||||||
@@ -169,6 +169,8 @@ computational environment for Jupyter, supporting interactive data science and s
|
|||||||
- [**cPanel & WHM**](https://cpanel.com): The hosting platform of choice.
|
- [**cPanel & WHM**](https://cpanel.com): The hosting platform of choice.
|
||||||
- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js
|
- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js
|
||||||
- [**SSH Web Client**](https://github.com/roke22/PHP-SSH2-Web-Client): SSH Web Client with PHP.
|
- [**SSH Web Client**](https://github.com/roke22/PHP-SSH2-Web-Client): SSH Web Client with PHP.
|
||||||
|
- [**Shellvault**](https://www.shellvault.io): The cloud-based SSH terminal you can access from anywhere.
|
||||||
|
- [**Juno**](http://junolab.org/): A flexible Julia IDE, based on Atom.
|
||||||
|
|
||||||
[And much more...](https://github.com/xtermjs/xterm.js/network/dependents)
|
[And much more...](https://github.com/xtermjs/xterm.js/network/dependents)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# Node.js
|
||||||
|
# Build a general Node.js application with npm.
|
||||||
|
# Add steps that analyze code, save build artifacts, deploy, and more:
|
||||||
|
# https://docs.microsoft.com/vsts/pipelines/languages/javascript
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- job: Linux
|
||||||
|
pool:
|
||||||
|
vmImage: 'ubuntu-16.04'
|
||||||
|
steps:
|
||||||
|
- task: NodeTool@0
|
||||||
|
inputs:
|
||||||
|
versionSpec: '8.x'
|
||||||
|
displayName: 'Install Node.js'
|
||||||
|
- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2
|
||||||
|
inputs:
|
||||||
|
versionSpec: "1.9.4"
|
||||||
|
displayName: 'Install Yarn'
|
||||||
|
- script: |
|
||||||
|
yarn
|
||||||
|
displayName: 'Install dependencies and build'
|
||||||
|
- script: |
|
||||||
|
yarn mocha
|
||||||
|
displayName: 'Test'
|
||||||
|
- script: |
|
||||||
|
yarn lint
|
||||||
|
displayName: 'Lint'
|
||||||
|
|
||||||
|
- job: macOS
|
||||||
|
pool:
|
||||||
|
vmImage: 'xcode9-macos10.13'
|
||||||
|
steps:
|
||||||
|
- task: NodeTool@0
|
||||||
|
inputs:
|
||||||
|
versionSpec: '8.x'
|
||||||
|
displayName: 'Install Node.js'
|
||||||
|
- script: |
|
||||||
|
yarn
|
||||||
|
displayName: 'Install dependencies and build'
|
||||||
|
- script: |
|
||||||
|
yarn mocha
|
||||||
|
displayName: 'Test'
|
||||||
|
- script: |
|
||||||
|
yarn lint
|
||||||
|
displayName: 'Lint'
|
||||||
|
- script: |
|
||||||
|
yarn test-coverage
|
||||||
|
export COVERALLS_GIT_BRANCH=$BUILD_SOURCEBRANCH
|
||||||
|
yarn coveralls
|
||||||
|
displayName: 'Generate and publish coverage'
|
||||||
|
|
||||||
|
- job: Windows
|
||||||
|
pool:
|
||||||
|
vmImage: 'vs2017-win2016'
|
||||||
|
steps:
|
||||||
|
- task: NodeTool@0
|
||||||
|
inputs:
|
||||||
|
versionSpec: '8.x'
|
||||||
|
displayName: 'Install Node.js'
|
||||||
|
- script: |
|
||||||
|
yarn
|
||||||
|
displayName: 'Install dependencies and build'
|
||||||
|
- script: |
|
||||||
|
yarn mocha
|
||||||
|
displayName: 'Test'
|
||||||
|
- script: |
|
||||||
|
yarn lint
|
||||||
|
displayName: 'Lint'
|
||||||
@@ -1,11 +1,28 @@
|
|||||||
import * as Terminal from '../build/xterm';
|
/**
|
||||||
import * as attach from '../build/addons/attach/attach';
|
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||||
import * as fit from '../build/addons/fit/fit';
|
* @license MIT
|
||||||
import * as fullscreen from '../build/addons/fullscreen/fullscreen';
|
*
|
||||||
import * as search from '../build/addons/search/search';
|
* This file is the entry point for browserify.
|
||||||
import * as webLinks from '../build/addons/webLinks/webLinks';
|
*/
|
||||||
import * as winptyCompat from '../build/addons/winptyCompat/winptyCompat';
|
|
||||||
|
|
||||||
|
/// <reference path="../typings/xterm.d.ts"/>
|
||||||
|
|
||||||
|
import { Terminal } from '../lib/public/Terminal';
|
||||||
|
import * as attach from '../lib/addons/attach/attach';
|
||||||
|
import * as fit from '../lib/addons/fit/fit';
|
||||||
|
import * as fullscreen from '../lib/addons/fullscreen/fullscreen';
|
||||||
|
import * as search from '../lib/addons/search/search';
|
||||||
|
import * as webLinks from '../lib/addons/webLinks/webLinks';
|
||||||
|
import * as winptyCompat from '../lib/addons/winptyCompat/winptyCompat';
|
||||||
|
|
||||||
|
// Pulling in the module's types relies on the <reference> above, it's looks a
|
||||||
|
// little weird here as we're importing "this" module
|
||||||
|
import { Terminal as TerminalType } from 'xterm';
|
||||||
|
|
||||||
|
export interface IWindowWithTerminal extends Window {
|
||||||
|
term: TerminalType;
|
||||||
|
}
|
||||||
|
declare let window: IWindowWithTerminal;
|
||||||
|
|
||||||
Terminal.applyAddon(attach);
|
Terminal.applyAddon(attach);
|
||||||
Terminal.applyAddon(fit);
|
Terminal.applyAddon(fit);
|
||||||
@@ -15,20 +32,20 @@ Terminal.applyAddon(webLinks);
|
|||||||
Terminal.applyAddon(winptyCompat);
|
Terminal.applyAddon(winptyCompat);
|
||||||
|
|
||||||
|
|
||||||
var term,
|
let term;
|
||||||
protocol,
|
let protocol;
|
||||||
socketURL,
|
let socketURL;
|
||||||
socket,
|
let socket;
|
||||||
pid;
|
let pid;
|
||||||
|
|
||||||
var terminalContainer = document.getElementById('terminal-container'),
|
const terminalContainer = document.getElementById('terminal-container');
|
||||||
actionElements = {
|
const actionElements = {
|
||||||
findNext: document.querySelector('#find-next'),
|
findNext: <HTMLInputElement>document.querySelector('#find-next'),
|
||||||
findPrevious: document.querySelector('#find-previous')
|
findPrevious: <HTMLInputElement>document.querySelector('#find-previous')
|
||||||
},
|
};
|
||||||
paddingElement = document.getElementById('padding');
|
const paddingElement = <HTMLInputElement>document.getElementById('padding');
|
||||||
|
|
||||||
function setPadding() {
|
function setPadding(): void {
|
||||||
term.element.style.padding = parseInt(paddingElement.value, 10).toString() + 'px';
|
term.element.style.padding = parseInt(paddingElement.value, 10).toString() + 'px';
|
||||||
term.fit();
|
term.fit();
|
||||||
}
|
}
|
||||||
@@ -52,20 +69,20 @@ const disposeRecreateButtonHandler = () => {
|
|||||||
|
|
||||||
document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
|
document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
|
||||||
|
|
||||||
function createTerminal() {
|
function createTerminal(): void {
|
||||||
// Clean terminal
|
// Clean terminal
|
||||||
while (terminalContainer.children.length) {
|
while (terminalContainer.children.length) {
|
||||||
terminalContainer.removeChild(terminalContainer.children[0]);
|
terminalContainer.removeChild(terminalContainer.children[0]);
|
||||||
}
|
}
|
||||||
term = new Terminal({});
|
term = new Terminal({});
|
||||||
window.term = term; // Expose `term` to window for debugging purposes
|
window.term = term; // Expose `term` to window for debugging purposes
|
||||||
term.on('resize', function (size) {
|
term.on('resize', (size: { cols: number, rows: number }) => {
|
||||||
if (!pid) {
|
if (!pid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var cols = size.cols,
|
const cols = size.cols;
|
||||||
rows = size.rows,
|
const rows = size.rows;
|
||||||
url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows;
|
const url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows;
|
||||||
|
|
||||||
fetch(url, {method: 'POST'});
|
fetch(url, {method: 'POST'});
|
||||||
});
|
});
|
||||||
@@ -80,32 +97,42 @@ function createTerminal() {
|
|||||||
|
|
||||||
addDomListener(paddingElement, 'change', setPadding);
|
addDomListener(paddingElement, 'change', setPadding);
|
||||||
|
|
||||||
addDomListener(actionElements.findNext, 'keypress', function (e) {
|
addDomListener(actionElements.findNext, 'keypress', (e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
term.findNext(actionElements.findNext.value);
|
const searchOptions = {
|
||||||
|
regex: (document.getElementById('regex') as HTMLInputElement).checked,
|
||||||
|
wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked,
|
||||||
|
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked
|
||||||
|
};
|
||||||
|
term.findNext(actionElements.findNext.value, searchOptions);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
addDomListener(actionElements.findPrevious, 'keypress', function (e) {
|
addDomListener(actionElements.findPrevious, 'keypress', (e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
term.findPrevious(actionElements.findPrevious.value);
|
const searchOptions = {
|
||||||
|
regex: (document.getElementById('regex') as HTMLInputElement).checked,
|
||||||
|
wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked,
|
||||||
|
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked
|
||||||
|
};
|
||||||
|
term.findPrevious(actionElements.findPrevious.value, searchOptions);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// fit is called within a setTimeout, cols and rows need this.
|
// fit is called within a setTimeout, cols and rows need this.
|
||||||
setTimeout(function () {
|
setTimeout(() => {
|
||||||
initOptions(term);
|
initOptions(term);
|
||||||
document.getElementById(`opt-cols`).value = term.cols;
|
// TODO: Clean this up, opt-cols/rows doesn't exist anymore
|
||||||
document.getElementById(`opt-rows`).value = term.rows;
|
(<HTMLInputElement>document.getElementById(`opt-cols`)).value = term.cols;
|
||||||
paddingElement.value = 0;
|
(<HTMLInputElement>document.getElementById(`opt-rows`)).value = term.rows;
|
||||||
|
paddingElement.value = '0';
|
||||||
|
|
||||||
// Set terminal size again to set the specific dimensions on the demo
|
// Set terminal size again to set the specific dimensions on the demo
|
||||||
updateTerminalSize();
|
updateTerminalSize();
|
||||||
|
|
||||||
fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then(function (res) {
|
fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then((res) => {
|
||||||
|
res.text().then((processId) => {
|
||||||
res.text().then(function (processId) {
|
|
||||||
pid = processId;
|
pid = processId;
|
||||||
socketURL += processId;
|
socketURL += processId;
|
||||||
socket = new WebSocket(socketURL);
|
socket = new WebSocket(socketURL);
|
||||||
@@ -117,22 +144,20 @@ function createTerminal() {
|
|||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function runRealTerminal() {
|
function runRealTerminal(): void {
|
||||||
term.attach(socket);
|
term.attach(socket);
|
||||||
term._initialized = true;
|
term._initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function runFakeTerminal() {
|
function runFakeTerminal(): void {
|
||||||
if (term._initialized) {
|
if (term._initialized) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
term._initialized = true;
|
term._initialized = true;
|
||||||
|
|
||||||
var shellprompt = '$ ';
|
term.prompt = () => {
|
||||||
|
term.write('\r\n$ ');
|
||||||
term.prompt = function () {
|
|
||||||
term.write('\r\n' + shellprompt);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
term.writeln('Welcome to xterm.js');
|
term.writeln('Welcome to xterm.js');
|
||||||
@@ -141,14 +166,12 @@ function runFakeTerminal() {
|
|||||||
term.writeln('');
|
term.writeln('');
|
||||||
term.prompt();
|
term.prompt();
|
||||||
|
|
||||||
term._core.register(term.addDisposableListener('key', function (key, ev) {
|
term._core.register(term.addDisposableListener('key', (key, ev) => {
|
||||||
var printable = (
|
const printable = !ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey;
|
||||||
!ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey
|
|
||||||
);
|
|
||||||
|
|
||||||
if (ev.keyCode == 13) {
|
if (ev.keyCode === 13) {
|
||||||
term.prompt();
|
term.prompt();
|
||||||
} else if (ev.keyCode == 8) {
|
} else if (ev.keyCode === 8) {
|
||||||
// Do not delete the prompt
|
// Do not delete the prompt
|
||||||
if (term.x > 2) {
|
if (term.x > 2) {
|
||||||
term.write('\b \b');
|
term.write('\b \b');
|
||||||
@@ -158,13 +181,13 @@ function runFakeTerminal() {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
term._core.register(term.addDisposableListener('paste', function (data, ev) {
|
term._core.register(term.addDisposableListener('paste', (data, ev) => {
|
||||||
term.write(data);
|
term.write(data);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function initOptions(term) {
|
function initOptions(term: TerminalType): void {
|
||||||
var blacklistedOptions = [
|
const blacklistedOptions = [
|
||||||
// Internal only options
|
// Internal only options
|
||||||
'cancelEvents',
|
'cancelEvents',
|
||||||
'convertEol',
|
'convertEol',
|
||||||
@@ -176,7 +199,7 @@ function initOptions(term) {
|
|||||||
// Complex option
|
// Complex option
|
||||||
'theme'
|
'theme'
|
||||||
];
|
];
|
||||||
var stringOptions = {
|
const stringOptions = {
|
||||||
bellSound: null,
|
bellSound: null,
|
||||||
bellStyle: ['none', 'sound'],
|
bellStyle: ['none', 'sound'],
|
||||||
cursorStyle: ['block', 'underline', 'bar'],
|
cursorStyle: ['block', 'underline', 'bar'],
|
||||||
@@ -186,9 +209,9 @@ function initOptions(term) {
|
|||||||
fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
|
fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
|
||||||
rendererType: ['dom', 'canvas']
|
rendererType: ['dom', 'canvas']
|
||||||
};
|
};
|
||||||
var options = Object.keys(term._core.options);
|
const options = Object.keys((<any>term)._core.options);
|
||||||
var booleanOptions = [];
|
const booleanOptions = [];
|
||||||
var numberOptions = [];
|
const numberOptions = [];
|
||||||
options.filter(o => blacklistedOptions.indexOf(o) === -1).forEach(o => {
|
options.filter(o => blacklistedOptions.indexOf(o) === -1).forEach(o => {
|
||||||
switch (typeof term.getOption(o)) {
|
switch (typeof term.getOption(o)) {
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
@@ -204,7 +227,7 @@ function initOptions(term) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var html = '';
|
let html = '';
|
||||||
html += '<div class="option-group">';
|
html += '<div class="option-group">';
|
||||||
booleanOptions.forEach(o => {
|
booleanOptions.forEach(o => {
|
||||||
html += `<div class="option"><label><input id="opt-${o}" type="checkbox" ${term.getOption(o) ? 'checked' : ''}/> ${o}</label></div>`;
|
html += `<div class="option"><label><input id="opt-${o}" type="checkbox" ${term.getOption(o) ? 'checked' : ''}/> ${o}</label></div>`;
|
||||||
@@ -218,24 +241,24 @@ function initOptions(term) {
|
|||||||
if (stringOptions[o]) {
|
if (stringOptions[o]) {
|
||||||
html += `<div class="option"><label>${o} <select id="opt-${o}">${stringOptions[o].map(v => `<option ${term.getOption(o) === v ? 'selected' : ''}>${v}</option>`).join('')}</select></label></div>`;
|
html += `<div class="option"><label>${o} <select id="opt-${o}">${stringOptions[o].map(v => `<option ${term.getOption(o) === v ? 'selected' : ''}>${v}</option>`).join('')}</select></label></div>`;
|
||||||
} else {
|
} else {
|
||||||
html += `<div class="option"><label>${o} <input id="opt-${o}" type="text" value="${term.getOption(o)}"/></label></div>`
|
html += `<div class="option"><label>${o} <input id="opt-${o}" type="text" value="${term.getOption(o)}"/></label></div>`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
html += '</div>';
|
html += '</div>';
|
||||||
|
|
||||||
var container = document.getElementById('options-container');
|
const container = document.getElementById('options-container');
|
||||||
container.innerHTML = html;
|
container.innerHTML = html;
|
||||||
|
|
||||||
// Attach listeners
|
// Attach listeners
|
||||||
booleanOptions.forEach(o => {
|
booleanOptions.forEach(o => {
|
||||||
var input = document.getElementById(`opt-${o}`);
|
const input = <HTMLInputElement>document.getElementById(`opt-${o}`);
|
||||||
addDomListener(input, 'change', () => {
|
addDomListener(input, 'change', () => {
|
||||||
console.log('change', o, input.checked);
|
console.log('change', o, input.checked);
|
||||||
term.setOption(o, input.checked);
|
term.setOption(o, input.checked);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
numberOptions.forEach(o => {
|
numberOptions.forEach(o => {
|
||||||
var input = document.getElementById(`opt-${o}`);
|
const input = <HTMLInputElement>document.getElementById(`opt-${o}`);
|
||||||
addDomListener(input, 'change', () => {
|
addDomListener(input, 'change', () => {
|
||||||
console.log('change', o, input.value);
|
console.log('change', o, input.value);
|
||||||
if (o === 'cols' || o === 'rows') {
|
if (o === 'cols' || o === 'rows') {
|
||||||
@@ -246,7 +269,7 @@ function initOptions(term) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
Object.keys(stringOptions).forEach(o => {
|
Object.keys(stringOptions).forEach(o => {
|
||||||
var input = document.getElementById(`opt-${o}`);
|
const input = <HTMLInputElement>document.getElementById(`opt-${o}`);
|
||||||
addDomListener(input, 'change', () => {
|
addDomListener(input, 'change', () => {
|
||||||
console.log('change', o, input.value);
|
console.log('change', o, input.value);
|
||||||
term.setOption(o, input.value);
|
term.setOption(o, input.value);
|
||||||
@@ -254,16 +277,16 @@ function initOptions(term) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addDomListener(element, type, handler) {
|
function addDomListener(element: HTMLElement, type: string, handler: (...args: any[]) => any): void {
|
||||||
element.addEventListener(type, handler);
|
element.addEventListener(type, handler);
|
||||||
term._core.register({ dispose: () => element.removeEventListener(type, handler) });
|
term._core.register({ dispose: () => element.removeEventListener(type, handler) });
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTerminalSize() {
|
function updateTerminalSize(): void {
|
||||||
var cols = parseInt(document.getElementById(`opt-cols`).value, 10);
|
const cols = parseInt((<HTMLInputElement>document.getElementById(`opt-cols`)).value, 10);
|
||||||
var rows = parseInt(document.getElementById(`opt-rows`).value, 10);
|
const rows = parseInt((<HTMLInputElement>document.getElementById(`opt-rows`)).value, 10);
|
||||||
var width = (cols * term._core.renderer.dimensions.actualCellWidth + term._core.viewport.scrollBarWidth).toString() + 'px';
|
const width = (cols * term._core.renderer.dimensions.actualCellWidth + term._core.viewport.scrollBarWidth).toString() + 'px';
|
||||||
var height = (rows * term._core.renderer.dimensions.actualCellHeight).toString() + 'px';
|
const height = (rows * term._core.renderer.dimensions.actualCellHeight).toString() + 'px';
|
||||||
terminalContainer.style.width = width;
|
terminalContainer.style.width = width;
|
||||||
terminalContainer.style.height = height;
|
terminalContainer.style.height = height;
|
||||||
term.fit();
|
term.fit();
|
||||||
+4
-1
@@ -16,6 +16,9 @@
|
|||||||
<p>
|
<p>
|
||||||
<label>Find next <input id="find-next"/></label>
|
<label>Find next <input id="find-next"/></label>
|
||||||
<label>Find previous <input id="find-previous"/></label>
|
<label>Find previous <input id="find-previous"/></label>
|
||||||
|
<label>Use regex<input type="checkbox" id="regex"/></label>
|
||||||
|
<label>Case sensitive<input type="checkbox" id="case-sensitive"/></label>
|
||||||
|
<label>Whole word<input type="checkbox" id="whole-word"/></label>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -33,6 +36,6 @@
|
|||||||
<hr/>
|
<hr/>
|
||||||
<p><strong>Attention:</strong> The demo is a barebones implementation and is designed for the development and evaluation of xterm.js only. Exposing the demo to the public as is would introduce security risks for the host.</p>
|
<p><strong>Attention:</strong> The demo is a barebones implementation and is designed for the development and evaluation of xterm.js only. Exposing the demo to the public as is would introduce security risks for the host.</p>
|
||||||
<button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button>
|
<button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button>
|
||||||
<script src="dist/bundle.js" defer ></script>
|
<script src="dist/client-bundle.js" defer ></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -17,12 +17,8 @@ app.get('/style.css', function(req, res){
|
|||||||
res.sendFile(__dirname + '/style.css');
|
res.sendFile(__dirname + '/style.css');
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/dist/bundle.js', function(req, res){
|
app.get('/dist/client-bundle.js', function(req, res){
|
||||||
res.sendFile(__dirname + '/dist/bundle.js');
|
res.sendFile(__dirname + '/dist/client-bundle.js');
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/dist/bundle.js.map', function(req, res){
|
|
||||||
res.sendFile(__dirname + '/dist/bundle.js.map');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/terminals', function (req, res) {
|
app.post('/terminals', function (req, res) {
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*
|
||||||
|
* This file is the entry point for browserify.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const cp = require('child_process');
|
||||||
|
const path = require('path');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
|
||||||
|
// Launch server
|
||||||
|
cp.spawn('node', [path.resolve(__dirname, 'server.js')], { stdio: 'inherit' });
|
||||||
|
|
||||||
|
// Build/watch client source
|
||||||
|
const clientConfig = {
|
||||||
|
entry: path.resolve(__dirname, 'client.ts'),
|
||||||
|
devtool: 'inline-source-map',
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
use: 'ts-loader',
|
||||||
|
exclude: /node_modules/
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: [ '.tsx', '.ts', '.js' ]
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
filename: 'client-bundle.js',
|
||||||
|
path: path.resolve(__dirname, 'dist')
|
||||||
|
},
|
||||||
|
mode: 'development',
|
||||||
|
watch: true
|
||||||
|
};
|
||||||
|
const compiler = webpack(clientConfig);
|
||||||
|
|
||||||
|
compiler.watch({
|
||||||
|
// Example watchOptions
|
||||||
|
aggregateTimeout: 300,
|
||||||
|
poll: undefined
|
||||||
|
}, (err, stats) => {
|
||||||
|
// Print watch/build result here...
|
||||||
|
console.log(stats.toString({
|
||||||
|
colors: true
|
||||||
|
}));
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es5",
|
||||||
|
"rootDir": ".",
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"client.ts",
|
||||||
|
"../typings/xterm.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -4,6 +4,11 @@
|
|||||||
],
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es5"
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"es6"
|
||||||
|
],
|
||||||
|
"noEmit": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-12
@@ -14,7 +14,6 @@ const source = require('vinyl-source-stream');
|
|||||||
const sourcemaps = require('gulp-sourcemaps');
|
const sourcemaps = require('gulp-sourcemaps');
|
||||||
const ts = require('gulp-typescript');
|
const ts = require('gulp-typescript');
|
||||||
const util = require('gulp-util');
|
const util = require('gulp-util');
|
||||||
const webpack = require('webpack-stream');
|
|
||||||
|
|
||||||
const buildDir = process.env.BUILD_DIR || 'build';
|
const buildDir = process.env.BUILD_DIR || 'build';
|
||||||
const tsProject = ts.createProject('tsconfig.json');
|
const tsProject = ts.createProject('tsconfig.json');
|
||||||
@@ -137,17 +136,6 @@ gulp.task('sorcery-addons', ['browserify-addons'], function () {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task('webpack', ['build'], function() {
|
|
||||||
return gulp.src('demo/main.js')
|
|
||||||
.pipe(webpack(require('./webpack.config.js')))
|
|
||||||
.pipe(gulp.dest('demo/dist/'));
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
gulp.task('watch-demo', ['webpack'], () => {
|
|
||||||
gulp.watch(['./demo/*', './lib/**/*'], ['webpack']);
|
|
||||||
});
|
|
||||||
|
|
||||||
gulp.task('build', ['sorcery', 'sorcery-addons']);
|
gulp.task('build', ['sorcery', 'sorcery-addons']);
|
||||||
gulp.task('test', ['mocha']);
|
gulp.task('test', ['mocha']);
|
||||||
gulp.task('default', ['build']);
|
gulp.task('default', ['build']);
|
||||||
|
|||||||
+12
-9
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "xterm",
|
"name": "xterm",
|
||||||
"description": "Full xterm terminal, in your browser",
|
"description": "Full xterm terminal, in your browser",
|
||||||
"version": "3.6.0",
|
"version": "3.8.0",
|
||||||
"main": "lib/public/Terminal.js",
|
"main": "lib/public/Terminal.js",
|
||||||
"types": "typings/xterm.d.ts",
|
"types": "typings/xterm.d.ts",
|
||||||
"repository": "https://github.com/xtermjs/xterm.js",
|
"repository": "https://github.com/xtermjs/xterm.js",
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
"@types/jsdom": "11.0.1",
|
"@types/jsdom": "11.0.1",
|
||||||
"@types/mocha": "^2.2.33",
|
"@types/mocha": "^2.2.33",
|
||||||
"@types/node": "6.0.108",
|
"@types/node": "6.0.108",
|
||||||
|
"@types/webpack": "^4.4.11",
|
||||||
"browserify": "^13.3.0",
|
"browserify": "^13.3.0",
|
||||||
"chai": "3.5.0",
|
"chai": "3.5.0",
|
||||||
"concurrently": "^3.5.1",
|
"concurrently": "^3.5.1",
|
||||||
@@ -32,24 +33,25 @@
|
|||||||
"merge-stream": "^1.0.1",
|
"merge-stream": "^1.0.1",
|
||||||
"node-pty": "0.7.6",
|
"node-pty": "0.7.6",
|
||||||
"nodemon": "1.10.2",
|
"nodemon": "1.10.2",
|
||||||
"npm-run-all": "^4.1.2",
|
|
||||||
"nyc": "^11.8.0",
|
"nyc": "^11.8.0",
|
||||||
"sorcery": "^0.10.0",
|
"sorcery": "^0.10.0",
|
||||||
"source-map-loader": "^0.2.3",
|
"source-map-loader": "^0.2.3",
|
||||||
|
"ts-loader": "^4.5.0",
|
||||||
"tslint": "^5.9.1",
|
"tslint": "^5.9.1",
|
||||||
"tslint-consistent-codestyle": "^1.13.0",
|
"tslint-consistent-codestyle": "^1.13.0",
|
||||||
"typescript": "2.8.3",
|
"typescript": "3.0",
|
||||||
"vinyl-buffer": "^1.0.0",
|
"vinyl-buffer": "^1.0.0",
|
||||||
"vinyl-source-stream": "^1.1.0",
|
"vinyl-source-stream": "^1.1.0",
|
||||||
"webpack": "^3.10.0",
|
"webpack": "^4.17.1",
|
||||||
"webpack-stream": "^4.0.0",
|
"webpack-cli": "^3.1.0",
|
||||||
"zmodem.js": "^0.1.5"
|
"zmodem.js": "^0.1.5"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "concurrently --kill-others-on-fail --names \"demo,server\" \"gulp watch-demo\" \"node demo/app\"",
|
"start": "node demo/start",
|
||||||
"start-zmodem": "node demo/zmodem/app",
|
"start-zmodem": "node demo/zmodem/app",
|
||||||
"lint": "tslint 'src/**/*.ts'",
|
"lint": "tslint 'src/**/*.ts' './demo/**/*.ts'",
|
||||||
"test": "npm-run-all mocha lint",
|
"test": "npm run mocha",
|
||||||
|
"posttest": "npm run lint",
|
||||||
"test-debug": "node --inspect-brk node_modules/.bin/gulp test",
|
"test-debug": "node --inspect-brk node_modules/.bin/gulp test",
|
||||||
"test-suite": "gulp mocha-suite --test",
|
"test-suite": "gulp mocha-suite --test",
|
||||||
"test-coverage": "nyc -x gulpfile.js -x '**/*test*' npm run mocha",
|
"test-coverage": "nyc -x gulpfile.js -x '**/*test*' npm run mocha",
|
||||||
@@ -61,6 +63,7 @@
|
|||||||
"prepublish": "npm run build",
|
"prepublish": "npm run build",
|
||||||
"coveralls": "nyc report --reporter=text-lcov | coveralls",
|
"coveralls": "nyc report --reporter=text-lcov | coveralls",
|
||||||
"webpack": "gulp webpack",
|
"webpack": "gulp webpack",
|
||||||
"watch": "concurrently --kill-others-on-fail --names \"lib,attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem,css\" \"tsc -w\" \"tsc -w -p ./src/addons/attach\" \"tsc -w -p ./src/addons/fit\" \"tsc -w -p ./src/addons/fullscreen\" \"tsc -w -p ./src/addons/search\" \"tsc -w -p ./src/addons/terminado\" \"tsc -w -p ./src/addons/webLinks\" \"tsc -w -p ./src/addons/winptyCompat\" \"tsc -w -p ./src/addons/zmodem\" \"gulp watch-css\""
|
"watch": "concurrently --kill-others-on-fail --names \"lib,css\" \"tsc -w\" \"gulp watch-css\"",
|
||||||
|
"watch-addons": "concurrently --kill-others-on-fail --names \"attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem\" \"tsc -w -p ./src/addons/attach\" \"tsc -w -p ./src/addons/fit\" \"tsc -w -p ./src/addons/fullscreen\" \"tsc -w -p ./src/addons/search\" \"tsc -w -p ./src/addons/terminado\" \"tsc -w -p ./src/addons/webLinks\" \"tsc -w -p ./src/addons/winptyCompat\" \"tsc -w -p ./src/addons/zmodem\""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+249
-50
@@ -3,11 +3,12 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { assert } from 'chai';
|
import { assert, expect } from 'chai';
|
||||||
import { ITerminal } from './Types';
|
import { ITerminal } from './Types';
|
||||||
import { Buffer } from './Buffer';
|
import { Buffer, DEFAULT_ATTR, CHAR_DATA_CHAR_INDEX } from './Buffer';
|
||||||
import { CircularList } from './common/CircularList';
|
import { CircularList } from './common/CircularList';
|
||||||
import { MockTerminal } from './utils/TestUtils.test';
|
import { MockTerminal, TestTerminal } from './utils/TestUtils.test';
|
||||||
|
import { BufferLine } from './BufferLine';
|
||||||
|
|
||||||
const INIT_COLS = 80;
|
const INIT_COLS = 80;
|
||||||
const INIT_ROWS = 24;
|
const INIT_ROWS = 24;
|
||||||
@@ -36,13 +37,13 @@ describe('Buffer', () => {
|
|||||||
|
|
||||||
describe('fillViewportRows', () => {
|
describe('fillViewportRows', () => {
|
||||||
it('should fill the buffer with blank lines based on the size of the viewport', () => {
|
it('should fill the buffer with blank lines based on the size of the viewport', () => {
|
||||||
const blankLineChar = terminal.blankLine()[0];
|
const blankLineChar = BufferLine.blankLine(terminal.cols, DEFAULT_ATTR).get(0);
|
||||||
buffer.fillViewportRows();
|
buffer.fillViewportRows();
|
||||||
assert.equal(buffer.lines.length, INIT_ROWS);
|
assert.equal(buffer.lines.length, INIT_ROWS);
|
||||||
for (let y = 0; y < INIT_ROWS; y++) {
|
for (let y = 0; y < INIT_ROWS; y++) {
|
||||||
assert.equal(buffer.lines.get(y).length, INIT_COLS);
|
assert.equal(buffer.lines.get(y).length, INIT_COLS);
|
||||||
for (let x = 0; x < INIT_COLS; x++) {
|
for (let x = 0; x < INIT_COLS; x++) {
|
||||||
assert.deepEqual(buffer.lines.get(y)[x], blankLineChar);
|
assert.deepEqual(buffer.lines.get(y).get(x), blankLineChar);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -66,40 +67,40 @@ describe('Buffer', () => {
|
|||||||
describe('wrapped', () => {
|
describe('wrapped', () => {
|
||||||
it('should return a range for the first row', () => {
|
it('should return a range for the first row', () => {
|
||||||
buffer.fillViewportRows();
|
buffer.fillViewportRows();
|
||||||
(<any> buffer.lines.get(1)).isWrapped = true;
|
buffer.lines.get(1).isWrapped = true;
|
||||||
assert.deepEqual(buffer.getWrappedRangeForLine(0), { first: 0, last: 1 });
|
assert.deepEqual(buffer.getWrappedRangeForLine(0), { first: 0, last: 1 });
|
||||||
});
|
});
|
||||||
it('should return a range for a middle row wrapping upwards', () => {
|
it('should return a range for a middle row wrapping upwards', () => {
|
||||||
buffer.fillViewportRows();
|
buffer.fillViewportRows();
|
||||||
(<any> buffer.lines.get(12)).isWrapped = true;
|
buffer.lines.get(12).isWrapped = true;
|
||||||
assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 11, last: 12 });
|
assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 11, last: 12 });
|
||||||
});
|
});
|
||||||
it('should return a range for a middle row wrapping downwards', () => {
|
it('should return a range for a middle row wrapping downwards', () => {
|
||||||
buffer.fillViewportRows();
|
buffer.fillViewportRows();
|
||||||
(<any> buffer.lines.get(13)).isWrapped = true;
|
buffer.lines.get(13).isWrapped = true;
|
||||||
assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 12, last: 13 });
|
assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 12, last: 13 });
|
||||||
});
|
});
|
||||||
it('should return a range for a middle row wrapping both ways', () => {
|
it('should return a range for a middle row wrapping both ways', () => {
|
||||||
buffer.fillViewportRows();
|
buffer.fillViewportRows();
|
||||||
(<any> buffer.lines.get(11)).isWrapped = true;
|
buffer.lines.get(11).isWrapped = true;
|
||||||
(<any> buffer.lines.get(12)).isWrapped = true;
|
buffer.lines.get(12).isWrapped = true;
|
||||||
(<any> buffer.lines.get(13)).isWrapped = true;
|
buffer.lines.get(13).isWrapped = true;
|
||||||
(<any> buffer.lines.get(14)).isWrapped = true;
|
buffer.lines.get(14).isWrapped = true;
|
||||||
assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 10, last: 14 });
|
assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 10, last: 14 });
|
||||||
});
|
});
|
||||||
it('should return a range for the last row', () => {
|
it('should return a range for the last row', () => {
|
||||||
buffer.fillViewportRows();
|
buffer.fillViewportRows();
|
||||||
(<any> buffer.lines.get(23)).isWrapped = true;
|
buffer.lines.get(23).isWrapped = true;
|
||||||
assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 1), { first: 22, last: 23 });
|
assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 1), { first: 22, last: 23 });
|
||||||
});
|
});
|
||||||
it('should return a range for a row that wraps upward to first row', () => {
|
it('should return a range for a row that wraps upward to first row', () => {
|
||||||
buffer.fillViewportRows();
|
buffer.fillViewportRows();
|
||||||
(<any> buffer.lines.get(1)).isWrapped = true;
|
buffer.lines.get(1).isWrapped = true;
|
||||||
assert.deepEqual(buffer.getWrappedRangeForLine(1), { first: 0, last: 1 });
|
assert.deepEqual(buffer.getWrappedRangeForLine(1), { first: 0, last: 1 });
|
||||||
});
|
});
|
||||||
it('should return a range for a row that wraps downward to last row', () => {
|
it('should return a range for a row that wraps downward to last row', () => {
|
||||||
buffer.fillViewportRows();
|
buffer.fillViewportRows();
|
||||||
(<any> buffer.lines.get(buffer.lines.length - 1)).isWrapped = true;
|
buffer.lines.get(buffer.lines.length - 1).isWrapped = true;
|
||||||
assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 2), { first: 22, last: 23 });
|
assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 2), { first: 22, last: 23 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -154,11 +155,11 @@ describe('Buffer', () => {
|
|||||||
assert.equal(buffer.lines.maxLength, INIT_ROWS);
|
assert.equal(buffer.lines.maxLength, INIT_ROWS);
|
||||||
buffer.y = INIT_ROWS - 1;
|
buffer.y = INIT_ROWS - 1;
|
||||||
buffer.fillViewportRows();
|
buffer.fillViewportRows();
|
||||||
buffer.lines.get(5)[0][1] = 'a';
|
buffer.lines.get(5).get(0)[1] = 'a';
|
||||||
buffer.lines.get(INIT_ROWS - 1)[0][1] = 'b';
|
buffer.lines.get(INIT_ROWS - 1).get(0)[1] = 'b';
|
||||||
buffer.resize(INIT_COLS, INIT_ROWS - 5);
|
buffer.resize(INIT_COLS, INIT_ROWS - 5);
|
||||||
assert.equal(buffer.lines.get(0)[0][1], 'a');
|
assert.equal(buffer.lines.get(0).get(0)[1], 'a');
|
||||||
assert.equal(buffer.lines.get(INIT_ROWS - 1 - 5)[0][1], 'b');
|
assert.equal(buffer.lines.get(INIT_ROWS - 1 - 5).get(0)[1], 'b');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -179,7 +180,7 @@ describe('Buffer', () => {
|
|||||||
buffer.fillViewportRows();
|
buffer.fillViewportRows();
|
||||||
// Create 10 extra blank lines
|
// Create 10 extra blank lines
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
buffer.lines.push(terminal.blankLine());
|
buffer.lines.push(BufferLine.blankLine(terminal.cols, DEFAULT_ATTR));
|
||||||
}
|
}
|
||||||
// Set cursor to the bottom of the buffer
|
// Set cursor to the bottom of the buffer
|
||||||
buffer.y = INIT_ROWS - 1;
|
buffer.y = INIT_ROWS - 1;
|
||||||
@@ -199,7 +200,7 @@ describe('Buffer', () => {
|
|||||||
buffer.fillViewportRows();
|
buffer.fillViewportRows();
|
||||||
// Create 10 extra blank lines
|
// Create 10 extra blank lines
|
||||||
for (let i = 0; i < 10; i++) {
|
for (let i = 0; i < 10; i++) {
|
||||||
buffer.lines.push(terminal.blankLine());
|
buffer.lines.push(BufferLine.blankLine(terminal.cols, DEFAULT_ATTR));
|
||||||
}
|
}
|
||||||
// Set cursor to the bottom of the buffer
|
// Set cursor to the bottom of the buffer
|
||||||
buffer.y = INIT_ROWS - 1;
|
buffer.y = INIT_ROWS - 1;
|
||||||
@@ -272,34 +273,34 @@ describe('Buffer', () => {
|
|||||||
|
|
||||||
describe ('translateBufferLineToString', () => {
|
describe ('translateBufferLineToString', () => {
|
||||||
it('should handle selecting a section of ascii text', () => {
|
it('should handle selecting a section of ascii text', () => {
|
||||||
buffer.lines.set(0, [
|
const line = new BufferLine();
|
||||||
[ null, 'a', 1, 'a'.charCodeAt(0)],
|
line.push([ null, 'a', 1, 'a'.charCodeAt(0)]);
|
||||||
[ null, 'b', 1, 'b'.charCodeAt(0)],
|
line.push([ null, 'b', 1, 'b'.charCodeAt(0)]);
|
||||||
[ null, 'c', 1, 'c'.charCodeAt(0)],
|
line.push([ null, 'c', 1, 'c'.charCodeAt(0)]);
|
||||||
[ null, 'd', 1, 'd'.charCodeAt(0)]
|
line.push([ null, 'd', 1, 'd'.charCodeAt(0)]);
|
||||||
]);
|
buffer.lines.set(0, line);
|
||||||
|
|
||||||
const str = buffer.translateBufferLineToString(0, true, 0, 2);
|
const str = buffer.translateBufferLineToString(0, true, 0, 2);
|
||||||
assert.equal(str, 'ab');
|
assert.equal(str, 'ab');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle a cut-off double width character by including it', () => {
|
it('should handle a cut-off double width character by including it', () => {
|
||||||
buffer.lines.set(0, [
|
const line = new BufferLine();
|
||||||
[ null, '語', 2, 35486 ],
|
line.push([ null, '語', 2, 35486 ]);
|
||||||
[ null, '', 0, null],
|
line.push([ null, '', 0, null]);
|
||||||
[ null, 'a', 1, 'a'.charCodeAt(0)]
|
line.push([ null, 'a', 1, 'a'.charCodeAt(0)]);
|
||||||
]);
|
buffer.lines.set(0, line);
|
||||||
|
|
||||||
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
|
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
|
||||||
assert.equal(str1, '語');
|
assert.equal(str1, '語');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle a zero width character in the middle of the string by not including it', () => {
|
it('should handle a zero width character in the middle of the string by not including it', () => {
|
||||||
buffer.lines.set(0, [
|
const line = new BufferLine();
|
||||||
[ null, '語', 2, '語'.charCodeAt(0) ],
|
line.push([ null, '語', 2, '語'.charCodeAt(0) ]);
|
||||||
[ null, '', 0, null],
|
line.push([ null, '', 0, null]);
|
||||||
[ null, 'a', 1, 'a'.charCodeAt(0)]
|
line.push([ null, 'a', 1, 'a'.charCodeAt(0)]);
|
||||||
]);
|
buffer.lines.set(0, line);
|
||||||
|
|
||||||
const str0 = buffer.translateBufferLineToString(0, true, 0, 1);
|
const str0 = buffer.translateBufferLineToString(0, true, 0, 1);
|
||||||
assert.equal(str0, '語');
|
assert.equal(str0, '語');
|
||||||
@@ -312,10 +313,10 @@ describe('Buffer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle single width emojis', () => {
|
it('should handle single width emojis', () => {
|
||||||
buffer.lines.set(0, [
|
const line = new BufferLine();
|
||||||
[ null, '😁', 1, '😁'.charCodeAt(0) ],
|
line.push([ null, '😁', 1, '😁'.charCodeAt(0) ]);
|
||||||
[ null, 'a', 1, 'a'.charCodeAt(0)]
|
line.push([ null, 'a', 1, 'a'.charCodeAt(0)]);
|
||||||
]);
|
buffer.lines.set(0, line);
|
||||||
|
|
||||||
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
|
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
|
||||||
assert.equal(str1, '😁');
|
assert.equal(str1, '😁');
|
||||||
@@ -325,10 +326,10 @@ describe('Buffer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle double width emojis', () => {
|
it('should handle double width emojis', () => {
|
||||||
buffer.lines.set(0, [
|
const line = new BufferLine();
|
||||||
[ null, '😁', 2, '😁'.charCodeAt(0) ],
|
line.push([ null, '😁', 2, '😁'.charCodeAt(0) ]);
|
||||||
[ null, '', 0, null]
|
line.push([ null, '', 0, null]);
|
||||||
]);
|
buffer.lines.set(0, line);
|
||||||
|
|
||||||
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
|
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
|
||||||
assert.equal(str1, '😁');
|
assert.equal(str1, '😁');
|
||||||
@@ -336,14 +337,212 @@ describe('Buffer', () => {
|
|||||||
const str2 = buffer.translateBufferLineToString(0, true, 0, 2);
|
const str2 = buffer.translateBufferLineToString(0, true, 0, 2);
|
||||||
assert.equal(str2, '😁');
|
assert.equal(str2, '😁');
|
||||||
|
|
||||||
buffer.lines.set(0, [
|
const line2 = new BufferLine();
|
||||||
[ null, '😁', 2, '😁'.charCodeAt(0) ],
|
line2.push([ null, '😁', 2, '😁'.charCodeAt(0) ]);
|
||||||
[ null, '', 0, null],
|
line2.push([ null, '', 0, null]);
|
||||||
[ null, 'a', 1, 'a'.charCodeAt(0)]
|
line2.push([ null, 'a', 1, 'a'.charCodeAt(0)]);
|
||||||
]);
|
buffer.lines.set(0, line2);
|
||||||
|
|
||||||
const str3 = buffer.translateBufferLineToString(0, true, 0, 3);
|
const str3 = buffer.translateBufferLineToString(0, true, 0, 3);
|
||||||
assert.equal(str3, '😁a');
|
assert.equal(str3, '😁a');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('stringIndexToBufferIndex', () => {
|
||||||
|
let terminal: TestTerminal;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
terminal = new TestTerminal({rows: 5, cols: 10});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('multiline ascii', () => {
|
||||||
|
const input = 'This is ASCII text spanning multiple lines.';
|
||||||
|
terminal.writeSync(input);
|
||||||
|
const s = terminal.buffer.iterator(true).next().content;
|
||||||
|
assert.equal(input, s);
|
||||||
|
for (let i = 0; i < input.length; ++i) {
|
||||||
|
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||||
|
assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('combining e\u0301 in a sentence', () => {
|
||||||
|
const input = 'Sitting in the cafe\u0301 drinking coffee.';
|
||||||
|
terminal.writeSync(input);
|
||||||
|
const s = terminal.buffer.iterator(true).next().content;
|
||||||
|
assert.equal(input, s);
|
||||||
|
for (let i = 0; i < 19; ++i) {
|
||||||
|
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||||
|
assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
|
||||||
|
}
|
||||||
|
// string index 18 & 19 point to combining char e\u0301 ---> same buffer Index
|
||||||
|
assert.deepEqual(
|
||||||
|
terminal.buffer.stringIndexToBufferIndex(0, 18),
|
||||||
|
terminal.buffer.stringIndexToBufferIndex(0, 19));
|
||||||
|
// after the combining char every string index has an offset of -1
|
||||||
|
for (let i = 19; i < input.length; ++i) {
|
||||||
|
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||||
|
assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('multiline combining e\u0301', () => {
|
||||||
|
const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301';
|
||||||
|
terminal.writeSync(input);
|
||||||
|
const s = terminal.buffer.iterator(true).next().content;
|
||||||
|
assert.equal(input, s);
|
||||||
|
// every buffer cell index contains 2 string indices
|
||||||
|
for (let i = 0; i < input.length; ++i) {
|
||||||
|
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||||
|
assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('surrogate char in a sentence', () => {
|
||||||
|
const input = 'The 𝄞 is a clef widely used in modern notation.';
|
||||||
|
terminal.writeSync(input);
|
||||||
|
const s = terminal.buffer.iterator(true).next().content;
|
||||||
|
assert.equal(input, s);
|
||||||
|
for (let i = 0; i < 5; ++i) {
|
||||||
|
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||||
|
assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
|
||||||
|
}
|
||||||
|
// string index 4 & 5 point to surrogate char 𝄞 ---> same buffer Index
|
||||||
|
assert.deepEqual(
|
||||||
|
terminal.buffer.stringIndexToBufferIndex(0, 4),
|
||||||
|
terminal.buffer.stringIndexToBufferIndex(0, 5));
|
||||||
|
// after the combining char every string index has an offset of -1
|
||||||
|
for (let i = 5; i < input.length; ++i) {
|
||||||
|
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||||
|
assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('multiline surrogate char', () => {
|
||||||
|
const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞';
|
||||||
|
terminal.writeSync(input);
|
||||||
|
const s = terminal.buffer.iterator(true).next().content;
|
||||||
|
assert.equal(input, s);
|
||||||
|
// every buffer cell index contains 2 string indices
|
||||||
|
for (let i = 0; i < input.length; ++i) {
|
||||||
|
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||||
|
assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('surrogate char with combining', () => {
|
||||||
|
// eye of Ra with acute accent - string length of 3
|
||||||
|
const input = '𓂀\u0301 - the eye hiroglyph with an acute accent.';
|
||||||
|
terminal.writeSync(input);
|
||||||
|
const s = terminal.buffer.iterator(true).next().content;
|
||||||
|
assert.equal(input, s);
|
||||||
|
// index 0..2 should map to 0
|
||||||
|
assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 1));
|
||||||
|
assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 2));
|
||||||
|
for (let i = 2; i < input.length; ++i) {
|
||||||
|
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||||
|
assert.deepEqual([((i - 2) / terminal.cols) | 0, (i - 2) % terminal.cols], bufferIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('multiline surrogate with combining', () => {
|
||||||
|
const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301';
|
||||||
|
terminal.writeSync(input);
|
||||||
|
const s = terminal.buffer.iterator(true).next().content;
|
||||||
|
assert.equal(input, s);
|
||||||
|
// every buffer cell index contains 3 string indices
|
||||||
|
for (let i = 0; i < input.length; ++i) {
|
||||||
|
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||||
|
assert.deepEqual([(((i / 3) | 0) / terminal.cols) | 0, ((i / 3) | 0) % terminal.cols], bufferIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fullwidth chars', () => {
|
||||||
|
const input = 'These 123 are some fat numbers.';
|
||||||
|
terminal.writeSync(input);
|
||||||
|
const s = terminal.buffer.iterator(true).next().content;
|
||||||
|
assert.equal(input, s);
|
||||||
|
for (let i = 0; i < 6; ++i) {
|
||||||
|
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||||
|
assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
|
||||||
|
}
|
||||||
|
// string index 6, 7, 8 take 2 cells
|
||||||
|
assert.deepEqual([0, 8], terminal.buffer.stringIndexToBufferIndex(0, 7));
|
||||||
|
assert.deepEqual([1, 0], terminal.buffer.stringIndexToBufferIndex(0, 8));
|
||||||
|
// rest of the string has offset of +3
|
||||||
|
for (let i = 9; i < input.length; ++i) {
|
||||||
|
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||||
|
assert.deepEqual([((i + 3) / terminal.cols) | 0, (i + 3) % terminal.cols], bufferIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('multiline fullwidth chars', () => {
|
||||||
|
const input = '12345678901234567890';
|
||||||
|
terminal.writeSync(input);
|
||||||
|
const s = terminal.buffer.iterator(true).next().content;
|
||||||
|
assert.equal(input, s);
|
||||||
|
for (let i = 9; i < input.length; ++i) {
|
||||||
|
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||||
|
assert.deepEqual([((i << 1) / terminal.cols) | 0, (i << 1) % terminal.cols], bufferIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fullwidth combining with emoji - match emoji cell', () => {
|
||||||
|
const input = 'Lots of ¥\u0301 make me 😃.';
|
||||||
|
terminal.writeSync(input);
|
||||||
|
const s = terminal.buffer.iterator(true).next().content;
|
||||||
|
assert.equal(input, s);
|
||||||
|
const stringIndex = s.match(/😃/).index;
|
||||||
|
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, stringIndex);
|
||||||
|
assert(terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX], '😃');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('multiline fullwidth chars with offset 1 (currently tests for broken behavior)', () => {
|
||||||
|
const input = 'a12345678901234567890';
|
||||||
|
// the 'a' at the beginning moves all fullwidth chars one to the right
|
||||||
|
// now the end of the line contains a dangling empty cell since
|
||||||
|
// the next fullwidth char has to wrap early
|
||||||
|
// the dangling last cell is wrongly added in the string
|
||||||
|
// --> fixable after resolving #1685
|
||||||
|
terminal.writeSync(input);
|
||||||
|
// TODO: reenable after fix
|
||||||
|
// const s = terminal.buffer.contents(true).toArray()[0];
|
||||||
|
// assert.equal(input, s);
|
||||||
|
for (let i = 10; i < input.length; ++i) {
|
||||||
|
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i + 1); // TODO: remove +1 after fix
|
||||||
|
const j = (i - 0) << 1;
|
||||||
|
assert.deepEqual([(j / terminal.cols) | 0, j % terminal.cols], bufferIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('BufferStringIterator', function(): void {
|
||||||
|
it('iterator does not ovrflow buffer limits', function(): void {
|
||||||
|
const terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5});
|
||||||
|
const data = [
|
||||||
|
'aaaaaaaaaa',
|
||||||
|
'aaaaaaaaa\n',
|
||||||
|
'aaaaaaaaaa',
|
||||||
|
'aaaaaaaaa\n',
|
||||||
|
'aaaaaaaaaa',
|
||||||
|
'aaaaaaaaaa',
|
||||||
|
'aaaaaaaaaa',
|
||||||
|
'aaaaaaaaa\n',
|
||||||
|
'aaaaaaaaaa',
|
||||||
|
'aaaaaaaaaa'
|
||||||
|
];
|
||||||
|
terminal.writeSync(data.join(''));
|
||||||
|
// brute force test with insane values
|
||||||
|
expect(() => {
|
||||||
|
for (let overscan = 0; overscan < 20; ++overscan) {
|
||||||
|
for (let start = -10; start < 20; ++start) {
|
||||||
|
for (let end = -10; end < 20; ++end) {
|
||||||
|
const it = terminal.buffer.iterator(false, start, end, overscan, overscan);
|
||||||
|
while (it.hasNext()) {
|
||||||
|
it.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).to.not.throw();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+105
-12
@@ -4,9 +4,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { CircularList } from './common/CircularList';
|
import { CircularList } from './common/CircularList';
|
||||||
import { LineData, CharData, ITerminal, IBuffer } from './Types';
|
import { CharData, ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult } from './Types';
|
||||||
import { EventEmitter } from './EventEmitter';
|
import { EventEmitter } from './common/EventEmitter';
|
||||||
import { IMarker } from 'xterm';
|
import { IMarker } from 'xterm';
|
||||||
|
import { BufferLine } from './BufferLine';
|
||||||
|
|
||||||
export const DEFAULT_ATTR = (0 << 18) | (257 << 9) | (256 << 0);
|
export const DEFAULT_ATTR = (0 << 18) | (257 << 9) | (256 << 0);
|
||||||
export const CHAR_DATA_ATTR_INDEX = 0;
|
export const CHAR_DATA_ATTR_INDEX = 0;
|
||||||
@@ -27,7 +28,7 @@ export const NULL_CELL_CODE = 32;
|
|||||||
* - scroll position
|
* - scroll position
|
||||||
*/
|
*/
|
||||||
export class Buffer implements IBuffer {
|
export class Buffer implements IBuffer {
|
||||||
public lines: CircularList<LineData>;
|
public lines: CircularList<IBufferLine>;
|
||||||
public ydisp: number;
|
public ydisp: number;
|
||||||
public ybase: number;
|
public ybase: number;
|
||||||
public y: number;
|
public y: number;
|
||||||
@@ -84,7 +85,7 @@ export class Buffer implements IBuffer {
|
|||||||
if (this.lines.length === 0) {
|
if (this.lines.length === 0) {
|
||||||
let i = this._terminal.rows;
|
let i = this._terminal.rows;
|
||||||
while (i--) {
|
while (i--) {
|
||||||
this.lines.push(this._terminal.blankLine());
|
this.lines.push(BufferLine.blankLine(this._terminal.cols, DEFAULT_ATTR));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,7 +98,7 @@ export class Buffer implements IBuffer {
|
|||||||
this.ybase = 0;
|
this.ybase = 0;
|
||||||
this.y = 0;
|
this.y = 0;
|
||||||
this.x = 0;
|
this.x = 0;
|
||||||
this.lines = new CircularList<LineData>(this._getCorrectBufferLength(this._terminal.rows));
|
this.lines = new CircularList<IBufferLine>(this._getCorrectBufferLength(this._terminal.rows));
|
||||||
this.scrollTop = 0;
|
this.scrollTop = 0;
|
||||||
this.scrollBottom = this._terminal.rows - 1;
|
this.scrollBottom = this._terminal.rows - 1;
|
||||||
this.setupTabStops();
|
this.setupTabStops();
|
||||||
@@ -146,7 +147,7 @@ export class Buffer implements IBuffer {
|
|||||||
} else {
|
} else {
|
||||||
// Add a blank line if there is no buffer left at the top to scroll to, or if there
|
// Add a blank line if there is no buffer left at the top to scroll to, or if there
|
||||||
// are blank lines after the cursor
|
// are blank lines after the cursor
|
||||||
this.lines.push(this._terminal.blankLine(undefined, undefined, newCols));
|
this.lines.push(BufferLine.blankLine(newCols, DEFAULT_ATTR));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,6 +194,36 @@ export class Buffer implements IBuffer {
|
|||||||
this.scrollBottom = newRows - 1;
|
this.scrollBottom = newRows - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translates a string index back to a BufferIndex.
|
||||||
|
* To get the correct buffer position the string must start at `startCol` 0
|
||||||
|
* (default in translateBufferLineToString).
|
||||||
|
* The method also works on wrapped line strings given rows were not trimmed.
|
||||||
|
* The method operates on the CharData string length, there are no
|
||||||
|
* additional content or boundary checks. Therefore the string and the buffer
|
||||||
|
* should not be altered in between.
|
||||||
|
* TODO: respect trim flag after fixing #1685
|
||||||
|
* @param lineIndex line index the string was retrieved from
|
||||||
|
* @param stringIndex index within the string
|
||||||
|
* @param startCol column offset the string was retrieved from
|
||||||
|
*/
|
||||||
|
public stringIndexToBufferIndex(lineIndex: number, stringIndex: number): BufferIndex {
|
||||||
|
while (stringIndex) {
|
||||||
|
const line = this.lines.get(lineIndex);
|
||||||
|
if (!line) {
|
||||||
|
[-1, -1];
|
||||||
|
}
|
||||||
|
for (let i = 0; i < line.length; ++i) {
|
||||||
|
stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length;
|
||||||
|
if (stringIndex < 0) {
|
||||||
|
return [lineIndex, i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lineIndex++;
|
||||||
|
}
|
||||||
|
return [lineIndex, 0];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Translates a buffer line to a string, with optional start and end columns.
|
* 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
|
* Wide characters will count as two columns in the resulting string. This
|
||||||
@@ -223,7 +254,7 @@ export class Buffer implements IBuffer {
|
|||||||
let endIndex = endCol;
|
let endIndex = endCol;
|
||||||
|
|
||||||
for (let i = 0; i < line.length; i++) {
|
for (let i = 0; i < line.length; i++) {
|
||||||
const char = line[i];
|
const char = line.get(i);
|
||||||
lineString += char[CHAR_DATA_CHAR_INDEX];
|
lineString += char[CHAR_DATA_CHAR_INDEX];
|
||||||
// Adjust start and end cols for wide characters if they affect their
|
// Adjust start and end cols for wide characters if they affect their
|
||||||
// column indexes
|
// column indexes
|
||||||
@@ -268,11 +299,11 @@ export class Buffer implements IBuffer {
|
|||||||
let first = y;
|
let first = y;
|
||||||
let last = y;
|
let last = y;
|
||||||
// Scan upwards for wrapped lines
|
// Scan upwards for wrapped lines
|
||||||
while (first > 0 && (<any>this.lines.get(first)).isWrapped) {
|
while (first > 0 && this.lines.get(first).isWrapped) {
|
||||||
first--;
|
first--;
|
||||||
}
|
}
|
||||||
// Scan downwards for wrapped lines
|
// Scan downwards for wrapped lines
|
||||||
while (last + 1 < this.lines.length && (<any>this.lines.get(last + 1)).isWrapped) {
|
while (last + 1 < this.lines.length && this.lines.get(last + 1).isWrapped) {
|
||||||
last++;
|
last++;
|
||||||
}
|
}
|
||||||
return { first, last };
|
return { first, last };
|
||||||
@@ -283,7 +314,7 @@ export class Buffer implements IBuffer {
|
|||||||
* @param i The index to start setting up tab stops from.
|
* @param i The index to start setting up tab stops from.
|
||||||
*/
|
*/
|
||||||
public setupTabStops(i?: number): void {
|
public setupTabStops(i?: number): void {
|
||||||
if (i != null) {
|
if (i !== null && i !== undefined) {
|
||||||
if (!this.tabs[i]) {
|
if (!this.tabs[i]) {
|
||||||
i = this.prevStop(i);
|
i = this.prevStop(i);
|
||||||
}
|
}
|
||||||
@@ -302,7 +333,7 @@ export class Buffer implements IBuffer {
|
|||||||
* @param x The position to move the cursor to the previous tab stop.
|
* @param x The position to move the cursor to the previous tab stop.
|
||||||
*/
|
*/
|
||||||
public prevStop(x?: number): number {
|
public prevStop(x?: number): number {
|
||||||
if (x == null) {
|
if (x === null || x === undefined) {
|
||||||
x = this.x;
|
x = this.x;
|
||||||
}
|
}
|
||||||
while (!this.tabs[--x] && x > 0);
|
while (!this.tabs[--x] && x > 0);
|
||||||
@@ -314,7 +345,7 @@ export class Buffer implements IBuffer {
|
|||||||
* @param x The position to move the cursor one tab stop forward.
|
* @param x The position to move the cursor one tab stop forward.
|
||||||
*/
|
*/
|
||||||
public nextStop(x?: number): number {
|
public nextStop(x?: number): number {
|
||||||
if (x == null) {
|
if (x === null || x === undefined) {
|
||||||
x = this.x;
|
x = this.x;
|
||||||
}
|
}
|
||||||
while (!this.tabs[++x] && x < this._terminal.cols);
|
while (!this.tabs[++x] && x < this._terminal.cols);
|
||||||
@@ -339,6 +370,10 @@ export class Buffer implements IBuffer {
|
|||||||
// TODO: This could probably be optimized by relying on sort order and trimming the array using .length
|
// TODO: This could probably be optimized by relying on sort order and trimming the array using .length
|
||||||
this.markers.splice(this.markers.indexOf(marker), 1);
|
this.markers.splice(this.markers.indexOf(marker), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator {
|
||||||
|
return new BufferStringIterator(this, trimRight, startIndex, endIndex, startOverscan, endOverscan);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Marker extends EventEmitter implements IMarker {
|
export class Marker extends EventEmitter implements IMarker {
|
||||||
@@ -365,3 +400,61 @@ export class Marker extends EventEmitter implements IMarker {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterator to get unwrapped content strings from the buffer.
|
||||||
|
* The iterator returns at least the string data between the borders
|
||||||
|
* `startIndex` and `endIndex` (exclusive) and will expand the lines
|
||||||
|
* by `startOverscan` to the top and by `endOverscan` to the bottom,
|
||||||
|
* if no new line was found in between.
|
||||||
|
* It will never read/return string data beyond `startIndex - startOverscan`
|
||||||
|
* or `endIndex + endOverscan`. Therefore the first and last line might be truncated.
|
||||||
|
* It is possible to always get the full string for the first and last line as well
|
||||||
|
* by setting the overscan values to the actual buffer length. This not recommended
|
||||||
|
* since it might return the whole buffer within a single string in a worst case scenario.
|
||||||
|
*/
|
||||||
|
export class BufferStringIterator implements IBufferStringIterator {
|
||||||
|
private _current: number;
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private _buffer: IBuffer,
|
||||||
|
private _trimRight: boolean,
|
||||||
|
private _startIndex: number = 0,
|
||||||
|
private _endIndex: number = _buffer.lines.length,
|
||||||
|
private _startOverscan: number = 0,
|
||||||
|
private _endOverscan: number = 0
|
||||||
|
) {
|
||||||
|
if (this._startIndex < 0) {
|
||||||
|
this._startIndex = 0;
|
||||||
|
}
|
||||||
|
if (this._endIndex > this._buffer.lines.length) {
|
||||||
|
this._endIndex = this._buffer.lines.length;
|
||||||
|
}
|
||||||
|
this._current = this._startIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasNext(): boolean {
|
||||||
|
return this._current < this._endIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public next(): IBufferStringIteratorResult {
|
||||||
|
const range = this._buffer.getWrappedRangeForLine(this._current);
|
||||||
|
// limit search window to overscan value at both borders
|
||||||
|
if (range.first < this._startIndex - this._startOverscan) {
|
||||||
|
range.first = this._startIndex - this._startOverscan;
|
||||||
|
}
|
||||||
|
if (range.last > this._endIndex + this._endOverscan) {
|
||||||
|
range.last = this._endIndex + this._endOverscan;
|
||||||
|
}
|
||||||
|
// limit to current buffer length
|
||||||
|
range.first = Math.max(range.first, 0);
|
||||||
|
range.last = Math.min(range.last, this._buffer.lines.length);
|
||||||
|
let result = '';
|
||||||
|
for (let i = range.first; i <= range.last; ++i) {
|
||||||
|
// TODO: always apply trimRight after fixing #1685
|
||||||
|
result += this._buffer.translateBufferLineToString(i, (this._trimRight) ? i === range.last : false);
|
||||||
|
}
|
||||||
|
this._current = range.last + 1;
|
||||||
|
return {range: range, content: result};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
import * as chai from 'chai';
|
||||||
|
import { BufferLine } from './BufferLine';
|
||||||
|
import { CharData, IBufferLine } from './Types';
|
||||||
|
import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, CHAR_DATA_ATTR_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX } from './Buffer';
|
||||||
|
|
||||||
|
|
||||||
|
class TestBufferLine extends BufferLine {
|
||||||
|
public toArray(): CharData[] {
|
||||||
|
return this._data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('BufferLine', function(): void {
|
||||||
|
it('ctor', function(): void {
|
||||||
|
let line: IBufferLine = new TestBufferLine();
|
||||||
|
chai.expect(line.length).equals(0);
|
||||||
|
chai.expect(line.pop()).equals(undefined);
|
||||||
|
chai.expect(line.isWrapped).equals(false);
|
||||||
|
line = new TestBufferLine(10);
|
||||||
|
chai.expect(line.length).equals(10);
|
||||||
|
chai.expect(line.pop()).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||||
|
chai.expect(line.isWrapped).equals(false);
|
||||||
|
line = new TestBufferLine(10, null, true);
|
||||||
|
chai.expect(line.length).equals(10);
|
||||||
|
chai.expect(line.pop()).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||||
|
chai.expect(line.isWrapped).equals(true);
|
||||||
|
line = new TestBufferLine(10, [123, 'a', 456, 789], true);
|
||||||
|
chai.expect(line.length).equals(10);
|
||||||
|
chai.expect(line.pop()).eql([123, 'a', 456, 789]);
|
||||||
|
chai.expect(line.isWrapped).equals(true);
|
||||||
|
});
|
||||||
|
it('splice', function(): void {
|
||||||
|
const line = new TestBufferLine();
|
||||||
|
const data: CharData[] = [
|
||||||
|
[1, 'a', 0, 0],
|
||||||
|
[2, 'b', 0, 0],
|
||||||
|
[3, 'c', 0, 0]
|
||||||
|
];
|
||||||
|
for (let i = 0; i < data.length; ++i) line.push(data[i]);
|
||||||
|
chai.expect(line.length).equals(data.length);
|
||||||
|
const removed1 = line.splice(1, 1, [4, 'd', 0, 0]);
|
||||||
|
const removed2 = data.splice(1, 1, [4, 'd', 0, 0]);
|
||||||
|
chai.expect(removed1).eql(removed2);
|
||||||
|
chai.expect(line.toArray()).eql(data);
|
||||||
|
});
|
||||||
|
it('TerminalLine.blankLine', function(): void {
|
||||||
|
const line = TestBufferLine.blankLine(5, 123);
|
||||||
|
chai.expect(line.length).equals(5);
|
||||||
|
chai.expect(line.isWrapped).equals(false);
|
||||||
|
const ch = line.get(0);
|
||||||
|
chai.expect(ch[CHAR_DATA_ATTR_INDEX]).equals(123);
|
||||||
|
chai.expect(ch[CHAR_DATA_CHAR_INDEX]).equals(NULL_CELL_CHAR);
|
||||||
|
chai.expect(ch[CHAR_DATA_WIDTH_INDEX]).equals(NULL_CELL_WIDTH);
|
||||||
|
chai.expect(ch[CHAR_DATA_CODE_INDEX]).equals(NULL_CELL_CODE);
|
||||||
|
});
|
||||||
|
it('insertCells', function(): void {
|
||||||
|
const line = new TestBufferLine();
|
||||||
|
const data: CharData[] = [
|
||||||
|
[1, 'a', 0, 0],
|
||||||
|
[2, 'b', 0, 0],
|
||||||
|
[3, 'c', 0, 0]
|
||||||
|
];
|
||||||
|
for (let i = 0; i < data.length; ++i) line.push(data[i]);
|
||||||
|
line.insertCells(1, 3, [4, 'd', 0, 0]);
|
||||||
|
chai.expect(line.toArray()).eql([[1, 'a', 0, 0], [4, 'd', 0, 0], [4, 'd', 0, 0]]);
|
||||||
|
});
|
||||||
|
it('deleteCells', function(): void {
|
||||||
|
const line = new TestBufferLine();
|
||||||
|
const data: CharData[] = [
|
||||||
|
[1, 'a', 0, 0],
|
||||||
|
[2, 'b', 0, 0],
|
||||||
|
[3, 'c', 0, 0],
|
||||||
|
[4, 'd', 0, 0],
|
||||||
|
[5, 'e', 0, 0]
|
||||||
|
];
|
||||||
|
for (let i = 0; i < data.length; ++i) line.push(data[i]);
|
||||||
|
line.deleteCells(1, 2, [6, 'f', 0, 0]);
|
||||||
|
chai.expect(line.toArray()).eql([[1, 'a', 0, 0], [4, 'd', 0, 0], [5, 'e', 0, 0], [6, 'f', 0, 0], [6, 'f', 0, 0]]);
|
||||||
|
});
|
||||||
|
it('replaceCells', function(): void {
|
||||||
|
const line = new TestBufferLine();
|
||||||
|
const data: CharData[] = [
|
||||||
|
[1, 'a', 0, 0],
|
||||||
|
[2, 'b', 0, 0],
|
||||||
|
[3, 'c', 0, 0],
|
||||||
|
[4, 'd', 0, 0],
|
||||||
|
[5, 'e', 0, 0]
|
||||||
|
];
|
||||||
|
for (let i = 0; i < data.length; ++i) line.push(data[i]);
|
||||||
|
line.replaceCells(2, 4, [6, 'f', 0, 0]);
|
||||||
|
chai.expect(line.toArray()).eql([[1, 'a', 0, 0], [2, 'b', 0, 0], [6, 'f', 0, 0], [6, 'f', 0, 0], [5, 'e', 0, 0]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
import { CharData, IBufferLine } from './Types';
|
||||||
|
import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a terminal line.
|
||||||
|
*/
|
||||||
|
export class BufferLine implements IBufferLine {
|
||||||
|
static blankLine(cols: number, attr: number, isWrapped?: boolean): IBufferLine {
|
||||||
|
const ch: CharData = [attr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
|
||||||
|
return new BufferLine(cols, ch, isWrapped);
|
||||||
|
}
|
||||||
|
protected _data: CharData[];
|
||||||
|
public isWrapped = false;
|
||||||
|
public length: number;
|
||||||
|
|
||||||
|
constructor(cols?: number, ch?: CharData, isWrapped?: boolean) {
|
||||||
|
this._data = [];
|
||||||
|
this.length = this._data.length;
|
||||||
|
if (cols) {
|
||||||
|
if (!ch) {
|
||||||
|
ch = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
|
||||||
|
}
|
||||||
|
for (let i = 0; i < cols; i++) {
|
||||||
|
this.push(ch); // Note: the ctor ch is not cloned (resembles old behavior)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isWrapped) {
|
||||||
|
this.isWrapped = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public get(index: number): CharData {
|
||||||
|
return this._data[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
public set(index: number, data: CharData): void {
|
||||||
|
this._data[index] = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public pop(): CharData | undefined {
|
||||||
|
const data = this._data.pop();
|
||||||
|
this.length = this._data.length;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public push(data: CharData): void {
|
||||||
|
this._data.push(data);
|
||||||
|
this.length = this._data.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public splice(start: number, deleteCount: number, ...items: CharData[]): CharData[] {
|
||||||
|
const removed = this._data.splice(start, deleteCount, ...items);
|
||||||
|
this.length = this._data.length;
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** insert n cells ch at pos, right cells are lost (stable length) */
|
||||||
|
public insertCells(pos: number, n: number, ch: CharData): void {
|
||||||
|
while (n--) {
|
||||||
|
this.splice(pos, 0, ch);
|
||||||
|
this.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** delete n cells at pos, right side is filled with fill (stable length) */
|
||||||
|
public deleteCells(pos: number, n: number, fill: CharData): void {
|
||||||
|
while (n--) {
|
||||||
|
this.splice(pos, 1);
|
||||||
|
this.push(fill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** replace cells from pos to pos + n - 1 with fill */
|
||||||
|
public replaceCells(start: number, end: number, fill: CharData): void {
|
||||||
|
while (start < end && start < this.length) {
|
||||||
|
this.set(start++, fill); // Note: fill is not cloned (resembles old behavior)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { ITerminal, IBufferSet } from './Types';
|
import { ITerminal, IBufferSet } from './Types';
|
||||||
import { Buffer } from './Buffer';
|
import { Buffer } from './Buffer';
|
||||||
import { EventEmitter } from './EventEmitter';
|
import { EventEmitter } from './common/EventEmitter';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The BufferSet represents the set of two buffers used by xterm terminals (normal and alt) and
|
* The BufferSet represents the set of two buffers used by xterm terminals (normal and alt) and
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { TestTerminal } from './utils/TestUtils.test';
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import { getStringCellWidth } from './CharWidth';
|
||||||
|
import { IBuffer } from './Types';
|
||||||
|
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
|
||||||
|
|
||||||
|
|
||||||
|
describe('getStringCellWidth', function(): void {
|
||||||
|
let terminal: TestTerminal;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
terminal = new TestTerminal({rows: 5, cols: 30});
|
||||||
|
});
|
||||||
|
|
||||||
|
function sumWidths(buffer: IBuffer, start: number, end: number, sentinel: string): number {
|
||||||
|
let result = 0;
|
||||||
|
for (let i = start; i < end; ++i) {
|
||||||
|
const line = buffer.lines.get(i);
|
||||||
|
for (let j = 0; j < line.length; ++j) { // TODO: change to trimBorder with multiline
|
||||||
|
const ch = line.get(j);
|
||||||
|
result += ch[CHAR_DATA_WIDTH_INDEX];
|
||||||
|
// return on sentinel
|
||||||
|
if (ch[CHAR_DATA_CHAR_INDEX] === sentinel) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
it('ASCII chars', function(): void {
|
||||||
|
const input = 'This is just ASCII text.#';
|
||||||
|
terminal.writeSync(input);
|
||||||
|
const s = terminal.buffer.iterator(true).next().content;
|
||||||
|
assert.equal(input, s);
|
||||||
|
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
|
||||||
|
});
|
||||||
|
it('combining chars', function(): void {
|
||||||
|
const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301#';
|
||||||
|
terminal.writeSync(input);
|
||||||
|
const s = terminal.buffer.iterator(true).next().content;
|
||||||
|
assert.equal(input, s);
|
||||||
|
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
|
||||||
|
});
|
||||||
|
it('surrogate chars', function(): void {
|
||||||
|
const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞#';
|
||||||
|
terminal.writeSync(input);
|
||||||
|
const s = terminal.buffer.iterator(true).next().content;
|
||||||
|
assert.equal(input, s);
|
||||||
|
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
|
||||||
|
});
|
||||||
|
it('surrogate combining chars', function(): void {
|
||||||
|
const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301#';
|
||||||
|
terminal.writeSync(input);
|
||||||
|
const s = terminal.buffer.iterator(true).next().content;
|
||||||
|
assert.equal(input, s);
|
||||||
|
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
|
||||||
|
});
|
||||||
|
it('fullwidth chars', function(): void {
|
||||||
|
const input = '1234567890#';
|
||||||
|
terminal.writeSync(input);
|
||||||
|
const s = terminal.buffer.iterator(true).next().content;
|
||||||
|
assert.equal(input, s);
|
||||||
|
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
|
||||||
|
});
|
||||||
|
it('fullwidth chars offset 1', function(): void {
|
||||||
|
const input = 'a1234567890#';
|
||||||
|
terminal.writeSync(input);
|
||||||
|
const s = terminal.buffer.iterator(true).next().content;
|
||||||
|
assert.equal(input, s);
|
||||||
|
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
|
||||||
|
});
|
||||||
|
// TODO: multiline tests once #1685 is resolved
|
||||||
|
});
|
||||||
@@ -169,3 +169,25 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
|
|||||||
return wcwidthHigh(num);
|
return wcwidthHigh(num);
|
||||||
};
|
};
|
||||||
})({nul: 0, control: 0}); // configurable options
|
})({nul: 0, control: 0}); // configurable options
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the terminal cell width for a string.
|
||||||
|
*/
|
||||||
|
export function getStringCellWidth(s: string): number {
|
||||||
|
let result = 0;
|
||||||
|
for (let i = 0; i < s.length; ++i) {
|
||||||
|
let code = s.charCodeAt(i);
|
||||||
|
if (0xD800 <= code && code <= 0xDBFF) {
|
||||||
|
const low = s.charCodeAt(i + 1);
|
||||||
|
if (isNaN(low)) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
code = ((code - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;
|
||||||
|
}
|
||||||
|
if (0xDC00 <= code && code <= 0xDFFF) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result += wcwidth(code);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|||||||
+392
-1
@@ -3,9 +3,102 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { assert } from 'chai';
|
import { assert, expect } from 'chai';
|
||||||
import { InputHandler } from './InputHandler';
|
import { InputHandler } from './InputHandler';
|
||||||
import { MockInputHandlingTerminal } from './utils/TestUtils.test';
|
import { MockInputHandlingTerminal } from './utils/TestUtils.test';
|
||||||
|
import { NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, CHAR_DATA_CHAR_INDEX } from './Buffer';
|
||||||
|
import { Terminal } from './Terminal';
|
||||||
|
import { IBufferLine } from './Types';
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: This and the sections related to this object in associated tests can be
|
||||||
|
// removed safely after InputHandler refactors are finished
|
||||||
|
class OldInputHandler extends InputHandler {
|
||||||
|
public eraseInLine(params: number[]): void {
|
||||||
|
switch (params[0]) {
|
||||||
|
case 0:
|
||||||
|
this.eraseRight(this._terminal.buffer.x, this._terminal.buffer.y);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
this.eraseLeft(this._terminal.buffer.x, this._terminal.buffer.y);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
this.eraseLine(this._terminal.buffer.y);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public eraseInDisplay(params: number[]): void {
|
||||||
|
let j;
|
||||||
|
switch (params[0]) {
|
||||||
|
case 0:
|
||||||
|
this.eraseRight(this._terminal.buffer.x, this._terminal.buffer.y);
|
||||||
|
j = this._terminal.buffer.y + 1;
|
||||||
|
for (; j < this._terminal.rows; j++) {
|
||||||
|
this.eraseLine(j);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
this.eraseLeft(this._terminal.buffer.x, this._terminal.buffer.y);
|
||||||
|
j = this._terminal.buffer.y;
|
||||||
|
while (j--) {
|
||||||
|
this.eraseLine(j);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
j = this._terminal.rows;
|
||||||
|
while (j--) this.eraseLine(j);
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
// Clear scrollback (everything not in viewport)
|
||||||
|
const scrollBackSize = this._terminal.buffer.lines.length - this._terminal.rows;
|
||||||
|
if (scrollBackSize > 0) {
|
||||||
|
this._terminal.buffer.lines.trimStart(scrollBackSize);
|
||||||
|
this._terminal.buffer.ybase = Math.max(this._terminal.buffer.ybase - scrollBackSize, 0);
|
||||||
|
this._terminal.buffer.ydisp = Math.max(this._terminal.buffer.ydisp - scrollBackSize, 0);
|
||||||
|
// Force a scroll event to refresh viewport
|
||||||
|
this._terminal.emit('scroll', 0);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erase in the identified line everything from "x" to the end of the line (right).
|
||||||
|
* @param x The column from which to start erasing to the end of the line.
|
||||||
|
* @param y The line in which to operate.
|
||||||
|
*/
|
||||||
|
public eraseRight(x: number, y: number): void {
|
||||||
|
const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + y);
|
||||||
|
if (!line) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
line.replaceCells(x, this._terminal.cols, [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||||
|
this._terminal.updateRange(y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erase in the identified line everything from "x" to the start of the line (left).
|
||||||
|
* @param x The column from which to start erasing to the start of the line.
|
||||||
|
* @param y The line in which to operate.
|
||||||
|
*/
|
||||||
|
public eraseLeft(x: number, y: number): void {
|
||||||
|
const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + y);
|
||||||
|
if (!line) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
line.replaceCells(0, x + 1, [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||||
|
this._terminal.updateRange(y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erase all content in the given line
|
||||||
|
* @param y The line to erase all of its contents.
|
||||||
|
*/
|
||||||
|
public eraseLine(y: number): void {
|
||||||
|
this.eraseRight(0, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
describe('InputHandler', () => {
|
describe('InputHandler', () => {
|
||||||
describe('save and restore cursor', () => {
|
describe('save and restore cursor', () => {
|
||||||
@@ -84,4 +177,302 @@ describe('InputHandler', () => {
|
|||||||
assert.equal(terminal.bracketedPasteMode, false);
|
assert.equal(terminal.bracketedPasteMode, false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('regression tests', function(): void {
|
||||||
|
type CharData = [number, string, number, number];
|
||||||
|
|
||||||
|
function lineContent(line: IBufferLine): string {
|
||||||
|
let content = '';
|
||||||
|
for (let i = 0; i < line.length; ++i) content += line.get(i)[CHAR_DATA_CHAR_INDEX];
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
function termContent(term: Terminal): string[] {
|
||||||
|
const result = [];
|
||||||
|
for (let i = 0; i < term.rows; ++i) result.push(lineContent(term.buffer.lines.get(i)));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
it('insertChars', function(): void {
|
||||||
|
const term = new Terminal();
|
||||||
|
const inputHandler = new InputHandler(term);
|
||||||
|
|
||||||
|
// old variant of the method
|
||||||
|
function insertChars(params: number[]): void {
|
||||||
|
let param = params[0];
|
||||||
|
if (param < 1) param = 1;
|
||||||
|
|
||||||
|
// make buffer local for faster access
|
||||||
|
const buffer = term.buffer;
|
||||||
|
|
||||||
|
const row = buffer.y + buffer.ybase;
|
||||||
|
let j = buffer.x;
|
||||||
|
const ch: CharData = [term.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm
|
||||||
|
while (param-- && j < term.cols) {
|
||||||
|
buffer.lines.get(row).splice(j++, 0, ch);
|
||||||
|
buffer.lines.get(row).pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert some data in first and second line
|
||||||
|
inputHandler.parse(Array(term.cols - 9).join('a'));
|
||||||
|
inputHandler.parse('1234567890');
|
||||||
|
inputHandler.parse(Array(term.cols - 9).join('a'));
|
||||||
|
inputHandler.parse('1234567890');
|
||||||
|
const line1: IBufferLine = term.buffer.lines.get(0); // line for old variant
|
||||||
|
const line2: IBufferLine = term.buffer.lines.get(1); // line for new variant
|
||||||
|
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '1234567890');
|
||||||
|
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '1234567890');
|
||||||
|
|
||||||
|
// insert one char from params = [0]
|
||||||
|
term.buffer.y = 0;
|
||||||
|
term.buffer.x = 70;
|
||||||
|
insertChars([0]);
|
||||||
|
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' 123456789');
|
||||||
|
term.buffer.y = 1;
|
||||||
|
term.buffer.x = 70;
|
||||||
|
inputHandler.insertChars([0]);
|
||||||
|
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' 123456789');
|
||||||
|
expect(lineContent(line2)).equals(lineContent(line1));
|
||||||
|
|
||||||
|
// insert one char from params = [1]
|
||||||
|
term.buffer.y = 0;
|
||||||
|
term.buffer.x = 70;
|
||||||
|
insertChars([1]);
|
||||||
|
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' 12345678');
|
||||||
|
term.buffer.y = 1;
|
||||||
|
term.buffer.x = 70;
|
||||||
|
inputHandler.insertChars([1]);
|
||||||
|
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' 12345678');
|
||||||
|
expect(lineContent(line2)).equals(lineContent(line1));
|
||||||
|
|
||||||
|
// insert two chars from params = [2]
|
||||||
|
term.buffer.y = 0;
|
||||||
|
term.buffer.x = 70;
|
||||||
|
insertChars([2]);
|
||||||
|
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' 123456');
|
||||||
|
term.buffer.y = 1;
|
||||||
|
term.buffer.x = 70;
|
||||||
|
inputHandler.insertChars([2]);
|
||||||
|
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' 123456');
|
||||||
|
expect(lineContent(line2)).equals(lineContent(line1));
|
||||||
|
|
||||||
|
// insert 10 chars from params = [10]
|
||||||
|
term.buffer.y = 0;
|
||||||
|
term.buffer.x = 70;
|
||||||
|
insertChars([10]);
|
||||||
|
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' ');
|
||||||
|
term.buffer.y = 1;
|
||||||
|
term.buffer.x = 70;
|
||||||
|
inputHandler.insertChars([10]);
|
||||||
|
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' ');
|
||||||
|
expect(lineContent(line2)).equals(lineContent(line1));
|
||||||
|
});
|
||||||
|
it('deleteChars', function(): void {
|
||||||
|
const term = new Terminal();
|
||||||
|
const inputHandler = new InputHandler(term);
|
||||||
|
|
||||||
|
// old variant of the method
|
||||||
|
function deleteChars(params: number[]): void {
|
||||||
|
let param: number = params[0];
|
||||||
|
if (param < 1) {
|
||||||
|
param = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// make buffer local for faster access
|
||||||
|
const buffer = term.buffer;
|
||||||
|
|
||||||
|
const row = buffer.y + buffer.ybase;
|
||||||
|
const ch: CharData = [term.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm
|
||||||
|
while (param--) {
|
||||||
|
buffer.lines.get(row).splice(buffer.x, 1);
|
||||||
|
buffer.lines.get(row).push(ch);
|
||||||
|
}
|
||||||
|
term.updateRange(buffer.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert some data in first and second line
|
||||||
|
inputHandler.parse(Array(term.cols - 9).join('a'));
|
||||||
|
inputHandler.parse('1234567890');
|
||||||
|
inputHandler.parse(Array(term.cols - 9).join('a'));
|
||||||
|
inputHandler.parse('1234567890');
|
||||||
|
const line1: IBufferLine = term.buffer.lines.get(0); // line for old variant
|
||||||
|
const line2: IBufferLine = term.buffer.lines.get(1); // line for new variant
|
||||||
|
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '1234567890');
|
||||||
|
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '1234567890');
|
||||||
|
|
||||||
|
// delete one char from params = [0]
|
||||||
|
term.buffer.y = 0;
|
||||||
|
term.buffer.x = 70;
|
||||||
|
deleteChars([0]);
|
||||||
|
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '234567890 ');
|
||||||
|
term.buffer.y = 1;
|
||||||
|
term.buffer.x = 70;
|
||||||
|
inputHandler.deleteChars([0]);
|
||||||
|
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '234567890 ');
|
||||||
|
expect(lineContent(line2)).equals(lineContent(line1));
|
||||||
|
|
||||||
|
// insert one char from params = [1]
|
||||||
|
term.buffer.y = 0;
|
||||||
|
term.buffer.x = 70;
|
||||||
|
deleteChars([1]);
|
||||||
|
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '34567890 ');
|
||||||
|
term.buffer.y = 1;
|
||||||
|
term.buffer.x = 70;
|
||||||
|
inputHandler.deleteChars([1]);
|
||||||
|
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '34567890 ');
|
||||||
|
expect(lineContent(line2)).equals(lineContent(line1));
|
||||||
|
|
||||||
|
// insert two chars from params = [2]
|
||||||
|
term.buffer.y = 0;
|
||||||
|
term.buffer.x = 70;
|
||||||
|
deleteChars([2]);
|
||||||
|
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '567890 ');
|
||||||
|
term.buffer.y = 1;
|
||||||
|
term.buffer.x = 70;
|
||||||
|
inputHandler.deleteChars([2]);
|
||||||
|
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '567890 ');
|
||||||
|
expect(lineContent(line2)).equals(lineContent(line1));
|
||||||
|
|
||||||
|
// insert 10 chars from params = [10]
|
||||||
|
term.buffer.y = 0;
|
||||||
|
term.buffer.x = 70;
|
||||||
|
deleteChars([10]);
|
||||||
|
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' ');
|
||||||
|
term.buffer.y = 1;
|
||||||
|
term.buffer.x = 70;
|
||||||
|
inputHandler.deleteChars([10]);
|
||||||
|
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' ');
|
||||||
|
expect(lineContent(line2)).equals(lineContent(line1));
|
||||||
|
});
|
||||||
|
it('eraseInLine', function(): void {
|
||||||
|
const term = new Terminal();
|
||||||
|
const inputHandler = new InputHandler(term);
|
||||||
|
const oldInputHandler = new OldInputHandler(term);
|
||||||
|
|
||||||
|
// fill 6 lines to test 3 different states
|
||||||
|
inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||||
|
inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||||
|
inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||||
|
inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||||
|
inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||||
|
inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||||
|
|
||||||
|
// params[0] - right erase
|
||||||
|
term.buffer.y = 0;
|
||||||
|
term.buffer.x = 70;
|
||||||
|
oldInputHandler.eraseInLine([0]);
|
||||||
|
expect(lineContent(term.buffer.lines.get(0))).equals(Array(71).join('a') + ' ');
|
||||||
|
term.buffer.y = 1;
|
||||||
|
term.buffer.x = 70;
|
||||||
|
inputHandler.eraseInLine([0]);
|
||||||
|
expect(lineContent(term.buffer.lines.get(1))).equals(Array(71).join('a') + ' ');
|
||||||
|
|
||||||
|
// params[1] - left erase
|
||||||
|
term.buffer.y = 2;
|
||||||
|
term.buffer.x = 70;
|
||||||
|
oldInputHandler.eraseInLine([1]);
|
||||||
|
expect(lineContent(term.buffer.lines.get(2))).equals(Array(71).join(' ') + ' aaaaaaaaa');
|
||||||
|
term.buffer.y = 3;
|
||||||
|
term.buffer.x = 70;
|
||||||
|
inputHandler.eraseInLine([1]);
|
||||||
|
expect(lineContent(term.buffer.lines.get(3))).equals(Array(71).join(' ') + ' aaaaaaaaa');
|
||||||
|
|
||||||
|
// params[1] - left erase
|
||||||
|
term.buffer.y = 4;
|
||||||
|
term.buffer.x = 70;
|
||||||
|
oldInputHandler.eraseInLine([2]);
|
||||||
|
expect(lineContent(term.buffer.lines.get(4))).equals(Array(term.cols + 1).join(' '));
|
||||||
|
term.buffer.y = 5;
|
||||||
|
term.buffer.x = 70;
|
||||||
|
inputHandler.eraseInLine([2]);
|
||||||
|
expect(lineContent(term.buffer.lines.get(5))).equals(Array(term.cols + 1).join(' '));
|
||||||
|
|
||||||
|
});
|
||||||
|
it('eraseInDisplay', function(): void {
|
||||||
|
const termOld = new Terminal();
|
||||||
|
const inputHandlerOld = new OldInputHandler(termOld);
|
||||||
|
const termNew = new Terminal();
|
||||||
|
const inputHandlerNew = new InputHandler(termNew);
|
||||||
|
|
||||||
|
// fill display with a's
|
||||||
|
for (let i = 0; i < termOld.rows; ++i) inputHandlerOld.parse(Array(termOld.cols + 1).join('a'));
|
||||||
|
for (let i = 0; i < termNew.rows; ++i) inputHandlerNew.parse(Array(termOld.cols + 1).join('a'));
|
||||||
|
const data = [];
|
||||||
|
for (let i = 0; i < termOld.rows; ++i) data.push(Array(termOld.cols + 1).join('a'));
|
||||||
|
expect(termContent(termOld)).eql(data);
|
||||||
|
expect(termContent(termOld)).eql(termContent(termNew));
|
||||||
|
|
||||||
|
// params [0] - right and below erase
|
||||||
|
termOld.buffer.y = 5;
|
||||||
|
termOld.buffer.x = 40;
|
||||||
|
inputHandlerOld.eraseInDisplay([0]);
|
||||||
|
termNew.buffer.y = 5;
|
||||||
|
termNew.buffer.x = 40;
|
||||||
|
inputHandlerNew.eraseInDisplay([0]);
|
||||||
|
expect(termContent(termNew)).eql(termContent(termOld));
|
||||||
|
|
||||||
|
// reset
|
||||||
|
termOld.buffer.y = 0;
|
||||||
|
termOld.buffer.x = 0;
|
||||||
|
termNew.buffer.y = 0;
|
||||||
|
termNew.buffer.x = 0;
|
||||||
|
for (let i = 0; i < termOld.rows; ++i) inputHandlerOld.parse(Array(termOld.cols + 1).join('a'));
|
||||||
|
for (let i = 0; i < termNew.rows; ++i) inputHandlerNew.parse(Array(termOld.cols + 1).join('a'));
|
||||||
|
|
||||||
|
// params [1] - left and above
|
||||||
|
termOld.buffer.y = 5;
|
||||||
|
termOld.buffer.x = 40;
|
||||||
|
inputHandlerOld.eraseInDisplay([1]);
|
||||||
|
termNew.buffer.y = 5;
|
||||||
|
termNew.buffer.x = 40;
|
||||||
|
inputHandlerNew.eraseInDisplay([1]);
|
||||||
|
expect(termContent(termNew)).eql(termContent(termOld));
|
||||||
|
|
||||||
|
// reset
|
||||||
|
termOld.buffer.y = 0;
|
||||||
|
termOld.buffer.x = 0;
|
||||||
|
termNew.buffer.y = 0;
|
||||||
|
termNew.buffer.x = 0;
|
||||||
|
for (let i = 0; i < termOld.rows; ++i) inputHandlerOld.parse(Array(termOld.cols + 1).join('a'));
|
||||||
|
for (let i = 0; i < termNew.rows; ++i) inputHandlerNew.parse(Array(termOld.cols + 1).join('a'));
|
||||||
|
|
||||||
|
// params [2] - whole screen
|
||||||
|
termOld.buffer.y = 5;
|
||||||
|
termOld.buffer.x = 40;
|
||||||
|
inputHandlerOld.eraseInDisplay([2]);
|
||||||
|
termNew.buffer.y = 5;
|
||||||
|
termNew.buffer.x = 40;
|
||||||
|
inputHandlerNew.eraseInDisplay([2]);
|
||||||
|
expect(termContent(termNew)).eql(termContent(termOld));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('convertEol setting', function(): void {
|
||||||
|
// not converting
|
||||||
|
let s = '';
|
||||||
|
const termNotConverting = new Terminal({cols: 15, rows: 10});
|
||||||
|
(termNotConverting as any)._inputHandler.parse('Hello\nWorld');
|
||||||
|
for (let i = 0; i < termNotConverting.cols; ++i) {
|
||||||
|
s += termNotConverting.buffer.lines.get(0).get(i)[CHAR_DATA_CHAR_INDEX];
|
||||||
|
}
|
||||||
|
expect(s).equals('Hello ');
|
||||||
|
s = '';
|
||||||
|
for (let i = 0; i < termNotConverting.cols; ++i) {
|
||||||
|
s += termNotConverting.buffer.lines.get(1).get(i)[CHAR_DATA_CHAR_INDEX];
|
||||||
|
}
|
||||||
|
expect(s).equals(' World ');
|
||||||
|
|
||||||
|
// converting
|
||||||
|
s = '';
|
||||||
|
const termConverting = new Terminal({cols: 15, rows: 10, convertEol: true});
|
||||||
|
(termConverting as any)._inputHandler.parse('Hello\nWorld');
|
||||||
|
for (let i = 0; i < termConverting.cols; ++i) {
|
||||||
|
s += termConverting.buffer.lines.get(0).get(i)[CHAR_DATA_CHAR_INDEX];
|
||||||
|
}
|
||||||
|
expect(s).equals('Hello ');
|
||||||
|
s = '';
|
||||||
|
for (let i = 0; i < termConverting.cols; ++i) {
|
||||||
|
s += termConverting.buffer.lines.get(1).get(i)[CHAR_DATA_CHAR_INDEX];
|
||||||
|
}
|
||||||
|
expect(s).equals('World ');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+82
-82
@@ -4,7 +4,7 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CharData, IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types';
|
import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types';
|
||||||
import { C0, C1 } from './common/data/EscapeSequences';
|
import { C0, C1 } from './common/data/EscapeSequences';
|
||||||
import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets';
|
import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets';
|
||||||
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer';
|
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer';
|
||||||
@@ -13,6 +13,7 @@ import { wcwidth } from './CharWidth';
|
|||||||
import { EscapeSequenceParser } from './EscapeSequenceParser';
|
import { EscapeSequenceParser } from './EscapeSequenceParser';
|
||||||
import { ICharset } from './core/Types';
|
import { ICharset } from './core/Types';
|
||||||
import { Disposable } from './common/Lifecycle';
|
import { Disposable } from './common/Lifecycle';
|
||||||
|
import { BufferLine } from './BufferLine';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map collect to glevel. Used in `selectCharset`.
|
* Map collect to glevel. Used in `selectCharset`.
|
||||||
@@ -116,7 +117,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
|||||||
private _surrogateHigh: string;
|
private _surrogateHigh: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private _terminal: IInputHandlingTerminal,
|
protected _terminal: IInputHandlingTerminal,
|
||||||
private _parser: IEscapeSequenceParser = new EscapeSequenceParser())
|
private _parser: IEscapeSequenceParser = new EscapeSequenceParser())
|
||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
@@ -381,18 +382,20 @@ export class InputHandler extends Disposable implements IInputHandler {
|
|||||||
// since they always follow a cell consuming char
|
// since they always follow a cell consuming char
|
||||||
// therefore we can test for buffer.x to avoid overflow left
|
// therefore we can test for buffer.x to avoid overflow left
|
||||||
if (!chWidth && buffer.x) {
|
if (!chWidth && buffer.x) {
|
||||||
if (bufferRow[buffer.x - 1]) {
|
const chMinusOne = bufferRow.get(buffer.x - 1);
|
||||||
if (!bufferRow[buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) {
|
if (chMinusOne) {
|
||||||
|
if (!chMinusOne[CHAR_DATA_WIDTH_INDEX]) {
|
||||||
// found empty cell after fullwidth, need to go 2 cells back
|
// found empty cell after fullwidth, need to go 2 cells back
|
||||||
// it is save to step 2 cells back here
|
// it is save to step 2 cells back here
|
||||||
// since an empty cell is only set by fullwidth chars
|
// since an empty cell is only set by fullwidth chars
|
||||||
if (bufferRow[buffer.x - 2]) {
|
const chMinusTwo = bufferRow.get(buffer.x - 2);
|
||||||
bufferRow[buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char;
|
if (chMinusTwo) {
|
||||||
bufferRow[buffer.x - 2][CHAR_DATA_CODE_INDEX] = code;
|
chMinusTwo[CHAR_DATA_CHAR_INDEX] += char;
|
||||||
|
chMinusTwo[CHAR_DATA_CODE_INDEX] = code;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
bufferRow[buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char;
|
chMinusOne[CHAR_DATA_CHAR_INDEX] += char;
|
||||||
bufferRow[buffer.x - 1][CHAR_DATA_CODE_INDEX] = code;
|
chMinusOne[CHAR_DATA_CODE_INDEX] = code;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@@ -412,7 +415,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
|||||||
} else {
|
} else {
|
||||||
// The line already exists (eg. the initial viewport), mark it as a
|
// The line already exists (eg. the initial viewport), mark it as a
|
||||||
// wrapped line
|
// wrapped line
|
||||||
(<any>buffer.lines.get(buffer.y)).isWrapped = true;
|
buffer.lines.get(buffer.y).isWrapped = true;
|
||||||
}
|
}
|
||||||
// row changed, get it again
|
// row changed, get it again
|
||||||
bufferRow = buffer.lines.get(buffer.y + buffer.ybase);
|
bufferRow = buffer.lines.get(buffer.y + buffer.ybase);
|
||||||
@@ -435,10 +438,11 @@ export class InputHandler extends Disposable implements IInputHandler {
|
|||||||
// remove last cell
|
// remove last cell
|
||||||
// if it's width is 0, we have to adjust the second last cell as well
|
// if it's width is 0, we have to adjust the second last cell as well
|
||||||
const removed = bufferRow.pop();
|
const removed = bufferRow.pop();
|
||||||
|
const chMinusTwo = bufferRow.get(buffer.x - 2);
|
||||||
if (removed[CHAR_DATA_WIDTH_INDEX] === 0
|
if (removed[CHAR_DATA_WIDTH_INDEX] === 0
|
||||||
&& bufferRow[this._terminal.cols - 2]
|
&& chMinusTwo
|
||||||
&& bufferRow[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) {
|
&& chMinusTwo[CHAR_DATA_WIDTH_INDEX] === 2) {
|
||||||
bufferRow[this._terminal.cols - 2] = [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
|
bufferRow.set(this._terminal.cols - 2, [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// insert empty cell at cursor
|
// insert empty cell at cursor
|
||||||
@@ -447,11 +451,11 @@ export class InputHandler extends Disposable implements IInputHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// write current char to buffer and advance cursor
|
// write current char to buffer and advance cursor
|
||||||
bufferRow[buffer.x++] = [curAttr, char, chWidth, code];
|
bufferRow.set(buffer.x++, [curAttr, char, chWidth, code]);
|
||||||
|
|
||||||
// fullwidth char - also set next cell to placeholder stub and advance cursor
|
// fullwidth char - also set next cell to placeholder stub and advance cursor
|
||||||
if (chWidth === 2) {
|
if (chWidth === 2) {
|
||||||
bufferRow[buffer.x++] = [curAttr, '', 0, undefined];
|
bufferRow.set(buffer.x++, [curAttr, '', 0, undefined]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._terminal.updateRange(buffer.y);
|
this._terminal.updateRange(buffer.y);
|
||||||
@@ -473,7 +477,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
|||||||
// make buffer local for faster access
|
// make buffer local for faster access
|
||||||
const buffer = this._terminal.buffer;
|
const buffer = this._terminal.buffer;
|
||||||
|
|
||||||
if (this._terminal.convertEol) {
|
if (this._terminal.options.convertEol) {
|
||||||
buffer.x = 0;
|
buffer.x = 0;
|
||||||
}
|
}
|
||||||
buffer.y++;
|
buffer.y++;
|
||||||
@@ -546,20 +550,12 @@ export class InputHandler extends Disposable implements IInputHandler {
|
|||||||
* Insert Ps (Blank) Character(s) (default = 1) (ICH).
|
* Insert Ps (Blank) Character(s) (default = 1) (ICH).
|
||||||
*/
|
*/
|
||||||
public insertChars(params: number[]): void {
|
public insertChars(params: number[]): void {
|
||||||
let param = params[0];
|
this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).insertCells(
|
||||||
if (param < 1) param = 1;
|
this._terminal.buffer.x,
|
||||||
|
params[0] || 1,
|
||||||
// make buffer local for faster access
|
[this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
|
||||||
const buffer = this._terminal.buffer;
|
);
|
||||||
|
this._terminal.updateRange(this._terminal.buffer.y);
|
||||||
const row = buffer.y + buffer.ybase;
|
|
||||||
let j = buffer.x;
|
|
||||||
const ch: CharData = [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm
|
|
||||||
|
|
||||||
while (param-- && j < this._terminal.cols) {
|
|
||||||
buffer.lines.get(row).splice(j++, 0, ch);
|
|
||||||
buffer.lines.get(row).pop();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -719,6 +715,21 @@ export class InputHandler extends Disposable implements IInputHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to erase cells in a terminal row.
|
||||||
|
* The cell gets replaced with the eraseChar of the terminal.
|
||||||
|
* @param y row index
|
||||||
|
* @param start first cell index to be erased
|
||||||
|
* @param end end - 1 is last erased cell
|
||||||
|
*/
|
||||||
|
private _eraseInBufferLine(y: number, start: number, end: number): void {
|
||||||
|
this._terminal.buffer.lines.get(this._terminal.buffer.ybase + y).replaceCells(
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
[this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CSI Ps J Erase in Display (ED).
|
* CSI Ps J Erase in Display (ED).
|
||||||
* Ps = 0 -> Erase Below (default).
|
* Ps = 0 -> Erase Below (default).
|
||||||
@@ -735,22 +746,30 @@ export class InputHandler extends Disposable implements IInputHandler {
|
|||||||
let j;
|
let j;
|
||||||
switch (params[0]) {
|
switch (params[0]) {
|
||||||
case 0:
|
case 0:
|
||||||
this._terminal.eraseRight(this._terminal.buffer.x, this._terminal.buffer.y);
|
j = this._terminal.buffer.y;
|
||||||
j = this._terminal.buffer.y + 1;
|
this._terminal.updateRange(j);
|
||||||
|
this._eraseInBufferLine(j++, this._terminal.buffer.x, this._terminal.cols);
|
||||||
for (; j < this._terminal.rows; j++) {
|
for (; j < this._terminal.rows; j++) {
|
||||||
this._terminal.eraseLine(j);
|
this._eraseInBufferLine(j, 0, this._terminal.cols);
|
||||||
}
|
}
|
||||||
|
this._terminal.updateRange(j);
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
this._terminal.eraseLeft(this._terminal.buffer.x, this._terminal.buffer.y);
|
|
||||||
j = this._terminal.buffer.y;
|
j = this._terminal.buffer.y;
|
||||||
|
this._terminal.updateRange(j);
|
||||||
|
this._eraseInBufferLine(j, 0, this._terminal.buffer.x + 1);
|
||||||
while (j--) {
|
while (j--) {
|
||||||
this._terminal.eraseLine(j);
|
this._eraseInBufferLine(j, 0, this._terminal.cols);
|
||||||
}
|
}
|
||||||
|
this._terminal.updateRange(0);
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
j = this._terminal.rows;
|
j = this._terminal.rows;
|
||||||
while (j--) this._terminal.eraseLine(j);
|
this._terminal.updateRange(j - 1);
|
||||||
|
while (j--) {
|
||||||
|
this._eraseInBufferLine(j, 0, this._terminal.cols);
|
||||||
|
}
|
||||||
|
this._terminal.updateRange(0);
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
// Clear scrollback (everything not in viewport)
|
// Clear scrollback (everything not in viewport)
|
||||||
@@ -780,15 +799,16 @@ export class InputHandler extends Disposable implements IInputHandler {
|
|||||||
public eraseInLine(params: number[]): void {
|
public eraseInLine(params: number[]): void {
|
||||||
switch (params[0]) {
|
switch (params[0]) {
|
||||||
case 0:
|
case 0:
|
||||||
this._terminal.eraseRight(this._terminal.buffer.x, this._terminal.buffer.y);
|
this._eraseInBufferLine(this._terminal.buffer.y, this._terminal.buffer.x, this._terminal.cols);
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
this._terminal.eraseLeft(this._terminal.buffer.x, this._terminal.buffer.y);
|
this._eraseInBufferLine(this._terminal.buffer.y, 0, this._terminal.buffer.x + 1);
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
this._terminal.eraseLine(this._terminal.buffer.y);
|
this._eraseInBufferLine(this._terminal.buffer.y, 0, this._terminal.cols);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
this._terminal.updateRange(this._terminal.buffer.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -812,7 +832,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
|||||||
// test: echo -e '\e[44m\e[1L\e[0m'
|
// test: echo -e '\e[44m\e[1L\e[0m'
|
||||||
// blankLine(true) - xterm/linux behavior
|
// blankLine(true) - xterm/linux behavior
|
||||||
buffer.lines.splice(scrollBottomAbsolute - 1, 1);
|
buffer.lines.splice(scrollBottomAbsolute - 1, 1);
|
||||||
buffer.lines.splice(row, 0, this._terminal.blankLine(true));
|
buffer.lines.splice(row, 0, BufferLine.blankLine(this._terminal.cols, this._terminal.eraseAttr()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// this.maxRange();
|
// this.maxRange();
|
||||||
@@ -842,7 +862,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
|||||||
// test: echo -e '\e[44m\e[1M\e[0m'
|
// test: echo -e '\e[44m\e[1M\e[0m'
|
||||||
// blankLine(true) - xterm/linux behavior
|
// blankLine(true) - xterm/linux behavior
|
||||||
buffer.lines.splice(row, 1);
|
buffer.lines.splice(row, 1);
|
||||||
buffer.lines.splice(j, 0, this._terminal.blankLine(true));
|
buffer.lines.splice(j, 0, BufferLine.blankLine(this._terminal.cols, this._terminal.eraseAttr()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// this.maxRange();
|
// this.maxRange();
|
||||||
@@ -855,22 +875,12 @@ export class InputHandler extends Disposable implements IInputHandler {
|
|||||||
* Delete Ps Character(s) (default = 1) (DCH).
|
* Delete Ps Character(s) (default = 1) (DCH).
|
||||||
*/
|
*/
|
||||||
public deleteChars(params: number[]): void {
|
public deleteChars(params: number[]): void {
|
||||||
let param: number = params[0];
|
this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).deleteCells(
|
||||||
if (param < 1) {
|
this._terminal.buffer.x,
|
||||||
param = 1;
|
params[0] || 1,
|
||||||
}
|
[this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
|
||||||
|
);
|
||||||
// make buffer local for faster access
|
this._terminal.updateRange(this._terminal.buffer.y);
|
||||||
const buffer = this._terminal.buffer;
|
|
||||||
|
|
||||||
const row = buffer.y + buffer.ybase;
|
|
||||||
const ch: CharData = [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm
|
|
||||||
|
|
||||||
while (param--) {
|
|
||||||
buffer.lines.get(row).splice(buffer.x, 1);
|
|
||||||
buffer.lines.get(row).push(ch);
|
|
||||||
}
|
|
||||||
this._terminal.updateRange(buffer.y);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -884,7 +894,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
|||||||
|
|
||||||
while (param--) {
|
while (param--) {
|
||||||
buffer.lines.splice(buffer.ybase + buffer.scrollTop, 1);
|
buffer.lines.splice(buffer.ybase + buffer.scrollTop, 1);
|
||||||
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, this._terminal.blankLine());
|
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, BufferLine.blankLine(this._terminal.cols, DEFAULT_ATTR));
|
||||||
}
|
}
|
||||||
// this.maxRange();
|
// this.maxRange();
|
||||||
this._terminal.updateRange(buffer.scrollTop);
|
this._terminal.updateRange(buffer.scrollTop);
|
||||||
@@ -903,7 +913,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
|||||||
|
|
||||||
while (param--) {
|
while (param--) {
|
||||||
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1);
|
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1);
|
||||||
buffer.lines.splice(buffer.ybase + buffer.scrollTop, 0, this._terminal.blankLine());
|
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, BufferLine.blankLine(this._terminal.cols, DEFAULT_ATTR));
|
||||||
}
|
}
|
||||||
// this.maxRange();
|
// this.maxRange();
|
||||||
this._terminal.updateRange(buffer.scrollTop);
|
this._terminal.updateRange(buffer.scrollTop);
|
||||||
@@ -916,21 +926,11 @@ export class InputHandler extends Disposable implements IInputHandler {
|
|||||||
* Erase Ps Character(s) (default = 1) (ECH).
|
* Erase Ps Character(s) (default = 1) (ECH).
|
||||||
*/
|
*/
|
||||||
public eraseChars(params: number[]): void {
|
public eraseChars(params: number[]): void {
|
||||||
let param = params[0];
|
this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).replaceCells(
|
||||||
if (param < 1) {
|
this._terminal.buffer.x,
|
||||||
param = 1;
|
this._terminal.buffer.x + (params[0] || 1),
|
||||||
}
|
[this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
|
||||||
|
);
|
||||||
// make buffer local for faster access
|
|
||||||
const buffer = this._terminal.buffer;
|
|
||||||
|
|
||||||
const row = buffer.y + buffer.ybase;
|
|
||||||
let j = buffer.x;
|
|
||||||
const ch: CharData = [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm
|
|
||||||
|
|
||||||
while (param-- && j < this._terminal.cols) {
|
|
||||||
buffer.lines.get(row)[j++] = ch;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -982,17 +982,14 @@ export class InputHandler extends Disposable implements IInputHandler {
|
|||||||
* CSI Ps b Repeat the preceding graphic character Ps times (REP).
|
* CSI Ps b Repeat the preceding graphic character Ps times (REP).
|
||||||
*/
|
*/
|
||||||
public repeatPrecedingCharacter(params: number[]): void {
|
public repeatPrecedingCharacter(params: number[]): void {
|
||||||
let param = params[0] || 1;
|
|
||||||
|
|
||||||
// make buffer local for faster access
|
// make buffer local for faster access
|
||||||
const buffer = this._terminal.buffer;
|
const buffer = this._terminal.buffer;
|
||||||
|
|
||||||
const line = buffer.lines.get(buffer.ybase + buffer.y);
|
const line = buffer.lines.get(buffer.ybase + buffer.y);
|
||||||
const ch = line[buffer.x - 1] || [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
|
line.replaceCells(buffer.x,
|
||||||
|
buffer.x + (params[0] || 1),
|
||||||
while (param--) {
|
line.get(buffer.x - 1) || [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
|
||||||
line[buffer.x++] = ch;
|
);
|
||||||
}
|
// FIXME: no updateRange here?
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1631,6 +1628,9 @@ export class InputHandler extends Disposable implements IInputHandler {
|
|||||||
// not bold nor faint
|
// not bold nor faint
|
||||||
flags &= ~FLAGS.BOLD;
|
flags &= ~FLAGS.BOLD;
|
||||||
flags &= ~FLAGS.DIM;
|
flags &= ~FLAGS.DIM;
|
||||||
|
} else if (p === 23) {
|
||||||
|
// not italic
|
||||||
|
flags &= ~FLAGS.ITALIC;
|
||||||
} else if (p === 24) {
|
} else if (p === 24) {
|
||||||
// not underlined
|
// not underlined
|
||||||
flags &= ~FLAGS.UNDERLINE;
|
flags &= ~FLAGS.UNDERLINE;
|
||||||
|
|||||||
+102
-5
@@ -5,10 +5,11 @@
|
|||||||
|
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import { IMouseZoneManager, IMouseZone } from './ui/Types';
|
import { IMouseZoneManager, IMouseZone } from './ui/Types';
|
||||||
import { ILinkMatcher, LineData, ITerminal } from './Types';
|
import { ILinkMatcher, ITerminal, IBufferLine } from './Types';
|
||||||
import { Linkifier } from './Linkifier';
|
import { Linkifier } from './Linkifier';
|
||||||
import { MockBuffer, MockTerminal } from './utils/TestUtils.test';
|
import { MockBuffer, MockTerminal, TestTerminal } from './utils/TestUtils.test';
|
||||||
import { CircularList } from './common/CircularList';
|
import { CircularList } from './common/CircularList';
|
||||||
|
import { BufferLine } from './BufferLine';
|
||||||
|
|
||||||
class TestLinkifier extends Linkifier {
|
class TestLinkifier extends Linkifier {
|
||||||
constructor(terminal: ITerminal) {
|
constructor(terminal: ITerminal) {
|
||||||
@@ -41,15 +42,16 @@ describe('Linkifier', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
terminal = new MockTerminal();
|
terminal = new MockTerminal();
|
||||||
terminal.cols = 100;
|
terminal.cols = 100;
|
||||||
|
terminal.rows = 10;
|
||||||
terminal.buffer = new MockBuffer();
|
terminal.buffer = new MockBuffer();
|
||||||
(<MockBuffer>terminal.buffer).setLines(new CircularList<LineData>(20));
|
(<MockBuffer>terminal.buffer).setLines(new CircularList<IBufferLine>(20));
|
||||||
terminal.buffer.ydisp = 0;
|
terminal.buffer.ydisp = 0;
|
||||||
linkifier = new TestLinkifier(terminal);
|
linkifier = new TestLinkifier(terminal);
|
||||||
mouseZoneManager = new TestMouseZoneManager();
|
mouseZoneManager = new TestMouseZoneManager();
|
||||||
});
|
});
|
||||||
|
|
||||||
function stringToRow(text: string): LineData {
|
function stringToRow(text: string): IBufferLine {
|
||||||
const result: LineData = [];
|
const result = new BufferLine();
|
||||||
for (let i = 0; i < text.length; i++) {
|
for (let i = 0; i < text.length; i++) {
|
||||||
result.push([0, text.charAt(i), 1, text.charCodeAt(i)]);
|
result.push([0, text.charAt(i), 1, text.charCodeAt(i)]);
|
||||||
}
|
}
|
||||||
@@ -63,6 +65,7 @@ describe('Linkifier', () => {
|
|||||||
function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, links: {x: number, length: number}[], done: MochaDone): void {
|
function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, links: {x: number, length: number}[], done: MochaDone): void {
|
||||||
addRow(rowText);
|
addRow(rowText);
|
||||||
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
|
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
|
||||||
|
terminal.rows = terminal.buffer.lines.length - 1;
|
||||||
linkifier.linkifyRows();
|
linkifier.linkifyRows();
|
||||||
// Allow linkify to happen
|
// Allow linkify to happen
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -237,4 +240,98 @@ describe('Linkifier', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('unicode handling', () => {
|
||||||
|
let terminal: TestTerminal;
|
||||||
|
|
||||||
|
// other than the tests above unicode testing needs the full terminal instance
|
||||||
|
// to get the special handling of fullwidth, surrogate and combining chars in the input handler
|
||||||
|
beforeEach(() => {
|
||||||
|
terminal = new TestTerminal({cols: 10, rows: 5});
|
||||||
|
linkifier = new TestLinkifier(terminal);
|
||||||
|
mouseZoneManager = new TestMouseZoneManager();
|
||||||
|
linkifier.attachToDom(mouseZoneManager);
|
||||||
|
});
|
||||||
|
|
||||||
|
function assertLinkifiesInTerminal(rowText: string, linkMatcherRegex: RegExp, links: {x1: number, y1: number, x2: number, y2: number}[], done: MochaDone): void {
|
||||||
|
terminal.writeSync(rowText);
|
||||||
|
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
|
||||||
|
linkifier.linkifyRows();
|
||||||
|
// Allow linkify to happen
|
||||||
|
setTimeout(() => {
|
||||||
|
assert.equal(mouseZoneManager.zones.length, links.length);
|
||||||
|
links.forEach((l, i) => {
|
||||||
|
assert.equal(mouseZoneManager.zones[i].x1, l.x1 + 1);
|
||||||
|
assert.equal(mouseZoneManager.zones[i].x2, l.x2 + 1);
|
||||||
|
assert.equal(mouseZoneManager.zones[i].y1, l.y1 + 1);
|
||||||
|
assert.equal(mouseZoneManager.zones[i].y2, l.y2 + 1);
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('unicode before the match', () => {
|
||||||
|
it('combining - match within one line', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done);
|
||||||
|
});
|
||||||
|
it('combining - match over two lines', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
|
||||||
|
});
|
||||||
|
it('surrogate - match within one line', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done);
|
||||||
|
});
|
||||||
|
it('surrogate - match over two lines', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
|
||||||
|
});
|
||||||
|
it('combining surrogate - match within one line', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done);
|
||||||
|
});
|
||||||
|
it('combining surrogate - match over two lines', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
|
||||||
|
});
|
||||||
|
it('fullwidth - match within one line', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
|
||||||
|
});
|
||||||
|
it('fullwidth - match over two lines', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
|
||||||
|
});
|
||||||
|
it('combining fullwidth - match within one line', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
|
||||||
|
});
|
||||||
|
it('combining fullwidth - match over two lines', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('unicode within the match', () => {
|
||||||
|
it('combining - match within one line', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('test cafe\u0301', /cafe\u0301/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done);
|
||||||
|
});
|
||||||
|
it('combining - match over two lines', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('testtest cafe\u0301', /cafe\u0301/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done);
|
||||||
|
});
|
||||||
|
it('surrogate - match within one line', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('test a𝄞b', /a𝄞b/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
|
||||||
|
});
|
||||||
|
it('surrogate - match over two lines', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('testtest a𝄞b', /a𝄞b/, [{x1: 9, x2: 2, y1: 0, y2: 1}], done);
|
||||||
|
});
|
||||||
|
it('combining surrogate - match within one line', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('test a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
|
||||||
|
});
|
||||||
|
it('combining surrogate - match over two lines', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('testtest a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 9, x2: 2, y1: 0, y2: 1}], done);
|
||||||
|
});
|
||||||
|
it('fullwidth - match within one line', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('test a1b', /a1b/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done);
|
||||||
|
});
|
||||||
|
it('fullwidth - match over two lines', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('testtest a1b', /a1b/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done);
|
||||||
|
});
|
||||||
|
it('combining fullwidth - match within one line', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('test a¥\u0301b', /a¥\u0301b/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done);
|
||||||
|
});
|
||||||
|
it('combining fullwidth - match over two lines', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('testtest a¥\u0301b', /a¥\u0301b/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+101
-91
@@ -4,9 +4,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { IMouseZoneManager } from './ui/Types';
|
import { IMouseZoneManager } from './ui/Types';
|
||||||
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal, LineData } from './Types';
|
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal, IBufferStringIteratorResult } from './Types';
|
||||||
import { MouseZone } from './ui/MouseZoneManager';
|
import { MouseZone } from './ui/MouseZoneManager';
|
||||||
import { EventEmitter } from './EventEmitter';
|
import { EventEmitter } from './common/EventEmitter';
|
||||||
|
import { CHAR_DATA_ATTR_INDEX } from './Buffer';
|
||||||
|
import { getStringCellWidth } from './CharWidth';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Linkifier applies links to rows shortly after they have been refreshed.
|
* The Linkifier applies links to rows shortly after they have been refreshed.
|
||||||
@@ -19,12 +21,19 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
|||||||
*/
|
*/
|
||||||
protected static readonly TIME_BEFORE_LINKIFY = 200;
|
protected static readonly TIME_BEFORE_LINKIFY = 200;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limit of the unwrapping line expansion (overscan) at the top and bottom
|
||||||
|
* of the actual viewport in ASCII characters.
|
||||||
|
* A limit of 2000 should match most sane urls.
|
||||||
|
*/
|
||||||
|
protected static readonly OVERSCAN_CHAR_LIMIT = 2000;
|
||||||
|
|
||||||
protected _linkMatchers: ILinkMatcher[] = [];
|
protected _linkMatchers: ILinkMatcher[] = [];
|
||||||
|
|
||||||
private _mouseZoneManager: IMouseZoneManager;
|
private _mouseZoneManager: IMouseZoneManager;
|
||||||
private _rowsTimeoutId: number;
|
private _rowsTimeoutId: number;
|
||||||
private _nextLinkMatcherId = 0;
|
private _nextLinkMatcherId = 0;
|
||||||
private _rowsToLinkify: {start: number, end: number};
|
private _rowsToLinkify: { start: number, end: number };
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected _terminal: ITerminal
|
protected _terminal: ITerminal
|
||||||
@@ -79,9 +88,37 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
|||||||
*/
|
*/
|
||||||
private _linkifyRows(): void {
|
private _linkifyRows(): void {
|
||||||
this._rowsTimeoutId = null;
|
this._rowsTimeoutId = null;
|
||||||
for (let i = this._rowsToLinkify.start; i <= this._rowsToLinkify.end; i++) {
|
const buffer = this._terminal.buffer;
|
||||||
this._linkifyRow(i);
|
|
||||||
|
// Ensure the start row exists
|
||||||
|
const absoluteRowIndexStart = buffer.ydisp + this._rowsToLinkify.start;
|
||||||
|
if (absoluteRowIndexStart >= buffer.lines.length) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Invalidate bad end row values (if a resize happened)
|
||||||
|
const absoluteRowIndexEnd = buffer.ydisp + Math.min(this._rowsToLinkify.end, this._terminal.rows) + 1;
|
||||||
|
|
||||||
|
// Iterate over the range of unwrapped content strings within start..end
|
||||||
|
// (excluding).
|
||||||
|
// _doLinkifyRow gets full unwrapped lines with the start row as buffer offset
|
||||||
|
// for every matcher.
|
||||||
|
// The unwrapping is needed to also match content that got wrapped across
|
||||||
|
// several buffer lines. To avoid a worst case scenario where the whole buffer
|
||||||
|
// contains just a single unwrapped string we limit this line expansion beyond
|
||||||
|
// the viewport to +OVERSCAN_CHAR_LIMIT chars (overscan) at top and bottom.
|
||||||
|
// This comes with the tradeoff that matches longer than OVERSCAN_CHAR_LIMIT
|
||||||
|
// chars will not match anymore at the viewport borders.
|
||||||
|
const overscanLineLimit = Math.ceil(Linkifier.OVERSCAN_CHAR_LIMIT / this._terminal.cols);
|
||||||
|
const iterator = this._terminal.buffer.iterator(
|
||||||
|
false, absoluteRowIndexStart, absoluteRowIndexEnd, overscanLineLimit, overscanLineLimit);
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
const lineData: IBufferStringIteratorResult = iterator.next();
|
||||||
|
for (let i = 0; i < this._linkMatchers.length; i++) {
|
||||||
|
this._doLinkifyRow(lineData.range.first, lineData.content, this._linkMatchers[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this._rowsToLinkify.start = null;
|
this._rowsToLinkify.start = null;
|
||||||
this._rowsToLinkify.end = null;
|
this._rowsToLinkify.end = null;
|
||||||
}
|
}
|
||||||
@@ -152,90 +189,61 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Linkifies a row.
|
|
||||||
* @param rowIndex The index of the row to linkify.
|
|
||||||
*/
|
|
||||||
private _linkifyRow(rowIndex: number): void {
|
|
||||||
// Ensure the row exists
|
|
||||||
let absoluteRowIndex = this._terminal.buffer.ydisp + rowIndex;
|
|
||||||
if (absoluteRowIndex >= this._terminal.buffer.lines.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((<any>this._terminal.buffer.lines.get(absoluteRowIndex)).isWrapped) {
|
|
||||||
// Only attempt to linkify rows that start in the viewport
|
|
||||||
if (rowIndex !== 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// If the first row is wrapped, backtrack to find the origin row and linkify that
|
|
||||||
let line: LineData;
|
|
||||||
|
|
||||||
do {
|
|
||||||
rowIndex--;
|
|
||||||
absoluteRowIndex--;
|
|
||||||
line = this._terminal.buffer.lines.get(absoluteRowIndex);
|
|
||||||
|
|
||||||
if (!line) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
} while ((<any>line).isWrapped);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct full unwrapped line text
|
|
||||||
let text = this._terminal.buffer.translateBufferLineToString(absoluteRowIndex, false);
|
|
||||||
let currentIndex = absoluteRowIndex + 1;
|
|
||||||
while (currentIndex < this._terminal.buffer.lines.length &&
|
|
||||||
(<any>this._terminal.buffer.lines.get(currentIndex)).isWrapped) {
|
|
||||||
text += this._terminal.buffer.translateBufferLineToString(currentIndex++, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < this._linkMatchers.length; i++) {
|
|
||||||
this._doLinkifyRow(rowIndex, text, this._linkMatchers[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Linkifies a row given a specific handler.
|
* Linkifies a row given a specific handler.
|
||||||
* @param rowIndex The row index to linkify.
|
* @param rowIndex The row index to linkify (absolute index).
|
||||||
* @param text The text of the row (excludes text in the row that's already
|
* @param text string content of the unwrapped row.
|
||||||
* linkified).
|
|
||||||
* @param matcher The link matcher for this line.
|
* @param matcher The link matcher for this line.
|
||||||
* @param offset The how much of the row has already been linkified.
|
|
||||||
* @return The link element(s) that were added.
|
|
||||||
*/
|
*/
|
||||||
private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher, offset: number = 0): void {
|
private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher): void {
|
||||||
// Find the first match
|
// clone regex to do a global search on text
|
||||||
const match = text.match(matcher.regex);
|
const rex = new RegExp(matcher.regex.source, matcher.regex.flags + 'g');
|
||||||
if (!match || match.length === 0) {
|
let match;
|
||||||
return;
|
let stringIndex = -1;
|
||||||
}
|
while ((match = rex.exec(text)) !== null) {
|
||||||
const uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex];
|
const uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex];
|
||||||
|
if (!uri) {
|
||||||
// Get index, match.index is for the outer match which includes negated chars
|
// something matched but does not comply with the given matchIndex
|
||||||
const index = text.indexOf(uri);
|
// since this is most likely a bug the regex itself we simply do nothing here
|
||||||
|
// DEBUG: print match and throw
|
||||||
// Ensure the link is valid before registering
|
if ((<any>this._terminal).debug) {
|
||||||
if (matcher.validationCallback) {
|
console.log({match, matcher});
|
||||||
matcher.validationCallback(uri, isValid => {
|
throw new Error('match found without corresponding matchIndex');
|
||||||
// Discard link if the line has already changed
|
|
||||||
if (this._rowsTimeoutId) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (isValid) {
|
break;
|
||||||
this._addLink(offset + index, rowIndex, uri, matcher);
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this._addLink(offset + index, rowIndex, uri, matcher);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recursively check for links in the rest of the text
|
// Get index, match.index is for the outer match which includes negated chars
|
||||||
const remainingStartIndex = index + uri.length;
|
// therefore we cannot use match.index directly, instead we search the position
|
||||||
const remainingText = text.substr(remainingStartIndex);
|
// of the match group in text again
|
||||||
if (remainingText.length > 0) {
|
// also correct regex and string search offsets for the next loop run
|
||||||
this._doLinkifyRow(rowIndex, remainingText, matcher, offset + remainingStartIndex);
|
stringIndex = text.indexOf(uri, stringIndex + 1);
|
||||||
|
rex.lastIndex = stringIndex + uri.length;
|
||||||
|
|
||||||
|
// get the buffer index as [absolute row, col] for the match
|
||||||
|
const bufferIndex = this._terminal.buffer.stringIndexToBufferIndex(rowIndex, stringIndex);
|
||||||
|
|
||||||
|
const line = this._terminal.buffer.lines.get(bufferIndex[0]);
|
||||||
|
const char = line.get(bufferIndex[1]);
|
||||||
|
let fg: number | undefined;
|
||||||
|
if (char) {
|
||||||
|
const attr: number = char[CHAR_DATA_ATTR_INDEX];
|
||||||
|
fg = (attr >> 9) & 0x1ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matcher.validationCallback) {
|
||||||
|
matcher.validationCallback(uri, isValid => {
|
||||||
|
// Discard link if the line has already changed
|
||||||
|
if (this._rowsTimeoutId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isValid) {
|
||||||
|
this._addLink(bufferIndex[1], bufferIndex[0] - this._terminal.buffer.ydisp, uri, matcher, fg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this._addLink(bufferIndex[1], bufferIndex[0] - this._terminal.buffer.ydisp, uri, matcher, fg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,12 +253,14 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
|||||||
* @param y The row the link is on.
|
* @param y The row the link is on.
|
||||||
* @param uri The URI of the link.
|
* @param uri The URI of the link.
|
||||||
* @param matcher The link matcher for the link.
|
* @param matcher The link matcher for the link.
|
||||||
|
* @param fg The link color for hover event.
|
||||||
*/
|
*/
|
||||||
private _addLink(x: number, y: number, uri: string, matcher: ILinkMatcher): void {
|
private _addLink(x: number, y: number, uri: string, matcher: ILinkMatcher, fg: number): void {
|
||||||
|
const width = getStringCellWidth(uri);
|
||||||
const x1 = x % this._terminal.cols;
|
const x1 = x % this._terminal.cols;
|
||||||
const y1 = y + Math.floor(x / this._terminal.cols);
|
const y1 = y + Math.floor(x / this._terminal.cols);
|
||||||
let x2 = (x1 + uri.length) % this._terminal.cols;
|
let x2 = (x1 + width) % this._terminal.cols;
|
||||||
let y2 = y1 + Math.floor((x1 + uri.length) / this._terminal.cols);
|
let y2 = y1 + Math.floor((x1 + width) / this._terminal.cols);
|
||||||
if (x2 === 0) {
|
if (x2 === 0) {
|
||||||
x2 = this._terminal.cols;
|
x2 = this._terminal.cols;
|
||||||
y2--;
|
y2--;
|
||||||
@@ -268,17 +278,17 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
|||||||
window.open(uri, '_blank');
|
window.open(uri, '_blank');
|
||||||
},
|
},
|
||||||
e => {
|
e => {
|
||||||
this.emit(LinkHoverEventTypes.HOVER, this._createLinkHoverEvent(x1, y1, x2, y2));
|
this.emit(LinkHoverEventTypes.HOVER, this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||||
this._terminal.element.classList.add('xterm-cursor-pointer');
|
this._terminal.element.classList.add('xterm-cursor-pointer');
|
||||||
},
|
},
|
||||||
e => {
|
e => {
|
||||||
this.emit(LinkHoverEventTypes.TOOLTIP, this._createLinkHoverEvent(x1, y1, x2, y2));
|
this.emit(LinkHoverEventTypes.TOOLTIP, this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||||
if (matcher.hoverTooltipCallback) {
|
if (matcher.hoverTooltipCallback) {
|
||||||
matcher.hoverTooltipCallback(e, uri);
|
matcher.hoverTooltipCallback(e, uri);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
this.emit(LinkHoverEventTypes.LEAVE, this._createLinkHoverEvent(x1, y1, x2, y2));
|
this.emit(LinkHoverEventTypes.LEAVE, this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||||
this._terminal.element.classList.remove('xterm-cursor-pointer');
|
this._terminal.element.classList.remove('xterm-cursor-pointer');
|
||||||
if (matcher.hoverLeaveCallback) {
|
if (matcher.hoverLeaveCallback) {
|
||||||
matcher.hoverLeaveCallback();
|
matcher.hoverLeaveCallback();
|
||||||
@@ -293,7 +303,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number): ILinkHoverEvent {
|
private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number, fg: number): ILinkHoverEvent {
|
||||||
return { x1, y1, x2, y2, cols: this._terminal.cols };
|
return { x1, y1, x2, y2, cols: this._terminal.cols, fg };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import { CharMeasure } from './ui/CharMeasure';
|
|||||||
import { SelectionManager, SelectionMode } from './SelectionManager';
|
import { SelectionManager, SelectionMode } from './SelectionManager';
|
||||||
import { SelectionModel } from './SelectionModel';
|
import { SelectionModel } from './SelectionModel';
|
||||||
import { BufferSet } from './BufferSet';
|
import { BufferSet } from './BufferSet';
|
||||||
import { LineData, CharData, ITerminal, IBuffer } from './Types';
|
import { ITerminal, IBuffer, IBufferLine } from './Types';
|
||||||
import { MockTerminal } from './utils/TestUtils.test';
|
import { MockTerminal } from './utils/TestUtils.test';
|
||||||
|
import { BufferLine } from './BufferLine';
|
||||||
|
|
||||||
class TestMockTerminal extends MockTerminal {
|
class TestMockTerminal extends MockTerminal {
|
||||||
emit(event: string, data: any): void {}
|
emit(event: string, data: any): void {}
|
||||||
@@ -52,16 +53,18 @@ describe('SelectionManager', () => {
|
|||||||
selectionManager = new TestSelectionManager(terminal, null);
|
selectionManager = new TestSelectionManager(terminal, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
function stringToRow(text: string): LineData {
|
function stringToRow(text: string): IBufferLine {
|
||||||
const result: LineData = [];
|
const result = new BufferLine();
|
||||||
for (let i = 0; i < text.length; i++) {
|
for (let i = 0; i < text.length; i++) {
|
||||||
result.push([0, text.charAt(i), 1, text.charCodeAt(i)]);
|
result.push([0, text.charAt(i), 1, text.charCodeAt(i)]);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function stringArrayToRow(chars: string[]): LineData {
|
function stringArrayToRow(chars: string[]): IBufferLine {
|
||||||
return chars.map(c => <CharData>[0, c, 1, c.charCodeAt(0)]);
|
const line = new BufferLine();
|
||||||
|
chars.map(c => line.push([0, c, 1, c.charCodeAt(0)]));
|
||||||
|
return line;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('_selectWordAt', () => {
|
describe('_selectWordAt', () => {
|
||||||
@@ -97,7 +100,8 @@ describe('SelectionManager', () => {
|
|||||||
});
|
});
|
||||||
it('should expand selection for wide characters', () => {
|
it('should expand selection for wide characters', () => {
|
||||||
// Wide characters use a special format
|
// Wide characters use a special format
|
||||||
buffer.lines.set(0, [
|
const line = new BufferLine();
|
||||||
|
const data: [number, string, number, number][] = [
|
||||||
[null, '中', 2, '中'.charCodeAt(0)],
|
[null, '中', 2, '中'.charCodeAt(0)],
|
||||||
[null, '', 0, null],
|
[null, '', 0, null],
|
||||||
[null, '文', 2, '文'.charCodeAt(0)],
|
[null, '文', 2, '文'.charCodeAt(0)],
|
||||||
@@ -113,7 +117,9 @@ describe('SelectionManager', () => {
|
|||||||
[null, 'f', 1, 'f'.charCodeAt(0)],
|
[null, 'f', 1, 'f'.charCodeAt(0)],
|
||||||
[null, 'o', 1, 'o'.charCodeAt(0)],
|
[null, 'o', 1, 'o'.charCodeAt(0)],
|
||||||
[null, 'o', 1, 'o'.charCodeAt(0)]
|
[null, 'o', 1, 'o'.charCodeAt(0)]
|
||||||
]);
|
];
|
||||||
|
for (let i = 0; i < data.length; ++i) line.push(data[i]);
|
||||||
|
buffer.lines.set(0, line);
|
||||||
// Ensure wide characters take up 2 columns
|
// Ensure wide characters take up 2 columns
|
||||||
selectionManager.selectWordAt([0, 0]);
|
selectionManager.selectWordAt([0, 0]);
|
||||||
assert.equal(selectionManager.selectionText, '中文');
|
assert.equal(selectionManager.selectionText, '中文');
|
||||||
@@ -186,7 +192,7 @@ describe('SelectionManager', () => {
|
|||||||
it('should expand upwards or downards for wrapped lines', () => {
|
it('should expand upwards or downards for wrapped lines', () => {
|
||||||
buffer.lines.set(0, stringToRow(' foo'));
|
buffer.lines.set(0, stringToRow(' foo'));
|
||||||
buffer.lines.set(1, stringToRow('bar '));
|
buffer.lines.set(1, stringToRow('bar '));
|
||||||
(<any>buffer.lines.get(1)).isWrapped = true;
|
buffer.lines.get(1).isWrapped = true;
|
||||||
selectionManager.selectWordAt([1, 1]);
|
selectionManager.selectWordAt([1, 1]);
|
||||||
assert.equal(selectionManager.selectionText, 'foobar');
|
assert.equal(selectionManager.selectionText, 'foobar');
|
||||||
selectionManager.model.clearSelection();
|
selectionManager.model.clearSelection();
|
||||||
@@ -200,10 +206,10 @@ describe('SelectionManager', () => {
|
|||||||
buffer.lines.set(2, stringToRow('bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'));
|
buffer.lines.set(2, stringToRow('bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'));
|
||||||
buffer.lines.set(3, stringToRow('cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc'));
|
buffer.lines.set(3, stringToRow('cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc'));
|
||||||
buffer.lines.set(4, stringToRow('bar '));
|
buffer.lines.set(4, stringToRow('bar '));
|
||||||
(<any>buffer.lines.get(1)).isWrapped = true;
|
buffer.lines.get(1).isWrapped = true;
|
||||||
(<any>buffer.lines.get(2)).isWrapped = true;
|
buffer.lines.get(2).isWrapped = true;
|
||||||
(<any>buffer.lines.get(3)).isWrapped = true;
|
buffer.lines.get(3).isWrapped = true;
|
||||||
(<any>buffer.lines.get(4)).isWrapped = true;
|
buffer.lines.get(4).isWrapped = true;
|
||||||
selectionManager.selectWordAt([78, 0]);
|
selectionManager.selectWordAt([78, 0]);
|
||||||
assert.equal(selectionManager.selectionText, expectedText);
|
assert.equal(selectionManager.selectionText, expectedText);
|
||||||
selectionManager.model.clearSelection();
|
selectionManager.model.clearSelection();
|
||||||
@@ -339,7 +345,7 @@ describe('SelectionManager', () => {
|
|||||||
it('should select the entire wrapped line', () => {
|
it('should select the entire wrapped line', () => {
|
||||||
buffer.lines.set(0, stringToRow('foo'));
|
buffer.lines.set(0, stringToRow('foo'));
|
||||||
const line2 = stringToRow('bar');
|
const line2 = stringToRow('bar');
|
||||||
(<any>line2).isWrapped = true;
|
line2.isWrapped = true;
|
||||||
buffer.lines.set(1, line2);
|
buffer.lines.set(1, line2);
|
||||||
selectionManager.selectLineAt(0);
|
selectionManager.selectLineAt(0);
|
||||||
assert.equal(selectionManager.selectionText, 'foobar', 'The selected text is correct');
|
assert.equal(selectionManager.selectionText, 'foobar', 'The selected text is correct');
|
||||||
|
|||||||
+22
-21
@@ -3,11 +3,12 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ITerminal, ISelectionManager, IBuffer, CharData, XtermListener } from './Types';
|
import { ITerminal, ISelectionManager, IBuffer, CharData, IBufferLine } from './Types';
|
||||||
|
import { XtermListener } from './common/Types';
|
||||||
import { MouseHelper } from './utils/MouseHelper';
|
import { MouseHelper } from './utils/MouseHelper';
|
||||||
import * as Browser from './shared/utils/Browser';
|
import * as Browser from './shared/utils/Browser';
|
||||||
import { CharMeasure } from './ui/CharMeasure';
|
import { CharMeasure } from './ui/CharMeasure';
|
||||||
import { EventEmitter } from './EventEmitter';
|
import { EventEmitter } from './common/EventEmitter';
|
||||||
import { SelectionModel } from './SelectionModel';
|
import { SelectionModel } from './SelectionModel';
|
||||||
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_CODE_INDEX } from './Buffer';
|
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_CODE_INDEX } from './Buffer';
|
||||||
import { AltClickHandler } from './handlers/AltClickHandler';
|
import { AltClickHandler } from './handlers/AltClickHandler';
|
||||||
@@ -204,7 +205,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
for (let i = start[1] + 1; i <= end[1] - 1; i++) {
|
for (let i = start[1] + 1; i <= end[1] - 1; i++) {
|
||||||
const bufferLine = this._buffer.lines.get(i);
|
const bufferLine = this._buffer.lines.get(i);
|
||||||
const lineText = this._buffer.translateBufferLineToString(i, true);
|
const lineText = this._buffer.translateBufferLineToString(i, true);
|
||||||
if ((<any>bufferLine).isWrapped) {
|
if (bufferLine.isWrapped) {
|
||||||
result[result.length - 1] += lineText;
|
result[result.length - 1] += lineText;
|
||||||
} else {
|
} else {
|
||||||
result.push(lineText);
|
result.push(lineText);
|
||||||
@@ -215,7 +216,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
if (start[1] !== end[1]) {
|
if (start[1] !== end[1]) {
|
||||||
const bufferLine = this._buffer.lines.get(end[1]);
|
const bufferLine = this._buffer.lines.get(end[1]);
|
||||||
const lineText = this._buffer.translateBufferLineToString(end[1], true, 0, end[0]);
|
const lineText = this._buffer.translateBufferLineToString(end[1], true, 0, end[0]);
|
||||||
if ((<any>bufferLine).isWrapped) {
|
if (bufferLine.isWrapped) {
|
||||||
result[result.length - 1] += lineText;
|
result[result.length - 1] += lineText;
|
||||||
} else {
|
} else {
|
||||||
result.push(lineText);
|
result.push(lineText);
|
||||||
@@ -500,7 +501,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
|
|
||||||
// If the mouse is over the second half of a wide character, adjust the
|
// If the mouse is over the second half of a wide character, adjust the
|
||||||
// selection to cover the whole character
|
// selection to cover the whole character
|
||||||
const char = line[this._model.selectionStart[0]];
|
const char = line.get(this._model.selectionStart[0]);
|
||||||
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||||
this._model.selectionStart[0]++;
|
this._model.selectionStart[0]++;
|
||||||
}
|
}
|
||||||
@@ -590,7 +591,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
// selection. Note that selections at the very end of the line will never
|
// selection. Note that selections at the very end of the line will never
|
||||||
// have a character.
|
// have a character.
|
||||||
if (this._model.selectionEnd[1] < this._buffer.lines.length) {
|
if (this._model.selectionEnd[1] < this._buffer.lines.length) {
|
||||||
const char = this._buffer.lines.get(this._model.selectionEnd[1])[this._model.selectionEnd[0]];
|
const char = this._buffer.lines.get(this._model.selectionEnd[1]).get(this._model.selectionEnd[0]);
|
||||||
if (char && char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
if (char && char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||||
this._model.selectionEnd[0]++;
|
this._model.selectionEnd[0]++;
|
||||||
}
|
}
|
||||||
@@ -661,10 +662,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
* latter takes into account wide characters.
|
* latter takes into account wide characters.
|
||||||
* @param coords The coordinates to find the 2 index for.
|
* @param coords The coordinates to find the 2 index for.
|
||||||
*/
|
*/
|
||||||
private _convertViewportColToCharacterIndex(bufferLine: any, coords: [number, number]): number {
|
private _convertViewportColToCharacterIndex(bufferLine: IBufferLine, coords: [number, number]): number {
|
||||||
let charIndex = coords[0];
|
let charIndex = coords[0];
|
||||||
for (let i = 0; coords[0] >= i; i++) {
|
for (let i = 0; coords[0] >= i; i++) {
|
||||||
const char = bufferLine[i];
|
const char = bufferLine.get(i);
|
||||||
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||||
// Wide characters aren't included in the line string so decrement the
|
// Wide characters aren't included in the line string so decrement the
|
||||||
// index so the index is back on the wide character.
|
// index so the index is back on the wide character.
|
||||||
@@ -733,24 +734,24 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
|
|
||||||
// Consider the initial position, skip it and increment the wide char
|
// Consider the initial position, skip it and increment the wide char
|
||||||
// variable
|
// variable
|
||||||
if (bufferLine[startCol][CHAR_DATA_WIDTH_INDEX] === 0) {
|
if (bufferLine.get(startCol)[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||||
leftWideCharCount++;
|
leftWideCharCount++;
|
||||||
startCol--;
|
startCol--;
|
||||||
}
|
}
|
||||||
if (bufferLine[endCol][CHAR_DATA_WIDTH_INDEX] === 2) {
|
if (bufferLine.get(endCol)[CHAR_DATA_WIDTH_INDEX] === 2) {
|
||||||
rightWideCharCount++;
|
rightWideCharCount++;
|
||||||
endCol++;
|
endCol++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adjust the end index for characters whose length are > 1 (emojis)
|
// Adjust the end index for characters whose length are > 1 (emojis)
|
||||||
if (bufferLine[endCol][CHAR_DATA_CHAR_INDEX].length > 1) {
|
if (bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length > 1) {
|
||||||
rightLongCharOffset += bufferLine[endCol][CHAR_DATA_CHAR_INDEX].length - 1;
|
rightLongCharOffset += bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length - 1;
|
||||||
endIndex += bufferLine[endCol][CHAR_DATA_CHAR_INDEX].length - 1;
|
endIndex += bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expand the string in both directions until a space is hit
|
// Expand the string in both directions until a space is hit
|
||||||
while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine[startCol - 1])) {
|
while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.get(startCol - 1))) {
|
||||||
const char = bufferLine[startCol - 1];
|
const char = bufferLine.get(startCol - 1);
|
||||||
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||||
// If the next character is a wide char, record it and skip the column
|
// If the next character is a wide char, record it and skip the column
|
||||||
leftWideCharCount++;
|
leftWideCharCount++;
|
||||||
@@ -764,8 +765,8 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
startIndex--;
|
startIndex--;
|
||||||
startCol--;
|
startCol--;
|
||||||
}
|
}
|
||||||
while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine[endCol + 1])) {
|
while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.get(endCol + 1))) {
|
||||||
const char = bufferLine[endCol + 1];
|
const char = bufferLine.get(endCol + 1);
|
||||||
if (char[CHAR_DATA_WIDTH_INDEX] === 2) {
|
if (char[CHAR_DATA_WIDTH_INDEX] === 2) {
|
||||||
// If the next character is a wide char, record it and skip the column
|
// If the next character is a wide char, record it and skip the column
|
||||||
rightWideCharCount++;
|
rightWideCharCount++;
|
||||||
@@ -808,9 +809,9 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
|
|
||||||
// Recurse upwards if the line is wrapped and the word wraps to the above line
|
// Recurse upwards if the line is wrapped and the word wraps to the above line
|
||||||
if (followWrappedLinesAbove) {
|
if (followWrappedLinesAbove) {
|
||||||
if (start === 0 && bufferLine[0][CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
|
if (start === 0 && bufferLine.get(0)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
|
||||||
const previousBufferLine = this._buffer.lines.get(coords[1] - 1);
|
const previousBufferLine = this._buffer.lines.get(coords[1] - 1);
|
||||||
if (previousBufferLine && (<any>bufferLine).isWrapped && previousBufferLine[this._terminal.cols - 1][CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
|
if (previousBufferLine && bufferLine.isWrapped && previousBufferLine.get(this._terminal.cols - 1)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
|
||||||
const previousLineWordPosition = this._getWordAt([this._terminal.cols - 1, coords[1] - 1], false, true, false);
|
const previousLineWordPosition = this._getWordAt([this._terminal.cols - 1, coords[1] - 1], false, true, false);
|
||||||
if (previousLineWordPosition) {
|
if (previousLineWordPosition) {
|
||||||
const offset = this._terminal.cols - previousLineWordPosition.start;
|
const offset = this._terminal.cols - previousLineWordPosition.start;
|
||||||
@@ -823,9 +824,9 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
|
|
||||||
// Recurse downwards if the line is wrapped and the word wraps to the next line
|
// Recurse downwards if the line is wrapped and the word wraps to the next line
|
||||||
if (followWrappedLinesBelow) {
|
if (followWrappedLinesBelow) {
|
||||||
if (start + length === this._terminal.cols && bufferLine[this._terminal.cols - 1][CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
|
if (start + length === this._terminal.cols && bufferLine.get(this._terminal.cols - 1)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
|
||||||
const nextBufferLine = this._buffer.lines.get(coords[1] + 1);
|
const nextBufferLine = this._buffer.lines.get(coords[1] + 1);
|
||||||
if (nextBufferLine && (<any>nextBufferLine).isWrapped && nextBufferLine[0][CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
|
if (nextBufferLine && nextBufferLine.isWrapped && nextBufferLine.get(0)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
|
||||||
const nextLineWordPosition = this._getWordAt([0, coords[1] + 1], false, false, true);
|
const nextLineWordPosition = this._getWordAt([0, coords[1] + 1], false, false, true);
|
||||||
if (nextLineWordPosition) {
|
if (nextLineWordPosition) {
|
||||||
length += nextLineWordPosition.length;
|
length += nextLineWordPosition.length;
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ function terminalToString(term: Terminal): string {
|
|||||||
for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) {
|
for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) {
|
||||||
lineText = '';
|
lineText = '';
|
||||||
for (let cell = 0; cell < term.cols; ++cell) {
|
for (let cell = 0; cell < term.cols; ++cell) {
|
||||||
lineText += term.buffer.lines.get(line)[cell][CHAR_DATA_CHAR_INDEX];
|
lineText += term.buffer.lines.get(line).get(cell)[CHAR_DATA_CHAR_INDEX];
|
||||||
}
|
}
|
||||||
// rtrim empty cells as xterm does
|
// rtrim empty cells as xterm does
|
||||||
lineText = lineText.replace(/\s+$/, '');
|
lineText = lineText.replace(/\s+$/, '');
|
||||||
@@ -105,6 +105,7 @@ if (os.platform() !== 'win32') {
|
|||||||
// omit stack trace for escape sequence files
|
// omit stack trace for escape sequence files
|
||||||
Error.stackTraceLimit = 0;
|
Error.stackTraceLimit = 0;
|
||||||
const files = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '..')});
|
const files = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '..')});
|
||||||
|
// for (let i = 0; i < files.length; ++i) console.debug(i, files[i]);
|
||||||
// only successful tests for now
|
// only successful tests for now
|
||||||
const skip = [
|
const skip = [
|
||||||
10, 16, 17, 19, 32, 33, 34, 35, 36, 39,
|
10, 16, 17, 19, 32, 33, 34, 35, 36, 39,
|
||||||
@@ -163,7 +164,5 @@ describe('typings', () => {
|
|||||||
const fixtureDir = path.join(__dirname, '..', 'fixtures', 'typings-test');
|
const fixtureDir = path.join(__dirname, '..', 'fixtures', 'typings-test');
|
||||||
const result = cp.spawnSync(tsc, { cwd: fixtureDir });
|
const result = cp.spawnSync(tsc, { cwd: fixtureDir });
|
||||||
assert.equal(result.status, 0, `build did not succeed:\nstdout: ${result.stdout.toString()}\nstderr: ${result.stderr.toString()}\n`);
|
assert.equal(result.status, 0, `build did not succeed:\nstdout: ${result.stdout.toString()}\nstderr: ${result.stderr.toString()}\n`);
|
||||||
// Clean up
|
|
||||||
fs.unlinkSync(path.join(fixtureDir, 'typings-test.js'));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+262
-143
@@ -6,7 +6,8 @@
|
|||||||
import { assert, expect } from 'chai';
|
import { assert, expect } from 'chai';
|
||||||
import { Terminal } from './Terminal';
|
import { Terminal } from './Terminal';
|
||||||
import { MockViewport, MockCompositionHelper, MockRenderer } from './utils/TestUtils.test';
|
import { MockViewport, MockCompositionHelper, MockRenderer } from './utils/TestUtils.test';
|
||||||
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer';
|
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, DEFAULT_ATTR } from './Buffer';
|
||||||
|
import { BufferLine } from './BufferLine';
|
||||||
|
|
||||||
const INIT_COLS = 80;
|
const INIT_COLS = 80;
|
||||||
const INIT_ROWS = 24;
|
const INIT_ROWS = 24;
|
||||||
@@ -25,7 +26,7 @@ describe('term.js addons', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
term = new TestTerminal(termOptions);
|
term = new TestTerminal(termOptions);
|
||||||
term.refresh = () => {};
|
term.refresh = () => { };
|
||||||
(<any>term).renderer = new MockRenderer();
|
(<any>term).renderer = new MockRenderer();
|
||||||
term.viewport = new MockViewport();
|
term.viewport = new MockViewport();
|
||||||
(<any>term)._compositionHelper = new MockCompositionHelper();
|
(<any>term)._compositionHelper = new MockCompositionHelper();
|
||||||
@@ -36,8 +37,8 @@ describe('term.js addons', () => {
|
|||||||
};
|
};
|
||||||
(<any>term).element = {
|
(<any>term).element = {
|
||||||
classList: {
|
classList: {
|
||||||
toggle: () => {},
|
toggle: () => { },
|
||||||
remove: () => {}
|
remove: () => { }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -67,24 +68,142 @@ describe('term.js addons', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('on', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
term.on('key', () => { });
|
||||||
|
term.on('keypress', () => { });
|
||||||
|
term.on('keydown', () => { });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('data', () => {
|
||||||
|
it('should emit a data event', (done) => {
|
||||||
|
term.on('data', () => {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
term.handler('fake');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`keypress (including 'key' event)`, () => {
|
||||||
|
it('should receive a string and event object', (done) => {
|
||||||
|
let steps = 0;
|
||||||
|
|
||||||
|
const finish = () => {
|
||||||
|
if ((++steps) === 2) {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const evKeyPress = <KeyboardEvent>{
|
||||||
|
preventDefault: () => { },
|
||||||
|
stopPropagation: () => { },
|
||||||
|
type: 'keypress',
|
||||||
|
keyCode: 13
|
||||||
|
};
|
||||||
|
|
||||||
|
term.on('keypress', (key, event) => {
|
||||||
|
assert.equal(typeof key, 'string');
|
||||||
|
expect(event).to.be.an.instanceof(Object);
|
||||||
|
finish();
|
||||||
|
});
|
||||||
|
|
||||||
|
term.on('key', (key, event) => {
|
||||||
|
assert.equal(typeof key, 'string');
|
||||||
|
expect(event).to.be.an.instanceof(Object);
|
||||||
|
finish();
|
||||||
|
});
|
||||||
|
|
||||||
|
term.keyPress(evKeyPress);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`keydown (including 'key' event)`, () => {
|
||||||
|
it(`should receive an event object for 'keydown' and a string and event object for 'key'`, (done) => {
|
||||||
|
let steps = 0;
|
||||||
|
|
||||||
|
const finish = () => {
|
||||||
|
if ((++steps) === 2) {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const evKeyDown = <KeyboardEvent>{
|
||||||
|
preventDefault: () => { },
|
||||||
|
stopPropagation: () => { },
|
||||||
|
type: 'keydown',
|
||||||
|
keyCode: 13
|
||||||
|
};
|
||||||
|
|
||||||
|
term.on('keydown', (event) => {
|
||||||
|
expect(event).to.be.an.instanceof(Object);
|
||||||
|
finish();
|
||||||
|
});
|
||||||
|
|
||||||
|
term.on('key', (key, event) => {
|
||||||
|
assert.equal(typeof key, 'string');
|
||||||
|
expect(event).to.be.an.instanceof(Object);
|
||||||
|
finish();
|
||||||
|
});
|
||||||
|
|
||||||
|
term.keyDown(evKeyDown);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resize', () => {
|
||||||
|
it('should receive an object: {cols: number, rows: number}', (done) => {
|
||||||
|
term.on('resize', (data) => {
|
||||||
|
expect(data).to.have.keys(['cols', 'rows']);
|
||||||
|
assert.equal(typeof data.cols, 'number');
|
||||||
|
assert.equal(typeof data.rows, 'number');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
term.resize(1, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scroll', () => {
|
||||||
|
it('should receive a number', (done) => {
|
||||||
|
term.on('scroll', (ydisp) => {
|
||||||
|
assert.equal(typeof ydisp, 'number');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
term.scroll();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('title', () => {
|
||||||
|
it('should receive a string', (done) => {
|
||||||
|
term.on('title', (title) => {
|
||||||
|
assert.equal(typeof title, 'string');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
term.handleTitle('title');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('attachCustomKeyEventHandler', () => {
|
describe('attachCustomKeyEventHandler', () => {
|
||||||
const evKeyDown = <KeyboardEvent>{
|
const evKeyDown = <KeyboardEvent>{
|
||||||
preventDefault: () => {},
|
preventDefault: () => { },
|
||||||
stopPropagation: () => {},
|
stopPropagation: () => { },
|
||||||
type: 'keydown',
|
type: 'keydown',
|
||||||
keyCode: 77
|
keyCode: 77
|
||||||
};
|
};
|
||||||
const evKeyPress = <KeyboardEvent>{
|
const evKeyPress = <KeyboardEvent>{
|
||||||
preventDefault: () => {},
|
preventDefault: () => { },
|
||||||
stopPropagation: () => {},
|
stopPropagation: () => { },
|
||||||
type: 'keypress',
|
type: 'keypress',
|
||||||
keyCode: 77
|
keyCode: 77
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
term.handler = () => {};
|
term.handler = () => { };
|
||||||
term.showCursor = () => {};
|
term.showCursor = () => { };
|
||||||
term.clearSelection = () => {};
|
term.clearSelection = () => { };
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should process the keydown/keypress event based on what the handler returns', () => {
|
it('should process the keydown/keypress event based on what the handler returns', () => {
|
||||||
@@ -141,7 +260,7 @@ describe('term.js addons', () => {
|
|||||||
assert.equal(term.buffer.lines.length, term.rows);
|
assert.equal(term.buffer.lines.length, term.rows);
|
||||||
assert.deepEqual(term.buffer.lines.get(0), promptLine);
|
assert.deepEqual(term.buffer.lines.get(0), promptLine);
|
||||||
for (let i = 1; i < term.rows; i++) {
|
for (let i = 1; i < term.rows; i++) {
|
||||||
assert.deepEqual(term.buffer.lines.get(i), term.blankLine());
|
assert.deepEqual(term.buffer.lines.get(i), BufferLine.blankLine(term.cols, DEFAULT_ATTR));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
it('should clear a buffer larger than rows', () => {
|
it('should clear a buffer larger than rows', () => {
|
||||||
@@ -158,7 +277,7 @@ describe('term.js addons', () => {
|
|||||||
assert.equal(term.buffer.lines.length, term.rows);
|
assert.equal(term.buffer.lines.length, term.rows);
|
||||||
assert.deepEqual(term.buffer.lines.get(0), promptLine);
|
assert.deepEqual(term.buffer.lines.get(0), promptLine);
|
||||||
for (let i = 1; i < term.rows; i++) {
|
for (let i = 1; i < term.rows; i++) {
|
||||||
assert.deepEqual(term.buffer.lines.get(i), term.blankLine());
|
assert.deepEqual(term.buffer.lines.get(i), BufferLine.blankLine(term.cols, DEFAULT_ATTR));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
it('should not break the prompt when cleared twice', () => {
|
it('should not break the prompt when cleared twice', () => {
|
||||||
@@ -171,7 +290,7 @@ describe('term.js addons', () => {
|
|||||||
assert.equal(term.buffer.lines.length, term.rows);
|
assert.equal(term.buffer.lines.length, term.rows);
|
||||||
assert.deepEqual(term.buffer.lines.get(0), promptLine);
|
assert.deepEqual(term.buffer.lines.get(0), promptLine);
|
||||||
for (let i = 1; i < term.rows; i++) {
|
for (let i = 1; i < term.rows; i++) {
|
||||||
assert.deepEqual(term.buffer.lines.get(i), term.blankLine());
|
assert.deepEqual(term.buffer.lines.get(i), BufferLine.blankLine(term.cols, DEFAULT_ATTR));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -304,8 +423,8 @@ describe('term.js addons', () => {
|
|||||||
type: 'keydown',
|
type: 'keydown',
|
||||||
key: 'a',
|
key: 'a',
|
||||||
keyCode: 65,
|
keyCode: 65,
|
||||||
preventDefault: () => {},
|
preventDefault: () => { },
|
||||||
stopPropagation: () => {}
|
stopPropagation: () => { }
|
||||||
};
|
};
|
||||||
|
|
||||||
term.buffer.ydisp = 0;
|
term.buffer.ydisp = 0;
|
||||||
@@ -337,62 +456,62 @@ describe('term.js addons', () => {
|
|||||||
describe('scroll() function', () => {
|
describe('scroll() function', () => {
|
||||||
describe('when scrollback > 0', () => {
|
describe('when scrollback > 0', () => {
|
||||||
it('should create a new line and scroll', () => {
|
it('should create a new line and scroll', () => {
|
||||||
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
|
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
|
||||||
term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
|
||||||
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||||
term.scroll();
|
term.scroll();
|
||||||
assert.equal(term.buffer.lines.length, INIT_ROWS + 1);
|
assert.equal(term.buffer.lines.length, INIT_ROWS + 1);
|
||||||
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a');
|
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX], 'b');
|
assert.equal(term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
|
||||||
assert.equal(term.buffer.lines.get(INIT_ROWS)[0][CHAR_DATA_CHAR_INDEX], ' ');
|
assert.equal(term.buffer.lines.get(INIT_ROWS).get(0)[CHAR_DATA_CHAR_INDEX], ' ');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should properly scroll inside a scroll region (scrollTop set)', () => {
|
it('should properly scroll inside a scroll region (scrollTop set)', () => {
|
||||||
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
|
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
|
||||||
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
|
||||||
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
|
term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX] = 'c';
|
||||||
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||||
term.buffer.scrollTop = 1;
|
term.buffer.scrollTop = 1;
|
||||||
term.scroll();
|
term.scroll();
|
||||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||||
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a');
|
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||||
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c');
|
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should properly scroll inside a scroll region (scrollBottom set)', () => {
|
it('should properly scroll inside a scroll region (scrollBottom set)', () => {
|
||||||
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
|
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
|
||||||
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
|
||||||
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
|
term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX] = 'c';
|
||||||
term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd';
|
term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX] = 'd';
|
||||||
term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e';
|
term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX] = 'e';
|
||||||
term.buffer.y = 3;
|
term.buffer.y = 3;
|
||||||
term.buffer.scrollBottom = 3;
|
term.buffer.scrollBottom = 3;
|
||||||
term.scroll();
|
term.scroll();
|
||||||
assert.equal(term.buffer.lines.length, INIT_ROWS + 1);
|
assert.equal(term.buffer.lines.length, INIT_ROWS + 1);
|
||||||
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a', '\'a\' should be pushed to the scrollback');
|
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a', '\'a\' should be pushed to the scrollback');
|
||||||
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'b');
|
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
|
||||||
assert.equal(term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX], 'c');
|
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
|
||||||
assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], 'd');
|
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
|
||||||
assert.equal(term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
||||||
assert.equal(term.buffer.lines.get(5)[0][CHAR_DATA_CHAR_INDEX], 'e');
|
assert.equal(term.buffer.lines.get(5).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => {
|
it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => {
|
||||||
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
|
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
|
||||||
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
|
||||||
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
|
term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX] = 'c';
|
||||||
term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd';
|
term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX] = 'd';
|
||||||
term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e';
|
term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX] = 'e';
|
||||||
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||||
term.buffer.scrollTop = 1;
|
term.buffer.scrollTop = 1;
|
||||||
term.buffer.scrollBottom = 3;
|
term.buffer.scrollBottom = 3;
|
||||||
term.scroll();
|
term.scroll();
|
||||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||||
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a');
|
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||||
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer');
|
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer');
|
||||||
assert.equal(term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX], 'd');
|
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
|
||||||
assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
||||||
assert.equal(term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX], 'e');
|
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -403,65 +522,65 @@ describe('term.js addons', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should create a new line and shift everything up', () => {
|
it('should create a new line and shift everything up', () => {
|
||||||
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
|
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
|
||||||
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
|
||||||
term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX] = 'c';
|
term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX] = 'c';
|
||||||
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||||
term.scroll();
|
term.scroll();
|
||||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||||
// 'a' gets pushed out of buffer
|
// 'a' gets pushed out of buffer
|
||||||
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'b');
|
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
|
||||||
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], ' ');
|
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], ' ');
|
||||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 2)[0][CHAR_DATA_CHAR_INDEX], 'c');
|
assert.equal(term.buffer.lines.get(INIT_ROWS - 2).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
|
||||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX], ' ');
|
assert.equal(term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX], ' ');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should properly scroll inside a scroll region (scrollTop set)', () => {
|
it('should properly scroll inside a scroll region (scrollTop set)', () => {
|
||||||
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
|
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
|
||||||
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
|
||||||
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
|
term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX] = 'c';
|
||||||
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||||
term.buffer.scrollTop = 1;
|
term.buffer.scrollTop = 1;
|
||||||
term.scroll();
|
term.scroll();
|
||||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||||
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a');
|
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||||
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c');
|
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should properly scroll inside a scroll region (scrollBottom set)', () => {
|
it('should properly scroll inside a scroll region (scrollBottom set)', () => {
|
||||||
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
|
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
|
||||||
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
|
||||||
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
|
term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX] = 'c';
|
||||||
term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd';
|
term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX] = 'd';
|
||||||
term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e';
|
term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX] = 'e';
|
||||||
term.buffer.y = 3;
|
term.buffer.y = 3;
|
||||||
term.buffer.scrollBottom = 3;
|
term.buffer.scrollBottom = 3;
|
||||||
term.scroll();
|
term.scroll();
|
||||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||||
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'b');
|
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
|
||||||
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c');
|
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
|
||||||
assert.equal(term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX], 'd');
|
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
|
||||||
assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
||||||
assert.equal(term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX], 'e');
|
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => {
|
it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => {
|
||||||
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
|
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
|
||||||
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
|
||||||
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
|
term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX] = 'c';
|
||||||
term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd';
|
term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX] = 'd';
|
||||||
term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e';
|
term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX] = 'e';
|
||||||
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||||
term.buffer.scrollTop = 1;
|
term.buffer.scrollTop = 1;
|
||||||
term.buffer.scrollBottom = 3;
|
term.buffer.scrollBottom = 3;
|
||||||
term.scroll();
|
term.scroll();
|
||||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||||
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a');
|
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||||
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer');
|
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer');
|
||||||
assert.equal(term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX], 'd');
|
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
|
||||||
assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
||||||
assert.equal(term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX], 'e');
|
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -472,9 +591,9 @@ describe('term.js addons', () => {
|
|||||||
let evKeyPress: any;
|
let evKeyPress: any;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
term.handler = () => {};
|
term.handler = () => { };
|
||||||
term.showCursor = () => {};
|
term.showCursor = () => { };
|
||||||
term.clearSelection = () => {};
|
term.clearSelection = () => { };
|
||||||
// term.compositionHelper = {
|
// term.compositionHelper = {
|
||||||
// isComposing: false,
|
// isComposing: false,
|
||||||
// keydown: {
|
// keydown: {
|
||||||
@@ -484,15 +603,15 @@ describe('term.js addons', () => {
|
|||||||
// }
|
// }
|
||||||
// };
|
// };
|
||||||
evKeyDown = {
|
evKeyDown = {
|
||||||
preventDefault: () => {},
|
preventDefault: () => { },
|
||||||
stopPropagation: () => {},
|
stopPropagation: () => { },
|
||||||
type: 'keydown',
|
type: 'keydown',
|
||||||
altKey: null,
|
altKey: null,
|
||||||
keyCode: null
|
keyCode: null
|
||||||
};
|
};
|
||||||
evKeyPress = {
|
evKeyPress = {
|
||||||
preventDefault: () => {},
|
preventDefault: () => { },
|
||||||
stopPropagation: () => {},
|
stopPropagation: () => { },
|
||||||
type: 'keypress',
|
type: 'keypress',
|
||||||
altKey: null,
|
altKey: null,
|
||||||
charCode: null,
|
charCode: null,
|
||||||
@@ -654,11 +773,11 @@ describe('term.js addons', () => {
|
|||||||
const high = String.fromCharCode(0xD800);
|
const high = String.fromCharCode(0xD800);
|
||||||
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
||||||
term.write(high + String.fromCharCode(i));
|
term.write(high + String.fromCharCode(i));
|
||||||
const tchar = term.buffer.lines.get(0)[0];
|
const tchar = term.buffer.lines.get(0).get(0);
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||||
expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' ');
|
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||||
term.reset();
|
term.reset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -667,9 +786,9 @@ describe('term.js addons', () => {
|
|||||||
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
||||||
term.buffer.x = term.cols - 1;
|
term.buffer.x = term.cols - 1;
|
||||||
term.write(high + String.fromCharCode(i));
|
term.write(high + String.fromCharCode(i));
|
||||||
expect(term.buffer.lines.get(0)[term.buffer.x - 1][CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
expect(term.buffer.lines.get(0).get(term.buffer.x - 1)[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
||||||
expect(term.buffer.lines.get(0)[term.buffer.x - 1][CHAR_DATA_CHAR_INDEX].length).eql(2);
|
expect(term.buffer.lines.get(0).get(term.buffer.x - 1)[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||||
expect(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX]).eql(' ');
|
expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||||
term.reset();
|
term.reset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -679,10 +798,10 @@ describe('term.js addons', () => {
|
|||||||
term.buffer.x = term.cols - 1;
|
term.buffer.x = term.cols - 1;
|
||||||
term.wraparoundMode = true;
|
term.wraparoundMode = true;
|
||||||
term.write('a' + high + String.fromCharCode(i));
|
term.write('a' + high + String.fromCharCode(i));
|
||||||
expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX]).eql('a');
|
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('a');
|
||||||
expect(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
||||||
expect(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX].length).eql(2);
|
expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||||
expect(term.buffer.lines.get(1)[1][CHAR_DATA_CHAR_INDEX]).eql(' ');
|
expect(term.buffer.lines.get(1).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||||
term.reset();
|
term.reset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -693,9 +812,9 @@ describe('term.js addons', () => {
|
|||||||
term.wraparoundMode = false;
|
term.wraparoundMode = false;
|
||||||
term.write('a' + high + String.fromCharCode(i));
|
term.write('a' + high + String.fromCharCode(i));
|
||||||
// auto wraparound mode should cut off the rest of the line
|
// auto wraparound mode should cut off the rest of the line
|
||||||
expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX]).eql('a');
|
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('a');
|
||||||
expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX].length).eql(1);
|
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||||
expect(term.buffer.lines.get(1)[1][CHAR_DATA_CHAR_INDEX]).eql(' ');
|
expect(term.buffer.lines.get(1).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||||
term.reset();
|
term.reset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -704,11 +823,11 @@ describe('term.js addons', () => {
|
|||||||
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
||||||
term.write(high);
|
term.write(high);
|
||||||
term.write(String.fromCharCode(i));
|
term.write(String.fromCharCode(i));
|
||||||
const tchar = term.buffer.lines.get(0)[0];
|
const tchar = term.buffer.lines.get(0).get(0);
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||||
expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' ');
|
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||||
term.reset();
|
term.reset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -717,30 +836,30 @@ describe('term.js addons', () => {
|
|||||||
describe('unicode - combining characters', () => {
|
describe('unicode - combining characters', () => {
|
||||||
it('café', () => {
|
it('café', () => {
|
||||||
term.write('cafe\u0301');
|
term.write('cafe\u0301');
|
||||||
expect(term.buffer.lines.get(0)[3][CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
|
expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
|
||||||
expect(term.buffer.lines.get(0)[3][CHAR_DATA_CHAR_INDEX].length).eql(2);
|
expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||||
expect(term.buffer.lines.get(0)[3][CHAR_DATA_WIDTH_INDEX]).eql(1);
|
expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||||
});
|
});
|
||||||
it('café - end of line', () => {
|
it('café - end of line', () => {
|
||||||
term.buffer.x = term.cols - 1 - 3;
|
term.buffer.x = term.cols - 1 - 3;
|
||||||
term.write('cafe\u0301');
|
term.write('cafe\u0301');
|
||||||
expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
|
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
|
||||||
expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX].length).eql(2);
|
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||||
expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_WIDTH_INDEX]).eql(1);
|
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||||
expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' ');
|
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||||
expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX].length).eql(1);
|
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||||
expect(term.buffer.lines.get(0)[1][CHAR_DATA_WIDTH_INDEX]).eql(1);
|
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||||
});
|
});
|
||||||
it('multiple combined é', () => {
|
it('multiple combined é', () => {
|
||||||
term.wraparoundMode = true;
|
term.wraparoundMode = true;
|
||||||
term.write(Array(100).join('e\u0301'));
|
term.write(Array(100).join('e\u0301'));
|
||||||
for (let i = 0; i < term.cols; ++i) {
|
for (let i = 0; i < term.cols; ++i) {
|
||||||
const tchar = term.buffer.lines.get(0)[i];
|
const tchar = term.buffer.lines.get(0).get(i);
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
|
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||||
}
|
}
|
||||||
const tchar = term.buffer.lines.get(1)[0];
|
const tchar = term.buffer.lines.get(1).get(0);
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
|
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||||
@@ -749,12 +868,12 @@ describe('term.js addons', () => {
|
|||||||
term.wraparoundMode = true;
|
term.wraparoundMode = true;
|
||||||
term.write(Array(100).join('\uD800\uDC00\u0301'));
|
term.write(Array(100).join('\uD800\uDC00\u0301'));
|
||||||
for (let i = 0; i < term.cols; ++i) {
|
for (let i = 0; i < term.cols; ++i) {
|
||||||
const tchar = term.buffer.lines.get(0)[i];
|
const tchar = term.buffer.lines.get(0).get(i);
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\uD800\uDC00\u0301');
|
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\uD800\uDC00\u0301');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
|
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||||
}
|
}
|
||||||
const tchar = term.buffer.lines.get(1)[0];
|
const tchar = term.buffer.lines.get(1).get(0);
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\uD800\uDC00\u0301');
|
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\uD800\uDC00\u0301');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
|
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||||
@@ -777,7 +896,7 @@ describe('term.js addons', () => {
|
|||||||
term.wraparoundMode = true;
|
term.wraparoundMode = true;
|
||||||
term.write(Array(50).join('¥'));
|
term.write(Array(50).join('¥'));
|
||||||
for (let i = 0; i < term.cols; ++i) {
|
for (let i = 0; i < term.cols; ++i) {
|
||||||
const tchar = term.buffer.lines.get(0)[i];
|
const tchar = term.buffer.lines.get(0).get(i);
|
||||||
if (i % 2) {
|
if (i % 2) {
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||||
@@ -788,7 +907,7 @@ describe('term.js addons', () => {
|
|||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const tchar = term.buffer.lines.get(1)[0];
|
const tchar = term.buffer.lines.get(1).get(0);
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||||
@@ -798,7 +917,7 @@ describe('term.js addons', () => {
|
|||||||
term.buffer.x = 1;
|
term.buffer.x = 1;
|
||||||
term.write(Array(50).join('¥'));
|
term.write(Array(50).join('¥'));
|
||||||
for (let i = 1; i < term.cols - 1; ++i) {
|
for (let i = 1; i < term.cols - 1; ++i) {
|
||||||
const tchar = term.buffer.lines.get(0)[i];
|
const tchar = term.buffer.lines.get(0).get(i);
|
||||||
if (!(i % 2)) {
|
if (!(i % 2)) {
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||||
@@ -809,11 +928,11 @@ describe('term.js addons', () => {
|
|||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let tchar = term.buffer.lines.get(0)[term.cols - 1];
|
let tchar = term.buffer.lines.get(0).get(term.cols - 1);
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||||
tchar = term.buffer.lines.get(1)[0];
|
tchar = term.buffer.lines.get(1).get(0);
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||||
@@ -823,7 +942,7 @@ describe('term.js addons', () => {
|
|||||||
term.buffer.x = 1;
|
term.buffer.x = 1;
|
||||||
term.write(Array(50).join('¥\u0301'));
|
term.write(Array(50).join('¥\u0301'));
|
||||||
for (let i = 1; i < term.cols - 1; ++i) {
|
for (let i = 1; i < term.cols - 1; ++i) {
|
||||||
const tchar = term.buffer.lines.get(0)[i];
|
const tchar = term.buffer.lines.get(0).get(i);
|
||||||
if (!(i % 2)) {
|
if (!(i % 2)) {
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||||
@@ -834,11 +953,11 @@ describe('term.js addons', () => {
|
|||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let tchar = term.buffer.lines.get(0)[term.cols - 1];
|
let tchar = term.buffer.lines.get(0).get(term.cols - 1);
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||||
tchar = term.buffer.lines.get(1)[0];
|
tchar = term.buffer.lines.get(1).get(0);
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301');
|
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||||
@@ -847,7 +966,7 @@ describe('term.js addons', () => {
|
|||||||
term.wraparoundMode = true;
|
term.wraparoundMode = true;
|
||||||
term.write(Array(50).join('¥\u0301'));
|
term.write(Array(50).join('¥\u0301'));
|
||||||
for (let i = 0; i < term.cols; ++i) {
|
for (let i = 0; i < term.cols; ++i) {
|
||||||
const tchar = term.buffer.lines.get(0)[i];
|
const tchar = term.buffer.lines.get(0).get(i);
|
||||||
if (i % 2) {
|
if (i % 2) {
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||||
@@ -858,7 +977,7 @@ describe('term.js addons', () => {
|
|||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const tchar = term.buffer.lines.get(1)[0];
|
const tchar = term.buffer.lines.get(1).get(0);
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301');
|
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||||
@@ -868,7 +987,7 @@ describe('term.js addons', () => {
|
|||||||
term.buffer.x = 1;
|
term.buffer.x = 1;
|
||||||
term.write(Array(50).join('\ud843\ude6d\u0301'));
|
term.write(Array(50).join('\ud843\ude6d\u0301'));
|
||||||
for (let i = 1; i < term.cols - 1; ++i) {
|
for (let i = 1; i < term.cols - 1; ++i) {
|
||||||
const tchar = term.buffer.lines.get(0)[i];
|
const tchar = term.buffer.lines.get(0).get(i);
|
||||||
if (!(i % 2)) {
|
if (!(i % 2)) {
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||||
@@ -879,11 +998,11 @@ describe('term.js addons', () => {
|
|||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let tchar = term.buffer.lines.get(0)[term.cols - 1];
|
let tchar = term.buffer.lines.get(0).get(term.cols - 1);
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||||
tchar = term.buffer.lines.get(1)[0];
|
tchar = term.buffer.lines.get(1).get(0);
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301');
|
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
|
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||||
@@ -892,7 +1011,7 @@ describe('term.js addons', () => {
|
|||||||
term.wraparoundMode = true;
|
term.wraparoundMode = true;
|
||||||
term.write(Array(50).join('\ud843\ude6d\u0301'));
|
term.write(Array(50).join('\ud843\ude6d\u0301'));
|
||||||
for (let i = 0; i < term.cols; ++i) {
|
for (let i = 0; i < term.cols; ++i) {
|
||||||
const tchar = term.buffer.lines.get(0)[i];
|
const tchar = term.buffer.lines.get(0).get(i);
|
||||||
if (i % 2) {
|
if (i % 2) {
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||||
@@ -903,7 +1022,7 @@ describe('term.js addons', () => {
|
|||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const tchar = term.buffer.lines.get(1)[0];
|
const tchar = term.buffer.lines.get(1).get(0);
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301');
|
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
|
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||||
@@ -918,10 +1037,10 @@ describe('term.js addons', () => {
|
|||||||
term.insertMode = true;
|
term.insertMode = true;
|
||||||
term.write('abcde');
|
term.write('abcde');
|
||||||
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
||||||
expect(term.buffer.lines.get(0)[10][CHAR_DATA_CHAR_INDEX]).eql('a');
|
expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('a');
|
||||||
expect(term.buffer.lines.get(0)[14][CHAR_DATA_CHAR_INDEX]).eql('e');
|
expect(term.buffer.lines.get(0).get(14)[CHAR_DATA_CHAR_INDEX]).eql('e');
|
||||||
expect(term.buffer.lines.get(0)[15][CHAR_DATA_CHAR_INDEX]).eql('0');
|
expect(term.buffer.lines.get(0).get(15)[CHAR_DATA_CHAR_INDEX]).eql('0');
|
||||||
expect(term.buffer.lines.get(0)[79][CHAR_DATA_CHAR_INDEX]).eql('4');
|
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql('4');
|
||||||
});
|
});
|
||||||
it('fullwidth - insert', () => {
|
it('fullwidth - insert', () => {
|
||||||
term.write(Array(9).join('0123456789').slice(-80));
|
term.write(Array(9).join('0123456789').slice(-80));
|
||||||
@@ -930,11 +1049,11 @@ describe('term.js addons', () => {
|
|||||||
term.insertMode = true;
|
term.insertMode = true;
|
||||||
term.write('¥¥¥');
|
term.write('¥¥¥');
|
||||||
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
||||||
expect(term.buffer.lines.get(0)[10][CHAR_DATA_CHAR_INDEX]).eql('¥');
|
expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||||
expect(term.buffer.lines.get(0)[11][CHAR_DATA_CHAR_INDEX]).eql('');
|
expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||||
expect(term.buffer.lines.get(0)[14][CHAR_DATA_CHAR_INDEX]).eql('¥');
|
expect(term.buffer.lines.get(0).get(14)[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||||
expect(term.buffer.lines.get(0)[15][CHAR_DATA_CHAR_INDEX]).eql('');
|
expect(term.buffer.lines.get(0).get(15)[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||||
expect(term.buffer.lines.get(0)[79][CHAR_DATA_CHAR_INDEX]).eql('3');
|
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql('3');
|
||||||
});
|
});
|
||||||
it('fullwidth - right border', () => {
|
it('fullwidth - right border', () => {
|
||||||
term.write(Array(41).join('¥'));
|
term.write(Array(41).join('¥'));
|
||||||
@@ -943,14 +1062,14 @@ describe('term.js addons', () => {
|
|||||||
term.insertMode = true;
|
term.insertMode = true;
|
||||||
term.write('a');
|
term.write('a');
|
||||||
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
||||||
expect(term.buffer.lines.get(0)[10][CHAR_DATA_CHAR_INDEX]).eql('a');
|
expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('a');
|
||||||
expect(term.buffer.lines.get(0)[11][CHAR_DATA_CHAR_INDEX]).eql('¥');
|
expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||||
expect(term.buffer.lines.get(0)[79][CHAR_DATA_CHAR_INDEX]).eql(' '); // fullwidth char got replaced
|
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql(' '); // fullwidth char got replaced
|
||||||
term.write('b');
|
term.write('b');
|
||||||
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
||||||
expect(term.buffer.lines.get(0)[11][CHAR_DATA_CHAR_INDEX]).eql('b');
|
expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('b');
|
||||||
expect(term.buffer.lines.get(0)[12][CHAR_DATA_CHAR_INDEX]).eql('¥');
|
expect(term.buffer.lines.get(0).get(12)[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||||
expect(term.buffer.lines.get(0)[79][CHAR_DATA_CHAR_INDEX]).eql(''); // empty cell after fullwidth
|
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql(''); // empty cell after fullwidth
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+14
-83
@@ -21,13 +21,13 @@
|
|||||||
* http://linux.die.net/man/7/urxvt
|
* http://linux.die.net/man/7/urxvt
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, LineData, CharacterJoinerHandler } from './Types';
|
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler } from './Types';
|
||||||
import { IMouseZoneManager } from './ui/Types';
|
import { IMouseZoneManager } from './ui/Types';
|
||||||
import { IRenderer } from './renderer/Types';
|
import { IRenderer } from './renderer/Types';
|
||||||
import { BufferSet } from './BufferSet';
|
import { BufferSet } from './BufferSet';
|
||||||
import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer';
|
import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer';
|
||||||
import { CompositionHelper } from './CompositionHelper';
|
import { CompositionHelper } from './CompositionHelper';
|
||||||
import { EventEmitter } from './EventEmitter';
|
import { EventEmitter } from './common/EventEmitter';
|
||||||
import { Viewport } from './Viewport';
|
import { Viewport } from './Viewport';
|
||||||
import { rightClickHandler, moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from './handlers/Clipboard';
|
import { rightClickHandler, moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from './handlers/Clipboard';
|
||||||
import { C0 } from './common/data/EscapeSequences';
|
import { C0 } from './common/data/EscapeSequences';
|
||||||
@@ -52,6 +52,7 @@ import { DomRenderer } from './renderer/dom/DomRenderer';
|
|||||||
import { IKeyboardEvent } from './common/Types';
|
import { IKeyboardEvent } from './common/Types';
|
||||||
import { evaluateKeyboardEvent } from './core/input/Keyboard';
|
import { evaluateKeyboardEvent } from './core/input/Keyboard';
|
||||||
import { KeyboardResultType, ICharset } from './core/Types';
|
import { KeyboardResultType, ICharset } from './core/Types';
|
||||||
|
import { BufferLine } from './BufferLine';
|
||||||
|
|
||||||
// Let it work inside Node.js for automated testing purposes.
|
// Let it work inside Node.js for automated testing purposes.
|
||||||
const document = (typeof window !== 'undefined') ? window.document : null;
|
const document = (typeof window !== 'undefined') ? window.document : null;
|
||||||
@@ -133,7 +134,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
// TODO: This can be changed to an enum or boolean, 0 and 1 seem to be the only options
|
// TODO: This can be changed to an enum or boolean, 0 and 1 seem to be the only options
|
||||||
public cursorState: number;
|
public cursorState: number;
|
||||||
public cursorHidden: boolean;
|
public cursorHidden: boolean;
|
||||||
public convertEol: boolean;
|
|
||||||
|
|
||||||
private _customKeyEventHandler: CustomKeyEventHandler;
|
private _customKeyEventHandler: CustomKeyEventHandler;
|
||||||
|
|
||||||
@@ -251,7 +251,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
|
|
||||||
private _setup(): void {
|
private _setup(): void {
|
||||||
Object.keys(DEFAULT_OPTIONS).forEach((key) => {
|
Object.keys(DEFAULT_OPTIONS).forEach((key) => {
|
||||||
if (this.options[key] == null) {
|
if (this.options[key] === null || this.options[key] === undefined) {
|
||||||
this.options[key] = DEFAULT_OPTIONS[key];
|
this.options[key] = DEFAULT_OPTIONS[key];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -503,10 +503,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
/**
|
/**
|
||||||
* Binds the desired focus behavior on a given terminal object.
|
* Binds the desired focus behavior on a given terminal object.
|
||||||
*/
|
*/
|
||||||
private _onTextAreaFocus(): void {
|
private _onTextAreaFocus(ev: KeyboardEvent): void {
|
||||||
if (this.sendFocus) {
|
if (this.sendFocus) {
|
||||||
this.handler(C0.ESC + '[I');
|
this.handler(C0.ESC + '[I');
|
||||||
}
|
}
|
||||||
|
this.updateCursorStyle(ev);
|
||||||
this.element.classList.add('focus');
|
this.element.classList.add('focus');
|
||||||
this.showCursor();
|
this.showCursor();
|
||||||
this.emit('focus');
|
this.emit('focus');
|
||||||
@@ -679,7 +680,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
this.textarea.setAttribute('autocapitalize', 'off');
|
this.textarea.setAttribute('autocapitalize', 'off');
|
||||||
this.textarea.setAttribute('spellcheck', 'false');
|
this.textarea.setAttribute('spellcheck', 'false');
|
||||||
this.textarea.tabIndex = 0;
|
this.textarea.tabIndex = 0;
|
||||||
this.register(addDisposableDomListener(this.textarea, 'focus', () => this._onTextAreaFocus()));
|
this.register(addDisposableDomListener(this.textarea, 'focus', (ev: KeyboardEvent) => this._onTextAreaFocus(ev)));
|
||||||
this.register(addDisposableDomListener(this.textarea, 'blur', () => this._onTextAreaBlur()));
|
this.register(addDisposableDomListener(this.textarea, 'blur', () => this._onTextAreaBlur()));
|
||||||
this._helperContainer.appendChild(this.textarea);
|
this._helperContainer.appendChild(this.textarea);
|
||||||
|
|
||||||
@@ -960,9 +961,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
// 1, and 2 - with 64 added
|
// 1, and 2 - with 64 added
|
||||||
switch ((<any>ev).overrideType || ev.type) {
|
switch ((<any>ev).overrideType || ev.type) {
|
||||||
case 'mousedown':
|
case 'mousedown':
|
||||||
button = ev.button != null
|
button = ev.button !== null && ev.button !== undefined
|
||||||
? +ev.button
|
? +ev.button
|
||||||
: ev.which != null
|
: ev.which !== null && ev.which !== undefined
|
||||||
? ev.which - 1
|
? ev.which - 1
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -1169,7 +1170,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
* @param isWrapped Whether the new line is wrapped from the previous line.
|
* @param isWrapped Whether the new line is wrapped from the previous line.
|
||||||
*/
|
*/
|
||||||
public scroll(isWrapped?: boolean): void {
|
public scroll(isWrapped?: boolean): void {
|
||||||
const newLine = this.blankLine(undefined, isWrapped);
|
const newLine = BufferLine.blankLine(this.cols, DEFAULT_ATTR, isWrapped);
|
||||||
const topRow = this.buffer.ybase + this.buffer.scrollTop;
|
const topRow = this.buffer.ybase + this.buffer.scrollTop;
|
||||||
const bottomRow = this.buffer.ybase + this.buffer.scrollBottom;
|
const bottomRow = this.buffer.ybase + this.buffer.scrollBottom;
|
||||||
|
|
||||||
@@ -1587,7 +1588,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
|
|
||||||
if (ev.charCode) {
|
if (ev.charCode) {
|
||||||
key = ev.charCode;
|
key = ev.charCode;
|
||||||
} else if (ev.which == null) {
|
} else if (ev.which === null || ev.which === undefined) {
|
||||||
key = ev.keyCode;
|
key = ev.keyCode;
|
||||||
} else if (ev.which !== 0 && ev.charCode !== 0) {
|
} else if (ev.which !== 0 && ev.charCode !== 0) {
|
||||||
key = ev.which;
|
key = ev.which;
|
||||||
@@ -1707,41 +1708,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
this._refreshEnd = this.rows - 1;
|
this._refreshEnd = this.rows - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Erase in the identified line everything from "x" to the end of the line (right).
|
|
||||||
* @param x The column from which to start erasing to the end of the line.
|
|
||||||
* @param y The line in which to operate.
|
|
||||||
*/
|
|
||||||
public eraseRight(x: number, y: number): void {
|
|
||||||
const line = this.buffer.lines.get(this.buffer.ybase + y);
|
|
||||||
if (!line) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const ch: CharData = [this.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm
|
|
||||||
for (; x < this.cols; x++) {
|
|
||||||
line[x] = ch;
|
|
||||||
}
|
|
||||||
this.updateRange(y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Erase in the identified line everything from "x" to the start of the line (left).
|
|
||||||
* @param x The column from which to start erasing to the start of the line.
|
|
||||||
* @param y The line in which to operate.
|
|
||||||
*/
|
|
||||||
public eraseLeft(x: number, y: number): void {
|
|
||||||
const line = this.buffer.lines.get(this.buffer.ybase + y);
|
|
||||||
if (!line) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const ch: CharData = [this.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm
|
|
||||||
x++;
|
|
||||||
while (x--) {
|
|
||||||
line[x] = ch;
|
|
||||||
}
|
|
||||||
this.updateRange(y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear the entire buffer, making the prompt line the new first line.
|
* Clear the entire buffer, making the prompt line the new first line.
|
||||||
*/
|
*/
|
||||||
@@ -1756,47 +1722,12 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
this.buffer.ybase = 0;
|
this.buffer.ybase = 0;
|
||||||
this.buffer.y = 0;
|
this.buffer.y = 0;
|
||||||
for (let i = 1; i < this.rows; i++) {
|
for (let i = 1; i < this.rows; i++) {
|
||||||
this.buffer.lines.push(this.blankLine());
|
this.buffer.lines.push(BufferLine.blankLine(this.cols, DEFAULT_ATTR));
|
||||||
}
|
}
|
||||||
this.refresh(0, this.rows - 1);
|
this.refresh(0, this.rows - 1);
|
||||||
this.emit('scroll', this.buffer.ydisp);
|
this.emit('scroll', this.buffer.ydisp);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Erase all content in the given line
|
|
||||||
* @param y The line to erase all of its contents.
|
|
||||||
*/
|
|
||||||
public eraseLine(y: number): void {
|
|
||||||
this.eraseRight(0, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the data array of a blank line
|
|
||||||
* @param cur First bunch of data for each "blank" character.
|
|
||||||
* @param isWrapped Whether the new line is wrapped from the previous line.
|
|
||||||
* @param cols The number of columns in the terminal, if this is not
|
|
||||||
* set, the terminal's current column count would be used.
|
|
||||||
*/
|
|
||||||
public blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData {
|
|
||||||
const attr = cur ? this.eraseAttr() : DEFAULT_ATTR;
|
|
||||||
|
|
||||||
const ch: CharData = [attr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // width defaults to 1 halfwidth character
|
|
||||||
const line: LineData = [];
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
(<any>line).isWrapped = isWrapped;
|
|
||||||
}
|
|
||||||
|
|
||||||
cols = cols || this.cols;
|
|
||||||
for (let i = 0; i < cols; i++) {
|
|
||||||
line[i] = ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If cur return the back color xterm feature attribute. Else return default attribute.
|
* If cur return the back color xterm feature attribute. Else return default attribute.
|
||||||
* @param cur
|
* @param cur
|
||||||
@@ -1883,7 +1814,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
// blankLine(true) is xterm/linux behavior
|
// blankLine(true) is xterm/linux behavior
|
||||||
const scrollRegionHeight = this.buffer.scrollBottom - this.buffer.scrollTop;
|
const scrollRegionHeight = this.buffer.scrollBottom - this.buffer.scrollTop;
|
||||||
this.buffer.lines.shiftElements(this.buffer.y + this.buffer.ybase, scrollRegionHeight, 1);
|
this.buffer.lines.shiftElements(this.buffer.y + this.buffer.ybase, scrollRegionHeight, 1);
|
||||||
this.buffer.lines.set(this.buffer.y + this.buffer.ybase, this.blankLine(true));
|
this.buffer.lines.set(this.buffer.y + this.buffer.ybase, BufferLine.blankLine(this.cols, this.eraseAttr()));
|
||||||
this.updateRange(this.buffer.scrollTop);
|
this.updateRange(this.buffer.scrollTop);
|
||||||
this.updateRange(this.buffer.scrollBottom);
|
this.updateRange(this.buffer.scrollBottom);
|
||||||
} else {
|
} else {
|
||||||
@@ -1932,7 +1863,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
public matchColor(r1: number, g1: number, b1: number): number {
|
public matchColor(r1: number, g1: number, b1: number): number {
|
||||||
const hash = (r1 << 16) | (g1 << 8) | b1;
|
const hash = (r1 << 16) | (g1 << 8) | b1;
|
||||||
|
|
||||||
if (matchColorCache[hash] != null) {
|
if (matchColorCache[hash] !== null && matchColorCache[hash] !== undefined) {
|
||||||
return matchColorCache[hash];
|
return matchColorCache[hash];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+34
-22
@@ -7,11 +7,10 @@ import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions,
|
|||||||
import { IColorSet, IRenderer } from './renderer/Types';
|
import { IColorSet, IRenderer } from './renderer/Types';
|
||||||
import { IMouseZoneManager } from './ui/Types';
|
import { IMouseZoneManager } from './ui/Types';
|
||||||
import { ICharset } from './core/Types';
|
import { ICharset } from './core/Types';
|
||||||
|
import { ICircularList } from './common/Types';
|
||||||
|
|
||||||
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
|
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
|
||||||
|
|
||||||
export type XtermListener = (...args: any[]) => void;
|
|
||||||
|
|
||||||
export type CharData = [number, string, number, number];
|
export type CharData = [number, string, number, number];
|
||||||
export type LineData = CharData[];
|
export type LineData = CharData[];
|
||||||
|
|
||||||
@@ -20,6 +19,9 @@ export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: bo
|
|||||||
|
|
||||||
export type CharacterJoinerHandler = (text: string) => [number, number][];
|
export type CharacterJoinerHandler = (text: string) => [number, number][];
|
||||||
|
|
||||||
|
// BufferIndex denotes a position in the buffer: [rowIndex, colIndex]
|
||||||
|
export type BufferIndex = [number, number];
|
||||||
|
|
||||||
export const enum LinkHoverEventTypes {
|
export const enum LinkHoverEventTypes {
|
||||||
HOVER = 'linkhover',
|
HOVER = 'linkhover',
|
||||||
TOOLTIP = 'linktooltip',
|
TOOLTIP = 'linktooltip',
|
||||||
@@ -66,15 +68,10 @@ export interface IInputHandlingTerminal extends IEventEmitter {
|
|||||||
|
|
||||||
bell(): void;
|
bell(): void;
|
||||||
focus(): void;
|
focus(): void;
|
||||||
convertEol: boolean;
|
|
||||||
updateRange(y: number): void;
|
updateRange(y: number): void;
|
||||||
scroll(isWrapped?: boolean): void;
|
scroll(isWrapped?: boolean): void;
|
||||||
setgLevel(g: number): void;
|
setgLevel(g: number): void;
|
||||||
eraseAttr(): number;
|
eraseAttr(): number;
|
||||||
eraseRight(x: number, y: number): void;
|
|
||||||
eraseLine(y: number): void;
|
|
||||||
eraseLeft(x: number, y: number): void;
|
|
||||||
blankLine(cur?: boolean, isWrapped?: boolean): LineData;
|
|
||||||
is(term: string): boolean;
|
is(term: string): boolean;
|
||||||
setgCharset(g: number, charset: ICharset): void;
|
setgCharset(g: number, charset: ICharset): void;
|
||||||
resize(x: number, y: number): void;
|
resize(x: number, y: number): void;
|
||||||
@@ -204,6 +201,7 @@ export interface ILinkHoverEvent {
|
|||||||
x2: number;
|
x2: number;
|
||||||
y2: number;
|
y2: number;
|
||||||
cols: number;
|
cols: number;
|
||||||
|
fg: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
|
export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
|
||||||
@@ -233,7 +231,6 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce
|
|||||||
cancel(ev: Event, force?: boolean): boolean | void;
|
cancel(ev: Event, force?: boolean): boolean | void;
|
||||||
log(text: string): void;
|
log(text: string): void;
|
||||||
showCursor(): void;
|
showCursor(): void;
|
||||||
blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBufferAccessor {
|
export interface IBufferAccessor {
|
||||||
@@ -271,8 +268,18 @@ export interface ITerminalOptions extends IPublicTerminalOptions {
|
|||||||
useFlowControl?: boolean;
|
useFlowControl?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IBufferStringIteratorResult {
|
||||||
|
range: {first: number, last: number};
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IBufferStringIterator {
|
||||||
|
hasNext(): boolean;
|
||||||
|
next(): IBufferStringIteratorResult;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IBuffer {
|
export interface IBuffer {
|
||||||
readonly lines: ICircularList<LineData>;
|
readonly lines: ICircularList<IBufferLine>;
|
||||||
ydisp: number;
|
ydisp: number;
|
||||||
ybase: number;
|
ybase: number;
|
||||||
y: number;
|
y: number;
|
||||||
@@ -288,6 +295,8 @@ export interface IBuffer {
|
|||||||
getWrappedRangeForLine(y: number): { first: number, last: number };
|
getWrappedRangeForLine(y: number): { first: number, last: number };
|
||||||
nextStop(x?: number): number;
|
nextStop(x?: number): number;
|
||||||
prevStop(x?: number): number;
|
prevStop(x?: number): number;
|
||||||
|
stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[];
|
||||||
|
iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBufferSet extends IEventEmitter {
|
export interface IBufferSet extends IEventEmitter {
|
||||||
@@ -299,19 +308,6 @@ export interface IBufferSet extends IEventEmitter {
|
|||||||
activateAltBuffer(): void;
|
activateAltBuffer(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICircularList<T> extends IEventEmitter {
|
|
||||||
length: number;
|
|
||||||
maxLength: number;
|
|
||||||
|
|
||||||
get(index: number): T;
|
|
||||||
set(index: number, value: T): void;
|
|
||||||
push(value: T): void;
|
|
||||||
pop(): T;
|
|
||||||
splice(start: number, deleteCount: number, ...items: T[]): void;
|
|
||||||
trimStart(count: number): void;
|
|
||||||
shiftElements(start: number, count: number, offset: number): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ISelectionManager {
|
export interface ISelectionManager {
|
||||||
selectionText: string;
|
selectionText: string;
|
||||||
selectionStart: [number, number];
|
selectionStart: [number, number];
|
||||||
@@ -511,3 +507,19 @@ export interface IEscapeSequenceParser extends IDisposable {
|
|||||||
setErrorHandler(callback: (state: IParsingState) => IParsingState): void;
|
setErrorHandler(callback: (state: IParsingState) => IParsingState): void;
|
||||||
clearErrorHandler(): void;
|
clearErrorHandler(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for a line in the terminal buffer.
|
||||||
|
*/
|
||||||
|
export interface IBufferLine {
|
||||||
|
length: number;
|
||||||
|
isWrapped: boolean;
|
||||||
|
get(index: number): CharData;
|
||||||
|
set(index: number, value: CharData): void;
|
||||||
|
pop(): CharData | undefined;
|
||||||
|
push(data: CharData): void;
|
||||||
|
splice(start: number, deleteCount: number, ...items: CharData[]): CharData[];
|
||||||
|
insertCells(pos: number, n: number, ch: CharData): void;
|
||||||
|
deleteCells(pos: number, n: number, fill: CharData): void;
|
||||||
|
replaceCells(start: number, end: number, fill: CharData): void;
|
||||||
|
}
|
||||||
|
|||||||
+56
-14
@@ -22,12 +22,16 @@ export class Viewport extends Disposable implements IViewport {
|
|||||||
private _lastRecordedViewportHeight: number = 0;
|
private _lastRecordedViewportHeight: number = 0;
|
||||||
private _lastRecordedBufferHeight: number = 0;
|
private _lastRecordedBufferHeight: number = 0;
|
||||||
private _lastTouchY: number;
|
private _lastTouchY: number;
|
||||||
|
private _lastScrollTop: number = 0;
|
||||||
|
|
||||||
// Stores a partial line amount when scrolling, this is used to keep track of how much of a line
|
// Stores a partial line amount when scrolling, this is used to keep track of how much of a line
|
||||||
// is scrolled so we can "scroll" over partial lines and feel natural on touchpads. This is a
|
// is scrolled so we can "scroll" over partial lines and feel natural on touchpads. This is a
|
||||||
// quick fix and could have a more robust solution in place that reset the value when needed.
|
// quick fix and could have a more robust solution in place that reset the value when needed.
|
||||||
private _wheelPartialScroll: number = 0;
|
private _wheelPartialScroll: number = 0;
|
||||||
|
|
||||||
|
private _refreshAnimationFrame: number | null = null;
|
||||||
|
private _ignoreNextScrollEvent: boolean = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new Viewport.
|
* Creates a new Viewport.
|
||||||
* @param _terminal The terminal this viewport belongs to.
|
* @param _terminal The terminal this viewport belongs to.
|
||||||
@@ -62,6 +66,12 @@ export class Viewport extends Disposable implements IViewport {
|
|||||||
* necessary.
|
* necessary.
|
||||||
*/
|
*/
|
||||||
private _refresh(): void {
|
private _refresh(): void {
|
||||||
|
if (this._refreshAnimationFrame === null) {
|
||||||
|
this._refreshAnimationFrame = requestAnimationFrame(() => this._innerRefresh());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _innerRefresh(): void {
|
||||||
if (this._charMeasure.height > 0) {
|
if (this._charMeasure.height > 0) {
|
||||||
this._currentRowHeight = this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio;
|
this._currentRowHeight = this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio;
|
||||||
this._lastRecordedViewportHeight = this._viewportElement.offsetHeight;
|
this._lastRecordedViewportHeight = this._viewportElement.offsetHeight;
|
||||||
@@ -71,30 +81,53 @@ export class Viewport extends Disposable implements IViewport {
|
|||||||
this._scrollArea.style.height = this._lastRecordedBufferHeight + 'px';
|
this._scrollArea.style.height = this._lastRecordedBufferHeight + 'px';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync scrollTop
|
||||||
|
const scrollTop = this._terminal.buffer.ydisp * this._currentRowHeight;
|
||||||
|
if (this._viewportElement.scrollTop !== scrollTop) {
|
||||||
|
// Ignore the next scroll event which will be triggered by setting the scrollTop as we do not
|
||||||
|
// want this event to scroll the terminal
|
||||||
|
this._ignoreNextScrollEvent = true;
|
||||||
|
this._viewportElement.scrollTop = scrollTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._refreshAnimationFrame = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates dimensions and synchronizes the scroll area if necessary.
|
* Updates dimensions and synchronizes the scroll area if necessary.
|
||||||
*/
|
*/
|
||||||
public syncScrollArea(): void {
|
public syncScrollArea(): void {
|
||||||
|
// If buffer height changed
|
||||||
if (this._lastRecordedBufferLength !== this._terminal.buffer.lines.length) {
|
if (this._lastRecordedBufferLength !== this._terminal.buffer.lines.length) {
|
||||||
// If buffer height changed
|
|
||||||
this._lastRecordedBufferLength = this._terminal.buffer.lines.length;
|
this._lastRecordedBufferLength = this._terminal.buffer.lines.length;
|
||||||
this._refresh();
|
this._refresh();
|
||||||
} else if (this._lastRecordedViewportHeight !== (<any>this._terminal).renderer.dimensions.canvasHeight) {
|
return;
|
||||||
// If viewport height changed
|
|
||||||
this._refresh();
|
|
||||||
} else {
|
|
||||||
// If size has changed, refresh viewport
|
|
||||||
if (this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) {
|
|
||||||
this._refresh();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync scrollTop
|
// If viewport height changed
|
||||||
const scrollTop = this._terminal.buffer.ydisp * this._currentRowHeight;
|
if (this._lastRecordedViewportHeight !== (<any>this._terminal).renderer.dimensions.canvasHeight) {
|
||||||
if (this._viewportElement.scrollTop !== scrollTop) {
|
this._refresh();
|
||||||
this._viewportElement.scrollTop = scrollTop;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the buffer position doesn't match last scroll top
|
||||||
|
const newScrollTop = this._terminal.buffer.ydisp * this._currentRowHeight;
|
||||||
|
if (this._lastScrollTop !== newScrollTop) {
|
||||||
|
this._refresh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If element's scroll top changed, this can happen when hiding the element
|
||||||
|
if (this._lastScrollTop !== this._viewportElement.scrollTop) {
|
||||||
|
this._refresh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If row height changed
|
||||||
|
if (this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) {
|
||||||
|
this._refresh();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,13 +137,22 @@ export class Viewport extends Disposable implements IViewport {
|
|||||||
* @param ev The scroll event.
|
* @param ev The scroll event.
|
||||||
*/
|
*/
|
||||||
private _onScroll(ev: Event): void {
|
private _onScroll(ev: Event): void {
|
||||||
|
// Record current scroll top position
|
||||||
|
this._lastScrollTop = this._viewportElement.scrollTop;
|
||||||
|
|
||||||
// Don't attempt to scroll if the element is not visible, otherwise scrollTop will be corrupt
|
// Don't attempt to scroll if the element is not visible, otherwise scrollTop will be corrupt
|
||||||
// which causes the terminal to scroll the buffer to the top
|
// which causes the terminal to scroll the buffer to the top
|
||||||
if (!this._viewportElement.offsetParent) {
|
if (!this._viewportElement.offsetParent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newRow = Math.round(this._viewportElement.scrollTop / this._currentRowHeight);
|
// Ignore the event if it was flagged to ignore (when the source of the event is from Viewport)
|
||||||
|
if (this._ignoreNextScrollEvent) {
|
||||||
|
this._ignoreNextScrollEvent = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRow = Math.round(this._lastScrollTop / this._currentRowHeight);
|
||||||
const diff = newRow - this._terminal.buffer.ydisp;
|
const diff = newRow - this._terminal.buffer.ydisp;
|
||||||
this._terminal.scrollLines(diff, true);
|
this._terminal.scrollLines(diff, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean
|
|||||||
const fileReader = new FileReader();
|
const fileReader = new FileReader();
|
||||||
|
|
||||||
fileReader.addEventListener('load', () => {
|
fileReader.addEventListener('load', () => {
|
||||||
str = myTextDecoder.decode(this.result);
|
str = myTextDecoder.decode(fileReader.result);
|
||||||
displayData(str);
|
displayData(str);
|
||||||
});
|
});
|
||||||
fileReader.readAsArrayBuffer(ev.data);
|
fileReader.readAsArrayBuffer(ev.data);
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"es6",
|
||||||
|
],
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"outDir": "../../../lib/addons/attach/",
|
"outDir": "../../../lib/addons/attach/",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"es6",
|
||||||
|
],
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"outDir": "../../../lib/addons/fit/",
|
"outDir": "../../../lib/addons/fit/",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"es6",
|
||||||
|
],
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"outDir": "../../../lib/addons/fullscreen/",
|
"outDir": "../../../lib/addons/fullscreen/",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
|
|||||||
@@ -17,6 +17,18 @@ export interface ISearchAddonTerminal extends Terminal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ISearchHelper {
|
export interface ISearchHelper {
|
||||||
findNext(term: string): boolean;
|
findNext(term: string, searchOptions: ISearchOptions): boolean;
|
||||||
findPrevious(term: string): boolean;
|
findPrevious(term: string, searchOptions: ISearchOptions): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISearchOptions {
|
||||||
|
regex?: boolean;
|
||||||
|
wholeWord?: boolean;
|
||||||
|
caseSensitive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISearchResult {
|
||||||
|
term: string;
|
||||||
|
col: number;
|
||||||
|
row: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,8 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ISearchHelper, ISearchAddonTerminal } from './Interfaces';
|
import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult } from './Interfaces';
|
||||||
|
const nonWordCharacters = ' ~!@#$%^&*()_+`-=[]{}|\;:"\',./<>?';
|
||||||
interface ISearchResult {
|
|
||||||
term: string;
|
|
||||||
col: number;
|
|
||||||
row: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class that knows how to search the terminal and how to display the results.
|
* A class that knows how to search the terminal and how to display the results.
|
||||||
@@ -19,16 +14,16 @@ export class SearchHelper implements ISearchHelper {
|
|||||||
// TODO: Search for multiple instances on 1 line
|
// TODO: Search for multiple instances on 1 line
|
||||||
// TODO: Don't use the actual selection, instead use a "find selection" so multiple instances can be highlighted
|
// TODO: Don't use the actual selection, instead use a "find selection" so multiple instances can be highlighted
|
||||||
// TODO: Highlight other instances in the viewport
|
// TODO: Highlight other instances in the viewport
|
||||||
// TODO: Support regex, case sensitivity, etc.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the next instance of the term, then scroll to and select it. If it
|
* Find the next instance of the term, then scroll to and select it. If it
|
||||||
* doesn't exist, do nothing.
|
* doesn't exist, do nothing.
|
||||||
* @param term Tne search term.
|
* @param term Tne search term.
|
||||||
|
* @param searchOptions Search options.
|
||||||
* @return Whether a result was found.
|
* @return Whether a result was found.
|
||||||
*/
|
*/
|
||||||
public findNext(term: string): boolean {
|
public findNext(term: string, searchOptions?: ISearchOptions): boolean {
|
||||||
if (!term || term.length === 0) {
|
if (!term || term.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -43,7 +38,7 @@ export class SearchHelper implements ISearchHelper {
|
|||||||
|
|
||||||
// Search from ydisp + 1 to end
|
// Search from ydisp + 1 to end
|
||||||
for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) {
|
for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) {
|
||||||
result = this._findInLine(term, y);
|
result = this._findInLine(term, y, searchOptions);
|
||||||
if (result) {
|
if (result) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -52,7 +47,7 @@ export class SearchHelper implements ISearchHelper {
|
|||||||
// Search from the top to the current ydisp
|
// Search from the top to the current ydisp
|
||||||
if (!result) {
|
if (!result) {
|
||||||
for (let y = 0; y < startRow; y++) {
|
for (let y = 0; y < startRow; y++) {
|
||||||
result = this._findInLine(term, y);
|
result = this._findInLine(term, y, searchOptions);
|
||||||
if (result) {
|
if (result) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -67,9 +62,10 @@ export class SearchHelper implements ISearchHelper {
|
|||||||
* Find the previous instance of the term, then scroll to and select it. If it
|
* Find the previous instance of the term, then scroll to and select it. If it
|
||||||
* doesn't exist, do nothing.
|
* doesn't exist, do nothing.
|
||||||
* @param term Tne search term.
|
* @param term Tne search term.
|
||||||
|
* @param searchOptions Search options.
|
||||||
* @return Whether a result was found.
|
* @return Whether a result was found.
|
||||||
*/
|
*/
|
||||||
public findPrevious(term: string): boolean {
|
public findPrevious(term: string, searchOptions?: ISearchOptions): boolean {
|
||||||
if (!term || term.length === 0) {
|
if (!term || term.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -84,7 +80,7 @@ export class SearchHelper implements ISearchHelper {
|
|||||||
|
|
||||||
// Search from ydisp + 1 to end
|
// Search from ydisp + 1 to end
|
||||||
for (let y = startRow - 1; y >= 0; y--) {
|
for (let y = startRow - 1; y >= 0; y--) {
|
||||||
result = this._findInLine(term, y);
|
result = this._findInLine(term, y, searchOptions);
|
||||||
if (result) {
|
if (result) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -93,7 +89,7 @@ export class SearchHelper implements ISearchHelper {
|
|||||||
// Search from the top to the current ydisp
|
// Search from the top to the current ydisp
|
||||||
if (!result) {
|
if (!result) {
|
||||||
for (let y = this._terminal._core.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) {
|
for (let y = this._terminal._core.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) {
|
||||||
result = this._findInLine(term, y);
|
result = this._findInLine(term, y, searchOptions);
|
||||||
if (result) {
|
if (result) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -105,19 +101,61 @@ export class SearchHelper implements ISearchHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Searches a line for a search term.
|
* A found substring is a whole word if it doesn't have an alphanumeric character directly adjacent to it.
|
||||||
|
* @param searchIndex starting indext of the potential whole word substring
|
||||||
|
* @param line entire string in which the potential whole word was found
|
||||||
|
* @param term the substring that starts at searchIndex
|
||||||
|
*/
|
||||||
|
private _isWholeWord(searchIndex: number, line: string, term: string): boolean {
|
||||||
|
return (((searchIndex === 0) || (nonWordCharacters.indexOf(line[searchIndex - 1]) !== -1)) &&
|
||||||
|
(((searchIndex + term.length) === line.length) || (nonWordCharacters.indexOf(line[searchIndex + term.length]) !== -1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches a line for a search term. Takes the provided terminal line and searches the text line, which may contain
|
||||||
|
* subsequent terminal lines if the text is wrapped. If the provided line number is part of a wrapped text line that
|
||||||
|
* started on an earlier line then it is skipped since it will be properly searched when the terminal line that the
|
||||||
|
* text starts on is searched.
|
||||||
* @param term Tne search term.
|
* @param term Tne search term.
|
||||||
* @param y The line to search.
|
* @param y The line to search.
|
||||||
|
* @param searchOptions Search options.
|
||||||
* @return The search result if it was found.
|
* @return The search result if it was found.
|
||||||
*/
|
*/
|
||||||
private _findInLine(term: string, y: number): ISearchResult {
|
protected _findInLine(term: string, y: number, searchOptions: ISearchOptions = {}): ISearchResult {
|
||||||
const lowerStringLine = this._terminal._core.buffer.translateBufferLineToString(y, true).toLowerCase();
|
if (this._terminal._core.buffer.lines.get(y).isWrapped) {
|
||||||
const lowerTerm = term.toLowerCase();
|
return;
|
||||||
let searchIndex = lowerStringLine.indexOf(lowerTerm);
|
}
|
||||||
|
|
||||||
|
const stringLine = this.translateBufferLineToStringWithWrap(y, true);
|
||||||
|
const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
|
||||||
|
const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();
|
||||||
|
let searchIndex = -1;
|
||||||
|
|
||||||
|
if (searchOptions.regex) {
|
||||||
|
const searchRegex = RegExp(searchTerm, 'g');
|
||||||
|
const foundTerm = searchRegex.exec(searchStringLine);
|
||||||
|
if (foundTerm && foundTerm[0].length > 0) {
|
||||||
|
searchIndex = searchRegex.lastIndex - foundTerm[0].length;
|
||||||
|
term = foundTerm[0];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
searchIndex = searchStringLine.indexOf(searchTerm);
|
||||||
|
}
|
||||||
|
|
||||||
if (searchIndex >= 0) {
|
if (searchIndex >= 0) {
|
||||||
|
// Adjust the row number and search index if needed since a "line" of text can span multiple rows
|
||||||
|
if (searchIndex >= this._terminal.cols) {
|
||||||
|
y += Math.floor(searchIndex / this._terminal.cols);
|
||||||
|
searchIndex = searchIndex % this._terminal.cols;
|
||||||
|
}
|
||||||
|
if (searchOptions.wholeWord && !this._isWholeWord(searchIndex, searchStringLine, term)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const line = this._terminal._core.buffer.lines.get(y);
|
const line = this._terminal._core.buffer.lines.get(y);
|
||||||
|
|
||||||
for (let i = 0; i < searchIndex; i++) {
|
for (let i = 0; i < searchIndex; i++) {
|
||||||
const charData = line[i];
|
const charData = line.get(i);
|
||||||
// Adjust the searchIndex to normalize emoji into single chars
|
// Adjust the searchIndex to normalize emoji into single chars
|
||||||
const char = charData[1/*CHAR_DATA_CHAR_INDEX*/];
|
const char = charData[1/*CHAR_DATA_CHAR_INDEX*/];
|
||||||
if (char.length > 1) {
|
if (char.length > 1) {
|
||||||
@@ -138,6 +176,28 @@ export class SearchHelper implements ISearchHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translates a buffer line to a string, including subsequent lines if they are wraps.
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
public translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): string {
|
||||||
|
let lineString = '';
|
||||||
|
let lineWrapsToNext: boolean;
|
||||||
|
|
||||||
|
do {
|
||||||
|
const nextLine = this._terminal._core.buffer.lines.get(lineIndex + 1);
|
||||||
|
lineWrapsToNext = nextLine ? nextLine.isWrapped : false;
|
||||||
|
lineString += this._terminal._core.buffer.translateBufferLineToString(lineIndex, !lineWrapsToNext && trimRight);
|
||||||
|
lineIndex++;
|
||||||
|
} while (lineWrapsToNext);
|
||||||
|
|
||||||
|
return lineString;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Selects and scrolls to a result.
|
* Selects and scrolls to a result.
|
||||||
* @param result The result to select.
|
* @param result The result to select.
|
||||||
|
|||||||
@@ -0,0 +1,248 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { assert, expect } from 'chai';
|
||||||
|
import * as search from './search';
|
||||||
|
import { SearchHelper } from './SearchHelper';
|
||||||
|
import { ISearchOptions, ISearchResult } from './Interfaces';
|
||||||
|
|
||||||
|
|
||||||
|
class MockTerminalPlain {}
|
||||||
|
|
||||||
|
class MockTerminal {
|
||||||
|
private _core: any;
|
||||||
|
public searchHelper: TestSearchHelper;
|
||||||
|
public cols: number;
|
||||||
|
constructor(options: any) {
|
||||||
|
this._core = new (require('../../../lib/Terminal').Terminal)(options);
|
||||||
|
this.searchHelper = new TestSearchHelper(this as any);
|
||||||
|
this.cols = options.cols;
|
||||||
|
}
|
||||||
|
get core(): any {
|
||||||
|
return this._core;
|
||||||
|
}
|
||||||
|
pushWriteData(): void {
|
||||||
|
this._core._innerWrite();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestSearchHelper extends SearchHelper {
|
||||||
|
public findInLine(term: string, y: number, searchOptions?: ISearchOptions): ISearchResult {
|
||||||
|
return this._findInLine(term, y, searchOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('search addon', () => {
|
||||||
|
describe('apply', () => {
|
||||||
|
it('should register findNext and findPrevious', () => {
|
||||||
|
search.apply(<any>MockTerminalPlain);
|
||||||
|
assert.equal(typeof (<any>MockTerminalPlain).prototype.findNext, 'function');
|
||||||
|
assert.equal(typeof (<any>MockTerminalPlain).prototype.findPrevious, 'function');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('find', () => {
|
||||||
|
it('Searchhelper - should find correct position', () => {
|
||||||
|
search.apply(<any>MockTerminal);
|
||||||
|
const term = new MockTerminal({cols: 20, rows: 3});
|
||||||
|
term.core.write('Hello World\r\ntest\n123....hello');
|
||||||
|
term.pushWriteData();
|
||||||
|
const hello0 = term.searchHelper.findInLine('Hello', 0);
|
||||||
|
const hello1 = term.searchHelper.findInLine('Hello', 1);
|
||||||
|
const hello2 = term.searchHelper.findInLine('Hello', 2);
|
||||||
|
expect(hello0).eql({col: 0, row: 0, term: 'Hello'});
|
||||||
|
expect(hello1).eql(undefined);
|
||||||
|
expect(hello2).eql({col: 11, row: 2, term: 'Hello'});
|
||||||
|
});
|
||||||
|
it('should find search term accross line wrap', () => {
|
||||||
|
search.apply(<any>MockTerminal);
|
||||||
|
const term = new MockTerminal({cols: 10, rows: 5});
|
||||||
|
term.core.write('texttextHellotext\r\n');
|
||||||
|
term.core.write('texttexttextHellotext goodbye');
|
||||||
|
term.pushWriteData();
|
||||||
|
/*
|
||||||
|
texttextHe
|
||||||
|
llotext
|
||||||
|
texttextte
|
||||||
|
xtHellotex
|
||||||
|
t (these spaces included intentionally)
|
||||||
|
goodbye
|
||||||
|
*/
|
||||||
|
|
||||||
|
const hello0 = term.searchHelper.findInLine('Hello', 0);
|
||||||
|
const hello1 = term.searchHelper.findInLine('Hello', 1);
|
||||||
|
const hello2 = term.searchHelper.findInLine('Hello', 2);
|
||||||
|
const hello3 = term.searchHelper.findInLine('Hello', 3);
|
||||||
|
const llo = term.searchHelper.findInLine('llo', 1);
|
||||||
|
const goodbye = term.searchHelper.findInLine('goodbye', 2);
|
||||||
|
expect(hello0).eql({col: 8, row: 0, term: 'Hello'});
|
||||||
|
expect(hello1).eql(undefined);
|
||||||
|
expect(hello2).eql({col: 2, row: 3, term: 'Hello'});
|
||||||
|
expect(hello3).eql(undefined);
|
||||||
|
expect(llo).eql(undefined);
|
||||||
|
expect(goodbye).eql({col: 0, row: 5, term: 'goodbye'});
|
||||||
|
});
|
||||||
|
it('should respect search regex', () => {
|
||||||
|
search.apply(<any>MockTerminal);
|
||||||
|
const term = new MockTerminal({cols: 10, rows: 4});
|
||||||
|
term.core.write('abcdefghijklmnopqrstuvwxyz\r\n~/dev ');
|
||||||
|
/*
|
||||||
|
abcdefghij
|
||||||
|
klmnopqrst
|
||||||
|
uvwxyz
|
||||||
|
~/dev
|
||||||
|
*/
|
||||||
|
term.pushWriteData();
|
||||||
|
const searchOptions = {
|
||||||
|
regex: true,
|
||||||
|
wholeWord: false,
|
||||||
|
caseSensitive: false
|
||||||
|
};
|
||||||
|
const hello0 = term.searchHelper.findInLine('dee*', 0, searchOptions);
|
||||||
|
const hello1 = term.searchHelper.findInLine('jkk*', 0, searchOptions);
|
||||||
|
const hello2 = term.searchHelper.findInLine('mnn*', 1, searchOptions);
|
||||||
|
const tilda0 = term.searchHelper.findInLine('^~', 3, searchOptions);
|
||||||
|
const tilda1 = term.searchHelper.findInLine('^[~]', 3, searchOptions);
|
||||||
|
const tilda2 = term.searchHelper.findInLine('^\\~', 3, searchOptions);
|
||||||
|
expect(hello0).eql({col: 3, row: 0, term: 'de'});
|
||||||
|
expect(hello1).eql({col: 9, row: 0, term: 'jk'});
|
||||||
|
expect(hello2).eql(undefined);
|
||||||
|
expect(tilda0).eql({col: 0, row: 3, term: '~'});
|
||||||
|
expect(tilda1).eql({col: 0, row: 3, term: '~'});
|
||||||
|
expect(tilda2).eql({col: 0, row: 3, term: '~'});
|
||||||
|
});
|
||||||
|
it('should not select empty lines', () => {
|
||||||
|
search.apply(<any>MockTerminal);
|
||||||
|
const term = new MockTerminal({cols: 20, rows: 3});
|
||||||
|
term.core.write(' ');
|
||||||
|
term.pushWriteData();
|
||||||
|
const line = term.searchHelper.findInLine('^.*$', 0, { regex: true });
|
||||||
|
expect(line).eql(undefined);
|
||||||
|
});
|
||||||
|
it('should respect case sensitive', function(): void {
|
||||||
|
search.apply(<any>MockTerminal);
|
||||||
|
const term = new MockTerminal({cols: 20, rows: 4});
|
||||||
|
term.core.write('Hello World\r\n123....hello\r\nmoreTestHello');
|
||||||
|
term.pushWriteData();
|
||||||
|
const searchOptions = {
|
||||||
|
regex: false,
|
||||||
|
wholeWord: false,
|
||||||
|
caseSensitive: true
|
||||||
|
};
|
||||||
|
const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions);
|
||||||
|
const hello1 = term.searchHelper.findInLine('Hello', 1, searchOptions);
|
||||||
|
const hello2 = term.searchHelper.findInLine('Hello', 2, searchOptions);
|
||||||
|
expect(hello0).eql({col: 0, row: 0, term: 'Hello'});
|
||||||
|
expect(hello1).eql(undefined);
|
||||||
|
expect(hello2).eql({col: 8, row: 2, term: 'Hello'});
|
||||||
|
});
|
||||||
|
it('should respect case sensitive + regex', function(): void {
|
||||||
|
search.apply(<any>MockTerminal);
|
||||||
|
const term = new MockTerminal({cols: 20, rows: 4});
|
||||||
|
term.core.write('hellohello\r\nHelloHello');
|
||||||
|
term.pushWriteData();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* hellohello
|
||||||
|
* HelloHello
|
||||||
|
*/
|
||||||
|
|
||||||
|
const searchOptions = {
|
||||||
|
regex: true,
|
||||||
|
wholeWord: false,
|
||||||
|
caseSensitive: true
|
||||||
|
};
|
||||||
|
const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions);
|
||||||
|
const hello1 = term.searchHelper.findInLine('Hello$', 0, searchOptions);
|
||||||
|
const hello2 = term.searchHelper.findInLine('Hello', 1, searchOptions);
|
||||||
|
const hello3 = term.searchHelper.findInLine('Hello$', 1, searchOptions);
|
||||||
|
expect(hello0).eql(undefined);
|
||||||
|
expect(hello1).eql(undefined);
|
||||||
|
expect(hello2).eql({col: 0, row: 1, term: 'Hello'});
|
||||||
|
expect(hello3).eql({col: 5, row: 1, term: 'Hello'});
|
||||||
|
});
|
||||||
|
it('should respect whole-word search option', function(): void {
|
||||||
|
search.apply(<any>MockTerminal);
|
||||||
|
const term = new MockTerminal({cols: 20, rows: 5});
|
||||||
|
term.core.write('Hello World\r\nWorld Hello\r\nWorldHelloWorld\r\nHelloWorld\r\nWorldHello');
|
||||||
|
term.pushWriteData();
|
||||||
|
const searchOptions = {
|
||||||
|
regex: false,
|
||||||
|
wholeWord: true,
|
||||||
|
caseSensitive: false
|
||||||
|
};
|
||||||
|
const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions);
|
||||||
|
const hello1 = term.searchHelper.findInLine('Hello', 1, searchOptions);
|
||||||
|
const hello2 = term.searchHelper.findInLine('Hello', 2, searchOptions);
|
||||||
|
const hello3 = term.searchHelper.findInLine('Hello', 3, searchOptions);
|
||||||
|
const hello4 = term.searchHelper.findInLine('Hello', 4, searchOptions);
|
||||||
|
expect(hello0).eql({col: 0, row: 0, term: 'Hello'});
|
||||||
|
expect(hello1).eql({col: 6, row: 1, term: 'Hello'});
|
||||||
|
expect(hello2).eql(undefined);
|
||||||
|
expect(hello3).eql(undefined);
|
||||||
|
expect(hello4).eql(undefined);
|
||||||
|
});
|
||||||
|
it('should respect whole-word + case sensitive search options', function(): void {
|
||||||
|
search.apply(<any>MockTerminal);
|
||||||
|
const term = new MockTerminal({cols: 20, rows: 5});
|
||||||
|
term.core.write('Hello World\r\nHelloWorld');
|
||||||
|
term.pushWriteData();
|
||||||
|
const searchOptions = {
|
||||||
|
regex: false,
|
||||||
|
wholeWord: true,
|
||||||
|
caseSensitive: true
|
||||||
|
};
|
||||||
|
const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions);
|
||||||
|
const hello1 = term.searchHelper.findInLine('hello', 0, searchOptions);
|
||||||
|
const hello2 = term.searchHelper.findInLine('Hello', 1, searchOptions);
|
||||||
|
const hello3 = term.searchHelper.findInLine('hello', 1, searchOptions);
|
||||||
|
expect(hello0).eql({col: 0, row: 0, term: 'Hello'});
|
||||||
|
expect(hello1).eql(undefined);
|
||||||
|
expect(hello2).eql(undefined);
|
||||||
|
expect(hello3).eql(undefined);
|
||||||
|
});
|
||||||
|
it('should respect whole-word + regex search options', function(): void {
|
||||||
|
search.apply(<any>MockTerminal);
|
||||||
|
const term = new MockTerminal({cols: 20, rows: 5});
|
||||||
|
term.core.write('Hello World Hello\r\nHelloWorldHello');
|
||||||
|
term.pushWriteData();
|
||||||
|
const searchOptions = {
|
||||||
|
regex: true,
|
||||||
|
wholeWord: true,
|
||||||
|
caseSensitive: false
|
||||||
|
};
|
||||||
|
const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions);
|
||||||
|
const hello1 = term.searchHelper.findInLine('Hello$', 0, searchOptions);
|
||||||
|
const hello2 = term.searchHelper.findInLine('Hello', 1, searchOptions);
|
||||||
|
const hello3 = term.searchHelper.findInLine('Hello$', 1, searchOptions);
|
||||||
|
expect(hello0).eql({col: 0, row: 0, term: 'hello'});
|
||||||
|
expect(hello1).eql({col: 12, row: 0, term: 'hello'});
|
||||||
|
expect(hello2).eql(undefined);
|
||||||
|
expect(hello3).eql(undefined);
|
||||||
|
});
|
||||||
|
it('should respect all search options', function(): void {
|
||||||
|
search.apply(<any>MockTerminal);
|
||||||
|
const term = new MockTerminal({cols: 20, rows: 5});
|
||||||
|
term.core.write('Hello World Hello\r\nHelloWorldHello');
|
||||||
|
term.pushWriteData();
|
||||||
|
const searchOptions = {
|
||||||
|
regex: true,
|
||||||
|
wholeWord: true,
|
||||||
|
caseSensitive: true
|
||||||
|
};
|
||||||
|
const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions);
|
||||||
|
const hello1 = term.searchHelper.findInLine('Hello$', 0, searchOptions);
|
||||||
|
const hello2 = term.searchHelper.findInLine('hello', 0, searchOptions);
|
||||||
|
const hello3 = term.searchHelper.findInLine('hello$', 0, searchOptions);
|
||||||
|
const hello4 = term.searchHelper.findInLine('hello', 1, searchOptions);
|
||||||
|
const hello5 = term.searchHelper.findInLine('hello$', 1, searchOptions);
|
||||||
|
expect(hello0).eql({col: 0, row: 0, term: 'Hello'});
|
||||||
|
expect(hello1).eql({col: 12, row: 0, term: 'Hello'});
|
||||||
|
expect(hello2).eql(undefined);
|
||||||
|
expect(hello3).eql(undefined);
|
||||||
|
expect(hello4).eql(undefined);
|
||||||
|
expect(hello5).eql(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,42 +5,44 @@
|
|||||||
|
|
||||||
import { SearchHelper } from './SearchHelper';
|
import { SearchHelper } from './SearchHelper';
|
||||||
import { Terminal } from 'xterm';
|
import { Terminal } from 'xterm';
|
||||||
import { ISearchAddonTerminal } from './Interfaces';
|
import { ISearchAddonTerminal, ISearchOptions } from './Interfaces';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the next instance of the term, then scroll to and select it. If it
|
* Find the next instance of the term, then scroll to and select it. If it
|
||||||
* doesn't exist, do nothing.
|
* doesn't exist, do nothing.
|
||||||
* @param term Tne search term.
|
* @param term Tne search term.
|
||||||
|
* @param searchOptions Search options
|
||||||
* @return Whether a result was found.
|
* @return Whether a result was found.
|
||||||
*/
|
*/
|
||||||
export function findNext(terminal: Terminal, term: string): boolean {
|
export function findNext(terminal: Terminal, term: string, searchOptions: ISearchOptions = {}): boolean {
|
||||||
const addonTerminal = <ISearchAddonTerminal>terminal;
|
const addonTerminal = <ISearchAddonTerminal>terminal;
|
||||||
if (!addonTerminal.__searchHelper) {
|
if (!addonTerminal.__searchHelper) {
|
||||||
addonTerminal.__searchHelper = new SearchHelper(addonTerminal);
|
addonTerminal.__searchHelper = new SearchHelper(addonTerminal);
|
||||||
}
|
}
|
||||||
return addonTerminal.__searchHelper.findNext(term);
|
return addonTerminal.__searchHelper.findNext(term, searchOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the previous instance of the term, then scroll to and select it. If it
|
* Find the previous instance of the term, then scroll to and select it. If it
|
||||||
* doesn't exist, do nothing.
|
* doesn't exist, do nothing.
|
||||||
* @param term Tne search term.
|
* @param term Tne search term.
|
||||||
|
* @param searchOptions Search options
|
||||||
* @return Whether a result was found.
|
* @return Whether a result was found.
|
||||||
*/
|
*/
|
||||||
export function findPrevious(terminal: Terminal, term: string): boolean {
|
export function findPrevious(terminal: Terminal, term: string, searchOptions: ISearchOptions): boolean {
|
||||||
const addonTerminal = <ISearchAddonTerminal>terminal;
|
const addonTerminal = <ISearchAddonTerminal>terminal;
|
||||||
if (!addonTerminal.__searchHelper) {
|
if (!addonTerminal.__searchHelper) {
|
||||||
addonTerminal.__searchHelper = new SearchHelper(addonTerminal);
|
addonTerminal.__searchHelper = new SearchHelper(addonTerminal);
|
||||||
}
|
}
|
||||||
return addonTerminal.__searchHelper.findPrevious(term);
|
return addonTerminal.__searchHelper.findPrevious(term, searchOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function apply(terminalConstructor: typeof Terminal): void {
|
export function apply(terminalConstructor: typeof Terminal): void {
|
||||||
(<any>terminalConstructor.prototype).findNext = function(term: string): boolean {
|
(<any>terminalConstructor.prototype).findNext = function(term: string, searchOptions: ISearchOptions): boolean {
|
||||||
return findNext(this, term);
|
return findNext(this, term, searchOptions);
|
||||||
};
|
};
|
||||||
|
|
||||||
(<any>terminalConstructor.prototype).findPrevious = function(term: string): boolean {
|
(<any>terminalConstructor.prototype).findPrevious = function(term: string, searchOptions: ISearchOptions): boolean {
|
||||||
return findPrevious(this, term);
|
return findPrevious(this, term, searchOptions);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"es6",
|
||||||
|
],
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"outDir": "../../../lib/addons/search/",
|
"outDir": "../../../lib/addons/search/",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"es6",
|
||||||
|
],
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"outDir": "../../../lib/addons/terminado/",
|
"outDir": "../../../lib/addons/terminado/",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"es6",
|
||||||
|
],
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"outDir": "../../../lib/addons/webLinks/",
|
"outDir": "../../../lib/addons/webLinks/",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"es6",
|
||||||
|
],
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"outDir": "../../../lib/addons/winptyCompat/",
|
"outDir": "../../../lib/addons/winptyCompat/",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
|
|||||||
@@ -30,11 +30,11 @@ export function winptyCompatInit(terminal: Terminal): void {
|
|||||||
// wrapped.
|
// wrapped.
|
||||||
addonTerminal.on('linefeed', () => {
|
addonTerminal.on('linefeed', () => {
|
||||||
const line = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y - 1);
|
const line = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y - 1);
|
||||||
const lastChar = line[addonTerminal.cols - 1];
|
const lastChar = line.get(addonTerminal.cols - 1);
|
||||||
|
|
||||||
if (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE) {
|
if (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE) {
|
||||||
const nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y);
|
const nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y);
|
||||||
(<any>nextLine).isWrapped = true;
|
nextLine.isWrapped = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"es6",
|
||||||
|
],
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"outDir": "../../../lib/addons/zmodem/",
|
"outDir": "../../../lib/addons/zmodem/",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
|
|||||||
@@ -3,15 +3,15 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { EventEmitter } from '../EventEmitter';
|
import { EventEmitter } from './EventEmitter';
|
||||||
import { ICircularList } from '../Types';
|
import { ICircularList } from './Types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a circular list; a list with a maximum size that wraps around when push is called,
|
* Represents a circular list; a list with a maximum size that wraps around when push is called,
|
||||||
* overriding values at the start of the list.
|
* overriding values at the start of the list.
|
||||||
*/
|
*/
|
||||||
export class CircularList<T> extends EventEmitter implements ICircularList<T> {
|
export class CircularList<T> extends EventEmitter implements ICircularList<T> {
|
||||||
protected _array: T[];
|
protected _array: (T | undefined)[];
|
||||||
private _startIndex: number;
|
private _startIndex: number;
|
||||||
private _length: number;
|
private _length: number;
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
|
|||||||
|
|
||||||
// Reconstruct array, starting at index 0. Only transfer values from the
|
// Reconstruct array, starting at index 0. Only transfer values from the
|
||||||
// indexes 0 to length.
|
// indexes 0 to length.
|
||||||
const newArray = new Array<T>(newMaxLength);
|
const newArray = new Array<T | undefined>(newMaxLength);
|
||||||
for (let i = 0; i < Math.min(newMaxLength, this.length); i++) {
|
for (let i = 0; i < Math.min(newMaxLength, this.length); i++) {
|
||||||
newArray[i] = this._array[this._getCyclicIndex(i)];
|
newArray[i] = this._array[this._getCyclicIndex(i)];
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
|
|||||||
* @param index The index of the value to get.
|
* @param index The index of the value to get.
|
||||||
* @return The value corresponding to the index.
|
* @return The value corresponding to the index.
|
||||||
*/
|
*/
|
||||||
public get(index: number): T {
|
public get(index: number): T | undefined {
|
||||||
return this._array[this._getCyclicIndex(index)];
|
return this._array[this._getCyclicIndex(index)];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
|
|||||||
* @param index The index to set.
|
* @param index The index to set.
|
||||||
* @param value The value to set.
|
* @param value The value to set.
|
||||||
*/
|
*/
|
||||||
public set(index: number, value: T): void {
|
public set(index: number, value: T | undefined): void {
|
||||||
this._array[this._getCyclicIndex(index)] = value;
|
this._array[this._getCyclicIndex(index)] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
|
|||||||
* Removes and returns the last value on the list.
|
* Removes and returns the last value on the list.
|
||||||
* @return The popped value.
|
* @return The popped value.
|
||||||
*/
|
*/
|
||||||
public pop(): T {
|
public pop(): T | undefined {
|
||||||
return this._array[this._getCyclicIndex(this._length-- - 1)];
|
return this._array[this._getCyclicIndex(this._length-- - 1)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { XtermListener } from './Types';
|
import { XtermListener } from './Types';
|
||||||
import { IEventEmitter, IDisposable } from 'xterm';
|
import { IEventEmitter, IDisposable } from 'xterm';
|
||||||
import { Disposable } from './common/Lifecycle';
|
import { Disposable } from './Lifecycle';
|
||||||
|
|
||||||
export class EventEmitter extends Disposable implements IEventEmitter, IDisposable {
|
export class EventEmitter extends Disposable implements IEventEmitter, IDisposable {
|
||||||
private _events: {[type: string]: XtermListener[]};
|
private _events: {[type: string]: XtermListener[]};
|
||||||
@@ -30,14 +30,15 @@ export class EventEmitter extends Disposable implements IEventEmitter, IDisposab
|
|||||||
public addDisposableListener(type: string, handler: XtermListener): IDisposable {
|
public addDisposableListener(type: string, handler: XtermListener): IDisposable {
|
||||||
// TODO: Rename addDisposableEventListener to more easily disambiguate from Dom listener
|
// TODO: Rename addDisposableEventListener to more easily disambiguate from Dom listener
|
||||||
this.on(type, handler);
|
this.on(type, handler);
|
||||||
|
let disposed = false;
|
||||||
return {
|
return {
|
||||||
dispose: () => {
|
dispose: () => {
|
||||||
if (!handler) {
|
if (disposed) {
|
||||||
// Already disposed
|
// Already disposed
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.off(type, handler);
|
this.off(type, handler);
|
||||||
handler = null;
|
disposed = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,10 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { IEventEmitter } from 'xterm';
|
||||||
|
|
||||||
|
export type XtermListener = (...args: any[]) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A keyboard event interface which does not depend on the DOM, KeyboardEvent implicitly extends
|
* A keyboard event interface which does not depend on the DOM, KeyboardEvent implicitly extends
|
||||||
* this event.
|
* this event.
|
||||||
@@ -16,3 +20,16 @@ export interface IKeyboardEvent {
|
|||||||
key: string;
|
key: string;
|
||||||
type: string;
|
type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ICircularList<T> extends IEventEmitter {
|
||||||
|
length: number;
|
||||||
|
maxLength: number;
|
||||||
|
|
||||||
|
get(index: number): T | undefined;
|
||||||
|
set(index: number, value: T): void;
|
||||||
|
push(value: T): void;
|
||||||
|
pop(): T | undefined;
|
||||||
|
splice(start: number, deleteCount: number, ...items: T[]): void;
|
||||||
|
trimStart(count: number): void;
|
||||||
|
shiftElements(start: number, count: number, offset: number): void;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ITerminal, ICircularList, LineData } from '../Types';
|
import { ITerminal, IBufferLine } from '../Types';
|
||||||
|
import { ICircularList } from '../common/Types';
|
||||||
import { C0 } from '../common/data/EscapeSequences';
|
import { C0 } from '../common/data/EscapeSequences';
|
||||||
|
|
||||||
const enum Direction {
|
const enum Direction {
|
||||||
@@ -18,7 +19,7 @@ export class AltClickHandler {
|
|||||||
private _startCol: number;
|
private _startCol: number;
|
||||||
private _endRow: number;
|
private _endRow: number;
|
||||||
private _endCol: number;
|
private _endCol: number;
|
||||||
private _lines: ICircularList<LineData>;
|
private _lines: ICircularList<IBufferLine>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private _mouseEvent: MouseEvent,
|
private _mouseEvent: MouseEvent,
|
||||||
@@ -138,7 +139,7 @@ export class AltClickHandler {
|
|||||||
for (let i = 0; i < Math.abs(startRow - endRow); i++) {
|
for (let i = 0; i < Math.abs(startRow - endRow); i++) {
|
||||||
const direction = this._verticalDirection() === Direction.UP ? -1 : 1;
|
const direction = this._verticalDirection() === Direction.UP ? -1 : 1;
|
||||||
|
|
||||||
if ((<any>this._lines.get(startRow + (direction * i))).isWrapped) {
|
if (this._lines.get(startRow + (direction * i)).isWrapped) {
|
||||||
wrappedRows++;
|
wrappedRows++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,12 +153,12 @@ export class AltClickHandler {
|
|||||||
*/
|
*/
|
||||||
private _wrappedRowsForRow(currentRow: number): number {
|
private _wrappedRowsForRow(currentRow: number): number {
|
||||||
let rowCount = 0;
|
let rowCount = 0;
|
||||||
let lineWraps = (<any>this._lines.get(currentRow)).isWrapped;
|
let lineWraps = this._lines.get(currentRow).isWrapped;
|
||||||
|
|
||||||
while (lineWraps && currentRow >= 0 && currentRow < this._terminal.rows) {
|
while (lineWraps && currentRow >= 0 && currentRow < this._terminal.rows) {
|
||||||
rowCount++;
|
rowCount++;
|
||||||
currentRow--;
|
currentRow--;
|
||||||
lineWraps = (<any>this._lines.get(currentRow)).isWrapped;
|
lineWraps = this._lines.get(currentRow).isWrapped;
|
||||||
}
|
}
|
||||||
|
|
||||||
return rowCount;
|
return rowCount;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { IRenderLayer, IColorSet, IRenderDimensions } from './Types';
|
import { IRenderLayer, IColorSet, IRenderDimensions } from './Types';
|
||||||
import { CharData, ITerminal } from '../Types';
|
import { CharData, ITerminal } from '../Types';
|
||||||
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from './atlas/Types';
|
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, IGlyphIdentifier } from './atlas/Types';
|
||||||
import BaseCharAtlas from './atlas/BaseCharAtlas';
|
import BaseCharAtlas from './atlas/BaseCharAtlas';
|
||||||
import { acquireCharAtlas } from './atlas/CharAtlasCache';
|
import { acquireCharAtlas } from './atlas/CharAtlasCache';
|
||||||
import { CHAR_DATA_CHAR_INDEX } from '../Buffer';
|
import { CHAR_DATA_CHAR_INDEX } from '../Buffer';
|
||||||
@@ -22,6 +22,19 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
|
|
||||||
protected _charAtlas: BaseCharAtlas;
|
protected _charAtlas: BaseCharAtlas;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object that's reused when drawing glyphs in order to reduce GC.
|
||||||
|
*/
|
||||||
|
private _currentGlyphIdentifier: IGlyphIdentifier = {
|
||||||
|
chars: '',
|
||||||
|
code: 0,
|
||||||
|
bg: 0,
|
||||||
|
fg: 0,
|
||||||
|
bold: false,
|
||||||
|
dim: false,
|
||||||
|
italic: false
|
||||||
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private _container: HTMLElement,
|
private _container: HTMLElement,
|
||||||
id: string,
|
id: string,
|
||||||
@@ -38,6 +51,9 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
|
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
this._container.removeChild(this._canvas);
|
this._container.removeChild(this._canvas);
|
||||||
|
if (this._charAtlas) {
|
||||||
|
this._charAtlas.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _initCanvas(): void {
|
private _initCanvas(): void {
|
||||||
@@ -245,9 +261,16 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && bold && fg < 8 && fg !== INVERTED_DEFAULT_COLOR;
|
const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && bold && fg < 8 && fg !== INVERTED_DEFAULT_COLOR;
|
||||||
|
|
||||||
fg += drawInBrightColor ? 8 : 0;
|
fg += drawInBrightColor ? 8 : 0;
|
||||||
|
this._currentGlyphIdentifier.chars = chars;
|
||||||
|
this._currentGlyphIdentifier.code = code;
|
||||||
|
this._currentGlyphIdentifier.bg = bg;
|
||||||
|
this._currentGlyphIdentifier.fg = fg;
|
||||||
|
this._currentGlyphIdentifier.bold = bold && terminal.options.enableBold;
|
||||||
|
this._currentGlyphIdentifier.dim = dim;
|
||||||
|
this._currentGlyphIdentifier.italic = italic;
|
||||||
const atlasDidDraw = this._charAtlas && this._charAtlas.draw(
|
const atlasDidDraw = this._charAtlas && this._charAtlas.draw(
|
||||||
this._ctx,
|
this._ctx,
|
||||||
{chars, code, bg, fg, bold: bold && terminal.options.enableBold, dim, italic},
|
this._currentGlyphIdentifier,
|
||||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||||
y * this._scaledCellHeight + this._scaledCharTop
|
y * this._scaledCellHeight + this._scaledCharTop
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
|
|
||||||
import { LineData, CharData } from '../Types';
|
|
||||||
import { MockTerminal, MockBuffer } from '../utils/TestUtils.test';
|
import { MockTerminal, MockBuffer } from '../utils/TestUtils.test';
|
||||||
import { CircularList } from '../common/CircularList';
|
import { CircularList } from '../common/CircularList';
|
||||||
|
|
||||||
import { ICharacterJoinerRegistry } from './Types';
|
import { ICharacterJoinerRegistry } from './Types';
|
||||||
import { CharacterJoinerRegistry } from './CharacterJoinerRegistry';
|
import { CharacterJoinerRegistry } from './CharacterJoinerRegistry';
|
||||||
|
import { BufferLine } from '../BufferLine';
|
||||||
|
import { IBufferLine } from '../Types';
|
||||||
|
|
||||||
describe('CharacterJoinerRegistry', () => {
|
describe('CharacterJoinerRegistry', () => {
|
||||||
let registry: ICharacterJoinerRegistry;
|
let registry: ICharacterJoinerRegistry;
|
||||||
@@ -14,22 +15,25 @@ describe('CharacterJoinerRegistry', () => {
|
|||||||
const terminal = new MockTerminal();
|
const terminal = new MockTerminal();
|
||||||
terminal.cols = 16;
|
terminal.cols = 16;
|
||||||
terminal.buffer = new MockBuffer();
|
terminal.buffer = new MockBuffer();
|
||||||
const lines = new CircularList<LineData>(7);
|
const lines = new CircularList<IBufferLine>(7);
|
||||||
lines.set(0, lineData('a -> b -> c -> d'));
|
lines.set(0, lineData([['a -> b -> c -> d']]));
|
||||||
lines.set(1, lineData('a -> b => c -> d'));
|
lines.set(1, lineData([['a -> b => c -> d']]));
|
||||||
lines.set(2, [...lineData('a -> b -', 0xFFFFFFFF), ...lineData('> c -> d', 0)]);
|
lines.set(2, lineData([['a -> b -', 0xFFFFFFFF], ['> c -> d', 0]]));
|
||||||
lines.set(3, lineData('no joined ranges'));
|
|
||||||
lines.set(4, []);
|
lines.set(3, lineData([['no joined ranges']]));
|
||||||
lines.set(5, [...lineData('a', 0x11111111), ...lineData(' -> b -> c -> '), ...lineData('d', 0x22222222)]);
|
lines.set(4, new BufferLine());
|
||||||
lines.set(6, [
|
lines.set(5, lineData([['a', 0x11111111], [' -> b -> c -> '], ['d', 0x22222222]]));
|
||||||
...lineData('wi'),
|
const line6 = lineData([['wi']]);
|
||||||
[0, '¥', 2, '¥'.charCodeAt(0)],
|
line6.push([0, '¥', 2, '¥'.charCodeAt(0)]);
|
||||||
[0, '', 0, null],
|
line6.push([0, '', 0, null]);
|
||||||
...lineData('deemo'),
|
let sub = lineData([['deemo']]);
|
||||||
[0, '\xf0\x9f\x98\x81', 1, 128513],
|
for (let i = 0; i < sub.length; ++i) line6.push(sub.get(i));
|
||||||
[0, ' ', 1, ' '.charCodeAt(0)],
|
line6.push([0, '\xf0\x9f\x98\x81', 1, 128513]);
|
||||||
...lineData('jiabc')
|
line6.push([0, ' ', 1, ' '.charCodeAt(0)]);
|
||||||
]);
|
sub = lineData([['jiabc']]);
|
||||||
|
for (let i = 0; i < sub.length; ++i) line6.push(sub.get(i));
|
||||||
|
lines.set(6, line6);
|
||||||
|
|
||||||
(<MockBuffer>terminal.buffer).setLines(lines);
|
(<MockBuffer>terminal.buffer).setLines(lines);
|
||||||
terminal.buffer.ydisp = 0;
|
terminal.buffer.ydisp = 0;
|
||||||
registry = new CharacterJoinerRegistry(terminal);
|
registry = new CharacterJoinerRegistry(terminal);
|
||||||
@@ -257,8 +261,16 @@ describe('CharacterJoinerRegistry', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function lineData(line: string, attr: number = 0): LineData {
|
type IPartialLineData = ([string] | [string, number]);
|
||||||
return line.split('').map<CharData>(char => [attr, char, 1, char.charCodeAt(0)]);
|
|
||||||
|
function lineData(data: IPartialLineData[]): IBufferLine {
|
||||||
|
const tline = new BufferLine();
|
||||||
|
for (let i = 0; i < data.length; ++i) {
|
||||||
|
const line = data[i][0];
|
||||||
|
const attr = <number>(data[i][1] || 0);
|
||||||
|
line.split('').map(char => tline.push([attr, char, 1, char.charCodeAt(0)]));
|
||||||
|
}
|
||||||
|
return tline;
|
||||||
}
|
}
|
||||||
|
|
||||||
function substringJoiner(substring: string): (sequence: string) => [number, number][] {
|
function substringJoiner(substring: string): (sequence: string) => [number, number][] {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
|
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
|
||||||
import { ITerminal, LineData } from '../Types';
|
import { ITerminal, IBufferLine } from '../Types';
|
||||||
import { ICharacterJoinerRegistry, ICharacterJoiner } from './Types';
|
import { ICharacterJoinerRegistry, ICharacterJoiner } from './Types';
|
||||||
|
|
||||||
export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
|
export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
|
||||||
@@ -51,10 +51,10 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
|
|||||||
let rangeStartColumn = 0;
|
let rangeStartColumn = 0;
|
||||||
let currentStringIndex = 0;
|
let currentStringIndex = 0;
|
||||||
let rangeStartStringIndex = 0;
|
let rangeStartStringIndex = 0;
|
||||||
let rangeAttr = line[0][CHAR_DATA_ATTR_INDEX] >> 9;
|
let rangeAttr = line.get(0)[CHAR_DATA_ATTR_INDEX] >> 9;
|
||||||
|
|
||||||
for (let x = 0; x < this._terminal.cols; x++) {
|
for (let x = 0; x < this._terminal.cols; x++) {
|
||||||
const charData = line[x];
|
const charData = line.get(x);
|
||||||
const chars = charData[CHAR_DATA_CHAR_INDEX];
|
const chars = charData[CHAR_DATA_CHAR_INDEX];
|
||||||
const width = charData[CHAR_DATA_WIDTH_INDEX];
|
const width = charData[CHAR_DATA_WIDTH_INDEX];
|
||||||
const attr = charData[CHAR_DATA_ATTR_INDEX] >> 9;
|
const attr = charData[CHAR_DATA_ATTR_INDEX] >> 9;
|
||||||
@@ -115,7 +115,7 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
|
|||||||
* @param startIndex Start position of the range to search in the string (inclusive)
|
* @param startIndex Start position of the range to search in the string (inclusive)
|
||||||
* @param endIndex End position of the range to search in the string (exclusive)
|
* @param endIndex End position of the range to search in the string (exclusive)
|
||||||
*/
|
*/
|
||||||
private _getJoinedRanges(line: string, startIndex: number, endIndex: number, lineData: LineData, startCol: number): [number, number][] {
|
private _getJoinedRanges(line: string, startIndex: number, endIndex: number, lineData: IBufferLine, startCol: number): [number, number][] {
|
||||||
const text = line.substring(startIndex, endIndex);
|
const text = line.substring(startIndex, endIndex);
|
||||||
// At this point we already know that there is at least one joiner so
|
// At this point we already know that there is at least one joiner so
|
||||||
// we can just pull its value and assign it directly rather than
|
// we can just pull its value and assign it directly rather than
|
||||||
@@ -140,7 +140,7 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
|
|||||||
* @param line Cell data for the relevant line in the terminal
|
* @param line Cell data for the relevant line in the terminal
|
||||||
* @param startCol Offset within the line to start from
|
* @param startCol Offset within the line to start from
|
||||||
*/
|
*/
|
||||||
private _stringRangesToCellRanges(ranges: [number, number][], line: LineData, startCol: number): void {
|
private _stringRangesToCellRanges(ranges: [number, number][], line: IBufferLine, startCol: number): void {
|
||||||
let currentRangeIndex = 0;
|
let currentRangeIndex = 0;
|
||||||
let currentRangeStarted = false;
|
let currentRangeStarted = false;
|
||||||
let currentStringIndex = 0;
|
let currentStringIndex = 0;
|
||||||
@@ -152,7 +152,7 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let x = startCol; x < this._terminal.cols; x++) {
|
for (let x = startCol; x < this._terminal.cols; x++) {
|
||||||
const charData = line[x];
|
const charData = line.get(x);
|
||||||
const width = charData[CHAR_DATA_WIDTH_INDEX];
|
const width = charData[CHAR_DATA_WIDTH_INDEX];
|
||||||
const length = charData[CHAR_DATA_CHAR_INDEX].length;
|
const length = charData[CHAR_DATA_CHAR_INDEX].length;
|
||||||
|
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const charData = terminal.buffer.lines.get(cursorY)[terminal.buffer.x];
|
const charData = terminal.buffer.lines.get(cursorY).get(terminal.buffer.x);
|
||||||
if (!charData) {
|
if (!charData) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import { ILinkHoverEvent, ITerminal, ILinkifierAccessor, LinkHoverEventTypes } from '../Types';
|
import { ILinkHoverEvent, ITerminal, ILinkifierAccessor, LinkHoverEventTypes } from '../Types';
|
||||||
import { IColorSet, IRenderDimensions } from './Types';
|
import { IColorSet, IRenderDimensions } from './Types';
|
||||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||||
|
import { INVERTED_DEFAULT_COLOR } from './atlas/Types';
|
||||||
|
|
||||||
export class LinkRenderLayer extends BaseRenderLayer {
|
export class LinkRenderLayer extends BaseRenderLayer {
|
||||||
private _state: ILinkHoverEvent = null;
|
private _state: ILinkHoverEvent = null;
|
||||||
@@ -39,7 +40,15 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _onLinkHover(e: ILinkHoverEvent): void {
|
private _onLinkHover(e: ILinkHoverEvent): void {
|
||||||
this._ctx.fillStyle = this._colors.foreground.css;
|
if (e.fg === INVERTED_DEFAULT_COLOR) {
|
||||||
|
this._ctx.fillStyle = this._colors.background.css;
|
||||||
|
} else if (e.fg < 256) {
|
||||||
|
// 256 color support
|
||||||
|
this._ctx.fillStyle = this._colors.ansi[e.fg].css;
|
||||||
|
} else {
|
||||||
|
this._ctx.fillStyle = this._colors.foreground.css;
|
||||||
|
}
|
||||||
|
|
||||||
if (e.y1 === e.y2) {
|
if (e.y1 === e.y2) {
|
||||||
// Single line link
|
// Single line link
|
||||||
this.fillBottomLineAtCells(e.x1, e.y1, e.x2 - e.x1);
|
this.fillBottomLineAtCells(e.x1, e.y1, e.x2 - e.x1);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { ColorManager } from './ColorManager';
|
|||||||
import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions, ICharacterJoinerRegistry } from './Types';
|
import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions, ICharacterJoinerRegistry } from './Types';
|
||||||
import { ITerminal, CharacterJoinerHandler } from '../Types';
|
import { ITerminal, CharacterJoinerHandler } from '../Types';
|
||||||
import { LinkRenderLayer } from './LinkRenderLayer';
|
import { LinkRenderLayer } from './LinkRenderLayer';
|
||||||
import { EventEmitter } from '../EventEmitter';
|
import { EventEmitter } from '../common/EventEmitter';
|
||||||
import { RenderDebouncer } from '../ui/RenderDebouncer';
|
import { RenderDebouncer } from '../ui/RenderDebouncer';
|
||||||
import { ScreenDprMonitor } from '../ui/ScreenDprMonitor';
|
import { ScreenDprMonitor } from '../ui/ScreenDprMonitor';
|
||||||
import { ITheme } from 'xterm';
|
import { ITheme } from 'xterm';
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export class TextRenderLayer extends BaseRenderLayer {
|
|||||||
const line = terminal.buffer.lines.get(row);
|
const line = terminal.buffer.lines.get(row);
|
||||||
const joinedRanges = joinerRegistry ? joinerRegistry.getJoinedCharacters(row) : [];
|
const joinedRanges = joinerRegistry ? joinerRegistry.getJoinedCharacters(row) : [];
|
||||||
for (let x = 0; x < terminal.cols; x++) {
|
for (let x = 0; x < terminal.cols; x++) {
|
||||||
const charData = line[x];
|
const charData = line.get(x);
|
||||||
let code: number = <number>charData[CHAR_DATA_CODE_INDEX];
|
let code: number = <number>charData[CHAR_DATA_CODE_INDEX];
|
||||||
|
|
||||||
// Can either represent character(s) for a single cell or multiple cells
|
// Can either represent character(s) for a single cell or multiple cells
|
||||||
@@ -124,7 +124,7 @@ export class TextRenderLayer extends BaseRenderLayer {
|
|||||||
// get removed, and `a` would not re-render because it thinks it's
|
// get removed, and `a` would not re-render because it thinks it's
|
||||||
// already in the correct state.
|
// already in the correct state.
|
||||||
// this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA;
|
// this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA;
|
||||||
if (lastCharX < line.length - 1 && line[lastCharX + 1][CHAR_DATA_CODE_INDEX] === NULL_CELL_CODE) {
|
if (lastCharX < line.length - 1 && line.get(lastCharX + 1)[CHAR_DATA_CODE_INDEX] === NULL_CELL_CODE) {
|
||||||
width = 2;
|
width = 2;
|
||||||
// this._clearChar(x + 1, y);
|
// this._clearChar(x + 1, y);
|
||||||
// The overlapping char's char data will force a clear and render when the
|
// The overlapping char's char data will force a clear and render when the
|
||||||
|
|||||||
@@ -4,10 +4,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { IGlyphIdentifier } from './Types';
|
import { IGlyphIdentifier } from './Types';
|
||||||
|
import { IDisposable } from 'xterm';
|
||||||
|
|
||||||
export default abstract class BaseCharAtlas {
|
export default abstract class BaseCharAtlas implements IDisposable {
|
||||||
private _didWarmUp: boolean = false;
|
private _didWarmUp: boolean = false;
|
||||||
|
|
||||||
|
public dispose(): void { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform any work needed to warm the cache before it can be used. May be called multiple times.
|
* Perform any work needed to warm the cache before it can be used. May be called multiple times.
|
||||||
* Implement _doWarmUp instead if you only want to get called once.
|
* Implement _doWarmUp instead if you only want to get called once.
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import BaseCharAtlas from './BaseCharAtlas';
|
|||||||
import { DEFAULT_ANSI_COLORS } from '../ColorManager';
|
import { DEFAULT_ANSI_COLORS } from '../ColorManager';
|
||||||
import { clearColor } from '../../shared/atlas/CharAtlasGenerator';
|
import { clearColor } from '../../shared/atlas/CharAtlasGenerator';
|
||||||
import LRUMap from './LRUMap';
|
import LRUMap from './LRUMap';
|
||||||
|
import { isFirefox, isSafari } from '../../shared/utils/Browser';
|
||||||
|
|
||||||
// In practice we're probably never going to exhaust a texture this large. For debugging purposes,
|
// In practice we're probably never going to exhaust a texture this large. For debugging purposes,
|
||||||
// however, it can be useful to set this to a really tiny value, to verify that LRU eviction works.
|
// however, it can be useful to set this to a really tiny value, to verify that LRU eviction works.
|
||||||
@@ -29,14 +30,29 @@ const TRANSPARENT_COLOR = {
|
|||||||
// cache.
|
// cache.
|
||||||
const FRAME_CACHE_DRAW_LIMIT = 100;
|
const FRAME_CACHE_DRAW_LIMIT = 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of milliseconds to wait before generating the ImageBitmap, this is to debounce/batch
|
||||||
|
* the operation as window.createImageBitmap is asynchronous.
|
||||||
|
*/
|
||||||
|
const GLYPH_BITMAP_COMMIT_DELAY = 100;
|
||||||
|
|
||||||
interface IGlyphCacheValue {
|
interface IGlyphCacheValue {
|
||||||
index: number;
|
index: number;
|
||||||
isEmpty: boolean;
|
isEmpty: boolean;
|
||||||
|
inBitmap: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGlyphCacheKey(glyph: IGlyphIdentifier): string {
|
function getGlyphCacheKey(glyph: IGlyphIdentifier): number {
|
||||||
const styleFlags = (glyph.bold ? 0 : 4) + (glyph.dim ? 0 : 2) + (glyph.italic ? 0 : 1);
|
// Note that this only returns a valid key when code < 256
|
||||||
return `${glyph.bg}_${glyph.fg}_${styleFlags}${glyph.chars}`;
|
// Layout:
|
||||||
|
// 0b00000000000000000000000000000001: italic (1)
|
||||||
|
// 0b00000000000000000000000000000010: dim (1)
|
||||||
|
// 0b00000000000000000000000000000100: bold (1)
|
||||||
|
// 0b00000000000000000000111111111000: fg (9)
|
||||||
|
// 0b00000000000111111111000000000000: bg (9)
|
||||||
|
// 0b00011111111000000000000000000000: code (8)
|
||||||
|
// 0b11100000000000000000000000000000: unused (3)
|
||||||
|
return glyph.code << 21 | glyph.bg << 12 | glyph.fg << 3 | (glyph.bold ? 0 : 4) + (glyph.dim ? 0 : 2) + (glyph.italic ? 0 : 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class DynamicCharAtlas extends BaseCharAtlas {
|
export default class DynamicCharAtlas extends BaseCharAtlas {
|
||||||
@@ -57,6 +73,15 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
|
|||||||
|
|
||||||
private _drawToCacheCount: number = 0;
|
private _drawToCacheCount: number = 0;
|
||||||
|
|
||||||
|
// An array of glyph keys that are waiting on the bitmap to be generated.
|
||||||
|
private _glyphsWaitingOnBitmap: IGlyphCacheValue[] = [];
|
||||||
|
|
||||||
|
// The timeout that is used to batch bitmap generation so it's not requested for every new glyph.
|
||||||
|
private _bitmapCommitTimeout: number | null = null;
|
||||||
|
|
||||||
|
// The bitmap to draw from, this is much faster on other browsers than others.
|
||||||
|
private _bitmap: ImageBitmap | null = null;
|
||||||
|
|
||||||
constructor(document: Document, private _config: ICharAtlasConfig) {
|
constructor(document: Document, private _config: ICharAtlasConfig) {
|
||||||
super();
|
super();
|
||||||
this._cacheCanvas = document.createElement('canvas');
|
this._cacheCanvas = document.createElement('canvas');
|
||||||
@@ -82,6 +107,13 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
|
|||||||
// document.body.appendChild(this._cacheCanvas);
|
// document.body.appendChild(this._cacheCanvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public dispose(): void {
|
||||||
|
if (this._bitmapCommitTimeout !== null) {
|
||||||
|
window.clearTimeout(this._bitmapCommitTimeout);
|
||||||
|
this._bitmapCommitTimeout = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public beginFrame(): void {
|
public beginFrame(): void {
|
||||||
this._drawToCacheCount = 0;
|
this._drawToCacheCount = 0;
|
||||||
}
|
}
|
||||||
@@ -92,9 +124,14 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
|
|||||||
x: number,
|
x: number,
|
||||||
y: number
|
y: number
|
||||||
): boolean {
|
): boolean {
|
||||||
|
// Space is always an empty cell, special case this as it's so common
|
||||||
|
if (glyph.code === 32) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const glyphKey = getGlyphCacheKey(glyph);
|
const glyphKey = getGlyphCacheKey(glyph);
|
||||||
const cacheValue = this._cacheMap.get(glyphKey);
|
const cacheValue = this._cacheMap.get(glyphKey);
|
||||||
if (cacheValue != null) {
|
if (cacheValue !== null && cacheValue !== undefined) {
|
||||||
this._drawFromCache(ctx, cacheValue, x, y);
|
this._drawFromCache(ctx, cacheValue, x, y);
|
||||||
return true;
|
return true;
|
||||||
} else if (this._canCache(glyph) && this._drawToCacheCount < FRAME_CACHE_DRAW_LIMIT) {
|
} else if (this._canCache(glyph) && this._drawToCacheCount < FRAME_CACHE_DRAW_LIMIT) {
|
||||||
@@ -124,11 +161,12 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
|
|||||||
return glyph.code < 256;
|
return glyph.code < 256;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _toCoordinates(index: number): [number, number] {
|
private _toCoordinateX(index: number): number {
|
||||||
return [
|
return (index % this._width) * this._config.scaledCharWidth;
|
||||||
(index % this._width) * this._config.scaledCharWidth,
|
}
|
||||||
Math.floor(index / this._width) * this._config.scaledCharHeight
|
|
||||||
];
|
private _toCoordinateY(index: number): number {
|
||||||
|
return Math.floor(index / this._width) * this._config.scaledCharHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _drawFromCache(
|
private _drawFromCache(
|
||||||
@@ -141,9 +179,10 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
|
|||||||
if (cacheValue.isEmpty) {
|
if (cacheValue.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const [cacheX, cacheY] = this._toCoordinates(cacheValue.index);
|
const cacheX = this._toCoordinateX(cacheValue.index);
|
||||||
|
const cacheY = this._toCoordinateY(cacheValue.index);
|
||||||
ctx.drawImage(
|
ctx.drawImage(
|
||||||
this._cacheCanvas,
|
cacheValue.inBitmap ? this._bitmap : this._cacheCanvas,
|
||||||
cacheX,
|
cacheX,
|
||||||
cacheY,
|
cacheY,
|
||||||
this._config.scaledCharWidth,
|
this._config.scaledCharWidth,
|
||||||
@@ -230,13 +269,58 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// copy the data from imageData to _cacheCanvas
|
// copy the data from imageData to _cacheCanvas
|
||||||
const [x, y] = this._toCoordinates(index);
|
const x = this._toCoordinateX(index);
|
||||||
|
const y = this._toCoordinateY(index);
|
||||||
// putImageData doesn't do any blending, so it will overwrite any existing cache entry for us
|
// putImageData doesn't do any blending, so it will overwrite any existing cache entry for us
|
||||||
this._cacheCtx.putImageData(imageData, x, y);
|
this._cacheCtx.putImageData(imageData, x, y);
|
||||||
|
|
||||||
return {
|
// Add the glyph and queue it to the bitmap (if the browser supports it)
|
||||||
|
const cacheValue = {
|
||||||
index,
|
index,
|
||||||
isEmpty
|
isEmpty,
|
||||||
|
inBitmap: false
|
||||||
};
|
};
|
||||||
|
this._addGlyphToBitmap(cacheValue);
|
||||||
|
|
||||||
|
return cacheValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addGlyphToBitmap(cacheValue: IGlyphCacheValue): void {
|
||||||
|
// Support is patchy for createImageBitmap at the moment, pass a canvas back
|
||||||
|
// if support is lacking as drawImage works there too. Firefox is also
|
||||||
|
// included here as ImageBitmap appears both buggy and has horrible
|
||||||
|
// performance (tested on v55).
|
||||||
|
if (!('createImageBitmap' in window) || isFirefox || isSafari) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the glyph to the queue
|
||||||
|
this._glyphsWaitingOnBitmap.push(cacheValue);
|
||||||
|
|
||||||
|
// Check if bitmap generation timeout already exists
|
||||||
|
if (this._bitmapCommitTimeout !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._bitmapCommitTimeout = window.setTimeout(() => this._generateBitmap(), GLYPH_BITMAP_COMMIT_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _generateBitmap(): void {
|
||||||
|
const glyphsMovingToBitmap = this._glyphsWaitingOnBitmap;
|
||||||
|
this._glyphsWaitingOnBitmap = [];
|
||||||
|
window.createImageBitmap(this._cacheCanvas).then(bitmap => {
|
||||||
|
// Set bitmap
|
||||||
|
this._bitmap = bitmap;
|
||||||
|
|
||||||
|
// Mark all new glyphs as in bitmap, excluding glyphs that came in after
|
||||||
|
// the bitmap was requested
|
||||||
|
for (let i = 0; i < glyphsMovingToBitmap.length; i++) {
|
||||||
|
const value = glyphsMovingToBitmap[i];
|
||||||
|
// It doesn't matter if the value was already evicted, it will be
|
||||||
|
// released from memory after this block if so.
|
||||||
|
value.inBitmap = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this._bitmapCommitTimeout = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,57 +9,57 @@ import LRUMap from './LRUMap';
|
|||||||
describe('LRUMap', () => {
|
describe('LRUMap', () => {
|
||||||
it('can be used to store and retrieve values', () => {
|
it('can be used to store and retrieve values', () => {
|
||||||
const map = new LRUMap(10);
|
const map = new LRUMap(10);
|
||||||
map.set('keya', 'valuea');
|
map.set(1, 'valuea');
|
||||||
map.set('keyb', 'valueb');
|
map.set(2, 'valueb');
|
||||||
map.set('keyc', 'valuec');
|
map.set(3, 'valuec');
|
||||||
assert.strictEqual(map.get('keya'), 'valuea');
|
assert.strictEqual(map.get(1), 'valuea');
|
||||||
assert.strictEqual(map.get('keyb'), 'valueb');
|
assert.strictEqual(map.get(2), 'valueb');
|
||||||
assert.strictEqual(map.get('keyc'), 'valuec');
|
assert.strictEqual(map.get(3), 'valuec');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('maintains a size from insertions', () => {
|
it('maintains a size from insertions', () => {
|
||||||
const map = new LRUMap(10);
|
const map = new LRUMap(10);
|
||||||
assert.strictEqual(map.size, 0);
|
assert.strictEqual(map.size, 0);
|
||||||
map.set('a', 'value');
|
map.set(1, 'value');
|
||||||
assert.strictEqual(map.size, 1);
|
assert.strictEqual(map.size, 1);
|
||||||
map.set('b', 'value');
|
map.set(2, 'value');
|
||||||
assert.strictEqual(map.size, 2);
|
assert.strictEqual(map.size, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('deletes the oldest entry when the capacity is exceeded', () => {
|
it('deletes the oldest entry when the capacity is exceeded', () => {
|
||||||
const map = new LRUMap(4);
|
const map = new LRUMap(4);
|
||||||
map.set('a', 'value');
|
map.set(1, 'value');
|
||||||
map.set('b', 'value');
|
map.set(2, 'value');
|
||||||
map.set('c', 'value');
|
map.set(3, 'value');
|
||||||
map.set('d', 'value');
|
map.set(4, 'value');
|
||||||
map.set('e', 'value');
|
map.set(5, 'value');
|
||||||
assert.isNull(map.get('a'));
|
assert.isNull(map.get(1));
|
||||||
assert.isNotNull(map.get('b'));
|
assert.isNotNull(map.get(2));
|
||||||
assert.isNotNull(map.get('c'));
|
assert.isNotNull(map.get(3));
|
||||||
assert.isNotNull(map.get('d'));
|
assert.isNotNull(map.get(4));
|
||||||
assert.isNotNull(map.get('e'));
|
assert.isNotNull(map.get(5));
|
||||||
assert.strictEqual(map.size, 4);
|
assert.strictEqual(map.size, 4);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('prevents a recently accessed entry from getting deleted', () => {
|
it('prevents a recently accessed entry from getting deleted', () => {
|
||||||
const map = new LRUMap(2);
|
const map = new LRUMap(2);
|
||||||
map.set('a', 'value');
|
map.set(1, 'value');
|
||||||
map.set('b', 'value');
|
map.set(2, 'value');
|
||||||
map.get('a');
|
map.get(1);
|
||||||
// a would normally get deleted here, except that we called get()
|
// a would normally get deleted here, except that we called get()
|
||||||
map.set('c', 'value');
|
map.set(3, 'value');
|
||||||
assert.isNotNull(map.get('a'));
|
assert.isNotNull(map.get(1));
|
||||||
// b got deleted instead of a
|
// b got deleted instead of a
|
||||||
assert.isNull(map.get('b'));
|
assert.isNull(map.get(2));
|
||||||
assert.isNotNull(map.get('c'));
|
assert.isNotNull(map.get(3));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports mutation', () => {
|
it('supports mutation', () => {
|
||||||
const map = new LRUMap(10);
|
const map = new LRUMap(10);
|
||||||
map.set('keya', 'oldvalue');
|
map.set(1, 'oldvalue');
|
||||||
map.set('keya', 'newvalue');
|
map.set(1, 'newvalue');
|
||||||
// mutation doesn't change the size
|
// mutation doesn't change the size
|
||||||
assert.strictEqual(map.size, 1);
|
assert.strictEqual(map.size, 1);
|
||||||
assert.strictEqual(map.get('keya'), 'newvalue');
|
assert.strictEqual(map.get(1), 'newvalue');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
interface ILinkedListNode<T> {
|
interface ILinkedListNode<T> {
|
||||||
prev: ILinkedListNode<T>;
|
prev: ILinkedListNode<T>;
|
||||||
next: ILinkedListNode<T>;
|
next: ILinkedListNode<T>;
|
||||||
key: string;
|
key: number;
|
||||||
value: T;
|
value: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class LRUMap<T> {
|
export default class LRUMap<T> {
|
||||||
private _map: { [key: string]: ILinkedListNode<T> } = {};
|
private _map: { [key: number]: ILinkedListNode<T> } = {};
|
||||||
private _head: ILinkedListNode<T> = null;
|
private _head: ILinkedListNode<T> = null;
|
||||||
private _tail: ILinkedListNode<T> = null;
|
private _tail: ILinkedListNode<T> = null;
|
||||||
private _nodePool: ILinkedListNode<T>[] = [];
|
private _nodePool: ILinkedListNode<T>[] = [];
|
||||||
@@ -68,7 +68,7 @@ export default class LRUMap<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public get(key: string): T | null {
|
public get(key: number): T | null {
|
||||||
// This is unsafe: We're assuming our keyspace doesn't overlap with Object.prototype. However,
|
// This is unsafe: We're assuming our keyspace doesn't overlap with Object.prototype. However,
|
||||||
// it's faster than calling hasOwnProperty, and in our case, it would never overlap.
|
// it's faster than calling hasOwnProperty, and in our case, it would never overlap.
|
||||||
const node = this._map[key];
|
const node = this._map[key];
|
||||||
@@ -80,12 +80,23 @@ export default class LRUMap<T> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a value from a key without marking it as the most recently used item.
|
||||||
|
*/
|
||||||
|
public peekValue(key: number): T | null {
|
||||||
|
const node = this._map[key];
|
||||||
|
if (node !== undefined) {
|
||||||
|
return node.value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public peek(): T | null {
|
public peek(): T | null {
|
||||||
const head = this._head;
|
const head = this._head;
|
||||||
return head === null ? null : head.value;
|
return head === null ? null : head.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public set(key: string, value: T): void {
|
public set(key: number, value: T): void {
|
||||||
// This is unsafe: See note above.
|
// This is unsafe: See note above.
|
||||||
let node = this._map[key];
|
let node = this._map[key];
|
||||||
if (node !== undefined) {
|
if (node !== undefined) {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export default class StaticCharAtlas extends BaseCharAtlas {
|
|||||||
y: number
|
y: number
|
||||||
): boolean {
|
): boolean {
|
||||||
// we're not warmed up yet
|
// we're not warmed up yet
|
||||||
if (this._texture == null) {
|
if (this._texture === null || this._texture === undefined) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { IRenderer, IRenderDimensions, IColorSet } from '../Types';
|
import { IRenderer, IRenderDimensions, IColorSet } from '../Types';
|
||||||
import { ITerminal, CharacterJoinerHandler } from '../../Types';
|
import { ILinkHoverEvent, ITerminal, CharacterJoinerHandler, LinkHoverEventTypes } from '../../Types';
|
||||||
import { ITheme } from 'xterm';
|
import { ITheme } from 'xterm';
|
||||||
import { EventEmitter } from '../../EventEmitter';
|
import { EventEmitter } from '../../common/EventEmitter';
|
||||||
import { ColorManager } from '../ColorManager';
|
import { ColorManager } from '../ColorManager';
|
||||||
import { RenderDebouncer } from '../../ui/RenderDebouncer';
|
import { RenderDebouncer } from '../../ui/RenderDebouncer';
|
||||||
import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, DomRendererRowFactory } from './DomRendererRowFactory';
|
import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from './DomRendererRowFactory';
|
||||||
|
|
||||||
const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-';
|
const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-';
|
||||||
const ROW_CONTAINER_CLASS = 'xterm-rows';
|
const ROW_CONTAINER_CLASS = 'xterm-rows';
|
||||||
@@ -79,6 +79,9 @@ export class DomRenderer extends EventEmitter implements IRenderer {
|
|||||||
this._terminal.element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass);
|
this._terminal.element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass);
|
||||||
this._terminal.screenElement.appendChild(this._rowContainer);
|
this._terminal.screenElement.appendChild(this._rowContainer);
|
||||||
this._terminal.screenElement.appendChild(this._selectionContainer);
|
this._terminal.screenElement.appendChild(this._selectionContainer);
|
||||||
|
|
||||||
|
this._terminal.linkifier.on(LinkHoverEventTypes.HOVER, (e: ILinkHoverEvent) => this._onLinkHover(e));
|
||||||
|
this._terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: ILinkHoverEvent) => this._onLinkLeave(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
@@ -160,13 +163,19 @@ export class DomRenderer extends EventEmitter implements IRenderer {
|
|||||||
`}`;
|
`}`;
|
||||||
// Cursor
|
// Cursor
|
||||||
styles +=
|
styles +=
|
||||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS} {` +
|
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS} {` +
|
||||||
|
` outline: 1px solid ${this.colorManager.colors.cursor.css};` +
|
||||||
|
` outline-offset: -1px;` +
|
||||||
|
`}` +
|
||||||
|
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` +
|
||||||
` background-color: ${this.colorManager.colors.cursor.css};` +
|
` background-color: ${this.colorManager.colors.cursor.css};` +
|
||||||
` color: ${this.colorManager.colors.cursorAccent.css};` +
|
` color: ${this.colorManager.colors.cursorAccent.css};` +
|
||||||
`}` +
|
`}` +
|
||||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS} {` +
|
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BAR_CLASS} {` +
|
||||||
` outline: 1px solid #fff;` +
|
` box-shadow: 1px 0 0 ${this.colorManager.colors.cursor.css} inset;` +
|
||||||
` outline-offset: -1px;` +
|
`}` +
|
||||||
|
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_UNDERLINE_CLASS} {` +
|
||||||
|
` box-shadow: 0 -1px 0 ${this.colorManager.colors.cursor.css} inset;` +
|
||||||
`}`;
|
`}`;
|
||||||
// Selection
|
// Selection
|
||||||
styles +=
|
styles +=
|
||||||
@@ -319,7 +328,8 @@ export class DomRenderer extends EventEmitter implements IRenderer {
|
|||||||
|
|
||||||
const row = y + terminal.buffer.ydisp;
|
const row = y + terminal.buffer.ydisp;
|
||||||
const lineData = terminal.buffer.lines.get(row);
|
const lineData = terminal.buffer.lines.get(row);
|
||||||
rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorX, terminal.charMeasure.width, terminal.cols));
|
const cursorStyle = terminal.options.cursorStyle;
|
||||||
|
rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorStyle, cursorX, terminal.charMeasure.width, terminal.cols));
|
||||||
}
|
}
|
||||||
|
|
||||||
this._terminal.emit('refresh', {start, end});
|
this._terminal.emit('refresh', {start, end});
|
||||||
@@ -331,4 +341,23 @@ export class DomRenderer extends EventEmitter implements IRenderer {
|
|||||||
|
|
||||||
public registerCharacterJoiner(handler: CharacterJoinerHandler): number { return -1; }
|
public registerCharacterJoiner(handler: CharacterJoinerHandler): number { return -1; }
|
||||||
public deregisterCharacterJoiner(joinerId: number): boolean { return false; }
|
public deregisterCharacterJoiner(joinerId: number): boolean { return false; }
|
||||||
|
|
||||||
|
private _onLinkHover(e: ILinkHoverEvent): void {
|
||||||
|
this._setCellUnderline(e.x1, e.x2, e.y1, e.y2, e.cols, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onLinkLeave(e: ILinkHoverEvent): void {
|
||||||
|
this._setCellUnderline(e.x1, e.x2, e.y1, e.y2, e.cols, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setCellUnderline(x: number, x2: number, y: number, y2: number, cols: number, enabled: boolean): void {
|
||||||
|
while (x !== x2 || y !== y2) {
|
||||||
|
const span = <HTMLElement>this._rowElements[y].children[x];
|
||||||
|
span.style.textDecoration = enabled ? 'underline' : 'none';
|
||||||
|
x = (x + 1) % cols;
|
||||||
|
if (x === 0) {
|
||||||
|
y++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,15 @@
|
|||||||
import jsdom = require('jsdom');
|
import jsdom = require('jsdom');
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import { DomRendererRowFactory } from './DomRendererRowFactory';
|
import { DomRendererRowFactory } from './DomRendererRowFactory';
|
||||||
import { LineData } from '../../Types';
|
|
||||||
import { DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from '../../Buffer';
|
import { DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from '../../Buffer';
|
||||||
import { FLAGS } from '../Types';
|
import { FLAGS } from '../Types';
|
||||||
|
import { BufferLine } from '../../BufferLine';
|
||||||
|
import { IBufferLine } from '../../Types';
|
||||||
|
|
||||||
describe('DomRendererRowFactory', () => {
|
describe('DomRendererRowFactory', () => {
|
||||||
let dom: jsdom.JSDOM;
|
let dom: jsdom.JSDOM;
|
||||||
let rowFactory: DomRendererRowFactory;
|
let rowFactory: DomRendererRowFactory;
|
||||||
let lineData: LineData;
|
let lineData: IBufferLine;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
dom = new jsdom.JSDOM('');
|
dom = new jsdom.JSDOM('');
|
||||||
@@ -23,7 +24,7 @@ describe('DomRendererRowFactory', () => {
|
|||||||
|
|
||||||
describe('createRow', () => {
|
describe('createRow', () => {
|
||||||
it('should create an element for every character in the row', () => {
|
it('should create an element for every character in the row', () => {
|
||||||
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
|
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
|
||||||
assert.equal(getFragmentHtml(fragment),
|
assert.equal(getFragmentHtml(fragment),
|
||||||
'<span> </span>' +
|
'<span> </span>' +
|
||||||
'<span> </span>'
|
'<span> </span>'
|
||||||
@@ -31,27 +32,29 @@ describe('DomRendererRowFactory', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should set correct attributes for double width characters', () => {
|
it('should set correct attributes for double width characters', () => {
|
||||||
lineData[0] = [DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)];
|
lineData.set(0, [DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)]);
|
||||||
// There should be no element for the following "empty" cell
|
// There should be no element for the following "empty" cell
|
||||||
lineData[1] = [DEFAULT_ATTR, '', 0, undefined];
|
lineData.set(1, [DEFAULT_ATTR, '', 0, undefined]);
|
||||||
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
|
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
|
||||||
assert.equal(getFragmentHtml(fragment),
|
assert.equal(getFragmentHtml(fragment),
|
||||||
'<span style="width: 10px;">語</span>'
|
'<span style="width: 10px;">語</span>'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add class for cursor', () => {
|
it('should add class for cursor and cursor style', () => {
|
||||||
const fragment = rowFactory.createRow(lineData, true, 0, 5, 20);
|
for (const style of ['block', 'bar', 'underline']) {
|
||||||
assert.equal(getFragmentHtml(fragment),
|
const fragment = rowFactory.createRow(lineData, true, style, 0, 5, 20);
|
||||||
'<span class="xterm-cursor"> </span>' +
|
assert.equal(getFragmentHtml(fragment),
|
||||||
'<span> </span>'
|
`<span class="xterm-cursor xterm-cursor-${style}"> </span>` +
|
||||||
);
|
'<span> </span>'
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not render cells that go beyond the terminal\'s columns', () => {
|
it('should not render cells that go beyond the terminal\'s columns', () => {
|
||||||
lineData[0] = [DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)];
|
lineData.set(0, [DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]);
|
||||||
lineData[1] = [DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)];
|
lineData.set(1, [DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]);
|
||||||
const fragment = rowFactory.createRow(lineData, false, 0, 5, 1);
|
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 1);
|
||||||
assert.equal(getFragmentHtml(fragment),
|
assert.equal(getFragmentHtml(fragment),
|
||||||
'<span>a</span>'
|
'<span>a</span>'
|
||||||
);
|
);
|
||||||
@@ -59,8 +62,8 @@ describe('DomRendererRowFactory', () => {
|
|||||||
|
|
||||||
describe('attributes', () => {
|
describe('attributes', () => {
|
||||||
it('should add class for bold', () => {
|
it('should add class for bold', () => {
|
||||||
lineData[0] = [DEFAULT_ATTR | (FLAGS.BOLD << 18), 'a', 1, 'a'.charCodeAt(0)];
|
lineData.set(0, [DEFAULT_ATTR | (FLAGS.BOLD << 18), 'a', 1, 'a'.charCodeAt(0)]);
|
||||||
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
|
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
|
||||||
assert.equal(getFragmentHtml(fragment),
|
assert.equal(getFragmentHtml(fragment),
|
||||||
'<span class="xterm-bold">a</span>' +
|
'<span class="xterm-bold">a</span>' +
|
||||||
'<span> </span>'
|
'<span> </span>'
|
||||||
@@ -68,8 +71,8 @@ describe('DomRendererRowFactory', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should add class for italic', () => {
|
it('should add class for italic', () => {
|
||||||
lineData[0] = [DEFAULT_ATTR | (FLAGS.ITALIC << 18), 'a', 1, 'a'.charCodeAt(0)];
|
lineData.set(0, [DEFAULT_ATTR | (FLAGS.ITALIC << 18), 'a', 1, 'a'.charCodeAt(0)]);
|
||||||
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
|
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
|
||||||
assert.equal(getFragmentHtml(fragment),
|
assert.equal(getFragmentHtml(fragment),
|
||||||
'<span class="xterm-italic">a</span>' +
|
'<span class="xterm-italic">a</span>' +
|
||||||
'<span> </span>'
|
'<span> </span>'
|
||||||
@@ -79,8 +82,8 @@ describe('DomRendererRowFactory', () => {
|
|||||||
it('should add classes for 256 foreground colors', () => {
|
it('should add classes for 256 foreground colors', () => {
|
||||||
const defaultAttrNoFgColor = (0 << 9) | (256 << 0);
|
const defaultAttrNoFgColor = (0 << 9) | (256 << 0);
|
||||||
for (let i = 0; i < 256; i++) {
|
for (let i = 0; i < 256; i++) {
|
||||||
lineData[0] = [defaultAttrNoFgColor | (i << 9), 'a', 1, 'a'.charCodeAt(0)];
|
lineData.set(0, [defaultAttrNoFgColor | (i << 9), 'a', 1, 'a'.charCodeAt(0)]);
|
||||||
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
|
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
|
||||||
assert.equal(getFragmentHtml(fragment),
|
assert.equal(getFragmentHtml(fragment),
|
||||||
`<span class="xterm-fg-${i}">a</span>` +
|
`<span class="xterm-fg-${i}">a</span>` +
|
||||||
'<span> </span>'
|
'<span> </span>'
|
||||||
@@ -91,8 +94,8 @@ describe('DomRendererRowFactory', () => {
|
|||||||
it('should add classes for 256 background colors', () => {
|
it('should add classes for 256 background colors', () => {
|
||||||
const defaultAttrNoBgColor = (257 << 9) | (0 << 0);
|
const defaultAttrNoBgColor = (257 << 9) | (0 << 0);
|
||||||
for (let i = 0; i < 256; i++) {
|
for (let i = 0; i < 256; i++) {
|
||||||
lineData[0] = [defaultAttrNoBgColor | (i << 0), 'a', 1, 'a'.charCodeAt(0)];
|
lineData.set(0, [defaultAttrNoBgColor | (i << 0), 'a', 1, 'a'.charCodeAt(0)]);
|
||||||
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
|
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
|
||||||
assert.equal(getFragmentHtml(fragment),
|
assert.equal(getFragmentHtml(fragment),
|
||||||
`<span class="xterm-bg-${i}">a</span>` +
|
`<span class="xterm-bg-${i}">a</span>` +
|
||||||
'<span> </span>'
|
'<span> </span>'
|
||||||
@@ -101,8 +104,8 @@ describe('DomRendererRowFactory', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly invert colors', () => {
|
it('should correctly invert colors', () => {
|
||||||
lineData[0] = [(FLAGS.INVERSE << 18) | (2 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)];
|
lineData.set(0, [(FLAGS.INVERSE << 18) | (2 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]);
|
||||||
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
|
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
|
||||||
assert.equal(getFragmentHtml(fragment),
|
assert.equal(getFragmentHtml(fragment),
|
||||||
'<span class="xterm-fg-1 xterm-bg-2">a</span>' +
|
'<span class="xterm-fg-1 xterm-bg-2">a</span>' +
|
||||||
'<span> </span>'
|
'<span> </span>'
|
||||||
@@ -110,8 +113,8 @@ describe('DomRendererRowFactory', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly invert default fg color', () => {
|
it('should correctly invert default fg color', () => {
|
||||||
lineData[0] = [(FLAGS.INVERSE << 18) | (257 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)];
|
lineData.set(0, [(FLAGS.INVERSE << 18) | (257 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]);
|
||||||
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
|
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
|
||||||
assert.equal(getFragmentHtml(fragment),
|
assert.equal(getFragmentHtml(fragment),
|
||||||
'<span class="xterm-fg-1 xterm-bg-15">a</span>' +
|
'<span class="xterm-fg-1 xterm-bg-15">a</span>' +
|
||||||
'<span> </span>'
|
'<span> </span>'
|
||||||
@@ -119,8 +122,8 @@ describe('DomRendererRowFactory', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly invert default bg color', () => {
|
it('should correctly invert default bg color', () => {
|
||||||
lineData[0] = [(FLAGS.INVERSE << 18) | (1 << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)];
|
lineData.set(0, [(FLAGS.INVERSE << 18) | (1 << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)]);
|
||||||
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
|
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
|
||||||
assert.equal(getFragmentHtml(fragment),
|
assert.equal(getFragmentHtml(fragment),
|
||||||
'<span class="xterm-fg-0 xterm-bg-1">a</span>' +
|
'<span class="xterm-fg-0 xterm-bg-1">a</span>' +
|
||||||
'<span> </span>'
|
'<span> </span>'
|
||||||
@@ -129,8 +132,8 @@ describe('DomRendererRowFactory', () => {
|
|||||||
|
|
||||||
it('should turn bold fg text bright', () => {
|
it('should turn bold fg text bright', () => {
|
||||||
for (let i = 0; i < 8; i++) {
|
for (let i = 0; i < 8; i++) {
|
||||||
lineData[0] = [(FLAGS.BOLD << 18) | (i << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)];
|
lineData.set(0, [(FLAGS.BOLD << 18) | (i << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)]);
|
||||||
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
|
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
|
||||||
assert.equal(getFragmentHtml(fragment),
|
assert.equal(getFragmentHtml(fragment),
|
||||||
`<span class="xterm-bold xterm-fg-${i + 8}">a</span>` +
|
`<span class="xterm-bold xterm-fg-${i + 8}">a</span>` +
|
||||||
'<span> </span>'
|
'<span> </span>'
|
||||||
@@ -146,8 +149,8 @@ describe('DomRendererRowFactory', () => {
|
|||||||
return element.innerHTML;
|
return element.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEmptyLineData(cols: number): LineData {
|
function createEmptyLineData(cols: number): IBufferLine {
|
||||||
const lineData: LineData = [];
|
const lineData = new BufferLine();
|
||||||
for (let i = 0; i < cols; i++) {
|
for (let i = 0; i < cols; i++) {
|
||||||
lineData.push([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
lineData.push([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,16 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { LineData } from '../../Types';
|
|
||||||
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../../Buffer';
|
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../../Buffer';
|
||||||
import { FLAGS } from '../Types';
|
import { FLAGS } from '../Types';
|
||||||
|
import { IBufferLine } from '../../Types';
|
||||||
|
|
||||||
export const BOLD_CLASS = 'xterm-bold';
|
export const BOLD_CLASS = 'xterm-bold';
|
||||||
export const ITALIC_CLASS = 'xterm-italic';
|
export const ITALIC_CLASS = 'xterm-italic';
|
||||||
export const CURSOR_CLASS = 'xterm-cursor';
|
export const CURSOR_CLASS = 'xterm-cursor';
|
||||||
|
export const CURSOR_STYLE_BLOCK_CLASS = 'xterm-cursor-block';
|
||||||
|
export const CURSOR_STYLE_BAR_CLASS = 'xterm-cursor-bar';
|
||||||
|
export const CURSOR_STYLE_UNDERLINE_CLASS = 'xterm-cursor-underline';
|
||||||
|
|
||||||
export class DomRendererRowFactory {
|
export class DomRendererRowFactory {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -17,7 +20,7 @@ export class DomRendererRowFactory {
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public createRow(lineData: LineData, isCursorRow: boolean, cursorX: number, cellWidth: number, cols: number): DocumentFragment {
|
public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cellWidth: number, cols: number): DocumentFragment {
|
||||||
const fragment = this._document.createDocumentFragment();
|
const fragment = this._document.createDocumentFragment();
|
||||||
let colCount = 0;
|
let colCount = 0;
|
||||||
|
|
||||||
@@ -27,7 +30,7 @@ export class DomRendererRowFactory {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const charData = lineData[x];
|
const charData = lineData.get(x);
|
||||||
const char: string = charData[CHAR_DATA_CHAR_INDEX];
|
const char: string = charData[CHAR_DATA_CHAR_INDEX];
|
||||||
const attr: number = charData[CHAR_DATA_ATTR_INDEX];
|
const attr: number = charData[CHAR_DATA_ATTR_INDEX];
|
||||||
const width: number = charData[CHAR_DATA_WIDTH_INDEX];
|
const width: number = charData[CHAR_DATA_WIDTH_INDEX];
|
||||||
@@ -48,6 +51,18 @@ export class DomRendererRowFactory {
|
|||||||
|
|
||||||
if (isCursorRow && x === cursorX) {
|
if (isCursorRow && x === cursorX) {
|
||||||
charElement.classList.add(CURSOR_CLASS);
|
charElement.classList.add(CURSOR_CLASS);
|
||||||
|
|
||||||
|
switch (cursorStyle) {
|
||||||
|
case 'bar':
|
||||||
|
charElement.classList.add(CURSOR_STYLE_BAR_CLASS);
|
||||||
|
break;
|
||||||
|
case 'underline':
|
||||||
|
charElement.classList.add(CURSOR_STYLE_UNDERLINE_CLASS);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
charElement.classList.add(CURSOR_STYLE_BLOCK_CLASS);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If inverse flag is on, the foreground should become the background.
|
// If inverse flag is on, the foreground should become the background.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { ICharMeasure, ITerminalOptions } from '../Types';
|
import { ICharMeasure, ITerminalOptions } from '../Types';
|
||||||
import { EventEmitter } from '../EventEmitter';
|
import { EventEmitter } from '../common/EventEmitter';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility class that measures the size of a character. Measurements are done in
|
* Utility class that measures the size of a character. Measurements are done in
|
||||||
|
|||||||
@@ -22,11 +22,16 @@ export class RenderDebouncer implements IDisposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public refresh(rowStart?: number, rowEnd?: number): void {
|
public refresh(rowStart: number, rowEnd: number): void {
|
||||||
rowStart = rowStart || 0;
|
// Get the min/max row start/end for the arg values
|
||||||
rowEnd = rowEnd || this._terminal.rows - 1;
|
rowStart = rowStart !== null && rowStart !== undefined ? rowStart : 0;
|
||||||
this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart;
|
rowEnd = rowEnd !== null && rowEnd !== undefined ? rowEnd : this._terminal.rows - 1;
|
||||||
this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd;
|
// Check whether the row start/end values have already been set
|
||||||
|
const isRowStartSet = this._rowStart !== undefined && this._rowStart !== null;
|
||||||
|
const isRowEndSet = this._rowEnd !== undefined && this._rowEnd !== null;
|
||||||
|
// Set the properties to the updated values
|
||||||
|
this._rowStart = isRowStartSet ? Math.min(this._rowStart, rowStart) : rowStart;
|
||||||
|
this._rowEnd = isRowEndSet ? Math.max(this._rowEnd, rowEnd) : rowEnd;
|
||||||
|
|
||||||
if (this._animationFrame) {
|
if (this._animationFrame) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export class MouseHelper {
|
|||||||
|
|
||||||
public static getCoordsRelativeToElement(event: {pageX: number, pageY: number}, element: HTMLElement): [number, number] {
|
public static getCoordsRelativeToElement(event: {pageX: number, pageY: number}, element: HTMLElement): [number, number] {
|
||||||
// Ignore browsers that don't support MouseEvent.pageX
|
// Ignore browsers that don't support MouseEvent.pageX
|
||||||
if (event.pageX == null) {
|
if (event.pageX === null || event.pageX === undefined) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+20
-16
@@ -4,10 +4,19 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Types';
|
import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Types';
|
||||||
import { LineData, IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ICircularList, ILinkifier, IMouseHelper, ILinkMatcherOptions, XtermListener, CharacterJoinerHandler } from '../Types';
|
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ILinkifier, IMouseHelper, ILinkMatcherOptions, CharacterJoinerHandler, IBufferLine, IBufferStringIterator } from '../Types';
|
||||||
import { Buffer, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from '../Buffer';
|
import { ICircularList, XtermListener } from '../common/Types';
|
||||||
|
import { Buffer } from '../Buffer';
|
||||||
import * as Browser from '../shared/utils/Browser';
|
import * as Browser from '../shared/utils/Browser';
|
||||||
import { ITheme, IDisposable, IMarker } from 'xterm';
|
import { ITheme, IDisposable, IMarker } from 'xterm';
|
||||||
|
import { Terminal } from '../Terminal';
|
||||||
|
|
||||||
|
export class TestTerminal extends Terminal {
|
||||||
|
writeSync(data: string): void {
|
||||||
|
this.writeBuffer.push(data);
|
||||||
|
this._innerWrite();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class MockTerminal implements ITerminal {
|
export class MockTerminal implements ITerminal {
|
||||||
markers: IMarker[];
|
markers: IMarker[];
|
||||||
@@ -145,14 +154,6 @@ export class MockTerminal implements ITerminal {
|
|||||||
refresh(start: number, end: number): void {
|
refresh(start: number, end: number): void {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData {
|
|
||||||
const line: LineData = [];
|
|
||||||
cols = cols || this.cols;
|
|
||||||
for (let i = 0; i < cols; i++) {
|
|
||||||
line.push([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
|
||||||
}
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; }
|
registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; }
|
||||||
deregisterCharacterJoiner(joinerId: number): void { }
|
deregisterCharacterJoiner(joinerId: number): void { }
|
||||||
}
|
}
|
||||||
@@ -228,9 +229,6 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
|
|||||||
eraseLeft(x: number, y: number): void {
|
eraseLeft(x: number, y: number): void {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
blankLine(cur?: boolean, isWrapped?: boolean): [number, string, number, number][] {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
prevStop(x?: number): number {
|
prevStop(x?: number): number {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
@@ -295,7 +293,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
|
|||||||
|
|
||||||
export class MockBuffer implements IBuffer {
|
export class MockBuffer implements IBuffer {
|
||||||
isCursorInViewport: boolean;
|
isCursorInViewport: boolean;
|
||||||
lines: ICircularList<[number, string, number, number][]>;
|
lines: ICircularList<IBufferLine>;
|
||||||
ydisp: number;
|
ydisp: number;
|
||||||
ybase: number;
|
ybase: number;
|
||||||
hasScrollback: boolean;
|
hasScrollback: boolean;
|
||||||
@@ -310,7 +308,7 @@ export class MockBuffer implements IBuffer {
|
|||||||
return Buffer.prototype.translateBufferLineToString.apply(this, arguments);
|
return Buffer.prototype.translateBufferLineToString.apply(this, arguments);
|
||||||
}
|
}
|
||||||
getWrappedRangeForLine(y: number): { first: number; last: number; } {
|
getWrappedRangeForLine(y: number): { first: number; last: number; } {
|
||||||
throw new Error('Method not implemented.');
|
return Buffer.prototype.getWrappedRangeForLine.apply(this, arguments);
|
||||||
}
|
}
|
||||||
nextStop(x?: number): number {
|
nextStop(x?: number): number {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
@@ -318,9 +316,15 @@ export class MockBuffer implements IBuffer {
|
|||||||
prevStop(x?: number): number {
|
prevStop(x?: number): number {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
setLines(lines: ICircularList<[number, string, number, number][]>): void {
|
setLines(lines: ICircularList<IBufferLine>): void {
|
||||||
this.lines = lines;
|
this.lines = lines;
|
||||||
}
|
}
|
||||||
|
stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[] {
|
||||||
|
return Buffer.prototype.stringIndexToBufferIndex.apply(this, arguments);
|
||||||
|
}
|
||||||
|
iterator(trimRight: boolean, startIndex?: number, endIndex?: number): IBufferStringIterator {
|
||||||
|
return Buffer.prototype.iterator.apply(this, arguments);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MockRenderer implements IRenderer {
|
export class MockRenderer implements IRenderer {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"lib": [
|
"lib": [
|
||||||
"dom",
|
"dom",
|
||||||
"es5",
|
"es5",
|
||||||
|
"es6",
|
||||||
"scripthost",
|
"scripthost",
|
||||||
"es2015.promise"
|
"es2015.promise"
|
||||||
],
|
],
|
||||||
|
|||||||
+3
-6
@@ -39,6 +39,7 @@
|
|||||||
"one-variable-per-declaration": true,
|
"one-variable-per-declaration": true,
|
||||||
"no-unsafe-finally": true,
|
"no-unsafe-finally": true,
|
||||||
"no-var-keyword": true,
|
"no-var-keyword": true,
|
||||||
|
"prefer-const": true,
|
||||||
"quotemark": [
|
"quotemark": [
|
||||||
true,
|
true,
|
||||||
"single"
|
"single"
|
||||||
@@ -59,10 +60,7 @@
|
|||||||
"esSpecCompliant": true
|
"esSpecCompliant": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"triple-equals": [
|
"triple-equals": true,
|
||||||
true,
|
|
||||||
"allow-null-check"
|
|
||||||
],
|
|
||||||
"typedef-whitespace": [
|
"typedef-whitespace": [
|
||||||
true,
|
true,
|
||||||
{
|
{
|
||||||
@@ -110,7 +108,6 @@
|
|||||||
},
|
},
|
||||||
"prefer-const-enum": [
|
"prefer-const-enum": [
|
||||||
true
|
true
|
||||||
],
|
]
|
||||||
"prefer-const": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+19
-9
@@ -39,6 +39,16 @@ declare module 'xterm' {
|
|||||||
*/
|
*/
|
||||||
bellStyle?: 'none' /*| 'visual'*/ | 'sound' /*| 'both'*/;
|
bellStyle?: 'none' /*| 'visual'*/ | 'sound' /*| 'both'*/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When enabled the cursor will be set to the beginning of the next line
|
||||||
|
* with every new line. This equivalent to sending '\r\n' for each '\n'.
|
||||||
|
* Normally the termios settings of the underlying PTY deals with the
|
||||||
|
* translation of '\n' to '\r\n' and this setting should not be used. If you
|
||||||
|
* deal with data from a non-PTY related source, this settings might be
|
||||||
|
* useful.
|
||||||
|
*/
|
||||||
|
convertEol?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The number of columns in the terminal.
|
* The number of columns in the terminal.
|
||||||
*/
|
*/
|
||||||
@@ -134,19 +144,13 @@ declare module 'xterm' {
|
|||||||
macOptionClickForcesSelection?: boolean;
|
macOptionClickForcesSelection?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* (EXPERIMENTAL) The type of renderer to use, this allows using the
|
* The type of renderer to use, this allows using the fallback DOM renderer
|
||||||
* fallback DOM renderer when canvas is too slow for the environment. The
|
* when canvas is too slow for the environment. The following features do
|
||||||
* following features do not work when the DOM renderer is used:
|
* not work when the DOM renderer is used:
|
||||||
*
|
*
|
||||||
* - Links
|
|
||||||
* - Line height
|
* - Line height
|
||||||
* - Letter spacing
|
* - Letter spacing
|
||||||
* - Cursor blink
|
* - Cursor blink
|
||||||
* - Cursor style
|
|
||||||
*
|
|
||||||
* This option is marked as experiemental because it will eventually be
|
|
||||||
* moved to an addon. You can only set this option in the constructor (not
|
|
||||||
* setOption).
|
|
||||||
*/
|
*/
|
||||||
rendererType?: RendererType;
|
rendererType?: RendererType;
|
||||||
|
|
||||||
@@ -425,6 +429,12 @@ declare module 'xterm' {
|
|||||||
*/
|
*/
|
||||||
emit(type: string, data?: any): void;
|
emit(type: string, data?: any): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an event listener to the Terminal, returning an IDisposable that can
|
||||||
|
* be used to conveniently remove the event listener.
|
||||||
|
* @param type The type of event.
|
||||||
|
* @param handler The event handler.
|
||||||
|
*/
|
||||||
addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable;
|
addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
entry: './demo/main.js',
|
|
||||||
output: {
|
|
||||||
path: path.resolve(__dirname, 'demo/dist'),
|
|
||||||
filename: 'bundle.js'
|
|
||||||
},
|
|
||||||
devtool: 'source-map',
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.js$/,
|
|
||||||
use: ["source-map-loader"],
|
|
||||||
enforce: "pre"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user