Compare commits

..
2 Commits
Author SHA1 Message Date
Paris Kasidiaris d2b3af5aa8 Bump version to 3.5.1
Signed-off-by: Paris Kasidiaris <paris@sourcelair.com>
2018-07-13 06:45:40 +00:00
Daniel Imms 5f7eb6e51d Fix charset drawing
Fixes #1565
2018-07-12 08:46:00 -07:00
88 changed files with 1136 additions and 10263 deletions
+19
View File
@@ -0,0 +1,19 @@
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
-4
View File
@@ -61,7 +61,6 @@ 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>
@@ -71,7 +70,6 @@ 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>
@@ -105,7 +103,6 @@ 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>
@@ -114,7 +111,6 @@ 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>
+5 -2
View File
@@ -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](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo), please provide an HTML document that demonstrates the problem.
- 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.
- Be polite. Issues with an indignant or belligerent tone tend to be moved to the
bottom of the pile.
@@ -33,7 +33,6 @@ 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).
@@ -51,3 +50,7 @@ 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
View File
@@ -1,4 +1,3 @@
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/)
+90 -23
View File
@@ -1,9 +1,6 @@
# [![xterm.js logo](logo-full.png)](https://xtermjs.org)
[![Build Status](https://dev.azure.com/xtermjs/xterm.js/_apis/build/status/xtermjs.xterm.js)](https://dev.azure.com/xtermjs/xterm.js/_build/latest?definitionId=3)
[![Coverage Status](https://coveralls.io/repos/github/xtermjs/xterm.js/badge.svg?branch=master)](https://coveralls.io/github/xtermjs/xterm.js?branch=master)
[![Gitter](https://badges.gitter.im/sourcelair/xterm.js.svg)](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/xterm/badge?style=rounded)](https://www.jsdelivr.com/package/npm/xterm)
[![Travis CI build status](https://api.travis-ci.org/xtermjs/xterm.js.svg)](https://travis-ci.org/xtermjs/xterm.js) [![VSTS Build status](https://xtermjs.visualstudio.com/_apis/public/build/definitions/3e323cf7-5760-460d-af64-ee5675baf366/1/badge)](https://xtermjs.visualstudio.com/xterm.js/_build/index?definitionId=1) [![Coverage Status](https://coveralls.io/repos/github/xtermjs/xterm.js/badge.svg?branch=master)](https://coveralls.io/github/xtermjs/xterm.js?branch=master) [![Gitter](https://badges.gitter.im/sourcelair/xterm.js.svg)](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/xterm/badge?style=rounded)](https://www.jsdelivr.com/package/npm/xterm)
Xterm.js is a terminal front-end component written in JavaScript that works in the browser.
@@ -60,12 +57,6 @@ 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.
@@ -83,8 +74,6 @@ 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()`.
@@ -118,10 +107,6 @@ 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.
@@ -168,23 +153,105 @@ 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.
- [**Juno**](http://junolab.org/): A flexible Julia IDE, based on Atom.
[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, &lt;file&gt; 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).
## Contributing
## Development and Contribution
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.
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.
## License Agreement
-68
View File
@@ -1,68 +0,0 @@
# Node.js
# Build a general Node.js application with npm.
# Add steps that analyze code, save build artifacts, deploy, and more:
# https://docs.microsoft.com/vsts/pipelines/languages/javascript
jobs:
- job: Linux
pool:
vmImage: 'ubuntu-16.04'
steps:
- task: NodeTool@0
inputs:
versionSpec: '8.x'
displayName: 'Install Node.js'
- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2
inputs:
versionSpec: "1.9.4"
displayName: 'Install Yarn'
- script: |
yarn
displayName: 'Install dependencies and build'
- script: |
yarn mocha
displayName: 'Test'
- script: |
yarn lint
displayName: 'Lint'
- job: macOS
pool:
vmImage: 'xcode9-macos10.13'
steps:
- task: NodeTool@0
inputs:
versionSpec: '8.x'
displayName: 'Install Node.js'
- script: |
yarn
displayName: 'Install dependencies and build'
- script: |
yarn mocha
displayName: 'Test'
- script: |
yarn lint
displayName: 'Lint'
- script: |
yarn test-coverage
export COVERALLS_GIT_BRANCH=$BUILD_SOURCEBRANCH
yarn coveralls
displayName: 'Generate and publish coverage'
- job: Windows
pool:
vmImage: 'vs2017-win2016'
steps:
- task: NodeTool@0
inputs:
versionSpec: '8.x'
displayName: 'Install Node.js'
- script: |
yarn
displayName: 'Install dependencies and build'
- script: |
yarn mocha
displayName: 'Test'
- script: |
yarn lint
displayName: 'Lint'
+6 -2
View File
@@ -17,8 +17,12 @@ app.get('/style.css', function(req, res){
res.sendFile(__dirname + '/style.css');
});
app.get('/dist/client-bundle.js', function(req, res){
res.sendFile(__dirname + '/dist/client-bundle.js');
app.get('/dist/bundle.js', function(req, res){
res.sendFile(__dirname + '/dist/bundle.js');
});
app.get('/dist/bundle.js.map', function(req, res){
res.sendFile(__dirname + '/dist/bundle.js.map');
});
app.post('/terminals', function (req, res) {
+1 -4
View File
@@ -16,9 +16,6 @@
<p>
<label>Find next <input id="find-next"/></label>
<label>Find previous <input id="find-previous"/></label>
<label>Use regex<input type="checkbox" id="regex"/></label>
<label>Case sensitive<input type="checkbox" id="case-sensitive"/></label>
<label>Whole word<input type="checkbox" id="whole-word"/></label>
</p>
</div>
<div>
@@ -36,6 +33,6 @@
<hr/>
<p><strong>Attention:</strong> The demo is a barebones implementation and is designed for the development and evaluation of xterm.js only. Exposing the demo to the public as is would introduce security risks for the host.</p>
<button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button>
<script src="dist/client-bundle.js" defer ></script>
<script src="dist/bundle.js" defer ></script>
</body>
</html>
+72 -94
View File
@@ -1,28 +1,11 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*
* This file is the entry point for browserify.
*/
import * as Terminal from '../build/xterm';
import * as attach from '../build/addons/attach/attach';
import * as fit from '../build/addons/fit/fit';
import * as fullscreen from '../build/addons/fullscreen/fullscreen';
import * as search from '../build/addons/search/search';
import * as webLinks from '../build/addons/webLinks/webLinks';
import * as winptyCompat from '../build/addons/winptyCompat/winptyCompat';
/// <reference path="../typings/xterm.d.ts"/>
import { Terminal } from '../lib/public/Terminal';
import * as attach from '../lib/addons/attach/attach';
import * as fit from '../lib/addons/fit/fit';
import * as fullscreen from '../lib/addons/fullscreen/fullscreen';
import * as search from '../lib/addons/search/search';
import * as webLinks from '../lib/addons/webLinks/webLinks';
import * as winptyCompat from '../lib/addons/winptyCompat/winptyCompat';
// Pulling in the module's types relies on the <reference> above, it's looks a
// little weird here as we're importing "this" module
import { Terminal as TerminalType } from 'xterm';
export interface IWindowWithTerminal extends Window {
term: TerminalType;
}
declare let window: IWindowWithTerminal;
Terminal.applyAddon(attach);
Terminal.applyAddon(fit);
@@ -32,20 +15,20 @@ Terminal.applyAddon(webLinks);
Terminal.applyAddon(winptyCompat);
let term;
let protocol;
let socketURL;
let socket;
let pid;
var term,
protocol,
socketURL,
socket,
pid;
const terminalContainer = document.getElementById('terminal-container');
const actionElements = {
findNext: <HTMLInputElement>document.querySelector('#find-next'),
findPrevious: <HTMLInputElement>document.querySelector('#find-previous')
};
const paddingElement = <HTMLInputElement>document.getElementById('padding');
var terminalContainer = document.getElementById('terminal-container'),
actionElements = {
findNext: document.querySelector('#find-next'),
findPrevious: document.querySelector('#find-previous')
},
paddingElement = document.getElementById('padding');
function setPadding(): void {
function setPadding() {
term.element.style.padding = parseInt(paddingElement.value, 10).toString() + 'px';
term.fit();
}
@@ -69,20 +52,20 @@ const disposeRecreateButtonHandler = () => {
document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
function createTerminal(): void {
function createTerminal() {
// Clean terminal
while (terminalContainer.children.length) {
terminalContainer.removeChild(terminalContainer.children[0]);
}
term = new Terminal({});
window.term = term; // Expose `term` to window for debugging purposes
term.on('resize', (size: { cols: number, rows: number }) => {
term.on('resize', function (size) {
if (!pid) {
return;
}
const cols = size.cols;
const rows = size.rows;
const url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows;
var cols = size.cols,
rows = size.rows,
url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows;
fetch(url, {method: 'POST'});
});
@@ -97,42 +80,32 @@ function createTerminal(): void {
addDomListener(paddingElement, 'change', setPadding);
addDomListener(actionElements.findNext, 'keypress', (e) => {
if (e.key === 'Enter') {
addDomListener(actionElements.findNext, 'keypress', function (e) {
if (e.key === "Enter") {
e.preventDefault();
const searchOptions = {
regex: (document.getElementById('regex') as HTMLInputElement).checked,
wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked,
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked
};
term.findNext(actionElements.findNext.value, searchOptions);
term.findNext(actionElements.findNext.value);
}
});
addDomListener(actionElements.findPrevious, 'keypress', (e) => {
if (e.key === 'Enter') {
addDomListener(actionElements.findPrevious, 'keypress', function (e) {
if (e.key === "Enter") {
e.preventDefault();
const searchOptions = {
regex: (document.getElementById('regex') as HTMLInputElement).checked,
wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked,
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked
};
term.findPrevious(actionElements.findPrevious.value, searchOptions);
term.findPrevious(actionElements.findPrevious.value);
}
});
// fit is called within a setTimeout, cols and rows need this.
setTimeout(() => {
setTimeout(function () {
initOptions(term);
// TODO: Clean this up, opt-cols/rows doesn't exist anymore
(<HTMLInputElement>document.getElementById(`opt-cols`)).value = term.cols;
(<HTMLInputElement>document.getElementById(`opt-rows`)).value = term.rows;
paddingElement.value = '0';
document.getElementById(`opt-cols`).value = term.cols;
document.getElementById(`opt-rows`).value = term.rows;
paddingElement.value = 0;
// Set terminal size again to set the specific dimensions on the demo
updateTerminalSize();
fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then((res) => {
res.text().then((processId) => {
fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then(function (res) {
res.text().then(function (processId) {
pid = processId;
socketURL += processId;
socket = new WebSocket(socketURL);
@@ -144,20 +117,22 @@ function createTerminal(): void {
}, 0);
}
function runRealTerminal(): void {
function runRealTerminal() {
term.attach(socket);
term._initialized = true;
}
function runFakeTerminal(): void {
function runFakeTerminal() {
if (term._initialized) {
return;
}
term._initialized = true;
term.prompt = () => {
term.write('\r\n$ ');
var shellprompt = '$ ';
term.prompt = function () {
term.write('\r\n' + shellprompt);
};
term.writeln('Welcome to xterm.js');
@@ -166,12 +141,14 @@ function runFakeTerminal(): void {
term.writeln('');
term.prompt();
term._core.register(term.addDisposableListener('key', (key, ev) => {
const printable = !ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey;
term._core.register(term.addDisposableListener('key', function (key, ev) {
var printable = (
!ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey
);
if (ev.keyCode === 13) {
if (ev.keyCode == 13) {
term.prompt();
} else if (ev.keyCode === 8) {
} else if (ev.keyCode == 8) {
// Do not delete the prompt
if (term.x > 2) {
term.write('\b \b');
@@ -181,13 +158,13 @@ function runFakeTerminal(): void {
}
}));
term._core.register(term.addDisposableListener('paste', (data, ev) => {
term._core.register(term.addDisposableListener('paste', function (data, ev) {
term.write(data);
}));
}
function initOptions(term: TerminalType): void {
const blacklistedOptions = [
function initOptions(term) {
var blacklistedOptions = [
// Internal only options
'cancelEvents',
'convertEol',
@@ -197,21 +174,22 @@ function initOptions(term: TerminalType): void {
'termName',
'useFlowControl',
// Complex option
'theme'
'theme',
// Only in constructor
'rendererType'
];
const stringOptions = {
var stringOptions = {
bellSound: null,
bellStyle: ['none', 'sound'],
cursorStyle: ['block', 'underline', 'bar'],
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'],
rendererType: ['dom', 'canvas']
fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900']
};
const options = Object.keys((<any>term)._core.options);
const booleanOptions = [];
const numberOptions = [];
var options = Object.keys(term._core.options);
var booleanOptions = [];
var numberOptions = [];
options.filter(o => blacklistedOptions.indexOf(o) === -1).forEach(o => {
switch (typeof term.getOption(o)) {
case 'boolean':
@@ -227,7 +205,7 @@ function initOptions(term: TerminalType): void {
}
});
let html = '';
var html = '';
html += '<div class="option-group">';
booleanOptions.forEach(o => {
html += `<div class="option"><label><input id="opt-${o}" type="checkbox" ${term.getOption(o) ? 'checked' : ''}/> ${o}</label></div>`;
@@ -241,24 +219,24 @@ function initOptions(term: TerminalType): void {
if (stringOptions[o]) {
html += `<div class="option"><label>${o} <select id="opt-${o}">${stringOptions[o].map(v => `<option ${term.getOption(o) === v ? 'selected' : ''}>${v}</option>`).join('')}</select></label></div>`;
} else {
html += `<div class="option"><label>${o} <input id="opt-${o}" type="text" value="${term.getOption(o)}"/></label></div>`;
html += `<div class="option"><label>${o} <input id="opt-${o}" type="text" value="${term.getOption(o)}"/></label></div>`
}
});
html += '</div>';
const container = document.getElementById('options-container');
var container = document.getElementById('options-container');
container.innerHTML = html;
// Attach listeners
booleanOptions.forEach(o => {
const input = <HTMLInputElement>document.getElementById(`opt-${o}`);
var input = document.getElementById(`opt-${o}`);
addDomListener(input, 'change', () => {
console.log('change', o, input.checked);
term.setOption(o, input.checked);
});
});
numberOptions.forEach(o => {
const input = <HTMLInputElement>document.getElementById(`opt-${o}`);
var input = document.getElementById(`opt-${o}`);
addDomListener(input, 'change', () => {
console.log('change', o, input.value);
if (o === 'cols' || o === 'rows') {
@@ -269,7 +247,7 @@ function initOptions(term: TerminalType): void {
});
});
Object.keys(stringOptions).forEach(o => {
const input = <HTMLInputElement>document.getElementById(`opt-${o}`);
var input = document.getElementById(`opt-${o}`);
addDomListener(input, 'change', () => {
console.log('change', o, input.value);
term.setOption(o, input.value);
@@ -277,16 +255,16 @@ function initOptions(term: TerminalType): void {
});
}
function addDomListener(element: HTMLElement, type: string, handler: (...args: any[]) => any): void {
function addDomListener(element, type, handler) {
element.addEventListener(type, handler);
term._core.register({ dispose: () => element.removeEventListener(type, handler) });
}
function updateTerminalSize(): void {
const cols = parseInt((<HTMLInputElement>document.getElementById(`opt-cols`)).value, 10);
const rows = parseInt((<HTMLInputElement>document.getElementById(`opt-rows`)).value, 10);
const width = (cols * term._core.renderer.dimensions.actualCellWidth + term._core.viewport.scrollBarWidth).toString() + 'px';
const height = (rows * term._core.renderer.dimensions.actualCellHeight).toString() + 'px';
function updateTerminalSize() {
var cols = parseInt(document.getElementById(`opt-cols`).value, 10);
var rows = parseInt(document.getElementById(`opt-rows`).value, 10);
var width = (cols * term._core.renderer.dimensions.actualCellWidth + term._core.viewport.scrollBarWidth).toString() + 'px';
var height = (rows * term._core.renderer.dimensions.actualCellHeight).toString() + 'px';
terminalContainer.style.width = width;
terminalContainer.style.height = height;
term.fit();
-49
View File
@@ -1,49 +0,0 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*
* This file is the entry point for browserify.
*/
const cp = require('child_process');
const path = require('path');
const webpack = require('webpack');
// Launch server
cp.spawn('node', [path.resolve(__dirname, 'server.js')], { stdio: 'inherit' });
// Build/watch client source
const clientConfig = {
entry: path.resolve(__dirname, 'client.ts'),
devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
resolve: {
extensions: [ '.tsx', '.ts', '.js' ]
},
output: {
filename: 'client-bundle.js',
path: path.resolve(__dirname, 'dist')
},
mode: 'development',
watch: true
};
const compiler = webpack(clientConfig);
compiler.watch({
// Example watchOptions
aggregateTimeout: 300,
poll: undefined
}, (err, stats) => {
// Print watch/build result here...
console.log(stats.toString({
colors: true
}));
});
-12
View File
@@ -1,12 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"rootDir": ".",
"sourceMap": true
},
"include": [
"client.ts",
"../typings/xterm.d.ts"
]
}
+1 -6
View File
@@ -4,11 +4,6 @@
],
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": [
"dom",
"es6"
],
"noEmit": true
"target": "es5"
}
}
+12
View File
@@ -14,6 +14,7 @@ const source = require('vinyl-source-stream');
const sourcemaps = require('gulp-sourcemaps');
const ts = require('gulp-typescript');
const util = require('gulp-util');
const webpack = require('webpack-stream');
const buildDir = process.env.BUILD_DIR || 'build';
const tsProject = ts.createProject('tsconfig.json');
@@ -136,6 +137,17 @@ gulp.task('sorcery-addons', ['browserify-addons'], function () {
})
});
gulp.task('webpack', ['build'], function() {
return gulp.src('demo/main.js')
.pipe(webpack(require('./webpack.config.js')))
.pipe(gulp.dest('demo/dist/'));
});
gulp.task('watch-demo', ['webpack'], () => {
gulp.watch(['./demo/*', './lib/**/*'], ['webpack']);
});
gulp.task('build', ['sorcery', 'sorcery-addons']);
gulp.task('test', ['mocha']);
gulp.task('default', ['build']);
+9 -13
View File
@@ -1,7 +1,7 @@
{
"name": "xterm",
"description": "Full xterm terminal, in your browser",
"version": "3.8.1",
"version": "3.5.1",
"main": "lib/public/Terminal.js",
"types": "typings/xterm.d.ts",
"repository": "https://github.com/xtermjs/xterm.js",
@@ -12,7 +12,6 @@
"@types/jsdom": "11.0.1",
"@types/mocha": "^2.2.33",
"@types/node": "6.0.108",
"@types/webpack": "^4.4.11",
"browserify": "^13.3.0",
"chai": "3.5.0",
"concurrently": "^3.5.1",
@@ -33,25 +32,23 @@
"merge-stream": "^1.0.1",
"node-pty": "0.7.6",
"nodemon": "1.10.2",
"npm-run-all": "^4.1.2",
"nyc": "^11.8.0",
"sorcery": "^0.10.0",
"source-map-loader": "^0.2.3",
"ts-loader": "^4.5.0",
"tslint": "^5.9.1",
"tslint-consistent-codestyle": "^1.13.0",
"typescript": "3.0",
"typescript": "2.8.3",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0",
"webpack": "^4.17.1",
"webpack-cli": "^3.1.0",
"webpack": "^3.10.0",
"webpack-stream": "^4.0.0",
"zmodem.js": "^0.1.5"
},
"scripts": {
"start": "node demo/start",
"start": "concurrently --kill-others-on-fail --names \"demo,server\" \"gulp watch-demo\" \"node demo/app\"",
"start-zmodem": "node demo/zmodem/app",
"lint": "tslint 'src/**/*.ts' './demo/**/*.ts'",
"test": "npm run mocha",
"posttest": "npm run lint",
"lint": "tslint 'src/**/*.ts'",
"test": "npm-run-all mocha lint",
"test-debug": "node --inspect-brk node_modules/.bin/gulp test",
"test-suite": "gulp mocha-suite --test",
"test-coverage": "nyc -x gulpfile.js -x '**/*test*' npm run mocha",
@@ -63,7 +60,6 @@
"prepublish": "npm run build",
"coveralls": "nyc report --reporter=text-lcov | coveralls",
"webpack": "gulp webpack",
"watch": "concurrently --kill-others-on-fail --names \"lib,css\" \"tsc -w\" \"gulp watch-css\"",
"watch-addons": "concurrently --kill-others-on-fail --names \"attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem\" \"tsc -w -p ./src/addons/attach\" \"tsc -w -p ./src/addons/fit\" \"tsc -w -p ./src/addons/fullscreen\" \"tsc -w -p ./src/addons/search\" \"tsc -w -p ./src/addons/terminado\" \"tsc -w -p ./src/addons/webLinks\" \"tsc -w -p ./src/addons/winptyCompat\" \"tsc -w -p ./src/addons/zmodem\""
"watch": "concurrently --kill-others-on-fail --names \"lib,attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem,css\" \"tsc -w\" \"tsc -w -p ./src/addons/attach\" \"tsc -w -p ./src/addons/fit\" \"tsc -w -p ./src/addons/fullscreen\" \"tsc -w -p ./src/addons/search\" \"tsc -w -p ./src/addons/terminado\" \"tsc -w -p ./src/addons/webLinks\" \"tsc -w -p ./src/addons/winptyCompat\" \"tsc -w -p ./src/addons/zmodem\" \"gulp watch-css\""
}
}
+2 -5
View File
@@ -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.rows)));
this.register(this._terminal.addDisposableListener('resize', data => this._onResize(data.cols, 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(rows: number): void {
private _onResize(cols: number, rows: number): void {
// Remove bottom boundary listener
this._rowElements[this._rowElements.length - 1].removeEventListener('focus', this._bottomBoundaryFocusListener);
@@ -259,9 +259,6 @@ 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]);
}
+50 -249
View File
@@ -3,12 +3,11 @@
* @license MIT
*/
import { assert, expect } from 'chai';
import { assert } from 'chai';
import { ITerminal } from './Types';
import { Buffer, DEFAULT_ATTR, CHAR_DATA_CHAR_INDEX } from './Buffer';
import { Buffer } from './Buffer';
import { CircularList } from './common/CircularList';
import { MockTerminal, TestTerminal } from './utils/TestUtils.test';
import { BufferLine } from './BufferLine';
import { MockTerminal } from './utils/TestUtils.test';
const INIT_COLS = 80;
const INIT_ROWS = 24;
@@ -37,13 +36,13 @@ describe('Buffer', () => {
describe('fillViewportRows', () => {
it('should fill the buffer with blank lines based on the size of the viewport', () => {
const blankLineChar = BufferLine.blankLine(terminal.cols, DEFAULT_ATTR).get(0);
const blankLineChar = terminal.blankLine()[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).get(x), blankLineChar);
assert.deepEqual(buffer.lines.get(y)[x], blankLineChar);
}
}
});
@@ -67,40 +66,40 @@ describe('Buffer', () => {
describe('wrapped', () => {
it('should return a range for the first row', () => {
buffer.fillViewportRows();
buffer.lines.get(1).isWrapped = true;
(<any> 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();
buffer.lines.get(12).isWrapped = true;
(<any> 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();
buffer.lines.get(13).isWrapped = true;
(<any> 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();
buffer.lines.get(11).isWrapped = true;
buffer.lines.get(12).isWrapped = true;
buffer.lines.get(13).isWrapped = true;
buffer.lines.get(14).isWrapped = true;
(<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;
assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 10, last: 14 });
});
it('should return a range for the last row', () => {
buffer.fillViewportRows();
buffer.lines.get(23).isWrapped = true;
(<any> 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();
buffer.lines.get(1).isWrapped = true;
(<any> 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();
buffer.lines.get(buffer.lines.length - 1).isWrapped = true;
(<any> buffer.lines.get(buffer.lines.length - 1)).isWrapped = true;
assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 2), { first: 22, last: 23 });
});
});
@@ -155,11 +154,11 @@ describe('Buffer', () => {
assert.equal(buffer.lines.maxLength, INIT_ROWS);
buffer.y = INIT_ROWS - 1;
buffer.fillViewportRows();
buffer.lines.get(5).get(0)[1] = 'a';
buffer.lines.get(INIT_ROWS - 1).get(0)[1] = 'b';
buffer.lines.get(5)[0][1] = 'a';
buffer.lines.get(INIT_ROWS - 1)[0][1] = 'b';
buffer.resize(INIT_COLS, INIT_ROWS - 5);
assert.equal(buffer.lines.get(0).get(0)[1], 'a');
assert.equal(buffer.lines.get(INIT_ROWS - 1 - 5).get(0)[1], 'b');
assert.equal(buffer.lines.get(0)[0][1], 'a');
assert.equal(buffer.lines.get(INIT_ROWS - 1 - 5)[0][1], 'b');
});
});
});
@@ -180,7 +179,7 @@ describe('Buffer', () => {
buffer.fillViewportRows();
// Create 10 extra blank lines
for (let i = 0; i < 10; i++) {
buffer.lines.push(BufferLine.blankLine(terminal.cols, DEFAULT_ATTR));
buffer.lines.push(terminal.blankLine());
}
// Set cursor to the bottom of the buffer
buffer.y = INIT_ROWS - 1;
@@ -200,7 +199,7 @@ describe('Buffer', () => {
buffer.fillViewportRows();
// Create 10 extra blank lines
for (let i = 0; i < 10; i++) {
buffer.lines.push(BufferLine.blankLine(terminal.cols, DEFAULT_ATTR));
buffer.lines.push(terminal.blankLine());
}
// Set cursor to the bottom of the buffer
buffer.y = INIT_ROWS - 1;
@@ -273,34 +272,34 @@ describe('Buffer', () => {
describe ('translateBufferLineToString', () => {
it('should handle selecting a section of ascii text', () => {
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);
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 str = buffer.translateBufferLineToString(0, true, 0, 2);
assert.equal(str, 'ab');
});
it('should handle a cut-off double width character by including it', () => {
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);
buffer.lines.set(0, [
[ null, '語', 2, 35486 ],
[ null, '', 0, null],
[ null, 'a', 1, 'a'.charCodeAt(0)]
]);
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', () => {
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);
buffer.lines.set(0, [
[ null, '語', 2, '語'.charCodeAt(0) ],
[ null, '', 0, null],
[ null, 'a', 1, 'a'.charCodeAt(0)]
]);
const str0 = buffer.translateBufferLineToString(0, true, 0, 1);
assert.equal(str0, '語');
@@ -313,10 +312,10 @@ describe('Buffer', () => {
});
it('should handle single width emojis', () => {
const line = new BufferLine();
line.push([ null, '😁', 1, '😁'.charCodeAt(0) ]);
line.push([ null, 'a', 1, 'a'.charCodeAt(0)]);
buffer.lines.set(0, line);
buffer.lines.set(0, [
[ null, '😁', 1, '😁'.charCodeAt(0) ],
[ null, 'a', 1, 'a'.charCodeAt(0)]
]);
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
assert.equal(str1, '😁');
@@ -326,10 +325,10 @@ describe('Buffer', () => {
});
it('should handle double width emojis', () => {
const line = new BufferLine();
line.push([ null, '😁', 2, '😁'.charCodeAt(0) ]);
line.push([ null, '', 0, null]);
buffer.lines.set(0, line);
buffer.lines.set(0, [
[ null, '😁', 2, '😁'.charCodeAt(0) ],
[ null, '', 0, null]
]);
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
assert.equal(str1, '😁');
@@ -337,212 +336,14 @@ describe('Buffer', () => {
const str2 = buffer.translateBufferLineToString(0, true, 0, 2);
assert.equal(str2, '😁');
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);
buffer.lines.set(0, [
[ null, '😁', 2, '😁'.charCodeAt(0) ],
[ null, '', 0, null],
[ null, 'a', 1, 'a'.charCodeAt(0)]
]);
const str3 = buffer.translateBufferLineToString(0, true, 0, 3);
assert.equal(str3, '😁a');
});
});
describe('stringIndexToBufferIndex', () => {
let terminal: TestTerminal;
beforeEach(() => {
terminal = new TestTerminal({rows: 5, cols: 10});
});
it('multiline ascii', () => {
const input = 'This is ASCII text spanning multiple lines.';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 0; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
}
});
it('combining e\u0301 in a sentence', () => {
const input = 'Sitting in the cafe\u0301 drinking coffee.';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 0; i < 19; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
}
// string index 18 & 19 point to combining char e\u0301 ---> same buffer Index
assert.deepEqual(
terminal.buffer.stringIndexToBufferIndex(0, 18),
terminal.buffer.stringIndexToBufferIndex(0, 19));
// after the combining char every string index has an offset of -1
for (let i = 19; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex);
}
});
it('multiline combining e\u0301', () => {
const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
// every buffer cell index contains 2 string indices
for (let i = 0; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex);
}
});
it('surrogate char in a sentence', () => {
const input = 'The 𝄞 is a clef widely used in modern notation.';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 0; i < 5; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
}
// string index 4 & 5 point to surrogate char 𝄞 ---> same buffer Index
assert.deepEqual(
terminal.buffer.stringIndexToBufferIndex(0, 4),
terminal.buffer.stringIndexToBufferIndex(0, 5));
// after the combining char every string index has an offset of -1
for (let i = 5; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex);
}
});
it('multiline surrogate char', () => {
const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
// every buffer cell index contains 2 string indices
for (let i = 0; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex);
}
});
it('surrogate char with combining', () => {
// eye of Ra with acute accent - string length of 3
const input = '𓂀\u0301 - the eye hiroglyph with an acute accent.';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
// index 0..2 should map to 0
assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 1));
assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 2));
for (let i = 2; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([((i - 2) / terminal.cols) | 0, (i - 2) % terminal.cols], bufferIndex);
}
});
it('multiline surrogate with combining', () => {
const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
// every buffer cell index contains 3 string indices
for (let i = 0; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([(((i / 3) | 0) / terminal.cols) | 0, ((i / 3) | 0) % terminal.cols], bufferIndex);
}
});
it('fullwidth chars', () => {
const input = 'These 123 are some fat numbers.';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 0; i < 6; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
}
// string index 6, 7, 8 take 2 cells
assert.deepEqual([0, 8], terminal.buffer.stringIndexToBufferIndex(0, 7));
assert.deepEqual([1, 0], terminal.buffer.stringIndexToBufferIndex(0, 8));
// rest of the string has offset of +3
for (let i = 9; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([((i + 3) / terminal.cols) | 0, (i + 3) % terminal.cols], bufferIndex);
}
});
it('multiline fullwidth chars', () => {
const input = '12345678901234567890';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 9; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
assert.deepEqual([((i << 1) / terminal.cols) | 0, (i << 1) % terminal.cols], bufferIndex);
}
});
it('fullwidth combining with emoji - match emoji cell', () => {
const input = 'Lots of ¥\u0301 make me 😃.';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
const stringIndex = s.match(/😃/).index;
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, stringIndex);
assert(terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX], '😃');
});
it('multiline fullwidth chars with offset 1 (currently tests for broken behavior)', () => {
const input = 'a12345678901234567890';
// the 'a' at the beginning moves all fullwidth chars one to the right
// now the end of the line contains a dangling empty cell since
// the next fullwidth char has to wrap early
// the dangling last cell is wrongly added in the string
// --> fixable after resolving #1685
terminal.writeSync(input);
// TODO: reenable after fix
// const s = terminal.buffer.contents(true).toArray()[0];
// assert.equal(input, s);
for (let i = 10; i < input.length; ++i) {
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i + 1); // TODO: remove +1 after fix
const j = (i - 0) << 1;
assert.deepEqual([(j / terminal.cols) | 0, j % terminal.cols], bufferIndex);
}
});
});
describe('BufferStringIterator', function(): void {
it('iterator does not ovrflow buffer limits', function(): void {
const terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5});
const data = [
'aaaaaaaaaa',
'aaaaaaaaa\n',
'aaaaaaaaaa',
'aaaaaaaaa\n',
'aaaaaaaaaa',
'aaaaaaaaaa',
'aaaaaaaaaa',
'aaaaaaaaa\n',
'aaaaaaaaaa',
'aaaaaaaaaa'
];
terminal.writeSync(data.join(''));
// brute force test with insane values
expect(() => {
for (let overscan = 0; overscan < 20; ++overscan) {
for (let start = -10; start < 20; ++start) {
for (let end = -10; end < 20; ++end) {
const it = terminal.buffer.iterator(false, start, end, overscan, overscan);
while (it.hasNext()) {
it.next();
}
}
}
}
}).to.not.throw();
});
});
});
+13 -110
View File
@@ -4,10 +4,9 @@
*/
import { CircularList } from './common/CircularList';
import { CharData, ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult } from './Types';
import { EventEmitter } from './common/EventEmitter';
import { LineData, CharData, ITerminal, IBuffer } 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;
@@ -16,10 +15,6 @@ 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):
@@ -28,7 +23,7 @@ export const NULL_CELL_CODE = 32;
* - scroll position
*/
export class Buffer implements IBuffer {
public lines: CircularList<IBufferLine>;
public lines: CircularList<LineData>;
public ydisp: number;
public ybase: number;
public y: number;
@@ -85,7 +80,7 @@ export class Buffer implements IBuffer {
if (this.lines.length === 0) {
let i = this._terminal.rows;
while (i--) {
this.lines.push(BufferLine.blankLine(this._terminal.cols, DEFAULT_ATTR));
this.lines.push(this._terminal.blankLine());
}
}
}
@@ -98,7 +93,7 @@ export class Buffer implements IBuffer {
this.ybase = 0;
this.y = 0;
this.x = 0;
this.lines = new CircularList<IBufferLine>(this._getCorrectBufferLength(this._terminal.rows));
this.lines = new CircularList<LineData>(this._getCorrectBufferLength(this._terminal.rows));
this.scrollTop = 0;
this.scrollBottom = this._terminal.rows - 1;
this.setupTabStops();
@@ -122,7 +117,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, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // does xterm use the default attr?
const ch: CharData = [DEFAULT_ATTR, ' ', 1, 32]; // 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);
@@ -147,7 +142,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(BufferLine.blankLine(newCols, DEFAULT_ATTR));
this.lines.push(this._terminal.blankLine(undefined, undefined, newCols));
}
}
}
@@ -194,36 +189,6 @@ export class Buffer implements IBuffer {
this.scrollBottom = newRows - 1;
}
/**
* Translates a string index back to a BufferIndex.
* To get the correct buffer position the string must start at `startCol` 0
* (default in translateBufferLineToString).
* The method also works on wrapped line strings given rows were not trimmed.
* The method operates on the CharData string length, there are no
* additional content or boundary checks. Therefore the string and the buffer
* should not be altered in between.
* TODO: respect trim flag after fixing #1685
* @param lineIndex line index the string was retrieved from
* @param stringIndex index within the string
* @param startCol column offset the string was retrieved from
*/
public stringIndexToBufferIndex(lineIndex: number, stringIndex: number): BufferIndex {
while (stringIndex) {
const line = this.lines.get(lineIndex);
if (!line) {
[-1, -1];
}
for (let i = 0; i < line.length; ++i) {
stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length;
if (stringIndex < 0) {
return [lineIndex, i];
}
}
lineIndex++;
}
return [lineIndex, 0];
}
/**
* Translates a buffer line to a string, with optional start and end columns.
* Wide characters will count as two columns in the resulting string. This
@@ -254,7 +219,7 @@ export class Buffer implements IBuffer {
let endIndex = endCol;
for (let i = 0; i < line.length; i++) {
const char = line.get(i);
const char = line[i];
lineString += char[CHAR_DATA_CHAR_INDEX];
// Adjust start and end cols for wide characters if they affect their
// column indexes
@@ -299,11 +264,11 @@ export class Buffer implements IBuffer {
let first = y;
let last = y;
// Scan upwards for wrapped lines
while (first > 0 && this.lines.get(first).isWrapped) {
while (first > 0 && (<any>this.lines.get(first)).isWrapped) {
first--;
}
// Scan downwards for wrapped lines
while (last + 1 < this.lines.length && this.lines.get(last + 1).isWrapped) {
while (last + 1 < this.lines.length && (<any>this.lines.get(last + 1)).isWrapped) {
last++;
}
return { first, last };
@@ -314,7 +279,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 && i !== undefined) {
if (i != null) {
if (!this.tabs[i]) {
i = this.prevStop(i);
}
@@ -333,7 +298,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 || x === undefined) {
if (x == null) {
x = this.x;
}
while (!this.tabs[--x] && x > 0);
@@ -345,7 +310,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 || x === undefined) {
if (x == null) {
x = this.x;
}
while (!this.tabs[++x] && x < this._terminal.cols);
@@ -370,10 +335,6 @@ export class Buffer implements IBuffer {
// TODO: This could probably be optimized by relying on sort order and trimming the array using .length
this.markers.splice(this.markers.indexOf(marker), 1);
}
public iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator {
return new BufferStringIterator(this, trimRight, startIndex, endIndex, startOverscan, endOverscan);
}
}
export class Marker extends EventEmitter implements IMarker {
@@ -400,61 +361,3 @@ export class Marker extends EventEmitter implements IMarker {
super.dispose();
}
}
/**
* Iterator to get unwrapped content strings from the buffer.
* The iterator returns at least the string data between the borders
* `startIndex` and `endIndex` (exclusive) and will expand the lines
* by `startOverscan` to the top and by `endOverscan` to the bottom,
* if no new line was found in between.
* It will never read/return string data beyond `startIndex - startOverscan`
* or `endIndex + endOverscan`. Therefore the first and last line might be truncated.
* It is possible to always get the full string for the first and last line as well
* by setting the overscan values to the actual buffer length. This not recommended
* since it might return the whole buffer within a single string in a worst case scenario.
*/
export class BufferStringIterator implements IBufferStringIterator {
private _current: number;
constructor (
private _buffer: IBuffer,
private _trimRight: boolean,
private _startIndex: number = 0,
private _endIndex: number = _buffer.lines.length,
private _startOverscan: number = 0,
private _endOverscan: number = 0
) {
if (this._startIndex < 0) {
this._startIndex = 0;
}
if (this._endIndex > this._buffer.lines.length) {
this._endIndex = this._buffer.lines.length;
}
this._current = this._startIndex;
}
public hasNext(): boolean {
return this._current < this._endIndex;
}
public next(): IBufferStringIteratorResult {
const range = this._buffer.getWrappedRangeForLine(this._current);
// limit search window to overscan value at both borders
if (range.first < this._startIndex - this._startOverscan) {
range.first = this._startIndex - this._startOverscan;
}
if (range.last > this._endIndex + this._endOverscan) {
range.last = this._endIndex + this._endOverscan;
}
// limit to current buffer length
range.first = Math.max(range.first, 0);
range.last = Math.min(range.last, this._buffer.lines.length);
let result = '';
for (let i = range.first; i <= range.last; ++i) {
// TODO: always apply trimRight after fixing #1685
result += this._buffer.translateBufferLineToString(i, (this._trimRight) ? i === range.last : false);
}
this._current = range.last + 1;
return {range: range, content: result};
}
}
-97
View File
@@ -1,97 +0,0 @@
/**
* 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]]);
});
});
-83
View File
@@ -1,83 +0,0 @@
/**
* 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)
}
}
}
+5 -2
View File
@@ -5,7 +5,7 @@
import { ITerminal, IBufferSet } from './Types';
import { Buffer } from './Buffer';
import { EventEmitter } from './common/EventEmitter';
import { EventEmitter } from './EventEmitter';
/**
* The BufferSet represents the set of two buffers used by xterm terminals (normal and alt) and
@@ -18,7 +18,7 @@ export class BufferSet extends EventEmitter implements IBufferSet {
/**
* Create a new BufferSet for the given terminal.
* @param _terminal - The terminal the BufferSet will belong to
* @param {Terminal} terminal - The terminal the BufferSet will belong to
*/
constructor(private _terminal: ITerminal) {
super();
@@ -35,6 +35,7 @@ export class BufferSet extends EventEmitter implements IBufferSet {
/**
* Returns the alt Buffer of the BufferSet
* @returns {Buffer}
*/
public get alt(): Buffer {
return this._alt;
@@ -42,6 +43,7 @@ export class BufferSet extends EventEmitter implements IBufferSet {
/**
* Returns the normal Buffer of the BufferSet
* @returns {Buffer}
*/
public get active(): Buffer {
return this._activeBuffer;
@@ -49,6 +51,7 @@ export class BufferSet extends EventEmitter implements IBufferSet {
/**
* Returns the currently active Buffer of the BufferSet
* @returns {Buffer}
*/
public get normal(): Buffer {
return this._normal;
-79
View File
@@ -1,79 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { TestTerminal } from './utils/TestUtils.test';
import { assert } from 'chai';
import { getStringCellWidth } from './CharWidth';
import { IBuffer } from './Types';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
describe('getStringCellWidth', function(): void {
let terminal: TestTerminal;
beforeEach(() => {
terminal = new TestTerminal({rows: 5, cols: 30});
});
function sumWidths(buffer: IBuffer, start: number, end: number, sentinel: string): number {
let result = 0;
for (let i = start; i < end; ++i) {
const line = buffer.lines.get(i);
for (let j = 0; j < line.length; ++j) { // TODO: change to trimBorder with multiline
const ch = line.get(j);
result += ch[CHAR_DATA_WIDTH_INDEX];
// return on sentinel
if (ch[CHAR_DATA_CHAR_INDEX] === sentinel) {
return result;
}
}
}
return result;
}
it('ASCII chars', function(): void {
const input = 'This is just ASCII text.#';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
});
it('combining chars', function(): void {
const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301#';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
});
it('surrogate chars', function(): void {
const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞#';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
});
it('surrogate combining chars', function(): void {
const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301#';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
});
it('fullwidth chars', function(): void {
const input = '1234567890#';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
});
it('fullwidth chars offset 1', function(): void {
const input = 'a1234567890#';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
});
// TODO: multiline tests once #1685 is resolved
});
-22
View File
@@ -169,25 +169,3 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
return wcwidthHigh(num);
};
})({nul: 0, control: 0}); // configurable options
/**
* Get the terminal cell width for a string.
*/
export function getStringCellWidth(s: string): number {
let result = 0;
for (let i = 0; i < s.length; ++i) {
let code = s.charCodeAt(i);
if (0xD800 <= code && code <= 0xDBFF) {
const low = s.charCodeAt(i + 1);
if (isNaN(low)) {
return result;
}
code = ((code - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;
}
if (0xDC00 <= code && code <= 0xDFFF) {
continue;
}
result += wcwidth(code);
}
return result;
}
+1 -1
View File
@@ -61,7 +61,7 @@ export class CompositionHelper {
/**
* Handles the compositionupdate event, updating the composition view.
* @param ev The event.
* @param {CompositionEvent} ev The event.
*/
public compositionupdate(ev: CompositionEvent): void {
this._compositionView.textContent = ev.data;
+2 -2
View File
@@ -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 i++;
while (i < l && data.charCodeAt(i) > 0x1f && data.charCodeAt(i) < 0x80);
do code = data.charCodeAt(++i);
while (i < l && code > 0x1f && code < 0x80);
i--;
continue;
}
@@ -5,7 +5,7 @@
import { XtermListener } from './Types';
import { IEventEmitter, IDisposable } from 'xterm';
import { Disposable } from './Lifecycle';
import { Disposable } from './common/Lifecycle';
export class EventEmitter extends Disposable implements IEventEmitter, IDisposable {
private _events: {[type: string]: XtermListener[]};
@@ -30,15 +30,14 @@ export class EventEmitter extends Disposable implements IEventEmitter, IDisposab
public addDisposableListener(type: string, handler: XtermListener): IDisposable {
// TODO: Rename addDisposableEventListener to more easily disambiguate from Dom listener
this.on(type, handler);
let disposed = false;
return {
dispose: () => {
if (disposed) {
if (!handler) {
// Already disposed
return;
}
this.off(type, handler);
disposed = true;
handler = null;
}
};
}
+1 -392
View File
@@ -3,102 +3,9 @@
* @license MIT
*/
import { assert, expect } from 'chai';
import { assert } 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', () => {
@@ -177,302 +84,4 @@ 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 ');
});
});
+137 -119
View File
@@ -4,16 +4,15 @@
* @license MIT
*/
import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types';
import { CharData, IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer } 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, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR } 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`.
@@ -25,6 +24,26 @@ const GLEVEL: {[key: string]: number} = {'(': 0, ')': 1, '*': 2, '+': 3, '-': 1,
* DCS subparser implementations
*/
/**
* DCS + q Pt ST (xterm)
* Request Terminfo String
* not supported
*/
class RequestTerminfo implements IDcsHandler {
private _data: string;
constructor(private _terminal: any) { }
hook(collect: string, params: number[], flag: number): void {
this._data = '';
}
put(data: string, start: number, end: number): void {
this._data += data.substring(start, end);
}
unhook(): void {
// invalid: DCS 0 + r Pt ST
this._terminal.send(`${C0.ESC}P0+r${this._data}${C0.ESC}\\`);
}
}
/**
* DCS $ q Pt ST
* DECRQSS (https://vt100.net/docs/vt510-rm/DECRQSS.html)
@@ -49,25 +68,25 @@ class DECRQSS implements IDcsHandler {
switch (this._data) {
// valid: DCS 1 $ r Pt ST (xterm)
case '"q': // DECSCA
return this._terminal.handler(`${C0.ESC}P1$r0"q${C0.ESC}\\`);
return this._terminal.send(`${C0.ESC}P1$r0"q${C0.ESC}\\`);
case '"p': // DECSCL
return this._terminal.handler(`${C0.ESC}P1$r61"p${C0.ESC}\\`);
return this._terminal.send(`${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.handler(`${C0.ESC}P1$r${pt}${C0.ESC}\\`);
return this._terminal.send(`${C0.ESC}P1$r${pt}${C0.ESC}\\`);
case 'm': // SGR
// TODO: report real settings instead of 0m
return this._terminal.handler(`${C0.ESC}P1$r0m${C0.ESC}\\`);
return this._terminal.send(`${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.handler(`${C0.ESC}P1$r${style} q${C0.ESC}\\`);
return this._terminal.send(`${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.handler(`${C0.ESC}P0$r${C0.ESC}\\`);
this._terminal.send(`${C0.ESC}P0$r${this._data}${C0.ESC}\\`);
}
}
}
@@ -97,7 +116,7 @@ export class InputHandler extends Disposable implements IInputHandler {
private _surrogateHigh: string;
constructor(
protected _terminal: IInputHandlingTerminal,
private _terminal: any, // TODO: reestablish IInputHandlingTerminal here
private _parser: IEscapeSequenceParser = new EscapeSequenceParser())
{
super();
@@ -110,16 +129,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, flag: String.fromCharCode(flag) });
this._terminal.error('Unknown CSI code: ', collect, params, String.fromCharCode(flag));
});
this._parser.setEscHandlerFallback((collect: string, flag: number) => {
this._terminal.error('Unknown ESC code: ', { collect, flag: String.fromCharCode(flag) });
this._terminal.error('Unknown ESC code: ', collect, 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);
});
/**
@@ -268,6 +287,7 @@ export class InputHandler extends Disposable implements IInputHandler {
* DCS handler
*/
this._parser.setDcsHandler('$q', new DECRQSS(this._terminal));
this._parser.setDcsHandler('+q', new RequestTerminfo(this._terminal));
}
public dispose(): void {
@@ -276,17 +296,10 @@ 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;
// TODO: Consolidate debug/logging #1560
if ((<any>this._terminal).debug) {
if (this._terminal.debug) {
this._terminal.log('data: ' + data);
}
@@ -361,20 +374,18 @@ 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) {
const chMinusOne = bufferRow.get(buffer.x - 1);
if (chMinusOne) {
if (!chMinusOne[CHAR_DATA_WIDTH_INDEX]) {
if (bufferRow[buffer.x - 1]) {
if (!bufferRow[buffer.x - 1][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
const chMinusTwo = bufferRow.get(buffer.x - 2);
if (chMinusTwo) {
chMinusTwo[CHAR_DATA_CHAR_INDEX] += char;
chMinusTwo[CHAR_DATA_CODE_INDEX] = code;
if (bufferRow[buffer.x - 2]) {
bufferRow[buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char;
bufferRow[buffer.x - 2][CHAR_DATA_CODE_INDEX] = code;
}
} else {
chMinusOne[CHAR_DATA_CHAR_INDEX] += char;
chMinusOne[CHAR_DATA_CODE_INDEX] = code;
bufferRow[buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char;
bufferRow[buffer.x - 1][CHAR_DATA_CODE_INDEX] = code;
}
}
continue;
@@ -394,7 +405,7 @@ export class InputHandler extends Disposable implements IInputHandler {
} else {
// The line already exists (eg. the initial viewport), mark it as a
// wrapped line
buffer.lines.get(buffer.y).isWrapped = true;
(<any>buffer.lines.get(buffer.y)).isWrapped = true;
}
// row changed, get it again
bufferRow = buffer.lines.get(buffer.y + buffer.ybase);
@@ -417,24 +428,23 @@ 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
&& chMinusTwo
&& chMinusTwo[CHAR_DATA_WIDTH_INDEX] === 2) {
bufferRow.set(this._terminal.cols - 2, [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
&& 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) */ ];
}
// insert empty cell at cursor
bufferRow.splice(buffer.x, 0, [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
bufferRow.splice(buffer.x, 0, [curAttr, ' ', 1, 32 /* ' '.charCodeAt(0) */ ]);
}
}
// write current char to buffer and advance cursor
bufferRow.set(buffer.x++, [curAttr, char, chWidth, code]);
bufferRow[buffer.x++] = [curAttr, char, chWidth, code];
// fullwidth char - also set next cell to placeholder stub and advance cursor
if (chWidth === 2) {
bufferRow.set(buffer.x++, [curAttr, '', 0, undefined]);
bufferRow[buffer.x++] = [curAttr, '', 0, undefined];
}
}
this._terminal.updateRange(buffer.y);
@@ -456,7 +466,7 @@ export class InputHandler extends Disposable implements IInputHandler {
// make buffer local for faster access
const buffer = this._terminal.buffer;
if (this._terminal.options.convertEol) {
if (this._terminal.convertEol) {
buffer.x = 0;
}
buffer.y++;
@@ -529,12 +539,20 @@ export class InputHandler extends Disposable implements IInputHandler {
* Insert Ps (Blank) Character(s) (default = 1) (ICH).
*/
public insertChars(params: number[]): void {
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);
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();
}
}
/**
@@ -694,21 +712,6 @@ 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).
@@ -725,30 +728,22 @@ export class InputHandler extends Disposable implements IInputHandler {
let j;
switch (params[0]) {
case 0:
j = this._terminal.buffer.y;
this._terminal.updateRange(j);
this._eraseInBufferLine(j++, this._terminal.buffer.x, this._terminal.cols);
this._terminal.eraseRight(this._terminal.buffer.x, this._terminal.buffer.y);
j = this._terminal.buffer.y + 1;
for (; j < this._terminal.rows; j++) {
this._eraseInBufferLine(j, 0, this._terminal.cols);
this._terminal.eraseLine(j);
}
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._eraseInBufferLine(j, 0, this._terminal.cols);
this._terminal.eraseLine(j);
}
this._terminal.updateRange(0);
break;
case 2:
j = this._terminal.rows;
this._terminal.updateRange(j - 1);
while (j--) {
this._eraseInBufferLine(j, 0, this._terminal.cols);
}
this._terminal.updateRange(0);
while (j--) this._terminal.eraseLine(j);
break;
case 3:
// Clear scrollback (everything not in viewport)
@@ -778,16 +773,15 @@ export class InputHandler extends Disposable implements IInputHandler {
public eraseInLine(params: number[]): void {
switch (params[0]) {
case 0:
this._eraseInBufferLine(this._terminal.buffer.y, this._terminal.buffer.x, this._terminal.cols);
this._terminal.eraseRight(this._terminal.buffer.x, this._terminal.buffer.y);
break;
case 1:
this._eraseInBufferLine(this._terminal.buffer.y, 0, this._terminal.buffer.x + 1);
this._terminal.eraseLeft(this._terminal.buffer.x, this._terminal.buffer.y);
break;
case 2:
this._eraseInBufferLine(this._terminal.buffer.y, 0, this._terminal.cols);
this._terminal.eraseLine(this._terminal.buffer.y);
break;
}
this._terminal.updateRange(this._terminal.buffer.y);
}
/**
@@ -811,7 +805,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, BufferLine.blankLine(this._terminal.cols, this._terminal.eraseAttr()));
buffer.lines.splice(row, 0, this._terminal.blankLine(true));
}
// this.maxRange();
@@ -841,7 +835,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, BufferLine.blankLine(this._terminal.cols, this._terminal.eraseAttr()));
buffer.lines.splice(j, 0, this._terminal.blankLine(true));
}
// this.maxRange();
@@ -854,12 +848,22 @@ export class InputHandler extends Disposable implements IInputHandler {
* Delete Ps Character(s) (default = 1) (DCH).
*/
public deleteChars(params: number[]): void {
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);
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);
}
/**
@@ -873,7 +877,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, BufferLine.blankLine(this._terminal.cols, DEFAULT_ATTR));
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, this._terminal.blankLine());
}
// this.maxRange();
this._terminal.updateRange(buffer.scrollTop);
@@ -892,7 +896,7 @@ export class InputHandler extends Disposable implements IInputHandler {
while (param--) {
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1);
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, BufferLine.blankLine(this._terminal.cols, DEFAULT_ATTR));
buffer.lines.splice(buffer.ybase + buffer.scrollTop, 0, this._terminal.blankLine());
}
// this.maxRange();
this._terminal.updateRange(buffer.scrollTop);
@@ -905,11 +909,21 @@ export class InputHandler extends Disposable implements IInputHandler {
* Erase Ps Character(s) (default = 1) (ECH).
*/
public eraseChars(params: number[]): void {
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]
);
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;
}
}
/**
@@ -961,14 +975,17 @@ 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);
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?
const ch = line[buffer.x - 1] || [DEFAULT_ATTR, ' ', 1, 32];
while (param--) {
line[buffer.x++] = ch;
}
}
/**
@@ -1015,24 +1032,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.handler(C0.ESC + '[?1;2c');
this._terminal.send(C0.ESC + '[?1;2c');
} else if (this._terminal.is('linux')) {
this._terminal.handler(C0.ESC + '[?6c');
this._terminal.send(C0.ESC + '[?6c');
}
} else if (collect === '>') {
// xterm and urxvt
// seem to spit this
// out around ~370 times (?).
if (this._terminal.is('xterm')) {
this._terminal.handler(C0.ESC + '[>0;276;0c');
this._terminal.send(C0.ESC + '[>0;276;0c');
} else if (this._terminal.is('rxvt-unicode')) {
this._terminal.handler(C0.ESC + '[>85;95;0c');
this._terminal.send(C0.ESC + '[>85;95;0c');
} else if (this._terminal.is('linux')) {
// not supported by linux console.
// linux console echoes parameters.
this._terminal.handler(params[0] + 'c');
this._terminal.send(params[0] + 'c');
} else if (this._terminal.is('screen')) {
this._terminal.handler(C0.ESC + '[>83;40003;0c');
this._terminal.send(C0.ESC + '[>83;40003;0c');
}
}
}
@@ -1607,9 +1624,6 @@ 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;
@@ -1698,13 +1712,15 @@ export class InputHandler extends Disposable implements IInputHandler {
switch (params[0]) {
case 5:
// status report
this._terminal.emit('data', `${C0.ESC}[0n`);
this._terminal.send(C0.ESC + '[0n');
break;
case 6:
// cursor position
const y = this._terminal.buffer.y + 1;
const x = this._terminal.buffer.x + 1;
this._terminal.emit('data', `${C0.ESC}[${y};${x}R`);
this._terminal.send(C0.ESC + '['
+ (this._terminal.buffer.y + 1)
+ ';'
+ (this._terminal.buffer.x + 1)
+ 'R');
break;
}
} else if (collect === '?') {
@@ -1713,25 +1729,27 @@ export class InputHandler extends Disposable implements IInputHandler {
switch (params[0]) {
case 6:
// cursor position
const y = this._terminal.buffer.y + 1;
const x = this._terminal.buffer.x + 1;
this._terminal.emit('data', `${C0.ESC}[?${y};${x}R`);
this._terminal.send(C0.ESC + '[?'
+ (this._terminal.buffer.y + 1)
+ ';'
+ (this._terminal.buffer.x + 1)
+ 'R');
break;
case 15:
// no printer
// this.handler(C0.ESC + '[?11n');
// this.send(C0.ESC + '[?11n');
break;
case 25:
// dont support user defined keys
// this.handler(C0.ESC + '[?21n');
// this.send(C0.ESC + '[?21n');
break;
case 26:
// north american keyboard
// this.handler(C0.ESC + '[?27;1;0;0n');
// this.send(C0.ESC + '[?27;1;0;0n');
break;
case 53:
// no dec locator/mouse
// this.handler(C0.ESC + '[?50n');
// this.send(C0.ESC + '[?50n');
break;
}
}
+5 -102
View File
@@ -5,11 +5,10 @@
import { assert } from 'chai';
import { IMouseZoneManager, IMouseZone } from './ui/Types';
import { ILinkMatcher, ITerminal, IBufferLine } from './Types';
import { ILinkMatcher, LineData, ITerminal } from './Types';
import { Linkifier } from './Linkifier';
import { MockBuffer, MockTerminal, TestTerminal } from './utils/TestUtils.test';
import { MockBuffer, MockTerminal } from './utils/TestUtils.test';
import { CircularList } from './common/CircularList';
import { BufferLine } from './BufferLine';
class TestLinkifier extends Linkifier {
constructor(terminal: ITerminal) {
@@ -42,16 +41,15 @@ describe('Linkifier', () => {
beforeEach(() => {
terminal = new MockTerminal();
terminal.cols = 100;
terminal.rows = 10;
terminal.buffer = new MockBuffer();
(<MockBuffer>terminal.buffer).setLines(new CircularList<IBufferLine>(20));
(<MockBuffer>terminal.buffer).setLines(new CircularList<LineData>(20));
terminal.buffer.ydisp = 0;
linkifier = new TestLinkifier(terminal);
mouseZoneManager = new TestMouseZoneManager();
});
function stringToRow(text: string): IBufferLine {
const result = new BufferLine();
function stringToRow(text: string): LineData {
const result: LineData = [];
for (let i = 0; i < text.length; i++) {
result.push([0, text.charAt(i), 1, text.charCodeAt(i)]);
}
@@ -65,7 +63,6 @@ describe('Linkifier', () => {
function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, links: {x: number, length: number}[], done: MochaDone): void {
addRow(rowText);
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
terminal.rows = terminal.buffer.lines.length - 1;
linkifier.linkifyRows();
// Allow linkify to happen
setTimeout(() => {
@@ -240,98 +237,4 @@ describe('Linkifier', () => {
});
});
});
describe('unicode handling', () => {
let terminal: TestTerminal;
// other than the tests above unicode testing needs the full terminal instance
// to get the special handling of fullwidth, surrogate and combining chars in the input handler
beforeEach(() => {
terminal = new TestTerminal({cols: 10, rows: 5});
linkifier = new TestLinkifier(terminal);
mouseZoneManager = new TestMouseZoneManager();
linkifier.attachToDom(mouseZoneManager);
});
function assertLinkifiesInTerminal(rowText: string, linkMatcherRegex: RegExp, links: {x1: number, y1: number, x2: number, y2: number}[], done: MochaDone): void {
terminal.writeSync(rowText);
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
linkifier.linkifyRows();
// Allow linkify to happen
setTimeout(() => {
assert.equal(mouseZoneManager.zones.length, links.length);
links.forEach((l, i) => {
assert.equal(mouseZoneManager.zones[i].x1, l.x1 + 1);
assert.equal(mouseZoneManager.zones[i].x2, l.x2 + 1);
assert.equal(mouseZoneManager.zones[i].y1, l.y1 + 1);
assert.equal(mouseZoneManager.zones[i].y2, l.y2 + 1);
});
done();
}, 0);
}
describe('unicode before the match', () => {
it('combining - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done);
});
it('combining - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
});
it('surrogate - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done);
});
it('surrogate - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
});
it('combining surrogate - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done);
});
it('combining surrogate - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
});
it('fullwidth - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
});
it('fullwidth - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
});
it('combining fullwidth - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
});
it('combining fullwidth - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
});
});
describe('unicode within the match', () => {
it('combining - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('test cafe\u0301', /cafe\u0301/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done);
});
it('combining - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('testtest cafe\u0301', /cafe\u0301/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done);
});
it('surrogate - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('test a𝄞b', /a𝄞b/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
});
it('surrogate - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('testtest a𝄞b', /a𝄞b/, [{x1: 9, x2: 2, y1: 0, y2: 1}], done);
});
it('combining surrogate - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('test a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
});
it('combining surrogate - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('testtest a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 9, x2: 2, y1: 0, y2: 1}], done);
});
it('fullwidth - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('test a1b', /a1b/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done);
});
it('fullwidth - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('testtest a1b', /a1b/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done);
});
it('combining fullwidth - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('test a¥\u0301b', /a¥\u0301b/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done);
});
it('combining fullwidth - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('testtest a¥\u0301b', /a¥\u0301b/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done);
});
});
});
});
+85 -103
View File
@@ -4,11 +4,9 @@
*/
import { IMouseZoneManager } from './ui/Types';
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal, IBufferStringIteratorResult } from './Types';
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal } from './Types';
import { MouseZone } from './ui/MouseZoneManager';
import { EventEmitter } from './common/EventEmitter';
import { CHAR_DATA_ATTR_INDEX } from './Buffer';
import { getStringCellWidth } from './CharWidth';
import { EventEmitter } from './EventEmitter';
/**
* The Linkifier applies links to rows shortly after they have been refreshed.
@@ -21,19 +19,12 @@ export class Linkifier extends EventEmitter implements ILinkifier {
*/
protected static readonly TIME_BEFORE_LINKIFY = 200;
/**
* Limit of the unwrapping line expansion (overscan) at the top and bottom
* of the actual viewport in ASCII characters.
* A limit of 2000 should match most sane urls.
*/
protected static readonly OVERSCAN_CHAR_LIMIT = 2000;
protected _linkMatchers: ILinkMatcher[] = [];
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
@@ -88,37 +79,9 @@ export class Linkifier extends EventEmitter implements ILinkifier {
*/
private _linkifyRows(): void {
this._rowsTimeoutId = null;
const buffer = this._terminal.buffer;
// Ensure the start row exists
const absoluteRowIndexStart = buffer.ydisp + this._rowsToLinkify.start;
if (absoluteRowIndexStart >= buffer.lines.length) {
return;
for (let i = this._rowsToLinkify.start; i <= this._rowsToLinkify.end; i++) {
this._linkifyRow(i);
}
// Invalidate bad end row values (if a resize happened)
const absoluteRowIndexEnd = buffer.ydisp + Math.min(this._rowsToLinkify.end, this._terminal.rows) + 1;
// Iterate over the range of unwrapped content strings within start..end
// (excluding).
// _doLinkifyRow gets full unwrapped lines with the start row as buffer offset
// for every matcher.
// The unwrapping is needed to also match content that got wrapped across
// several buffer lines. To avoid a worst case scenario where the whole buffer
// contains just a single unwrapped string we limit this line expansion beyond
// the viewport to +OVERSCAN_CHAR_LIMIT chars (overscan) at top and bottom.
// This comes with the tradeoff that matches longer than OVERSCAN_CHAR_LIMIT
// chars will not match anymore at the viewport borders.
const overscanLineLimit = Math.ceil(Linkifier.OVERSCAN_CHAR_LIMIT / this._terminal.cols);
const iterator = this._terminal.buffer.iterator(
false, absoluteRowIndexStart, absoluteRowIndexEnd, overscanLineLimit, overscanLineLimit);
while (iterator.hasNext()) {
const lineData: IBufferStringIteratorResult = iterator.next();
for (let i = 0; i < this._linkMatchers.length; i++) {
this._doLinkifyRow(lineData.range.first, lineData.content, this._linkMatchers[i]);
}
}
this._rowsToLinkify.start = null;
this._rowsToLinkify.end = null;
}
@@ -190,60 +153,81 @@ export class Linkifier extends EventEmitter implements ILinkifier {
}
/**
* Linkifies a row given a specific handler.
* @param rowIndex The row index to linkify (absolute index).
* @param text string content of the unwrapped row.
* @param matcher The link matcher for this line.
* Linkifies a row.
* @param rowIndex The index of the row to linkify.
*/
private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher): void {
// clone regex to do a global search on text
const rex = new RegExp(matcher.regex.source, matcher.regex.flags + 'g');
let match;
let stringIndex = -1;
while ((match = rex.exec(text)) !== null) {
const uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex];
if (!uri) {
// something matched but does not comply with the given matchIndex
// since this is most likely a bug the regex itself we simply do nothing here
// DEBUG: print match and throw
if ((<any>this._terminal).debug) {
console.log({match, matcher});
throw new Error('match found without corresponding matchIndex');
private _linkifyRow(rowIndex: number): void {
// Ensure the row exists
let absoluteRowIndex = this._terminal.buffer.ydisp + rowIndex;
if (absoluteRowIndex >= this._terminal.buffer.lines.length) {
return;
}
if ((<any>this._terminal.buffer.lines.get(absoluteRowIndex)).isWrapped) {
// Only attempt to linkify rows that start in the viewport
if (rowIndex !== 0) {
return;
}
// If the first row is wrapped, backtrack to find the origin row and linkify that
do {
rowIndex--;
absoluteRowIndex--;
} while ((<any>this._terminal.buffer.lines.get(absoluteRowIndex)).isWrapped);
}
// Construct full unwrapped line text
let text = this._terminal.buffer.translateBufferLineToString(absoluteRowIndex, false);
let currentIndex = absoluteRowIndex + 1;
while (currentIndex < this._terminal.buffer.lines.length &&
(<any>this._terminal.buffer.lines.get(currentIndex)).isWrapped) {
text += this._terminal.buffer.translateBufferLineToString(currentIndex++, false);
}
for (let i = 0; i < this._linkMatchers.length; i++) {
this._doLinkifyRow(rowIndex, text, this._linkMatchers[i]);
}
}
/**
* Linkifies a row given a specific handler.
* @param rowIndex The row index to linkify.
* @param text The text of the row (excludes text in the row that's already
* linkified).
* @param matcher The link matcher for this line.
* @param offset The how much of the row has already been linkified.
* @return The link element(s) that were added.
*/
private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher, offset: number = 0): void {
// Find the first match
const match = text.match(matcher.regex);
if (!match || match.length === 0) {
return;
}
const uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex];
// Get index, match.index is for the outer match which includes negated chars
const index = text.indexOf(uri);
// Ensure the link is valid before registering
if (matcher.validationCallback) {
matcher.validationCallback(uri, isValid => {
// Discard link if the line has already changed
if (this._rowsTimeoutId) {
return;
}
break;
}
if (isValid) {
this._addLink(offset + index, rowIndex, uri, matcher);
}
});
} else {
this._addLink(offset + index, rowIndex, uri, matcher);
}
// Get index, match.index is for the outer match which includes negated chars
// therefore we cannot use match.index directly, instead we search the position
// of the match group in text again
// also correct regex and string search offsets for the next loop run
stringIndex = text.indexOf(uri, stringIndex + 1);
rex.lastIndex = stringIndex + uri.length;
// get the buffer index as [absolute row, col] for the match
const bufferIndex = this._terminal.buffer.stringIndexToBufferIndex(rowIndex, stringIndex);
const line = this._terminal.buffer.lines.get(bufferIndex[0]);
const char = line.get(bufferIndex[1]);
let fg: number | undefined;
if (char) {
const attr: number = char[CHAR_DATA_ATTR_INDEX];
fg = (attr >> 9) & 0x1ff;
}
if (matcher.validationCallback) {
matcher.validationCallback(uri, isValid => {
// Discard link if the line has already changed
if (this._rowsTimeoutId) {
return;
}
if (isValid) {
this._addLink(bufferIndex[1], bufferIndex[0] - this._terminal.buffer.ydisp, uri, matcher, fg);
}
});
} else {
this._addLink(bufferIndex[1], bufferIndex[0] - this._terminal.buffer.ydisp, uri, matcher, fg);
}
// Recursively check for links in the rest of the text
const remainingStartIndex = index + uri.length;
const remainingText = text.substr(remainingStartIndex);
if (remainingText.length > 0) {
this._doLinkifyRow(rowIndex, remainingText, matcher, offset + remainingStartIndex);
}
}
@@ -253,14 +237,12 @@ 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, fg: number): void {
const width = getStringCellWidth(uri);
private _addLink(x: number, y: number, uri: string, matcher: ILinkMatcher): void {
const x1 = x % this._terminal.cols;
const y1 = y + Math.floor(x / this._terminal.cols);
let x2 = (x1 + width) % this._terminal.cols;
let y2 = y1 + Math.floor((x1 + width) / this._terminal.cols);
let x2 = (x1 + uri.length) % this._terminal.cols;
let y2 = y1 + Math.floor((x1 + uri.length) / this._terminal.cols);
if (x2 === 0) {
x2 = this._terminal.cols;
y2--;
@@ -278,17 +260,17 @@ export class Linkifier extends EventEmitter implements ILinkifier {
window.open(uri, '_blank');
},
e => {
this.emit(LinkHoverEventTypes.HOVER, this._createLinkHoverEvent(x1, y1, x2, y2, fg));
this.emit(LinkHoverEventTypes.HOVER, this._createLinkHoverEvent(x1, y1, x2, y2));
this._terminal.element.classList.add('xterm-cursor-pointer');
},
e => {
this.emit(LinkHoverEventTypes.TOOLTIP, this._createLinkHoverEvent(x1, y1, x2, y2, fg));
this.emit(LinkHoverEventTypes.TOOLTIP, this._createLinkHoverEvent(x1, y1, x2, y2));
if (matcher.hoverTooltipCallback) {
matcher.hoverTooltipCallback(e, uri);
}
},
() => {
this.emit(LinkHoverEventTypes.LEAVE, this._createLinkHoverEvent(x1, y1, x2, y2, fg));
this.emit(LinkHoverEventTypes.LEAVE, this._createLinkHoverEvent(x1, y1, x2, y2));
this._terminal.element.classList.remove('xterm-cursor-pointer');
if (matcher.hoverLeaveCallback) {
matcher.hoverLeaveCallback();
@@ -303,7 +285,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
));
}
private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number, fg: number): ILinkHoverEvent {
return { x1, y1, x2, y2, cols: this._terminal.cols, fg };
private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number): ILinkHoverEvent {
return { x1, y1, x2, y2, cols: this._terminal.cols };
}
}
+8 -50
View File
@@ -8,9 +8,8 @@ import { CharMeasure } from './ui/CharMeasure';
import { SelectionManager, SelectionMode } from './SelectionManager';
import { SelectionModel } from './SelectionModel';
import { BufferSet } from './BufferSet';
import { ITerminal, IBuffer, IBufferLine } from './Types';
import { LineData, CharData, ITerminal, IBuffer } from './Types';
import { MockTerminal } from './utils/TestUtils.test';
import { BufferLine } from './BufferLine';
class TestMockTerminal extends MockTerminal {
emit(event: string, data: any): void {}
@@ -53,18 +52,16 @@ describe('SelectionManager', () => {
selectionManager = new TestSelectionManager(terminal, null);
});
function stringToRow(text: string): IBufferLine {
const result = new BufferLine();
function stringToRow(text: string): LineData {
const result: LineData = [];
for (let i = 0; i < text.length; i++) {
result.push([0, text.charAt(i), 1, text.charCodeAt(i)]);
}
return result;
}
function stringArrayToRow(chars: string[]): IBufferLine {
const line = new BufferLine();
chars.map(c => line.push([0, c, 1, c.charCodeAt(0)]));
return line;
function stringArrayToRow(chars: string[]): LineData {
return chars.map(c => <CharData>[0, c, 1, c.charCodeAt(0)]);
}
describe('_selectWordAt', () => {
@@ -100,8 +97,7 @@ describe('SelectionManager', () => {
});
it('should expand selection for wide characters', () => {
// Wide characters use a special format
const line = new BufferLine();
const data: [number, string, number, number][] = [
buffer.lines.set(0, [
[null, '中', 2, '中'.charCodeAt(0)],
[null, '', 0, null],
[null, '文', 2, '文'.charCodeAt(0)],
@@ -117,9 +113,7 @@ 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, '中文');
@@ -189,42 +183,6 @@ 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
@@ -345,7 +303,7 @@ describe('SelectionManager', () => {
it('should select the entire wrapped line', () => {
buffer.lines.set(0, stringToRow('foo'));
const line2 = stringToRow('bar');
line2.isWrapped = true;
(<any>line2).isWrapped = true;
buffer.lines.set(1, line2);
selectionManager.selectLineAt(0);
assert.equal(selectionManager.selectionText, 'foobar', 'The selected text is correct');
+31 -96
View File
@@ -3,14 +3,13 @@
* @license MIT
*/
import { ITerminal, ISelectionManager, IBuffer, CharData, IBufferLine } from './Types';
import { XtermListener } from './common/Types';
import { ITerminal, ISelectionManager, IBuffer, CharData, XtermListener } from './Types';
import { MouseHelper } from './utils/MouseHelper';
import * as Browser from './shared/utils/Browser';
import { CharMeasure } from './ui/CharMeasure';
import { EventEmitter } from './common/EventEmitter';
import { EventEmitter } from './EventEmitter';
import { SelectionModel } from './SelectionModel';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_CODE_INDEX } from './Buffer';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
import { AltClickHandler } from './handlers/AltClickHandler';
/**
@@ -205,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 (bufferLine.isWrapped) {
if ((<any>bufferLine).isWrapped) {
result[result.length - 1] += lineText;
} else {
result.push(lineText);
@@ -216,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 (bufferLine.isWrapped) {
if ((<any>bufferLine).isWrapped) {
result[result.length - 1] += lineText;
} else {
result.push(lineText);
@@ -452,10 +451,8 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* Removes the listeners that are registered when mousedown is triggered.
*/
private _removeMouseDownListeners(): void {
if (this._terminal.element.ownerDocument) {
this._terminal.element.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener);
this._terminal.element.ownerDocument.removeEventListener('mouseup', this._mouseUpListener);
}
this._terminal.element.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener);
this._terminal.element.ownerDocument.removeEventListener('mouseup', this._mouseUpListener);
clearInterval(this._dragScrollIntervalTimer);
this._dragScrollIntervalTimer = null;
}
@@ -501,7 +498,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.get(this._model.selectionStart[0]);
const char = line[this._model.selectionStart[0]];
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
this._model.selectionStart[0]++;
}
@@ -577,21 +574,18 @@ 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. 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;
}
// 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;
}
// 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]).get(this._model.selectionEnd[0]);
const char = this._buffer.lines.get(this._model.selectionEnd[1])[this._model.selectionEnd[0]];
if (char && char[CHAR_DATA_WIDTH_INDEX] === 0) {
this._model.selectionEnd[0]++;
}
@@ -613,19 +607,10 @@ 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) {
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);
this._model.selectionEnd = [this._terminal.cols - 1, Math.min(this._terminal.buffer.ydisp + this._terminal.rows, this._terminal.buffer.lines.length - 1)];
} else {
if (this._activeSelectionMode !== SelectionMode.COLUMN) {
this._model.selectionEnd[0] = 0;
}
this._model.selectionEnd[1] = this._terminal.buffer.ydisp;
this._model.selectionEnd = [0, this._terminal.buffer.ydisp];
}
this.refresh();
}
@@ -662,10 +647,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: IBufferLine, coords: [number, number]): number {
private _convertViewportColToCharacterIndex(bufferLine: any, coords: [number, number]): number {
let charIndex = coords[0];
for (let i = 0; coords[0] >= i; i++) {
const char = bufferLine.get(i);
const char = bufferLine[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.
@@ -692,7 +677,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, followWrappedLinesAbove: boolean = true, followWrappedLinesBelow: boolean = true): IWordPosition {
private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): IWordPosition {
// Ensure coords are within viewport (eg. not within scroll bar)
if (coords[0] >= this._terminal.cols) {
return null;
@@ -734,24 +719,24 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
// Consider the initial position, skip it and increment the wide char
// variable
if (bufferLine.get(startCol)[CHAR_DATA_WIDTH_INDEX] === 0) {
if (bufferLine[startCol][CHAR_DATA_WIDTH_INDEX] === 0) {
leftWideCharCount++;
startCol--;
}
if (bufferLine.get(endCol)[CHAR_DATA_WIDTH_INDEX] === 2) {
if (bufferLine[endCol][CHAR_DATA_WIDTH_INDEX] === 2) {
rightWideCharCount++;
endCol++;
}
// Adjust the end index for characters whose length are > 1 (emojis)
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;
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;
}
// Expand the string in both directions until a space is hit
while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.get(startCol - 1))) {
const char = bufferLine.get(startCol - 1);
while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine[startCol - 1])) {
const char = bufferLine[startCol - 1];
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
// If the next character is a wide char, record it and skip the column
leftWideCharCount++;
@@ -765,8 +750,8 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
startIndex--;
startCol--;
}
while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.get(endCol + 1))) {
const char = bufferLine.get(endCol + 1);
while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine[endCol + 1])) {
const char = bufferLine[endCol + 1];
if (char[CHAR_DATA_WIDTH_INDEX] === 2) {
// If the next character is a wide char, record it and skip the column
rightWideCharCount++;
@@ -787,7 +772,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
// Calculate the start _column_, converting the the string indexes back to
// column coordinates.
let start =
const 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
@@ -795,7 +780,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
// Calculate the length in _columns_, converting the the string indexes back
// to column coordinates.
let length = Math.min(this._terminal.cols, // Disallow lengths larger than the terminal cols
const 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
@@ -807,34 +792,6 @@ 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 };
}
@@ -846,11 +803,6 @@ 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;
}
@@ -863,24 +815,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
private _selectToWordAt(coords: [number, number]): void {
const wordPosition = this._getWordAt(coords, true);
if (wordPosition) {
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];
this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : (wordPosition.start + wordPosition.length), coords[1]];
}
}
-5
View File
@@ -128,11 +128,6 @@ 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;
+2 -6
View File
@@ -76,13 +76,9 @@ export class SelectionModel {
return null;
}
// Use the selection start + length if the end doesn't exist or they're reversed
// Use the selection start if the end doesn't exist or they're reversed
if (!this.selectionEnd || this.areSelectionValuesReversed()) {
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]];
return [this.selectionStart[0] + this.selectionStartLength, this.selectionStart[1]];
}
// Ensure the the word/line is selected after a double/triple click
+3 -2
View File
@@ -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).get(cell)[CHAR_DATA_CHAR_INDEX];
lineText += term.buffer.lines.get(line)[cell][CHAR_DATA_CHAR_INDEX];
}
// rtrim empty cells as xterm does
lineText = lineText.replace(/\s+$/, '');
@@ -105,7 +105,6 @@ 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,
@@ -164,5 +163,7 @@ 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'));
});
});
+143 -262
View File
@@ -6,8 +6,7 @@
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, DEFAULT_ATTR } from './Buffer';
import { BufferLine } from './BufferLine';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer';
const INIT_COLS = 80;
const INIT_ROWS = 24;
@@ -26,7 +25,7 @@ describe('term.js addons', () => {
beforeEach(() => {
term = new TestTerminal(termOptions);
term.refresh = () => { };
term.refresh = () => {};
(<any>term).renderer = new MockRenderer();
term.viewport = new MockViewport();
(<any>term)._compositionHelper = new MockCompositionHelper();
@@ -37,8 +36,8 @@ describe('term.js addons', () => {
};
(<any>term).element = {
classList: {
toggle: () => { },
remove: () => { }
toggle: () => {},
remove: () => {}
}
};
});
@@ -68,142 +67,24 @@ describe('term.js addons', () => {
});
});
describe('on', () => {
beforeEach(() => {
term.on('key', () => { });
term.on('keypress', () => { });
term.on('keydown', () => { });
});
describe('data', () => {
it('should emit a data event', (done) => {
term.on('data', () => {
done();
});
term.handler('fake');
});
});
describe(`keypress (including 'key' event)`, () => {
it('should receive a string and event object', (done) => {
let steps = 0;
const finish = () => {
if ((++steps) === 2) {
done();
}
};
const evKeyPress = <KeyboardEvent>{
preventDefault: () => { },
stopPropagation: () => { },
type: 'keypress',
keyCode: 13
};
term.on('keypress', (key, event) => {
assert.equal(typeof key, 'string');
expect(event).to.be.an.instanceof(Object);
finish();
});
term.on('key', (key, event) => {
assert.equal(typeof key, 'string');
expect(event).to.be.an.instanceof(Object);
finish();
});
term.keyPress(evKeyPress);
});
});
describe(`keydown (including 'key' event)`, () => {
it(`should receive an event object for 'keydown' and a string and event object for 'key'`, (done) => {
let steps = 0;
const finish = () => {
if ((++steps) === 2) {
done();
}
};
const evKeyDown = <KeyboardEvent>{
preventDefault: () => { },
stopPropagation: () => { },
type: 'keydown',
keyCode: 13
};
term.on('keydown', (event) => {
expect(event).to.be.an.instanceof(Object);
finish();
});
term.on('key', (key, event) => {
assert.equal(typeof key, 'string');
expect(event).to.be.an.instanceof(Object);
finish();
});
term.keyDown(evKeyDown);
});
});
describe('resize', () => {
it('should receive an object: {cols: number, rows: number}', (done) => {
term.on('resize', (data) => {
expect(data).to.have.keys(['cols', 'rows']);
assert.equal(typeof data.cols, 'number');
assert.equal(typeof data.rows, 'number');
done();
});
term.resize(1, 1);
});
});
describe('scroll', () => {
it('should receive a number', (done) => {
term.on('scroll', (ydisp) => {
assert.equal(typeof ydisp, 'number');
done();
});
term.scroll();
});
});
describe('title', () => {
it('should receive a string', (done) => {
term.on('title', (title) => {
assert.equal(typeof title, 'string');
done();
});
term.handleTitle('title');
});
});
});
describe('attachCustomKeyEventHandler', () => {
const evKeyDown = <KeyboardEvent>{
preventDefault: () => { },
stopPropagation: () => { },
preventDefault: () => {},
stopPropagation: () => {},
type: 'keydown',
keyCode: 77
};
const evKeyPress = <KeyboardEvent>{
preventDefault: () => { },
stopPropagation: () => { },
preventDefault: () => {},
stopPropagation: () => {},
type: 'keypress',
keyCode: 77
};
beforeEach(() => {
term.handler = () => { };
term.showCursor = () => { };
term.clearSelection = () => { };
term.handler = () => {};
term.showCursor = () => {};
term.clearSelection = () => {};
});
it('should process the keydown/keypress event based on what the handler returns', () => {
@@ -260,7 +141,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), BufferLine.blankLine(term.cols, DEFAULT_ATTR));
assert.deepEqual(term.buffer.lines.get(i), term.blankLine());
}
});
it('should clear a buffer larger than rows', () => {
@@ -277,7 +158,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), BufferLine.blankLine(term.cols, DEFAULT_ATTR));
assert.deepEqual(term.buffer.lines.get(i), term.blankLine());
}
});
it('should not break the prompt when cleared twice', () => {
@@ -290,7 +171,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), BufferLine.blankLine(term.cols, DEFAULT_ATTR));
assert.deepEqual(term.buffer.lines.get(i), term.blankLine());
}
});
});
@@ -423,8 +304,8 @@ describe('term.js addons', () => {
type: 'keydown',
key: 'a',
keyCode: 65,
preventDefault: () => { },
stopPropagation: () => { }
preventDefault: () => {},
stopPropagation: () => {}
};
term.buffer.ydisp = 0;
@@ -456,62 +337,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).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
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.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).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], ' ');
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], ' ');
});
it('should properly scroll inside a scroll region (scrollTop set)', () => {
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(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.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).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
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');
});
it('should properly scroll inside a scroll region (scrollBottom set)', () => {
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.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.y = 3;
term.buffer.scrollBottom = 3;
term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS + 1);
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');
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');
});
it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => {
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.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.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).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');
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');
});
});
@@ -522,65 +403,65 @@ describe('term.js addons', () => {
});
it('should create a new line and shift everything up', () => {
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.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.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).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], ' ');
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], ' ');
});
it('should properly scroll inside a scroll region (scrollTop set)', () => {
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(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.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).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
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');
});
it('should properly scroll inside a scroll region (scrollBottom set)', () => {
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.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.y = 3;
term.buffer.scrollBottom = 3;
term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS);
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');
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');
});
it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => {
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.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.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).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');
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');
});
});
});
@@ -591,9 +472,9 @@ describe('term.js addons', () => {
let evKeyPress: any;
beforeEach(() => {
term.handler = () => { };
term.showCursor = () => { };
term.clearSelection = () => { };
term.handler = () => {};
term.showCursor = () => {};
term.clearSelection = () => {};
// term.compositionHelper = {
// isComposing: false,
// keydown: {
@@ -603,15 +484,15 @@ describe('term.js addons', () => {
// }
// };
evKeyDown = {
preventDefault: () => { },
stopPropagation: () => { },
preventDefault: () => {},
stopPropagation: () => {},
type: 'keydown',
altKey: null,
keyCode: null
};
evKeyPress = {
preventDefault: () => { },
stopPropagation: () => { },
preventDefault: () => {},
stopPropagation: () => {},
type: 'keypress',
altKey: null,
charCode: null,
@@ -773,11 +654,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).get(0);
const tchar = term.buffer.lines.get(0)[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).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' ');
term.reset();
}
});
@@ -786,9 +667,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).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(' ');
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(' ');
term.reset();
}
});
@@ -798,10 +679,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).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(' ');
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(' ');
term.reset();
}
});
@@ -812,9 +693,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).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(' ');
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(' ');
term.reset();
}
});
@@ -823,11 +704,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).get(0);
const tchar = term.buffer.lines.get(0)[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).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' ');
term.reset();
}
});
@@ -836,30 +717,30 @@ describe('term.js addons', () => {
describe('unicode - combining characters', () => {
it('café', () => {
term.write('cafe\u0301');
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);
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);
});
it('café - end of line', () => {
term.buffer.x = term.cols - 1 - 3;
term.write('cafe\u0301');
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);
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);
});
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).get(i);
const tchar = term.buffer.lines.get(0)[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).get(0);
const tchar = term.buffer.lines.get(1)[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);
@@ -868,12 +749,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).get(i);
const tchar = term.buffer.lines.get(0)[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).get(0);
const tchar = term.buffer.lines.get(1)[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);
@@ -896,7 +777,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).get(i);
const tchar = term.buffer.lines.get(0)[i];
if (i % 2) {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
@@ -907,7 +788,7 @@ describe('term.js addons', () => {
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
}
}
const tchar = term.buffer.lines.get(1).get(0);
const tchar = term.buffer.lines.get(1)[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);
@@ -917,7 +798,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).get(i);
const tchar = term.buffer.lines.get(0)[i];
if (!(i % 2)) {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
@@ -928,11 +809,11 @@ describe('term.js addons', () => {
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
}
}
let tchar = term.buffer.lines.get(0).get(term.cols - 1);
let tchar = term.buffer.lines.get(0)[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).get(0);
tchar = term.buffer.lines.get(1)[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);
@@ -942,7 +823,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).get(i);
const tchar = term.buffer.lines.get(0)[i];
if (!(i % 2)) {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
@@ -953,11 +834,11 @@ describe('term.js addons', () => {
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
}
}
let tchar = term.buffer.lines.get(0).get(term.cols - 1);
let tchar = term.buffer.lines.get(0)[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).get(0);
tchar = term.buffer.lines.get(1)[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);
@@ -966,7 +847,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).get(i);
const tchar = term.buffer.lines.get(0)[i];
if (i % 2) {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
@@ -977,7 +858,7 @@ describe('term.js addons', () => {
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
}
}
const tchar = term.buffer.lines.get(1).get(0);
const tchar = term.buffer.lines.get(1)[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);
@@ -987,7 +868,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).get(i);
const tchar = term.buffer.lines.get(0)[i];
if (!(i % 2)) {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
@@ -998,11 +879,11 @@ describe('term.js addons', () => {
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
}
}
let tchar = term.buffer.lines.get(0).get(term.cols - 1);
let tchar = term.buffer.lines.get(0)[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).get(0);
tchar = term.buffer.lines.get(1)[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);
@@ -1011,7 +892,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).get(i);
const tchar = term.buffer.lines.get(0)[i];
if (i % 2) {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
@@ -1022,7 +903,7 @@ describe('term.js addons', () => {
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
}
}
const tchar = term.buffer.lines.get(1).get(0);
const tchar = term.buffer.lines.get(1)[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);
@@ -1037,10 +918,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).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');
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');
});
it('fullwidth - insert', () => {
term.write(Array(9).join('0123456789').slice(-80));
@@ -1049,11 +930,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).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');
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');
});
it('fullwidth - right border', () => {
term.write(Array(41).join('¥'));
@@ -1062,14 +943,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).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
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
term.write('b');
expect(term.buffer.lines.get(0).length).eql(term.cols);
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
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
});
});
});
+142 -105
View File
@@ -21,13 +21,13 @@
* http://linux.die.net/man/7/urxvt
*/
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler } from './Types';
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, LineData } from './Types';
import { IMouseZoneManager } from './ui/Types';
import { IRenderer } from './renderer/Types';
import { BufferSet } from './BufferSet';
import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer';
import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR } from './Buffer';
import { CompositionHelper } from './CompositionHelper';
import { EventEmitter } from './common/EventEmitter';
import { EventEmitter } from './EventEmitter';
import { Viewport } from './Viewport';
import { rightClickHandler, moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from './handlers/Clipboard';
import { C0 } from './common/data/EscapeSequences';
@@ -52,7 +52,6 @@ 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;
@@ -73,7 +72,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'];
const CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows', 'rendererType'];
const DEFAULT_OPTIONS: ITerminalOptions = {
cols: 80,
@@ -134,7 +133,9 @@ 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
@@ -206,7 +207,6 @@ 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 options An object containing a set of options, the available options are:
* @param {object} 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 || this.options[key] === undefined) {
if (this.options[key] == null) {
this.options[key] = DEFAULT_OPTIONS[key];
}
});
@@ -270,6 +270,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.cursorState = 0;
this.cursorHidden = false;
this._sendDataQueue = '';
this._customKeyEventHandler = null;
// modes
@@ -347,7 +348,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/**
* Retrieves an option's value from the terminal.
* @param key The option key.
* @param {string} key The option key.
*/
public getOption(key: string): any {
if (!(key in DEFAULT_OPTIONS)) {
@@ -359,8 +360,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/**
* Sets an option on the terminal.
* @param key The option key.
* @param value The option value.
* @param {string} key The option key.
* @param {any} value The option value.
*/
public setOption(key: string, value: any): void {
if (!(key in DEFAULT_OPTIONS)) {
@@ -369,9 +370,6 @@ 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) {
@@ -398,11 +396,6 @@ 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}`);
@@ -462,18 +455,6 @@ 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) {
@@ -503,11 +484,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/**
* Binds the desired focus behavior on a given terminal object.
*/
private _onTextAreaFocus(ev: KeyboardEvent): void {
private _onTextAreaFocus(): void {
if (this.sendFocus) {
this.handler(C0.ESC + '[I');
this.send(C0.ESC + '[I');
}
this.updateCursorStyle(ev);
this.element.classList.add('focus');
this.showCursor();
this.emit('focus');
@@ -530,7 +510,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.handler(C0.ESC + '[O');
this.send(C0.ESC + '[O');
}
this.element.classList.remove('focus');
this.emit('blur');
@@ -622,7 +602,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/**
* Opens the terminal within an element.
*
* @param parent The element to create the terminal within.
* @param {HTMLElement} parent The element to create the terminal within.
*/
public open(parent: HTMLElement): void {
this._parent = parent || this._parent;
@@ -680,7 +660,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', (ev: KeyboardEvent) => this._onTextAreaFocus(ev)));
this.register(addDisposableDomListener(this.textarea, 'focus', () => this._onTextAreaFocus()));
this.register(addDisposableDomListener(this.textarea, 'blur', () => this._onTextAreaBlur()));
this._helperContainer.appendChild(this.textarea);
@@ -694,8 +674,12 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
// Performance: Add viewport and helper elements from the fragment
this.element.appendChild(fragment);
this._setupRenderer();
this._theme = this.options.theme;
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.options.theme = null;
this.viewport = new Viewport(this, this._viewportElement, this._viewportScrollArea, this.charMeasure);
this.viewport.onThemeChanged(this.renderer.colorManager.colors);
@@ -752,21 +736,11 @@ 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 set.
* @param theme The theme to ste.
*/
private _setTheme(theme: ITheme): void {
this._theme = theme;
const colors = this.renderer.setTheme(theme);
if (this.viewport) {
this.viewport.onThemeChanged(colors);
@@ -886,7 +860,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.handler(data);
self.send(data);
return;
}
@@ -899,7 +873,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.handler(C0.ESC + '['
self.send(C0.ESC + '['
+ button
+ ';'
+ (button === 3 ? 4 : 0)
@@ -919,14 +893,14 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
pos.y -= 32;
pos.x++;
pos.y++;
self.handler(C0.ESC + '[' + button + ';' + pos.x + ';' + pos.y + 'M');
self.send(C0.ESC + '[' + button + ';' + pos.x + ';' + pos.y + 'M');
return;
}
if (self.sgrMouse) {
pos.x -= 32;
pos.y -= 32;
self.handler(C0.ESC + '[<'
self.send(C0.ESC + '[<'
+ (((button & 3) === 3 ? button & ~3 : button) - 32)
+ ';'
+ pos.x
@@ -942,7 +916,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
encode(data, pos.x);
encode(data, pos.y);
self.handler(C0.ESC + '[M' + String.fromCharCode.apply(String, data));
self.send(C0.ESC + '[M' + String.fromCharCode.apply(String, data));
}
function getButton(ev: MouseEvent): number {
@@ -961,9 +935,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 && ev.button !== undefined
button = ev.button != null
? +ev.button
: ev.which !== null && ev.which !== undefined
: ev.which != null
? ev.which - 1
: null;
@@ -1091,7 +1065,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
for (let i = 0; i < Math.abs(amount); i++) {
data += sequence;
}
this.handler(data);
this.send(data);
}
return;
}
@@ -1124,8 +1098,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 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).
* @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).
*/
public refresh(start: number, end: number): void {
if (this.renderer) {
@@ -1135,8 +1109,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/**
* Queues linkification for the specified rows.
* @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).
* @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).
*/
private _queueLinkification(start: number, end: number): void {
if (this.linkifier) {
@@ -1170,7 +1144,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 = BufferLine.blankLine(this.cols, DEFAULT_ATTR, isWrapped);
const newLine = this.blankLine(undefined, isWrapped);
const topRow = this.buffer.ybase + this.buffer.scrollTop;
const bottomRow = this.buffer.ybase + this.buffer.scrollBottom;
@@ -1228,8 +1202,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/**
* Scroll the display of the terminal
* @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
* @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
* to avoid unwanted events being handled by the viewport when the event was triggered from the
* viewport originally.
*/
@@ -1260,7 +1234,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/**
* Scroll the display of the terminal by a number of pages.
* @param pageCount The number of pages to scroll (negative scrolls up).
* @param {number} pageCount The number of pages to scroll (negative scrolls up).
*/
public scrollPages(pageCount: number): void {
this.scrollLines(pageCount * (this.rows - 1));
@@ -1289,14 +1263,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/**
* Writes text to the terminal.
* @param data The text to write to the terminal.
* @param {string} 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;
@@ -1310,7 +1279,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.handler(C0.DC3);
this.send(C0.DC3);
this._xoffSentToCatchUp = true;
}
@@ -1325,11 +1294,6 @@ 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();
@@ -1337,7 +1301,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.handler(C0.DC1);
this.send(C0.DC1);
this._xoffSentToCatchUp = false;
}
@@ -1365,7 +1329,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/**
* Writes text to the terminal, followed by a break line character (\n).
* @param data The text to write to the terminal.
* @param {string} data The text to write to the terminal.
*/
public writeln(data: string): void {
this.write(data + '\r\n');
@@ -1410,18 +1374,6 @@ 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;
}
@@ -1478,7 +1430,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 ev The keydown event to be handled.
* @param {KeyboardEvent} ev The keydown event to be handled.
*/
protected _keyDown(event: KeyboardEvent): boolean {
if (this._customKeyEventHandler && this._customKeyEventHandler(event) === false) {
@@ -1575,7 +1527,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 ev The keypress event to be handled.
* @param {KeyboardEvent} ev The keypress event to be handled.
*/
protected _keyPress(ev: KeyboardEvent): boolean {
let key;
@@ -1588,7 +1540,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
if (ev.charCode) {
key = ev.charCode;
} else if (ev.which === null || ev.which === undefined) {
} else if (ev.which == null) {
key = ev.keyCode;
} else if (ev.which !== 0 && ev.charCode !== 0) {
key = ev.which;
@@ -1612,6 +1564,21 @@ 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
@@ -1652,8 +1619,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/**
* Resizes the terminal.
*
* @param x The number of columns to resize to.
* @param y The number of rows to resize to.
* @param {number} x The number of columns to resize to.
* @param {number} y The number of rows to resize to.
*/
public resize(x: number, y: number): void {
if (isNaN(x) || isNaN(y)) {
@@ -1687,7 +1654,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/**
* Updates the range of rows to refresh
* @param y The number of rows to refresh next.
* @param {number} y The number of rows to refresh next.
*/
public updateRange(y: number): void {
if (y < this._refreshStart) this._refreshStart = y;
@@ -1708,6 +1675,41 @@ 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.
*/
@@ -1722,21 +1724,56 @@ 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(BufferLine.blankLine(this.cols, DEFAULT_ATTR));
this.buffer.lines.push(this.blankLine());
}
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(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
return [this.eraseAttr(), ' ', 1, 32 /* ' '.charCodeAt(0) */];
}
return [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
return [DEFAULT_ATTR, ' ', 1, 32 /* ' '.charCodeAt(0) */];
}
/**
@@ -1749,7 +1786,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/**
* Emit the 'data' event and populate the given data.
* @param data The data to populate in the event.
* @param {string} data The data to populate in the event.
*/
public handler(data: string): void {
// Prevents all events to pty process if stdin is disabled
@@ -1771,7 +1808,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/**
* Emit the 'title' event and populate the given title.
* @param title The title to populate in the event.
* @param {string} title The title to populate in the event.
*/
public handleTitle(title: string): void {
/**
@@ -1814,7 +1851,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, BufferLine.blankLine(this.cols, this.eraseAttr()));
this.buffer.lines.set(this.buffer.y + this.buffer.ybase, this.blankLine(true));
this.updateRange(this.buffer.scrollTop);
this.updateRange(this.buffer.scrollBottom);
} else {
@@ -1863,7 +1900,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 && matchColorCache[hash] !== undefined) {
if (matchColorCache[hash] != null) {
return matchColorCache[hash];
}
+24 -41
View File
@@ -7,21 +7,17 @@ import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions,
import { IColorSet, IRenderer } from './renderer/Types';
import { IMouseZoneManager } from './ui/Types';
import { ICharset } from './core/Types';
import { ICircularList } from './common/Types';
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
export type XtermListener = (...args: any[]) => void;
export type CharData = [number, string, number, number];
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][];
// BufferIndex denotes a position in the buffer: [rowIndex, colIndex]
export type BufferIndex = [number, number];
export const enum LinkHoverEventTypes {
HOVER = 'linkhover',
TOOLTIP = 'linktooltip',
@@ -49,7 +45,6 @@ export interface IInputHandlingTerminal extends IEventEmitter {
wraparoundMode: boolean;
bracketedPasteMode: boolean;
curAttr: number;
savedCurAttr: number;
savedCols: number;
x10Mouse: boolean;
vt200Mouse: boolean;
@@ -68,11 +63,17 @@ 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;
@@ -83,10 +84,6 @@ 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 {
@@ -201,7 +198,6 @@ export interface ILinkHoverEvent {
x2: number;
y2: number;
cols: number;
fg: number;
}
export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
@@ -227,10 +223,12 @@ 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 {
@@ -268,18 +266,8 @@ export interface ITerminalOptions extends IPublicTerminalOptions {
useFlowControl?: boolean;
}
export interface IBufferStringIteratorResult {
range: {first: number, last: number};
content: string;
}
export interface IBufferStringIterator {
hasNext(): boolean;
next(): IBufferStringIteratorResult;
}
export interface IBuffer {
readonly lines: ICircularList<IBufferLine>;
readonly lines: ICircularList<LineData>;
ydisp: number;
ybase: number;
y: number;
@@ -295,8 +283,6 @@ export interface IBuffer {
getWrappedRangeForLine(y: number): { first: number, last: number };
nextStop(x?: number): number;
prevStop(x?: number): number;
stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[];
iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator;
}
export interface IBufferSet extends IEventEmitter {
@@ -308,6 +294,19 @@ export interface IBufferSet extends IEventEmitter {
activateAltBuffer(): void;
}
export interface ICircularList<T> extends IEventEmitter {
length: number;
maxLength: number;
get(index: number): T;
set(index: number, value: T): void;
push(value: T): void;
pop(): T;
splice(start: number, deleteCount: number, ...items: T[]): void;
trimStart(count: number): void;
shiftElements(start: number, count: number, offset: number): void;
}
export interface ISelectionManager {
selectionText: string;
selectionStart: [number, number];
@@ -507,19 +506,3 @@ 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;
}
+14 -56
View File
@@ -22,16 +22,12 @@ export class Viewport extends Disposable implements IViewport {
private _lastRecordedViewportHeight: number = 0;
private _lastRecordedBufferHeight: number = 0;
private _lastTouchY: number;
private _lastScrollTop: number = 0;
// Stores a partial line amount when scrolling, this is used to keep track of how much of a line
// is scrolled so we can "scroll" over partial lines and feel natural on touchpads. This is a
// quick fix and could have a more robust solution in place that reset the value when needed.
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.
@@ -66,12 +62,6 @@ 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;
@@ -81,53 +71,30 @@ 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;
}
/**
* Updates dimensions and synchronizes the scroll area if necessary.
*/
public syncScrollArea(): void {
// If buffer height changed
if (this._lastRecordedBufferLength !== this._terminal.buffer.lines.length) {
// If buffer height changed
this._lastRecordedBufferLength = this._terminal.buffer.lines.length;
this._refresh();
return;
} else if (this._lastRecordedViewportHeight !== (<any>this._terminal).renderer.dimensions.canvasHeight) {
// If viewport height changed
this._refresh();
} else {
// If size has changed, refresh viewport
if (this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) {
this._refresh();
}
}
// If viewport height changed
if (this._lastRecordedViewportHeight !== (<any>this._terminal).renderer.dimensions.canvasHeight) {
this._refresh();
return;
}
// If the buffer position doesn't match last scroll top
const newScrollTop = this._terminal.buffer.ydisp * this._currentRowHeight;
if (this._lastScrollTop !== newScrollTop) {
this._refresh();
return;
}
// If element's scroll top changed, this can happen when hiding the element
if (this._lastScrollTop !== this._viewportElement.scrollTop) {
this._refresh();
return;
}
// If row height changed
if (this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) {
this._refresh();
return;
// Sync scrollTop
const scrollTop = this._terminal.buffer.ydisp * this._currentRowHeight;
if (this._viewportElement.scrollTop !== scrollTop) {
this._viewportElement.scrollTop = scrollTop;
}
}
@@ -137,22 +104,13 @@ export class Viewport extends Disposable implements IViewport {
* @param ev The scroll event.
*/
private _onScroll(ev: Event): void {
// Record current scroll top position
this._lastScrollTop = this._viewportElement.scrollTop;
// Don't attempt to scroll if the element is not visible, otherwise scrollTop will be corrupt
// which causes the terminal to scroll the buffer to the top
if (!this._viewportElement.offsetParent) {
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._lastScrollTop / this._currentRowHeight);
const newRow = Math.round(this._viewportElement.scrollTop / this._currentRowHeight);
const diff = newRow - this._terminal.buffer.ydisp;
this._terminal.scrollLines(diff, true);
}
+1 -1
View File
@@ -53,7 +53,7 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean
const fileReader = new FileReader();
fileReader.addEventListener('load', () => {
str = myTextDecoder.decode(fileReader.result);
str = myTextDecoder.decode(this.result);
displayData(str);
});
fileReader.readAsArrayBuffer(ev.data);
-4
View File
@@ -2,10 +2,6 @@
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": [
"dom",
"es6",
],
"rootDir": ".",
"outDir": "../../../lib/addons/attach/",
"sourceMap": true,
-4
View File
@@ -2,10 +2,6 @@
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": [
"dom",
"es6",
],
"rootDir": ".",
"outDir": "../../../lib/addons/fit/",
"sourceMap": true,
-4
View File
@@ -2,10 +2,6 @@
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": [
"dom",
"es6",
],
"rootDir": ".",
"outDir": "../../../lib/addons/fullscreen/",
"sourceMap": true,
+2 -14
View File
@@ -17,18 +17,6 @@ export interface ISearchAddonTerminal extends Terminal {
}
export interface ISearchHelper {
findNext(term: string, searchOptions: ISearchOptions): boolean;
findPrevious(term: string, searchOptions: ISearchOptions): boolean;
}
export interface ISearchOptions {
regex?: boolean;
wholeWord?: boolean;
caseSensitive?: boolean;
}
export interface ISearchResult {
term: string;
col: number;
row: number;
findNext(term: string): boolean;
findPrevious(term: string): boolean;
}
+20 -80
View File
@@ -3,8 +3,13 @@
* @license MIT
*/
import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult } from './Interfaces';
const nonWordCharacters = ' ~!@#$%^&*()_+`-=[]{}|\;:"\',./<>?';
import { ISearchHelper, ISearchAddonTerminal } from './Interfaces';
interface ISearchResult {
term: string;
col: number;
row: number;
}
/**
* A class that knows how to search the terminal and how to display the results.
@@ -14,16 +19,16 @@ export class SearchHelper implements ISearchHelper {
// TODO: Search for multiple instances on 1 line
// TODO: Don't use the actual selection, instead use a "find selection" so multiple instances can be highlighted
// TODO: Highlight other instances in the viewport
// TODO: Support regex, case sensitivity, etc.
}
/**
* Find the next instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing.
* @param term Tne search term.
* @param searchOptions Search options.
* @return Whether a result was found.
*/
public findNext(term: string, searchOptions?: ISearchOptions): boolean {
public findNext(term: string): boolean {
if (!term || term.length === 0) {
return false;
}
@@ -38,7 +43,7 @@ export class SearchHelper implements ISearchHelper {
// Search from ydisp + 1 to end
for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) {
result = this._findInLine(term, y, searchOptions);
result = this._findInLine(term, y);
if (result) {
break;
}
@@ -47,7 +52,7 @@ export class SearchHelper implements ISearchHelper {
// Search from the top to the current ydisp
if (!result) {
for (let y = 0; y < startRow; y++) {
result = this._findInLine(term, y, searchOptions);
result = this._findInLine(term, y);
if (result) {
break;
}
@@ -62,10 +67,9 @@ export class SearchHelper implements ISearchHelper {
* Find the previous instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing.
* @param term Tne search term.
* @param searchOptions Search options.
* @return Whether a result was found.
*/
public findPrevious(term: string, searchOptions?: ISearchOptions): boolean {
public findPrevious(term: string): boolean {
if (!term || term.length === 0) {
return false;
}
@@ -80,7 +84,7 @@ export class SearchHelper implements ISearchHelper {
// Search from ydisp + 1 to end
for (let y = startRow - 1; y >= 0; y--) {
result = this._findInLine(term, y, searchOptions);
result = this._findInLine(term, y);
if (result) {
break;
}
@@ -89,7 +93,7 @@ export class SearchHelper implements ISearchHelper {
// Search from the top to the current ydisp
if (!result) {
for (let y = this._terminal._core.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) {
result = this._findInLine(term, y, searchOptions);
result = this._findInLine(term, y);
if (result) {
break;
}
@@ -101,61 +105,19 @@ export class SearchHelper implements ISearchHelper {
}
/**
* A found substring is a whole word if it doesn't have an alphanumeric character directly adjacent to it.
* @param searchIndex starting indext of the potential whole word substring
* @param line entire string in which the potential whole word was found
* @param term the substring that starts at searchIndex
*/
private _isWholeWord(searchIndex: number, line: string, term: string): boolean {
return (((searchIndex === 0) || (nonWordCharacters.indexOf(line[searchIndex - 1]) !== -1)) &&
(((searchIndex + term.length) === line.length) || (nonWordCharacters.indexOf(line[searchIndex + term.length]) !== -1)));
}
/**
* Searches a line for a search term. Takes the provided terminal line and searches the text line, which may contain
* subsequent terminal lines if the text is wrapped. If the provided line number is part of a wrapped text line that
* started on an earlier line then it is skipped since it will be properly searched when the terminal line that the
* text starts on is searched.
* Searches a line for a search term.
* @param term Tne search term.
* @param y The line to search.
* @param searchOptions Search options.
* @return The search result if it was found.
*/
protected _findInLine(term: string, y: number, searchOptions: ISearchOptions = {}): ISearchResult {
if (this._terminal._core.buffer.lines.get(y).isWrapped) {
return;
}
const stringLine = this.translateBufferLineToStringWithWrap(y, true);
const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();
let searchIndex = -1;
if (searchOptions.regex) {
const searchRegex = RegExp(searchTerm, 'g');
const foundTerm = searchRegex.exec(searchStringLine);
if (foundTerm && foundTerm[0].length > 0) {
searchIndex = searchRegex.lastIndex - foundTerm[0].length;
term = foundTerm[0];
}
} else {
searchIndex = searchStringLine.indexOf(searchTerm);
}
private _findInLine(term: string, y: number): ISearchResult {
const lowerStringLine = this._terminal._core.buffer.translateBufferLineToString(y, true).toLowerCase();
const lowerTerm = term.toLowerCase();
let searchIndex = lowerStringLine.indexOf(lowerTerm);
if (searchIndex >= 0) {
// Adjust the row number and search index if needed since a "line" of text can span multiple rows
if (searchIndex >= this._terminal.cols) {
y += Math.floor(searchIndex / this._terminal.cols);
searchIndex = searchIndex % this._terminal.cols;
}
if (searchOptions.wholeWord && !this._isWholeWord(searchIndex, searchStringLine, term)) {
return;
}
const line = this._terminal._core.buffer.lines.get(y);
for (let i = 0; i < searchIndex; i++) {
const charData = line.get(i);
const charData = line[i];
// Adjust the searchIndex to normalize emoji into single chars
const char = charData[1/*CHAR_DATA_CHAR_INDEX*/];
if (char.length > 1) {
@@ -176,28 +138,6 @@ export class SearchHelper implements ISearchHelper {
}
}
/**
* Translates a buffer line to a string, including subsequent lines if they are wraps.
* Wide characters will count as two columns in the resulting string. This
* function is useful for getting the actual text underneath the raw selection
* position.
* @param line The line being translated.
* @param trimRight Whether to trim whitespace to the right.
*/
public translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): string {
let lineString = '';
let lineWrapsToNext: boolean;
do {
const nextLine = this._terminal._core.buffer.lines.get(lineIndex + 1);
lineWrapsToNext = nextLine ? nextLine.isWrapped : false;
lineString += this._terminal._core.buffer.translateBufferLineToString(lineIndex, !lineWrapsToNext && trimRight);
lineIndex++;
} while (lineWrapsToNext);
return lineString;
}
/**
* Selects and scrolls to a result.
* @param result The result to select.
-248
View File
@@ -1,248 +0,0 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert, expect } from 'chai';
import * as search from './search';
import { SearchHelper } from './SearchHelper';
import { ISearchOptions, ISearchResult } from './Interfaces';
class MockTerminalPlain {}
class MockTerminal {
private _core: any;
public searchHelper: TestSearchHelper;
public cols: number;
constructor(options: any) {
this._core = new (require('../../../lib/Terminal').Terminal)(options);
this.searchHelper = new TestSearchHelper(this as any);
this.cols = options.cols;
}
get core(): any {
return this._core;
}
pushWriteData(): void {
this._core._innerWrite();
}
}
class TestSearchHelper extends SearchHelper {
public findInLine(term: string, y: number, searchOptions?: ISearchOptions): ISearchResult {
return this._findInLine(term, y, searchOptions);
}
}
describe('search addon', () => {
describe('apply', () => {
it('should register findNext and findPrevious', () => {
search.apply(<any>MockTerminalPlain);
assert.equal(typeof (<any>MockTerminalPlain).prototype.findNext, 'function');
assert.equal(typeof (<any>MockTerminalPlain).prototype.findPrevious, 'function');
});
});
describe('find', () => {
it('Searchhelper - should find correct position', () => {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 20, rows: 3});
term.core.write('Hello World\r\ntest\n123....hello');
term.pushWriteData();
const hello0 = term.searchHelper.findInLine('Hello', 0);
const hello1 = term.searchHelper.findInLine('Hello', 1);
const hello2 = term.searchHelper.findInLine('Hello', 2);
expect(hello0).eql({col: 0, row: 0, term: 'Hello'});
expect(hello1).eql(undefined);
expect(hello2).eql({col: 11, row: 2, term: 'Hello'});
});
it('should find search term accross line wrap', () => {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 10, rows: 5});
term.core.write('texttextHellotext\r\n');
term.core.write('texttexttextHellotext goodbye');
term.pushWriteData();
/*
texttextHe
llotext
texttextte
xtHellotex
t (these spaces included intentionally)
goodbye
*/
const hello0 = term.searchHelper.findInLine('Hello', 0);
const hello1 = term.searchHelper.findInLine('Hello', 1);
const hello2 = term.searchHelper.findInLine('Hello', 2);
const hello3 = term.searchHelper.findInLine('Hello', 3);
const llo = term.searchHelper.findInLine('llo', 1);
const goodbye = term.searchHelper.findInLine('goodbye', 2);
expect(hello0).eql({col: 8, row: 0, term: 'Hello'});
expect(hello1).eql(undefined);
expect(hello2).eql({col: 2, row: 3, term: 'Hello'});
expect(hello3).eql(undefined);
expect(llo).eql(undefined);
expect(goodbye).eql({col: 0, row: 5, term: 'goodbye'});
});
it('should respect search regex', () => {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 10, rows: 4});
term.core.write('abcdefghijklmnopqrstuvwxyz\r\n~/dev ');
/*
abcdefghij
klmnopqrst
uvwxyz
~/dev
*/
term.pushWriteData();
const searchOptions = {
regex: true,
wholeWord: false,
caseSensitive: false
};
const hello0 = term.searchHelper.findInLine('dee*', 0, searchOptions);
const hello1 = term.searchHelper.findInLine('jkk*', 0, searchOptions);
const hello2 = term.searchHelper.findInLine('mnn*', 1, searchOptions);
const tilda0 = term.searchHelper.findInLine('^~', 3, searchOptions);
const tilda1 = term.searchHelper.findInLine('^[~]', 3, searchOptions);
const tilda2 = term.searchHelper.findInLine('^\\~', 3, searchOptions);
expect(hello0).eql({col: 3, row: 0, term: 'de'});
expect(hello1).eql({col: 9, row: 0, term: 'jk'});
expect(hello2).eql(undefined);
expect(tilda0).eql({col: 0, row: 3, term: '~'});
expect(tilda1).eql({col: 0, row: 3, term: '~'});
expect(tilda2).eql({col: 0, row: 3, term: '~'});
});
it('should not select empty lines', () => {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 20, rows: 3});
term.core.write(' ');
term.pushWriteData();
const line = term.searchHelper.findInLine('^.*$', 0, { regex: true });
expect(line).eql(undefined);
});
it('should respect case sensitive', function(): void {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 20, rows: 4});
term.core.write('Hello World\r\n123....hello\r\nmoreTestHello');
term.pushWriteData();
const searchOptions = {
regex: false,
wholeWord: false,
caseSensitive: true
};
const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions);
const hello1 = term.searchHelper.findInLine('Hello', 1, searchOptions);
const hello2 = term.searchHelper.findInLine('Hello', 2, searchOptions);
expect(hello0).eql({col: 0, row: 0, term: 'Hello'});
expect(hello1).eql(undefined);
expect(hello2).eql({col: 8, row: 2, term: 'Hello'});
});
it('should respect case sensitive + regex', function(): void {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 20, rows: 4});
term.core.write('hellohello\r\nHelloHello');
term.pushWriteData();
/**
* hellohello
* HelloHello
*/
const searchOptions = {
regex: true,
wholeWord: false,
caseSensitive: true
};
const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions);
const hello1 = term.searchHelper.findInLine('Hello$', 0, searchOptions);
const hello2 = term.searchHelper.findInLine('Hello', 1, searchOptions);
const hello3 = term.searchHelper.findInLine('Hello$', 1, searchOptions);
expect(hello0).eql(undefined);
expect(hello1).eql(undefined);
expect(hello2).eql({col: 0, row: 1, term: 'Hello'});
expect(hello3).eql({col: 5, row: 1, term: 'Hello'});
});
it('should respect whole-word search option', function(): void {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 20, rows: 5});
term.core.write('Hello World\r\nWorld Hello\r\nWorldHelloWorld\r\nHelloWorld\r\nWorldHello');
term.pushWriteData();
const searchOptions = {
regex: false,
wholeWord: true,
caseSensitive: false
};
const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions);
const hello1 = term.searchHelper.findInLine('Hello', 1, searchOptions);
const hello2 = term.searchHelper.findInLine('Hello', 2, searchOptions);
const hello3 = term.searchHelper.findInLine('Hello', 3, searchOptions);
const hello4 = term.searchHelper.findInLine('Hello', 4, searchOptions);
expect(hello0).eql({col: 0, row: 0, term: 'Hello'});
expect(hello1).eql({col: 6, row: 1, term: 'Hello'});
expect(hello2).eql(undefined);
expect(hello3).eql(undefined);
expect(hello4).eql(undefined);
});
it('should respect whole-word + case sensitive search options', function(): void {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 20, rows: 5});
term.core.write('Hello World\r\nHelloWorld');
term.pushWriteData();
const searchOptions = {
regex: false,
wholeWord: true,
caseSensitive: true
};
const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions);
const hello1 = term.searchHelper.findInLine('hello', 0, searchOptions);
const hello2 = term.searchHelper.findInLine('Hello', 1, searchOptions);
const hello3 = term.searchHelper.findInLine('hello', 1, searchOptions);
expect(hello0).eql({col: 0, row: 0, term: 'Hello'});
expect(hello1).eql(undefined);
expect(hello2).eql(undefined);
expect(hello3).eql(undefined);
});
it('should respect whole-word + regex search options', function(): void {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 20, rows: 5});
term.core.write('Hello World Hello\r\nHelloWorldHello');
term.pushWriteData();
const searchOptions = {
regex: true,
wholeWord: true,
caseSensitive: false
};
const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions);
const hello1 = term.searchHelper.findInLine('Hello$', 0, searchOptions);
const hello2 = term.searchHelper.findInLine('Hello', 1, searchOptions);
const hello3 = term.searchHelper.findInLine('Hello$', 1, searchOptions);
expect(hello0).eql({col: 0, row: 0, term: 'hello'});
expect(hello1).eql({col: 12, row: 0, term: 'hello'});
expect(hello2).eql(undefined);
expect(hello3).eql(undefined);
});
it('should respect all search options', function(): void {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 20, rows: 5});
term.core.write('Hello World Hello\r\nHelloWorldHello');
term.pushWriteData();
const searchOptions = {
regex: true,
wholeWord: true,
caseSensitive: true
};
const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions);
const hello1 = term.searchHelper.findInLine('Hello$', 0, searchOptions);
const hello2 = term.searchHelper.findInLine('hello', 0, searchOptions);
const hello3 = term.searchHelper.findInLine('hello$', 0, searchOptions);
const hello4 = term.searchHelper.findInLine('hello', 1, searchOptions);
const hello5 = term.searchHelper.findInLine('hello$', 1, searchOptions);
expect(hello0).eql({col: 0, row: 0, term: 'Hello'});
expect(hello1).eql({col: 12, row: 0, term: 'Hello'});
expect(hello2).eql(undefined);
expect(hello3).eql(undefined);
expect(hello4).eql(undefined);
expect(hello5).eql(undefined);
});
});
});
+9 -11
View File
@@ -5,44 +5,42 @@
import { SearchHelper } from './SearchHelper';
import { Terminal } from 'xterm';
import { ISearchAddonTerminal, ISearchOptions } from './Interfaces';
import { ISearchAddonTerminal } from './Interfaces';
/**
* Find the next instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing.
* @param term Tne search term.
* @param searchOptions Search options
* @return Whether a result was found.
*/
export function findNext(terminal: Terminal, term: string, searchOptions: ISearchOptions = {}): boolean {
export function findNext(terminal: Terminal, term: string): boolean {
const addonTerminal = <ISearchAddonTerminal>terminal;
if (!addonTerminal.__searchHelper) {
addonTerminal.__searchHelper = new SearchHelper(addonTerminal);
}
return addonTerminal.__searchHelper.findNext(term, searchOptions);
return addonTerminal.__searchHelper.findNext(term);
}
/**
* Find the previous instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing.
* @param term Tne search term.
* @param searchOptions Search options
* @return Whether a result was found.
*/
export function findPrevious(terminal: Terminal, term: string, searchOptions: ISearchOptions): boolean {
export function findPrevious(terminal: Terminal, term: string): boolean {
const addonTerminal = <ISearchAddonTerminal>terminal;
if (!addonTerminal.__searchHelper) {
addonTerminal.__searchHelper = new SearchHelper(addonTerminal);
}
return addonTerminal.__searchHelper.findPrevious(term, searchOptions);
return addonTerminal.__searchHelper.findPrevious(term);
}
export function apply(terminalConstructor: typeof Terminal): void {
(<any>terminalConstructor.prototype).findNext = function(term: string, searchOptions: ISearchOptions): boolean {
return findNext(this, term, searchOptions);
(<any>terminalConstructor.prototype).findNext = function(term: string): boolean {
return findNext(this, term);
};
(<any>terminalConstructor.prototype).findPrevious = function(term: string, searchOptions: ISearchOptions): boolean {
return findPrevious(this, term, searchOptions);
(<any>terminalConstructor.prototype).findPrevious = function(term: string): boolean {
return findPrevious(this, term);
};
}
-4
View File
@@ -2,10 +2,6 @@
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": [
"dom",
"es6",
],
"rootDir": ".",
"outDir": "../../../lib/addons/search/",
"sourceMap": true,
-4
View File
@@ -2,10 +2,6 @@
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": [
"dom",
"es6",
],
"rootDir": ".",
"outDir": "../../../lib/addons/terminado/",
"sourceMap": true,
-4
View File
@@ -2,10 +2,6 @@
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": [
"dom",
"es6",
],
"rootDir": ".",
"outDir": "../../../lib/addons/webLinks/",
"sourceMap": true,
-4
View File
@@ -2,10 +2,6 @@
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": [
"dom",
"es6",
],
"rootDir": ".",
"outDir": "../../../lib/addons/winptyCompat/",
"sourceMap": true,
+3 -6
View File
@@ -6,9 +6,6 @@
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;
@@ -30,11 +27,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.get(addonTerminal.cols - 1);
const lastChar = line[addonTerminal.cols - 1];
if (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE) {
if (lastChar[3] !== 32 /* ' ' */) {
const nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y);
nextLine.isWrapped = true;
(<any>nextLine).isWrapped = true;
}
});
}
-4
View File
@@ -2,10 +2,6 @@
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": [
"dom",
"es6",
],
"rootDir": ".",
"outDir": "../../../lib/addons/zmodem/",
"sourceMap": true,
+7 -7
View File
@@ -3,15 +3,15 @@
* @license MIT
*/
import { EventEmitter } from './EventEmitter';
import { ICircularList } from './Types';
import { EventEmitter } from '../EventEmitter';
import { ICircularList } from '../Types';
/**
* Represents a circular list; a list with a maximum size that wraps around when push is called,
* overriding values at the start of the list.
*/
export class CircularList<T> extends EventEmitter implements ICircularList<T> {
protected _array: (T | undefined)[];
protected _array: T[];
private _startIndex: number;
private _length: number;
@@ -36,7 +36,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
// Reconstruct array, starting at index 0. Only transfer values from the
// indexes 0 to length.
const newArray = new Array<T | undefined>(newMaxLength);
const newArray = new Array<T>(newMaxLength);
for (let i = 0; i < Math.min(newMaxLength, this.length); i++) {
newArray[i] = this._array[this._getCyclicIndex(i)];
}
@@ -66,7 +66,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
* @param index The index of the value to get.
* @return The value corresponding to the index.
*/
public get(index: number): T | undefined {
public get(index: number): T {
return this._array[this._getCyclicIndex(index)];
}
@@ -78,7 +78,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
* @param index The index to set.
* @param value The value to set.
*/
public set(index: number, value: T | undefined): void {
public set(index: number, value: T): void {
this._array[this._getCyclicIndex(index)] = value;
}
@@ -104,7 +104,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
* Removes and returns the last value on the list.
* @return The popped value.
*/
public pop(): T | undefined {
public pop(): T {
return this._array[this._getCyclicIndex(this._length-- - 1)];
}
-45
View File
@@ -1,45 +0,0 @@
/**
* 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);
});
});
});
-14
View File
@@ -11,7 +11,6 @@ import { IDisposable } from 'xterm';
*/
export abstract class Disposable implements IDisposable {
protected _disposables: IDisposable[] = [];
protected _isDisposed: boolean = false;
constructor() {
}
@@ -20,7 +19,6 @@ 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;
}
@@ -32,16 +30,4 @@ 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);
}
}
}
-17
View File
@@ -3,10 +3,6 @@
* @license MIT
*/
import { IEventEmitter } from 'xterm';
export type XtermListener = (...args: any[]) => void;
/**
* A keyboard event interface which does not depend on the DOM, KeyboardEvent implicitly extends
* this event.
@@ -20,16 +16,3 @@ export interface IKeyboardEvent {
key: string;
type: string;
}
export interface ICircularList<T> extends IEventEmitter {
length: number;
maxLength: number;
get(index: number): T | undefined;
set(index: number, value: T): void;
push(value: T): void;
pop(): T | undefined;
splice(start: number, deleteCount: number, ...items: T[]): void;
trimStart(count: number): void;
shiftElements(start: number, count: number, offset: number): void;
}
+6 -7
View File
@@ -3,8 +3,7 @@
* @license MIT
*/
import { ITerminal, IBufferLine } from '../Types';
import { ICircularList } from '../common/Types';
import { ITerminal, ICircularList, LineData } from '../Types';
import { C0 } from '../common/data/EscapeSequences';
const enum Direction {
@@ -19,7 +18,7 @@ export class AltClickHandler {
private _startCol: number;
private _endRow: number;
private _endCol: number;
private _lines: ICircularList<IBufferLine>;
private _lines: ICircularList<LineData>;
constructor(
private _mouseEvent: MouseEvent,
@@ -51,7 +50,7 @@ export class AltClickHandler {
*/
public move(): void {
if (this._mouseEvent.altKey && this._endCol !== undefined && this._endRow !== undefined) {
this._terminal.handler(this._arrowSequences());
this._terminal.send(this._arrowSequences());
}
}
@@ -139,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 (this._lines.get(startRow + (direction * i)).isWrapped) {
if ((<any>this._lines.get(startRow + (direction * i))).isWrapped) {
wrappedRows++;
}
}
@@ -153,12 +152,12 @@ export class AltClickHandler {
*/
private _wrappedRowsForRow(currentRow: number): number {
let rowCount = 0;
let lineWraps = this._lines.get(currentRow).isWrapped;
let lineWraps = (<any>this._lines.get(currentRow)).isWrapped;
while (lineWraps && currentRow >= 0 && currentRow < this._terminal.rows) {
rowCount++;
currentRow--;
lineWraps = this._lines.get(currentRow).isWrapped;
lineWraps = (<any>this._lines.get(currentRow)).isWrapped;
}
return rowCount;
+3 -3
View File
@@ -35,7 +35,7 @@ export function bracketTextForPaste(text: string, bracketedPasteMode: boolean):
/**
* Binds copy functionality to the given terminal.
* @param ev The original copy event to be handled
* @param {ClipboardEvent} 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 ev The original paste event to be handled
* @param term The terminal on which to apply the handled paste event
* @param {ClipboardEvent} ev The original paste event to be handled
* @param {Terminal} term The terminal on which to apply the handled paste event
*/
export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void {
ev.stopPropagation();
-6
View File
@@ -65,12 +65,6 @@ 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);
}
+15 -43
View File
@@ -5,7 +5,7 @@
import { IRenderLayer, IColorSet, IRenderDimensions } from './Types';
import { CharData, ITerminal } from '../Types';
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, IGlyphIdentifier } from './atlas/Types';
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from './atlas/Types';
import BaseCharAtlas from './atlas/BaseCharAtlas';
import { acquireCharAtlas } from './atlas/CharAtlasCache';
import { CHAR_DATA_CHAR_INDEX } from '../Buffer';
@@ -22,19 +22,6 @@ export abstract class BaseRenderLayer implements IRenderLayer {
protected _charAtlas: BaseCharAtlas;
/**
* An object that's reused when drawing glyphs in order to reduce GC.
*/
private _currentGlyphIdentifier: IGlyphIdentifier = {
chars: '',
code: 0,
bg: 0,
fg: 0,
bold: false,
dim: false,
italic: false
};
constructor(
private _container: HTMLElement,
id: string,
@@ -49,13 +36,6 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this._container.appendChild(this._canvas);
}
public dispose(): void {
this._container.removeChild(this._canvas);
if (this._charAtlas) {
this._charAtlas.dispose();
}
}
private _initCanvas(): void {
this._ctx = this._canvas.getContext('2d', {alpha: this._alpha});
// Draw the background if this is an opaque layer
@@ -244,12 +224,12 @@ export abstract class BaseRenderLayer implements IRenderLayer {
}
/**
* Draws one or more characters at a cell. If possible this will draw using
* the character atlas to reduce draw time.
* Draws a character at a cell. If possible this will draw using the character
* atlas to reduce draw time.
* @param terminal The terminal.
* @param chars The character or characters.
* @param char The character.
* @param code The character code.
* @param width The width of the characters.
* @param width The width of the character.
* @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.
@@ -257,41 +237,33 @@ 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 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;
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;
fg += drawInBrightColor ? 8 : 0;
this._currentGlyphIdentifier.chars = chars;
this._currentGlyphIdentifier.code = code;
this._currentGlyphIdentifier.bg = bg;
this._currentGlyphIdentifier.fg = fg;
this._currentGlyphIdentifier.bold = bold && terminal.options.enableBold;
this._currentGlyphIdentifier.dim = dim;
this._currentGlyphIdentifier.italic = italic;
const atlasDidDraw = this._charAtlas && this._charAtlas.draw(
this._ctx,
this._currentGlyphIdentifier,
{char, code, bg, fg, bold: bold && terminal.options.enableBold, dim, italic},
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop
);
if (!atlasDidDraw) {
this._drawUncachedChars(terminal, chars, width, fg, x, y, bold && terminal.options.enableBold, dim, italic);
this._drawUncachedChar(terminal, char, width, fg, x, y, bold && terminal.options.enableBold, dim, italic);
}
}
/**
* 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.
* 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.
* @param terminal The terminal.
* @param chars The character.
* @param char 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 _drawUncachedChars(terminal: ITerminal, chars: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean, italic: boolean): void {
private _drawUncachedChar(terminal: ITerminal, char: 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';
@@ -313,7 +285,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
}
// Draw the character
this._ctx.fillText(
chars,
char,
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop);
this._ctx.restore();
@@ -1,290 +0,0 @@
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;
};
}
-276
View File
@@ -1,276 +0,0 @@
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;
}
}
+1 -1
View File
@@ -127,7 +127,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
return;
}
const charData = terminal.buffer.lines.get(cursorY).get(terminal.buffer.x);
const charData = terminal.buffer.lines.get(cursorY)[terminal.buffer.x];
if (!charData) {
return;
}
+1 -10
View File
@@ -6,7 +6,6 @@
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;
@@ -40,15 +39,7 @@ export class LinkRenderLayer extends BaseRenderLayer {
}
private _onLinkHover(e: ILinkHoverEvent): void {
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;
}
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);
+8 -24
View File
@@ -7,14 +7,13 @@ import { TextRenderLayer } from './TextRenderLayer';
import { SelectionRenderLayer } from './SelectionRenderLayer';
import { CursorRenderLayer } from './CursorRenderLayer';
import { ColorManager } from './ColorManager';
import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions, ICharacterJoinerRegistry } from './Types';
import { ITerminal, CharacterJoinerHandler } from '../Types';
import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Types';
import { ITerminal } from '../Types';
import { LinkRenderLayer } from './LinkRenderLayer';
import { EventEmitter } from '../common/EventEmitter';
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;
@@ -24,7 +23,6 @@ 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;
@@ -33,13 +31,12 @@ 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, this._characterJoinerRegistry, allowTransparency),
new TextRenderLayer(this._terminal.screenElement, 0, this.colorManager.colors, 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)
@@ -70,17 +67,12 @@ 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) {
@@ -179,8 +171,8 @@ export class Renderer extends EventEmitter implements IRenderer {
/**
* Queues a refresh between two rows (inclusive), to be done on next animation
* frame.
* @param start The start row.
* @param end The end row.
* @param {number} start The start row.
* @param {number} end The end row.
*/
public refreshRows(start: number, end: number): void {
if (this._isPaused) {
@@ -196,7 +188,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});
}
/**
@@ -258,12 +250,4 @@ 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);
}
}
+1 -1
View File
@@ -72,7 +72,7 @@ export class SelectionRenderLayer extends BaseRenderLayer {
this._ctx.fillStyle = this._colors.selection.css;
if (columnSelectMode) {
const startCol = start[0];
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
const width = end[0] - startCol;
const height = viewportCappedEndRow - viewportCappedStartRow + 1;
this.fillCells(startCol, viewportCappedStartRow, width, height);
+17 -63
View File
@@ -3,8 +3,8 @@
* @license MIT
*/
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 { 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 { CharData, ITerminal } from '../Types';
import { INVERTED_DEFAULT_COLOR } from './atlas/Types';
import { GridCache } from './GridCache';
@@ -22,12 +22,10 @@ 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, characterJoinerRegistry: ICharacterJoinerRegistry, alpha: boolean) {
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, alpha: boolean) {
super(container, 'text', zIndex, alpha, colors);
this._state = new GridCache<CharData>();
this._characterJoinerRegistry = characterJoinerRegistry;
}
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
@@ -54,10 +52,9 @@ export class TextRenderLayer extends BaseRenderLayer {
terminal: ITerminal,
firstRow: number,
lastRow: number,
joinerRegistry: ICharacterJoinerRegistry | null,
callback: (
code: number,
chars: string,
char: string,
width: number,
x: number,
y: number,
@@ -69,54 +66,22 @@ 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.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 charData = line[x];
const code: number = <number>charData[CHAR_DATA_CODE_INDEX];
const char: 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;
}
// 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 an overlapping char and the character to the right is a
// space, take ownership of the cell to the right.
if (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
@@ -124,7 +89,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 (lastCharX < line.length - 1 && line.get(lastCharX + 1)[CHAR_DATA_CODE_INDEX] === NULL_CELL_CODE) {
if (x < line.length - 1 && line[x + 1][CHAR_DATA_CODE_INDEX] === 32 /*' '*/) {
width = 2;
// this._clearChar(x + 1, y);
// The overlapping char's char data will force a clear and render when the
@@ -151,18 +116,7 @@ export class TextRenderLayer extends BaseRenderLayer {
}
}
callback(
code,
chars,
width,
x,
y,
fg,
bg,
flags
);
x = lastCharX;
callback(code, char, width, x, y, fg, bg, flags);
}
}
}
@@ -180,7 +134,7 @@ export class TextRenderLayer extends BaseRenderLayer {
ctx.save();
this._forEachCell(terminal, firstRow, lastRow, null, (code, chars, width, x, y, fg, bg, flags) => {
this._forEachCell(terminal, firstRow, lastRow, (code, char, 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
@@ -222,7 +176,7 @@ export class TextRenderLayer extends BaseRenderLayer {
}
private _drawForeground(terminal: ITerminal, firstRow: number, lastRow: number): void {
this._forEachCell(terminal, firstRow, lastRow, this._characterJoinerRegistry, (code, chars, width, x, y, fg, bg, flags) => {
this._forEachCell(terminal, firstRow, lastRow, (code, char, width, x, y, fg, bg, flags) => {
if (flags & FLAGS.INVISIBLE) {
return;
}
@@ -236,11 +190,11 @@ export class TextRenderLayer extends BaseRenderLayer {
} else {
this._ctx.fillStyle = this._colors.foreground.css;
}
this.fillBottomLineAtCells(x, y, width);
this.fillBottomLineAtCells(x, y);
this._ctx.restore();
}
this.drawChars(
terminal, chars, code,
this.drawChar(
terminal, char, code,
width, x, y,
fg, bg,
!!(flags & FLAGS.BOLD), !!(flags & FLAGS.DIM), !!(flags & FLAGS.ITALIC)
+2 -25
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { ITerminal, CharacterJoinerHandler } from '../Types';
import { ITerminal } from '../Types';
import { IEventEmitter, ITheme, IDisposable } from 'xterm';
import { IColorSet } from '../shared/Types';
@@ -40,8 +40,6 @@ 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 {
@@ -66,7 +64,7 @@ export interface IRenderDimensions {
actualCellHeight: number;
}
export interface IRenderLayer extends IDisposable {
export interface IRenderLayer {
/**
* Called when the terminal loses focus.
*/
@@ -103,16 +101,6 @@ export interface IRenderLayer extends IDisposable {
*/
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.
*/
@@ -123,14 +111,3 @@ export interface IRenderLayer extends IDisposable {
*/
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][];
}
+1 -4
View File
@@ -4,13 +4,10 @@
*/
import { IGlyphIdentifier } from './Types';
import { IDisposable } from 'xterm';
export default abstract class BaseCharAtlas implements IDisposable {
export default abstract class BaseCharAtlas {
private _didWarmUp: boolean = false;
public dispose(): void { }
/**
* Perform any work needed to warm the cache before it can be used. May be called multiple times.
* Implement _doWarmUp instead if you only want to get called once.
+15 -99
View File
@@ -10,7 +10,6 @@ import BaseCharAtlas from './BaseCharAtlas';
import { DEFAULT_ANSI_COLORS } from '../ColorManager';
import { clearColor } from '../../shared/atlas/CharAtlasGenerator';
import LRUMap from './LRUMap';
import { isFirefox, isSafari } from '../../shared/utils/Browser';
// In practice we're probably never going to exhaust a texture this large. For debugging purposes,
// however, it can be useful to set this to a really tiny value, to verify that LRU eviction works.
@@ -30,29 +29,14 @@ const TRANSPARENT_COLOR = {
// cache.
const FRAME_CACHE_DRAW_LIMIT = 100;
/**
* The number of milliseconds to wait before generating the ImageBitmap, this is to debounce/batch
* the operation as window.createImageBitmap is asynchronous.
*/
const GLYPH_BITMAP_COMMIT_DELAY = 100;
interface IGlyphCacheValue {
index: number;
isEmpty: boolean;
inBitmap: boolean;
}
function getGlyphCacheKey(glyph: IGlyphIdentifier): number {
// Note that this only returns a valid key when code < 256
// Layout:
// 0b00000000000000000000000000000001: italic (1)
// 0b00000000000000000000000000000010: dim (1)
// 0b00000000000000000000000000000100: bold (1)
// 0b00000000000000000000111111111000: fg (9)
// 0b00000000000111111111000000000000: bg (9)
// 0b00011111111000000000000000000000: code (8)
// 0b11100000000000000000000000000000: unused (3)
return glyph.code << 21 | glyph.bg << 12 | glyph.fg << 3 | (glyph.bold ? 0 : 4) + (glyph.dim ? 0 : 2) + (glyph.italic ? 0 : 1);
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}`;
}
export default class DynamicCharAtlas extends BaseCharAtlas {
@@ -73,15 +57,6 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
private _drawToCacheCount: number = 0;
// An array of glyph keys that are waiting on the bitmap to be generated.
private _glyphsWaitingOnBitmap: IGlyphCacheValue[] = [];
// The timeout that is used to batch bitmap generation so it's not requested for every new glyph.
private _bitmapCommitTimeout: number | null = null;
// The bitmap to draw from, this is much faster on other browsers than others.
private _bitmap: ImageBitmap | null = null;
constructor(document: Document, private _config: ICharAtlasConfig) {
super();
this._cacheCanvas = document.createElement('canvas');
@@ -107,13 +82,6 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
// document.body.appendChild(this._cacheCanvas);
}
public dispose(): void {
if (this._bitmapCommitTimeout !== null) {
window.clearTimeout(this._bitmapCommitTimeout);
this._bitmapCommitTimeout = null;
}
}
public beginFrame(): void {
this._drawToCacheCount = 0;
}
@@ -124,14 +92,9 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
x: number,
y: number
): boolean {
// Space is always an empty cell, special case this as it's so common
if (glyph.code === 32) {
return true;
}
const glyphKey = getGlyphCacheKey(glyph);
const cacheValue = this._cacheMap.get(glyphKey);
if (cacheValue !== null && cacheValue !== undefined) {
if (cacheValue != null) {
this._drawFromCache(ctx, cacheValue, x, y);
return true;
} else if (this._canCache(glyph) && this._drawToCacheCount < FRAME_CACHE_DRAW_LIMIT) {
@@ -161,12 +124,11 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
return glyph.code < 256;
}
private _toCoordinateX(index: number): number {
return (index % this._width) * this._config.scaledCharWidth;
}
private _toCoordinateY(index: number): number {
return Math.floor(index / this._width) * this._config.scaledCharHeight;
private _toCoordinates(index: number): [number, number] {
return [
(index % this._width) * this._config.scaledCharWidth,
Math.floor(index / this._width) * this._config.scaledCharHeight
];
}
private _drawFromCache(
@@ -179,10 +141,9 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
if (cacheValue.isEmpty) {
return;
}
const cacheX = this._toCoordinateX(cacheValue.index);
const cacheY = this._toCoordinateY(cacheValue.index);
const [cacheX, cacheY] = this._toCoordinates(cacheValue.index);
ctx.drawImage(
cacheValue.inBitmap ? this._bitmap : this._cacheCanvas,
this._cacheCanvas,
cacheX,
cacheY,
this._config.scaledCharWidth,
@@ -255,7 +216,7 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
this._tmpCtx.globalAlpha = DIM_OPACITY;
}
// Draw the character
this._tmpCtx.fillText(glyph.chars, 0, 0);
this._tmpCtx.fillText(glyph.char, 0, 0);
this._tmpCtx.restore();
// clear the background from the character to avoid issues with drawing over the previous
@@ -269,58 +230,13 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
}
// copy the data from imageData to _cacheCanvas
const x = this._toCoordinateX(index);
const y = this._toCoordinateY(index);
const [x, y] = this._toCoordinates(index);
// putImageData doesn't do any blending, so it will overwrite any existing cache entry for us
this._cacheCtx.putImageData(imageData, x, y);
// Add the glyph and queue it to the bitmap (if the browser supports it)
const cacheValue = {
return {
index,
isEmpty,
inBitmap: false
isEmpty
};
this._addGlyphToBitmap(cacheValue);
return cacheValue;
}
private _addGlyphToBitmap(cacheValue: IGlyphCacheValue): void {
// Support is patchy for createImageBitmap at the moment, pass a canvas back
// if support is lacking as drawImage works there too. Firefox is also
// included here as ImageBitmap appears both buggy and has horrible
// performance (tested on v55).
if (!('createImageBitmap' in window) || isFirefox || isSafari) {
return;
}
// Add the glyph to the queue
this._glyphsWaitingOnBitmap.push(cacheValue);
// Check if bitmap generation timeout already exists
if (this._bitmapCommitTimeout !== null) {
return;
}
this._bitmapCommitTimeout = window.setTimeout(() => this._generateBitmap(), GLYPH_BITMAP_COMMIT_DELAY);
}
private _generateBitmap(): void {
const glyphsMovingToBitmap = this._glyphsWaitingOnBitmap;
this._glyphsWaitingOnBitmap = [];
window.createImageBitmap(this._cacheCanvas).then(bitmap => {
// Set bitmap
this._bitmap = bitmap;
// Mark all new glyphs as in bitmap, excluding glyphs that came in after
// the bitmap was requested
for (let i = 0; i < glyphsMovingToBitmap.length; i++) {
const value = glyphsMovingToBitmap[i];
// It doesn't matter if the value was already evicted, it will be
// released from memory after this block if so.
value.inBitmap = true;
}
});
this._bitmapCommitTimeout = null;
}
}
+28 -28
View File
@@ -9,57 +9,57 @@ import LRUMap from './LRUMap';
describe('LRUMap', () => {
it('can be used to store and retrieve values', () => {
const map = new LRUMap(10);
map.set(1, 'valuea');
map.set(2, 'valueb');
map.set(3, 'valuec');
assert.strictEqual(map.get(1), 'valuea');
assert.strictEqual(map.get(2), 'valueb');
assert.strictEqual(map.get(3), 'valuec');
map.set('keya', 'valuea');
map.set('keyb', 'valueb');
map.set('keyc', 'valuec');
assert.strictEqual(map.get('keya'), 'valuea');
assert.strictEqual(map.get('keyb'), 'valueb');
assert.strictEqual(map.get('keyc'), 'valuec');
});
it('maintains a size from insertions', () => {
const map = new LRUMap(10);
assert.strictEqual(map.size, 0);
map.set(1, 'value');
map.set('a', 'value');
assert.strictEqual(map.size, 1);
map.set(2, 'value');
map.set('b', 'value');
assert.strictEqual(map.size, 2);
});
it('deletes the oldest entry when the capacity is exceeded', () => {
const map = new LRUMap(4);
map.set(1, 'value');
map.set(2, 'value');
map.set(3, 'value');
map.set(4, 'value');
map.set(5, 'value');
assert.isNull(map.get(1));
assert.isNotNull(map.get(2));
assert.isNotNull(map.get(3));
assert.isNotNull(map.get(4));
assert.isNotNull(map.get(5));
map.set('a', 'value');
map.set('b', 'value');
map.set('c', 'value');
map.set('d', 'value');
map.set('e', 'value');
assert.isNull(map.get('a'));
assert.isNotNull(map.get('b'));
assert.isNotNull(map.get('c'));
assert.isNotNull(map.get('d'));
assert.isNotNull(map.get('e'));
assert.strictEqual(map.size, 4);
});
it('prevents a recently accessed entry from getting deleted', () => {
const map = new LRUMap(2);
map.set(1, 'value');
map.set(2, 'value');
map.get(1);
map.set('a', 'value');
map.set('b', 'value');
map.get('a');
// a would normally get deleted here, except that we called get()
map.set(3, 'value');
assert.isNotNull(map.get(1));
map.set('c', 'value');
assert.isNotNull(map.get('a'));
// b got deleted instead of a
assert.isNull(map.get(2));
assert.isNotNull(map.get(3));
assert.isNull(map.get('b'));
assert.isNotNull(map.get('c'));
});
it('supports mutation', () => {
const map = new LRUMap(10);
map.set(1, 'oldvalue');
map.set(1, 'newvalue');
map.set('keya', 'oldvalue');
map.set('keya', 'newvalue');
// mutation doesn't change the size
assert.strictEqual(map.size, 1);
assert.strictEqual(map.get(1), 'newvalue');
assert.strictEqual(map.get('keya'), 'newvalue');
});
});
+4 -15
View File
@@ -6,12 +6,12 @@
interface ILinkedListNode<T> {
prev: ILinkedListNode<T>;
next: ILinkedListNode<T>;
key: number;
key: string;
value: T;
}
export default class LRUMap<T> {
private _map: { [key: number]: ILinkedListNode<T> } = {};
private _map: { [key: string]: ILinkedListNode<T> } = {};
private _head: ILinkedListNode<T> = null;
private _tail: ILinkedListNode<T> = null;
private _nodePool: ILinkedListNode<T>[] = [];
@@ -68,7 +68,7 @@ export default class LRUMap<T> {
}
}
public get(key: number): T | null {
public get(key: string): T | null {
// This is unsafe: We're assuming our keyspace doesn't overlap with Object.prototype. However,
// it's faster than calling hasOwnProperty, and in our case, it would never overlap.
const node = this._map[key];
@@ -80,23 +80,12 @@ export default class LRUMap<T> {
return null;
}
/**
* Gets a value from a key without marking it as the most recently used item.
*/
public peekValue(key: number): T | null {
const node = this._map[key];
if (node !== undefined) {
return node.value;
}
return null;
}
public peek(): T | null {
const head = this._head;
return head === null ? null : head.value;
}
public set(key: number, value: T): void {
public set(key: string, value: T): void {
// This is unsafe: See note above.
let node = this._map[key];
if (node !== undefined) {
+1 -1
View File
@@ -53,7 +53,7 @@ export default class StaticCharAtlas extends BaseCharAtlas {
y: number
): boolean {
// we're not warmed up yet
if (this._texture === null || this._texture === undefined) {
if (this._texture == null) {
return false;
}
+1 -1
View File
@@ -7,7 +7,7 @@ export const INVERTED_DEFAULT_COLOR = -1;
export const DIM_OPACITY = 0.5;
export interface IGlyphIdentifier {
chars: string;
char: string;
code: number;
bg: number;
fg: number;
+11 -52
View File
@@ -4,12 +4,12 @@
*/
import { IRenderer, IRenderDimensions, IColorSet } from '../Types';
import { ILinkHoverEvent, ITerminal, CharacterJoinerHandler, LinkHoverEventTypes } from '../../Types';
import { ITerminal } from '../../Types';
import { ITheme } from 'xterm';
import { EventEmitter } from '../../common/EventEmitter';
import { EventEmitter } from '../../EventEmitter';
import { ColorManager } from '../ColorManager';
import { RenderDebouncer } from '../../ui/RenderDebouncer';
import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from './DomRendererRowFactory';
import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, DomRendererRowFactory } from './DomRendererRowFactory';
const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-';
const ROW_CONTAINER_CLASS = 'xterm-rows';
@@ -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.cols, this._terminal.rows);
this._refreshRowElements(this._terminal.rows, this._terminal.cols);
this._selectionContainer = document.createElement('div');
this._selectionContainer.classList.add(SELECTION_CLASS);
this._selectionContainer.setAttribute('aria-hidden', 'true');
@@ -79,18 +79,6 @@ export class DomRenderer extends EventEmitter implements IRenderer {
this._terminal.element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass);
this._terminal.screenElement.appendChild(this._rowContainer);
this._terminal.screenElement.appendChild(this._selectionContainer);
this._terminal.linkifier.on(LinkHoverEventTypes.HOVER, (e: ILinkHoverEvent) => this._onLinkHover(e));
this._terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: ILinkHoverEvent) => this._onLinkLeave(e));
}
public dispose(): void {
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 {
@@ -163,19 +151,13 @@ export class DomRenderer extends EventEmitter implements IRenderer {
`}`;
// Cursor
styles +=
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS} {` +
` outline: 1px solid ${this.colorManager.colors.cursor.css};` +
` outline-offset: -1px;` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS} {` +
` background-color: ${this.colorManager.colors.cursor.css};` +
` color: ${this.colorManager.colors.cursorAccent.css};` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BAR_CLASS} {` +
` box-shadow: 1px 0 0 ${this.colorManager.colors.cursor.css} inset;` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_UNDERLINE_CLASS} {` +
` box-shadow: 0 -1px 0 ${this.colorManager.colors.cursor.css} inset;` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS} {` +
` outline: 1px solid #fff;` +
` outline-offset: -1px;` +
`}`;
// Selection
styles +=
@@ -259,14 +241,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, start[0], end[0], viewportCappedEndRow - viewportCappedStartRow + 1)
this._createSelectionElement(viewportCappedStartRow, startCol, end[0], viewportCappedEndRow - viewportStartRow + 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
@@ -328,8 +310,7 @@ export class DomRenderer extends EventEmitter implements IRenderer {
const row = y + terminal.buffer.ydisp;
const lineData = terminal.buffer.lines.get(row);
const cursorStyle = terminal.options.cursorStyle;
rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorStyle, cursorX, terminal.charMeasure.width, terminal.cols));
rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorX, terminal.charMeasure.width, terminal.cols));
}
this._terminal.emit('refresh', {start, end});
@@ -338,26 +319,4 @@ 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; }
private _onLinkHover(e: ILinkHoverEvent): void {
this._setCellUnderline(e.x1, e.x2, e.y1, e.y2, e.cols, true);
}
private _onLinkLeave(e: ILinkHoverEvent): void {
this._setCellUnderline(e.x1, e.x2, e.y1, e.y2, e.cols, false);
}
private _setCellUnderline(x: number, x2: number, y: number, y2: number, cols: number, enabled: boolean): void {
while (x !== x2 || y !== y2) {
const span = <HTMLElement>this._rowElements[y].children[x];
span.style.textDecoration = enabled ? 'underline' : 'none';
x = (x + 1) % cols;
if (x === 0) {
y++;
}
}
}
}
+35 -38
View File
@@ -6,15 +6,14 @@
import jsdom = require('jsdom');
import { assert } from 'chai';
import { DomRendererRowFactory } from './DomRendererRowFactory';
import { DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from '../../Buffer';
import { LineData } from '../../Types';
import { DEFAULT_ATTR } 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: IBufferLine;
let lineData: LineData;
beforeEach(() => {
dom = new jsdom.JSDOM('');
@@ -24,7 +23,7 @@ describe('DomRendererRowFactory', () => {
describe('createRow', () => {
it('should create an element for every character in the row', () => {
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span> </span>' +
'<span> </span>'
@@ -32,29 +31,27 @@ describe('DomRendererRowFactory', () => {
});
it('should set correct attributes for double width characters', () => {
lineData.set(0, [DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)]);
lineData[0] = [DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)];
// There should be no element for the following "empty" cell
lineData.set(1, [DEFAULT_ATTR, '', 0, undefined]);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
lineData[1] = [DEFAULT_ATTR, '', 0, undefined];
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span style="width: 10px;">語</span>'
);
});
it('should add class for cursor and cursor style', () => {
for (const style of ['block', 'bar', 'underline']) {
const fragment = rowFactory.createRow(lineData, true, style, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-cursor xterm-cursor-${style}"> </span>` +
'<span> </span>'
);
}
it('should add class for cursor', () => {
const fragment = rowFactory.createRow(lineData, true, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-cursor"> </span>' +
'<span> </span>'
);
});
it('should not render cells that go beyond the terminal\'s columns', () => {
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, undefined, 0, 5, 1);
lineData[0] = [DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)];
lineData[1] = [DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5, 1);
assert.equal(getFragmentHtml(fragment),
'<span>a</span>'
);
@@ -62,8 +59,8 @@ describe('DomRendererRowFactory', () => {
describe('attributes', () => {
it('should add class for bold', () => {
lineData.set(0, [DEFAULT_ATTR | (FLAGS.BOLD << 18), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
lineData[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>' +
'<span> </span>'
@@ -71,8 +68,8 @@ describe('DomRendererRowFactory', () => {
});
it('should add class for italic', () => {
lineData.set(0, [DEFAULT_ATTR | (FLAGS.ITALIC << 18), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
lineData[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>' +
'<span> </span>'
@@ -82,8 +79,8 @@ describe('DomRendererRowFactory', () => {
it('should add classes for 256 foreground colors', () => {
const defaultAttrNoFgColor = (0 << 9) | (256 << 0);
for (let i = 0; i < 256; i++) {
lineData.set(0, [defaultAttrNoFgColor | (i << 9), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
lineData[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>` +
'<span> </span>'
@@ -94,8 +91,8 @@ describe('DomRendererRowFactory', () => {
it('should add classes for 256 background colors', () => {
const defaultAttrNoBgColor = (257 << 9) | (0 << 0);
for (let i = 0; i < 256; i++) {
lineData.set(0, [defaultAttrNoBgColor | (i << 0), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
lineData[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>` +
'<span> </span>'
@@ -104,8 +101,8 @@ describe('DomRendererRowFactory', () => {
});
it('should correctly invert colors', () => {
lineData.set(0, [(FLAGS.INVERSE << 18) | (2 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
lineData[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>' +
'<span> </span>'
@@ -113,8 +110,8 @@ describe('DomRendererRowFactory', () => {
});
it('should correctly invert default fg color', () => {
lineData.set(0, [(FLAGS.INVERSE << 18) | (257 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
lineData[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>' +
'<span> </span>'
@@ -122,8 +119,8 @@ describe('DomRendererRowFactory', () => {
});
it('should correctly invert default bg color', () => {
lineData.set(0, [(FLAGS.INVERSE << 18) | (1 << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
lineData[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>' +
'<span> </span>'
@@ -132,8 +129,8 @@ describe('DomRendererRowFactory', () => {
it('should turn bold fg text bright', () => {
for (let i = 0; i < 8; i++) {
lineData.set(0, [(FLAGS.BOLD << 18) | (i << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)]);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20);
lineData[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>` +
'<span> </span>'
@@ -149,10 +146,10 @@ describe('DomRendererRowFactory', () => {
return element.innerHTML;
}
function createEmptyLineData(cols: number): IBufferLine {
const lineData = new BufferLine();
function createEmptyLineData(cols: number): LineData {
const lineData: LineData = [];
for (let i = 0; i < cols; i++) {
lineData.push([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
lineData.push([DEFAULT_ATTR, ' ', 1, 32 /* ' '.charCodeAt(0) */]);
}
return lineData;
}
+3 -18
View File
@@ -3,16 +3,13 @@
* @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';
export const CURSOR_CLASS = 'xterm-cursor';
export const CURSOR_STYLE_BLOCK_CLASS = 'xterm-cursor-block';
export const CURSOR_STYLE_BAR_CLASS = 'xterm-cursor-bar';
export const CURSOR_STYLE_UNDERLINE_CLASS = 'xterm-cursor-underline';
export class DomRendererRowFactory {
constructor(
@@ -20,7 +17,7 @@ export class DomRendererRowFactory {
) {
}
public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cellWidth: number, cols: number): DocumentFragment {
public createRow(lineData: LineData, isCursorRow: boolean, cursorX: number, cellWidth: number, cols: number): DocumentFragment {
const fragment = this._document.createDocumentFragment();
let colCount = 0;
@@ -30,7 +27,7 @@ export class DomRendererRowFactory {
continue;
}
const charData = lineData.get(x);
const charData = lineData[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];
@@ -51,18 +48,6 @@ export class DomRendererRowFactory {
if (isCursorRow && x === cursorX) {
charElement.classList.add(CURSOR_CLASS);
switch (cursorStyle) {
case 'bar':
charElement.classList.add(CURSOR_STYLE_BAR_CLASS);
break;
case 'underline':
charElement.classList.add(CURSOR_STYLE_UNDERLINE_CLASS);
break;
default:
charElement.classList.add(CURSOR_STYLE_BLOCK_CLASS);
break;
}
}
// If inverse flag is on, the foreground should become the background.
+2 -2
View File
@@ -22,8 +22,8 @@ export const isLinux = platform.indexOf('Linux') >= 0;
/**
* Return if the given array contains the given element
* @param arr The array to search for the given element.
* @param el The element to look for into the array
* @param {Array} array The array to search for the given element.
* @param {Object} el The element to look for into the array
*/
function contains(arr: any[], el: any): boolean {
return arr.indexOf(el) >= 0;
+1 -1
View File
@@ -4,7 +4,7 @@
*/
import { ICharMeasure, ITerminalOptions } from '../Types';
import { EventEmitter } from '../common/EventEmitter';
import { EventEmitter } from '../EventEmitter';
/**
* Utility class that measures the size of a character. Measurements are done in
+5 -10
View File
@@ -22,16 +22,11 @@ export class RenderDebouncer implements IDisposable {
}
}
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;
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;
if (this._animationFrame) {
return;
+1 -1
View File
@@ -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 || event.pageX === undefined) {
if (event.pageX == null) {
return null;
}
+21 -35
View File
@@ -4,19 +4,10 @@
*/
import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Types';
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ILinkifier, IMouseHelper, ILinkMatcherOptions, CharacterJoinerHandler, IBufferLine, IBufferStringIterator } from '../Types';
import { ICircularList, XtermListener } from '../common/Types';
import { LineData, IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ICircularList, ILinkifier, IMouseHelper, ILinkMatcherOptions, XtermListener } from '../Types';
import { Buffer } from '../Buffer';
import * as Browser from '../shared/utils/Browser';
import { ITheme, IDisposable, IMarker } from 'xterm';
import { Terminal } from '../Terminal';
export class TestTerminal extends Terminal {
writeSync(data: string): void {
this.writeBuffer.push(data);
this._innerWrite();
}
}
export class MockTerminal implements ITerminal {
markers: IMarker[];
@@ -93,6 +84,9 @@ 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;
@@ -154,8 +148,14 @@ export class MockTerminal implements ITerminal {
refresh(start: number, end: number): void {
throw new Error('Method not implemented.');
}
registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; }
deregisterCharacterJoiner(joinerId: number): void { }
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;
}
}
export class MockCharMeasure implements ICharMeasure {
@@ -229,12 +229,18 @@ 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.');
}
@@ -277,23 +283,11 @@ 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<IBufferLine>;
lines: ICircularList<[number, string, number, number][]>;
ydisp: number;
ybase: number;
hasScrollback: boolean;
@@ -308,7 +302,7 @@ export class MockBuffer implements IBuffer {
return Buffer.prototype.translateBufferLineToString.apply(this, arguments);
}
getWrappedRangeForLine(y: number): { first: number; last: number; } {
return Buffer.prototype.getWrappedRangeForLine.apply(this, arguments);
throw new Error('Method not implemented.');
}
nextStop(x?: number): number {
throw new Error('Method not implemented.');
@@ -316,15 +310,9 @@ export class MockBuffer implements IBuffer {
prevStop(x?: number): number {
throw new Error('Method not implemented.');
}
setLines(lines: ICircularList<IBufferLine>): void {
setLines(lines: ICircularList<[number, string, number, number][]>): void {
this.lines = lines;
}
stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[] {
return Buffer.prototype.stringIndexToBufferIndex.apply(this, arguments);
}
iterator(trimRight: boolean, startIndex?: number, endIndex?: number): IBufferStringIterator {
return Buffer.prototype.iterator.apply(this, arguments);
}
}
export class MockRenderer implements IRenderer {
@@ -356,8 +344,6 @@ 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
View File
@@ -5,7 +5,6 @@
"lib": [
"dom",
"es5",
"es6",
"scripthost",
"es2015.promise"
],
+6 -3
View File
@@ -39,7 +39,6 @@
"one-variable-per-declaration": true,
"no-unsafe-finally": true,
"no-var-keyword": true,
"prefer-const": true,
"quotemark": [
true,
"single"
@@ -60,7 +59,10 @@
"esSpecCompliant": true
}
],
"triple-equals": true,
"triple-equals": [
true,
"allow-null-check"
],
"typedef-whitespace": [
true,
{
@@ -108,6 +110,7 @@
},
"prefer-const-enum": [
true
]
],
"prefer-const": true
}
}
+9 -64
View File
@@ -39,16 +39,6 @@ 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.
*/
@@ -144,13 +134,19 @@ declare module 'xterm' {
macOptionClickForcesSelection?: boolean;
/**
* The type of renderer to use, this allows using the fallback DOM renderer
* when canvas is too slow for the environment. The following features do
* not work when the DOM renderer is used:
* (EXPERIMENTAL) The type of renderer to use, this allows using the
* fallback DOM renderer when canvas is too slow for the environment. The
* following features do not work when the DOM renderer is used:
*
* - Links
* - Line height
* - Letter spacing
* - Cursor blink
* - Cursor style
*
* This option is marked as experiemental because it will eventually be
* moved to an addon. You can only set this option in the constructor (not
* setOption).
*/
rendererType?: RendererType;
@@ -420,21 +416,8 @@ 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;
/**
@@ -487,44 +470,6 @@ 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
View File
@@ -0,0 +1,10 @@
const path = require('path');
module.exports = {
entry: './demo/main.js',
output: {
path: path.resolve(__dirname, 'demo/dist'),
filename: 'bundle.js'
},
devtool: 'source-map'
};
-6395
View File
File diff suppressed because it is too large Load Diff