Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27c3663470 | ||
|
|
3a310f1597 | ||
|
|
4d0217f816 | ||
|
|
559e932a2e | ||
|
|
aa427010e1 | ||
|
|
1aec57e8c0 | ||
|
|
33b65bbfaa | ||
|
|
8e39650b5f | ||
|
|
f148238117 | ||
|
|
6057586ff3 | ||
|
|
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 | ||
|
|
86800d2d71 | ||
|
|
41fdc9071a | ||
|
|
14d75364eb | ||
|
|
2127128ef5 | ||
|
|
95ebba6fdb | ||
|
|
ff63d3eb23 | ||
|
|
05b8ef1727 | ||
|
|
458f2d8bf1 | ||
|
|
87a84b608c | ||
|
|
dd7a9c3b7a | ||
|
|
d1ca0d7839 | ||
|
|
3419480b98 | ||
|
|
a003a0c585 | ||
|
|
e3dfe4bc32 | ||
|
|
c9ec7aa215 | ||
|
|
7675be7da3 | ||
|
|
1a03b703f1 | ||
|
|
9165581266 | ||
|
|
6a770296f9 | ||
|
|
cbfb99a5cf | ||
|
|
bae3f64c10 | ||
|
|
7c7102d8a6 | ||
|
|
8e9ba23227 | ||
|
|
1d91e3c5bf | ||
|
|
45d4622920 | ||
|
|
b7da4b6bfe | ||
|
|
fdf51c119b | ||
|
|
6de8c91059 | ||
|
|
490f0ad329 | ||
|
|
882aea357f | ||
|
|
d8eedfa1c7 | ||
|
|
90e57c9b3c | ||
|
|
a269e7f5d9 | ||
|
|
4a3c1a4cc6 | ||
|
|
804cb36adc | ||
|
|
5aaf9ef11b | ||
|
|
c65dd1f476 | ||
|
|
05f136b3da | ||
|
|
1c4cb8c620 | ||
|
|
fec66c7dac | ||
|
|
e82f78fc84 | ||
|
|
67d6ad57c3 | ||
|
|
48f1c4667a | ||
|
|
6a9da97163 | ||
|
|
7b68cb9fe7 | ||
|
|
6691f80906 | ||
|
|
bdfe877c8f | ||
|
|
ee3668eb03 | ||
|
|
4a443c4a45 | ||
|
|
d964c23c43 | ||
|
|
20d4c64ffe | ||
|
|
bf3709f4c5 | ||
|
|
8f22d8dda4 | ||
|
|
0f6a8d3991 | ||
|
|
5b8da8c89a | ||
|
|
f2e5ea5783 | ||
|
|
cb888c343e | ||
|
|
18f37440dc | ||
|
|
3582f8733d | ||
|
|
b84dba2e8d | ||
|
|
61fc4ca8a6 | ||
|
|
bbd234b951 | ||
|
|
0545cfa121 | ||
|
|
26969b725e | ||
|
|
eb31732af0 | ||
|
|
9c08f78017 | ||
|
|
54dcbeb6cf | ||
|
|
a52aab4618 | ||
|
|
5f5a49995a | ||
|
|
ac7239732d | ||
|
|
68d537fb28 | ||
|
|
7ce071c575 | ||
|
|
42f1aba00c | ||
|
|
dfb1b4db95 | ||
|
|
6900bbc07a | ||
|
|
d4b507ddb1 | ||
|
|
1bad71f13a | ||
|
|
63b3a6f323 | ||
|
|
ba7976f3b1 | ||
|
|
b5b350fdcf | ||
|
|
87e292414d | ||
|
|
5e3ebbde5e | ||
|
|
6d9d2a3304 | ||
|
|
eabb25c796 |
-19
@@ -1,19 +0,0 @@
|
||||
language: node_js
|
||||
os:
|
||||
- linux
|
||||
node_js:
|
||||
- 6
|
||||
before_install:
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test ; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get -qq update ; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get -qq install g++-4.8 ; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export CXX=g++-4.8 ; fi
|
||||
- npm install -g npm@5.1.0
|
||||
env:
|
||||
matrix:
|
||||
- NPM_COMMAND=tsc
|
||||
- NPM_COMMAND=lint
|
||||
- NPM_COMMAND=test-coverage
|
||||
notifications:
|
||||
email: false
|
||||
script: npm run $NPM_COMMAND
|
||||
@@ -61,6 +61,7 @@ irokas <akasidiari@inf.uth.gr>
|
||||
Jakob Gillich <jakob@gillich.me>
|
||||
Jan Kuri <jkuri88@gmail.com>
|
||||
Jean Bruenn <himself@jeanbruenn.info>
|
||||
Jeff Principe <princjef@gmail.com>
|
||||
Jeremy Danyow <jedanyow@microsoft.com>
|
||||
Jianhui Zhao <jianhuizhao329@gmail.com>
|
||||
Joao Moreno <jomo@microsoft.com>
|
||||
@@ -70,6 +71,7 @@ Jon Austin <jon.i.austin@gmail.com>
|
||||
Jon Kohler <jon@nutanix.com>
|
||||
Jon Masters <jon.masters@sky.com>
|
||||
Jörg Breitbart <jerch@rockborn.de>
|
||||
Jose Anton <roke22@gmail.com>
|
||||
jpoth <poth.john@gmail.com>
|
||||
Justin Luk <jluk@users.noreply.github.com>
|
||||
Justin Mecham <justin@mecham.me>
|
||||
@@ -103,6 +105,7 @@ Peng Xiao <pengxiao@outlook.com>
|
||||
Peter Baumgarten <me@peterbaumgarten.com>
|
||||
Philip Olson <philip.olson@protonmail.ch>
|
||||
PowerHat <31401273+7PH@users.noreply.github.com>
|
||||
PowerHat <b.raymond@protonmail.com>
|
||||
pro-src <34285059+pro-src@users.noreply.github.com>
|
||||
pro-src <rodneyd.teal@gmail.com>
|
||||
Rick Baker <rick@ricktbaker.com>
|
||||
@@ -111,6 +114,7 @@ Saad Malik <simfox3@gmail.com>
|
||||
Samuel Williams <samuel.williams@oriontransfer.co.nz>
|
||||
Saswat Das <saswatds@users.noreply.github.com>
|
||||
Saul Costa <saul@codevolve.com>
|
||||
Segev Finer <segev208@gmail.com>
|
||||
Shuanglei Tao <tsl0922@gmail.com>
|
||||
sitzmar <sitzmar@gmail.com>
|
||||
Steven Silvester <steven.silvester@ieee.org>
|
||||
|
||||
+2
-5
@@ -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
|
||||
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
|
||||
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)
|
||||
- Fork [xterm.js](https://github.com/sourcelair/xterm.js/)
|
||||
([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
|
||||
- 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).
|
||||
@@ -50,7 +51,3 @@ By contributing code to xterm.js you
|
||||
holder has explicitly granted the right to use it like this,
|
||||
through a compatible open source license or through a direct
|
||||
agreement with you.)
|
||||
|
||||
### Third party dependencies
|
||||
|
||||
We prefer to not include any non-dev third party dependencies in order to keep our code minimal, performant and secure. If you plan on adding a dependency on a third party library it's a good idea to discuss the need in an issue with the maintainers first.
|
||||
|
||||
@@ -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) 2012-2013, Christopher Jeffrey (https://github.com/chjj/)
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# [](https://xtermjs.org)
|
||||
|
||||
[](https://travis-ci.org/xtermjs/xterm.js) [](https://xtermjs.visualstudio.com/xterm.js/_build/index?definitionId=1) [](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://www.jsdelivr.com/package/npm/xterm)
|
||||
[](https://xtermjs.visualstudio.com/xterm.js/_build/index?definitionId=1)
|
||||
[](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://www.jsdelivr.com/package/npm/xterm)
|
||||
|
||||
Xterm.js is a terminal front-end component written in JavaScript that works in the browser.
|
||||
|
||||
@@ -57,6 +60,12 @@ The proposed way to load xterm.js is via the ES6 module syntax.
|
||||
import { Terminal } from 'xterm';
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
The full API for xterm.js is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API.
|
||||
|
||||
Note that some APIs are marked *experimental*, these are added so we can experiment with new ideas without committing to support it like a normal semver API. Note that these APIs can change radically between versions so be sure to read release notes if you plan on using experimental APIs.
|
||||
|
||||
### Addons
|
||||
|
||||
Addons are JavaScript modules that extend the `Terminal` prototype with new methods and attributes to provide additional functionality. There are a handful available in the main repository in the `src/addons` directory and you can even write your own, by using xterm.js' public API.
|
||||
@@ -74,6 +83,8 @@ var xterm = new Terminal(); // Instantiate the terminal
|
||||
xterm.fit(); // Use the `fit` method, provided by the `fit` addon
|
||||
```
|
||||
|
||||
You will also need to include the addon's CSS file if it has one in the folder.
|
||||
|
||||
#### Importing Addons in TypeScript
|
||||
|
||||
There are currently no typings for addons if they are accessed via extending Terminal prototype, so you will need to upcast if using TypeScript, eg. `(<any>xterm).fit()`.
|
||||
@@ -107,6 +118,10 @@ Since xterm.js is typically implemented as a developer tool, only modern browser
|
||||
|
||||
Xterm.js works seamlessly in Electron apps and may even work on earlier versions of the browsers but these are the browsers we strive to keep working.
|
||||
|
||||
## API
|
||||
|
||||
The current full API documentation is available in the [TypeScript declaration file on the repository](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), switch the tag (press `w` when viewing the file) to point at the specific version tag you're using.
|
||||
|
||||
## Real-world uses
|
||||
Xterm.js is used in several world-class applications to provide great terminal experiences.
|
||||
|
||||
@@ -153,105 +168,22 @@ computational environment for Jupyter, supporting interactive data science and s
|
||||
- [**genact**](https://github.com/svenstaro/genact): A nonsense activity generator.
|
||||
- [**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
|
||||
- [**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.
|
||||
|
||||
[And much more...](https://github.com/xtermjs/xterm.js/network/dependents)
|
||||
|
||||
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list. Note: Please add any new contributions to the end of the list only.
|
||||
|
||||
## Demo
|
||||
|
||||
Xterm.js ships with a barebones demo implementation, designed for the development and evaluation of the library only. Exposing the demo to the public as is would introduce security risks for the host.
|
||||
|
||||
Below you can find instructions on how to run the demo on different platforms.
|
||||
|
||||
### SourceLair
|
||||
|
||||
SourceLair will run the demo and builder in parallel automatically. Just make sure to choose the "Node.js" project type, when cloning the xterm.js repo (or just use this shortcut; https://lair.io/xtermjs/xtermjs).
|
||||
|
||||
Then open your project's [Public URL](https://help.sourcelair.com/projects/the-public-url/) to access the demo.
|
||||
|
||||
### Docker
|
||||
|
||||
First, make sure you have Docker Engine 1.13.0 (or newer) and Docker Compose 1.10.0 (or newer).
|
||||
|
||||
Xterm.js [provides a pre-built Docker image](https://hub.docker.com/r/xtermjs/xterm.js/) to help run the demo easily (Git tags are built as [tagged Docker images](https://hub.docker.com/r/xtermjs/xterm.js/tags/) too).
|
||||
|
||||
To run the just demo (with no editing access). run the following command in your terminal:
|
||||
|
||||
```
|
||||
docker run -p 3000:3000 xtermjs/xterm.js
|
||||
```
|
||||
|
||||
Then open http://0.0.0.0:3000 in a web browser to access the demo.
|
||||
|
||||
To run the demo and builder in parallel, run the following command in your terminal:
|
||||
|
||||
```
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
Then open http://0.0.0.0:3000 in a web browser to access the demo. If you prefer a different port than `3000` to access the xterm.js demo, then set the `XTERMJS_PORT` environment variable to the desired port.
|
||||
|
||||
### Foreman (or other Procfile runner)
|
||||
|
||||
First, be sure that a C++ compiler such as GCC-C++ or Clang is installed, then run the following commands in your terminal:
|
||||
|
||||
```
|
||||
npm install
|
||||
foreman start # Replace foreman with "honcho", "forego" etc. depending on your runner
|
||||
```
|
||||
|
||||
Then open http://0.0.0.0:3000 in a web browser to access the demo.
|
||||
|
||||
### Linux or macOS
|
||||
|
||||
First, be sure that a C++ compiler such as GCC-C++ or Clang is installed, then run the following commands in your terminal:
|
||||
|
||||
```
|
||||
npm install
|
||||
npm start # Run this in its own terminal
|
||||
npm run watch # Run this in its own terminal
|
||||
```
|
||||
|
||||
Then open http://0.0.0.0:3000 in a web browser to access the demo.
|
||||
|
||||
### Windows
|
||||
|
||||
First, ensure [node-gyp](https://github.com/nodejs/node-gyp) is installed and configured correctly, then run the following commands in your terminal:
|
||||
|
||||
```
|
||||
npm install
|
||||
npm start # Run this in its own terminal
|
||||
npm run watch # Run this in its own terminal
|
||||
```
|
||||
|
||||
Then open http://127.0.0.1:3000 in a web browser to access the demo.
|
||||
|
||||
*Note: Do not use ConEmu, as it seems to break the demo for some reason.*
|
||||
|
||||
## Testing
|
||||
|
||||
Tests are run using the following npm scripts:
|
||||
|
||||
- `npm test`: This will run both unit tests and the linter
|
||||
- `npm run test-suite <file>`: This will run all tests within a particular file, <file> is the test file name excluding the extension (eg. "Linkifier.test")
|
||||
- `npm run test-debug`: This will run unit tests with `--inspect-brk`, this can then be automatically debugged using [VS Code auto attach](https://code.visualstudio.com/docs/nodejs/nodejs-debugging#_auto-attach-feature) or manually attached to by a debugger
|
||||
- `npm run test-coverage`: This will run all unit tests and produce a coverage report
|
||||
- `npm run lint`: This will run the linter only
|
||||
|
||||
## Releases
|
||||
|
||||
Xterm.js follows a monthly release cycle roughly.
|
||||
|
||||
The existing releases are available at this GitHub repo's [Releases](https://github.com/sourcelair/xterm.js/releases), while the roadmap is available as [Milestones](https://github.com/sourcelair/xterm.js/milestones).
|
||||
|
||||
## Development and Contribution
|
||||
## Contributing
|
||||
|
||||
Xterm.js is maintained by [SourceLair](https://www.sourcelair.com/) and a few external contributors, but we would love to receive contributions from everyone!
|
||||
|
||||
To contribute either code, documentation or issues to xterm.js please read the [Contributing document](CONTRIBUTING.md) beforehand. The development of xterm.js does not require any special tool. All you need is an editor that supports JavaScript/TypeScript and a browser. You will need Node.js installed locally to get all the features working in the demo.
|
||||
|
||||
### Code structure
|
||||
|
||||
`src/` is roughly split up into areas of functionality such as `renderer/` that handles all rendering and `utils/` which provides general utility functions. The `shared/` folder contains code that can be used from either the main thread or a web worker thread, all code inside a `shared/` folder should only ever import other code from a `shared/` folder to minimize the amount of code ran when launching a web worker.
|
||||
You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Contributing) to learn how to contribute and setup xterm.js for development.
|
||||
|
||||
## License Agreement
|
||||
|
||||
|
||||
+3
-4
@@ -174,9 +174,7 @@ function initOptions(term) {
|
||||
'termName',
|
||||
'useFlowControl',
|
||||
// Complex option
|
||||
'theme',
|
||||
// Only in constructor
|
||||
'rendererType'
|
||||
'theme'
|
||||
];
|
||||
var stringOptions = {
|
||||
bellSound: null,
|
||||
@@ -185,7 +183,8 @@ function initOptions(term) {
|
||||
experimentalCharAtlas: ['none', 'static', 'dynamic'],
|
||||
fontFamily: null,
|
||||
fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
|
||||
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']
|
||||
};
|
||||
var options = Object.keys(term._core.options);
|
||||
var booleanOptions = [];
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
],
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5"
|
||||
"target": "es5",
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "xterm",
|
||||
"description": "Full xterm terminal, in your browser",
|
||||
"version": "3.5.1",
|
||||
"version": "3.7.0",
|
||||
"main": "lib/public/Terminal.js",
|
||||
"types": "typings/xterm.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
@@ -35,6 +35,7 @@
|
||||
"npm-run-all": "^4.1.2",
|
||||
"nyc": "^11.8.0",
|
||||
"sorcery": "^0.10.0",
|
||||
"source-map-loader": "^0.2.3",
|
||||
"tslint": "^5.9.1",
|
||||
"tslint-consistent-codestyle": "^1.13.0",
|
||||
"typescript": "2.8.3",
|
||||
|
||||
@@ -72,7 +72,7 @@ export class AccessibilityManager extends Disposable {
|
||||
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);
|
||||
|
||||
this.register(this._renderRowsDebouncer);
|
||||
this.register(this._terminal.addDisposableListener('resize', data => this._onResize(data.cols, data.rows)));
|
||||
this.register(this._terminal.addDisposableListener('resize', data => this._onResize(data.rows)));
|
||||
this.register(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end)));
|
||||
this.register(this._terminal.addDisposableListener('scroll', data => this._refreshRows()));
|
||||
// Line feed is an issue as the prompt won't be read out after a command is run
|
||||
@@ -157,7 +157,7 @@ export class AccessibilityManager extends Disposable {
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
private _onResize(cols: number, rows: number): void {
|
||||
private _onResize(rows: number): void {
|
||||
// Remove bottom boundary listener
|
||||
this._rowElements[this._rowElements.length - 1].removeEventListener('focus', this._bottomBoundaryFocusListener);
|
||||
|
||||
@@ -259,6 +259,9 @@ export class AccessibilityManager extends Disposable {
|
||||
if (!this._terminal.renderer.dimensions.actualCellHeight) {
|
||||
return;
|
||||
}
|
||||
if (this._rowElements.length !== this._terminal.rows) {
|
||||
this._onResize(this._terminal.rows);
|
||||
}
|
||||
for (let i = 0; i < this._terminal.rows; i++) {
|
||||
this._refreshRowDimensions(this._rowElements[i]);
|
||||
}
|
||||
|
||||
+49
-48
@@ -5,9 +5,10 @@
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { ITerminal } from './Types';
|
||||
import { Buffer } from './Buffer';
|
||||
import { Buffer, DEFAULT_ATTR } from './Buffer';
|
||||
import { CircularList } from './common/CircularList';
|
||||
import { MockTerminal } from './utils/TestUtils.test';
|
||||
import { BufferLine } from './BufferLine';
|
||||
|
||||
const INIT_COLS = 80;
|
||||
const INIT_ROWS = 24;
|
||||
@@ -36,13 +37,13 @@ describe('Buffer', () => {
|
||||
|
||||
describe('fillViewportRows', () => {
|
||||
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();
|
||||
assert.equal(buffer.lines.length, INIT_ROWS);
|
||||
for (let y = 0; y < INIT_ROWS; y++) {
|
||||
assert.equal(buffer.lines.get(y).length, INIT_COLS);
|
||||
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', () => {
|
||||
it('should return a range for the first row', () => {
|
||||
buffer.fillViewportRows();
|
||||
(<any> buffer.lines.get(1)).isWrapped = true;
|
||||
buffer.lines.get(1).isWrapped = true;
|
||||
assert.deepEqual(buffer.getWrappedRangeForLine(0), { first: 0, last: 1 });
|
||||
});
|
||||
it('should return a range for a middle row wrapping upwards', () => {
|
||||
buffer.fillViewportRows();
|
||||
(<any> buffer.lines.get(12)).isWrapped = true;
|
||||
buffer.lines.get(12).isWrapped = true;
|
||||
assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 11, last: 12 });
|
||||
});
|
||||
it('should return a range for a middle row wrapping downwards', () => {
|
||||
buffer.fillViewportRows();
|
||||
(<any> buffer.lines.get(13)).isWrapped = true;
|
||||
buffer.lines.get(13).isWrapped = true;
|
||||
assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 12, last: 13 });
|
||||
});
|
||||
it('should return a range for a middle row wrapping both ways', () => {
|
||||
buffer.fillViewportRows();
|
||||
(<any> buffer.lines.get(11)).isWrapped = true;
|
||||
(<any> buffer.lines.get(12)).isWrapped = true;
|
||||
(<any> buffer.lines.get(13)).isWrapped = true;
|
||||
(<any> buffer.lines.get(14)).isWrapped = true;
|
||||
buffer.lines.get(11).isWrapped = true;
|
||||
buffer.lines.get(12).isWrapped = true;
|
||||
buffer.lines.get(13).isWrapped = true;
|
||||
buffer.lines.get(14).isWrapped = true;
|
||||
assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 10, last: 14 });
|
||||
});
|
||||
it('should return a range for the last row', () => {
|
||||
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 });
|
||||
});
|
||||
it('should return a range for a row that wraps upward to first row', () => {
|
||||
buffer.fillViewportRows();
|
||||
(<any> buffer.lines.get(1)).isWrapped = true;
|
||||
buffer.lines.get(1).isWrapped = true;
|
||||
assert.deepEqual(buffer.getWrappedRangeForLine(1), { first: 0, last: 1 });
|
||||
});
|
||||
it('should return a range for a row that wraps downward to last row', () => {
|
||||
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 });
|
||||
});
|
||||
});
|
||||
@@ -154,11 +155,11 @@ describe('Buffer', () => {
|
||||
assert.equal(buffer.lines.maxLength, INIT_ROWS);
|
||||
buffer.y = INIT_ROWS - 1;
|
||||
buffer.fillViewportRows();
|
||||
buffer.lines.get(5)[0][1] = 'a';
|
||||
buffer.lines.get(INIT_ROWS - 1)[0][1] = 'b';
|
||||
buffer.lines.get(5).get(0)[1] = 'a';
|
||||
buffer.lines.get(INIT_ROWS - 1).get(0)[1] = 'b';
|
||||
buffer.resize(INIT_COLS, INIT_ROWS - 5);
|
||||
assert.equal(buffer.lines.get(0)[0][1], 'a');
|
||||
assert.equal(buffer.lines.get(INIT_ROWS - 1 - 5)[0][1], 'b');
|
||||
assert.equal(buffer.lines.get(0).get(0)[1], 'a');
|
||||
assert.equal(buffer.lines.get(INIT_ROWS - 1 - 5).get(0)[1], 'b');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -179,7 +180,7 @@ describe('Buffer', () => {
|
||||
buffer.fillViewportRows();
|
||||
// Create 10 extra blank lines
|
||||
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
|
||||
buffer.y = INIT_ROWS - 1;
|
||||
@@ -199,7 +200,7 @@ describe('Buffer', () => {
|
||||
buffer.fillViewportRows();
|
||||
// Create 10 extra blank lines
|
||||
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
|
||||
buffer.y = INIT_ROWS - 1;
|
||||
@@ -272,34 +273,34 @@ describe('Buffer', () => {
|
||||
|
||||
describe ('translateBufferLineToString', () => {
|
||||
it('should handle selecting a section of ascii text', () => {
|
||||
buffer.lines.set(0, [
|
||||
[ null, 'a', 1, 'a'.charCodeAt(0)],
|
||||
[ null, 'b', 1, 'b'.charCodeAt(0)],
|
||||
[ null, 'c', 1, 'c'.charCodeAt(0)],
|
||||
[ null, 'd', 1, 'd'.charCodeAt(0)]
|
||||
]);
|
||||
const line = new BufferLine();
|
||||
line.push([ null, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.push([ null, 'b', 1, 'b'.charCodeAt(0)]);
|
||||
line.push([ null, 'c', 1, 'c'.charCodeAt(0)]);
|
||||
line.push([ null, 'd', 1, 'd'.charCodeAt(0)]);
|
||||
buffer.lines.set(0, line);
|
||||
|
||||
const str = buffer.translateBufferLineToString(0, true, 0, 2);
|
||||
assert.equal(str, 'ab');
|
||||
});
|
||||
|
||||
it('should handle a cut-off double width character by including it', () => {
|
||||
buffer.lines.set(0, [
|
||||
[ null, '語', 2, 35486 ],
|
||||
[ null, '', 0, null],
|
||||
[ null, 'a', 1, 'a'.charCodeAt(0)]
|
||||
]);
|
||||
const line = new BufferLine();
|
||||
line.push([ null, '語', 2, 35486 ]);
|
||||
line.push([ null, '', 0, null]);
|
||||
line.push([ null, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
buffer.lines.set(0, line);
|
||||
|
||||
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
|
||||
assert.equal(str1, '語');
|
||||
});
|
||||
|
||||
it('should handle a zero width character in the middle of the string by not including it', () => {
|
||||
buffer.lines.set(0, [
|
||||
[ null, '語', 2, '語'.charCodeAt(0) ],
|
||||
[ null, '', 0, null],
|
||||
[ null, 'a', 1, 'a'.charCodeAt(0)]
|
||||
]);
|
||||
const line = new BufferLine();
|
||||
line.push([ null, '語', 2, '語'.charCodeAt(0) ]);
|
||||
line.push([ null, '', 0, null]);
|
||||
line.push([ null, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
buffer.lines.set(0, line);
|
||||
|
||||
const str0 = buffer.translateBufferLineToString(0, true, 0, 1);
|
||||
assert.equal(str0, '語');
|
||||
@@ -312,10 +313,10 @@ describe('Buffer', () => {
|
||||
});
|
||||
|
||||
it('should handle single width emojis', () => {
|
||||
buffer.lines.set(0, [
|
||||
[ null, '😁', 1, '😁'.charCodeAt(0) ],
|
||||
[ null, 'a', 1, 'a'.charCodeAt(0)]
|
||||
]);
|
||||
const line = new BufferLine();
|
||||
line.push([ null, '😁', 1, '😁'.charCodeAt(0) ]);
|
||||
line.push([ null, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
buffer.lines.set(0, line);
|
||||
|
||||
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
|
||||
assert.equal(str1, '😁');
|
||||
@@ -325,10 +326,10 @@ describe('Buffer', () => {
|
||||
});
|
||||
|
||||
it('should handle double width emojis', () => {
|
||||
buffer.lines.set(0, [
|
||||
[ null, '😁', 2, '😁'.charCodeAt(0) ],
|
||||
[ null, '', 0, null]
|
||||
]);
|
||||
const line = new BufferLine();
|
||||
line.push([ null, '😁', 2, '😁'.charCodeAt(0) ]);
|
||||
line.push([ null, '', 0, null]);
|
||||
buffer.lines.set(0, line);
|
||||
|
||||
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
|
||||
assert.equal(str1, '😁');
|
||||
@@ -336,11 +337,11 @@ describe('Buffer', () => {
|
||||
const str2 = buffer.translateBufferLineToString(0, true, 0, 2);
|
||||
assert.equal(str2, '😁');
|
||||
|
||||
buffer.lines.set(0, [
|
||||
[ null, '😁', 2, '😁'.charCodeAt(0) ],
|
||||
[ null, '', 0, null],
|
||||
[ null, 'a', 1, 'a'.charCodeAt(0)]
|
||||
]);
|
||||
const line2 = new BufferLine();
|
||||
line2.push([ null, '😁', 2, '😁'.charCodeAt(0) ]);
|
||||
line2.push([ null, '', 0, null]);
|
||||
line2.push([ null, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
buffer.lines.set(0, line2);
|
||||
|
||||
const str3 = buffer.translateBufferLineToString(0, true, 0, 3);
|
||||
assert.equal(str3, '😁a');
|
||||
|
||||
+17
-12
@@ -4,9 +4,10 @@
|
||||
*/
|
||||
|
||||
import { CircularList } from './common/CircularList';
|
||||
import { LineData, CharData, ITerminal, IBuffer } from './Types';
|
||||
import { CharData, ITerminal, IBuffer, IBufferLine } from './Types';
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
import { IMarker } from 'xterm';
|
||||
import { BufferLine } from './BufferLine';
|
||||
|
||||
export const DEFAULT_ATTR = (0 << 18) | (257 << 9) | (256 << 0);
|
||||
export const CHAR_DATA_ATTR_INDEX = 0;
|
||||
@@ -15,6 +16,10 @@ export const CHAR_DATA_WIDTH_INDEX = 2;
|
||||
export const CHAR_DATA_CODE_INDEX = 3;
|
||||
export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1
|
||||
|
||||
export const NULL_CELL_CHAR = ' ';
|
||||
export const NULL_CELL_WIDTH = 1;
|
||||
export const NULL_CELL_CODE = 32;
|
||||
|
||||
/**
|
||||
* This class represents a terminal buffer (an internal state of the terminal), where the
|
||||
* following information is stored (in high-level):
|
||||
@@ -23,7 +28,7 @@ export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1
|
||||
* - scroll position
|
||||
*/
|
||||
export class Buffer implements IBuffer {
|
||||
public lines: CircularList<LineData>;
|
||||
public lines: CircularList<IBufferLine>;
|
||||
public ydisp: number;
|
||||
public ybase: number;
|
||||
public y: number;
|
||||
@@ -80,7 +85,7 @@ export class Buffer implements IBuffer {
|
||||
if (this.lines.length === 0) {
|
||||
let i = this._terminal.rows;
|
||||
while (i--) {
|
||||
this.lines.push(this._terminal.blankLine());
|
||||
this.lines.push(BufferLine.blankLine(this._terminal.cols, DEFAULT_ATTR));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,7 +98,7 @@ export class Buffer implements IBuffer {
|
||||
this.ybase = 0;
|
||||
this.y = 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.scrollBottom = this._terminal.rows - 1;
|
||||
this.setupTabStops();
|
||||
@@ -117,7 +122,7 @@ export class Buffer implements IBuffer {
|
||||
if (this.lines.length > 0) {
|
||||
// Deal with columns increasing (we don't do anything when columns reduce)
|
||||
if (this._terminal.cols < newCols) {
|
||||
const ch: CharData = [DEFAULT_ATTR, ' ', 1, 32]; // does xterm use the default attr?
|
||||
const ch: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // does xterm use the default attr?
|
||||
for (let i = 0; i < this.lines.length; i++) {
|
||||
while (this.lines.get(i).length < newCols) {
|
||||
this.lines.get(i).push(ch);
|
||||
@@ -142,7 +147,7 @@ export class Buffer implements IBuffer {
|
||||
} else {
|
||||
// Add a blank line if there is no buffer left at the top to scroll to, or if there
|
||||
// are blank lines after the cursor
|
||||
this.lines.push(this._terminal.blankLine(undefined, undefined, newCols));
|
||||
this.lines.push(BufferLine.blankLine(newCols, DEFAULT_ATTR));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -219,7 +224,7 @@ export class Buffer implements IBuffer {
|
||||
let endIndex = endCol;
|
||||
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const char = line[i];
|
||||
const char = line.get(i);
|
||||
lineString += char[CHAR_DATA_CHAR_INDEX];
|
||||
// Adjust start and end cols for wide characters if they affect their
|
||||
// column indexes
|
||||
@@ -264,11 +269,11 @@ export class Buffer implements IBuffer {
|
||||
let first = y;
|
||||
let last = y;
|
||||
// Scan upwards for wrapped lines
|
||||
while (first > 0 && (<any>this.lines.get(first)).isWrapped) {
|
||||
while (first > 0 && this.lines.get(first).isWrapped) {
|
||||
first--;
|
||||
}
|
||||
// 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++;
|
||||
}
|
||||
return { first, last };
|
||||
@@ -279,7 +284,7 @@ export class Buffer implements IBuffer {
|
||||
* @param i The index to start setting up tab stops from.
|
||||
*/
|
||||
public setupTabStops(i?: number): void {
|
||||
if (i != null) {
|
||||
if (i !== null && i !== undefined) {
|
||||
if (!this.tabs[i]) {
|
||||
i = this.prevStop(i);
|
||||
}
|
||||
@@ -298,7 +303,7 @@ export class Buffer implements IBuffer {
|
||||
* @param x The position to move the cursor to the previous tab stop.
|
||||
*/
|
||||
public prevStop(x?: number): number {
|
||||
if (x == null) {
|
||||
if (x === null || x === undefined) {
|
||||
x = this.x;
|
||||
}
|
||||
while (!this.tabs[--x] && x > 0);
|
||||
@@ -310,7 +315,7 @@ export class Buffer implements IBuffer {
|
||||
* @param x The position to move the cursor one tab stop forward.
|
||||
*/
|
||||
public nextStop(x?: number): number {
|
||||
if (x == null) {
|
||||
if (x === null || x === undefined) {
|
||||
x = this.x;
|
||||
}
|
||||
while (!this.tabs[++x] && x < this._terminal.cols);
|
||||
|
||||
@@ -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
-4
@@ -18,7 +18,7 @@ export class BufferSet extends EventEmitter implements IBufferSet {
|
||||
|
||||
/**
|
||||
* Create a new BufferSet for the given terminal.
|
||||
* @param {Terminal} terminal - The terminal the BufferSet will belong to
|
||||
* @param _terminal - The terminal the BufferSet will belong to
|
||||
*/
|
||||
constructor(private _terminal: ITerminal) {
|
||||
super();
|
||||
@@ -35,7 +35,6 @@ export class BufferSet extends EventEmitter implements IBufferSet {
|
||||
|
||||
/**
|
||||
* Returns the alt Buffer of the BufferSet
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
public get alt(): Buffer {
|
||||
return this._alt;
|
||||
@@ -43,7 +42,6 @@ export class BufferSet extends EventEmitter implements IBufferSet {
|
||||
|
||||
/**
|
||||
* Returns the normal Buffer of the BufferSet
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
public get active(): Buffer {
|
||||
return this._activeBuffer;
|
||||
@@ -51,7 +49,6 @@ export class BufferSet extends EventEmitter implements IBufferSet {
|
||||
|
||||
/**
|
||||
* Returns the currently active Buffer of the BufferSet
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
public get normal(): Buffer {
|
||||
return this._normal;
|
||||
|
||||
@@ -61,7 +61,7 @@ export class CompositionHelper {
|
||||
|
||||
/**
|
||||
* Handles the compositionupdate event, updating the composition view.
|
||||
* @param {CompositionEvent} ev The event.
|
||||
* @param ev The event.
|
||||
*/
|
||||
public compositionupdate(ev: CompositionEvent): void {
|
||||
this._compositionView.textContent = ev.data;
|
||||
|
||||
@@ -375,8 +375,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
// shortcut for most chars (print action)
|
||||
if (currentState === ParserState.GROUND && code > 0x1f && code < 0x80) {
|
||||
print = (~print) ? print : i;
|
||||
do code = data.charCodeAt(++i);
|
||||
while (i < l && code > 0x1f && code < 0x80);
|
||||
do i++;
|
||||
while (i < l && data.charCodeAt(i) > 0x1f && data.charCodeAt(i) < 0x80);
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
|
||||
+392
-1
@@ -3,9 +3,102 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { assert, expect } from 'chai';
|
||||
import { InputHandler } from './InputHandler';
|
||||
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('save and restore cursor', () => {
|
||||
@@ -84,4 +177,302 @@ describe('InputHandler', () => {
|
||||
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 ');
|
||||
});
|
||||
});
|
||||
|
||||
+120
-117
@@ -4,15 +4,16 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { CharData, IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer } from './Types';
|
||||
import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types';
|
||||
import { C0, C1 } from './common/data/EscapeSequences';
|
||||
import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets';
|
||||
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR } 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';
|
||||
import { FLAGS } from './renderer/Types';
|
||||
import { wcwidth } from './CharWidth';
|
||||
import { EscapeSequenceParser } from './EscapeSequenceParser';
|
||||
import { ICharset } from './core/Types';
|
||||
import { Disposable } from './common/Lifecycle';
|
||||
import { BufferLine } from './BufferLine';
|
||||
|
||||
/**
|
||||
* Map collect to glevel. Used in `selectCharset`.
|
||||
@@ -40,7 +41,7 @@ class RequestTerminfo implements IDcsHandler {
|
||||
}
|
||||
unhook(): void {
|
||||
// invalid: DCS 0 + r Pt ST
|
||||
this._terminal.send(`${C0.ESC}P0+r${this._data}${C0.ESC}\\`);
|
||||
this._terminal.handler(`${C0.ESC}P0+r${this._data}${C0.ESC}\\`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,25 +69,25 @@ class DECRQSS implements IDcsHandler {
|
||||
switch (this._data) {
|
||||
// valid: DCS 1 $ r Pt ST (xterm)
|
||||
case '"q': // DECSCA
|
||||
return this._terminal.send(`${C0.ESC}P1$r0"q${C0.ESC}\\`);
|
||||
return this._terminal.handler(`${C0.ESC}P1$r0"q${C0.ESC}\\`);
|
||||
case '"p': // DECSCL
|
||||
return this._terminal.send(`${C0.ESC}P1$r61"p${C0.ESC}\\`);
|
||||
return this._terminal.handler(`${C0.ESC}P1$r61"p${C0.ESC}\\`);
|
||||
case 'r': // DECSTBM
|
||||
const pt = '' + (this._terminal.buffer.scrollTop + 1) +
|
||||
';' + (this._terminal.buffer.scrollBottom + 1) + 'r';
|
||||
return this._terminal.send(`${C0.ESC}P1$r${pt}${C0.ESC}\\`);
|
||||
return this._terminal.handler(`${C0.ESC}P1$r${pt}${C0.ESC}\\`);
|
||||
case 'm': // SGR
|
||||
// TODO: report real settings instead of 0m
|
||||
return this._terminal.send(`${C0.ESC}P1$r0m${C0.ESC}\\`);
|
||||
return this._terminal.handler(`${C0.ESC}P1$r0m${C0.ESC}\\`);
|
||||
case ' q': // DECSCUSR
|
||||
const STYLES: {[key: string]: number} = {'block': 2, 'underline': 4, 'bar': 6};
|
||||
let style = STYLES[this._terminal.getOption('cursorStyle')];
|
||||
style -= this._terminal.getOption('cursorBlink');
|
||||
return this._terminal.send(`${C0.ESC}P1$r${style} q${C0.ESC}\\`);
|
||||
return this._terminal.handler(`${C0.ESC}P1$r${style} q${C0.ESC}\\`);
|
||||
default:
|
||||
// invalid: DCS 0 $ r Pt ST (xterm)
|
||||
this._terminal.error('Unknown DCS $q %s', this._data);
|
||||
this._terminal.send(`${C0.ESC}P0$r${this._data}${C0.ESC}\\`);
|
||||
this._terminal.handler(`${C0.ESC}P0$r${this._data}${C0.ESC}\\`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,7 +117,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
private _surrogateHigh: string;
|
||||
|
||||
constructor(
|
||||
private _terminal: any, // TODO: reestablish IInputHandlingTerminal here
|
||||
protected _terminal: IInputHandlingTerminal,
|
||||
private _parser: IEscapeSequenceParser = new EscapeSequenceParser())
|
||||
{
|
||||
super();
|
||||
@@ -129,16 +130,16 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
* custom fallback handlers
|
||||
*/
|
||||
this._parser.setCsiHandlerFallback((collect: string, params: number[], flag: number) => {
|
||||
this._terminal.error('Unknown CSI code: ', collect, params, String.fromCharCode(flag));
|
||||
this._terminal.error('Unknown CSI code: ', { collect, params, flag: String.fromCharCode(flag) });
|
||||
});
|
||||
this._parser.setEscHandlerFallback((collect: string, flag: number) => {
|
||||
this._terminal.error('Unknown ESC code: ', collect, String.fromCharCode(flag));
|
||||
this._terminal.error('Unknown ESC code: ', { collect, flag: String.fromCharCode(flag) });
|
||||
});
|
||||
this._parser.setExecuteHandlerFallback((code: number) => {
|
||||
this._terminal.error('Unknown EXECUTE code: ', code);
|
||||
this._terminal.error('Unknown EXECUTE code: ', { code });
|
||||
});
|
||||
this._parser.setOscHandlerFallback((identifier: number, data: string) => {
|
||||
this._terminal.error('Unknown OSC code: ', identifier, data);
|
||||
this._terminal.error('Unknown OSC code: ', { identifier, data });
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -296,10 +297,17 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
}
|
||||
|
||||
public parse(data: string): void {
|
||||
// Ensure the terminal is not disposed
|
||||
if (!this._terminal) {
|
||||
return;
|
||||
}
|
||||
|
||||
let buffer = this._terminal.buffer;
|
||||
const cursorStartX = buffer.x;
|
||||
const cursorStartY = buffer.y;
|
||||
if (this._terminal.debug) {
|
||||
|
||||
// TODO: Consolidate debug/logging #1560
|
||||
if ((<any>this._terminal).debug) {
|
||||
this._terminal.log('data: ' + data);
|
||||
}
|
||||
|
||||
@@ -374,18 +382,20 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
// since they always follow a cell consuming char
|
||||
// therefore we can test for buffer.x to avoid overflow left
|
||||
if (!chWidth && buffer.x) {
|
||||
if (bufferRow[buffer.x - 1]) {
|
||||
if (!bufferRow[buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) {
|
||||
const chMinusOne = bufferRow.get(buffer.x - 1);
|
||||
if (chMinusOne) {
|
||||
if (!chMinusOne[CHAR_DATA_WIDTH_INDEX]) {
|
||||
// found empty cell after fullwidth, need to go 2 cells back
|
||||
// it is save to step 2 cells back here
|
||||
// since an empty cell is only set by fullwidth chars
|
||||
if (bufferRow[buffer.x - 2]) {
|
||||
bufferRow[buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char;
|
||||
bufferRow[buffer.x - 2][CHAR_DATA_CODE_INDEX] = code;
|
||||
const chMinusTwo = bufferRow.get(buffer.x - 2);
|
||||
if (chMinusTwo) {
|
||||
chMinusTwo[CHAR_DATA_CHAR_INDEX] += char;
|
||||
chMinusTwo[CHAR_DATA_CODE_INDEX] = code;
|
||||
}
|
||||
} else {
|
||||
bufferRow[buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char;
|
||||
bufferRow[buffer.x - 1][CHAR_DATA_CODE_INDEX] = code;
|
||||
chMinusOne[CHAR_DATA_CHAR_INDEX] += char;
|
||||
chMinusOne[CHAR_DATA_CODE_INDEX] = code;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
@@ -405,7 +415,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
} else {
|
||||
// The line already exists (eg. the initial viewport), mark it as a
|
||||
// wrapped line
|
||||
(<any>buffer.lines.get(buffer.y)).isWrapped = true;
|
||||
buffer.lines.get(buffer.y).isWrapped = true;
|
||||
}
|
||||
// row changed, get it again
|
||||
bufferRow = buffer.lines.get(buffer.y + buffer.ybase);
|
||||
@@ -428,23 +438,24 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
// remove last cell
|
||||
// if it's width is 0, we have to adjust the second last cell as well
|
||||
const removed = bufferRow.pop();
|
||||
const chMinusTwo = bufferRow.get(buffer.x - 2);
|
||||
if (removed[CHAR_DATA_WIDTH_INDEX] === 0
|
||||
&& bufferRow[this._terminal.cols - 2]
|
||||
&& bufferRow[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) {
|
||||
bufferRow[this._terminal.cols - 2] = [curAttr, ' ', 1, 32 /* ' '.charCodeAt(0) */ ];
|
||||
&& chMinusTwo
|
||||
&& chMinusTwo[CHAR_DATA_WIDTH_INDEX] === 2) {
|
||||
bufferRow.set(this._terminal.cols - 2, [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
}
|
||||
|
||||
// insert empty cell at cursor
|
||||
bufferRow.splice(buffer.x, 0, [curAttr, ' ', 1, 32 /* ' '.charCodeAt(0) */ ]);
|
||||
bufferRow.splice(buffer.x, 0, [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
if (chWidth === 2) {
|
||||
bufferRow[buffer.x++] = [curAttr, '', 0, undefined];
|
||||
bufferRow.set(buffer.x++, [curAttr, '', 0, undefined]);
|
||||
}
|
||||
}
|
||||
this._terminal.updateRange(buffer.y);
|
||||
@@ -466,7 +477,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
// make buffer local for faster access
|
||||
const buffer = this._terminal.buffer;
|
||||
|
||||
if (this._terminal.convertEol) {
|
||||
if (this._terminal.options.convertEol) {
|
||||
buffer.x = 0;
|
||||
}
|
||||
buffer.y++;
|
||||
@@ -539,20 +550,12 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
* Insert Ps (Blank) Character(s) (default = 1) (ICH).
|
||||
*/
|
||||
public insertChars(params: number[]): void {
|
||||
let param = params[0];
|
||||
if (param < 1) param = 1;
|
||||
|
||||
// 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(), ' ', 1, 32]; // xterm
|
||||
|
||||
while (param-- && j < this._terminal.cols) {
|
||||
buffer.lines.get(row).splice(j++, 0, ch);
|
||||
buffer.lines.get(row).pop();
|
||||
}
|
||||
this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).insertCells(
|
||||
this._terminal.buffer.x,
|
||||
params[0] || 1,
|
||||
[this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
|
||||
);
|
||||
this._terminal.updateRange(this._terminal.buffer.y);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -712,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).
|
||||
* Ps = 0 -> Erase Below (default).
|
||||
@@ -728,22 +746,30 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
let j;
|
||||
switch (params[0]) {
|
||||
case 0:
|
||||
this._terminal.eraseRight(this._terminal.buffer.x, this._terminal.buffer.y);
|
||||
j = this._terminal.buffer.y + 1;
|
||||
j = this._terminal.buffer.y;
|
||||
this._terminal.updateRange(j);
|
||||
this._eraseInBufferLine(j++, this._terminal.buffer.x, this._terminal.cols);
|
||||
for (; j < this._terminal.rows; j++) {
|
||||
this._terminal.eraseLine(j);
|
||||
this._eraseInBufferLine(j, 0, this._terminal.cols);
|
||||
}
|
||||
this._terminal.updateRange(j);
|
||||
break;
|
||||
case 1:
|
||||
this._terminal.eraseLeft(this._terminal.buffer.x, this._terminal.buffer.y);
|
||||
j = this._terminal.buffer.y;
|
||||
this._terminal.updateRange(j);
|
||||
this._eraseInBufferLine(j, 0, this._terminal.buffer.x + 1);
|
||||
while (j--) {
|
||||
this._terminal.eraseLine(j);
|
||||
this._eraseInBufferLine(j, 0, this._terminal.cols);
|
||||
}
|
||||
this._terminal.updateRange(0);
|
||||
break;
|
||||
case 2:
|
||||
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;
|
||||
case 3:
|
||||
// Clear scrollback (everything not in viewport)
|
||||
@@ -773,15 +799,16 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
public eraseInLine(params: number[]): void {
|
||||
switch (params[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;
|
||||
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;
|
||||
case 2:
|
||||
this._terminal.eraseLine(this._terminal.buffer.y);
|
||||
this._eraseInBufferLine(this._terminal.buffer.y, 0, this._terminal.cols);
|
||||
break;
|
||||
}
|
||||
this._terminal.updateRange(this._terminal.buffer.y);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -805,7 +832,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
// test: echo -e '\e[44m\e[1L\e[0m'
|
||||
// blankLine(true) - xterm/linux behavior
|
||||
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();
|
||||
@@ -835,7 +862,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
// test: echo -e '\e[44m\e[1M\e[0m'
|
||||
// blankLine(true) - xterm/linux behavior
|
||||
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();
|
||||
@@ -848,22 +875,12 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
* Delete Ps Character(s) (default = 1) (DCH).
|
||||
*/
|
||||
public deleteChars(params: number[]): void {
|
||||
let param: number = params[0];
|
||||
if (param < 1) {
|
||||
param = 1;
|
||||
}
|
||||
|
||||
// make buffer local for faster access
|
||||
const buffer = this._terminal.buffer;
|
||||
|
||||
const row = buffer.y + buffer.ybase;
|
||||
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm
|
||||
|
||||
while (param--) {
|
||||
buffer.lines.get(row).splice(buffer.x, 1);
|
||||
buffer.lines.get(row).push(ch);
|
||||
}
|
||||
this._terminal.updateRange(buffer.y);
|
||||
this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).deleteCells(
|
||||
this._terminal.buffer.x,
|
||||
params[0] || 1,
|
||||
[this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
|
||||
);
|
||||
this._terminal.updateRange(this._terminal.buffer.y);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -877,7 +894,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
|
||||
while (param--) {
|
||||
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._terminal.updateRange(buffer.scrollTop);
|
||||
@@ -896,7 +913,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
|
||||
while (param--) {
|
||||
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._terminal.updateRange(buffer.scrollTop);
|
||||
@@ -909,21 +926,11 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
* Erase Ps Character(s) (default = 1) (ECH).
|
||||
*/
|
||||
public eraseChars(params: number[]): void {
|
||||
let param = params[0];
|
||||
if (param < 1) {
|
||||
param = 1;
|
||||
}
|
||||
|
||||
// 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(), ' ', 1, 32]; // xterm
|
||||
|
||||
while (param-- && j < this._terminal.cols) {
|
||||
buffer.lines.get(row)[j++] = ch;
|
||||
}
|
||||
this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).replaceCells(
|
||||
this._terminal.buffer.x,
|
||||
this._terminal.buffer.x + (params[0] || 1),
|
||||
[this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -975,17 +982,14 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
* CSI Ps b Repeat the preceding graphic character Ps times (REP).
|
||||
*/
|
||||
public repeatPrecedingCharacter(params: number[]): void {
|
||||
let param = params[0] || 1;
|
||||
|
||||
// make buffer local for faster access
|
||||
const buffer = this._terminal.buffer;
|
||||
|
||||
const line = buffer.lines.get(buffer.ybase + buffer.y);
|
||||
const ch = line[buffer.x - 1] || [DEFAULT_ATTR, ' ', 1, 32];
|
||||
|
||||
while (param--) {
|
||||
line[buffer.x++] = ch;
|
||||
}
|
||||
line.replaceCells(buffer.x,
|
||||
buffer.x + (params[0] || 1),
|
||||
line.get(buffer.x - 1) || [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
|
||||
);
|
||||
// FIXME: no updateRange here?
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1032,24 +1036,24 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
|
||||
if (!collect) {
|
||||
if (this._terminal.is('xterm') || this._terminal.is('rxvt-unicode') || this._terminal.is('screen')) {
|
||||
this._terminal.send(C0.ESC + '[?1;2c');
|
||||
this._terminal.handler(C0.ESC + '[?1;2c');
|
||||
} else if (this._terminal.is('linux')) {
|
||||
this._terminal.send(C0.ESC + '[?6c');
|
||||
this._terminal.handler(C0.ESC + '[?6c');
|
||||
}
|
||||
} else if (collect === '>') {
|
||||
// xterm and urxvt
|
||||
// seem to spit this
|
||||
// out around ~370 times (?).
|
||||
if (this._terminal.is('xterm')) {
|
||||
this._terminal.send(C0.ESC + '[>0;276;0c');
|
||||
this._terminal.handler(C0.ESC + '[>0;276;0c');
|
||||
} else if (this._terminal.is('rxvt-unicode')) {
|
||||
this._terminal.send(C0.ESC + '[>85;95;0c');
|
||||
this._terminal.handler(C0.ESC + '[>85;95;0c');
|
||||
} else if (this._terminal.is('linux')) {
|
||||
// not supported by linux console.
|
||||
// linux console echoes parameters.
|
||||
this._terminal.send(params[0] + 'c');
|
||||
this._terminal.handler(params[0] + 'c');
|
||||
} else if (this._terminal.is('screen')) {
|
||||
this._terminal.send(C0.ESC + '[>83;40003;0c');
|
||||
this._terminal.handler(C0.ESC + '[>83;40003;0c');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1624,6 +1628,9 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
// not bold nor faint
|
||||
flags &= ~FLAGS.BOLD;
|
||||
flags &= ~FLAGS.DIM;
|
||||
} else if (p === 23) {
|
||||
// not italic
|
||||
flags &= ~FLAGS.ITALIC;
|
||||
} else if (p === 24) {
|
||||
// not underlined
|
||||
flags &= ~FLAGS.UNDERLINE;
|
||||
@@ -1712,15 +1719,13 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
switch (params[0]) {
|
||||
case 5:
|
||||
// status report
|
||||
this._terminal.send(C0.ESC + '[0n');
|
||||
this._terminal.emit('data', `${C0.ESC}[0n`);
|
||||
break;
|
||||
case 6:
|
||||
// cursor position
|
||||
this._terminal.send(C0.ESC + '['
|
||||
+ (this._terminal.buffer.y + 1)
|
||||
+ ';'
|
||||
+ (this._terminal.buffer.x + 1)
|
||||
+ 'R');
|
||||
const y = this._terminal.buffer.y + 1;
|
||||
const x = this._terminal.buffer.x + 1;
|
||||
this._terminal.emit('data', `${C0.ESC}[${y};${x}R`);
|
||||
break;
|
||||
}
|
||||
} else if (collect === '?') {
|
||||
@@ -1729,27 +1734,25 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
switch (params[0]) {
|
||||
case 6:
|
||||
// cursor position
|
||||
this._terminal.send(C0.ESC + '[?'
|
||||
+ (this._terminal.buffer.y + 1)
|
||||
+ ';'
|
||||
+ (this._terminal.buffer.x + 1)
|
||||
+ 'R');
|
||||
const y = this._terminal.buffer.y + 1;
|
||||
const x = this._terminal.buffer.x + 1;
|
||||
this._terminal.emit('data', `${C0.ESC}[?${y};${x}R`);
|
||||
break;
|
||||
case 15:
|
||||
// no printer
|
||||
// this.send(C0.ESC + '[?11n');
|
||||
// this.handler(C0.ESC + '[?11n');
|
||||
break;
|
||||
case 25:
|
||||
// dont support user defined keys
|
||||
// this.send(C0.ESC + '[?21n');
|
||||
// this.handler(C0.ESC + '[?21n');
|
||||
break;
|
||||
case 26:
|
||||
// north american keyboard
|
||||
// this.send(C0.ESC + '[?27;1;0;0n');
|
||||
// this.handler(C0.ESC + '[?27;1;0;0n');
|
||||
break;
|
||||
case 53:
|
||||
// no dec locator/mouse
|
||||
// this.send(C0.ESC + '[?50n');
|
||||
// this.handler(C0.ESC + '[?50n');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { IMouseZoneManager, IMouseZone } from './ui/Types';
|
||||
import { ILinkMatcher, LineData, ITerminal } from './Types';
|
||||
import { ILinkMatcher, ITerminal, IBufferLine } from './Types';
|
||||
import { Linkifier } from './Linkifier';
|
||||
import { MockBuffer, MockTerminal } from './utils/TestUtils.test';
|
||||
import { CircularList } from './common/CircularList';
|
||||
import { BufferLine } from './BufferLine';
|
||||
|
||||
class TestLinkifier extends Linkifier {
|
||||
constructor(terminal: ITerminal) {
|
||||
@@ -42,14 +43,14 @@ describe('Linkifier', () => {
|
||||
terminal = new MockTerminal();
|
||||
terminal.cols = 100;
|
||||
terminal.buffer = new MockBuffer();
|
||||
(<MockBuffer>terminal.buffer).setLines(new CircularList<LineData>(20));
|
||||
(<MockBuffer>terminal.buffer).setLines(new CircularList<IBufferLine>(20));
|
||||
terminal.buffer.ydisp = 0;
|
||||
linkifier = new TestLinkifier(terminal);
|
||||
mouseZoneManager = new TestMouseZoneManager();
|
||||
});
|
||||
|
||||
function stringToRow(text: string): LineData {
|
||||
const result: LineData = [];
|
||||
function stringToRow(text: string): IBufferLine {
|
||||
const result = new BufferLine();
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
result.push([0, text.charAt(i), 1, text.charCodeAt(i)]);
|
||||
}
|
||||
|
||||
+29
-13
@@ -4,9 +4,10 @@
|
||||
*/
|
||||
|
||||
import { IMouseZoneManager } from './ui/Types';
|
||||
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal } from './Types';
|
||||
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal, IBufferLine } from './Types';
|
||||
import { MouseZone } from './ui/MouseZoneManager';
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
import { CHAR_DATA_ATTR_INDEX } from './Buffer';
|
||||
|
||||
/**
|
||||
* The Linkifier applies links to rows shortly after they have been refreshed.
|
||||
@@ -24,7 +25,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
private _mouseZoneManager: IMouseZoneManager;
|
||||
private _rowsTimeoutId: number;
|
||||
private _nextLinkMatcherId = 0;
|
||||
private _rowsToLinkify: {start: number, end: number};
|
||||
private _rowsToLinkify: { start: number, end: number };
|
||||
|
||||
constructor(
|
||||
protected _terminal: ITerminal
|
||||
@@ -163,23 +164,31 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((<any>this._terminal.buffer.lines.get(absoluteRowIndex)).isWrapped) {
|
||||
if (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: IBufferLine;
|
||||
|
||||
do {
|
||||
rowIndex--;
|
||||
absoluteRowIndex--;
|
||||
} while ((<any>this._terminal.buffer.lines.get(absoluteRowIndex)).isWrapped);
|
||||
line = this._terminal.buffer.lines.get(absoluteRowIndex);
|
||||
|
||||
if (!line) {
|
||||
break;
|
||||
}
|
||||
|
||||
} while (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) {
|
||||
this._terminal.buffer.lines.get(currentIndex).isWrapped) {
|
||||
text += this._terminal.buffer.translateBufferLineToString(currentIndex++, false);
|
||||
}
|
||||
|
||||
@@ -208,6 +217,12 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
// Get index, match.index is for the outer match which includes negated chars
|
||||
const index = text.indexOf(uri);
|
||||
|
||||
// Get cell color
|
||||
const line = this._terminal.buffer.lines.get(this._terminal.buffer.ydisp + rowIndex);
|
||||
const char = line.get(index);
|
||||
const attr: number = char[CHAR_DATA_ATTR_INDEX];
|
||||
const fg = (attr >> 9) & 0x1ff;
|
||||
|
||||
// Ensure the link is valid before registering
|
||||
if (matcher.validationCallback) {
|
||||
matcher.validationCallback(uri, isValid => {
|
||||
@@ -216,11 +231,11 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
return;
|
||||
}
|
||||
if (isValid) {
|
||||
this._addLink(offset + index, rowIndex, uri, matcher);
|
||||
this._addLink(offset + index, rowIndex, uri, matcher, fg);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this._addLink(offset + index, rowIndex, uri, matcher);
|
||||
this._addLink(offset + index, rowIndex, uri, matcher, fg);
|
||||
}
|
||||
|
||||
// Recursively check for links in the rest of the text
|
||||
@@ -237,8 +252,9 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
* @param y The row the link is on.
|
||||
* @param uri The URI of 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 x1 = x % this._terminal.cols;
|
||||
const y1 = y + Math.floor(x / this._terminal.cols);
|
||||
let x2 = (x1 + uri.length) % this._terminal.cols;
|
||||
@@ -260,17 +276,17 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
window.open(uri, '_blank');
|
||||
},
|
||||
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');
|
||||
},
|
||||
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) {
|
||||
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');
|
||||
if (matcher.hoverLeaveCallback) {
|
||||
matcher.hoverLeaveCallback();
|
||||
@@ -285,7 +301,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
));
|
||||
}
|
||||
|
||||
private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number): ILinkHoverEvent {
|
||||
return { x1, y1, x2, y2, cols: this._terminal.cols };
|
||||
private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number, fg: number): ILinkHoverEvent {
|
||||
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 { SelectionModel } from './SelectionModel';
|
||||
import { BufferSet } from './BufferSet';
|
||||
import { LineData, CharData, ITerminal, IBuffer } from './Types';
|
||||
import { ITerminal, IBuffer, IBufferLine } from './Types';
|
||||
import { MockTerminal } from './utils/TestUtils.test';
|
||||
import { BufferLine } from './BufferLine';
|
||||
|
||||
class TestMockTerminal extends MockTerminal {
|
||||
emit(event: string, data: any): void {}
|
||||
@@ -52,16 +53,18 @@ describe('SelectionManager', () => {
|
||||
selectionManager = new TestSelectionManager(terminal, null);
|
||||
});
|
||||
|
||||
function stringToRow(text: string): LineData {
|
||||
const result: LineData = [];
|
||||
function stringToRow(text: string): IBufferLine {
|
||||
const result = new BufferLine();
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
result.push([0, text.charAt(i), 1, text.charCodeAt(i)]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function stringArrayToRow(chars: string[]): LineData {
|
||||
return chars.map(c => <CharData>[0, c, 1, c.charCodeAt(0)]);
|
||||
function stringArrayToRow(chars: string[]): IBufferLine {
|
||||
const line = new BufferLine();
|
||||
chars.map(c => line.push([0, c, 1, c.charCodeAt(0)]));
|
||||
return line;
|
||||
}
|
||||
|
||||
describe('_selectWordAt', () => {
|
||||
@@ -97,7 +100,8 @@ describe('SelectionManager', () => {
|
||||
});
|
||||
it('should expand selection for wide characters', () => {
|
||||
// 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, '', 0, null],
|
||||
[null, '文', 2, '文'.charCodeAt(0)],
|
||||
@@ -113,7 +117,9 @@ describe('SelectionManager', () => {
|
||||
[null, 'f', 1, 'f'.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
|
||||
selectionManager.selectWordAt([0, 0]);
|
||||
assert.equal(selectionManager.selectionText, '中文');
|
||||
@@ -183,6 +189,42 @@ describe('SelectionManager', () => {
|
||||
selectionManager.selectWordAt([15, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'ij"');
|
||||
});
|
||||
it('should expand upwards or downards for wrapped lines', () => {
|
||||
buffer.lines.set(0, stringToRow(' foo'));
|
||||
buffer.lines.set(1, stringToRow('bar '));
|
||||
buffer.lines.get(1).isWrapped = true;
|
||||
selectionManager.selectWordAt([1, 1]);
|
||||
assert.equal(selectionManager.selectionText, 'foobar');
|
||||
selectionManager.model.clearSelection();
|
||||
selectionManager.selectWordAt([78, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'foobar');
|
||||
});
|
||||
it('should expand both upwards and downwards for word wrapped over many lines', () => {
|
||||
const expectedText = 'fooaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccbar';
|
||||
buffer.lines.set(0, stringToRow(' foo'));
|
||||
buffer.lines.set(1, stringToRow('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'));
|
||||
buffer.lines.set(2, stringToRow('bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'));
|
||||
buffer.lines.set(3, stringToRow('cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc'));
|
||||
buffer.lines.set(4, stringToRow('bar '));
|
||||
buffer.lines.get(1).isWrapped = true;
|
||||
buffer.lines.get(2).isWrapped = true;
|
||||
buffer.lines.get(3).isWrapped = true;
|
||||
buffer.lines.get(4).isWrapped = true;
|
||||
selectionManager.selectWordAt([78, 0]);
|
||||
assert.equal(selectionManager.selectionText, expectedText);
|
||||
selectionManager.model.clearSelection();
|
||||
selectionManager.selectWordAt([40, 1]);
|
||||
assert.equal(selectionManager.selectionText, expectedText);
|
||||
selectionManager.model.clearSelection();
|
||||
selectionManager.selectWordAt([40, 2]);
|
||||
assert.equal(selectionManager.selectionText, expectedText);
|
||||
selectionManager.model.clearSelection();
|
||||
selectionManager.selectWordAt([40, 3]);
|
||||
assert.equal(selectionManager.selectionText, expectedText);
|
||||
selectionManager.model.clearSelection();
|
||||
selectionManager.selectWordAt([1, 4]);
|
||||
assert.equal(selectionManager.selectionText, expectedText);
|
||||
});
|
||||
describe('emoji', () => {
|
||||
it('should treat a single emoji as a word when wrapped in spaces', () => {
|
||||
buffer.lines.set(0, stringToRow(' ⚽ a')); // The a is here to prevent the space being trimmed in selectionText
|
||||
@@ -303,7 +345,7 @@ describe('SelectionManager', () => {
|
||||
it('should select the entire wrapped line', () => {
|
||||
buffer.lines.set(0, stringToRow('foo'));
|
||||
const line2 = stringToRow('bar');
|
||||
(<any>line2).isWrapped = true;
|
||||
line2.isWrapped = true;
|
||||
buffer.lines.set(1, line2);
|
||||
selectionManager.selectLineAt(0);
|
||||
assert.equal(selectionManager.selectionText, 'foobar', 'The selected text is correct');
|
||||
|
||||
+94
-30
@@ -3,13 +3,13 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, ISelectionManager, IBuffer, CharData, XtermListener } from './Types';
|
||||
import { ITerminal, ISelectionManager, IBuffer, CharData, XtermListener, IBufferLine } from './Types';
|
||||
import { MouseHelper } from './utils/MouseHelper';
|
||||
import * as Browser from './shared/utils/Browser';
|
||||
import { CharMeasure } from './ui/CharMeasure';
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
import { SelectionModel } from './SelectionModel';
|
||||
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
|
||||
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_CODE_INDEX } from './Buffer';
|
||||
import { AltClickHandler } from './handlers/AltClickHandler';
|
||||
|
||||
/**
|
||||
@@ -204,7 +204,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
for (let i = start[1] + 1; i <= end[1] - 1; i++) {
|
||||
const bufferLine = this._buffer.lines.get(i);
|
||||
const lineText = this._buffer.translateBufferLineToString(i, true);
|
||||
if ((<any>bufferLine).isWrapped) {
|
||||
if (bufferLine.isWrapped) {
|
||||
result[result.length - 1] += lineText;
|
||||
} else {
|
||||
result.push(lineText);
|
||||
@@ -215,7 +215,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
if (start[1] !== end[1]) {
|
||||
const bufferLine = this._buffer.lines.get(end[1]);
|
||||
const lineText = this._buffer.translateBufferLineToString(end[1], true, 0, end[0]);
|
||||
if ((<any>bufferLine).isWrapped) {
|
||||
if (bufferLine.isWrapped) {
|
||||
result[result.length - 1] += lineText;
|
||||
} else {
|
||||
result.push(lineText);
|
||||
@@ -451,8 +451,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
* Removes the listeners that are registered when mousedown is triggered.
|
||||
*/
|
||||
private _removeMouseDownListeners(): void {
|
||||
this._terminal.element.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener);
|
||||
this._terminal.element.ownerDocument.removeEventListener('mouseup', this._mouseUpListener);
|
||||
if (this._terminal.element.ownerDocument) {
|
||||
this._terminal.element.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener);
|
||||
this._terminal.element.ownerDocument.removeEventListener('mouseup', this._mouseUpListener);
|
||||
}
|
||||
clearInterval(this._dragScrollIntervalTimer);
|
||||
this._dragScrollIntervalTimer = null;
|
||||
}
|
||||
@@ -498,7 +500,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
|
||||
// If the mouse is over the second half of a wide character, adjust the
|
||||
// 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) {
|
||||
this._model.selectionStart[0]++;
|
||||
}
|
||||
@@ -574,18 +576,21 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
this._dragScrollAmount = this._getMouseEventScrollAmount(event);
|
||||
|
||||
// If the cursor was above or below the viewport, make sure it's at the
|
||||
// start or end of the viewport respectively.
|
||||
if (this._dragScrollAmount > 0) {
|
||||
this._model.selectionEnd[0] = this._terminal.cols;
|
||||
} else if (this._dragScrollAmount < 0) {
|
||||
this._model.selectionEnd[0] = 0;
|
||||
// start or end of the viewport respectively. This should only happen when
|
||||
// NOT in column select mode.
|
||||
if (this._activeSelectionMode !== SelectionMode.COLUMN) {
|
||||
if (this._dragScrollAmount > 0) {
|
||||
this._model.selectionEnd[0] = this._terminal.cols;
|
||||
} else if (this._dragScrollAmount < 0) {
|
||||
this._model.selectionEnd[0] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// If the character is a wide character include the cell to the right in the
|
||||
// selection. Note that selections at the very end of the line will never
|
||||
// have a character.
|
||||
if (this._model.selectionEnd[1] < this._buffer.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) {
|
||||
this._model.selectionEnd[0]++;
|
||||
}
|
||||
@@ -607,10 +612,19 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
if (this._dragScrollAmount) {
|
||||
this._terminal.scrollLines(this._dragScrollAmount, false);
|
||||
// Re-evaluate selection
|
||||
// If the cursor was above or below the viewport, make sure it's at the
|
||||
// start or end of the viewport respectively. This should only happen when
|
||||
// NOT in column select mode.
|
||||
if (this._dragScrollAmount > 0) {
|
||||
this._model.selectionEnd = [this._terminal.cols - 1, Math.min(this._terminal.buffer.ydisp + this._terminal.rows, this._terminal.buffer.lines.length - 1)];
|
||||
if (this._activeSelectionMode !== SelectionMode.COLUMN) {
|
||||
this._model.selectionEnd[0] = this._terminal.cols;
|
||||
}
|
||||
this._model.selectionEnd[1] = Math.min(this._terminal.buffer.ydisp + this._terminal.rows, this._terminal.buffer.lines.length - 1);
|
||||
} else {
|
||||
this._model.selectionEnd = [0, this._terminal.buffer.ydisp];
|
||||
if (this._activeSelectionMode !== SelectionMode.COLUMN) {
|
||||
this._model.selectionEnd[0] = 0;
|
||||
}
|
||||
this._model.selectionEnd[1] = this._terminal.buffer.ydisp;
|
||||
}
|
||||
this.refresh();
|
||||
}
|
||||
@@ -647,10 +661,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
* latter takes into account wide characters.
|
||||
* @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];
|
||||
for (let i = 0; coords[0] >= i; i++) {
|
||||
const char = bufferLine[i];
|
||||
const char = bufferLine.get(i);
|
||||
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||
// Wide characters aren't included in the line string so decrement the
|
||||
// index so the index is back on the wide character.
|
||||
@@ -677,7 +691,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
* Gets positional information for the word at the coordinated specified.
|
||||
* @param coords The coordinates to get the word at.
|
||||
*/
|
||||
private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): IWordPosition {
|
||||
private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean, followWrappedLinesAbove: boolean = true, followWrappedLinesBelow: boolean = true): IWordPosition {
|
||||
// Ensure coords are within viewport (eg. not within scroll bar)
|
||||
if (coords[0] >= this._terminal.cols) {
|
||||
return null;
|
||||
@@ -719,24 +733,24 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
|
||||
// Consider the initial position, skip it and increment the wide char
|
||||
// variable
|
||||
if (bufferLine[startCol][CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||
if (bufferLine.get(startCol)[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||
leftWideCharCount++;
|
||||
startCol--;
|
||||
}
|
||||
if (bufferLine[endCol][CHAR_DATA_WIDTH_INDEX] === 2) {
|
||||
if (bufferLine.get(endCol)[CHAR_DATA_WIDTH_INDEX] === 2) {
|
||||
rightWideCharCount++;
|
||||
endCol++;
|
||||
}
|
||||
|
||||
// Adjust the end index for characters whose length are > 1 (emojis)
|
||||
if (bufferLine[endCol][CHAR_DATA_CHAR_INDEX].length > 1) {
|
||||
rightLongCharOffset += bufferLine[endCol][CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
endIndex += bufferLine[endCol][CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
if (bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length > 1) {
|
||||
rightLongCharOffset += bufferLine.get(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
|
||||
while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine[startCol - 1])) {
|
||||
const char = bufferLine[startCol - 1];
|
||||
while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.get(startCol - 1))) {
|
||||
const char = bufferLine.get(startCol - 1);
|
||||
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||
// If the next character is a wide char, record it and skip the column
|
||||
leftWideCharCount++;
|
||||
@@ -750,8 +764,8 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
startIndex--;
|
||||
startCol--;
|
||||
}
|
||||
while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine[endCol + 1])) {
|
||||
const char = bufferLine[endCol + 1];
|
||||
while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.get(endCol + 1))) {
|
||||
const char = bufferLine.get(endCol + 1);
|
||||
if (char[CHAR_DATA_WIDTH_INDEX] === 2) {
|
||||
// If the next character is a wide char, record it and skip the column
|
||||
rightWideCharCount++;
|
||||
@@ -772,7 +786,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
|
||||
// Calculate the start _column_, converting the the string indexes back to
|
||||
// column coordinates.
|
||||
const start =
|
||||
let start =
|
||||
startIndex // The index of the selection's start char in the line string
|
||||
+ charOffset // The difference between the initial char's column and index
|
||||
- leftWideCharCount // The number of wide chars left of the initial char
|
||||
@@ -780,7 +794,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
|
||||
// Calculate the length in _columns_, converting the the string indexes back
|
||||
// to column coordinates.
|
||||
const length = Math.min(this._terminal.cols, // Disallow lengths larger than the terminal cols
|
||||
let length = Math.min(this._terminal.cols, // Disallow lengths larger than the terminal cols
|
||||
endIndex // The index of the selection's end char in the line string
|
||||
- startIndex // The index of the selection's start char in the line string
|
||||
+ leftWideCharCount // The number of wide chars left of the initial char
|
||||
@@ -792,6 +806,34 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
return null;
|
||||
}
|
||||
|
||||
// Recurse upwards if the line is wrapped and the word wraps to the above line
|
||||
if (followWrappedLinesAbove) {
|
||||
if (start === 0 && bufferLine.get(0)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
|
||||
const previousBufferLine = this._buffer.lines.get(coords[1] - 1);
|
||||
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);
|
||||
if (previousLineWordPosition) {
|
||||
const offset = this._terminal.cols - previousLineWordPosition.start;
|
||||
start -= offset;
|
||||
length += offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recurse downwards if the line is wrapped and the word wraps to the next line
|
||||
if (followWrappedLinesBelow) {
|
||||
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);
|
||||
if (nextBufferLine && nextBufferLine.isWrapped && nextBufferLine.get(0)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
|
||||
const nextLineWordPosition = this._getWordAt([0, coords[1] + 1], false, false, true);
|
||||
if (nextLineWordPosition) {
|
||||
length += nextLineWordPosition.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { start, length };
|
||||
}
|
||||
|
||||
@@ -803,6 +845,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
protected _selectWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): void {
|
||||
const wordPosition = this._getWordAt(coords, allowWhitespaceOnlySelection);
|
||||
if (wordPosition) {
|
||||
// Adjust negative start value
|
||||
while (wordPosition.start < 0) {
|
||||
wordPosition.start += this._terminal.cols;
|
||||
coords[1]--;
|
||||
}
|
||||
this._model.selectionStart = [wordPosition.start, coords[1]];
|
||||
this._model.selectionStartLength = wordPosition.length;
|
||||
}
|
||||
@@ -815,7 +862,24 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
private _selectToWordAt(coords: [number, number]): void {
|
||||
const wordPosition = this._getWordAt(coords, true);
|
||||
if (wordPosition) {
|
||||
this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : (wordPosition.start + wordPosition.length), coords[1]];
|
||||
let endRow = coords[1];
|
||||
|
||||
// Adjust negative start value
|
||||
while (wordPosition.start < 0) {
|
||||
wordPosition.start += this._terminal.cols;
|
||||
endRow--;
|
||||
}
|
||||
|
||||
// Adjust wrapped length value, this only needs to happen when values are reversed as in that
|
||||
// case we're interested in the start of the word, not the end
|
||||
if (!this._model.areSelectionValuesReversed()) {
|
||||
while (wordPosition.start + wordPosition.length > this._terminal.cols) {
|
||||
wordPosition.length -= this._terminal.cols;
|
||||
endRow++;
|
||||
}
|
||||
}
|
||||
|
||||
this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : wordPosition.start + wordPosition.length, endRow];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -128,6 +128,11 @@ describe('SelectionManager', () => {
|
||||
model.selectionEnd = [3, 2];
|
||||
assert.deepEqual(model.finalSelectionEnd, [4, 2]);
|
||||
});
|
||||
it('should return the end on a different row when start + length overflows onto a following row', () => {
|
||||
model.selectionStart = [78, 2];
|
||||
model.selectionStartLength = 4;
|
||||
assert.deepEqual(model.finalSelectionEnd, [2, 3]);
|
||||
});
|
||||
it('should return selection end if selection end is after selection start + length', () => {
|
||||
model.selectionStart = [2, 2];
|
||||
model.selectionStartLength = 2;
|
||||
|
||||
@@ -76,9 +76,13 @@ export class SelectionModel {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Use the selection start if the end doesn't exist or they're reversed
|
||||
// Use the selection start + length if the end doesn't exist or they're reversed
|
||||
if (!this.selectionEnd || this.areSelectionValuesReversed()) {
|
||||
return [this.selectionStart[0] + this.selectionStartLength, this.selectionStart[1]];
|
||||
const startPlusLength = this.selectionStart[0] + this.selectionStartLength;
|
||||
if (startPlusLength > this._terminal.cols) {
|
||||
return [startPlusLength % this._terminal.cols, this.selectionStart[1] + Math.floor(startPlusLength / this._terminal.cols)];
|
||||
}
|
||||
return [startPlusLength, this.selectionStart[1]];
|
||||
}
|
||||
|
||||
// Ensure the the word/line is selected after a double/triple click
|
||||
|
||||
@@ -67,7 +67,7 @@ function terminalToString(term: Terminal): string {
|
||||
for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) {
|
||||
lineText = '';
|
||||
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
|
||||
lineText = lineText.replace(/\s+$/, '');
|
||||
@@ -105,6 +105,7 @@ if (os.platform() !== 'win32') {
|
||||
// omit stack trace for escape sequence files
|
||||
Error.stackTraceLimit = 0;
|
||||
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
|
||||
const skip = [
|
||||
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 result = cp.spawnSync(tsc, { cwd: fixtureDir });
|
||||
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'));
|
||||
});
|
||||
});
|
||||
|
||||
+125
-124
@@ -6,7 +6,8 @@
|
||||
import { assert, expect } from 'chai';
|
||||
import { Terminal } from './Terminal';
|
||||
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_ROWS = 24;
|
||||
@@ -141,7 +142,7 @@ describe('term.js addons', () => {
|
||||
assert.equal(term.buffer.lines.length, term.rows);
|
||||
assert.deepEqual(term.buffer.lines.get(0), promptLine);
|
||||
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', () => {
|
||||
@@ -158,7 +159,7 @@ describe('term.js addons', () => {
|
||||
assert.equal(term.buffer.lines.length, term.rows);
|
||||
assert.deepEqual(term.buffer.lines.get(0), promptLine);
|
||||
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', () => {
|
||||
@@ -171,7 +172,7 @@ describe('term.js addons', () => {
|
||||
assert.equal(term.buffer.lines.length, term.rows);
|
||||
assert.deepEqual(term.buffer.lines.get(0), promptLine);
|
||||
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));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -337,62 +338,62 @@ describe('term.js addons', () => {
|
||||
describe('scroll() function', () => {
|
||||
describe('when scrollback > 0', () => {
|
||||
it('should create a new line and scroll', () => {
|
||||
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
|
||||
term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
|
||||
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.scroll();
|
||||
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(INIT_ROWS - 1)[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(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS).get(0)[CHAR_DATA_CHAR_INDEX], ' ');
|
||||
});
|
||||
|
||||
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(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
|
||||
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
|
||||
term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
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.scrollTop = 1;
|
||||
term.scroll();
|
||||
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(1)[0][CHAR_DATA_CHAR_INDEX], 'c');
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
|
||||
});
|
||||
|
||||
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(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
|
||||
term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd';
|
||||
term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e';
|
||||
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
|
||||
term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX] = 'c';
|
||||
term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX] = 'd';
|
||||
term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX] = 'e';
|
||||
term.buffer.y = 3;
|
||||
term.buffer.scrollBottom = 3;
|
||||
term.scroll();
|
||||
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(1)[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(3)[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(5)[0][CHAR_DATA_CHAR_INDEX], 'e');
|
||||
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).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
|
||||
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
|
||||
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
|
||||
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).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
|
||||
});
|
||||
|
||||
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(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
|
||||
term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd';
|
||||
term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e';
|
||||
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
|
||||
term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX] = 'c';
|
||||
term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX] = 'd';
|
||||
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.scrollTop = 1;
|
||||
term.buffer.scrollBottom = 3;
|
||||
term.scroll();
|
||||
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(1)[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(3)[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(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||
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).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
|
||||
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).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -403,65 +404,65 @@ describe('term.js addons', () => {
|
||||
});
|
||||
|
||||
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(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX] = 'c';
|
||||
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
|
||||
term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
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
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||
term.scroll();
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||
// '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(1)[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 - 1)[0][CHAR_DATA_CHAR_INDEX], ' ');
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], ' ');
|
||||
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).get(0)[CHAR_DATA_CHAR_INDEX], ' ');
|
||||
});
|
||||
|
||||
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(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
|
||||
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
|
||||
term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
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.scrollTop = 1;
|
||||
term.scroll();
|
||||
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(1)[0][CHAR_DATA_CHAR_INDEX], 'c');
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
|
||||
});
|
||||
|
||||
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(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
|
||||
term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd';
|
||||
term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e';
|
||||
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
|
||||
term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX] = 'c';
|
||||
term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX] = 'd';
|
||||
term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX] = 'e';
|
||||
term.buffer.y = 3;
|
||||
term.buffer.scrollBottom = 3;
|
||||
term.scroll();
|
||||
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(1)[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(3)[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(0).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
|
||||
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
|
||||
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).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
|
||||
});
|
||||
|
||||
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(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
|
||||
term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd';
|
||||
term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e';
|
||||
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
|
||||
term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX] = 'c';
|
||||
term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX] = 'd';
|
||||
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.scrollTop = 1;
|
||||
term.buffer.scrollBottom = 3;
|
||||
term.scroll();
|
||||
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(1)[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(3)[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(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||
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).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
|
||||
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).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -654,11 +655,11 @@ describe('term.js addons', () => {
|
||||
const high = String.fromCharCode(0xD800);
|
||||
for (let i = 0xDC00; i <= 0xDCFF; ++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].length).eql(2);
|
||||
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();
|
||||
}
|
||||
});
|
||||
@@ -667,9 +668,9 @@ describe('term.js addons', () => {
|
||||
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
||||
term.buffer.x = term.cols - 1;
|
||||
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)[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(0).get(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].length).eql(2);
|
||||
expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
term.reset();
|
||||
}
|
||||
});
|
||||
@@ -679,10 +680,10 @@ describe('term.js addons', () => {
|
||||
term.buffer.x = term.cols - 1;
|
||||
term.wraparoundMode = true;
|
||||
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(1)[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)[1][CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('a');
|
||||
expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
||||
expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(term.buffer.lines.get(1).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
term.reset();
|
||||
}
|
||||
});
|
||||
@@ -693,9 +694,9 @@ describe('term.js addons', () => {
|
||||
term.wraparoundMode = false;
|
||||
term.write('a' + high + String.fromCharCode(i));
|
||||
// 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)[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(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('a');
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(term.buffer.lines.get(1).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
term.reset();
|
||||
}
|
||||
});
|
||||
@@ -704,11 +705,11 @@ describe('term.js addons', () => {
|
||||
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
||||
term.write(high);
|
||||
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].length).eql(2);
|
||||
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();
|
||||
}
|
||||
});
|
||||
@@ -717,30 +718,30 @@ describe('term.js addons', () => {
|
||||
describe('unicode - combining characters', () => {
|
||||
it('café', () => {
|
||||
term.write('cafe\u0301');
|
||||
expect(term.buffer.lines.get(0)[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)[3][CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
|
||||
expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
});
|
||||
it('café - end of line', () => {
|
||||
term.buffer.x = term.cols - 1 - 3;
|
||||
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)[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)[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)[1][CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
});
|
||||
it('multiple combined é', () => {
|
||||
term.wraparoundMode = true;
|
||||
term.write(Array(100).join('e\u0301'));
|
||||
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].length).eql(2);
|
||||
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].length).eql(2);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
@@ -749,12 +750,12 @@ describe('term.js addons', () => {
|
||||
term.wraparoundMode = true;
|
||||
term.write(Array(100).join('\uD800\uDC00\u0301'));
|
||||
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].length).eql(3);
|
||||
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].length).eql(3);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
@@ -777,7 +778,7 @@ describe('term.js addons', () => {
|
||||
term.wraparoundMode = true;
|
||||
term.write(Array(50).join('¥'));
|
||||
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) {
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
@@ -788,7 +789,7 @@ describe('term.js addons', () => {
|
||||
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].length).eql(1);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
@@ -798,7 +799,7 @@ describe('term.js addons', () => {
|
||||
term.buffer.x = 1;
|
||||
term.write(Array(50).join('¥'));
|
||||
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)) {
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
@@ -809,11 +810,11 @@ describe('term.js addons', () => {
|
||||
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].length).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].length).eql(1);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
@@ -823,7 +824,7 @@ describe('term.js addons', () => {
|
||||
term.buffer.x = 1;
|
||||
term.write(Array(50).join('¥\u0301'));
|
||||
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)) {
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
@@ -834,11 +835,11 @@ describe('term.js addons', () => {
|
||||
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].length).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].length).eql(2);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
@@ -847,7 +848,7 @@ describe('term.js addons', () => {
|
||||
term.wraparoundMode = true;
|
||||
term.write(Array(50).join('¥\u0301'));
|
||||
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) {
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
@@ -858,7 +859,7 @@ describe('term.js addons', () => {
|
||||
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].length).eql(2);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
@@ -868,7 +869,7 @@ describe('term.js addons', () => {
|
||||
term.buffer.x = 1;
|
||||
term.write(Array(50).join('\ud843\ude6d\u0301'));
|
||||
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)) {
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
@@ -879,11 +880,11 @@ describe('term.js addons', () => {
|
||||
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].length).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].length).eql(3);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
@@ -892,7 +893,7 @@ describe('term.js addons', () => {
|
||||
term.wraparoundMode = true;
|
||||
term.write(Array(50).join('\ud843\ude6d\u0301'));
|
||||
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) {
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
@@ -903,7 +904,7 @@ describe('term.js addons', () => {
|
||||
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].length).eql(3);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
@@ -918,10 +919,10 @@ describe('term.js addons', () => {
|
||||
term.insertMode = true;
|
||||
term.write('abcde');
|
||||
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)[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)[79][CHAR_DATA_CHAR_INDEX]).eql('4');
|
||||
expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('a');
|
||||
expect(term.buffer.lines.get(0).get(14)[CHAR_DATA_CHAR_INDEX]).eql('e');
|
||||
expect(term.buffer.lines.get(0).get(15)[CHAR_DATA_CHAR_INDEX]).eql('0');
|
||||
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql('4');
|
||||
});
|
||||
it('fullwidth - insert', () => {
|
||||
term.write(Array(9).join('0123456789').slice(-80));
|
||||
@@ -930,11 +931,11 @@ describe('term.js addons', () => {
|
||||
term.insertMode = true;
|
||||
term.write('¥¥¥');
|
||||
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)[11][CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(term.buffer.lines.get(0)[14][CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(term.buffer.lines.get(0)[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(10)[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(term.buffer.lines.get(0).get(14)[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(term.buffer.lines.get(0).get(15)[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql('3');
|
||||
});
|
||||
it('fullwidth - right border', () => {
|
||||
term.write(Array(41).join('¥'));
|
||||
@@ -943,14 +944,14 @@ describe('term.js addons', () => {
|
||||
term.insertMode = true;
|
||||
term.write('a');
|
||||
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)[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(10)[CHAR_DATA_CHAR_INDEX]).eql('a');
|
||||
expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql(' '); // fullwidth char got replaced
|
||||
term.write('b');
|
||||
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)[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(11)[CHAR_DATA_CHAR_INDEX]).eql('b');
|
||||
expect(term.buffer.lines.get(0).get(12)[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql(''); // empty cell after fullwidth
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+104
-141
@@ -21,11 +21,11 @@
|
||||
* http://linux.die.net/man/7/urxvt
|
||||
*/
|
||||
|
||||
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, LineData } from './Types';
|
||||
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler } from './Types';
|
||||
import { IMouseZoneManager } from './ui/Types';
|
||||
import { IRenderer } from './renderer/Types';
|
||||
import { BufferSet } from './BufferSet';
|
||||
import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR } 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 { EventEmitter } from './EventEmitter';
|
||||
import { Viewport } from './Viewport';
|
||||
@@ -52,6 +52,7 @@ import { DomRenderer } from './renderer/dom/DomRenderer';
|
||||
import { IKeyboardEvent } from './common/Types';
|
||||
import { evaluateKeyboardEvent } from './core/input/Keyboard';
|
||||
import { KeyboardResultType, ICharset } from './core/Types';
|
||||
import { BufferLine } from './BufferLine';
|
||||
|
||||
// Let it work inside Node.js for automated testing purposes.
|
||||
const document = (typeof window !== 'undefined') ? window.document : null;
|
||||
@@ -72,7 +73,7 @@ const WRITE_BATCH_SIZE = 300;
|
||||
/**
|
||||
* The set of options that only have an effect when set in the Terminal constructor.
|
||||
*/
|
||||
const CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows', 'rendererType'];
|
||||
const CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows'];
|
||||
|
||||
const DEFAULT_OPTIONS: ITerminalOptions = {
|
||||
cols: 80,
|
||||
@@ -133,9 +134,7 @@ 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
|
||||
public cursorState: number;
|
||||
public cursorHidden: boolean;
|
||||
public convertEol: boolean;
|
||||
|
||||
private _sendDataQueue: string;
|
||||
private _customKeyEventHandler: CustomKeyEventHandler;
|
||||
|
||||
// modes
|
||||
@@ -207,6 +206,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
public mouseHelper: MouseHelper;
|
||||
private _accessibilityManager: AccessibilityManager;
|
||||
private _screenDprMonitor: ScreenDprMonitor;
|
||||
private _theme: ITheme;
|
||||
|
||||
public cols: number;
|
||||
public rows: number;
|
||||
@@ -214,7 +214,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
/**
|
||||
* Creates a new `Terminal` object.
|
||||
*
|
||||
* @param {object} options An object containing a set of options, the available options are:
|
||||
* @param options An object containing a set of options, the available options are:
|
||||
* - `cursorBlink` (boolean): Whether the terminal cursor blinks
|
||||
* - `cols` (number): The number of columns of the terminal (horizontal size)
|
||||
* - `rows` (number): The number of rows of the terminal (vertical size)
|
||||
@@ -251,7 +251,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
|
||||
private _setup(): void {
|
||||
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];
|
||||
}
|
||||
});
|
||||
@@ -270,7 +270,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
|
||||
this.cursorState = 0;
|
||||
this.cursorHidden = false;
|
||||
this._sendDataQueue = '';
|
||||
this._customKeyEventHandler = null;
|
||||
|
||||
// modes
|
||||
@@ -348,7 +347,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
|
||||
/**
|
||||
* Retrieves an option's value from the terminal.
|
||||
* @param {string} key The option key.
|
||||
* @param key The option key.
|
||||
*/
|
||||
public getOption(key: string): any {
|
||||
if (!(key in DEFAULT_OPTIONS)) {
|
||||
@@ -360,8 +359,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
|
||||
/**
|
||||
* Sets an option on the terminal.
|
||||
* @param {string} key The option key.
|
||||
* @param {any} value The option value.
|
||||
* @param key The option key.
|
||||
* @param value The option value.
|
||||
*/
|
||||
public setOption(key: string, value: any): void {
|
||||
if (!(key in DEFAULT_OPTIONS)) {
|
||||
@@ -370,6 +369,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
if (CONSTRUCTOR_ONLY_OPTIONS.indexOf(key) !== -1) {
|
||||
console.error(`Option "${key}" can only be set in the constructor`);
|
||||
}
|
||||
if (this.options[key] === value) {
|
||||
return;
|
||||
}
|
||||
switch (key) {
|
||||
case 'bellStyle':
|
||||
if (!value) {
|
||||
@@ -396,6 +398,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
console.warn(`${key} cannot be less than 1, value: ${value}`);
|
||||
return;
|
||||
}
|
||||
case 'rendererType':
|
||||
if (!value) {
|
||||
value = 'canvas';
|
||||
}
|
||||
break;
|
||||
case 'tabStopWidth':
|
||||
if (value < 1) {
|
||||
console.warn(`${key} cannot be less than 1, value: ${value}`);
|
||||
@@ -455,6 +462,18 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
this.renderer.onResize(this.cols, this.rows);
|
||||
this.refresh(0, this.rows - 1);
|
||||
}
|
||||
case 'rendererType':
|
||||
if (this.renderer) {
|
||||
this.unregister(this.renderer);
|
||||
this.renderer.dispose();
|
||||
this.renderer = null;
|
||||
}
|
||||
this._setupRenderer();
|
||||
this.renderer.onCharSizeChanged();
|
||||
if (this._theme) {
|
||||
this.renderer.setTheme(this._theme);
|
||||
}
|
||||
break;
|
||||
case 'scrollback':
|
||||
this.buffers.resize(this.cols, this.rows);
|
||||
if (this.viewport) {
|
||||
@@ -484,10 +503,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
/**
|
||||
* Binds the desired focus behavior on a given terminal object.
|
||||
*/
|
||||
private _onTextAreaFocus(): void {
|
||||
private _onTextAreaFocus(ev: KeyboardEvent): void {
|
||||
if (this.sendFocus) {
|
||||
this.send(C0.ESC + '[I');
|
||||
this.handler(C0.ESC + '[I');
|
||||
}
|
||||
this.updateCursorStyle(ev);
|
||||
this.element.classList.add('focus');
|
||||
this.showCursor();
|
||||
this.emit('focus');
|
||||
@@ -510,7 +530,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
this.textarea.value = '';
|
||||
this.refresh(this.buffer.y, this.buffer.y);
|
||||
if (this.sendFocus) {
|
||||
this.send(C0.ESC + '[O');
|
||||
this.handler(C0.ESC + '[O');
|
||||
}
|
||||
this.element.classList.remove('focus');
|
||||
this.emit('blur');
|
||||
@@ -602,7 +622,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
/**
|
||||
* Opens the terminal within an element.
|
||||
*
|
||||
* @param {HTMLElement} parent The element to create the terminal within.
|
||||
* @param parent The element to create the terminal within.
|
||||
*/
|
||||
public open(parent: HTMLElement): void {
|
||||
this._parent = parent || this._parent;
|
||||
@@ -660,7 +680,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
this.textarea.setAttribute('autocapitalize', 'off');
|
||||
this.textarea.setAttribute('spellcheck', 'false');
|
||||
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._helperContainer.appendChild(this.textarea);
|
||||
|
||||
@@ -674,12 +694,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
// Performance: Add viewport and helper elements from the fragment
|
||||
this.element.appendChild(fragment);
|
||||
|
||||
switch (this.options.rendererType) {
|
||||
case 'canvas': this.renderer = new Renderer(this, this.options.theme); break;
|
||||
case 'dom': this.renderer = new DomRenderer(this, this.options.theme); break;
|
||||
default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`);
|
||||
}
|
||||
this.register(this.renderer);
|
||||
this._setupRenderer();
|
||||
this._theme = this.options.theme;
|
||||
this.options.theme = null;
|
||||
this.viewport = new Viewport(this, this._viewportElement, this._viewportScrollArea, this.charMeasure);
|
||||
this.viewport.onThemeChanged(this.renderer.colorManager.colors);
|
||||
@@ -736,11 +752,21 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
|
||||
}
|
||||
|
||||
private _setupRenderer(): void {
|
||||
switch (this.options.rendererType) {
|
||||
case 'canvas': this.renderer = new Renderer(this, this.options.theme); break;
|
||||
case 'dom': this.renderer = new DomRenderer(this, this.options.theme); break;
|
||||
default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`);
|
||||
}
|
||||
this.register(this.renderer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the theme on the renderer. The renderer must have been initialized.
|
||||
* @param theme The theme to ste.
|
||||
* @param theme The theme to set.
|
||||
*/
|
||||
private _setTheme(theme: ITheme): void {
|
||||
this._theme = theme;
|
||||
const colors = this.renderer.setTheme(theme);
|
||||
if (this.viewport) {
|
||||
this.viewport.onThemeChanged(colors);
|
||||
@@ -860,7 +886,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
else if (button === 3) return;
|
||||
else data += '0';
|
||||
data += '~[' + pos.x + ',' + pos.y + ']\r';
|
||||
self.send(data);
|
||||
self.handler(data);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -873,7 +899,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
else if (button === 1) button = 4;
|
||||
else if (button === 2) button = 6;
|
||||
else if (button === 3) button = 3;
|
||||
self.send(C0.ESC + '['
|
||||
self.handler(C0.ESC + '['
|
||||
+ button
|
||||
+ ';'
|
||||
+ (button === 3 ? 4 : 0)
|
||||
@@ -893,14 +919,14 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
pos.y -= 32;
|
||||
pos.x++;
|
||||
pos.y++;
|
||||
self.send(C0.ESC + '[' + button + ';' + pos.x + ';' + pos.y + 'M');
|
||||
self.handler(C0.ESC + '[' + button + ';' + pos.x + ';' + pos.y + 'M');
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.sgrMouse) {
|
||||
pos.x -= 32;
|
||||
pos.y -= 32;
|
||||
self.send(C0.ESC + '[<'
|
||||
self.handler(C0.ESC + '[<'
|
||||
+ (((button & 3) === 3 ? button & ~3 : button) - 32)
|
||||
+ ';'
|
||||
+ pos.x
|
||||
@@ -916,7 +942,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
encode(data, pos.x);
|
||||
encode(data, pos.y);
|
||||
|
||||
self.send(C0.ESC + '[M' + String.fromCharCode.apply(String, data));
|
||||
self.handler(C0.ESC + '[M' + String.fromCharCode.apply(String, data));
|
||||
}
|
||||
|
||||
function getButton(ev: MouseEvent): number {
|
||||
@@ -935,9 +961,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
// 1, and 2 - with 64 added
|
||||
switch ((<any>ev).overrideType || ev.type) {
|
||||
case 'mousedown':
|
||||
button = ev.button != null
|
||||
button = ev.button !== null && ev.button !== undefined
|
||||
? +ev.button
|
||||
: ev.which != null
|
||||
: ev.which !== null && ev.which !== undefined
|
||||
? ev.which - 1
|
||||
: null;
|
||||
|
||||
@@ -1065,7 +1091,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
for (let i = 0; i < Math.abs(amount); i++) {
|
||||
data += sequence;
|
||||
}
|
||||
this.send(data);
|
||||
this.handler(data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1098,8 +1124,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
/**
|
||||
* Tells the renderer to refresh terminal content between two rows (inclusive) at the next
|
||||
* opportunity.
|
||||
* @param {number} start The row to start from (between 0 and this.rows - 1).
|
||||
* @param {number} end The row to end at (between start and this.rows - 1).
|
||||
* @param start The row to start from (between 0 and this.rows - 1).
|
||||
* @param end The row to end at (between start and this.rows - 1).
|
||||
*/
|
||||
public refresh(start: number, end: number): void {
|
||||
if (this.renderer) {
|
||||
@@ -1109,8 +1135,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
|
||||
/**
|
||||
* Queues linkification for the specified rows.
|
||||
* @param {number} start The row to start from (between 0 and this.rows - 1).
|
||||
* @param {number} end The row to end at (between start and this.rows - 1).
|
||||
* @param start The row to start from (between 0 and this.rows - 1).
|
||||
* @param end The row to end at (between start and this.rows - 1).
|
||||
*/
|
||||
private _queueLinkification(start: number, end: number): void {
|
||||
if (this.linkifier) {
|
||||
@@ -1144,7 +1170,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
* @param isWrapped Whether the new line is wrapped from the previous line.
|
||||
*/
|
||||
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 bottomRow = this.buffer.ybase + this.buffer.scrollBottom;
|
||||
|
||||
@@ -1202,8 +1228,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
|
||||
/**
|
||||
* Scroll the display of the terminal
|
||||
* @param {number} disp The number of lines to scroll down (negative scroll up).
|
||||
* @param {boolean} suppressScrollEvent Don't emit the scroll event as scrollLines. This is used
|
||||
* @param disp The number of lines to scroll down (negative scroll up).
|
||||
* @param suppressScrollEvent Don't emit the scroll event as scrollLines. This is used
|
||||
* to avoid unwanted events being handled by the viewport when the event was triggered from the
|
||||
* viewport originally.
|
||||
*/
|
||||
@@ -1234,7 +1260,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
|
||||
/**
|
||||
* Scroll the display of the terminal by a number of pages.
|
||||
* @param {number} pageCount The number of pages to scroll (negative scrolls up).
|
||||
* @param pageCount The number of pages to scroll (negative scrolls up).
|
||||
*/
|
||||
public scrollPages(pageCount: number): void {
|
||||
this.scrollLines(pageCount * (this.rows - 1));
|
||||
@@ -1263,9 +1289,14 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
|
||||
/**
|
||||
* Writes text to the terminal.
|
||||
* @param {string} data The text to write to the terminal.
|
||||
* @param data The text to write to the terminal.
|
||||
*/
|
||||
public write(data: string): void {
|
||||
// Ensure the terminal isn't disposed
|
||||
if (this._isDisposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore falsy data values (including the empty string)
|
||||
if (!data) {
|
||||
return;
|
||||
@@ -1279,7 +1310,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
if (this.options.useFlowControl && !this._xoffSentToCatchUp && this.writeBuffer.length >= WRITE_BUFFER_PAUSE_THRESHOLD) {
|
||||
// XOFF - stop pty pipe
|
||||
// XON will be triggered by emulator before processing data chunk
|
||||
this.send(C0.DC3);
|
||||
this.handler(C0.DC3);
|
||||
this._xoffSentToCatchUp = true;
|
||||
}
|
||||
|
||||
@@ -1294,6 +1325,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
}
|
||||
|
||||
protected _innerWrite(): void {
|
||||
// Ensure the terminal isn't disposed
|
||||
if (this._isDisposed) {
|
||||
this.writeBuffer = [];
|
||||
}
|
||||
|
||||
const writeBatch = this.writeBuffer.splice(0, WRITE_BATCH_SIZE);
|
||||
while (writeBatch.length > 0) {
|
||||
const data = writeBatch.shift();
|
||||
@@ -1301,7 +1337,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
// If XOFF was sent in order to catch up with the pty process, resume it if
|
||||
// the writeBuffer is empty to allow more data to come in.
|
||||
if (this._xoffSentToCatchUp && writeBatch.length === 0 && this.writeBuffer.length === 0) {
|
||||
this.send(C0.DC1);
|
||||
this.handler(C0.DC1);
|
||||
this._xoffSentToCatchUp = false;
|
||||
}
|
||||
|
||||
@@ -1329,7 +1365,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
|
||||
/**
|
||||
* Writes text to the terminal, followed by a break line character (\n).
|
||||
* @param {string} data The text to write to the terminal.
|
||||
* @param data The text to write to the terminal.
|
||||
*/
|
||||
public writeln(data: string): void {
|
||||
this.write(data + '\r\n');
|
||||
@@ -1374,6 +1410,18 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
}
|
||||
}
|
||||
|
||||
public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
|
||||
const joinerId = this.renderer.registerCharacterJoiner(handler);
|
||||
this.refresh(0, this.rows - 1);
|
||||
return joinerId;
|
||||
}
|
||||
|
||||
public deregisterCharacterJoiner(joinerId: number): void {
|
||||
if (this.renderer.deregisterCharacterJoiner(joinerId)) {
|
||||
this.refresh(0, this.rows - 1);
|
||||
}
|
||||
}
|
||||
|
||||
public get markers(): IMarker[] {
|
||||
return this.buffer.markers;
|
||||
}
|
||||
@@ -1430,7 +1478,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
* Handle a keydown event
|
||||
* Key Resources:
|
||||
* - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent
|
||||
* @param {KeyboardEvent} ev The keydown event to be handled.
|
||||
* @param ev The keydown event to be handled.
|
||||
*/
|
||||
protected _keyDown(event: KeyboardEvent): boolean {
|
||||
if (this._customKeyEventHandler && this._customKeyEventHandler(event) === false) {
|
||||
@@ -1527,7 +1575,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
* Handle a keypress event.
|
||||
* Key Resources:
|
||||
* - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent
|
||||
* @param {KeyboardEvent} ev The keypress event to be handled.
|
||||
* @param ev The keypress event to be handled.
|
||||
*/
|
||||
protected _keyPress(ev: KeyboardEvent): boolean {
|
||||
let key;
|
||||
@@ -1540,7 +1588,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
|
||||
if (ev.charCode) {
|
||||
key = ev.charCode;
|
||||
} else if (ev.which == null) {
|
||||
} else if (ev.which === null || ev.which === undefined) {
|
||||
key = ev.keyCode;
|
||||
} else if (ev.which !== 0 && ev.charCode !== 0) {
|
||||
key = ev.which;
|
||||
@@ -1564,21 +1612,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send data for handling to the terminal
|
||||
* @param {string} data
|
||||
*/
|
||||
public send(data: string): void {
|
||||
if (!this._sendDataQueue) {
|
||||
setTimeout(() => {
|
||||
this.handler(this._sendDataQueue);
|
||||
this._sendDataQueue = '';
|
||||
}, 1);
|
||||
}
|
||||
|
||||
this._sendDataQueue += data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ring the bell.
|
||||
* Note: We could do sweet things with webaudio here
|
||||
@@ -1619,8 +1652,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
/**
|
||||
* Resizes the terminal.
|
||||
*
|
||||
* @param {number} x The number of columns to resize to.
|
||||
* @param {number} y The number of rows to resize to.
|
||||
* @param x The number of columns to resize to.
|
||||
* @param y The number of rows to resize to.
|
||||
*/
|
||||
public resize(x: number, y: number): void {
|
||||
if (isNaN(x) || isNaN(y)) {
|
||||
@@ -1654,7 +1687,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
|
||||
/**
|
||||
* Updates the range of rows to refresh
|
||||
* @param {number} y The number of rows to refresh next.
|
||||
* @param y The number of rows to refresh next.
|
||||
*/
|
||||
public updateRange(y: number): void {
|
||||
if (y < this._refreshStart) this._refreshStart = y;
|
||||
@@ -1675,41 +1708,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
this._refreshEnd = this.rows - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase in the identified line everything from "x" to the end of the line (right).
|
||||
* @param {number} x The column from which to start erasing to the end of the line.
|
||||
* @param {number} 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(), ' ', 1, 32 /* ' '.charCodeAt(0) */]; // 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 {number} x The column from which to start erasing to the start of the line.
|
||||
* @param {number} 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(), ' ', 1, 32 /* ' '.charCodeAt(0) */]; // xterm
|
||||
x++;
|
||||
while (x--) {
|
||||
line[x] = ch;
|
||||
}
|
||||
this.updateRange(y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the entire buffer, making the prompt line the new first line.
|
||||
*/
|
||||
@@ -1724,56 +1722,21 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
this.buffer.ybase = 0;
|
||||
this.buffer.y = 0;
|
||||
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.emit('scroll', this.buffer.ydisp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase all content in the given line
|
||||
* @param {number} 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 {boolean} cur First bunch of data for each "blank" character.
|
||||
* @param {boolean} isWrapped Whether the new line is wrapped from the previous line.
|
||||
* @param {boolean} 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, ' ', 1, 32 /* ' '.charCodeAt(0) */]; // 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.
|
||||
* @param cur
|
||||
*/
|
||||
public ch(cur?: boolean): CharData {
|
||||
if (cur) {
|
||||
return [this.eraseAttr(), ' ', 1, 32 /* ' '.charCodeAt(0) */];
|
||||
return [this.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
|
||||
}
|
||||
return [DEFAULT_ATTR, ' ', 1, 32 /* ' '.charCodeAt(0) */];
|
||||
return [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1786,7 +1749,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
|
||||
/**
|
||||
* Emit the 'data' event and populate the given data.
|
||||
* @param {string} data The data to populate in the event.
|
||||
* @param data The data to populate in the event.
|
||||
*/
|
||||
public handler(data: string): void {
|
||||
// Prevents all events to pty process if stdin is disabled
|
||||
@@ -1808,7 +1771,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
|
||||
/**
|
||||
* Emit the 'title' event and populate the given title.
|
||||
* @param {string} title The title to populate in the event.
|
||||
* @param title The title to populate in the event.
|
||||
*/
|
||||
public handleTitle(title: string): void {
|
||||
/**
|
||||
@@ -1851,7 +1814,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
// blankLine(true) is xterm/linux behavior
|
||||
const scrollRegionHeight = this.buffer.scrollBottom - this.buffer.scrollTop;
|
||||
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.scrollBottom);
|
||||
} else {
|
||||
@@ -1900,7 +1863,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
public matchColor(r1: number, g1: number, b1: number): number {
|
||||
const hash = (r1 << 16) | (g1 << 8) | b1;
|
||||
|
||||
if (matchColorCache[hash] != null) {
|
||||
if (matchColorCache[hash] !== null && matchColorCache[hash] !== undefined) {
|
||||
return matchColorCache[hash];
|
||||
}
|
||||
|
||||
|
||||
+25
-9
@@ -18,6 +18,8 @@ export type LineData = CharData[];
|
||||
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void;
|
||||
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
|
||||
|
||||
export type CharacterJoinerHandler = (text: string) => [number, number][];
|
||||
|
||||
export const enum LinkHoverEventTypes {
|
||||
HOVER = 'linkhover',
|
||||
TOOLTIP = 'linktooltip',
|
||||
@@ -45,6 +47,7 @@ export interface IInputHandlingTerminal extends IEventEmitter {
|
||||
wraparoundMode: boolean;
|
||||
bracketedPasteMode: boolean;
|
||||
curAttr: number;
|
||||
savedCurAttr: number;
|
||||
savedCols: number;
|
||||
x10Mouse: boolean;
|
||||
vt200Mouse: boolean;
|
||||
@@ -63,17 +66,11 @@ export interface IInputHandlingTerminal extends IEventEmitter {
|
||||
|
||||
bell(): void;
|
||||
focus(): void;
|
||||
convertEol: boolean;
|
||||
updateRange(y: number): void;
|
||||
scroll(isWrapped?: boolean): void;
|
||||
setgLevel(g: number): void;
|
||||
eraseAttr(): number;
|
||||
eraseRight(x: number, y: number): void;
|
||||
eraseLine(y: number): void;
|
||||
eraseLeft(x: number, y: number): void;
|
||||
blankLine(cur?: boolean, isWrapped?: boolean): LineData;
|
||||
is(term: string): boolean;
|
||||
send(data: string): void;
|
||||
setgCharset(g: number, charset: ICharset): void;
|
||||
resize(x: number, y: number): void;
|
||||
log(text: string, data?: any): void;
|
||||
@@ -84,6 +81,10 @@ export interface IInputHandlingTerminal extends IEventEmitter {
|
||||
error(text: string, data?: any): void;
|
||||
setOption(key: string, value: any): void;
|
||||
tabSet(): void;
|
||||
handler(data: string): void;
|
||||
handleTitle(title: string): void;
|
||||
index(): void;
|
||||
reverseIndex(): void;
|
||||
}
|
||||
|
||||
export interface IViewport extends IDisposable {
|
||||
@@ -198,6 +199,7 @@ export interface ILinkHoverEvent {
|
||||
x2: number;
|
||||
y2: number;
|
||||
cols: number;
|
||||
fg: number;
|
||||
}
|
||||
|
||||
export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
|
||||
@@ -223,12 +225,10 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce
|
||||
* @param data The data to populate in the event.
|
||||
*/
|
||||
handler(data: string): void;
|
||||
send(data: string): void;
|
||||
scrollLines(disp: number, suppressScrollEvent?: boolean): void;
|
||||
cancel(ev: Event, force?: boolean): boolean | void;
|
||||
log(text: string): void;
|
||||
showCursor(): void;
|
||||
blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData;
|
||||
}
|
||||
|
||||
export interface IBufferAccessor {
|
||||
@@ -267,7 +267,7 @@ export interface ITerminalOptions extends IPublicTerminalOptions {
|
||||
}
|
||||
|
||||
export interface IBuffer {
|
||||
readonly lines: ICircularList<LineData>;
|
||||
readonly lines: ICircularList<IBufferLine>;
|
||||
ydisp: number;
|
||||
ybase: number;
|
||||
y: number;
|
||||
@@ -506,3 +506,19 @@ export interface IEscapeSequenceParser extends IDisposable {
|
||||
setErrorHandler(callback: (state: IParsingState) => IParsingState): 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;
|
||||
}
|
||||
|
||||
+27
-6
@@ -28,6 +28,9 @@ export class Viewport extends Disposable implements IViewport {
|
||||
// quick fix and could have a more robust solution in place that reset the value when needed.
|
||||
private _wheelPartialScroll: number = 0;
|
||||
|
||||
private _refreshAnimationFrame: number | null = null;
|
||||
private _ignoreNextScrollEvent: boolean = false;
|
||||
|
||||
/**
|
||||
* Creates a new Viewport.
|
||||
* @param _terminal The terminal this viewport belongs to.
|
||||
@@ -62,6 +65,12 @@ export class Viewport extends Disposable implements IViewport {
|
||||
* necessary.
|
||||
*/
|
||||
private _refresh(): void {
|
||||
if (this._refreshAnimationFrame === null) {
|
||||
this._refreshAnimationFrame = requestAnimationFrame(() => this._innerRefresh());
|
||||
}
|
||||
}
|
||||
|
||||
private _innerRefresh(): void {
|
||||
if (this._charMeasure.height > 0) {
|
||||
this._currentRowHeight = this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio;
|
||||
this._lastRecordedViewportHeight = this._viewportElement.offsetHeight;
|
||||
@@ -71,6 +80,17 @@ export class Viewport extends Disposable implements IViewport {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,12 +110,6 @@ export class Viewport extends Disposable implements IViewport {
|
||||
this._refresh();
|
||||
}
|
||||
}
|
||||
|
||||
// Sync scrollTop
|
||||
const scrollTop = this._terminal.buffer.ydisp * this._currentRowHeight;
|
||||
if (this._viewportElement.scrollTop !== scrollTop) {
|
||||
this._viewportElement.scrollTop = scrollTop;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,6 +124,13 @@ export class Viewport extends Disposable implements IViewport {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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._viewportElement.scrollTop / this._currentRowHeight);
|
||||
const diff = newRow - this._terminal.buffer.ydisp;
|
||||
this._terminal.scrollLines(diff, true);
|
||||
|
||||
@@ -53,7 +53,7 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean
|
||||
const fileReader = new FileReader();
|
||||
|
||||
fileReader.addEventListener('load', () => {
|
||||
str = myTextDecoder.decode(this.result);
|
||||
str = myTextDecoder.decode(fileReader.result);
|
||||
displayData(str);
|
||||
});
|
||||
fileReader.readAsArrayBuffer(ev.data);
|
||||
|
||||
@@ -117,7 +117,7 @@ export class SearchHelper implements ISearchHelper {
|
||||
if (searchIndex >= 0) {
|
||||
const line = this._terminal._core.buffer.lines.get(y);
|
||||
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
|
||||
const char = charData[1/*CHAR_DATA_CHAR_INDEX*/];
|
||||
if (char.length > 1) {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 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 { ISearchHelper } from './Interfaces';
|
||||
|
||||
|
||||
class MockTerminalPlain {}
|
||||
|
||||
class MockTerminal {
|
||||
private _core: any;
|
||||
public searchHelper: ISearchHelper;
|
||||
constructor(options: any) {
|
||||
this._core = new (require('../../../lib/Terminal').Terminal)(options);
|
||||
this.searchHelper = new SearchHelper(this as any);
|
||||
}
|
||||
get core(): any {
|
||||
return this._core;
|
||||
}
|
||||
pushWriteData(): void {
|
||||
this._core._innerWrite();
|
||||
}
|
||||
}
|
||||
|
||||
describe('search addon', function(): void {
|
||||
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');
|
||||
});
|
||||
});
|
||||
it('Searchhelper - should find correct position', function(): void {
|
||||
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 as any)._findInLine('Hello', 0);
|
||||
const hello1 = (term.searchHelper as any)._findInLine('Hello', 1);
|
||||
const hello2 = (term.searchHelper as any)._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'});
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,9 @@
|
||||
import { Terminal } from 'xterm';
|
||||
import { IWinptyCompatAddonTerminal } from './Interfaces';
|
||||
|
||||
const CHAR_DATA_CODE_INDEX = 3;
|
||||
const NULL_CELL_CODE = 32;
|
||||
|
||||
export function winptyCompatInit(terminal: Terminal): void {
|
||||
const addonTerminal = <IWinptyCompatAddonTerminal>terminal;
|
||||
|
||||
@@ -27,11 +30,11 @@ export function winptyCompatInit(terminal: Terminal): void {
|
||||
// wrapped.
|
||||
addonTerminal.on('linefeed', () => {
|
||||
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[3] !== 32 /* ' ' */) {
|
||||
if (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE) {
|
||||
const nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y);
|
||||
(<any>nextLine).isWrapped = true;
|
||||
nextLine.isWrapped = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { Disposable } from './Lifecycle';
|
||||
|
||||
class TestDisposable extends Disposable {
|
||||
public get isDisposed(): boolean {
|
||||
return this._isDisposed;
|
||||
}
|
||||
}
|
||||
|
||||
describe('Disposable', () => {
|
||||
describe('register', () => {
|
||||
it('should register disposables', () => {
|
||||
const d = new TestDisposable();
|
||||
const d2 = {
|
||||
dispose: () => { throw new Error(); }
|
||||
};
|
||||
d.register(d2);
|
||||
assert.throws(() => d.dispose());
|
||||
});
|
||||
});
|
||||
describe('unregister', () => {
|
||||
it('should unregister disposables', () => {
|
||||
const d = new TestDisposable();
|
||||
const d2 = {
|
||||
dispose: () => { throw new Error(); }
|
||||
};
|
||||
d.register(d2);
|
||||
d.unregister(d2);
|
||||
assert.doesNotThrow(() => d.dispose());
|
||||
});
|
||||
});
|
||||
describe('dispose', () => {
|
||||
it('should set is disposed flag', () => {
|
||||
const d = new TestDisposable();
|
||||
assert.isFalse(d.isDisposed);
|
||||
d.dispose();
|
||||
assert.isTrue(d.isDisposed);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,7 @@ import { IDisposable } from 'xterm';
|
||||
*/
|
||||
export abstract class Disposable implements IDisposable {
|
||||
protected _disposables: IDisposable[] = [];
|
||||
protected _isDisposed: boolean = false;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
@@ -19,6 +20,7 @@ export abstract class Disposable implements IDisposable {
|
||||
* Disposes the object, triggering the `dispose` method on all registered IDisposables.
|
||||
*/
|
||||
public dispose(): void {
|
||||
this._isDisposed = true;
|
||||
this._disposables.forEach(d => d.dispose());
|
||||
this._disposables.length = 0;
|
||||
}
|
||||
@@ -30,4 +32,16 @@ export abstract class Disposable implements IDisposable {
|
||||
public register<T extends IDisposable>(d: T): void {
|
||||
this._disposables.push(d);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a disposable object if it has been registered, if not do
|
||||
* nothing.
|
||||
* @param d The disposable to unregister.
|
||||
*/
|
||||
public unregister<T extends IDisposable>(d: T): void {
|
||||
const index = this._disposables.indexOf(d);
|
||||
if (index !== -1) {
|
||||
this._disposables.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, ICircularList, LineData } from '../Types';
|
||||
import { ITerminal, ICircularList, IBufferLine } from '../Types';
|
||||
import { C0 } from '../common/data/EscapeSequences';
|
||||
|
||||
const enum Direction {
|
||||
@@ -18,7 +18,7 @@ export class AltClickHandler {
|
||||
private _startCol: number;
|
||||
private _endRow: number;
|
||||
private _endCol: number;
|
||||
private _lines: ICircularList<LineData>;
|
||||
private _lines: ICircularList<IBufferLine>;
|
||||
|
||||
constructor(
|
||||
private _mouseEvent: MouseEvent,
|
||||
@@ -50,7 +50,7 @@ export class AltClickHandler {
|
||||
*/
|
||||
public move(): void {
|
||||
if (this._mouseEvent.altKey && this._endCol !== undefined && this._endRow !== undefined) {
|
||||
this._terminal.send(this._arrowSequences());
|
||||
this._terminal.handler(this._arrowSequences());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ export class AltClickHandler {
|
||||
for (let i = 0; i < Math.abs(startRow - endRow); i++) {
|
||||
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++;
|
||||
}
|
||||
}
|
||||
@@ -152,12 +152,12 @@ export class AltClickHandler {
|
||||
*/
|
||||
private _wrappedRowsForRow(currentRow: number): number {
|
||||
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) {
|
||||
rowCount++;
|
||||
currentRow--;
|
||||
lineWraps = (<any>this._lines.get(currentRow)).isWrapped;
|
||||
lineWraps = this._lines.get(currentRow).isWrapped;
|
||||
}
|
||||
|
||||
return rowCount;
|
||||
|
||||
@@ -35,7 +35,7 @@ export function bracketTextForPaste(text: string, bracketedPasteMode: boolean):
|
||||
|
||||
/**
|
||||
* Binds copy functionality to the given terminal.
|
||||
* @param {ClipboardEvent} ev The original copy event to be handled
|
||||
* @param ev The original copy event to be handled
|
||||
*/
|
||||
export function copyHandler(ev: ClipboardEvent, term: ITerminal, selectionManager: ISelectionManager): void {
|
||||
if (term.browser.isMSIE) {
|
||||
@@ -50,8 +50,8 @@ export function copyHandler(ev: ClipboardEvent, term: ITerminal, selectionManage
|
||||
|
||||
/**
|
||||
* Redirect the clipboard's data to the terminal's input handler.
|
||||
* @param {ClipboardEvent} ev The original paste event to be handled
|
||||
* @param {Terminal} term The terminal on which to apply the handled paste event
|
||||
* @param ev The original paste event to be handled
|
||||
* @param term The terminal on which to apply the handled paste event
|
||||
*/
|
||||
export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void {
|
||||
ev.stopPropagation();
|
||||
|
||||
@@ -65,6 +65,12 @@ export class Terminal implements ITerminalApi {
|
||||
public deregisterLinkMatcher(matcherId: number): void {
|
||||
this._core.deregisterLinkMatcher(matcherId);
|
||||
}
|
||||
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
|
||||
return this._core.registerCharacterJoiner(handler);
|
||||
}
|
||||
public deregisterCharacterJoiner(joinerId: number): void {
|
||||
this._core.deregisterCharacterJoiner(joinerId);
|
||||
}
|
||||
public addMarker(cursorYOffset: number): IMarker {
|
||||
return this._core.addMarker(cursorYOffset);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
this._container.appendChild(this._canvas);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._container.removeChild(this._canvas);
|
||||
}
|
||||
|
||||
private _initCanvas(): void {
|
||||
this._ctx = this._canvas.getContext('2d', {alpha: this._alpha});
|
||||
// Draw the background if this is an opaque layer
|
||||
@@ -224,12 +228,12 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a character at a cell. If possible this will draw using the character
|
||||
* atlas to reduce draw time.
|
||||
* Draws one or more characters at a cell. If possible this will draw using
|
||||
* the character atlas to reduce draw time.
|
||||
* @param terminal The terminal.
|
||||
* @param char The character.
|
||||
* @param chars The character or characters.
|
||||
* @param code The character code.
|
||||
* @param width The width of the character.
|
||||
* @param width The width of the characters.
|
||||
* @param x The column to draw at.
|
||||
* @param y The row to draw at.
|
||||
* @param fg The foreground color, in the format stored within the attributes.
|
||||
@@ -237,33 +241,34 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* This is used to validate whether a cached image can be used.
|
||||
* @param bold Whether the text is bold.
|
||||
*/
|
||||
protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean, dim: boolean, italic: boolean): void {
|
||||
const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && bold && fg < 8;
|
||||
protected drawChars(terminal: ITerminal, chars: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean, dim: boolean, italic: boolean): void {
|
||||
const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && bold && fg < 8 && fg !== INVERTED_DEFAULT_COLOR;
|
||||
|
||||
fg += drawInBrightColor ? 8 : 0;
|
||||
const atlasDidDraw = this._charAtlas && this._charAtlas.draw(
|
||||
this._ctx,
|
||||
{char, code, bg, fg, bold: bold && terminal.options.enableBold, dim, italic},
|
||||
{chars, code, bg, fg, bold: bold && terminal.options.enableBold, dim, italic},
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop
|
||||
);
|
||||
|
||||
if (!atlasDidDraw) {
|
||||
this._drawUncachedChar(terminal, char, width, fg, x, y, bold && terminal.options.enableBold, dim, italic);
|
||||
this._drawUncachedChars(terminal, chars, width, fg, x, y, bold && terminal.options.enableBold, dim, italic);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a character at a cell. The character will be clipped to
|
||||
* ensure that it fits with the cell, including the cell to the right if it's
|
||||
* a wide character.
|
||||
* Draws one or more characters at one or more cells. The character(s) will be
|
||||
* clipped to ensure that they fit with the cell(s), including the cell to the
|
||||
* right if the last character is a wide character.
|
||||
* @param terminal The terminal.
|
||||
* @param char The character.
|
||||
* @param chars The character.
|
||||
* @param width The width of the character.
|
||||
* @param fg The foreground color, in the format stored within the attributes.
|
||||
* @param x The column to draw at.
|
||||
* @param y The row to draw at.
|
||||
*/
|
||||
private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean, italic: boolean): void {
|
||||
private _drawUncachedChars(terminal: ITerminal, chars: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean, italic: boolean): void {
|
||||
this._ctx.save();
|
||||
this._ctx.font = this._getFont(terminal, bold, italic);
|
||||
this._ctx.textBaseline = 'top';
|
||||
@@ -285,7 +290,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
}
|
||||
// Draw the character
|
||||
this._ctx.fillText(
|
||||
char,
|
||||
chars,
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop);
|
||||
this._ctx.restore();
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { MockTerminal, MockBuffer } from '../utils/TestUtils.test';
|
||||
import { CircularList } from '../common/CircularList';
|
||||
|
||||
import { ICharacterJoinerRegistry } from './Types';
|
||||
import { CharacterJoinerRegistry } from './CharacterJoinerRegistry';
|
||||
import { BufferLine } from '../BufferLine';
|
||||
import { IBufferLine } from '../Types';
|
||||
|
||||
describe('CharacterJoinerRegistry', () => {
|
||||
let registry: ICharacterJoinerRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
const terminal = new MockTerminal();
|
||||
terminal.cols = 16;
|
||||
terminal.buffer = new MockBuffer();
|
||||
const lines = new CircularList<IBufferLine>(7);
|
||||
lines.set(0, lineData([['a -> b -> c -> d']]));
|
||||
lines.set(1, lineData([['a -> b => c -> d']]));
|
||||
lines.set(2, lineData([['a -> b -', 0xFFFFFFFF], ['> c -> d', 0]]));
|
||||
|
||||
lines.set(3, lineData([['no joined ranges']]));
|
||||
lines.set(4, new BufferLine());
|
||||
lines.set(5, lineData([['a', 0x11111111], [' -> b -> c -> '], ['d', 0x22222222]]));
|
||||
const line6 = lineData([['wi']]);
|
||||
line6.push([0, '¥', 2, '¥'.charCodeAt(0)]);
|
||||
line6.push([0, '', 0, null]);
|
||||
let sub = lineData([['deemo']]);
|
||||
for (let i = 0; i < sub.length; ++i) line6.push(sub.get(i));
|
||||
line6.push([0, '\xf0\x9f\x98\x81', 1, 128513]);
|
||||
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);
|
||||
terminal.buffer.ydisp = 0;
|
||||
registry = new CharacterJoinerRegistry(terminal);
|
||||
});
|
||||
|
||||
it('has no joiners upon creation', () => {
|
||||
assert.deepEqual(registry.getJoinedCharacters(0), []);
|
||||
});
|
||||
|
||||
it('returns ranges matched by the registered joiners', () => {
|
||||
registry.registerCharacterJoiner(substringJoiner('->'));
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(0),
|
||||
[[2, 4], [7, 9], [12, 14]]
|
||||
);
|
||||
});
|
||||
|
||||
it('processes the input using all provided joiners', () => {
|
||||
registry.registerCharacterJoiner(substringJoiner('->'));
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(1),
|
||||
[[2, 4], [12, 14]]
|
||||
);
|
||||
|
||||
registry.registerCharacterJoiner(substringJoiner('=>'));
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(1),
|
||||
[[2, 4], [7, 9], [12, 14]]
|
||||
);
|
||||
});
|
||||
|
||||
it('removes deregistered joiners from future calls', () => {
|
||||
const joiner1 = registry.registerCharacterJoiner(substringJoiner('->'));
|
||||
const joiner2 = registry.registerCharacterJoiner(substringJoiner('=>'));
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(1),
|
||||
[[2, 4], [7, 9], [12, 14]]
|
||||
);
|
||||
|
||||
registry.deregisterCharacterJoiner(joiner1);
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(1),
|
||||
[[7, 9]]
|
||||
);
|
||||
|
||||
registry.deregisterCharacterJoiner(joiner2);
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(1),
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
it('doesn\'t process joins on differently-styled characters', () => {
|
||||
registry.registerCharacterJoiner(substringJoiner('->'));
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(2),
|
||||
[[2, 4], [12, 14]]
|
||||
);
|
||||
});
|
||||
|
||||
it('returns an empty list of ranges if there is nothing to be joined', () => {
|
||||
registry.registerCharacterJoiner(substringJoiner('->'));
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(3),
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
it('returns an empty list of ranges if the line is empty', () => {
|
||||
registry.registerCharacterJoiner(substringJoiner('->'));
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(4),
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
it('returns false when trying to deregister a joiner that does not exist', () => {
|
||||
registry.registerCharacterJoiner(substringJoiner('->'));
|
||||
assert.deepEqual(registry.deregisterCharacterJoiner(123), false);
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(0),
|
||||
[[2, 4], [7, 9], [12, 14]]
|
||||
);
|
||||
});
|
||||
|
||||
it('doesn\'t process same-styled ranges that only have one character', () => {
|
||||
registry.registerCharacterJoiner(substringJoiner('a'));
|
||||
registry.registerCharacterJoiner(substringJoiner('b'));
|
||||
registry.registerCharacterJoiner(substringJoiner('d'));
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(5),
|
||||
[[5, 6]]
|
||||
);
|
||||
});
|
||||
|
||||
it('handles ranges that extend all the way to the end of the line', () => {
|
||||
registry.registerCharacterJoiner(substringJoiner('-> d'));
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(2),
|
||||
[[12, 16]]
|
||||
);
|
||||
});
|
||||
|
||||
it('handles adjacent ranges', () => {
|
||||
registry.registerCharacterJoiner(substringJoiner('->'));
|
||||
registry.registerCharacterJoiner(substringJoiner('> c '));
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(2),
|
||||
[[2, 4], [8, 12], [12, 14]]
|
||||
);
|
||||
});
|
||||
|
||||
it('handles fullwidth characters in the middle of ranges', () => {
|
||||
registry.registerCharacterJoiner(substringJoiner('wi¥de'));
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(6),
|
||||
[[0, 6]]
|
||||
);
|
||||
});
|
||||
|
||||
it('handles fullwidth characters at the end of ranges', () => {
|
||||
registry.registerCharacterJoiner(substringJoiner('wi¥'));
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(6),
|
||||
[[0, 4]]
|
||||
);
|
||||
});
|
||||
|
||||
it('handles emojis in the middle of ranges', () => {
|
||||
registry.registerCharacterJoiner(substringJoiner('emo\xf0\x9f\x98\x81 ji'));
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(6),
|
||||
[[6, 13]]
|
||||
);
|
||||
});
|
||||
|
||||
it('handles emojis at the end of ranges', () => {
|
||||
registry.registerCharacterJoiner(substringJoiner('emo\xf0\x9f\x98\x81 '));
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(6),
|
||||
[[6, 11]]
|
||||
);
|
||||
});
|
||||
|
||||
it('handles ranges after wide and emoji characters', () => {
|
||||
registry.registerCharacterJoiner(substringJoiner('abc'));
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(6),
|
||||
[[13, 16]]
|
||||
);
|
||||
});
|
||||
|
||||
describe('range merging', () => {
|
||||
it('inserts a new range before the existing ones', () => {
|
||||
registry.registerCharacterJoiner(() => [[1, 2], [2, 3]]);
|
||||
registry.registerCharacterJoiner(() => [[0, 1]]);
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(0),
|
||||
[[0, 1], [1, 2], [2, 3]]
|
||||
);
|
||||
});
|
||||
|
||||
it('inserts in between two ranges', () => {
|
||||
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
|
||||
registry.registerCharacterJoiner(() => [[2, 4]]);
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(0),
|
||||
[[0, 2], [2, 4], [4, 6]]
|
||||
);
|
||||
});
|
||||
|
||||
it('inserts after the last range', () => {
|
||||
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
|
||||
registry.registerCharacterJoiner(() => [[6, 8]]);
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(0),
|
||||
[[0, 2], [4, 6], [6, 8]]
|
||||
);
|
||||
});
|
||||
|
||||
it('extends the beginning of a range', () => {
|
||||
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
|
||||
registry.registerCharacterJoiner(() => [[3, 5]]);
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(0),
|
||||
[[0, 2], [3, 6]]
|
||||
);
|
||||
});
|
||||
|
||||
it('extends the end of a range', () => {
|
||||
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
|
||||
registry.registerCharacterJoiner(() => [[1, 4]]);
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(0),
|
||||
[[0, 4], [4, 6]]
|
||||
);
|
||||
});
|
||||
|
||||
it('extends the last range', () => {
|
||||
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
|
||||
registry.registerCharacterJoiner(() => [[5, 7]]);
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(0),
|
||||
[[0, 2], [4, 7]]
|
||||
);
|
||||
});
|
||||
|
||||
it('connects two ranges', () => {
|
||||
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
|
||||
registry.registerCharacterJoiner(() => [[1, 5]]);
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(0),
|
||||
[[0, 6]]
|
||||
);
|
||||
});
|
||||
|
||||
it('connects more than two ranges', () => {
|
||||
registry.registerCharacterJoiner(() => [[0, 2], [4, 6], [8, 10], [12, 14]]);
|
||||
registry.registerCharacterJoiner(() => [[1, 10]]);
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(0),
|
||||
[[0, 10], [12, 14]]
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
type IPartialLineData = ([string] | [string, number]);
|
||||
|
||||
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][] {
|
||||
return (sequence: string): [number, number][] => {
|
||||
const ranges: [number, number][] = [];
|
||||
let searchIndex = 0;
|
||||
let matchIndex = -1;
|
||||
|
||||
while ((matchIndex = sequence.indexOf(substring, searchIndex)) !== -1) {
|
||||
const matchEndIndex = matchIndex + substring.length;
|
||||
searchIndex = matchEndIndex;
|
||||
ranges.push([matchIndex, matchEndIndex]);
|
||||
}
|
||||
|
||||
return ranges;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
|
||||
import { ITerminal, IBufferLine } from '../Types';
|
||||
import { ICharacterJoinerRegistry, ICharacterJoiner } from './Types';
|
||||
|
||||
export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
|
||||
|
||||
private _characterJoiners: ICharacterJoiner[] = [];
|
||||
private _nextCharacterJoinerId: number = 0;
|
||||
|
||||
constructor(private _terminal: ITerminal) {
|
||||
}
|
||||
|
||||
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
|
||||
const joiner: ICharacterJoiner = {
|
||||
id: this._nextCharacterJoinerId++,
|
||||
handler
|
||||
};
|
||||
|
||||
this._characterJoiners.push(joiner);
|
||||
return joiner.id;
|
||||
}
|
||||
|
||||
public deregisterCharacterJoiner(joinerId: number): boolean {
|
||||
for (let i = 0; i < this._characterJoiners.length; i++) {
|
||||
if (this._characterJoiners[i].id === joinerId) {
|
||||
this._characterJoiners.splice(i, 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public getJoinedCharacters(row: number): [number, number][] {
|
||||
if (this._characterJoiners.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const line = this._terminal.buffer.lines.get(row);
|
||||
if (line.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const ranges: [number, number][] = [];
|
||||
const lineStr = this._terminal.buffer.translateBufferLineToString(row, true);
|
||||
|
||||
// Because some cells can be represented by multiple javascript characters,
|
||||
// we track the cell and the string indexes separately. This allows us to
|
||||
// translate the string ranges we get from the joiners back into cell ranges
|
||||
// for use when rendering
|
||||
let rangeStartColumn = 0;
|
||||
let currentStringIndex = 0;
|
||||
let rangeStartStringIndex = 0;
|
||||
let rangeAttr = line.get(0)[CHAR_DATA_ATTR_INDEX] >> 9;
|
||||
|
||||
for (let x = 0; x < this._terminal.cols; x++) {
|
||||
const charData = line.get(x);
|
||||
const chars = charData[CHAR_DATA_CHAR_INDEX];
|
||||
const width = charData[CHAR_DATA_WIDTH_INDEX];
|
||||
const attr = charData[CHAR_DATA_ATTR_INDEX] >> 9;
|
||||
|
||||
if (width === 0) {
|
||||
// If this character is of width 0, skip it.
|
||||
continue;
|
||||
}
|
||||
|
||||
// End of range
|
||||
if (attr !== rangeAttr) {
|
||||
// If we ended up with a sequence of more than one character,
|
||||
// look for ranges to join.
|
||||
if (x - rangeStartColumn > 1) {
|
||||
const joinedRanges = this._getJoinedRanges(
|
||||
lineStr,
|
||||
rangeStartStringIndex,
|
||||
currentStringIndex,
|
||||
line,
|
||||
rangeStartColumn
|
||||
);
|
||||
for (let i = 0; i < joinedRanges.length; i++) {
|
||||
ranges.push(joinedRanges[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset our markers for a new range.
|
||||
rangeStartColumn = x;
|
||||
rangeStartStringIndex = currentStringIndex;
|
||||
rangeAttr = attr;
|
||||
}
|
||||
|
||||
currentStringIndex += chars.length;
|
||||
}
|
||||
|
||||
// Process any trailing ranges.
|
||||
if (this._terminal.cols - rangeStartColumn > 1) {
|
||||
const joinedRanges = this._getJoinedRanges(
|
||||
lineStr,
|
||||
rangeStartStringIndex,
|
||||
currentStringIndex,
|
||||
line,
|
||||
rangeStartColumn
|
||||
);
|
||||
for (let i = 0; i < joinedRanges.length; i++) {
|
||||
ranges.push(joinedRanges[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return ranges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a segment of a line of text, find all ranges of text that should be
|
||||
* joined in a single rendering unit. Ranges are internally converted to
|
||||
* column ranges, rather than string ranges.
|
||||
* @param line String representation of the full line of text
|
||||
* @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)
|
||||
*/
|
||||
private _getJoinedRanges(line: string, startIndex: number, endIndex: number, lineData: IBufferLine, startCol: number): [number, number][] {
|
||||
const text = line.substring(startIndex, endIndex);
|
||||
// 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
|
||||
// merging it into an empty array, which incurs unnecessary writes.
|
||||
const joinedRanges: [number, number][] = this._characterJoiners[0].handler(text);
|
||||
for (let i = 1; i < this._characterJoiners.length; i++) {
|
||||
// We merge any overlapping ranges across the different joiners
|
||||
const joinerRanges = this._characterJoiners[i].handler(text);
|
||||
for (let j = 0; j < joinerRanges.length; j++) {
|
||||
CharacterJoinerRegistry._mergeRanges(joinedRanges, joinerRanges[j]);
|
||||
}
|
||||
}
|
||||
this._stringRangesToCellRanges(joinedRanges, lineData, startCol);
|
||||
return joinedRanges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the provided ranges in-place to adjust for variations between
|
||||
* string length and cell width so that the range represents a cell range,
|
||||
* rather than the string range the joiner provides.
|
||||
* @param ranges String ranges containing start (inclusive) and end (exclusive) index
|
||||
* @param line Cell data for the relevant line in the terminal
|
||||
* @param startCol Offset within the line to start from
|
||||
*/
|
||||
private _stringRangesToCellRanges(ranges: [number, number][], line: IBufferLine, startCol: number): void {
|
||||
let currentRangeIndex = 0;
|
||||
let currentRangeStarted = false;
|
||||
let currentStringIndex = 0;
|
||||
let currentRange = ranges[currentRangeIndex];
|
||||
|
||||
// If we got through all of the ranges, stop searching
|
||||
if (!currentRange) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let x = startCol; x < this._terminal.cols; x++) {
|
||||
const charData = line.get(x);
|
||||
const width = charData[CHAR_DATA_WIDTH_INDEX];
|
||||
const length = charData[CHAR_DATA_CHAR_INDEX].length;
|
||||
|
||||
// We skip zero-width characters when creating the string to join the text
|
||||
// so we do the same here
|
||||
if (width === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Adjust the start of the range
|
||||
if (!currentRangeStarted && currentRange[0] <= currentStringIndex) {
|
||||
currentRange[0] = x;
|
||||
currentRangeStarted = true;
|
||||
}
|
||||
|
||||
// Adjust the end of the range
|
||||
if (currentRange[1] <= currentStringIndex) {
|
||||
currentRange[1] = x;
|
||||
|
||||
// We're finished with this range, so we move to the next one
|
||||
currentRange = ranges[++currentRangeIndex];
|
||||
|
||||
// If there are no more ranges left, stop searching
|
||||
if (!currentRange) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Ranges can be on adjacent characters. Because the end index of the
|
||||
// ranges are exclusive, this means that the index for the start of a
|
||||
// range can be the same as the end index of the previous range. To
|
||||
// account for the start of the next range, we check here just in case.
|
||||
if (currentRange[0] <= currentStringIndex) {
|
||||
currentRange[0] = x;
|
||||
currentRangeStarted = true;
|
||||
} else {
|
||||
currentRangeStarted = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust the string index based on the character length to line up with
|
||||
// the column adjustment
|
||||
currentStringIndex += length;
|
||||
}
|
||||
|
||||
// If there is still a range left at the end, it must extend all the way to
|
||||
// the end of the line.
|
||||
if (currentRange) {
|
||||
currentRange[1] = this._terminal.cols;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the range defined by the provided start and end into the list of
|
||||
* existing ranges. The merge is done in place on the existing range for
|
||||
* performance and is also returned.
|
||||
* @param ranges Existing range list
|
||||
* @param newRange Tuple of two numbers representing the new range to merge in.
|
||||
* @returns The ranges input with the new range merged in place
|
||||
*/
|
||||
private static _mergeRanges(ranges: [number, number][], newRange: [number, number]): [number, number][] {
|
||||
let inRange = false;
|
||||
for (let i = 0; i < ranges.length; i++) {
|
||||
const range = ranges[i];
|
||||
if (!inRange) {
|
||||
if (newRange[1] <= range[0]) {
|
||||
// Case 1: New range is before the search range
|
||||
ranges.splice(i, 0, newRange);
|
||||
return ranges;
|
||||
}
|
||||
|
||||
if (newRange[1] <= range[1]) {
|
||||
// Case 2: New range is either wholly contained within the
|
||||
// search range or overlaps with the front of it
|
||||
range[0] = Math.min(newRange[0], range[0]);
|
||||
return ranges;
|
||||
}
|
||||
|
||||
if (newRange[0] < range[1]) {
|
||||
// Case 3: New range either wholly contains the search range
|
||||
// or overlaps with the end of it
|
||||
range[0] = Math.min(newRange[0], range[0]);
|
||||
inRange = true;
|
||||
}
|
||||
|
||||
// Case 4: New range starts after the search range
|
||||
continue;
|
||||
} else {
|
||||
if (newRange[1] <= range[0]) {
|
||||
// Case 5: New range extends from previous range but doesn't
|
||||
// reach the current one
|
||||
ranges[i - 1][1] = newRange[1];
|
||||
return ranges;
|
||||
}
|
||||
|
||||
if (newRange[1] <= range[1]) {
|
||||
// Case 6: New range extends from prvious range into the
|
||||
// current range
|
||||
ranges[i - 1][1] = Math.max(newRange[1], range[1]);
|
||||
ranges.splice(i, 1);
|
||||
inRange = false;
|
||||
return ranges;
|
||||
}
|
||||
|
||||
// Case 7: New range extends from previous range past the
|
||||
// end of the current range
|
||||
ranges.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
if (inRange) {
|
||||
// Case 8: New range extends past the last existing range
|
||||
ranges[ranges.length - 1][1] = newRange[1];
|
||||
} else {
|
||||
// Case 9: New range starts after the last existing range
|
||||
ranges.push(newRange);
|
||||
}
|
||||
|
||||
return ranges;
|
||||
}
|
||||
}
|
||||
@@ -127,7 +127,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
return;
|
||||
}
|
||||
|
||||
const charData = terminal.buffer.lines.get(cursorY)[terminal.buffer.x];
|
||||
const charData = terminal.buffer.lines.get(cursorY).get(terminal.buffer.x);
|
||||
if (!charData) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { ILinkHoverEvent, ITerminal, ILinkifierAccessor, LinkHoverEventTypes } from '../Types';
|
||||
import { IColorSet, IRenderDimensions } from './Types';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { INVERTED_DEFAULT_COLOR } from './atlas/Types';
|
||||
|
||||
export class LinkRenderLayer extends BaseRenderLayer {
|
||||
private _state: ILinkHoverEvent = null;
|
||||
@@ -39,7 +40,15 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
|
||||
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) {
|
||||
// Single line link
|
||||
this.fillBottomLineAtCells(e.x1, e.y1, e.x2 - e.x1);
|
||||
|
||||
@@ -7,13 +7,14 @@ import { TextRenderLayer } from './TextRenderLayer';
|
||||
import { SelectionRenderLayer } from './SelectionRenderLayer';
|
||||
import { CursorRenderLayer } from './CursorRenderLayer';
|
||||
import { ColorManager } from './ColorManager';
|
||||
import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Types';
|
||||
import { ITerminal } from '../Types';
|
||||
import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions, ICharacterJoinerRegistry } from './Types';
|
||||
import { ITerminal, CharacterJoinerHandler } from '../Types';
|
||||
import { LinkRenderLayer } from './LinkRenderLayer';
|
||||
import { EventEmitter } from '../EventEmitter';
|
||||
import { RenderDebouncer } from '../ui/RenderDebouncer';
|
||||
import { ScreenDprMonitor } from '../ui/ScreenDprMonitor';
|
||||
import { ITheme } from 'xterm';
|
||||
import { CharacterJoinerRegistry } from '../renderer/CharacterJoinerRegistry';
|
||||
|
||||
export class Renderer extends EventEmitter implements IRenderer {
|
||||
private _renderDebouncer: RenderDebouncer;
|
||||
@@ -23,6 +24,7 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
private _screenDprMonitor: ScreenDprMonitor;
|
||||
private _isPaused: boolean = false;
|
||||
private _needsFullRefresh: boolean = false;
|
||||
private _characterJoinerRegistry: ICharacterJoinerRegistry;
|
||||
|
||||
public colorManager: ColorManager;
|
||||
public dimensions: IRenderDimensions;
|
||||
@@ -31,12 +33,13 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
super();
|
||||
const allowTransparency = this._terminal.options.allowTransparency;
|
||||
this.colorManager = new ColorManager(document, allowTransparency);
|
||||
this._characterJoinerRegistry = new CharacterJoinerRegistry(_terminal);
|
||||
if (theme) {
|
||||
this.colorManager.setTheme(theme);
|
||||
}
|
||||
|
||||
this._renderLayers = [
|
||||
new TextRenderLayer(this._terminal.screenElement, 0, this.colorManager.colors, allowTransparency),
|
||||
new TextRenderLayer(this._terminal.screenElement, 0, this.colorManager.colors, this._characterJoinerRegistry, allowTransparency),
|
||||
new SelectionRenderLayer(this._terminal.screenElement, 1, this.colorManager.colors),
|
||||
new LinkRenderLayer(this._terminal.screenElement, 2, this.colorManager.colors, this._terminal),
|
||||
new CursorRenderLayer(this._terminal.screenElement, 3, this.colorManager.colors)
|
||||
@@ -67,12 +70,17 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
// Detect whether IntersectionObserver is detected and enable renderer pause
|
||||
// and resume based on terminal visibility if so
|
||||
if ('IntersectionObserver' in window) {
|
||||
const observer = new IntersectionObserver(e => this.onIntersectionChange(e[0]), {threshold: 0});
|
||||
const observer = new IntersectionObserver(e => this.onIntersectionChange(e[0]), { threshold: 0 });
|
||||
observer.observe(this._terminal.element);
|
||||
this.register({ dispose: () => observer.disconnect() });
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
this._renderLayers.forEach(l => l.dispose());
|
||||
}
|
||||
|
||||
public onIntersectionChange(entry: IntersectionObserverEntry): void {
|
||||
this._isPaused = entry.intersectionRatio === 0;
|
||||
if (!this._isPaused && this._needsFullRefresh) {
|
||||
@@ -171,8 +179,8 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
/**
|
||||
* Queues a refresh between two rows (inclusive), to be done on next animation
|
||||
* frame.
|
||||
* @param {number} start The start row.
|
||||
* @param {number} end The end row.
|
||||
* @param start The start row.
|
||||
* @param end The end row.
|
||||
*/
|
||||
public refreshRows(start: number, end: number): void {
|
||||
if (this._isPaused) {
|
||||
@@ -188,7 +196,7 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
*/
|
||||
private _renderRows(start: number, end: number): void {
|
||||
this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end));
|
||||
this._terminal.emit('refresh', {start, end});
|
||||
this._terminal.emit('refresh', { start, end });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -250,4 +258,12 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows;
|
||||
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols;
|
||||
}
|
||||
|
||||
public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
|
||||
return this._characterJoinerRegistry.registerCharacterJoiner(handler);
|
||||
}
|
||||
|
||||
public deregisterCharacterJoiner(joinerId: number): boolean {
|
||||
return this._characterJoinerRegistry.deregisterCharacterJoiner(joinerId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
||||
this._ctx.fillStyle = this._colors.selection.css;
|
||||
|
||||
if (columnSelectMode) {
|
||||
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
|
||||
const startCol = start[0];
|
||||
const width = end[0] - startCol;
|
||||
const height = viewportCappedEndRow - viewportCappedStartRow + 1;
|
||||
this.fillCells(startCol, viewportCappedStartRow, width, height);
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../Buffer';
|
||||
import { FLAGS, IColorSet, IRenderDimensions } from './Types';
|
||||
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, NULL_CELL_CODE } from '../Buffer';
|
||||
import { FLAGS, IColorSet, IRenderDimensions, ICharacterJoinerRegistry } from './Types';
|
||||
import { CharData, ITerminal } from '../Types';
|
||||
import { INVERTED_DEFAULT_COLOR } from './atlas/Types';
|
||||
import { GridCache } from './GridCache';
|
||||
@@ -22,10 +22,12 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
private _characterWidth: number;
|
||||
private _characterFont: string;
|
||||
private _characterOverlapCache: { [key: string]: boolean } = {};
|
||||
private _characterJoinerRegistry: ICharacterJoinerRegistry;
|
||||
|
||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, alpha: boolean) {
|
||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, characterJoinerRegistry: ICharacterJoinerRegistry, alpha: boolean) {
|
||||
super(container, 'text', zIndex, alpha, colors);
|
||||
this._state = new GridCache<CharData>();
|
||||
this._characterJoinerRegistry = characterJoinerRegistry;
|
||||
}
|
||||
|
||||
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
|
||||
@@ -52,9 +54,10 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
terminal: ITerminal,
|
||||
firstRow: number,
|
||||
lastRow: number,
|
||||
joinerRegistry: ICharacterJoinerRegistry | null,
|
||||
callback: (
|
||||
code: number,
|
||||
char: string,
|
||||
chars: string,
|
||||
width: number,
|
||||
x: number,
|
||||
y: number,
|
||||
@@ -66,22 +69,54 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
for (let y = firstRow; y <= lastRow; y++) {
|
||||
const row = y + terminal.buffer.ydisp;
|
||||
const line = terminal.buffer.lines.get(row);
|
||||
const joinedRanges = joinerRegistry ? joinerRegistry.getJoinedCharacters(row) : [];
|
||||
for (let x = 0; x < terminal.cols; x++) {
|
||||
const charData = line[x];
|
||||
const code: number = <number>charData[CHAR_DATA_CODE_INDEX];
|
||||
const char: string = charData[CHAR_DATA_CHAR_INDEX];
|
||||
const charData = line.get(x);
|
||||
let code: number = <number>charData[CHAR_DATA_CODE_INDEX];
|
||||
|
||||
// Can either represent character(s) for a single cell or multiple cells
|
||||
// if indicated by a character joiner.
|
||||
let chars: string = charData[CHAR_DATA_CHAR_INDEX];
|
||||
const attr: number = charData[CHAR_DATA_ATTR_INDEX];
|
||||
let width: number = charData[CHAR_DATA_WIDTH_INDEX];
|
||||
|
||||
// If true, indicates that the current character(s) to draw were joined.
|
||||
let isJoined = false;
|
||||
let lastCharX = x;
|
||||
|
||||
// The character to the left is a wide character, drawing is owned by
|
||||
// the char at x-1
|
||||
if (width === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the character is an overlapping char and the character to the right is a
|
||||
// space, take ownership of the cell to the right.
|
||||
if (this._isOverlapping(charData)) {
|
||||
// Process any joined character ranges as needed. Because of how the
|
||||
// ranges are produced, we know that they are valid for the characters
|
||||
// and attributes of our input.
|
||||
if (joinedRanges.length > 0 && x === joinedRanges[0][0]) {
|
||||
isJoined = true;
|
||||
const range = joinedRanges.shift();
|
||||
|
||||
// We already know the exact start and end column of the joined range,
|
||||
// so we get the string and width representing it directly
|
||||
chars = terminal.buffer.translateBufferLineToString(
|
||||
row,
|
||||
true,
|
||||
range[0],
|
||||
range[1]
|
||||
);
|
||||
width = range[1] - range[0];
|
||||
code = Infinity;
|
||||
|
||||
// Skip over the cells occupied by this range in the loop
|
||||
lastCharX = range[1] - 1;
|
||||
}
|
||||
|
||||
// If the character is an overlapping char and the character to the
|
||||
// right is a space, take ownership of the cell to the right. We skip
|
||||
// this check for joined characters because their rendering likely won't
|
||||
// yield the same result as rendering the last character individually.
|
||||
if (!isJoined && this._isOverlapping(charData)) {
|
||||
// If the character is overlapping, we want to force a re-render on every
|
||||
// frame. This is specifically to work around the case where two
|
||||
// overlaping chars `a` and `b` are adjacent, the cursor is moved to b and a
|
||||
@@ -89,7 +124,7 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
// get removed, and `a` would not re-render because it thinks it's
|
||||
// already in the correct state.
|
||||
// this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA;
|
||||
if (x < line.length - 1 && line[x + 1][CHAR_DATA_CODE_INDEX] === 32 /*' '*/) {
|
||||
if (lastCharX < line.length - 1 && line.get(lastCharX + 1)[CHAR_DATA_CODE_INDEX] === NULL_CELL_CODE) {
|
||||
width = 2;
|
||||
// this._clearChar(x + 1, y);
|
||||
// The overlapping char's char data will force a clear and render when the
|
||||
@@ -116,7 +151,18 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
}
|
||||
|
||||
callback(code, char, width, x, y, fg, bg, flags);
|
||||
callback(
|
||||
code,
|
||||
chars,
|
||||
width,
|
||||
x,
|
||||
y,
|
||||
fg,
|
||||
bg,
|
||||
flags
|
||||
);
|
||||
|
||||
x = lastCharX;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,7 +180,7 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
|
||||
ctx.save();
|
||||
|
||||
this._forEachCell(terminal, firstRow, lastRow, (code, char, width, x, y, fg, bg, flags) => {
|
||||
this._forEachCell(terminal, firstRow, lastRow, null, (code, chars, width, x, y, fg, bg, flags) => {
|
||||
// libvte and xterm both draw the background (but not foreground) of invisible characters,
|
||||
// so we should too.
|
||||
let nextFillStyle = null; // null represents default background color
|
||||
@@ -176,7 +222,7 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
|
||||
private _drawForeground(terminal: ITerminal, firstRow: number, lastRow: number): void {
|
||||
this._forEachCell(terminal, firstRow, lastRow, (code, char, width, x, y, fg, bg, flags) => {
|
||||
this._forEachCell(terminal, firstRow, lastRow, this._characterJoinerRegistry, (code, chars, width, x, y, fg, bg, flags) => {
|
||||
if (flags & FLAGS.INVISIBLE) {
|
||||
return;
|
||||
}
|
||||
@@ -190,11 +236,11 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.foreground.css;
|
||||
}
|
||||
this.fillBottomLineAtCells(x, y);
|
||||
this.fillBottomLineAtCells(x, y, width);
|
||||
this._ctx.restore();
|
||||
}
|
||||
this.drawChar(
|
||||
terminal, char, code,
|
||||
this.drawChars(
|
||||
terminal, chars, code,
|
||||
width, x, y,
|
||||
fg, bg,
|
||||
!!(flags & FLAGS.BOLD), !!(flags & FLAGS.DIM), !!(flags & FLAGS.ITALIC)
|
||||
|
||||
+25
-2
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal } from '../Types';
|
||||
import { ITerminal, CharacterJoinerHandler } from '../Types';
|
||||
import { IEventEmitter, ITheme, IDisposable } from 'xterm';
|
||||
import { IColorSet } from '../shared/Types';
|
||||
|
||||
@@ -40,6 +40,8 @@ export interface IRenderer extends IEventEmitter, IDisposable {
|
||||
onOptionsChanged(): void;
|
||||
clear(): void;
|
||||
refreshRows(start: number, end: number): void;
|
||||
registerCharacterJoiner(handler: CharacterJoinerHandler): number;
|
||||
deregisterCharacterJoiner(joinerId: number): boolean;
|
||||
}
|
||||
|
||||
export interface IColorManager {
|
||||
@@ -64,7 +66,7 @@ export interface IRenderDimensions {
|
||||
actualCellHeight: number;
|
||||
}
|
||||
|
||||
export interface IRenderLayer {
|
||||
export interface IRenderLayer extends IDisposable {
|
||||
/**
|
||||
* Called when the terminal loses focus.
|
||||
*/
|
||||
@@ -101,6 +103,16 @@ export interface IRenderLayer {
|
||||
*/
|
||||
onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number], columnSelectMode: boolean): void;
|
||||
|
||||
/**
|
||||
* Registers a handler to join characters to render as a group
|
||||
*/
|
||||
registerCharacterJoiner?(joiner: ICharacterJoiner): void;
|
||||
|
||||
/**
|
||||
* Deregisters the specified character joiner handler
|
||||
*/
|
||||
deregisterCharacterJoiner?(joinerId: number): void;
|
||||
|
||||
/**
|
||||
* Resize the render layer.
|
||||
*/
|
||||
@@ -111,3 +123,14 @@ export interface IRenderLayer {
|
||||
*/
|
||||
reset(terminal: ITerminal): void;
|
||||
}
|
||||
|
||||
export interface ICharacterJoiner {
|
||||
id: number;
|
||||
handler: CharacterJoinerHandler;
|
||||
}
|
||||
|
||||
export interface ICharacterJoinerRegistry {
|
||||
registerCharacterJoiner(handler: (text: string) => [number, number][]): number;
|
||||
deregisterCharacterJoiner(joinerId: number): boolean;
|
||||
getJoinedCharacters(row: number): [number, number][];
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ interface IGlyphCacheValue {
|
||||
|
||||
function getGlyphCacheKey(glyph: IGlyphIdentifier): string {
|
||||
const styleFlags = (glyph.bold ? 0 : 4) + (glyph.dim ? 0 : 2) + (glyph.italic ? 0 : 1);
|
||||
return `${glyph.bg}_${glyph.fg}_${styleFlags}${glyph.char}`;
|
||||
return `${glyph.bg}_${glyph.fg}_${styleFlags}${glyph.chars}`;
|
||||
}
|
||||
|
||||
export default class DynamicCharAtlas extends BaseCharAtlas {
|
||||
@@ -94,7 +94,7 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
|
||||
): boolean {
|
||||
const glyphKey = getGlyphCacheKey(glyph);
|
||||
const cacheValue = this._cacheMap.get(glyphKey);
|
||||
if (cacheValue != null) {
|
||||
if (cacheValue !== null && cacheValue !== undefined) {
|
||||
this._drawFromCache(ctx, cacheValue, x, y);
|
||||
return true;
|
||||
} else if (this._canCache(glyph) && this._drawToCacheCount < FRAME_CACHE_DRAW_LIMIT) {
|
||||
@@ -216,7 +216,7 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
|
||||
this._tmpCtx.globalAlpha = DIM_OPACITY;
|
||||
}
|
||||
// Draw the character
|
||||
this._tmpCtx.fillText(glyph.char, 0, 0);
|
||||
this._tmpCtx.fillText(glyph.chars, 0, 0);
|
||||
this._tmpCtx.restore();
|
||||
|
||||
// clear the background from the character to avoid issues with drawing over the previous
|
||||
|
||||
@@ -53,7 +53,7 @@ export default class StaticCharAtlas extends BaseCharAtlas {
|
||||
y: number
|
||||
): boolean {
|
||||
// we're not warmed up yet
|
||||
if (this._texture == null) {
|
||||
if (this._texture === null || this._texture === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ export const INVERTED_DEFAULT_COLOR = -1;
|
||||
export const DIM_OPACITY = 0.5;
|
||||
|
||||
export interface IGlyphIdentifier {
|
||||
char: string;
|
||||
chars: string;
|
||||
code: number;
|
||||
bg: number;
|
||||
fg: number;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { IRenderer, IRenderDimensions, IColorSet } from '../Types';
|
||||
import { ITerminal } from '../../Types';
|
||||
import { ITerminal, CharacterJoinerHandler } from '../../Types';
|
||||
import { ITheme } from 'xterm';
|
||||
import { EventEmitter } from '../../EventEmitter';
|
||||
import { ColorManager } from '../ColorManager';
|
||||
@@ -52,7 +52,7 @@ export class DomRenderer extends EventEmitter implements IRenderer {
|
||||
this._rowContainer.classList.add(ROW_CONTAINER_CLASS);
|
||||
this._rowContainer.style.lineHeight = 'normal';
|
||||
this._rowContainer.setAttribute('aria-hidden', 'true');
|
||||
this._refreshRowElements(this._terminal.rows, this._terminal.cols);
|
||||
this._refreshRowElements(this._terminal.cols, this._terminal.rows);
|
||||
this._selectionContainer = document.createElement('div');
|
||||
this._selectionContainer.classList.add(SELECTION_CLASS);
|
||||
this._selectionContainer.setAttribute('aria-hidden', 'true');
|
||||
@@ -81,6 +81,15 @@ export class DomRenderer extends EventEmitter implements IRenderer {
|
||||
this._terminal.screenElement.appendChild(this._selectionContainer);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._terminal.element.classList.remove(TERMINAL_CLASS_PREFIX + this._terminalClass);
|
||||
this._terminal.screenElement.removeChild(this._rowContainer);
|
||||
this._terminal.screenElement.removeChild(this._selectionContainer);
|
||||
this._terminal.screenElement.removeChild(this._themeStyleElement);
|
||||
this._terminal.screenElement.removeChild(this._dimensionsStyleElement);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private _updateDimensions(): void {
|
||||
this.dimensions.scaledCharWidth = this._terminal.charMeasure.width * window.devicePixelRatio;
|
||||
this.dimensions.scaledCharHeight = this._terminal.charMeasure.height * window.devicePixelRatio;
|
||||
@@ -241,14 +250,14 @@ export class DomRenderer extends EventEmitter implements IRenderer {
|
||||
|
||||
// Create the selections
|
||||
const documentFragment = document.createDocumentFragment();
|
||||
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
|
||||
|
||||
if (columnSelectMode) {
|
||||
documentFragment.appendChild(
|
||||
this._createSelectionElement(viewportCappedStartRow, startCol, end[0], viewportCappedEndRow - viewportStartRow + 1)
|
||||
this._createSelectionElement(viewportCappedStartRow, start[0], end[0], viewportCappedEndRow - viewportCappedStartRow + 1)
|
||||
);
|
||||
} else {
|
||||
// Draw first row
|
||||
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
|
||||
const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
|
||||
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol));
|
||||
// Draw middle rows
|
||||
@@ -319,4 +328,7 @@ export class DomRenderer extends EventEmitter implements IRenderer {
|
||||
private get _terminalSelector(): string {
|
||||
return `.${TERMINAL_CLASS_PREFIX}${this._terminalClass}`;
|
||||
}
|
||||
|
||||
public registerCharacterJoiner(handler: CharacterJoinerHandler): number { return -1; }
|
||||
public deregisterCharacterJoiner(joinerId: number): boolean { return false; }
|
||||
}
|
||||
|
||||
@@ -6,14 +6,15 @@
|
||||
import jsdom = require('jsdom');
|
||||
import { assert } from 'chai';
|
||||
import { DomRendererRowFactory } from './DomRendererRowFactory';
|
||||
import { LineData } from '../../Types';
|
||||
import { DEFAULT_ATTR } from '../../Buffer';
|
||||
import { DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from '../../Buffer';
|
||||
import { FLAGS } from '../Types';
|
||||
import { BufferLine } from '../../BufferLine';
|
||||
import { IBufferLine } from '../../Types';
|
||||
|
||||
describe('DomRendererRowFactory', () => {
|
||||
let dom: jsdom.JSDOM;
|
||||
let rowFactory: DomRendererRowFactory;
|
||||
let lineData: LineData;
|
||||
let lineData: IBufferLine;
|
||||
|
||||
beforeEach(() => {
|
||||
dom = new jsdom.JSDOM('');
|
||||
@@ -31,9 +32,9 @@ describe('DomRendererRowFactory', () => {
|
||||
});
|
||||
|
||||
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
|
||||
lineData[1] = [DEFAULT_ATTR, '', 0, undefined];
|
||||
lineData.set(1, [DEFAULT_ATTR, '', 0, undefined]);
|
||||
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span style="width: 10px;">語</span>'
|
||||
@@ -49,8 +50,8 @@ describe('DomRendererRowFactory', () => {
|
||||
});
|
||||
|
||||
it('should not render cells that go beyond the terminal\'s columns', () => {
|
||||
lineData[0] = [DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)];
|
||||
lineData[1] = [DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)];
|
||||
lineData.set(0, [DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
lineData.set(1, [DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]);
|
||||
const fragment = rowFactory.createRow(lineData, false, 0, 5, 1);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span>a</span>'
|
||||
@@ -59,7 +60,7 @@ describe('DomRendererRowFactory', () => {
|
||||
|
||||
describe('attributes', () => {
|
||||
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);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-bold">a</span>' +
|
||||
@@ -68,7 +69,7 @@ describe('DomRendererRowFactory', () => {
|
||||
});
|
||||
|
||||
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);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-italic">a</span>' +
|
||||
@@ -79,7 +80,7 @@ describe('DomRendererRowFactory', () => {
|
||||
it('should add classes for 256 foreground colors', () => {
|
||||
const defaultAttrNoFgColor = (0 << 9) | (256 << 0);
|
||||
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);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
`<span class="xterm-fg-${i}">a</span>` +
|
||||
@@ -91,7 +92,7 @@ describe('DomRendererRowFactory', () => {
|
||||
it('should add classes for 256 background colors', () => {
|
||||
const defaultAttrNoBgColor = (257 << 9) | (0 << 0);
|
||||
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);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
`<span class="xterm-bg-${i}">a</span>` +
|
||||
@@ -101,7 +102,7 @@ describe('DomRendererRowFactory', () => {
|
||||
});
|
||||
|
||||
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);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-fg-1 xterm-bg-2">a</span>' +
|
||||
@@ -110,7 +111,7 @@ describe('DomRendererRowFactory', () => {
|
||||
});
|
||||
|
||||
it('should correctly invert default fg color', () => {
|
||||
lineData[0] = [(FLAGS.INVERSE << 18) | (257 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)];
|
||||
lineData.set(0, [(FLAGS.INVERSE << 18) | (257 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-fg-1 xterm-bg-15">a</span>' +
|
||||
@@ -119,7 +120,7 @@ describe('DomRendererRowFactory', () => {
|
||||
});
|
||||
|
||||
it('should correctly invert default bg color', () => {
|
||||
lineData[0] = [(FLAGS.INVERSE << 18) | (1 << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)];
|
||||
lineData.set(0, [(FLAGS.INVERSE << 18) | (1 << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-fg-0 xterm-bg-1">a</span>' +
|
||||
@@ -129,7 +130,7 @@ describe('DomRendererRowFactory', () => {
|
||||
|
||||
it('should turn bold fg text bright', () => {
|
||||
for (let i = 0; i < 8; i++) {
|
||||
lineData[0] = [(FLAGS.BOLD << 18) | (i << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)];
|
||||
lineData.set(0, [(FLAGS.BOLD << 18) | (i << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)]);
|
||||
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
`<span class="xterm-bold xterm-fg-${i + 8}">a</span>` +
|
||||
@@ -146,10 +147,10 @@ describe('DomRendererRowFactory', () => {
|
||||
return element.innerHTML;
|
||||
}
|
||||
|
||||
function createEmptyLineData(cols: number): LineData {
|
||||
const lineData: LineData = [];
|
||||
function createEmptyLineData(cols: number): IBufferLine {
|
||||
const lineData = new BufferLine();
|
||||
for (let i = 0; i < cols; i++) {
|
||||
lineData.push([DEFAULT_ATTR, ' ', 1, 32 /* ' '.charCodeAt(0) */]);
|
||||
lineData.push([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
}
|
||||
return lineData;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { LineData } from '../../Types';
|
||||
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../../Buffer';
|
||||
import { FLAGS } from '../Types';
|
||||
import { IBufferLine } from '../../Types';
|
||||
|
||||
export const BOLD_CLASS = 'xterm-bold';
|
||||
export const ITALIC_CLASS = 'xterm-italic';
|
||||
@@ -17,7 +17,7 @@ export class DomRendererRowFactory {
|
||||
) {
|
||||
}
|
||||
|
||||
public createRow(lineData: LineData, isCursorRow: boolean, cursorX: number, cellWidth: number, cols: number): DocumentFragment {
|
||||
public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorX: number, cellWidth: number, cols: number): DocumentFragment {
|
||||
const fragment = this._document.createDocumentFragment();
|
||||
let colCount = 0;
|
||||
|
||||
@@ -27,7 +27,7 @@ export class DomRendererRowFactory {
|
||||
continue;
|
||||
}
|
||||
|
||||
const charData = lineData[x];
|
||||
const charData = lineData.get(x);
|
||||
const char: string = charData[CHAR_DATA_CHAR_INDEX];
|
||||
const attr: number = charData[CHAR_DATA_ATTR_INDEX];
|
||||
const width: number = charData[CHAR_DATA_WIDTH_INDEX];
|
||||
|
||||
@@ -22,8 +22,8 @@ export const isLinux = platform.indexOf('Linux') >= 0;
|
||||
|
||||
/**
|
||||
* Return if the given array contains the given element
|
||||
* @param {Array} array The array to search for the given element.
|
||||
* @param {Object} el The element to look for into the array
|
||||
* @param arr The array to search for the given element.
|
||||
* @param el The element to look for into the array
|
||||
*/
|
||||
function contains(arr: any[], el: any): boolean {
|
||||
return arr.indexOf(el) >= 0;
|
||||
|
||||
@@ -22,11 +22,16 @@ export class RenderDebouncer implements IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
public refresh(rowStart?: number, rowEnd?: number): void {
|
||||
rowStart = rowStart || 0;
|
||||
rowEnd = rowEnd || this._terminal.rows - 1;
|
||||
this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart;
|
||||
this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd;
|
||||
public refresh(rowStart: number, rowEnd: number): void {
|
||||
// Get the min/max row start/end for the arg values
|
||||
rowStart = rowStart !== null && rowStart !== undefined ? rowStart : 0;
|
||||
rowEnd = rowEnd !== null && rowEnd !== undefined ? rowEnd : this._terminal.rows - 1;
|
||||
// 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) {
|
||||
return;
|
||||
|
||||
@@ -11,7 +11,7 @@ export class MouseHelper {
|
||||
|
||||
public static getCoordsRelativeToElement(event: {pageX: number, pageY: number}, element: HTMLElement): [number, number] {
|
||||
// Ignore browsers that don't support MouseEvent.pageX
|
||||
if (event.pageX == null) {
|
||||
if (event.pageX === null || event.pageX === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
+19
-20
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
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 } from '../Types';
|
||||
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ICircularList, ILinkifier, IMouseHelper, ILinkMatcherOptions, XtermListener, CharacterJoinerHandler, IBufferLine } from '../Types';
|
||||
import { Buffer } from '../Buffer';
|
||||
import * as Browser from '../shared/utils/Browser';
|
||||
import { ITheme, IDisposable, IMarker } from 'xterm';
|
||||
@@ -84,9 +84,6 @@ export class MockTerminal implements ITerminal {
|
||||
write(data: string): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
send(data: string): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
bracketedPasteMode: boolean;
|
||||
mouseHelper: IMouseHelper;
|
||||
renderer: IRenderer;
|
||||
@@ -148,14 +145,8 @@ export class MockTerminal implements ITerminal {
|
||||
refresh(start: number, end: number): void {
|
||||
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, ' ', 1, 32]);
|
||||
}
|
||||
return line;
|
||||
}
|
||||
registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; }
|
||||
deregisterCharacterJoiner(joinerId: number): void { }
|
||||
}
|
||||
|
||||
export class MockCharMeasure implements ICharMeasure {
|
||||
@@ -229,18 +220,12 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
|
||||
eraseLeft(x: number, y: number): void {
|
||||
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 {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
is(term: string): boolean {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
send(data: string): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
setgCharset(g: number, charset: { [key: string]: string; }): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
@@ -283,11 +268,23 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
|
||||
tabSet(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
handler(data: string): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
handleTitle(title: string): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
index(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
reverseIndex(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
export class MockBuffer implements IBuffer {
|
||||
isCursorInViewport: boolean;
|
||||
lines: ICircularList<[number, string, number, number][]>;
|
||||
lines: ICircularList<IBufferLine>;
|
||||
ydisp: number;
|
||||
ybase: number;
|
||||
hasScrollback: boolean;
|
||||
@@ -310,7 +307,7 @@ export class MockBuffer implements IBuffer {
|
||||
prevStop(x?: number): number {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
setLines(lines: ICircularList<[number, string, number, number][]>): void {
|
||||
setLines(lines: ICircularList<IBufferLine>): void {
|
||||
this.lines = lines;
|
||||
}
|
||||
}
|
||||
@@ -344,6 +341,8 @@ export class MockRenderer implements IRenderer {
|
||||
onWindowResize(devicePixelRatio: number): void {}
|
||||
clear(): void {}
|
||||
refreshRows(start: number, end: number): void {}
|
||||
registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; }
|
||||
deregisterCharacterJoiner(): boolean { return true; }
|
||||
}
|
||||
|
||||
export class MockViewport implements IViewport {
|
||||
|
||||
+1
-4
@@ -59,10 +59,7 @@
|
||||
"esSpecCompliant": true
|
||||
}
|
||||
],
|
||||
"triple-equals": [
|
||||
true,
|
||||
"allow-null-check"
|
||||
],
|
||||
"triple-equals": true,
|
||||
"typedef-whitespace": [
|
||||
true,
|
||||
{
|
||||
|
||||
Vendored
+61
@@ -39,6 +39,16 @@ declare module 'xterm' {
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@@ -416,8 +426,21 @@ declare module 'xterm' {
|
||||
*/
|
||||
off(type: 'blur' | 'focus' | 'linefeed' | 'selection' | 'data' | 'key' | 'keypress' | 'keydown' | 'refresh' | 'resize' | 'scroll' | 'title' | string, listener: (...args: any[]) => void): void;
|
||||
|
||||
/**
|
||||
* Emits an event on the terminal.
|
||||
* @param type The type of event
|
||||
* @param data data associated with the event.
|
||||
* @deprecated This is being removed from the API with no replacement, see
|
||||
* issue #1505.
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
@@ -470,6 +493,44 @@ declare module 'xterm' {
|
||||
*/
|
||||
deregisterLinkMatcher(matcherId: number): void;
|
||||
|
||||
/**
|
||||
* (EXPERIMENTAL) Registers a character joiner, allowing custom sequences of
|
||||
* characters to be rendered as a single unit. This is useful in particular
|
||||
* for rendering ligatures and graphemes, among other things.
|
||||
*
|
||||
* Each registered character joiner is called with a string of text
|
||||
* representing a portion of a line in the terminal that can be rendered as
|
||||
* a single unit. The joiner must return a sorted array, where each entry is
|
||||
* itself an array of length two, containing the start (inclusive) and end
|
||||
* (exclusive) index of a substring of the input that should be rendered as
|
||||
* a single unit. When multiple joiners are provided, the results of each
|
||||
* are collected. If there are any overlapping substrings between them, they
|
||||
* are combined into one larger unit that is drawn together.
|
||||
*
|
||||
* All character joiners that are registered get called every time a line is
|
||||
* rendered in the terminal, so it is essential for the handler function to
|
||||
* run as quickly as possible to avoid slowdowns when rendering. Similarly,
|
||||
* joiners should strive to return the smallest possible substrings to
|
||||
* render together, since they aren't drawn as optimally as individual
|
||||
* characters.
|
||||
*
|
||||
* NOTE: character joiners are only used by the canvas renderer.
|
||||
*
|
||||
* @param handler The function that determines character joins. It is called
|
||||
* with a string of text that is eligible for joining and returns an array
|
||||
* where each entry is an array containing the start (inclusive) and end
|
||||
* (exclusive) indexes of ranges that should be rendered as a single unit.
|
||||
* @return The ID of the new joiner, this can be used to deregister
|
||||
*/
|
||||
registerCharacterJoiner(handler: (text: string) => [number, number][]): number;
|
||||
|
||||
/**
|
||||
* (EXPERIMENTAL) Deregisters the character joiner if one was registered.
|
||||
* NOTE: character joiners are only used by the canvas renderer.
|
||||
* @param joinerId The character joiner's ID (returned after register)
|
||||
*/
|
||||
deregisterCharacterJoiner(joinerId: number): void;
|
||||
|
||||
/**
|
||||
* (EXPERIMENTAL) Adds a marker to the normal buffer and returns it. If the
|
||||
* alt buffer is active, undefined is returned.
|
||||
|
||||
+10
-1
@@ -6,5 +6,14 @@ module.exports = {
|
||||
path: path.resolve(__dirname, 'demo/dist'),
|
||||
filename: 'bundle.js'
|
||||
},
|
||||
devtool: 'source-map'
|
||||
devtool: 'source-map',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: ["source-map-loader"],
|
||||
enforce: "pre"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user