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
136 changed files with 2349 additions and 15138 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
--- ---
name: Question name: Question
about: The issue tracker is not for questions. Please ask questions on https://stackoverflow.com/questions/tagged/xtermjs about: The issue tracker is not for questions. Please ask questions on https://stackoverflow.com/questions/tagged/xtermjs or https://gitter.im/sourcelair/xterm.js.
--- ---
🛑 The issue tracker is not for questions 🛑 🛑 The issue tracker is not for questions 🛑
If you have a question, please ask it on https://stackoverflow.com/questions/tagged/xtermjs. If you have a question, please ask it on https://stackoverflow.com/questions/tagged/xtermjs or https://gitter.im/sourcelair/xterm.js.
+1
View File
@@ -12,6 +12,7 @@ npm-debug.log
/.idea/ /.idea/
.env .env
build/ build/
.vscode/
.DS_Store .DS_Store
fixtures/typings-test/*.js fixtures/typings-test/*.js
package-lock.json package-lock.json
+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
-44
View File
@@ -1,44 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Unit Tests",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/mocha",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/mocha.cmd"
},
"runtimeArgs": [
"--colors",
"--recursive",
"${workspaceRoot}/lib"
],
"sourceMaps": true,
"outFiles": [ "${workspaceRoot}/lib/**/*.js" ],
"internalConsoleOptions": "openOnSessionStart"
},
{
"type": "chrome",
"request": "launch",
"name": "Demo Client",
"url": "http://0.0.0.0:3000",
"windows": {
"url": "http://127.0.0.1:3000"
},
"webRoot": "${workspaceFolder}/"
},
{
"type": "node",
"request": "launch",
"name": "Demo Server",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"start-debug"
],
"port": 9229
}
]
}
+130
View File
@@ -0,0 +1,130 @@
List of xterm.js contributors. Updated before every release.
7PH <b.raymond@cern.ch>
7PH <b.raymond@protonmail.com>
Adrian Labbé <adrilabbelol@gmail.com>
Aleksandr Andrienko <aandrienko@codenvy.com>
Aleksandr Andriienko <oandriie@redhat.com>
Alessandro Nadalin <alessandro.nadalin@gmail.com>
Alexander Olsson <noseglid@gmail.com>
Alexandre Petit-Pas <petitpasalexandre@gmail.com>
Alexey Kontsevoy <alexey@gravitational.com>
Andres Mejia <amejia004@gmail.com>
Anish Athalye <me@anishathalye.com>
Anthony Lapenna <lapenna.anthony@gmail.com>
Antonin Stefanutti <antonin@stefanutti.fr>
Antonis Kalipetis <akalipetis@sourcelair.com>
Anton Skshidlevsky <meefik@gmail.com>
Anton Yurovskykh <anton.yurovskykh@gmail.com>
Artem Arbatskiy <artyom.arbatskiy@gmail.com>
Austin Robertson <austinrobertson@gmail.com>
ayapi <colors.aya@gmail.com>
Ben Hall <ben@benhall.me.uk>
Benjamin Fischer <benjamin.fischer@rwth-aachen.de>
Benjamin Raymond <b.raymond@cern.ch>
Benjamin Woodruff <github@benjam.info>
Bill Church <billchurch@users.noreply.github.com>
Bob Reid <bobreid@Bobs-MacBook-Pro.local>
bottleofwater <nison.mael+bottleofwater@gmail.com>
Brandon Bayer <b@bayer.ws>
Brian Mock <brian@mockbrian.com>
Bruno Ribeiro <b.m.fernandes.ribeiro@gmail.com>
Bruno Ribeito <b.m.fernandes.ribeiro@gmail.com>
Carson Anderson <carson@betterservers.com>
CHaBou <chabup@delean.fr>
Christian Budde Christensen <budde377@gmail.com>
Christof Marti <chrmarti@microsoft.com>
Christopher Jeffrey <chjjeffrey@gmail.com>
coderaiser <mnemonic.enemy@gmail.com>
Damien Tournoud <damien@platform.sh>
Dan Brown <jdanbrown@gmail.com>
Daniel Griffen <dagriffe@microsoft.com>
Daniel Griffen <daniel@griffen.io>
Daniel Imms <daimms@microsoft.com>
Daniel Risacher <drisacher@gmail.com>
Dan Kaplun <dbkaplun@twitch.tv>
Darin Morrison <freebroccolo@users.noreply.github.com>
dcylabs <dcylabs@gmail.com>
Dominik Csapak <d.csapak@proxmox.com>
Edgar Andrés Margffoy Tuay <andfoy@gmail.com>
Elliot Saba <staticfloat@gmail.com>
Exile <exile@chamber.ee>
Felipe Gasper <felipe@felipegasper.com>
Felix <30559812+felixse@users.noreply.github.com>
ficristo <ficristo.work@gmail.com>
Gary Ritchie <gary@rstudio.com>
hiro-su <h.sugipon@gmail.com>
Ian Lewis <ianlewis@google.com>
imoses <ido@twiggle.com>
InDieTasten <indietasten@gmail.com>
irokas <akasidiari@inf.uth.gr>
Jakob Gillich <jakob@gillich.me>
Jan Kuri <jkuri88@gmail.com>
Jean Bruenn <himself@jeanbruenn.info>
Jeremy Danyow <jedanyow@microsoft.com>
Jianhui Zhao <jianhuizhao329@gmail.com>
Joao Moreno <jomo@microsoft.com>
Joao Moreno <mail@joaomoreno.com>
Johannes Zellner <johannes@nebulon.de>
Jon Austin <jon.i.austin@gmail.com>
Jon Kohler <jon@nutanix.com>
Jon Masters <jon.masters@sky.com>
Jörg Breitbart <jerch@rockborn.de>
jpoth <poth.john@gmail.com>
Justin Luk <jluk@users.noreply.github.com>
Justin Mecham <justin@mecham.me>
Kirill Merkushev <lanwen@yandex.ru>
Krasimir Tsonev <krasimir@outset.ws>
Ledion Bitincka <lbitincka@gmail.com>
Linus Unnebäck <linus@folkdatorn.se>
Luca <LucaT1@users.noreply.github.com>
Lucian Buzzo <lucian.buzzo@gmail.com>
Lukas Drgon <lukas.drgon@gmail.com>
Lukas Geiger <lukas.geiger94@gmail.com>
Maël Nison <nison.mael@gmail.com>
Marc Dumais <marc.dumais@ericsson.com>
Marek Libra <mlibra@redhat.com>
Markus F.X.J. Oberhumer <markus@oberhumer.com>
Martin Chloride <i@martincl2.me>
Martin Koppehel <martin.koppehel@st.ovgu.de>
Martin Wang <jiahaow@ca.ibm.com>
Matt Bierner <matb@microsoft.com>
Matthew James <matthew.d.james87@gmail.com>
Michael Irwin <mikesir87@gmail.com>
Mikko Karvonen <mikko.karvonen@arm.com>
mofux <t.zilz@mofux.org>
muji <muji@tmpfs.org>
Nicolas Ramz <nicolas.ramz@gmail.com>
npezza93 <npezza93@gmail.com>
Oleksandr Andriienko <oandriie@redhat.com>
Paris Kasidiaris <pariskasidiaris@gmail.com>
Paris Kasidiaris <paris@sourcelair.com>
Peng Xiao <pengxiao@outlook.com>
Peter Baumgarten <me@peterbaumgarten.com>
Philip Olson <philip.olson@protonmail.ch>
PowerHat <31401273+7PH@users.noreply.github.com>
pro-src <34285059+pro-src@users.noreply.github.com>
pro-src <rodneyd.teal@gmail.com>
Rick Baker <rick@ricktbaker.com>
runarberg <runar@greenqloud.com>
Saad Malik <simfox3@gmail.com>
Samuel Williams <samuel.williams@oriontransfer.co.nz>
Saswat Das <saswatds@users.noreply.github.com>
Saul Costa <saul@codevolve.com>
Shuanglei Tao <tsl0922@gmail.com>
sitzmar <sitzmar@gmail.com>
Steven Silvester <steven.silvester@ieee.org>
stuicey <stuicey@users.noreply.github.com>
Sven-Hendrik Haase <svenstaro@gmail.com>
t-amqi <umberos@gmail.com>
Thanasis Daglis <thanasis@sourcelair.com>
thomas <thomas@kreativkonzentrat.de>
Thomas Zilz <t.zilz@mofux.org>
Tine Jozelj <tine.jozelj@outlook.com>
Tyler Jewell <tjewell@codenvy.com>
Vincent Woo <me@vincentwoo.com>
Viraj Sinha <virajosinha@gmail.com>
yutaka <shigure@refy.net>
YuviPanda <yuvipanda@gmail.com>
ZHAO Xudong <zxdong@gmail.com>
赵雪珂 <zhaoxueke@onething.net>
+11 -9
View File
@@ -23,32 +23,34 @@ opening an issue, read these pointers.
- Mention precisely what went wrong. What did you expect to happen? What happened instead? Describe the - Mention precisely what went wrong. What did you expect to happen? What happened instead? Describe the
exact steps a maintainer has to take to make the problem occur. exact steps a maintainer has to take to make the problem occur.
- If the problem can not be reproduced in the [demo of xterm.js](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 - Be polite. Issues with an indignant or belligerent tone tend to be moved to the
bottom of the pile. bottom of the pile.
## Contributing code ## Contributing code
You can find issues to work on by looking at the [help wanted](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) or [good first issue](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) issues. It's a good idea to comment on the issue saying that you're taking it, just in case someone else comes along and you duplicate work. Once you have your issue, here are the steps to contribute: - Make sure you have a [GitHub account](https://github.com/join)
- Fork [xterm.js](https://github.com/sourcelair/xterm.js/) - Fork [xterm.js](https://github.com/sourcelair/xterm.js/)
([how to fork a repo](https://help.github.com/articles/fork-a-repo)). ([how to fork a repo](https://help.github.com/articles/fork-a-repo))
- Get the [xterm.js demo](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo) running. - Make your changes
- Make your changes.
- If your changes are easy to test or likely to regress, add tests. Tests go into `test`, directory. - If your changes are easy to test or likely to regress, add tests. Tests go into `test`, directory.
- Follow the general code style of the rest of the project (see below). - Follow the general code style of the rest of the project (see below).
- Submit a pull request - Submit a pull request
([how to create a pull request](https://help.github.com/articles/fork-a-repo)). ([how to create a pull request](https://help.github.com/articles/fork-a-repo)).
Don't put more than one feature/fix in a single pull request. Don't put more than one feature/fix in a single pull request.
By contributing code to xterm.js you: By contributing code to xterm.js you
- Agree to license the contributed code under xterm.js' [MIT - agree to license the contributed code under xterm.js' [MIT
license](LICENSE). license](LICENSE).
- Confirm that you have the right to contribute and license the code - confirm that you have the right to contribute and license the code
in question. (Either you hold all rights on the code, or the rights in question. (Either you hold all rights on the code, or the rights
holder has explicitly granted the right to use it like this, holder has explicitly granted the right to use it like this,
through a compatible open source license or through a direct through a compatible open source license or through a direct
agreement with you.) 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 -1
View File
@@ -16,4 +16,4 @@ RUN npm install
COPY . /usr/src/app COPY . /usr/src/app
# Run the tests and build, to make sure everything is working nicely # Run the tests and build, to make sure everything is working nicely
RUN npm run build && npm run test RUN npm run build && npm run webpack && npm run test
-1
View File
@@ -1,4 +1,3 @@
Copyright (c) 2017-2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)
Copyright (c) 2014-2016, SourceLair Private Company (https://www.sourcelair.com) Copyright (c) 2014-2016, SourceLair Private Company (https://www.sourcelair.com)
Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/) Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/)
+137 -53
View File
@@ -1,23 +1,23 @@
# [![xterm.js logo](logo-full.png)](https://xtermjs.org) # [![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) [![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)
[![Coverage Status](https://coveralls.io/repos/github/xtermjs/xterm.js/badge.svg?branch=master)](https://coveralls.io/github/xtermjs/xterm.js?branch=master)
Xterm.js is a front-end component written in TypeScript that lets applications bring fully-featured terminals to their users in the browser. It's used by popular projects such as VS Code, Hyper and Theia. Xterm.js is a terminal front-end component written in JavaScript that works in the browser.
It enables applications to provide fully featured terminals to their users and create great development experiences.
## Features ## Features
- **Text-based application support**: Use xterm.js to work with applications like `bash`, `git` etc.
- **Terminal apps just work**: Xterm.js works with most terminal apps such as `bash`, `vim` and `tmux`, this includes support for curses-based apps and mouse event support. - **Curses-based application support**: Use xterm.js to work with applications like `vim`, `tmux` etc.
- **Perfomant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer. - **Mouse events support**: Xterm.js captures mouse events like click and scroll and passes them to the terminal's back-end controlling process
- **Rich unicode support**: Supports CJK, emojis and IMEs. - **CJK (Chinese, Japanese, Korean) character support**: Xterm.js renders CJK characters seamlessly
- **Self-contained**: Requires zero dependencies to work. - **IME support**: Insert international (including CJK) characters using IME input with your keyboard
- **Accessible**: Screen reader support can be turned on using the `screenReaderMode` option. - **Self-contained library**: Xterm.js works on its own. It does not require any external libraries like jQuery or React to work
- **And much more**: Links, theming, addons, well documented API, etc. - **Modular, event-based API**: Lets you build addons and themes with ease
## What xterm.js is not ## What xterm.js is not
- Xterm.js is not a terminal application that you can download and use on your computer
- Xterm.js is not a terminal application that you can download and use on your computer. - Xterm.js is not `bash`. Xterm.js can be connected to processes like `bash` and let you interact with them (provide input, receive output)
- Xterm.js is not `bash`. Xterm.js can be connected to processes like `bash` and let you interact with them (provide input, receive output).
## Getting Started ## Getting Started
@@ -27,7 +27,7 @@ First you need to install the module, we ship exclusively through [npm](https://
npm install xterm npm install xterm
``` ```
To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to the head of your html page. Then create a `<div id="terminal"></div>` onto which xterm can attach itself. Finally instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`. To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to the head of your html page. Then create a `<div id="terminal"></div>` onto which xterm can attach itself.
```html ```html
<!doctype html> <!doctype html>
@@ -41,15 +41,17 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t
<script> <script>
var term = new Terminal(); var term = new Terminal();
term.open(document.getElementById('terminal')); term.open(document.getElementById('terminal'));
term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')       term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')
</script> </script>
</body> </body>
</html> </html>
``` ```
Finally instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`.
### Importing ### Importing
The recommended way to load xterm.js is via the ES6 module syntax: The proposed way to load xterm.js is via the ES6 module syntax.
```javascript ```javascript
import { Terminal } from 'xterm'; import { Terminal } from 'xterm';
@@ -57,34 +59,42 @@ import { Terminal } from 'xterm';
### Addons ### 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 the [public API](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts). 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.
To use an addon, just import the JavaScript module and pass it to `Terminal`'s `applyAddon` method: To use an addon, just import the JavaScript module and pass it to `Terminal`'s `applyAddon` method:
```javascript ```javascript
import { Terminal } from 'xterm'; import { Terminal } from xterm;
import * as fit from 'xterm/lib/addons/fit/fit'; import * as fit from 'xterm/lib/addons/fit/fit';
Terminal.applyAddon(fit); Terminal.applyAddon(fit);
var xterm = new Terminal(); // Instantiate the terminal var xterm = new Terminal(); // Instantiate the terminal
xterm.fit(); // Use the `fit` method, provided by the `fit` addon 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 #### 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. `(xterm as any).fit()`. Alternatively, you can import the addon function and enhance the terminal on demand. This has better typing support and is friendly to treeshaking. 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()`.
Alternatively, you can import addon function and enhance the terminal on demand. This would have better typing support and is friendly to treeshaking. E.g.:
```typescript ```typescript
import { Terminal } from 'xterm'; import { Terminal } from 'xterm';
import { fit } from 'xterm/lib/addons/fit/fit'; import { fit } from 'xterm/lib/addons/fit/fit';
const xterm = new Terminal(); const xterm = new Terminal();
fit(xterm); // Fit the terminal when necessary // Fit the terminal when necessary:
fit(xterm);
``` ```
#### Third party addons
There are also the following third party addons available:
- [xterm-webfont](https://www.npmjs.com/package/xterm-webfont)
## Browser Support ## Browser Support
Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Here is a list of the versions we aim to support: Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Here is a list of the versions we aim to support:
@@ -95,28 +105,22 @@ Since xterm.js is typically implemented as a developer tool, only modern browser
- Safari latest - Safari latest
- IE11 - IE11
Xterm.js works seamlessly in [Electron](https://electronjs.org/) apps and may even work on earlier versions of the browsers, these are the versions we strive to keep working. 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 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 to enable experimentation with new ideas without committing to support it like a normal [semver](https://semver.org/) API. Note that these APIs can change radically between versions so be sure to read release notes if you plan on using experimental APIs.
## Real-world uses ## Real-world uses
Xterm.js is used in several world-class applications to provide great terminal experiences. Xterm.js is used in several world-class applications to provide great terminal experiences.
- [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js. - [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js
- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js. - [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js
- [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js. - [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js
- [**Katacoda**](https://www.katacoda.com/): Katacoda is an Interactive Learning Platform for software developers, covering the latest Cloud Native technologies. - [**Katacoda**](https://www.katacoda.com/): Katacoda is an Interactive Learning Platform for software developers, covering the latest Cloud Native technologies.
- [**Eclipse Che**](http://www.eclipse.org/che): Developer workspace server, cloud IDE, and Eclipse next-generation IDE. - [**Eclipse Che**](http://www.eclipse.org/che): Developer workspace server, cloud IDE, and Eclipse next-generation IDE.
- [**Codenvy**](http://www.codenvy.com): Cloud workspaces for development teams. - [**Codenvy**](http://www.codenvy.com): Cloud workspaces for development teams.
- [**CoderPad**](https://coderpad.io): Online interviewing platform for programmers. Run code in many programming languages, with results displayed by xterm.js. - [**CoderPad**](https://coderpad.io): Online interviewing platform for programmers. Run code in many programming languages, with results displayed by `xterm.js`.
- [**WebSSH2**](https://github.com/billchurch/WebSSH2): A web based SSH2 client using xterm.js, socket.io, and ssh2. - [**WebSSH2**](https://github.com/billchurch/WebSSH2): A web based SSH2 client using `xterm.js`, socket.io, and ssh2.
- [**Spyder Terminal**](https://github.com/spyder-ide/spyder-terminal): A full fledged system terminal embedded on Spyder IDE. - [**Spyder Terminal**](https://github.com/spyder-ide/spyder-terminal): A full fledged system terminal embedded on Spyder IDE.
- [**Cloud Commander**](https://cloudcmd.io "Cloud Commander"): Orthodox web file manager with console and editor. - [**Cloud Commander**](https://cloudcmd.io "Cloud Commander"): Orthodox web file manager with console and editor.
- [**Codevolve**](https://www.codevolve.com "Codevolve"): Online platform for interactive coding and web development courses. Live container-backed terminal uses xterm.js. - [**Codevolve**](https://www.codevolve.com "Codevolve"): Online platform for interactive coding and web development courses. Live container-backed terminal uses `xterm.js`.
- [**RStudio**](https://www.rstudio.com/products/RStudio "RStudio"): RStudio is an integrated development environment (IDE) for R. - [**RStudio**](https://www.rstudio.com/products/RStudio "RStudio"): RStudio is an integrated development environment (IDE) for R.
- [**Terminal for Atom**](https://github.com/jsmecham/atom-terminal-tab): A simple terminal for the Atom text editor. - [**Terminal for Atom**](https://github.com/jsmecham/atom-terminal-tab): A simple terminal for the Atom text editor.
- [**Eclipse Orion**](https://orionhub.org): A modern, open source software development environment that runs in the cloud. Code, deploy and run in the cloud. - [**Eclipse Orion**](https://orionhub.org): A modern, open source software development environment that runs in the cloud. Code, deploy and run in the cloud.
@@ -124,8 +128,9 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**Hexlet**](https://en.hexlet.io): Practical programming courses (JavaScript, PHP, Unix, databases, functional programming). A steady path from the first line of code to the first job. - [**Hexlet**](https://en.hexlet.io): Practical programming courses (JavaScript, PHP, Unix, databases, functional programming). A steady path from the first line of code to the first job.
- [**Selenoid UI**](https://github.com/aerokube/selenoid-ui): Simple UI for the scallable golang implementation of Selenium Hub named Selenoid. We use XTerm for streaming logs over websockets from docker containers. - [**Selenoid UI**](https://github.com/aerokube/selenoid-ui): Simple UI for the scallable golang implementation of Selenium Hub named Selenoid. We use XTerm for streaming logs over websockets from docker containers.
- [**Portainer**](https://portainer.io): Simple management UI for Docker. - [**Portainer**](https://portainer.io): Simple management UI for Docker.
- [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising xterm.js, SJCL & websockets. - [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising `xterm.js`, SJCL & websockets.
- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages. - [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible
computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages.
- [**Theia**](https://github.com/theia-ide/theia): Theia is a cloud & desktop IDE framework implemented in TypeScript. - [**Theia**](https://github.com/theia-ide/theia): Theia is a cloud & desktop IDE framework implemented in TypeScript.
- [**Opshell**](https://github.com/ricktbaker/opshell) Ops Helper tool to make life easier working with AWS instances across multiple organizations. - [**Opshell**](https://github.com/ricktbaker/opshell) Ops Helper tool to make life easier working with AWS instances across multiple organizations.
- [**Proxmox VE**](https://www.proxmox.com/en/proxmox-ve): Proxmox VE is a complete open-source platform for enterprise virtualization. It uses xterm.js for container terminals and the host shell. - [**Proxmox VE**](https://www.proxmox.com/en/proxmox-ve): Proxmox VE is a complete open-source platform for enterprise virtualization. It uses xterm.js for container terminals and the host shell.
@@ -137,37 +142,116 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**Azure Cloud Shell**](https://shell.azure.com): Azure Cloud Shell is a Microsoft-managed admin machine built on Azure, for Azure. - [**Azure Cloud Shell**](https://shell.azure.com): Azure Cloud Shell is a Microsoft-managed admin machine built on Azure, for Azure.
- [**atom-xterm**](https://atom.io/packages/atom-xterm): Atom plugin for providing terminals inside your Atom workspace. - [**atom-xterm**](https://atom.io/packages/atom-xterm): Atom plugin for providing terminals inside your Atom workspace.
- [**rtty**](https://github.com/zhaojh329/rtty): A reverse proxy WebTTY. It is composed of the client and the server. - [**rtty**](https://github.com/zhaojh329/rtty): A reverse proxy WebTTY. It is composed of the client and the server.
- [**Pisth**](https://github.com/ColdGrub1384/Pisth): An SFTP and SSH client for iOS. - [**Pisth**](https://github.com/ColdGrub1384/Pisth): An SFTP and SSH client for iOS
- [**abstruse**](https://github.com/bleenco/abstruse): Abstruse CI is a continuous integration platform based on Node.JS and Docker. - [**abstruse**](https://github.com/bleenco/abstruse): Abstruse CI is a continuous integration platform based on Node.JS and Docker.
- [**Azure Data Studio**](https://github.com/Microsoft/azuredatastudio): A data management tool that enables working with SQL Server, Azure SQL DB and SQL DW from Windows, macOS and Linux. - [**Microsoft SQL Operations Studio**](https://github.com/Microsoft/sqlopsstudio): A data management tool that enables working with SQL Server, Azure SQL DB and SQL DW from Windows, macOS and Linux
- [**FreeMAN**](https://github.com/matthew-matvei/freeman): A free, cross-platform file manager for power users. - [**FreeMAN**](https://github.com/matthew-matvei/freeman): A free, cross-platform file manager for power users
- [**Fluent Terminal**](https://github.com/felixse/FluentTerminal): A terminal emulator based on UWP and web technologies. - [**Fluent Terminal**](https://github.com/felixse/FluentTerminal): A terminal emulator based on UWP and web technologies.
- [**Hyper**](https://hyper.is): A terminal built on web technologies. - [**Hyper**](https://hyper.is): A terminal built on web technologies
- [**Diag**](https://diag.ai): A better way to troubleshoot problems faster. Capture, share and reapply troubleshooting knowledge so you can focus on solving problems that matter. - [**Diag**](https://diag.ai): A better way to troubleshoot problems faster. Capture, share and reapply troubleshooting knowledge so you can focus on solving problems that matter.
- [**GoTTY**](https://github.com/yudai/gotty): A simple command line tool that shares your terminal as a web application based on xterm.js. - [**GoTTY**](https://github.com/yudai/gotty): A simple command line tool that shares your terminal as a web application based on xterm.js.
- [**genact**](https://github.com/svenstaro/genact): A nonsense activity generator. - [**genact**](https://github.com/svenstaro/genact): A nonsense activity generator.
- [**cPanel & WHM**](https://cpanel.com): The hosting platform of choice. - [**cPanel & WHM**](https://cpanel.com): The hosting platform of choice.
- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js. - [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js
- [**SSH Web Client**](https://github.com/roke22/PHP-SSH2-Web-Client): SSH Web Client with PHP.
- [**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.
- [**webssh**](https://github.com/huashengdun/webssh): Web based ssh client.
- [**info-beamer hosted**](https://info-beamer.com): Uses xterm.js to manage digital signage devices from the web dashboard.
- [**Jumpserver**](https://github.com/jumpserver/luna): Jumpserver Luna project, Jumpserver is a bastion server project, Luna use xterm.js for web terminal emulation.
[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. 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 ## Releases
Xterm.js follows a monthly release cycle roughly. Xterm.js follows a monthly release cycle roughly.
All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), you can view the [high-level roadmap on the wiki](https://github.com/xtermjs/xterm.js/wiki/Roadmap) and see what we're working on now by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones). 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 ## 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'
+7
View File
@@ -0,0 +1,7 @@
#! /usr/bin/env sh
tail --lines=+3 AUTHORS > AUTHORS.tmp
git log --format='%aN <%aE>' >> AUTHORS.tmp
echo "List of xterm.js contributors. Updated before every release.\n" > AUTHORS
sort -u AUTHORS.tmp >> AUTHORS
rm -f AUTHORS.tmp
+3
View File
@@ -17,6 +17,9 @@ CURRENT_PACKAGE_JSON_VERSION=$(cat package.json \
export BUILD_DIR=dist export BUILD_DIR=dist
npm run build npm run build
# Update AUTHORS file
sh bin/generate-authors
# Update version in package.json # Update version in package.json
sed -i "s/\"version\": \"$CURRENT_PACKAGE_JSON_VERSION\"/\"version\": \"$NEW_VERSION\"/g" package.json sed -i "s/\"version\": \"$CURRENT_PACKAGE_JSON_VERSION\"/\"version\": \"$NEW_VERSION\"/g" package.json
+88
View File
@@ -0,0 +1,88 @@
var express = require('express');
var app = express();
var expressWs = require('express-ws')(app);
var os = require('os');
var pty = require('node-pty');
var terminals = {},
logs = {};
app.use('/build', express.static(__dirname + '/../build'));
app.get('/', function(req, res){
res.sendFile(__dirname + '/index.html');
});
app.get('/style.css', function(req, res){
res.sendFile(__dirname + '/style.css');
});
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) {
var cols = parseInt(req.query.cols),
rows = parseInt(req.query.rows),
term = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : 'bash', [], {
name: 'xterm-color',
cols: cols || 80,
rows: rows || 24,
cwd: process.env.PWD,
env: process.env
});
console.log('Created terminal with PID: ' + term.pid);
terminals[term.pid] = term;
logs[term.pid] = '';
term.on('data', function(data) {
logs[term.pid] += data;
});
res.send(term.pid.toString());
res.end();
});
app.post('/terminals/:pid/size', function (req, res) {
var pid = parseInt(req.params.pid),
cols = parseInt(req.query.cols),
rows = parseInt(req.query.rows),
term = terminals[pid];
term.resize(cols, rows);
console.log('Resized terminal ' + pid + ' to ' + cols + ' cols and ' + rows + ' rows.');
res.end();
});
app.ws('/terminals/:pid', function (ws, req) {
var term = terminals[parseInt(req.params.pid)];
console.log('Connected to terminal ' + term.pid);
ws.send(logs[term.pid]);
term.on('data', function(data) {
try {
ws.send(data);
} catch (ex) {
// The WebSocket is not open, ignore
}
});
ws.on('message', function(msg) {
term.write(msg);
});
ws.on('close', function () {
term.kill();
console.log('Closed terminal ' + term.pid);
// Clean things up
delete terminals[term.pid];
delete logs[term.pid];
});
});
var port = process.env.PORT || 3000,
host = os.platform() === 'win32' ? '127.0.0.1' : '0.0.0.0';
console.log('App listening to http://' + host + ':' + port);
app.listen(port, host);
+1 -4
View File
@@ -16,9 +16,6 @@
<p> <p>
<label>Find next <input id="find-next"/></label> <label>Find next <input id="find-next"/></label>
<label>Find previous <input id="find-previous"/></label> <label>Find previous <input id="find-previous"/></label>
<label>Use regex<input type="checkbox" id="regex"/></label>
<label>Case sensitive<input type="checkbox" id="case-sensitive"/></label>
<label>Whole word<input type="checkbox" id="whole-word"/></label>
</p> </p>
</div> </div>
<div> <div>
@@ -36,6 +33,6 @@
<hr/> <hr/>
<p><strong>Attention:</strong> The demo is a barebones implementation and is designed for the development and evaluation of xterm.js only. Exposing the demo to the public as is would introduce security risks for the host.</p> <p><strong>Attention:</strong> The demo is a barebones implementation and is designed for the development and evaluation of xterm.js only. Exposing the demo to the public as is would introduce security risks for the host.</p>
<button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button> <button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button>
<script src="dist/client-bundle.js" defer ></script> <script src="dist/bundle.js" defer ></script>
</body> </body>
</html> </html>
+77 -97
View File
@@ -1,29 +1,11 @@
/** import * as Terminal from '../build/xterm';
* Copyright (c) 2018 The xterm.js authors. All rights reserved. import * as attach from '../build/addons/attach/attach';
* @license MIT import * as fit from '../build/addons/fit/fit';
* import * as fullscreen from '../build/addons/fullscreen/fullscreen';
* This file is the entry point for browserify. 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';
import { ISearchOptions } from '../lib/addons/search/Interfaces';
// Pulling in the module's types relies on the <reference> above, it's looks a
// little weird here as we're importing "this" module
import { Terminal as TerminalType } from 'xterm';
export interface IWindowWithTerminal extends Window {
term: TerminalType;
}
declare let window: IWindowWithTerminal;
Terminal.applyAddon(attach); Terminal.applyAddon(attach);
Terminal.applyAddon(fit); Terminal.applyAddon(fit);
@@ -33,32 +15,24 @@ Terminal.applyAddon(webLinks);
Terminal.applyAddon(winptyCompat); Terminal.applyAddon(winptyCompat);
let term; var term,
let protocol; protocol,
let socketURL; socketURL,
let socket; socket,
let pid; pid;
const terminalContainer = document.getElementById('terminal-container'); var terminalContainer = document.getElementById('terminal-container'),
const actionElements = { actionElements = {
findNext: <HTMLInputElement>document.querySelector('#find-next'), findNext: document.querySelector('#find-next'),
findPrevious: <HTMLInputElement>document.querySelector('#find-previous') findPrevious: document.querySelector('#find-previous')
}; },
const paddingElement = <HTMLInputElement>document.getElementById('padding'); paddingElement = document.getElementById('padding');
function setPadding(): void { function setPadding() {
term.element.style.padding = parseInt(paddingElement.value, 10).toString() + 'px'; term.element.style.padding = parseInt(paddingElement.value, 10).toString() + 'px';
term.fit(); term.fit();
} }
function getSearchOptions(): ISearchOptions {
return {
regex: (document.getElementById('regex') as HTMLInputElement).checked,
wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked,
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked
};
}
createTerminal(); createTerminal();
const disposeRecreateButtonHandler = () => { const disposeRecreateButtonHandler = () => {
@@ -78,20 +52,20 @@ const disposeRecreateButtonHandler = () => {
document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler); document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
function createTerminal(): void { function createTerminal() {
// Clean terminal // Clean terminal
while (terminalContainer.children.length) { while (terminalContainer.children.length) {
terminalContainer.removeChild(terminalContainer.children[0]); terminalContainer.removeChild(terminalContainer.children[0]);
} }
term = new Terminal({}); term = new Terminal({});
window.term = term; // Expose `term` to window for debugging purposes window.term = term; // Expose `term` to window for debugging purposes
term.on('resize', (size: { cols: number, rows: number }) => { term.on('resize', function (size) {
if (!pid) { if (!pid) {
return; return;
} }
const cols = size.cols; var cols = size.cols,
const rows = size.rows; rows = size.rows,
const url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows; url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows;
fetch(url, {method: 'POST'}); fetch(url, {method: 'POST'});
}); });
@@ -106,31 +80,32 @@ function createTerminal(): void {
addDomListener(paddingElement, 'change', setPadding); addDomListener(paddingElement, 'change', setPadding);
addDomListener(actionElements.findNext, 'keyup', (e) => { addDomListener(actionElements.findNext, 'keypress', function (e) {
const searchOptions = getSearchOptions(); if (e.key === "Enter") {
searchOptions.incremental = e.key !== `Enter`; e.preventDefault();
term.findNext(actionElements.findNext.value, searchOptions); term.findNext(actionElements.findNext.value);
}
}); });
addDomListener(actionElements.findPrevious, 'keypress', function (e) {
addDomListener(actionElements.findPrevious, 'keyup', (e) => { if (e.key === "Enter") {
if (e.key === `Enter`) { e.preventDefault();
term.findPrevious(actionElements.findPrevious.value, getSearchOptions()); term.findPrevious(actionElements.findPrevious.value);
} }
}); });
// fit is called within a setTimeout, cols and rows need this. // fit is called within a setTimeout, cols and rows need this.
setTimeout(() => { setTimeout(function () {
initOptions(term); initOptions(term);
// TODO: Clean this up, opt-cols/rows doesn't exist anymore document.getElementById(`opt-cols`).value = term.cols;
(<HTMLInputElement>document.getElementById(`opt-cols`)).value = term.cols; document.getElementById(`opt-rows`).value = term.rows;
(<HTMLInputElement>document.getElementById(`opt-rows`)).value = term.rows; paddingElement.value = 0;
paddingElement.value = '0';
// Set terminal size again to set the specific dimensions on the demo // Set terminal size again to set the specific dimensions on the demo
updateTerminalSize(); updateTerminalSize();
fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then((res) => { fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then(function (res) {
res.text().then((processId) => {
res.text().then(function (processId) {
pid = processId; pid = processId;
socketURL += processId; socketURL += processId;
socket = new WebSocket(socketURL); socket = new WebSocket(socketURL);
@@ -142,20 +117,22 @@ function createTerminal(): void {
}, 0); }, 0);
} }
function runRealTerminal(): void { function runRealTerminal() {
term.attach(socket); term.attach(socket);
term._initialized = true; term._initialized = true;
} }
function runFakeTerminal(): void { function runFakeTerminal() {
if (term._initialized) { if (term._initialized) {
return; return;
} }
term._initialized = true; term._initialized = true;
term.prompt = () => { var shellprompt = '$ ';
term.write('\r\n$ ');
term.prompt = function () {
term.write('\r\n' + shellprompt);
}; };
term.writeln('Welcome to xterm.js'); term.writeln('Welcome to xterm.js');
@@ -164,12 +141,14 @@ function runFakeTerminal(): void {
term.writeln(''); term.writeln('');
term.prompt(); term.prompt();
term._core.register(term.addDisposableListener('key', (key, ev) => { term._core.register(term.addDisposableListener('key', function (key, ev) {
const printable = !ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey; var printable = (
!ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey
);
if (ev.keyCode === 13) { if (ev.keyCode == 13) {
term.prompt(); term.prompt();
} else if (ev.keyCode === 8) { } else if (ev.keyCode == 8) {
// Do not delete the prompt // Do not delete the prompt
if (term.x > 2) { if (term.x > 2) {
term.write('\b \b'); term.write('\b \b');
@@ -179,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); term.write(data);
})); }));
} }
function initOptions(term: TerminalType): void { function initOptions(term) {
const blacklistedOptions = [ var blacklistedOptions = [
// Internal only options // Internal only options
'cancelEvents', 'cancelEvents',
'convertEol', 'convertEol',
@@ -195,21 +174,22 @@ function initOptions(term: TerminalType): void {
'termName', 'termName',
'useFlowControl', 'useFlowControl',
// Complex option // Complex option
'theme' 'theme',
// Only in constructor
'rendererType'
]; ];
const stringOptions = { var stringOptions = {
bellSound: null, bellSound: null,
bellStyle: ['none', 'sound'], bellStyle: ['none', 'sound'],
cursorStyle: ['block', 'underline', 'bar'], cursorStyle: ['block', 'underline', 'bar'],
experimentalCharAtlas: ['none', 'static', 'dynamic'], experimentalCharAtlas: ['none', 'static', 'dynamic'],
fontFamily: null, fontFamily: null,
fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900']
rendererType: ['dom', 'canvas']
}; };
const options = Object.keys((<any>term)._core.options); var options = Object.keys(term._core.options);
const booleanOptions = []; var booleanOptions = [];
const numberOptions = []; var numberOptions = [];
options.filter(o => blacklistedOptions.indexOf(o) === -1).forEach(o => { options.filter(o => blacklistedOptions.indexOf(o) === -1).forEach(o => {
switch (typeof term.getOption(o)) { switch (typeof term.getOption(o)) {
case 'boolean': case 'boolean':
@@ -225,49 +205,49 @@ function initOptions(term: TerminalType): void {
} }
}); });
let html = ''; var html = '';
html += '<div class="option-group">'; html += '<div class="option-group">';
booleanOptions.forEach(o => { booleanOptions.forEach(o => {
html += `<div class="option"><label><input id="opt-${o}" type="checkbox" ${term.getOption(o) ? 'checked' : ''}/> ${o}</label></div>`; html += `<div class="option"><label><input id="opt-${o}" type="checkbox" ${term.getOption(o) ? 'checked' : ''}/> ${o}</label></div>`;
}); });
html += '</div><div class="option-group">'; html += '</div><div class="option-group">';
numberOptions.forEach(o => { numberOptions.forEach(o => {
html += `<div class="option"><label>${o} <input id="opt-${o}" type="number" value="${term.getOption(o)}" step="${o === 'lineHeight' ? '0.1' : '1'}"/></label></div>`; html += `<div class="option"><label>${o} <input id="opt-${o}" type="number" value="${term.getOption(o)}"/></label></div>`;
}); });
html += '</div><div class="option-group">'; html += '</div><div class="option-group">';
Object.keys(stringOptions).forEach(o => { Object.keys(stringOptions).forEach(o => {
if (stringOptions[o]) { if (stringOptions[o]) {
html += `<div class="option"><label>${o} <select id="opt-${o}">${stringOptions[o].map(v => `<option ${term.getOption(o) === v ? 'selected' : ''}>${v}</option>`).join('')}</select></label></div>`; html += `<div class="option"><label>${o} <select id="opt-${o}">${stringOptions[o].map(v => `<option ${term.getOption(o) === v ? 'selected' : ''}>${v}</option>`).join('')}</select></label></div>`;
} else { } else {
html += `<div class="option"><label>${o} <input id="opt-${o}" type="text" value="${term.getOption(o)}"/></label></div>`; html += `<div class="option"><label>${o} <input id="opt-${o}" type="text" value="${term.getOption(o)}"/></label></div>`
} }
}); });
html += '</div>'; html += '</div>';
const container = document.getElementById('options-container'); var container = document.getElementById('options-container');
container.innerHTML = html; container.innerHTML = html;
// Attach listeners // Attach listeners
booleanOptions.forEach(o => { booleanOptions.forEach(o => {
const input = <HTMLInputElement>document.getElementById(`opt-${o}`); var input = document.getElementById(`opt-${o}`);
addDomListener(input, 'change', () => { addDomListener(input, 'change', () => {
console.log('change', o, input.checked); console.log('change', o, input.checked);
term.setOption(o, input.checked); term.setOption(o, input.checked);
}); });
}); });
numberOptions.forEach(o => { numberOptions.forEach(o => {
const input = <HTMLInputElement>document.getElementById(`opt-${o}`); var input = document.getElementById(`opt-${o}`);
addDomListener(input, 'change', () => { addDomListener(input, 'change', () => {
console.log('change', o, input.value); console.log('change', o, input.value);
if (o === 'cols' || o === 'rows') { if (o === 'cols' || o === 'rows') {
updateTerminalSize(); updateTerminalSize();
} else { } else {
term.setOption(o, o === 'lineHeight' ? parseFloat(input.value) : parseInt(input.value, 10)); term.setOption(o, parseInt(input.value, 10));
} }
}); });
}); });
Object.keys(stringOptions).forEach(o => { Object.keys(stringOptions).forEach(o => {
const input = <HTMLInputElement>document.getElementById(`opt-${o}`); var input = document.getElementById(`opt-${o}`);
addDomListener(input, 'change', () => { addDomListener(input, 'change', () => {
console.log('change', o, input.value); console.log('change', o, input.value);
term.setOption(o, input.value); term.setOption(o, input.value);
@@ -275,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); element.addEventListener(type, handler);
term._core.register({ dispose: () => element.removeEventListener(type, handler) }); term._core.register({ dispose: () => element.removeEventListener(type, handler) });
} }
function updateTerminalSize(): void { function updateTerminalSize() {
const cols = parseInt((<HTMLInputElement>document.getElementById(`opt-cols`)).value, 10); var cols = parseInt(document.getElementById(`opt-cols`).value, 10);
const rows = parseInt((<HTMLInputElement>document.getElementById(`opt-rows`)).value, 10); var rows = parseInt(document.getElementById(`opt-rows`).value, 10);
const width = (cols * term._core.renderer.dimensions.actualCellWidth + term._core.viewport.scrollBarWidth).toString() + 'px'; var width = (cols * term._core.renderer.dimensions.actualCellWidth + term._core.viewport.scrollBarWidth).toString() + 'px';
const height = (rows * term._core.renderer.dimensions.actualCellHeight).toString() + 'px'; var height = (rows * term._core.renderer.dimensions.actualCellHeight).toString() + 'px';
terminalContainer.style.width = width; terminalContainer.style.width = width;
terminalContainer.style.height = height; terminalContainer.style.height = height;
term.fit(); term.fit();
-106
View File
@@ -1,106 +0,0 @@
var express = require('express');
var expressWs = require('express-ws');
var os = require('os');
var pty = require('node-pty');
function startServer() {
var app = express();
expressWs(app);
var terminals = {},
logs = {};
app.use('/build', express.static(__dirname + '/../build'));
app.get('/', function(req, res){
res.sendFile(__dirname + '/index.html');
});
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.post('/terminals', function (req, res) {
var cols = parseInt(req.query.cols),
rows = parseInt(req.query.rows),
term = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : 'bash', [], {
name: 'xterm-color',
cols: cols || 80,
rows: rows || 24,
cwd: process.env.PWD,
env: process.env
});
console.log('Created terminal with PID: ' + term.pid);
terminals[term.pid] = term;
logs[term.pid] = '';
term.on('data', function(data) {
logs[term.pid] += data;
});
res.send(term.pid.toString());
res.end();
});
app.post('/terminals/:pid/size', function (req, res) {
var pid = parseInt(req.params.pid),
cols = parseInt(req.query.cols),
rows = parseInt(req.query.rows),
term = terminals[pid];
term.resize(cols, rows);
console.log('Resized terminal ' + pid + ' to ' + cols + ' cols and ' + rows + ' rows.');
res.end();
});
app.ws('/terminals/:pid', function (ws, req) {
var term = terminals[parseInt(req.params.pid)];
console.log('Connected to terminal ' + term.pid);
ws.send(logs[term.pid]);
function buffer(socket, timeout) {
let s = '';
let sender = null;
return (data) => {
s += data;
if (!sender) {
sender = setTimeout(() => {
socket.send(s);
s = '';
sender = null;
}, timeout);
}
};
}
const send = buffer(ws, 5);
term.on('data', function(data) {
try {
send(data);
} catch (ex) {
// The WebSocket is not open, ignore
}
});
ws.on('message', function(msg) {
term.write(msg);
});
ws.on('close', function () {
term.kill();
console.log('Closed terminal ' + term.pid);
// Clean things up
delete terminals[term.pid];
delete logs[term.pid];
});
});
var port = process.env.PORT || 3000,
host = os.platform() === 'win32' ? '127.0.0.1' : '0.0.0.0';
console.log('App listening to http://' + host + ':' + port);
app.listen(port, host);
}
module.exports = startServer;
-53
View File
@@ -1,53 +0,0 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*
* This file is the entry point for browserify.
*/
const path = require('path');
const webpack = require('webpack');
const startServer = require('./server.js');
startServer();
// 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/
},
{
test: /\.js$/,
use: ["source-map-loader"],
enforce: "pre"
}
]
},
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 -1
View File
@@ -7,7 +7,7 @@ services:
volumes: volumes:
- ./:/usr/src/app - ./:/usr/src/app
ports: ports:
- ${XTERMJS_PORT:-3000}:3000 - ${XTERMJS_PORT:3000}:3000
command: ["npm", "start"] command: ["npm", "start"]
watch: watch:
+1 -6
View File
@@ -4,11 +4,6 @@
], ],
"compilerOptions": { "compilerOptions": {
"module": "commonjs", "module": "commonjs",
"target": "es5", "target": "es5"
"lib": [
"dom",
"es6"
],
"noEmit": true
} }
} }
+1 -7
View File
@@ -4,7 +4,7 @@
/// <reference path="../../typings/xterm.d.ts" /> /// <reference path="../../typings/xterm.d.ts" />
import { Terminal, IDisposable } from 'xterm'; import { Terminal } from 'xterm';
namespace constructor { namespace constructor {
{ {
@@ -119,12 +119,6 @@ namespace methods_core {
const t: Terminal = new Terminal(); const t: Terminal = new Terminal();
t.attachCustomKeyEventHandler((e: KeyboardEvent) => true); t.attachCustomKeyEventHandler((e: KeyboardEvent) => true);
t.attachCustomKeyEventHandler((e: KeyboardEvent) => false); t.attachCustomKeyEventHandler((e: KeyboardEvent) => false);
const d1: IDisposable = t.addCsiHandler("x",
(params: number[], collect: string): boolean => params[0]===1);
d1.dispose();
const d2: IDisposable = t.addOscHandler(199,
(data: string): boolean => true);
d2.dispose();
} }
namespace options { namespace options {
{ {
+12
View File
@@ -14,6 +14,7 @@ const source = require('vinyl-source-stream');
const sourcemaps = require('gulp-sourcemaps'); const sourcemaps = require('gulp-sourcemaps');
const ts = require('gulp-typescript'); const ts = require('gulp-typescript');
const util = require('gulp-util'); const util = require('gulp-util');
const webpack = require('webpack-stream');
const buildDir = process.env.BUILD_DIR || 'build'; const buildDir = process.env.BUILD_DIR || 'build';
const tsProject = ts.createProject('tsconfig.json'); const tsProject = ts.createProject('tsconfig.json');
@@ -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('build', ['sorcery', 'sorcery-addons']);
gulp.task('test', ['mocha']); gulp.task('test', ['mocha']);
gulp.task('default', ['build']); gulp.task('default', ['build']);
+12 -16
View File
@@ -1,7 +1,7 @@
{ {
"name": "xterm", "name": "xterm",
"description": "Full xterm terminal, in your browser", "description": "Full xterm terminal, in your browser",
"version": "3.12.0", "version": "3.5.1",
"main": "lib/public/Terminal.js", "main": "lib/public/Terminal.js",
"types": "typings/xterm.d.ts", "types": "typings/xterm.d.ts",
"repository": "https://github.com/xtermjs/xterm.js", "repository": "https://github.com/xtermjs/xterm.js",
@@ -12,7 +12,6 @@
"@types/jsdom": "11.0.1", "@types/jsdom": "11.0.1",
"@types/mocha": "^2.2.33", "@types/mocha": "^2.2.33",
"@types/node": "6.0.108", "@types/node": "6.0.108",
"@types/webpack": "^4.4.11",
"browserify": "^13.3.0", "browserify": "^13.3.0",
"chai": "3.5.0", "chai": "3.5.0",
"concurrently": "^3.5.1", "concurrently": "^3.5.1",
@@ -28,42 +27,39 @@
"gulp-sourcemaps": "1.9.1", "gulp-sourcemaps": "1.9.1",
"gulp-typescript": "^3.1.3", "gulp-typescript": "^3.1.3",
"gulp-util": "3.0.8", "gulp-util": "3.0.8",
"jsdoc": "3.4.3",
"jsdom": "^11.11.0", "jsdom": "^11.11.0",
"merge-stream": "^1.0.1", "merge-stream": "^1.0.1",
"node-pty": "0.7.6", "node-pty": "0.7.6",
"nodemon": "1.10.2", "nodemon": "1.10.2",
"npm-run-all": "^4.1.2",
"nyc": "^11.8.0", "nyc": "^11.8.0",
"sorcery": "^0.10.0", "sorcery": "^0.10.0",
"source-map-loader": "^0.2.4",
"ts-loader": "^4.5.0",
"tslint": "^5.9.1", "tslint": "^5.9.1",
"tslint-consistent-codestyle": "^1.13.0", "tslint-consistent-codestyle": "^1.13.0",
"typescript": "3.1", "typescript": "2.8.3",
"vinyl-buffer": "^1.0.0", "vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0", "vinyl-source-stream": "^1.1.0",
"webpack": "^4.17.1", "webpack": "^3.10.0",
"webpack-cli": "^3.1.0", "webpack-stream": "^4.0.0",
"zmodem.js": "^0.1.5" "zmodem.js": "^0.1.5"
}, },
"scripts": { "scripts": {
"start": "node demo/start", "start": "concurrently --kill-others-on-fail --names \"demo,server\" \"gulp watch-demo\" \"node demo/app\"",
"start-debug": "node --inspect-brk demo/start",
"start-zmodem": "node demo/zmodem/app", "start-zmodem": "node demo/zmodem/app",
"lint": "tslint 'src/**/*.ts' './demo/**/*.ts'", "lint": "tslint 'src/**/*.ts'",
"pretest": "npm run layering", "test": "npm-run-all mocha lint",
"test": "npm run mocha",
"posttest": "npm run lint",
"test-debug": "node --inspect-brk node_modules/.bin/gulp test", "test-debug": "node --inspect-brk node_modules/.bin/gulp test",
"test-suite": "gulp mocha-suite --test", "test-suite": "gulp mocha-suite --test",
"test-coverage": "nyc -x gulpfile.js -x '**/*test*' npm run mocha", "test-coverage": "nyc -x gulpfile.js -x '**/*test*' npm run mocha",
"mocha": "gulp test", "mocha": "gulp test",
"build:docs": "jsdoc -c jsdoc.json",
"tsc": "tsc", "tsc": "tsc",
"prebuild": "concurrently --kill-others-on-fail --names \"lib,attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem,css\" \"tsc\" \"tsc -p ./src/addons/attach\" \"tsc -p ./src/addons/fit\" \"tsc -p ./src/addons/fullscreen\" \"tsc -p ./src/addons/search\" \"tsc -p ./src/addons/terminado\" \"tsc -p ./src/addons/webLinks\" \"tsc -p ./src/addons/winptyCompat\" \"tsc -p ./src/addons/zmodem\" \"gulp css\"", "prebuild": "concurrently --kill-others-on-fail --names \"lib,attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem,css\" \"tsc\" \"tsc -p ./src/addons/attach\" \"tsc -p ./src/addons/fit\" \"tsc -p ./src/addons/fullscreen\" \"tsc -p ./src/addons/search\" \"tsc -p ./src/addons/terminado\" \"tsc -p ./src/addons/webLinks\" \"tsc -p ./src/addons/winptyCompat\" \"tsc -p ./src/addons/zmodem\" \"gulp css\"",
"build": "gulp build", "build": "gulp build",
"prepublish": "npm run build", "prepublish": "npm run build",
"coveralls": "nyc report --reporter=text-lcov | coveralls", "coveralls": "nyc report --reporter=text-lcov | coveralls",
"watch": "concurrently --kill-others-on-fail --names \"lib,css\" \"tsc -w\" \"gulp watch-css\"", "webpack": "gulp webpack",
"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\""
"layering": "concurrently --kill-others-on-fail --names \"common,core\" \"tsc -p ./src/common\" \"tsc -p ./src/core\""
} }
} }
+3 -6
View File
@@ -5,7 +5,7 @@
import * as Strings from './Strings'; import * as Strings from './Strings';
import { ITerminal, IBuffer } from './Types'; import { ITerminal, IBuffer } from './Types';
import { isMac } from './core/Platform'; import { isMac } from './shared/utils/Browser';
import { RenderDebouncer } from './ui/RenderDebouncer'; import { RenderDebouncer } from './ui/RenderDebouncer';
import { addDisposableDomListener } from './ui/Lifecycle'; import { addDisposableDomListener } from './ui/Lifecycle';
import { Disposable } from './common/Lifecycle'; import { Disposable } from './common/Lifecycle';
@@ -72,7 +72,7 @@ export class AccessibilityManager extends Disposable {
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot); this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);
this.register(this._renderRowsDebouncer); 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('refresh', data => this._refreshRows(data.start, data.end)));
this.register(this._terminal.addDisposableListener('scroll', data => this._refreshRows())); 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 // 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(); e.stopImmediatePropagation();
} }
private _onResize(rows: number): void { private _onResize(cols: number, rows: number): void {
// Remove bottom boundary listener // Remove bottom boundary listener
this._rowElements[this._rowElements.length - 1].removeEventListener('focus', this._bottomBoundaryFocusListener); 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) { if (!this._terminal.renderer.dimensions.actualCellHeight) {
return; return;
} }
if (this._rowElements.length !== this._terminal.rows) {
this._onResize(this._terminal.rows);
}
for (let i = 0; i < this._terminal.rows; i++) { for (let i = 0; i < this._terminal.rows; i++) {
this._refreshRowDimensions(this._rowElements[i]); this._refreshRowDimensions(this._rowElements[i]);
} }
+54 -1059
View File
File diff suppressed because it is too large Load Diff
+88 -397
View File
@@ -3,31 +3,18 @@
* @license MIT * @license MIT
*/ */
import { CircularList } from './common/CircularList';
import { LineData, CharData, ITerminal, IBuffer } from './Types';
import { EventEmitter } from './EventEmitter';
import { IMarker } from 'xterm'; import { IMarker } from 'xterm';
import { BufferLine } from './BufferLine';
import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from './BufferReflow';
import { CircularList, IDeleteEvent, IInsertEvent } from './common/CircularList';
import { EventEmitter } from './common/EventEmitter';
import { DEFAULT_COLOR } from './renderer/atlas/Types';
import { BufferIndex, CharData, IBuffer, IBufferLine, IBufferStringIterator, IBufferStringIteratorResult, ITerminal } from './Types';
export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0); export const DEFAULT_ATTR = (0 << 18) | (257 << 9) | (256 << 0);
export const CHAR_DATA_ATTR_INDEX = 0; export const CHAR_DATA_ATTR_INDEX = 0;
export const CHAR_DATA_CHAR_INDEX = 1; export const CHAR_DATA_CHAR_INDEX = 1;
export const CHAR_DATA_WIDTH_INDEX = 2; export const CHAR_DATA_WIDTH_INDEX = 2;
export const CHAR_DATA_CODE_INDEX = 3; export const CHAR_DATA_CODE_INDEX = 3;
export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1 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 = 0;
export const WHITESPACE_CELL_CHAR = ' ';
export const WHITESPACE_CELL_WIDTH = 1;
export const WHITESPACE_CELL_CODE = 32;
export const FILL_CHAR_DATA: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
/** /**
* This class represents a terminal buffer (an internal state of the terminal), where the * This class represents a terminal buffer (an internal state of the terminal), where the
* following information is stored (in high-level): * following information is stored (in high-level):
@@ -36,7 +23,7 @@ export const FILL_CHAR_DATA: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL
* - scroll position * - scroll position
*/ */
export class Buffer implements IBuffer { export class Buffer implements IBuffer {
public lines: CircularList<IBufferLine>; public lines: CircularList<LineData>;
public ydisp: number; public ydisp: number;
public ybase: number; public ybase: number;
public y: number; public y: number;
@@ -46,10 +33,7 @@ export class Buffer implements IBuffer {
public tabs: any; public tabs: any;
public savedY: number; public savedY: number;
public savedX: number; public savedX: number;
public savedCurAttr: number;
public markers: Marker[] = []; public markers: Marker[] = [];
private _cols: number;
private _rows: number;
/** /**
* Create a new Buffer. * Create a new Buffer.
@@ -61,24 +45,17 @@ export class Buffer implements IBuffer {
private _terminal: ITerminal, private _terminal: ITerminal,
private _hasScrollback: boolean private _hasScrollback: boolean
) { ) {
this._cols = this._terminal.cols;
this._rows = this._terminal.rows;
this.clear(); this.clear();
} }
public getBlankLine(attr: number, isWrapped?: boolean): IBufferLine {
const fillCharData: CharData = [attr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
return new BufferLine(this._cols, fillCharData, isWrapped);
}
public get hasScrollback(): boolean { public get hasScrollback(): boolean {
return this._hasScrollback && this.lines.maxLength > this._rows; return this._hasScrollback && this.lines.maxLength > this._terminal.rows;
} }
public get isCursorInViewport(): boolean { public get isCursorInViewport(): boolean {
const absoluteY = this.ybase + this.y; const absoluteY = this.ybase + this.y;
const relativeY = absoluteY - this.ydisp; const relativeY = absoluteY - this.ydisp;
return (relativeY >= 0 && relativeY < this._rows); return (relativeY >= 0 && relativeY < this._terminal.rows);
} }
/** /**
@@ -99,14 +76,11 @@ export class Buffer implements IBuffer {
/** /**
* Fills the buffer's viewport with blank lines. * Fills the buffer's viewport with blank lines.
*/ */
public fillViewportRows(fillAttr?: number): void { public fillViewportRows(): void {
if (this.lines.length === 0) { if (this.lines.length === 0) {
if (fillAttr === undefined) { let i = this._terminal.rows;
fillAttr = DEFAULT_ATTR;
}
let i = this._rows;
while (i--) { while (i--) {
this.lines.push(this.getBlankLine(fillAttr)); this.lines.push(this._terminal.blankLine());
} }
} }
} }
@@ -119,9 +93,9 @@ export class Buffer implements IBuffer {
this.ybase = 0; this.ybase = 0;
this.y = 0; this.y = 0;
this.x = 0; this.x = 0;
this.lines = new CircularList<IBufferLine>(this._getCorrectBufferLength(this._rows)); this.lines = new CircularList<LineData>(this._getCorrectBufferLength(this._terminal.rows));
this.scrollTop = 0; this.scrollTop = 0;
this.scrollBottom = this._rows - 1; this.scrollBottom = this._terminal.rows - 1;
this.setupTabStops(); this.setupTabStops();
} }
@@ -141,17 +115,20 @@ export class Buffer implements IBuffer {
// The following adjustments should only happen if the buffer has been // The following adjustments should only happen if the buffer has been
// initialized/filled. // initialized/filled.
if (this.lines.length > 0) { if (this.lines.length > 0) {
// Deal with columns increasing (reducing needs to happen after reflow) // Deal with columns increasing (we don't do anything when columns reduce)
if (this._cols < newCols) { if (this._terminal.cols < newCols) {
const ch: CharData = [DEFAULT_ATTR, ' ', 1, 32]; // does xterm use the default attr?
for (let i = 0; i < this.lines.length; i++) { for (let i = 0; i < this.lines.length; i++) {
this.lines.get(i).resize(newCols, FILL_CHAR_DATA); while (this.lines.get(i).length < newCols) {
this.lines.get(i).push(ch);
}
} }
} }
// Resize rows in both directions as needed // Resize rows in both directions as needed
let addToY = 0; let addToY = 0;
if (this._rows < newRows) { if (this._terminal.rows < newRows) {
for (let y = this._rows; y < newRows; y++) { for (let y = this._terminal.rows; y < newRows; y++) {
if (this.lines.length < newRows + this.ybase) { if (this.lines.length < newRows + this.ybase) {
if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) { if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) {
// There is room above the buffer and there are no empty elements below the line, // There is room above the buffer and there are no empty elements below the line,
@@ -165,12 +142,12 @@ export class Buffer implements IBuffer {
} else { } else {
// Add a blank line if there is no buffer left at the top to scroll to, or if there // Add a blank line if there is no buffer left at the top to scroll to, or if there
// are blank lines after the cursor // are blank lines after the cursor
this.lines.push(new BufferLine(newCols, FILL_CHAR_DATA)); this.lines.push(this._terminal.blankLine(undefined, undefined, newCols));
} }
} }
} }
} else { // (this._rows >= newRows) } else { // (this._terminal.rows >= newRows)
for (let y = this._rows; y > newRows; y--) { for (let y = this._terminal.rows; y > newRows; y--) {
if (this.lines.length > newRows + this.ybase) { if (this.lines.length > newRows + this.ybase) {
if (this.lines.length > this.ybase + this.y + 1) { if (this.lines.length > this.ybase + this.y + 1) {
// The line is a blank line below the cursor, remove it // The line is a blank line below the cursor, remove it
@@ -210,269 +187,6 @@ export class Buffer implements IBuffer {
} }
this.scrollBottom = newRows - 1; this.scrollBottom = newRows - 1;
if (this._isReflowEnabled) {
this._reflow(newCols, newRows);
// Trim the end of the line off if cols shrunk
if (this._cols > newCols) {
for (let i = 0; i < this.lines.length; i++) {
this.lines.get(i).resize(newCols, FILL_CHAR_DATA);
}
}
}
this._cols = newCols;
this._rows = newRows;
}
private get _isReflowEnabled(): boolean {
return this._hasScrollback && !(this._terminal as any).isWinptyCompatEnabled;
}
private _reflow(newCols: number, newRows: number): void {
if (this._cols === newCols) {
return;
}
// Iterate through rows, ignore the last one as it cannot be wrapped
if (newCols > this._cols) {
this._reflowLarger(newCols, newRows);
} else {
this._reflowSmaller(newCols, newRows);
}
}
private _reflowLarger(newCols: number, newRows: number): void {
const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, newCols, this.ybase + this.y);
if (toRemove.length > 0) {
const newLayoutResult = reflowLargerCreateNewLayout(this.lines, toRemove);
reflowLargerApplyNewLayout(this.lines, newLayoutResult.layout);
this._reflowLargerAdjustViewport(newCols, newRows, newLayoutResult.countRemoved);
}
}
private _reflowLargerAdjustViewport(newCols: number, newRows: number, countRemoved: number): void {
// Adjust viewport based on number of items removed
let viewportAdjustments = countRemoved;
while (viewportAdjustments-- > 0) {
if (this.ybase === 0) {
if (this.y > 0) {
this.y--;
}
if (this.lines.length < newRows) {
// Add an extra row at the bottom of the viewport
this.lines.push(new BufferLine(newCols, FILL_CHAR_DATA));
}
} else {
if (this.ydisp === this.ybase) {
this.ydisp--;
}
this.ybase--;
}
}
}
private _reflowSmaller(newCols: number, newRows: number): void {
// Gather all BufferLines that need to be inserted into the Buffer here so that they can be
// batched up and only committed once
const toInsert = [];
let countToInsert = 0;
// Go backwards as many lines may be trimmed and this will avoid considering them
for (let y = this.lines.length - 1; y >= 0; y--) {
// Check whether this line is a problem
let nextLine = this.lines.get(y) as BufferLine;
if (!nextLine.isWrapped && nextLine.getTrimmedLength() <= newCols) {
continue;
}
// Gather wrapped lines and adjust y to be the starting line
const wrappedLines: BufferLine[] = [nextLine];
while (nextLine.isWrapped && y > 0) {
nextLine = this.lines.get(--y) as BufferLine;
wrappedLines.unshift(nextLine);
}
// If these lines contain the cursor don't touch them, the program will handle fixing up
// wrapped lines with the cursor
const absoluteY = this.ybase + this.y;
if (absoluteY >= y && absoluteY < y + wrappedLines.length) {
continue;
}
const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength();
const destLineLengths = reflowSmallerGetNewLineLengths(wrappedLines, this._cols, newCols);
const linesToAdd = destLineLengths.length - wrappedLines.length;
let trimmedLines: number;
if (this.ybase === 0 && this.y !== this.lines.length - 1) {
// If the top section of the buffer is not yet filled
trimmedLines = Math.max(0, this.y - this.lines.maxLength + linesToAdd);
} else {
trimmedLines = Math.max(0, this.lines.length - this.lines.maxLength + linesToAdd);
}
// Add the new lines
const newLines: BufferLine[] = [];
for (let i = 0; i < linesToAdd; i++) {
const newLine = this.getBlankLine(DEFAULT_ATTR, true) as BufferLine;
newLines.push(newLine);
}
if (newLines.length > 0) {
toInsert.push({
// countToInsert here gets the actual index, taking into account other inserted items.
// using this we can iterate through the list forwards
start: y + wrappedLines.length + countToInsert,
newLines
});
countToInsert += newLines.length;
}
wrappedLines.push(...newLines);
// Copy buffer data to new locations, this needs to happen backwards to do in-place
let destLineIndex = destLineLengths.length - 1; // Math.floor(cellsNeeded / newCols);
let destCol = destLineLengths[destLineIndex]; // cellsNeeded % newCols;
if (destCol === 0) {
destLineIndex--;
destCol = destLineLengths[destLineIndex];
}
let srcLineIndex = wrappedLines.length - linesToAdd - 1;
let srcCol = lastLineLength;
while (srcLineIndex >= 0) {
const cellsToCopy = Math.min(srcCol, destCol);
wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol - cellsToCopy, destCol - cellsToCopy, cellsToCopy, true);
destCol -= cellsToCopy;
if (destCol === 0) {
destLineIndex--;
destCol = destLineLengths[destLineIndex];
}
srcCol -= cellsToCopy;
if (srcCol === 0) {
srcLineIndex--;
// TODO: srcCol shoudl take trimmed length into account
srcCol = wrappedLines[Math.max(srcLineIndex, 0)].getTrimmedLength(); // this._cols;
}
}
// Null out the end of the line ends if a wide character wrapped to the following line
for (let i = 0; i < wrappedLines.length; i++) {
if (destLineLengths[i] < newCols) {
wrappedLines[i].set(destLineLengths[i], FILL_CHAR_DATA);
}
}
// Adjust viewport as needed
let viewportAdjustments = linesToAdd - trimmedLines;
while (viewportAdjustments-- > 0) {
if (this.ybase === 0) {
if (this.y < newRows - 1) {
this.y++;
this.lines.pop();
} else {
this.ybase++;
this.ydisp++;
}
} else {
// Ensure ybase does not exceed its maximum value
if (this.ybase < Math.min(this.lines.maxLength, this.lines.length + countToInsert) - newRows) {
if (this.ybase === this.ydisp) {
this.ydisp++;
}
this.ybase++;
}
}
}
}
// Rearrange lines in the buffer if there are any insertions, this is done at the end rather
// than earlier so that it's a single O(n) pass through the buffer, instead of O(n^2) from many
// costly calls to CircularList.splice.
if (toInsert.length > 0) {
// Record buffer insert events and then play them back backwards so that the indexes are
// correct
const insertEvents: IInsertEvent[] = [];
// Record original lines so they don't get overridden when we rearrange the list
const originalLines: BufferLine[] = [];
for (let i = 0; i < this.lines.length; i++) {
originalLines.push(this.lines.get(i) as BufferLine);
}
const originalLinesLength = this.lines.length;
let originalLineIndex = originalLinesLength - 1;
let nextToInsertIndex = 0;
let nextToInsert = toInsert[nextToInsertIndex];
this.lines.length = Math.min(this.lines.maxLength, this.lines.length + countToInsert);
let countInsertedSoFar = 0;
for (let i = Math.min(this.lines.maxLength - 1, originalLinesLength + countToInsert - 1); i >= 0; i--) {
if (nextToInsert && nextToInsert.start > originalLineIndex + countInsertedSoFar) {
// Insert extra lines here, adjusting i as needed
for (let nextI = nextToInsert.newLines.length - 1; nextI >= 0; nextI--) {
this.lines.set(i--, nextToInsert.newLines[nextI]);
}
i++;
// Create insert events for later
insertEvents.push({
index: originalLineIndex + 1,
amount: nextToInsert.newLines.length
} as IInsertEvent);
countInsertedSoFar += nextToInsert.newLines.length;
nextToInsert = toInsert[++nextToInsertIndex];
} else {
this.lines.set(i, originalLines[originalLineIndex--]);
}
}
// Update markers
let insertCountEmitted = 0;
for (let i = insertEvents.length - 1; i >= 0; i--) {
insertEvents[i].index += insertCountEmitted;
this.lines.emit('insert', insertEvents[i]);
insertCountEmitted += insertEvents[i].amount;
}
const amountToTrim = Math.max(0, originalLinesLength + countToInsert - this.lines.maxLength);
if (amountToTrim > 0) {
this.lines.emitMayRemoveListeners('trim', amountToTrim);
}
}
}
// private _reflowSmallerGetLinesNeeded()
/**
* 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, trimRight: boolean = false): BufferIndex {
while (stringIndex) {
const line = this.lines.get(lineIndex);
if (!line) {
return [-1, -1];
}
const length = (trimRight) ? line.getTrimmedLength() : line.length;
for (let i = 0; i < length; ++i) {
if (line.get(i)[CHAR_DATA_WIDTH_INDEX]) {
// empty cells report a string length of 0, but get replaced
// with a whitespace in translateToString, thus replace with 1
stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length || 1;
}
if (stringIndex < 0) {
return [lineIndex, i];
}
}
lineIndex++;
}
return [lineIndex, 0];
} }
/** /**
@@ -485,23 +199,76 @@ export class Buffer implements IBuffer {
* @param startCol The column to start at. * @param startCol The column to start at.
* @param endCol The column to end at. * @param endCol The column to end at.
*/ */
public translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol: number = 0, endCol?: number): string { public translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol: number = 0, endCol: number = null): string {
// Get full line
let lineString = '';
const line = this.lines.get(lineIndex); const line = this.lines.get(lineIndex);
if (!line) { if (!line) {
return ''; return '';
} }
return line.translateToString(trimRight, startCol, endCol);
// Initialize column and index values. Column values represent the actual
// cell column, indexes represent the index in the string. Indexes are
// needed here because some chars are 0 characters long (eg. after wide
// chars) and some chars are longer than 1 characters long (eg. emojis).
let startIndex = startCol;
// Only set endCol to the line length when it is null. 0 is a valid column.
if (endCol === null) {
endCol = line.length;
}
let endIndex = endCol;
for (let i = 0; i < line.length; 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
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
if (startCol >= i) {
startIndex--;
}
if (endCol > i) {
endIndex--;
}
} else {
// Adjust the columns to take glyphs that are represented by multiple
// code points into account.
if (char[CHAR_DATA_CHAR_INDEX].length > 1) {
if (startCol > i) {
startIndex += char[CHAR_DATA_CHAR_INDEX].length - 1;
}
if (endCol > i) {
endIndex += char[CHAR_DATA_CHAR_INDEX].length - 1;
}
}
}
}
// Calculate the final end col by trimming whitespace on the right of the
// line if needed.
if (trimRight) {
const rightWhitespaceIndex = lineString.search(/\s+$/);
if (rightWhitespaceIndex !== -1) {
endIndex = Math.min(endIndex, rightWhitespaceIndex);
}
// Return the empty string if only trimmed whitespace is selected
if (endIndex <= startIndex) {
return '';
}
}
return lineString.substring(startIndex, endIndex);
} }
public getWrappedRangeForLine(y: number): { first: number, last: number } { public getWrappedRangeForLine(y: number): { first: number, last: number } {
let first = y; let first = y;
let last = y; let last = y;
// Scan upwards for wrapped lines // Scan upwards for wrapped lines
while (first > 0 && this.lines.get(first).isWrapped) { while (first > 0 && (<any>this.lines.get(first)).isWrapped) {
first--; first--;
} }
// Scan downwards for wrapped lines // 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++; last++;
} }
return { first, last }; return { first, last };
@@ -512,7 +279,7 @@ export class Buffer implements IBuffer {
* @param i The index to start setting up tab stops from. * @param i The index to start setting up tab stops from.
*/ */
public setupTabStops(i?: number): void { public setupTabStops(i?: number): void {
if (i !== null && i !== undefined) { if (i != null) {
if (!this.tabs[i]) { if (!this.tabs[i]) {
i = this.prevStop(i); i = this.prevStop(i);
} }
@@ -521,7 +288,7 @@ export class Buffer implements IBuffer {
i = 0; i = 0;
} }
for (; i < this._cols; i += this._terminal.options.tabStopWidth) { for (; i < this._terminal.cols; i += this._terminal.options.tabStopWidth) {
this.tabs[i] = true; this.tabs[i] = true;
} }
} }
@@ -531,11 +298,11 @@ export class Buffer implements IBuffer {
* @param x The position to move the cursor to the previous tab stop. * @param x The position to move the cursor to the previous tab stop.
*/ */
public prevStop(x?: number): number { public prevStop(x?: number): number {
if (x === null || x === undefined) { if (x == null) {
x = this.x; x = this.x;
} }
while (!this.tabs[--x] && x > 0); while (!this.tabs[--x] && x > 0);
return x >= this._cols ? this._cols - 1 : x < 0 ? 0 : x; return x >= this._terminal.cols ? this._terminal.cols - 1 : x < 0 ? 0 : x;
} }
/** /**
@@ -543,11 +310,11 @@ export class Buffer implements IBuffer {
* @param x The position to move the cursor one tab stop forward. * @param x The position to move the cursor one tab stop forward.
*/ */
public nextStop(x?: number): number { public nextStop(x?: number): number {
if (x === null || x === undefined) { if (x == null) {
x = this.x; x = this.x;
} }
while (!this.tabs[++x] && x < this._cols); while (!this.tabs[++x] && x < this._terminal.cols);
return x >= this._cols ? this._cols - 1 : x < 0 ? 0 : x; return x >= this._terminal.cols ? this._terminal.cols - 1 : x < 0 ? 0 : x;
} }
public addMarker(y: number): Marker { public addMarker(y: number): Marker {
@@ -560,33 +327,14 @@ export class Buffer implements IBuffer {
marker.dispose(); marker.dispose();
} }
})); }));
marker.register(this.lines.addDisposableListener('insert', (event: IInsertEvent) => {
if (marker.line >= event.index) {
marker.line += event.amount;
}
}));
marker.register(this.lines.addDisposableListener('delete', (event: IDeleteEvent) => {
// Delete the marker if it's within the range
if (marker.line >= event.index && marker.line < event.index + event.amount) {
marker.dispose();
}
// Shift the marker if it's after the deleted range
if (marker.line > event.index) {
marker.line -= event.amount;
}
}));
marker.register(marker.addDisposableListener('dispose', () => this._removeMarker(marker))); marker.register(marker.addDisposableListener('dispose', () => this._removeMarker(marker)));
return marker; return marker;
} }
private _removeMarker(marker: Marker): void { private _removeMarker(marker: Marker): void {
// TODO: This could probably be optimized by relying on sort order and trimming the array using .length
this.markers.splice(this.markers.indexOf(marker), 1); this.markers.splice(this.markers.indexOf(marker), 1);
} }
public iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator {
return new BufferStringIterator(this, trimRight, startIndex, endIndex, startOverscan, endOverscan);
}
} }
export class Marker extends EventEmitter implements IMarker { export class Marker extends EventEmitter implements IMarker {
@@ -613,60 +361,3 @@ export class Marker extends EventEmitter implements IMarker {
super.dispose(); super.dispose();
} }
} }
/**
* Iterator to get unwrapped content strings from the buffer.
* The iterator returns at least the string data between the borders
* `startIndex` and `endIndex` (exclusive) and will expand the lines
* by `startOverscan` to the top and by `endOverscan` to the bottom,
* if no new line was found in between.
* It will never read/return string data beyond `startIndex - startOverscan`
* or `endIndex + endOverscan`. Therefore the first and last line might be truncated.
* It is possible to always get the full string for the first and last line as well
* by setting the overscan values to the actual buffer length. This not recommended
* since it might return the whole buffer within a single string in a worst case scenario.
*/
export class BufferStringIterator implements IBufferStringIterator {
private _current: number;
constructor (
private _buffer: IBuffer,
private _trimRight: boolean,
private _startIndex: number = 0,
private _endIndex: number = _buffer.lines.length,
private _startOverscan: number = 0,
private _endOverscan: number = 0
) {
if (this._startIndex < 0) {
this._startIndex = 0;
}
if (this._endIndex > this._buffer.lines.length) {
this._endIndex = this._buffer.lines.length;
}
this._current = this._startIndex;
}
public hasNext(): boolean {
return this._current < this._endIndex;
}
public next(): IBufferStringIteratorResult {
const range = this._buffer.getWrappedRangeForLine(this._current);
// limit search window to overscan value at both borders
if (range.first < this._startIndex - this._startOverscan) {
range.first = this._startIndex - this._startOverscan;
}
if (range.last > this._endIndex + this._endOverscan) {
range.last = this._endIndex + this._endOverscan;
}
// limit to current buffer length
range.first = Math.max(range.first, 0);
range.last = Math.min(range.last, this._buffer.lines.length);
let result = '';
for (let i = range.first; i <= range.last; ++i) {
result += this._buffer.translateBufferLineToString(i, this._trimRight);
}
this._current = range.last + 1;
return {range: range, content: result};
}
}
-305
View File
@@ -1,305 +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, DEFAULT_ATTR } from './Buffer';
class TestBufferLine extends BufferLine {
public get combined(): {[index: number]: string} {
return this._combined;
}
public toArray(): CharData[] {
const result = [];
for (let i = 0; i < this.length; ++i) {
result.push(this.get(i));
}
return result;
}
}
describe('BufferLine', function(): void {
it('ctor', function(): void {
let line: IBufferLine = new TestBufferLine(0);
chai.expect(line.length).equals(0);
chai.expect(line.isWrapped).equals(false);
line = new TestBufferLine(10);
chai.expect(line.length).equals(10);
chai.expect(line.get(0)).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.get(0)).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
chai.expect(line.isWrapped).equals(true);
line = new TestBufferLine(10, [123, 'a', 456, 'a'.charCodeAt(0)], true);
chai.expect(line.length).equals(10);
chai.expect(line.get(0)).eql([123, 'a', 456, 'a'.charCodeAt(0)]);
chai.expect(line.isWrapped).equals(true);
});
it('insertCells', function(): void {
const line = new TestBufferLine(3);
line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]);
line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]);
line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]);
line.insertCells(1, 3, [4, 'd', 0, 'd'.charCodeAt(0)]);
chai.expect(line.toArray()).eql([
[1, 'a', 0, 'a'.charCodeAt(0)],
[4, 'd', 0, 'd'.charCodeAt(0)],
[4, 'd', 0, 'd'.charCodeAt(0)]
]);
});
it('deleteCells', function(): void {
const line = new TestBufferLine(5);
line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]);
line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]);
line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]);
line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]);
line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]);
line.deleteCells(1, 2, [6, 'f', 0, 'f'.charCodeAt(0)]);
chai.expect(line.toArray()).eql([
[1, 'a', 0, 'a'.charCodeAt(0)],
[4, 'd', 0, 'd'.charCodeAt(0)],
[5, 'e', 0, 'e'.charCodeAt(0)],
[6, 'f', 0, 'f'.charCodeAt(0)],
[6, 'f', 0, 'f'.charCodeAt(0)]
]);
});
it('replaceCells', function(): void {
const line = new TestBufferLine(5);
line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]);
line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]);
line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]);
line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]);
line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]);
line.replaceCells(2, 4, [6, 'f', 0, 'f'.charCodeAt(0)]);
chai.expect(line.toArray()).eql([
[1, 'a', 0, 'a'.charCodeAt(0)],
[2, 'b', 0, 'b'.charCodeAt(0)],
[6, 'f', 0, 'f'.charCodeAt(0)],
[6, 'f', 0, 'f'.charCodeAt(0)],
[5, 'e', 0, 'e'.charCodeAt(0)]
]);
});
it('fill', function(): void {
const line = new TestBufferLine(5);
line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]);
line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]);
line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]);
line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]);
line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]);
line.fill([123, 'z', 0, 'z'.charCodeAt(0)]);
chai.expect(line.toArray()).eql([
[123, 'z', 0, 'z'.charCodeAt(0)],
[123, 'z', 0, 'z'.charCodeAt(0)],
[123, 'z', 0, 'z'.charCodeAt(0)],
[123, 'z', 0, 'z'.charCodeAt(0)],
[123, 'z', 0, 'z'.charCodeAt(0)]
]);
});
it('clone', function(): void {
const line = new TestBufferLine(5, null, true);
line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]);
line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]);
line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]);
line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]);
line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]);
const line2 = line.clone();
chai.expect(TestBufferLine.prototype.toArray.apply(line2)).eql(line.toArray());
chai.expect(line2.length).equals(line.length);
chai.expect(line2.isWrapped).equals(line.isWrapped);
});
it('copyFrom', function(): void {
const line = new TestBufferLine(5);
line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]);
line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]);
line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]);
line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]);
line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]);
const line2 = new TestBufferLine(5, [1, 'a', 0, 'a'.charCodeAt(0)], true);
line2.copyFrom(line);
chai.expect(line2.toArray()).eql(line.toArray());
chai.expect(line2.length).equals(line.length);
chai.expect(line2.isWrapped).equals(line.isWrapped);
});
it('should support combining chars', function(): void {
// CHAR_DATA_CODE_INDEX resembles current behavior in InputHandler.print
// --> set code to the last charCodeAt value of the string
// Note: needs to be fixed once the string pointer is in place
const line = new TestBufferLine(2, [1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]);
chai.expect(line.toArray()).eql([[1, 'e\u0301', 0, '\u0301'.charCodeAt(0)], [1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]]);
const line2 = new TestBufferLine(5, [1, 'a', 0, '\u0301'.charCodeAt(0)], true);
line2.copyFrom(line);
chai.expect(line2.toArray()).eql(line.toArray());
const line3 = line.clone();
chai.expect(TestBufferLine.prototype.toArray.apply(line3)).eql(line.toArray());
});
describe('resize', function(): void {
it('enlarge(false)', function(): void {
const line = new TestBufferLine(5, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)]);
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('enlarge(true)', function(): void {
const line = new TestBufferLine(5, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)]);
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('shrink(true) - should apply new size', function(): void {
const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)]);
chai.expect(line.toArray()).eql(Array(5).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('shrink to 0 length', function(): void {
const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.resize(0, [1, 'a', 0, 'a'.charCodeAt(0)]);
chai.expect(line.toArray()).eql(Array(0).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('should remove combining data on replaced cells after shrinking then enlarging', () => {
const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
line.set(2, [ null, '😁', 1, '😁'.charCodeAt(0) ]);
line.set(9, [ null, '😁', 1, '😁'.charCodeAt(0) ]);
chai.expect(line.translateToString()).eql('aa😁aaaaaa😁');
chai.expect(Object.keys(line.combined).length).eql(2);
line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)]);
chai.expect(line.translateToString()).eql('aa😁aa');
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)]);
chai.expect(line.translateToString()).eql('aa😁aaaaaaa');
chai.expect(Object.keys(line.combined).length).eql(1);
});
});
describe('getTrimLength', function(): void {
it('empty line', function(): void {
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
chai.expect(line.getTrimmedLength()).equal(0);
});
it('ASCII', function(): void {
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
line.set(2, [1, 'a', 1, 'a'.charCodeAt(0)]);
chai.expect(line.getTrimmedLength()).equal(3);
});
it('surrogate', function(): void {
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
line.set(2, [1, '𝄞', 1, '𝄞'.charCodeAt(0)]);
chai.expect(line.getTrimmedLength()).equal(3);
});
it('combining', function(): void {
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
line.set(2, [1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]);
chai.expect(line.getTrimmedLength()).equal(3);
});
it('fullwidth', function(): void {
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
line.set(2, [1, '1', 2, '1'.charCodeAt(0)]);
line.set(3, [0, '', 0, undefined]);
chai.expect(line.getTrimmedLength()).equal(4); // also counts null cell after fullwidth
});
});
describe('translateToString with and w\'o trimming', function(): void {
it('empty line', function(): void {
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
chai.expect(line.translateToString(false)).equal(' ');
chai.expect(line.translateToString(true)).equal('');
});
it('ASCII', function(): void {
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
line.set(2, [1, 'a', 1, 'a'.charCodeAt(0)]);
line.set(4, [1, 'a', 1, 'a'.charCodeAt(0)]);
line.set(5, [1, 'a', 1, 'a'.charCodeAt(0)]);
chai.expect(line.translateToString(false)).equal('a a aa ');
chai.expect(line.translateToString(true)).equal('a a aa');
chai.expect(line.translateToString(false, 0, 5)).equal('a a a');
chai.expect(line.translateToString(false, 0, 4)).equal('a a ');
chai.expect(line.translateToString(false, 0, 3)).equal('a a');
chai.expect(line.translateToString(true, 0, 5)).equal('a a a');
chai.expect(line.translateToString(true, 0, 4)).equal('a a ');
chai.expect(line.translateToString(true, 0, 3)).equal('a a');
});
it('surrogate', function(): void {
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
line.set(2, [1, '𝄞', 1, '𝄞'.charCodeAt(0)]);
line.set(4, [1, '𝄞', 1, '𝄞'.charCodeAt(0)]);
line.set(5, [1, '𝄞', 1, '𝄞'.charCodeAt(0)]);
chai.expect(line.translateToString(false)).equal('a 𝄞 𝄞𝄞 ');
chai.expect(line.translateToString(true)).equal('a 𝄞 𝄞𝄞');
chai.expect(line.translateToString(false, 0, 5)).equal('a 𝄞 𝄞');
chai.expect(line.translateToString(false, 0, 4)).equal('a 𝄞 ');
chai.expect(line.translateToString(false, 0, 3)).equal('a 𝄞');
chai.expect(line.translateToString(true, 0, 5)).equal('a 𝄞 𝄞');
chai.expect(line.translateToString(true, 0, 4)).equal('a 𝄞 ');
chai.expect(line.translateToString(true, 0, 3)).equal('a 𝄞');
});
it('combining', function(): void {
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
line.set(2, [1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]);
line.set(4, [1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]);
line.set(5, [1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]);
chai.expect(line.translateToString(false)).equal('a e\u0301 e\u0301e\u0301 ');
chai.expect(line.translateToString(true)).equal('a e\u0301 e\u0301e\u0301');
chai.expect(line.translateToString(false, 0, 5)).equal('a e\u0301 e\u0301');
chai.expect(line.translateToString(false, 0, 4)).equal('a e\u0301 ');
chai.expect(line.translateToString(false, 0, 3)).equal('a e\u0301');
chai.expect(line.translateToString(true, 0, 5)).equal('a e\u0301 e\u0301');
chai.expect(line.translateToString(true, 0, 4)).equal('a e\u0301 ');
chai.expect(line.translateToString(true, 0, 3)).equal('a e\u0301');
});
it('fullwidth', function(): void {
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
line.set(2, [1, '1', 2, '1'.charCodeAt(0)]);
line.set(3, [0, '', 0, undefined]);
line.set(5, [1, '1', 2, '1'.charCodeAt(0)]);
line.set(6, [0, '', 0, undefined]);
line.set(7, [1, '1', 2, '1'.charCodeAt(0)]);
line.set(8, [0, '', 0, undefined]);
chai.expect(line.translateToString(false)).equal('a 1 11 ');
chai.expect(line.translateToString(true)).equal('a 1 11');
chai.expect(line.translateToString(false, 0, 7)).equal('a 1 1');
chai.expect(line.translateToString(false, 0, 6)).equal('a 1 1');
chai.expect(line.translateToString(false, 0, 5)).equal('a 1 ');
chai.expect(line.translateToString(false, 0, 4)).equal('a 1');
chai.expect(line.translateToString(false, 0, 3)).equal('a 1');
chai.expect(line.translateToString(false, 0, 2)).equal('a ');
chai.expect(line.translateToString(true, 0, 7)).equal('a 1 1');
chai.expect(line.translateToString(true, 0, 6)).equal('a 1 1');
chai.expect(line.translateToString(true, 0, 5)).equal('a 1 ');
chai.expect(line.translateToString(true, 0, 4)).equal('a 1');
chai.expect(line.translateToString(true, 0, 3)).equal('a 1');
chai.expect(line.translateToString(true, 0, 2)).equal('a ');
});
it('space at end', function(): void {
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false);
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
line.set(2, [1, 'a', 1, 'a'.charCodeAt(0)]);
line.set(4, [1, 'a', 1, 'a'.charCodeAt(0)]);
line.set(5, [1, 'a', 1, 'a'.charCodeAt(0)]);
line.set(6, [1, ' ', 1, ' '.charCodeAt(0)]);
chai.expect(line.translateToString(false)).equal('a a aa ');
chai.expect(line.translateToString(true)).equal('a a aa ');
});
it('should always return some sane value', function(): void {
// sanity check - broken line with invalid out of bound null width cells
// this can atm happen with deleting/inserting chars in inputhandler by "breaking"
// fullwidth pairs --> needs to be fixed after settling BufferLine impl
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE], false);
chai.expect(line.translateToString(false)).equal(' ');
chai.expect(line.translateToString(true)).equal('');
});
it('should work with endCol=0', () => {
const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE], false);
line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]);
chai.expect(line.translateToString(true, 0, 0)).equal('');
});
});
});
-233
View File
@@ -1,233 +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, WHITESPACE_CELL_CHAR } from './Buffer';
/** typed array slots taken by one cell */
const CELL_SIZE = 3;
/** cell member indices */
const enum Cell {
FLAGS = 0,
STRING = 1,
WIDTH = 2
}
/** single vs. combined char distinction */
const IS_COMBINED_BIT_MASK = 0x80000000;
/**
* Typed array based bufferline implementation.
*/
export class BufferLine implements IBufferLine {
protected _data: Uint32Array | null = null;
protected _combined: {[index: number]: string} = {};
public length: number;
constructor(cols: number, fillCharData?: CharData, public isWrapped: boolean = false) {
if (!fillCharData) {
fillCharData = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
}
if (cols) {
this._data = new Uint32Array(cols * CELL_SIZE);
for (let i = 0; i < cols; ++i) {
this.set(i, fillCharData);
}
}
this.length = cols;
}
public get(index: number): CharData {
const stringData = this._data[index * CELL_SIZE + Cell.STRING];
return [
this._data[index * CELL_SIZE + Cell.FLAGS],
(stringData & IS_COMBINED_BIT_MASK)
? this._combined[index]
: (stringData) ? String.fromCharCode(stringData) : '',
this._data[index * CELL_SIZE + Cell.WIDTH],
(stringData & IS_COMBINED_BIT_MASK)
? this._combined[index].charCodeAt(this._combined[index].length - 1)
: stringData
];
}
public getWidth(index: number): number {
return this._data[index * CELL_SIZE + Cell.WIDTH];
}
public set(index: number, value: CharData): void {
this._data[index * CELL_SIZE + Cell.FLAGS] = value[0];
if (value[1].length > 1) {
this._combined[index] = value[1];
this._data[index * CELL_SIZE + Cell.STRING] = index | IS_COMBINED_BIT_MASK;
} else {
this._data[index * CELL_SIZE + Cell.STRING] = value[1].charCodeAt(0);
}
this._data[index * CELL_SIZE + Cell.WIDTH] = value[2];
}
public insertCells(pos: number, n: number, fillCharData: CharData): void {
pos %= this.length;
if (n < this.length - pos) {
for (let i = this.length - pos - n - 1; i >= 0; --i) {
this.set(pos + n + i, this.get(pos + i));
}
for (let i = 0; i < n; ++i) {
this.set(pos + i, fillCharData);
}
} else {
for (let i = pos; i < this.length; ++i) {
this.set(i, fillCharData);
}
}
}
public deleteCells(pos: number, n: number, fillCharData: CharData): void {
pos %= this.length;
if (n < this.length - pos) {
for (let i = 0; i < this.length - pos - n; ++i) {
this.set(pos + i, this.get(pos + n + i));
}
for (let i = this.length - n; i < this.length; ++i) {
this.set(i, fillCharData);
}
} else {
for (let i = pos; i < this.length; ++i) {
this.set(i, fillCharData);
}
}
}
public replaceCells(start: number, end: number, fillCharData: CharData): void {
while (start < end && start < this.length) {
this.set(start++, fillCharData);
}
}
public resize(cols: number, fillCharData: CharData): void {
if (cols === this.length) {
return;
}
if (cols > this.length) {
const data = new Uint32Array(cols * CELL_SIZE);
if (this.length) {
if (cols * CELL_SIZE < this._data.length) {
data.set(this._data.subarray(0, cols * CELL_SIZE));
} else {
data.set(this._data);
}
}
this._data = data;
for (let i = this.length; i < cols; ++i) {
this.set(i, fillCharData);
}
} else {
if (cols) {
const data = new Uint32Array(cols * CELL_SIZE);
data.set(this._data.subarray(0, cols * CELL_SIZE));
this._data = data;
// Remove any cut off combined data
const keys = Object.keys(this._combined);
for (let i = 0; i < keys.length; i++) {
const key = parseInt(keys[i], 10);
if (key >= cols) {
delete this._combined[key];
}
}
} else {
this._data = null;
this._combined = {};
}
}
this.length = cols;
}
/** fill a line with fillCharData */
public fill(fillCharData: CharData): void {
this._combined = {};
for (let i = 0; i < this.length; ++i) {
this.set(i, fillCharData);
}
}
/** alter to a full copy of line */
public copyFrom(line: BufferLine): void {
if (this.length !== line.length) {
this._data = new Uint32Array(line._data);
} else {
// use high speed copy if lengths are equal
this._data.set(line._data);
}
this.length = line.length;
this._combined = {};
for (const el in line._combined) {
this._combined[el] = line._combined[el];
}
this.isWrapped = line.isWrapped;
}
/** create a new clone */
public clone(): IBufferLine {
const newLine = new BufferLine(0);
// creation of new typed array from another is actually pretty slow :(
// still faster than copying values one by one
newLine._data = new Uint32Array(this._data);
newLine.length = this.length;
for (const el in this._combined) {
newLine._combined[el] = this._combined[el];
}
newLine.isWrapped = this.isWrapped;
return newLine;
}
public getTrimmedLength(): number {
for (let i = this.length - 1; i >= 0; --i) {
if (this._data[i * CELL_SIZE + Cell.STRING] !== 0) { // 0 ==> ''.charCodeAt(0) ==> NaN ==> 0
return i + this._data[i * CELL_SIZE + Cell.WIDTH];
}
}
return 0;
}
public copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number, applyInReverse: boolean): void {
const srcData = src._data;
if (applyInReverse) {
for (let cell = length - 1; cell >= 0; cell--) {
for (let i = 0; i < CELL_SIZE; i++) {
this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i];
}
}
} else {
for (let cell = 0; cell < length; cell++) {
for (let i = 0; i < CELL_SIZE; i++) {
this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i];
}
}
}
// Move any combined data over as needed
const srcCombinedKeys = Object.keys(src._combined);
for (let i = 0; i < srcCombinedKeys.length; i++) {
const key = parseInt(srcCombinedKeys[i], 10);
if (key >= srcCol) {
this._combined[key - srcCol + destCol] = src._combined[key];
}
}
}
public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length): string {
if (trimRight) {
endCol = Math.min(endCol, this.getTrimmedLength());
}
let result = '';
while (startCol < endCol) {
const stringData = this._data[startCol * CELL_SIZE + Cell.STRING];
result += (stringData & IS_COMBINED_BIT_MASK) ? this._combined[startCol] : (stringData) ? String.fromCharCode(stringData) : WHITESPACE_CELL_CHAR;
startCol += this._data[startCol * CELL_SIZE + Cell.WIDTH] || 1;
}
return result;
}
}
-93
View File
@@ -1,93 +0,0 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { BufferLine } from './BufferLine';
import { reflowSmallerGetNewLineLengths } from './BufferReflow';
import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer';
describe('BufferReflow', () => {
describe('reflowSmallerGetNewLineLengths', () => {
it('should return correct line lengths for a small line with wide characters', () => {
const line = new BufferLine(4);
line.set(0, [null, '汉', 2, '汉'.charCodeAt(0)]);
line.set(1, [null, '', 0, undefined]);
line.set(2, [null, '语', 2, '语'.charCodeAt(0)]);
line.set(3, [null, '', 0, undefined]);
assert.equal(line.translateToString(true), '汉语');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 4, 3), [2, 2], 'line: 汉, 语');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 4, 2), [2, 2], 'line: 汉, 语');
});
it('should return correct line lengths for a large line with wide characters', () => {
const line = new BufferLine(12);
for (let i = 0; i < 12; i += 4) {
line.set(i, [null, '汉', 2, '汉'.charCodeAt(0)]);
line.set(i + 2, [null, '语', 2, '语'.charCodeAt(0)]);
}
for (let i = 1; i < 12; i += 2) {
line.set(i, [null, '', 0, undefined]);
line.set(i, [null, '', 0, undefined]);
}
assert.equal(line.translateToString(), '汉语汉语汉语');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 11), [10, 2], 'line: 汉语汉语汉, 语');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 10), [10, 2], 'line: 汉语汉语汉, 语');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 9), [8, 4], 'line: 汉语汉语, 汉语');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 8), [8, 4], 'line: 汉语汉语, 汉语');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 7), [6, 6], 'line: 汉语汉, 语汉语');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 6), [6, 6], 'line: 汉语汉, 语汉语');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 5), [4, 4, 4], 'line: 汉语, 汉语, 汉语');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 4), [4, 4, 4], 'line: 汉语, 汉语, 汉语');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 3), [2, 2, 2, 2, 2, 2], 'line: 汉, 语, 汉, 语, 汉, 语');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 2), [2, 2, 2, 2, 2, 2], 'line: 汉, 语, 汉, 语, 汉, 语');
});
it('should return correct line lengths for a string with wide and single characters', () => {
const line = new BufferLine(6);
line.set(0, [null, 'a', 1, 'a'.charCodeAt(0)]);
line.set(1, [null, '汉', 2, '汉'.charCodeAt(0)]);
line.set(2, [null, '', 0, undefined]);
line.set(3, [null, '语', 2, '语'.charCodeAt(0)]);
line.set(4, [null, '', 0, undefined]);
line.set(5, [null, 'b', 1, 'b'.charCodeAt(0)]);
assert.equal(line.translateToString(), 'a汉语b');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 6, 5), [5, 1], 'line: a汉语b');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 6, 4), [3, 3], 'line: a汉, 语b');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 6, 3), [3, 3], 'line: a汉, 语b');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 6, 2), [1, 2, 2, 1], 'line: a, 汉, 语, b');
});
it('should return correct line lengths for a wrapped line with wide and single characters', () => {
const line1 = new BufferLine(6);
line1.set(0, [null, 'a', 1, 'a'.charCodeAt(0)]);
line1.set(1, [null, '汉', 2, '汉'.charCodeAt(0)]);
line1.set(2, [null, '', 0, undefined]);
line1.set(3, [null, '语', 2, '语'.charCodeAt(0)]);
line1.set(4, [null, '', 0, undefined]);
line1.set(5, [null, 'b', 1, 'b'.charCodeAt(0)]);
const line2 = new BufferLine(6, undefined, true);
line2.set(0, [null, 'a', 1, 'a'.charCodeAt(0)]);
line2.set(1, [null, '汉', 2, '汉'.charCodeAt(0)]);
line2.set(2, [null, '', 0, undefined]);
line2.set(3, [null, '语', 2, '语'.charCodeAt(0)]);
line2.set(4, [null, '', 0, undefined]);
line2.set(5, [null, 'b', 1, 'b'.charCodeAt(0)]);
assert.equal(line1.translateToString(), 'a汉语b');
assert.equal(line2.translateToString(), 'a汉语b');
assert.deepEqual(reflowSmallerGetNewLineLengths([line1, line2], 6, 5), [5, 4, 3], 'lines: a汉语, ba汉, 语b');
assert.deepEqual(reflowSmallerGetNewLineLengths([line1, line2], 6, 4), [3, 4, 4, 1], 'lines: a汉, 语ba, 汉语, b');
assert.deepEqual(reflowSmallerGetNewLineLengths([line1, line2], 6, 3), [3, 3, 3, 3], 'lines: a汉, 语b, a汉, 语b');
assert.deepEqual(reflowSmallerGetNewLineLengths([line1, line2], 6, 2), [1, 2, 2, 2, 2, 2, 1], 'lines: a, 汉, 语, ba, 汉, 语, b');
});
it('should work on lines ending in null space', () => {
const line = new BufferLine(5);
line.set(0, [null, '汉', 2, '汉'.charCodeAt(0)]);
line.set(1, [null, '', 0, undefined]);
line.set(2, [null, '语', 2, '语'.charCodeAt(0)]);
line.set(3, [null, '', 0, undefined]);
line.set(4, [null, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
assert.equal(line.translateToString(true), '汉语');
assert.equal(line.translateToString(false), '汉语 ');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 4, 3), [2, 2], 'line: 汉, 语');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 4, 2), [2, 2], 'line: 汉, 语');
});
});
});
-205
View File
@@ -1,205 +0,0 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { FILL_CHAR_DATA } from './Buffer';
import { BufferLine } from './BufferLine';
import { CircularList, IDeleteEvent } from './common/CircularList';
import { IBufferLine } from './Types';
export interface INewLayoutResult {
layout: number[];
countRemoved: number;
}
/**
* Evaluates and returns indexes to be removed after a reflow larger occurs. Lines will be removed
* when a wrapped line unwraps.
* @param lines The buffer lines.
* @param newCols The columns after resize.
*/
export function reflowLargerGetLinesToRemove(lines: CircularList<IBufferLine>, newCols: number, bufferAbsoluteY: number): number[] {
// Gather all BufferLines that need to be removed from the Buffer here so that they can be
// batched up and only committed once
const toRemove: number[] = [];
for (let y = 0; y < lines.length - 1; y++) {
// Check if this row is wrapped
let i = y;
let nextLine = lines.get(++i) as BufferLine;
if (!nextLine.isWrapped) {
continue;
}
// Check how many lines it's wrapped for
const wrappedLines: BufferLine[] = [lines.get(y) as BufferLine];
while (i < lines.length && nextLine.isWrapped) {
wrappedLines.push(nextLine);
nextLine = lines.get(++i) as BufferLine;
}
// If these lines contain the cursor don't touch them, the program will handle fixing up wrapped
// lines with the cursor
if (bufferAbsoluteY >= y && bufferAbsoluteY < i) {
y += wrappedLines.length - 1;
continue;
}
// Copy buffer data to new locations
let destLineIndex = 0;
let destCol = wrappedLines[destLineIndex].getTrimmedLength();
let srcLineIndex = 1;
let srcCol = 0;
while (srcLineIndex < wrappedLines.length) {
const srcTrimmedTineLength = wrappedLines[srcLineIndex].getTrimmedLength();
const srcRemainingCells = srcTrimmedTineLength - srcCol;
const destRemainingCells = newCols - destCol;
const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells);
wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy, false);
destCol += cellsToCopy;
if (destCol === newCols) {
destLineIndex++;
destCol = 0;
}
srcCol += cellsToCopy;
if (srcCol === srcTrimmedTineLength) {
srcLineIndex++;
srcCol = 0;
}
// Make sure the last cell isn't wide, if it is copy it to the current dest
if (destCol === 0 && destLineIndex !== 0) {
if (wrappedLines[destLineIndex - 1].getWidth(newCols - 1) === 2) {
wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[destLineIndex - 1], newCols - 1, destCol++, 1, false);
// Null out the end of the last row
wrappedLines[destLineIndex - 1].set(newCols - 1, FILL_CHAR_DATA);
}
}
}
// Clear out remaining cells or fragments could remain;
wrappedLines[destLineIndex].replaceCells(destCol, newCols, FILL_CHAR_DATA);
// Work backwards and remove any rows at the end that only contain null cells
let countToRemove = 0;
for (let i = wrappedLines.length - 1; i > 0; i--) {
if (i > destLineIndex || wrappedLines[i].getTrimmedLength() === 0) {
countToRemove++;
} else {
break;
}
}
if (countToRemove > 0) {
toRemove.push(y + wrappedLines.length - countToRemove); // index
toRemove.push(countToRemove);
}
y += wrappedLines.length - 1;
}
return toRemove;
}
/**
* Creates and return the new layout for lines given an array of indexes to be removed.
* @param lines The buffer lines.
* @param toRemove The indexes to remove.
*/
export function reflowLargerCreateNewLayout(lines: CircularList<IBufferLine>, toRemove: number[]): INewLayoutResult {
const layout: number[] = [];
// First iterate through the list and get the actual indexes to use for rows
let nextToRemoveIndex = 0;
let nextToRemoveStart = toRemove[nextToRemoveIndex];
let countRemovedSoFar = 0;
for (let i = 0; i < lines.length; i++) {
if (nextToRemoveStart === i) {
const countToRemove = toRemove[++nextToRemoveIndex];
// Tell markers that there was a deletion
lines.emit('delete', {
index: i - countRemovedSoFar,
amount: countToRemove
} as IDeleteEvent);
i += countToRemove - 1;
countRemovedSoFar += countToRemove;
nextToRemoveStart = toRemove[++nextToRemoveIndex];
} else {
layout.push(i);
}
}
return {
layout,
countRemoved: countRemovedSoFar
};
}
/**
* Applies a new layout to the buffer. This essentially does the same as many splice calls but it's
* done all at once in a single iteration through the list since splice is very expensive.
* @param lines The buffer lines.
* @param newLayout The new layout to apply.
*/
export function reflowLargerApplyNewLayout(lines: CircularList<IBufferLine>, newLayout: number[]): void {
// Record original lines so they don't get overridden when we rearrange the list
const newLayoutLines: BufferLine[] = [];
for (let i = 0; i < newLayout.length; i++) {
newLayoutLines.push(lines.get(newLayout[i]) as BufferLine);
}
// Rearrange the list
for (let i = 0; i < newLayoutLines.length; i++) {
lines.set(i, newLayoutLines[i]);
}
lines.length = newLayout.length;
}
/**
* Gets the new line lengths for a given wrapped line. The purpose of this function it to pre-
* compute the wrapping points since wide characters may need to be wrapped onto the following line.
* This function will return an array of numbers of where each line wraps to, the resulting array
* will only contain the values `newCols` (when the line does not end with a wide character) and
* `newCols - 1` (when the line does end with a wide character), except for the last value which
* will contain the remaining items to fill the line.
*
* Calling this with a `newCols` value of `1` will lock up.
*
* @param wrappedLines The wrapped lines to evaluate.
* @param oldCols The columns before resize.
* @param newCols The columns after resize.
*/
export function reflowSmallerGetNewLineLengths(wrappedLines: BufferLine[], oldCols: number, newCols: number): number[] {
const newLineLengths: number[] = [];
const cellsNeeded = wrappedLines.map(l => l.getTrimmedLength()).reduce((p, c) => p + c);
// Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and
// linesNeeded
let srcCol = 0;
let srcLine = 0;
let cellsAvailable = 0;
while (cellsAvailable < cellsNeeded) {
if (cellsNeeded - cellsAvailable < newCols) {
// Add the final line and exit the loop
newLineLengths.push(cellsNeeded - cellsAvailable);
break;
}
srcCol += newCols;
const oldTrimmedLength = wrappedLines[srcLine].getTrimmedLength();
if (srcCol > oldTrimmedLength) {
srcCol -= oldTrimmedLength;
srcLine++;
}
const endsWithWide = wrappedLines[srcLine].getWidth(srcCol - 1) === 2;
if (endsWithWide) {
srcCol--;
}
const lineLength = endsWithWide ? newCols - 1 : newCols;
newLineLengths.push(lineLength);
cellsAvailable += lineLength;
}
return newLineLengths;
}
+3 -29
View File
@@ -7,7 +7,7 @@ import { assert } from 'chai';
import { ITerminal } from './Types'; import { ITerminal } from './Types';
import { BufferSet } from './BufferSet'; import { BufferSet } from './BufferSet';
import { Buffer } from './Buffer'; import { Buffer } from './Buffer';
import { MockTerminal } from './ui/TestUtils.test'; import { MockTerminal } from './utils/TestUtils.test';
describe('BufferSet', () => { describe('BufferSet', () => {
let terminal: ITerminal; let terminal: ITerminal;
@@ -15,8 +15,8 @@ describe('BufferSet', () => {
beforeEach(() => { beforeEach(() => {
terminal = new MockTerminal(); terminal = new MockTerminal();
(terminal as any).cols = 80; terminal.cols = 80;
(terminal as any).rows = 24; terminal.rows = 24;
terminal.options.scrollback = 1000; terminal.options.scrollback = 1000;
bufferSet = new BufferSet(terminal); bufferSet = new BufferSet(terminal);
}); });
@@ -48,30 +48,4 @@ describe('BufferSet', () => {
assert.equal(bufferSet.active, bufferSet.alt); assert.equal(bufferSet.active, bufferSet.alt);
}); });
}); });
describe('cursor handling when swapping buffers', () => {
beforeEach(() => {
bufferSet.normal.x = 0;
bufferSet.normal.y = 0;
bufferSet.alt.x = 0;
bufferSet.alt.y = 0;
});
it('should keep the cursor stationary when activating alt buffer', () => {
bufferSet.activateNormalBuffer();
bufferSet.active.x = 30;
bufferSet.active.y = 10;
bufferSet.activateAltBuffer();
assert.equal(bufferSet.active.x, 30);
assert.equal(bufferSet.active.y, 10);
});
it('should keep the cursor stationary when activating normal buffer', () => {
bufferSet.activateAltBuffer();
bufferSet.active.x = 30;
bufferSet.active.y = 10;
bufferSet.activateNormalBuffer();
assert.equal(bufferSet.active.x, 30);
assert.equal(bufferSet.active.y, 10);
});
});
}); });
+7 -8
View File
@@ -5,7 +5,7 @@
import { ITerminal, IBufferSet } from './Types'; import { ITerminal, IBufferSet } from './Types';
import { Buffer } from './Buffer'; import { Buffer } from './Buffer';
import { EventEmitter } from './common/EventEmitter'; import { EventEmitter } from './EventEmitter';
/** /**
* The BufferSet represents the set of two buffers used by xterm terminals (normal and alt) and * The BufferSet represents the set of two buffers used by xterm terminals (normal and alt) and
@@ -18,7 +18,7 @@ export class BufferSet extends EventEmitter implements IBufferSet {
/** /**
* Create a new BufferSet for the given terminal. * 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) { constructor(private _terminal: ITerminal) {
super(); super();
@@ -35,6 +35,7 @@ export class BufferSet extends EventEmitter implements IBufferSet {
/** /**
* Returns the alt Buffer of the BufferSet * Returns the alt Buffer of the BufferSet
* @returns {Buffer}
*/ */
public get alt(): Buffer { public get alt(): Buffer {
return this._alt; return this._alt;
@@ -42,6 +43,7 @@ export class BufferSet extends EventEmitter implements IBufferSet {
/** /**
* Returns the normal Buffer of the BufferSet * Returns the normal Buffer of the BufferSet
* @returns {Buffer}
*/ */
public get active(): Buffer { public get active(): Buffer {
return this._activeBuffer; return this._activeBuffer;
@@ -49,6 +51,7 @@ export class BufferSet extends EventEmitter implements IBufferSet {
/** /**
* Returns the currently active Buffer of the BufferSet * Returns the currently active Buffer of the BufferSet
* @returns {Buffer}
*/ */
public get normal(): Buffer { public get normal(): Buffer {
return this._normal; return this._normal;
@@ -61,8 +64,6 @@ export class BufferSet extends EventEmitter implements IBufferSet {
if (this._activeBuffer === this._normal) { if (this._activeBuffer === this._normal) {
return; return;
} }
this._normal.x = this._alt.x;
this._normal.y = this._alt.y;
// The alt buffer should always be cleared when we switch to the normal // The alt buffer should always be cleared when we switch to the normal
// buffer. This frees up memory since the alt buffer should always be new // buffer. This frees up memory since the alt buffer should always be new
// when activated. // when activated.
@@ -77,15 +78,13 @@ export class BufferSet extends EventEmitter implements IBufferSet {
/** /**
* Sets the alt Buffer of the BufferSet as its currently active Buffer * Sets the alt Buffer of the BufferSet as its currently active Buffer
*/ */
public activateAltBuffer(fillAttr?: number): void { public activateAltBuffer(): void {
if (this._activeBuffer === this._alt) { if (this._activeBuffer === this._alt) {
return; return;
} }
// Since the alt buffer is always cleared when the normal buffer is // Since the alt buffer is always cleared when the normal buffer is
// activated, we want to fill it when switching to it. // activated, we want to fill it when switching to it.
this._alt.fillViewportRows(fillAttr); this._alt.fillViewportRows();
this._alt.x = this._normal.x;
this._alt.y = this._normal.y;
this._activeBuffer = this._alt; this._activeBuffer = this._alt;
this.emit('activate', { this.emit('activate', {
activeBuffer: this._alt, activeBuffer: this._alt,
-254
View File
@@ -1,254 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { TestTerminal } from './ui/TestUtils.test';
import { assert } from 'chai';
import { getStringCellWidth, wcwidth } 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
});
it('wcwidth should match all values from the old implementation', function(): void {
// old implementation
const wcwidthOld = (function(opts: {nul: number, control: number}): (ucs: number) => number {
// extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c
// combining characters
const COMBINING_BMP = [
[0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489],
[0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2],
[0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603],
[0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670],
[0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED],
[0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A],
[0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902],
[0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D],
[0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981],
[0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD],
[0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C],
[0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D],
[0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC],
[0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD],
[0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C],
[0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D],
[0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0],
[0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48],
[0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC],
[0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD],
[0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D],
[0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6],
[0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E],
[0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC],
[0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35],
[0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E],
[0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97],
[0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030],
[0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039],
[0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F],
[0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753],
[0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD],
[0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD],
[0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922],
[0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B],
[0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34],
[0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42],
[0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF],
[0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063],
[0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F],
[0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B],
[0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F],
[0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB]
];
const COMBINING_HIGH = [
[0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F],
[0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169],
[0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD],
[0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F],
[0xE0100, 0xE01EF]
];
// binary search
function bisearch(ucs: number, data: number[][]): boolean {
let min = 0;
let max = data.length - 1;
let mid;
if (ucs < data[0][0] || ucs > data[max][1]) {
return false;
}
while (max >= min) {
mid = (min + max) >> 1;
if (ucs > data[mid][1]) {
min = mid + 1;
} else if (ucs < data[mid][0]) {
max = mid - 1;
} else {
return true;
}
}
return false;
}
function wcwidthBMP(ucs: number): number {
// test for 8-bit control characters
if (ucs === 0) {
return opts.nul;
}
if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) {
return opts.control;
}
// binary search in table of non-spacing characters
if (bisearch(ucs, COMBINING_BMP)) {
return 0;
}
// if we arrive here, ucs is not a combining or C0/C1 control character
if (isWideBMP(ucs)) {
return 2;
}
return 1;
}
function isWideBMP(ucs: number): boolean {
return (
ucs >= 0x1100 && (
ucs <= 0x115f || // Hangul Jamo init. consonants
ucs === 0x2329 ||
ucs === 0x232a ||
(ucs >= 0x2e80 && ucs <= 0xa4cf && ucs !== 0x303f) || // CJK..Yi
(ucs >= 0xac00 && ucs <= 0xd7a3) || // Hangul Syllables
(ucs >= 0xf900 && ucs <= 0xfaff) || // CJK Compat Ideographs
(ucs >= 0xfe10 && ucs <= 0xfe19) || // Vertical forms
(ucs >= 0xfe30 && ucs <= 0xfe6f) || // CJK Compat Forms
(ucs >= 0xff00 && ucs <= 0xff60) || // Fullwidth Forms
(ucs >= 0xffe0 && ucs <= 0xffe6)));
}
function wcwidthHigh(ucs: number): 0 | 1 | 2 {
if (bisearch(ucs, COMBINING_HIGH)) {
return 0;
}
if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) {
return 2;
}
return 1;
}
const control = opts.control | 0;
let table: number[] | Uint32Array = null;
function initTable(): number[] | Uint32Array {
// lookup table for BMP
const CODEPOINTS = 65536; // BMP holds 65536 codepoints
const BITWIDTH = 2; // a codepoint can have a width of 0, 1 or 2
const ITEMSIZE = 32; // using uint32_t
const CONTAINERSIZE = CODEPOINTS * BITWIDTH / ITEMSIZE;
const CODEPOINTS_PER_ITEM = ITEMSIZE / BITWIDTH;
table = (typeof Uint32Array === 'undefined')
? new Array(CONTAINERSIZE)
: new Uint32Array(CONTAINERSIZE);
for (let i = 0; i < CONTAINERSIZE; ++i) {
let num = 0;
let pos = CODEPOINTS_PER_ITEM;
while (pos--) {
num = (num << 2) | wcwidthBMP(CODEPOINTS_PER_ITEM * i + pos);
}
table[i] = num;
}
return table;
}
// get width from lookup table
// position in container : num / CODEPOINTS_PER_ITEM
// ==> n = table[Math.floor(num / 16)]
// ==> n = table[num >> 4]
// 16 codepoints per number: FFEEDDCCBBAA99887766554433221100
// position in number : (num % CODEPOINTS_PER_ITEM) * BITWIDTH
// ==> m = (n % 16) * 2
// ==> m = (num & 15) << 1
// right shift to position m
// ==> n = n >> m e.g. m=12 000000000000FFEEDDCCBBAA99887766
// we are only interested in 2 LSBs, cut off higher bits
// ==> n = n & 3 e.g. 000000000000000000000000000000XX
return function (num: number): number {
num = num | 0; // get asm.js like optimization under V8
if (num < 32) {
return control | 0;
}
if (num < 127) {
return 1;
}
const t = table || initTable();
if (num < 65536) {
return t[num >> 4] >> ((num & 15) << 1) & 3;
}
// do a full search for high codepoints
return wcwidthHigh(num);
};
})({nul: 0, control: 0}); // configurable options
// test full BMP range old vs new implmenetation
for (let i = 0; i < 65536; ++i) {
assert.equal(wcwidth(i), wcwidthOld(i), `mismatch for i: ${i}`);
}
});
+158 -158
View File
@@ -3,169 +3,169 @@
* @license MIT * @license MIT
*/ */
import { fill } from './common/TypedArrayUtils';
export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: number) => number { export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: number) => number {
// extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c // extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c
// combining characters // combining characters
const COMBINING_BMP = [ const COMBINING_BMP = [
[0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489], [0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489],
[0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2], [0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2],
[0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603], [0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603],
[0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670], [0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670],
[0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED], [0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED],
[0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A], [0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A],
[0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902], [0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902],
[0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D], [0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D],
[0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981], [0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981],
[0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD], [0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD],
[0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C], [0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C],
[0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D], [0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D],
[0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC], [0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC],
[0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD], [0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD],
[0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C], [0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C],
[0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D], [0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D],
[0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0], [0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0],
[0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48], [0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48],
[0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC], [0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC],
[0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD], [0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD],
[0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D], [0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D],
[0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6], [0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6],
[0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E], [0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E],
[0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC], [0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC],
[0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35], [0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35],
[0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E], [0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E],
[0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97], [0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97],
[0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030], [0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030],
[0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039], [0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039],
[0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F], [0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F],
[0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753], [0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753],
[0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD], [0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD],
[0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD], [0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD],
[0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922], [0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922],
[0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B], [0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B],
[0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34], [0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34],
[0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42], [0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42],
[0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF], [0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF],
[0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063], [0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063],
[0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F], [0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F],
[0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B], [0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B],
[0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F], [0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F],
[0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB] [0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB]
]; ];
const COMBINING_HIGH = [ const COMBINING_HIGH = [
[0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F], [0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F],
[0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169], [0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169],
[0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD], [0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD],
[0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F], [0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F],
[0xE0100, 0xE01EF] [0xE0100, 0xE01EF]
]; ];
// binary search // binary search
function bisearch(ucs: number, data: number[][]): boolean { function bisearch(ucs: number, data: number[][]): boolean {
let min = 0; let min = 0;
let max = data.length - 1; let max = data.length - 1;
let mid; let mid;
if (ucs < data[0][0] || ucs > data[max][1]) { if (ucs < data[0][0] || ucs > data[max][1]) {
return false;
}
while (max >= min) {
mid = (min + max) >> 1;
if (ucs > data[mid][1]) {
min = mid + 1;
} else if (ucs < data[mid][0]) {
max = mid - 1;
} else {
return true;
}
}
return false; return false;
} }
while (max >= min) { function wcwidthBMP(ucs: number): number {
mid = (min + max) >> 1; // test for 8-bit control characters
if (ucs > data[mid][1]) { if (ucs === 0) {
min = mid + 1; return opts.nul;
} else if (ucs < data[mid][0]) { }
max = mid - 1; if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) {
} else { return opts.control;
return true; }
// binary search in table of non-spacing characters
if (bisearch(ucs, COMBINING_BMP)) {
return 0;
}
// if we arrive here, ucs is not a combining or C0/C1 control character
if (isWideBMP(ucs)) {
return 2;
} }
}
return false;
}
function wcwidthHigh(ucs: number): 0 | 1 | 2 {
if (bisearch(ucs, COMBINING_HIGH)) {
return 0;
}
if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) {
return 2;
}
return 1;
}
const control = opts.control | 0;
// create lookup table for BMP plane
const table = new Uint8Array(65536);
fill(table, 1);
table[0] = opts.nul;
// control chars
fill(table, opts.control, 1, 32);
fill(table, opts.control, 0x7f, 0xa0);
// apply wide char rules first
// wide chars
fill(table, 2, 0x1100, 0x1160);
table[0x2329] = 2;
table[0x232a] = 2;
fill(table, 2, 0x2e80, 0xa4d0);
table[0x303f] = 1; // wrongly in last line
fill(table, 2, 0xac00, 0xd7a4);
fill(table, 2, 0xf900, 0xfb00);
fill(table, 2, 0xfe10, 0xfe1a);
fill(table, 2, 0xfe30, 0xfe70);
fill(table, 2, 0xff00, 0xff61);
fill(table, 2, 0xffe0, 0xffe7);
// apply combining last to ensure we overwrite
// wrongly wide set chars:
// the original algo evals combining first and falls
// through to wide check so we simply do here the opposite
// combining 0
for (let r = 0; r < COMBINING_BMP.length; ++r) {
fill(table, 0, COMBINING_BMP[r][0], COMBINING_BMP[r][1] + 1);
}
return function (num: number): number {
if (num < 32) {
return control | 0;
}
if (num < 127) {
return 1; return 1;
} }
if (num < 65536) { function isWideBMP(ucs: number): boolean {
return table[num]; return (
ucs >= 0x1100 && (
ucs <= 0x115f || // Hangul Jamo init. consonants
ucs === 0x2329 ||
ucs === 0x232a ||
(ucs >= 0x2e80 && ucs <= 0xa4cf && ucs !== 0x303f) || // CJK..Yi
(ucs >= 0xac00 && ucs <= 0xd7a3) || // Hangul Syllables
(ucs >= 0xf900 && ucs <= 0xfaff) || // CJK Compat Ideographs
(ucs >= 0xfe10 && ucs <= 0xfe19) || // Vertical forms
(ucs >= 0xfe30 && ucs <= 0xfe6f) || // CJK Compat Forms
(ucs >= 0xff00 && ucs <= 0xff60) || // Fullwidth Forms
(ucs >= 0xffe0 && ucs <= 0xffe6)));
} }
// do a full search for high codepoints function wcwidthHigh(ucs: number): 0 | 1 | 2 {
return wcwidthHigh(num); if (bisearch(ucs, COMBINING_HIGH)) {
}; return 0;
}
if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) {
return 2;
}
return 1;
}
const control = opts.control | 0;
let table: number[] | Uint32Array = null;
function initTable(): number[] | Uint32Array {
// lookup table for BMP
const CODEPOINTS = 65536; // BMP holds 65536 codepoints
const BITWIDTH = 2; // a codepoint can have a width of 0, 1 or 2
const ITEMSIZE = 32; // using uint32_t
const CONTAINERSIZE = CODEPOINTS * BITWIDTH / ITEMSIZE;
const CODEPOINTS_PER_ITEM = ITEMSIZE / BITWIDTH;
table = (typeof Uint32Array === 'undefined')
? new Array(CONTAINERSIZE)
: new Uint32Array(CONTAINERSIZE);
for (let i = 0; i < CONTAINERSIZE; ++i) {
let num = 0;
let pos = CODEPOINTS_PER_ITEM;
while (pos--) {
num = (num << 2) | wcwidthBMP(CODEPOINTS_PER_ITEM * i + pos);
}
table[i] = num;
}
return table;
}
// get width from lookup table
// position in container : num / CODEPOINTS_PER_ITEM
// ==> n = table[Math.floor(num / 16)]
// ==> n = table[num >> 4]
// 16 codepoints per number: FFEEDDCCBBAA99887766554433221100
// position in number : (num % CODEPOINTS_PER_ITEM) * BITWIDTH
// ==> m = (n % 16) * 2
// ==> m = (num & 15) << 1
// right shift to position m
// ==> n = n >> m e.g. m=12 000000000000FFEEDDCCBBAA99887766
// we are only interested in 2 LSBs, cut off higher bits
// ==> n = n & 3 e.g. 000000000000000000000000000000XX
return function (num: number): number {
num = num | 0; // get asm.js like optimization under V8
if (num < 32) {
return control | 0;
}
if (num < 127) {
return 1;
}
const t = table || initTable();
if (num < 65536) {
return t[num >> 4] >> ((num & 15) << 1) & 3;
}
// do a full search for high codepoints
return wcwidthHigh(num);
};
})({nul: 0, control: 0}); // configurable options })({nul: 0, control: 0}); // configurable options
/**
* Get the terminal cell width for a string.
*/
export function getStringCellWidth(s: string): number {
let result = 0;
const length = s.length;
for (let i = 0; i < length; ++i) {
let code = s.charCodeAt(i);
// surrogate pair first
if (0xD800 <= code && code <= 0xDBFF) {
if (++i >= length) {
// this should not happen with strings retrieved from
// Buffer.translateToString as it converts from UTF-32
// and therefore always should contain the second part
// for any other string we still have to handle it somehow:
// simply treat the lonely surrogate first as a single char (UCS-2 behavior)
return result + wcwidth(code);
}
const second = s.charCodeAt(i);
// convert surrogate pair to high codepoint only for valid second part (UTF-16)
// otherwise treat them independently (UCS-2 behavior)
if (0xDC00 <= second && second <= 0xDFFF) {
code = (code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;
} else {
result += wcwidth(second);
}
}
result += wcwidth(code);
}
return result;
}
+7 -9
View File
@@ -61,7 +61,7 @@ export class CompositionHelper {
/** /**
* Handles the compositionupdate event, updating the composition view. * Handles the compositionupdate event, updating the composition view.
* @param ev The event. * @param {CompositionEvent} ev The event.
*/ */
public compositionupdate(ev: CompositionEvent): void { public compositionupdate(ev: CompositionEvent): void {
this._compositionView.textContent = ev.data; this._compositionView.textContent = ev.data;
@@ -111,17 +111,17 @@ export class CompositionHelper {
/** /**
* Finalizes the composition, resuming regular input actions. This is called when a composition * Finalizes the composition, resuming regular input actions. This is called when a composition
* is ending. * is ending.
* @param waitForPropagation Whether to wait for events to propagate before sending * @param waitForPropogation Whether to wait for events to propogate before sending
* the input. This should be false if a non-composition keystroke is entered before the * the input. This should be false if a non-composition keystroke is entered before the
* compositionend event is triggered, such as enter, so that the composition is sent before * compositionend event is triggered, such as enter, so that the composition is send before
* the command is executed. * the command is executed.
*/ */
private _finalizeComposition(waitForPropagation: boolean): void { private _finalizeComposition(waitForPropogation: boolean): void {
this._compositionView.classList.remove('active'); this._compositionView.classList.remove('active');
this._isComposing = false; this._isComposing = false;
this._clearTextareaPosition(); this._clearTextareaPosition();
if (!waitForPropagation) { if (!waitForPropogation) {
// Cancel any delayed composition send requests and send the input immediately. // Cancel any delayed composition send requests and send the input immediately.
this._isSendingComposition = false; this._isSendingComposition = false;
const input = this._textarea.value.substring(this._compositionPosition.start, this._compositionPosition.end); const input = this._textarea.value.substring(this._compositionPosition.start, this._compositionPosition.end);
@@ -136,8 +136,8 @@ export class CompositionHelper {
// Since composition* events happen before the changes take place in the textarea on most // Since composition* events happen before the changes take place in the textarea on most
// browsers, use a setTimeout with 0ms time to allow the native compositionend event to // browsers, use a setTimeout with 0ms time to allow the native compositionend event to
// complete. This ensures the correct character is retrieved. // complete. This ensures the correct character is retrieved, this solution was used
// This solution was used because: // because:
// - The compositionend event's data property is unreliable, at least on Chromium // - The compositionend event's data property is unreliable, at least on Chromium
// - The last compositionupdate event's data property does not always accurately describe // - The last compositionupdate event's data property does not always accurately describe
// the character, a counter example being Korean where an ending consonsant can move to // the character, a counter example being Korean where an ending consonsant can move to
@@ -203,8 +203,6 @@ export class CompositionHelper {
this._compositionView.style.top = cursorTop + 'px'; this._compositionView.style.top = cursorTop + 'px';
this._compositionView.style.height = cellHeight + 'px'; this._compositionView.style.height = cellHeight + 'px';
this._compositionView.style.lineHeight = cellHeight + 'px'; this._compositionView.style.lineHeight = cellHeight + 'px';
this._compositionView.style.fontFamily = this._terminal.options.fontFamily;
this._compositionView.style.fontSize = this._terminal.options.fontSize + 'px';
// Sync the textarea to the exact position of the composition view so the IME knows where the // Sync the textarea to the exact position of the composition view so the IME knows where the
// text is. // text is.
const compositionViewBounds = this._compositionView.getBoundingClientRect(); const compositionViewBounds = this._compositionView.getBoundingClientRect();
File diff suppressed because it is too large Load Diff
+33 -98
View File
@@ -4,16 +4,7 @@
*/ */
import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from './Types'; import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from './Types';
import { IDisposable } from 'xterm';
import { Disposable } from './common/Lifecycle'; import { Disposable } from './common/Lifecycle';
import { utf32ToString } from './core/input/TextDecoder';
interface IHandlerCollection<T> {
[key: string]: T[];
}
type CsiHandler = (params: number[], collect: string) => boolean | void;
type OscHandler = (data: string) => boolean | void;
/** /**
* Returns an array filled with numbers between the low and high parameters (right exclusive). * Returns an array filled with numbers between the low and high parameters (right exclusive).
@@ -75,9 +66,8 @@ export class TransitionTable {
const PRINTABLES = r(0x20, 0x7f); const PRINTABLES = r(0x20, 0x7f);
const EXECUTABLES = r(0x00, 0x18); const EXECUTABLES = r(0x00, 0x18);
EXECUTABLES.push(0x19); EXECUTABLES.push(0x19);
EXECUTABLES.push.apply(EXECUTABLES, r(0x1c, 0x20)); EXECUTABLES.concat(r(0x1c, 0x20));
// Pseudo-character placeholder for printable non-ascii characters. const DEFAULT_TRANSITION = ParserAction.ERROR << 4 | ParserState.GROUND;
const NON_ASCII_PRINTABLE = 0xA0;
/** /**
* VT500 compatible transition table. * VT500 compatible transition table.
@@ -89,10 +79,10 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
const states: number[] = r(ParserState.GROUND, ParserState.DCS_PASSTHROUGH + 1); const states: number[] = r(ParserState.GROUND, ParserState.DCS_PASSTHROUGH + 1);
let state: any; let state: any;
// table with default transition // table with default transition [any] --> DEFAULT_TRANSITION
for (state in states) { for (state in states) {
// NOTE: table lookup is capped at 0xa0 in parse to keep the table small // NOTE: table lookup is capped at 0xa0 in parse to keep the table small
for (let code = 0; code <= NON_ASCII_PRINTABLE; ++code) { for (let code = 0; code < 160; ++code) {
table.add(code, state, ParserAction.ERROR, ParserState.GROUND); table.add(code, state, ParserAction.ERROR, ParserState.GROUND);
} }
} }
@@ -135,7 +125,6 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
table.addMany(PRINTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); table.addMany(PRINTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING);
table.addMany(EXECUTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); table.addMany(EXECUTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING);
table.add(0x9c, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.GROUND); table.add(0x9c, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.GROUND);
table.add(0x7f, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING);
// csi entries // csi entries
table.add(0x5b, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.CSI_ENTRY); table.add(0x5b, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.CSI_ENTRY);
table.addMany(r(0x40, 0x7f), ParserState.CSI_ENTRY, ParserAction.CSI_DISPATCH, ParserState.GROUND); table.addMany(r(0x40, 0x7f), ParserState.CSI_ENTRY, ParserAction.CSI_DISPATCH, ParserState.GROUND);
@@ -195,7 +184,6 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
table.addMany(PRINTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH); table.addMany(PRINTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);
table.add(0x7f, ParserState.DCS_PASSTHROUGH, ParserAction.IGNORE, ParserState.DCS_PASSTHROUGH); table.add(0x7f, ParserState.DCS_PASSTHROUGH, ParserAction.IGNORE, ParserState.DCS_PASSTHROUGH);
table.addMany([0x1b, 0x9c], ParserState.DCS_PASSTHROUGH, ParserAction.DCS_UNHOOK, ParserState.GROUND); table.addMany([0x1b, 0x9c], ParserState.DCS_PASSTHROUGH, ParserAction.DCS_UNHOOK, ParserState.GROUND);
table.add(NON_ASCII_PRINTABLE, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);
return table; return table;
})(); })();
@@ -204,7 +192,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
*/ */
class DcsDummy implements IDcsHandler { class DcsDummy implements IDcsHandler {
hook(collect: string, params: number[], flag: number): void { } hook(collect: string, params: number[], flag: number): void { }
put(data: Uint32Array, start: number, end: number): void { } put(data: string, start: number, end: number): void { }
unhook(): void { } unhook(): void { }
} }
@@ -230,17 +218,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
protected _collect: string; protected _collect: string;
// handler lookup containers // handler lookup containers
protected _printHandler: (data: Uint32Array, start: number, end: number) => void; protected _printHandler: (data: string, start: number, end: number) => void;
protected _executeHandlers: any; protected _executeHandlers: any;
protected _csiHandlers: IHandlerCollection<CsiHandler>; protected _csiHandlers: any;
protected _escHandlers: any; protected _escHandlers: any;
protected _oscHandlers: IHandlerCollection<OscHandler>; protected _oscHandlers: any;
protected _dcsHandlers: any; protected _dcsHandlers: any;
protected _activeDcsHandler: IDcsHandler | null; protected _activeDcsHandler: IDcsHandler | null;
protected _errorHandler: (state: IParsingState) => IParsingState; protected _errorHandler: (state: IParsingState) => IParsingState;
// fallback handlers // fallback handlers
protected _printHandlerFb: (data: Uint32Array, start: number, end: number) => void; protected _printHandlerFb: (data: string, start: number, end: number) => void;
protected _executeHandlerFb: (code: number) => void; protected _executeHandlerFb: (code: number) => void;
protected _csiHandlerFb: (collect: string, params: number[], flag: number) => void; protected _csiHandlerFb: (collect: string, params: number[], flag: number) => void;
protected _escHandlerFb: (collect: string, flag: number) => void; protected _escHandlerFb: (collect: string, flag: number) => void;
@@ -273,9 +261,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._dcsHandlers = Object.create(null); this._dcsHandlers = Object.create(null);
this._activeDcsHandler = null; this._activeDcsHandler = null;
this._errorHandler = this._errorHandlerFb; this._errorHandler = this._errorHandlerFb;
// swallow 7bit ST (ESC+\)
this.setEscHandler('\\', () => {});
} }
public dispose(): void { public dispose(): void {
@@ -288,15 +273,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._errorHandlerFb = null; this._errorHandlerFb = null;
this._printHandler = null; this._printHandler = null;
this._executeHandlers = null; this._executeHandlers = null;
this._escHandlers = null;
this._csiHandlers = null; this._csiHandlers = null;
this._escHandlers = null;
this._oscHandlers = null; this._oscHandlers = null;
this._dcsHandlers = null; this._dcsHandlers = null;
this._activeDcsHandler = null; this._activeDcsHandler = null;
this._errorHandler = null; this._errorHandler = null;
} }
setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void { setPrintHandler(callback: (data: string, start: number, end: number) => void): void {
this._printHandler = callback; this._printHandler = callback;
} }
clearPrintHandler(): void { clearPrintHandler(): void {
@@ -313,24 +298,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._executeHandlerFb = callback; this._executeHandlerFb = callback;
} }
addCsiHandler(flag: string, callback: CsiHandler): IDisposable {
const index = flag.charCodeAt(0);
if (this._csiHandlers[index] === undefined) {
this._csiHandlers[index] = [];
}
const handlerList = this._csiHandlers[index];
handlerList.push(callback);
return {
dispose: () => {
const handlerIndex = handlerList.indexOf(callback);
if (handlerIndex !== -1) {
handlerList.splice(handlerIndex, 1);
}
}
};
}
setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void { setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void {
this._csiHandlers[flag.charCodeAt(0)] = [callback]; this._csiHandlers[flag.charCodeAt(0)] = callback;
} }
clearCsiHandler(flag: string): void { clearCsiHandler(flag: string): void {
if (this._csiHandlers[flag.charCodeAt(0)]) delete this._csiHandlers[flag.charCodeAt(0)]; if (this._csiHandlers[flag.charCodeAt(0)]) delete this._csiHandlers[flag.charCodeAt(0)];
@@ -349,23 +318,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._escHandlerFb = callback; this._escHandlerFb = callback;
} }
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
if (this._oscHandlers[ident] === undefined) {
this._oscHandlers[ident] = [];
}
const handlerList = this._oscHandlers[ident];
handlerList.push(callback);
return {
dispose: () => {
const handlerIndex = handlerList.indexOf(callback);
if (handlerIndex !== -1) {
handlerList.splice(handlerIndex, 1);
}
}
};
}
setOscHandler(ident: number, callback: (data: string) => void): void { setOscHandler(ident: number, callback: (data: string) => void): void {
this._oscHandlers[ident] = [callback]; this._oscHandlers[ident] = callback;
} }
clearOscHandler(ident: number): void { clearOscHandler(ident: number): void {
if (this._oscHandlers[ident]) delete this._oscHandlers[ident]; if (this._oscHandlers[ident]) delete this._oscHandlers[ident];
@@ -399,7 +353,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._activeDcsHandler = null; this._activeDcsHandler = null;
} }
parse(data: Uint32Array, length: number): void { parse(data: string): void {
let code = 0; let code = 0;
let transition = 0; let transition = 0;
let error = false; let error = false;
@@ -414,14 +368,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
let callback: Function | null = null; let callback: Function | null = null;
// process input string // process input string
for (let i = 0; i < length; ++i) { const l = data.length;
code = data[i]; for (let i = 0; i < l; ++i) {
code = data.charCodeAt(i);
// shortcut for most chars (print action) // shortcut for most chars (print action)
if (currentState === ParserState.GROUND && code > 0x1f && code < 0x80) { if (currentState === ParserState.GROUND && code > 0x1f && code < 0x80) {
print = (~print) ? print : i; print = (~print) ? print : i;
do i++; do code = data.charCodeAt(++i);
while (i < length && data[i] > 0x1f && data[i] < 0x80); while (i < l && code > 0x1f && code < 0x80);
i--; i--;
continue; continue;
} }
@@ -433,7 +388,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
} }
// normal transition & action lookup // normal transition & action lookup
transition = table[currentState << 8 | (code < 0xa0 ? code : NON_ASCII_PRINTABLE)]; transition = (code < 0xa0) ? (table[currentState << 8 | code]) : DEFAULT_TRANSITION;
switch (transition >> 4) { switch (transition >> 4) {
case ParserAction.PRINT: case ParserAction.PRINT:
print = (~print) ? print : i; print = (~print) ? print : i;
@@ -465,6 +420,10 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
case ParserState.GROUND: case ParserState.GROUND:
print = (~print) ? print : i; print = (~print) ? print : i;
break; break;
case ParserState.OSC_STRING:
osc += String.fromCharCode(code);
transition |= ParserState.OSC_STRING;
break;
case ParserState.CSI_IGNORE: case ParserState.CSI_IGNORE:
transition |= ParserState.CSI_IGNORE; transition |= ParserState.CSI_IGNORE;
break; break;
@@ -501,17 +460,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
} }
break; break;
case ParserAction.CSI_DISPATCH: case ParserAction.CSI_DISPATCH:
// Trigger CSI Handler callback = this._csiHandlers[code];
const handlers = this._csiHandlers[code]; if (callback) callback(params, collect);
let j = handlers ? handlers.length - 1 : -1; else this._csiHandlerFb(collect, params, code);
for (; j >= 0; j--) {
if (handlers[j](params, collect)) {
break;
}
}
if (j < 0) {
this._csiHandlerFb(collect, params, code);
}
break; break;
case ParserAction.PARAM: case ParserAction.PARAM:
if (code === 0x3b) params.push(0); if (code === 0x3b) params.push(0);
@@ -563,15 +514,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
osc = ''; osc = '';
break; break;
case ParserAction.OSC_PUT: case ParserAction.OSC_PUT:
for (let j = i + 1; ; j++) { osc += data.charAt(i);
if (j >= length
|| (code = data[j]) < 0x20
|| (code > 0x7f && code <= 0x9f)) {
osc += utf32ToString(data, i, j);
i = j - 1;
break;
}
}
break; break;
case ParserAction.OSC_END: case ParserAction.OSC_END:
if (osc && code !== 0x18 && code !== 0x1a) { if (osc && code !== 0x18 && code !== 0x1a) {
@@ -586,17 +529,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
// or with an explicit NaN OSC handler // or with an explicit NaN OSC handler
const identifier = parseInt(osc.substring(0, idx)); const identifier = parseInt(osc.substring(0, idx));
const content = osc.substring(idx + 1); const content = osc.substring(idx + 1);
// Trigger OSC Handler callback = this._oscHandlers[identifier];
const handlers = this._oscHandlers[identifier]; if (callback) callback(content);
let j = handlers ? handlers.length - 1 : -1; else this._oscHandlerFb(identifier, content);
for (; j >= 0; j--) {
if (handlers[j](content)) {
break;
}
}
if (j < 0) {
this._oscHandlerFb(identifier, content);
}
} }
} }
if (code === 0x1b) transition |= ParserState.ESCAPE; if (code === 0x1b) transition |= ParserState.ESCAPE;
@@ -611,9 +546,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
// push leftover pushable buffers to terminal // push leftover pushable buffers to terminal
if (currentState === ParserState.GROUND && ~print) { if (currentState === ParserState.GROUND && ~print) {
this._printHandler(data, print, length); this._printHandler(data, print, data.length);
} else if (currentState === ParserState.DCS_PASSTHROUGH && ~dcs && dcsHandler) { } else if (currentState === ParserState.DCS_PASSTHROUGH && ~dcs && dcsHandler) {
dcsHandler.put(data, dcs, length); dcsHandler.put(data, dcs, data.length);
} }
// save non pushable buffers // save non pushable buffers
@@ -5,7 +5,7 @@
import { XtermListener } from './Types'; import { XtermListener } from './Types';
import { IEventEmitter, IDisposable } from 'xterm'; import { IEventEmitter, IDisposable } from 'xterm';
import { Disposable } from './Lifecycle'; import { Disposable } from './common/Lifecycle';
export class EventEmitter extends Disposable implements IEventEmitter, IDisposable { export class EventEmitter extends Disposable implements IEventEmitter, IDisposable {
private _events: {[type: string]: XtermListener[]}; private _events: {[type: string]: XtermListener[]};
@@ -14,7 +14,7 @@ export class EventEmitter extends Disposable implements IEventEmitter, IDisposab
super(); super();
// Restore the previous events if available, this will happen if the // Restore the previous events if available, this will happen if the
// constructor is called multiple times on the same object (terminal reset). // constructor is called multiple times on the same object (terminal reset).
this._events = (<any>this)._events || {}; this._events = this._events || {};
} }
public on(type: string, listener: XtermListener): void { public on(type: string, listener: XtermListener): void {
@@ -23,22 +23,21 @@ export class EventEmitter extends Disposable implements IEventEmitter, IDisposab
} }
/** /**
* Adds a disposable listener to the EventEmitter, returning the disposable. * Adds a disposabe listener to the EventEmitter, returning the disposable.
* @param type The event type. * @param type The event type.
* @param handler The handler for the listener. * @param handler The handler for the listener.
*/ */
public addDisposableListener(type: string, handler: XtermListener): IDisposable { public addDisposableListener(type: string, handler: XtermListener): IDisposable {
// TODO: Rename addDisposableEventListener to more easily disambiguate from Dom listener // TODO: Rename addDisposableEventListener to more easily disambiguate from Dom listener
this.on(type, handler); this.on(type, handler);
let disposed = false;
return { return {
dispose: () => { dispose: () => {
if (disposed) { if (!handler) {
// Already disposed // Already disposed
return; return;
} }
this.off(type, handler); this.off(type, handler);
disposed = true; handler = null;
} }
}; };
} }
@@ -75,19 +74,6 @@ export class EventEmitter extends Disposable implements IEventEmitter, IDisposab
} }
} }
public emitMayRemoveListeners(type: string, ...args: any[]): void {
if (!this._events[type]) {
return;
}
const obj = this._events[type];
let length = obj.length;
for (let i = 0; i < obj.length; i++) {
obj[i].apply(this, args);
i -= length - obj.length;
length = obj.length;
}
}
public listeners(type: string): XtermListener[] { public listeners(type: string): XtermListener[] {
return this._events[type] || []; return this._events[type] || [];
} }
+2 -315
View File
@@ -3,12 +3,9 @@
* @license MIT * @license MIT
*/ */
import { assert, expect } from 'chai'; import { assert } from 'chai';
import { InputHandler } from './InputHandler'; import { InputHandler } from './InputHandler';
import { MockInputHandlingTerminal } from './ui/TestUtils.test'; import { MockInputHandlingTerminal } from './utils/TestUtils.test';
import { CHAR_DATA_ATTR_INDEX, DEFAULT_ATTR } from './Buffer';
import { Terminal } from './Terminal';
import { IBufferLine } from './Types';
describe('InputHandler', () => { describe('InputHandler', () => {
describe('save and restore cursor', () => { describe('save and restore cursor', () => {
@@ -87,314 +84,4 @@ describe('InputHandler', () => {
assert.equal(terminal.bracketedPasteMode, false); assert.equal(terminal.bracketedPasteMode, false);
}); });
}); });
describe('regression tests', function(): void {
function termContent(term: Terminal, trim: boolean): string[] {
const result = [];
for (let i = 0; i < term.rows; ++i) result.push(term.buffer.lines.get(i).translateToString(trim));
return result;
}
it('insertChars', function(): void {
const term = new Terminal();
const inputHandler = new InputHandler(term);
// 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);
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + '1234567890');
// insert one char from params = [0]
term.buffer.y = 0;
term.buffer.x = 70;
inputHandler.insertChars([0]);
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' 123456789');
// insert one char from params = [1]
term.buffer.y = 0;
term.buffer.x = 70;
inputHandler.insertChars([1]);
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' 12345678');
// insert two chars from params = [2]
term.buffer.y = 0;
term.buffer.x = 70;
inputHandler.insertChars([2]);
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' 123456');
// insert 10 chars from params = [10]
term.buffer.y = 0;
term.buffer.x = 70;
inputHandler.insertChars([10]);
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' ');
expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a'));
});
it('deleteChars', function(): void {
const term = new Terminal();
const inputHandler = new InputHandler(term);
// 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);
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + '1234567890');
// delete one char from params = [0]
term.buffer.y = 0;
term.buffer.x = 70;
inputHandler.deleteChars([0]);
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + '234567890 ');
expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a') + '234567890');
// insert one char from params = [1]
term.buffer.y = 0;
term.buffer.x = 70;
inputHandler.deleteChars([1]);
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + '34567890 ');
expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a') + '34567890');
// insert two chars from params = [2]
term.buffer.y = 0;
term.buffer.x = 70;
inputHandler.deleteChars([2]);
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + '567890 ');
expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a') + '567890');
// insert 10 chars from params = [10]
term.buffer.y = 0;
term.buffer.x = 70;
inputHandler.deleteChars([10]);
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' ');
expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a'));
});
it('eraseInLine', function(): void {
const term = new Terminal();
const inputHandler = new InputHandler(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'));
// params[0] - right erase
term.buffer.y = 0;
term.buffer.x = 70;
inputHandler.eraseInLine([0]);
expect(term.buffer.lines.get(0).translateToString(false)).equals(Array(71).join('a') + ' ');
// params[1] - left erase
term.buffer.y = 1;
term.buffer.x = 70;
inputHandler.eraseInLine([1]);
expect(term.buffer.lines.get(1).translateToString(false)).equals(Array(71).join(' ') + ' aaaaaaaaa');
// params[1] - left erase
term.buffer.y = 2;
term.buffer.x = 70;
inputHandler.eraseInLine([2]);
expect(term.buffer.lines.get(2).translateToString(false)).equals(Array(term.cols + 1).join(' '));
});
it('eraseInDisplay', function(): void {
const term = new Terminal({cols: 80, rows: 7});
const inputHandler = new InputHandler(term);
// fill display with a's
for (let i = 0; i < term.rows; ++i) inputHandler.parse(Array(term.cols + 1).join('a'));
// params [0] - right and below erase
term.buffer.y = 5;
term.buffer.x = 40;
inputHandler.eraseInDisplay([0]);
expect(termContent(term, false)).eql([
Array(term.cols + 1).join('a'),
Array(term.cols + 1).join('a'),
Array(term.cols + 1).join('a'),
Array(term.cols + 1).join('a'),
Array(term.cols + 1).join('a'),
Array(40 + 1).join('a') + Array(term.cols - 40 + 1).join(' '),
Array(term.cols + 1).join(' ')
]);
expect(termContent(term, true)).eql([
Array(term.cols + 1).join('a'),
Array(term.cols + 1).join('a'),
Array(term.cols + 1).join('a'),
Array(term.cols + 1).join('a'),
Array(term.cols + 1).join('a'),
Array(40 + 1).join('a'),
''
]);
// reset
term.buffer.y = 0;
term.buffer.x = 0;
for (let i = 0; i < term.rows; ++i) inputHandler.parse(Array(term.cols + 1).join('a'));
// params [1] - left and above
term.buffer.y = 5;
term.buffer.x = 40;
inputHandler.eraseInDisplay([1]);
expect(termContent(term, false)).eql([
Array(term.cols + 1).join(' '),
Array(term.cols + 1).join(' '),
Array(term.cols + 1).join(' '),
Array(term.cols + 1).join(' '),
Array(term.cols + 1).join(' '),
Array(41 + 1).join(' ') + Array(term.cols - 41 + 1).join('a'),
Array(term.cols + 1).join('a')
]);
expect(termContent(term, true)).eql([
'',
'',
'',
'',
'',
Array(41 + 1).join(' ') + Array(term.cols - 41 + 1).join('a'),
Array(term.cols + 1).join('a')
]);
// reset
term.buffer.y = 0;
term.buffer.x = 0;
for (let i = 0; i < term.rows; ++i) inputHandler.parse(Array(term.cols + 1).join('a'));
// params [2] - whole screen
term.buffer.y = 5;
term.buffer.x = 40;
inputHandler.eraseInDisplay([2]);
expect(termContent(term, false)).eql([
Array(term.cols + 1).join(' '),
Array(term.cols + 1).join(' '),
Array(term.cols + 1).join(' '),
Array(term.cols + 1).join(' '),
Array(term.cols + 1).join(' '),
Array(term.cols + 1).join(' '),
Array(term.cols + 1).join(' ')
]);
expect(termContent(term, true)).eql([
'',
'',
'',
'',
'',
'',
''
]);
// reset and add a wrapped line
term.buffer.y = 0;
term.buffer.x = 0;
inputHandler.parse(Array(term.cols + 1).join('a')); // line 0
inputHandler.parse(Array(term.cols + 10).join('a')); // line 1 and 2
for (let i = 3; i < term.rows; ++i) inputHandler.parse(Array(term.cols + 1).join('a'));
// params[1] left and above with wrap
// confirm precondition that line 2 is wrapped
expect(term.buffer.lines.get(2).isWrapped).true;
term.buffer.y = 2;
term.buffer.x = 40;
inputHandler.eraseInDisplay([1]);
expect(term.buffer.lines.get(2).isWrapped).false;
// reset and add a wrapped line
term.buffer.y = 0;
term.buffer.x = 0;
inputHandler.parse(Array(term.cols + 1).join('a')); // line 0
inputHandler.parse(Array(term.cols + 10).join('a')); // line 1 and 2
for (let i = 3; i < term.rows; ++i) inputHandler.parse(Array(term.cols + 1).join('a'));
// params[1] left and above with wrap
// confirm precondition that line 2 is wrapped
expect(term.buffer.lines.get(2).isWrapped).true;
term.buffer.y = 1;
term.buffer.x = 90; // Cursor is beyond last column
inputHandler.eraseInDisplay([1]);
expect(term.buffer.lines.get(2).isWrapped).false;
});
});
it('convertEol setting', function(): void {
// not converting
const termNotConverting = new Terminal({cols: 15, rows: 10});
(termNotConverting as any)._inputHandler.parse('Hello\nWorld');
expect(termNotConverting.buffer.lines.get(0).translateToString(false)).equals('Hello ');
expect(termNotConverting.buffer.lines.get(1).translateToString(false)).equals(' World ');
expect(termNotConverting.buffer.lines.get(0).translateToString(true)).equals('Hello');
expect(termNotConverting.buffer.lines.get(1).translateToString(true)).equals(' World');
// converting
const termConverting = new Terminal({cols: 15, rows: 10, convertEol: true});
(termConverting as any)._inputHandler.parse('Hello\nWorld');
expect(termConverting.buffer.lines.get(0).translateToString(false)).equals('Hello ');
expect(termConverting.buffer.lines.get(1).translateToString(false)).equals('World ');
expect(termConverting.buffer.lines.get(0).translateToString(true)).equals('Hello');
expect(termConverting.buffer.lines.get(1).translateToString(true)).equals('World');
});
describe('print', () => {
it('should not cause an infinite loop (regression test)', () => {
const term = new Terminal();
const inputHandler = new InputHandler(term);
const container = new Uint32Array(10);
container[0] = 0x200B;
inputHandler.print(container, 0, 1);
});
});
describe('alt screen', () => {
let term: Terminal;
let handler: InputHandler;
beforeEach(() => {
term = new Terminal();
handler = new InputHandler(term);
});
it('should handle DECSET/DECRST 47 (alt screen buffer)', () => {
handler.parse('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST');
expect(term.buffer.translateBufferLineToString(0, true)).to.equal('');
expect(term.buffer.translateBufferLineToString(1, true)).to.equal(' TEST');
// Text color of 'TEST' should be red
expect((term.buffer.lines.get(1).get(4)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1);
});
it('should handle DECSET/DECRST 1047 (alt screen buffer)', () => {
handler.parse('\x1b[?1047h\r\n\x1b[31mJUNK\x1b[?1047lTEST');
expect(term.buffer.translateBufferLineToString(0, true)).to.equal('');
expect(term.buffer.translateBufferLineToString(1, true)).to.equal(' TEST');
// Text color of 'TEST' should be red
expect((term.buffer.lines.get(1).get(4)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1);
});
it('should handle DECSET/DECRST 1048 (alt screen cursor)', () => {
handler.parse('\x1b[?1048h\r\n\x1b[31mJUNK\x1b[?1048lTEST');
expect(term.buffer.translateBufferLineToString(0, true)).to.equal('TEST');
expect(term.buffer.translateBufferLineToString(1, true)).to.equal('JUNK');
// Text color of 'TEST' should be default
expect(term.buffer.lines.get(0).get(0)[CHAR_DATA_ATTR_INDEX]).to.equal(DEFAULT_ATTR);
// Text color of 'JUNK' should be red
expect((term.buffer.lines.get(1).get(0)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1);
});
it('should handle DECSET/DECRST 1049 (alt screen buffer+cursor)', () => {
handler.parse('\x1b[?1049h\r\n\x1b[31mJUNK\x1b[?1049lTEST');
expect(term.buffer.translateBufferLineToString(0, true)).to.equal('TEST');
expect(term.buffer.translateBufferLineToString(1, true)).to.equal('');
// Text color of 'TEST' should be default
expect(term.buffer.lines.get(0).get(0)[CHAR_DATA_ATTR_INDEX]).to.equal(DEFAULT_ATTR);
});
it('should handle DECSET/DECRST 1049 - maintains saved cursor for alt buffer', () => {
handler.parse('\x1b[?1049h\r\n\x1b[31m\x1b[s\x1b[?1049lTEST');
expect(term.buffer.translateBufferLineToString(0, true)).to.equal('TEST');
// Text color of 'TEST' should be default
expect(term.buffer.lines.get(0).get(0)[CHAR_DATA_ATTR_INDEX]).to.equal(DEFAULT_ATTR);
handler.parse('\x1b[?1049h\x1b[uTEST');
expect(term.buffer.translateBufferLineToString(1, true)).to.equal('TEST');
// Text color of 'TEST' should be red
expect((term.buffer.lines.get(1).get(0)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1);
});
it('should handle DECSET/DECRST 1049 - clears alt buffer with erase attributes', () => {
handler.parse('\x1b[42m\x1b[?1049h');
// Buffer should be filled with green background
expect(term.buffer.lines.get(20).get(10)[CHAR_DATA_ATTR_INDEX] & 0x1ff).to.equal(2);
});
});
}); });
+215 -240
View File
@@ -4,18 +4,15 @@
* @license MIT * @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 { C0, C1 } from './common/data/EscapeSequences';
import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets'; import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR } from './Buffer';
import { FLAGS } from './renderer/Types'; import { FLAGS } from './renderer/Types';
import { wcwidth } from './CharWidth'; import { wcwidth } from './CharWidth';
import { EscapeSequenceParser } from './EscapeSequenceParser'; import { EscapeSequenceParser } from './EscapeSequenceParser';
import { ICharset } from './core/Types'; import { ICharset } from './core/Types';
import { IDisposable } from 'xterm';
import { Disposable } from './common/Lifecycle'; import { Disposable } from './common/Lifecycle';
import { concat } from './common/TypedArrayUtils';
import { StringToUtf32, stringFromCodePoint, utf32ToString } from './core/input/TextDecoder';
/** /**
* Map collect to glevel. Used in `selectCharset`. * Map collect to glevel. Used in `selectCharset`.
@@ -27,6 +24,26 @@ const GLEVEL: {[key: string]: number} = {'(': 0, ')': 1, '*': 2, '+': 3, '-': 1,
* DCS subparser implementations * 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 * DCS $ q Pt ST
* DECRQSS (https://vt100.net/docs/vt510-rm/DECRQSS.html) * DECRQSS (https://vt100.net/docs/vt510-rm/DECRQSS.html)
@@ -34,43 +51,42 @@ const GLEVEL: {[key: string]: number} = {'(': 0, ')': 1, '*': 2, '+': 3, '-': 1,
* Response: DECRPSS (https://vt100.net/docs/vt510-rm/DECRPSS.html) * Response: DECRPSS (https://vt100.net/docs/vt510-rm/DECRPSS.html)
*/ */
class DECRQSS implements IDcsHandler { class DECRQSS implements IDcsHandler {
private _data: Uint32Array = new Uint32Array(0); private _data: string;
constructor(private _terminal: any) { } constructor(private _terminal: any) { }
hook(collect: string, params: number[], flag: number): void { hook(collect: string, params: number[], flag: number): void {
this._data = new Uint32Array(0); // reset data
this._data = '';
} }
put(data: Uint32Array, start: number, end: number): void { put(data: string, start: number, end: number): void {
this._data = concat(this._data, data.subarray(start, end)); this._data += data.substring(start, end);
} }
unhook(): void { unhook(): void {
const data = utf32ToString(this._data); switch (this._data) {
this._data = new Uint32Array(0);
switch (data) {
// valid: DCS 1 $ r Pt ST (xterm) // valid: DCS 1 $ r Pt ST (xterm)
case '"q': // DECSCA 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 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 case 'r': // DECSTBM
const pt = '' + (this._terminal.buffer.scrollTop + 1) + const pt = '' + (this._terminal.buffer.scrollTop + 1) +
';' + (this._terminal.buffer.scrollBottom + 1) + 'r'; ';' + (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 case 'm': // SGR
// TODO: report real settings instead of 0m // 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 case ' q': // DECSCUSR
const STYLES: {[key: string]: number} = {'block': 2, 'underline': 4, 'bar': 6}; const STYLES: {[key: string]: number} = {'block': 2, 'underline': 4, 'bar': 6};
let style = STYLES[this._terminal.getOption('cursorStyle')]; let style = STYLES[this._terminal.getOption('cursorStyle')];
style -= this._terminal.getOption('cursorBlink'); 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: default:
// invalid: DCS 0 $ r Pt ST (xterm) // invalid: DCS 0 $ r Pt ST (xterm)
this._terminal.error('Unknown DCS $q %s', data); 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}\\`);
} }
} }
} }
@@ -81,17 +97,11 @@ class DECRQSS implements IDcsHandler {
* not supported * not supported
*/ */
/** /**
* DCS + q Pt ST (xterm) * DCS + p Pt ST (xterm)
* Request Terminfo String * Set Terminfo Data
* not implemented * not supported
*/ */
/**
* DCS + p Pt ST (xterm)
* Set Terminfo Data
* not supported
*/
@@ -103,31 +113,32 @@ class DECRQSS implements IDcsHandler {
* each function's header comment. * each function's header comment.
*/ */
export class InputHandler extends Disposable implements IInputHandler { export class InputHandler extends Disposable implements IInputHandler {
private _parseBuffer: Uint32Array = new Uint32Array(4096); private _surrogateHigh: string;
private _stringDecoder: StringToUtf32 = new StringToUtf32();
constructor( constructor(
protected _terminal: IInputHandlingTerminal, private _terminal: any, // TODO: reestablish IInputHandlingTerminal here
private _parser: IEscapeSequenceParser = new EscapeSequenceParser()) private _parser: IEscapeSequenceParser = new EscapeSequenceParser())
{ {
super(); super();
this.register(this._parser); this.register(this._parser);
this._surrogateHigh = '';
/** /**
* custom fallback handlers * custom fallback handlers
*/ */
this._parser.setCsiHandlerFallback((collect: string, params: number[], flag: number) => { 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._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._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._parser.setOscHandlerFallback((identifier: number, data: string) => {
this._terminal.error('Unknown OSC code: ', { identifier, data }); this._terminal.error('Unknown OSC code: ', identifier, data);
}); });
/** /**
@@ -276,6 +287,7 @@ export class InputHandler extends Disposable implements IInputHandler {
* DCS handler * DCS handler
*/ */
this._parser.setDcsHandler('$q', new DECRQSS(this._terminal)); this._parser.setDcsHandler('$q', new DECRQSS(this._terminal));
this._parser.setDcsHandler('+q', new RequestTerminfo(this._terminal));
} }
public dispose(): void { public dispose(): void {
@@ -284,27 +296,20 @@ export class InputHandler extends Disposable implements IInputHandler {
} }
public parse(data: string): void { public parse(data: string): void {
// Ensure the terminal is not disposed
if (!this._terminal) {
return;
}
let buffer = this._terminal.buffer; let buffer = this._terminal.buffer;
const cursorStartX = buffer.x; const cursorStartX = buffer.x;
const cursorStartY = buffer.y; const cursorStartY = buffer.y;
if (this._terminal.debug) {
// TODO: Consolidate debug/logging #1560
if ((<any>this._terminal).debug) {
this._terminal.log('data: ' + data); this._terminal.log('data: ' + data);
} }
if (this._parseBuffer.length < data.length) { // apply leftover surrogate high from last write
this._parseBuffer = new Uint32Array(data.length); if (this._surrogateHigh) {
data = this._surrogateHigh + data;
this._surrogateHigh = '';
} }
for (let i = 0; i < data.length; ++i) {
this._parseBuffer[i] = data.charCodeAt(i); this._parser.parse(data);
}
this._parser.parse(this._parseBuffer, this._stringDecoder.decode(data, this._parseBuffer));
buffer = this._terminal.buffer; buffer = this._terminal.buffer;
if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) { if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) {
@@ -312,9 +317,10 @@ export class InputHandler extends Disposable implements IInputHandler {
} }
} }
public print(data: Uint32Array, start: number, end: number): void { public print(data: string, start: number, end: number): void {
let code: number;
let char: string; let char: string;
let code: number;
let low: number;
let chWidth: number; let chWidth: number;
const buffer: IBuffer = this._terminal.buffer; const buffer: IBuffer = this._terminal.buffer;
const charset: ICharset = this._terminal.charset; const charset: ICharset = this._terminal.charset;
@@ -326,23 +332,36 @@ export class InputHandler extends Disposable implements IInputHandler {
let bufferRow = buffer.lines.get(buffer.y + buffer.ybase); let bufferRow = buffer.lines.get(buffer.y + buffer.ybase);
this._terminal.updateRange(buffer.y); this._terminal.updateRange(buffer.y);
for (let pos = start; pos < end; ++pos) { for (let stringPosition = start; stringPosition < end; ++stringPosition) {
code = data[pos]; char = data.charAt(stringPosition);
char = stringFromCodePoint(code); code = data.charCodeAt(stringPosition);
// surrogate pair handling
if (0xD800 <= code && code <= 0xDBFF) {
// we got a surrogate high
// get surrogate low (next 2 bytes)
low = data.charCodeAt(stringPosition + 1);
if (isNaN(low)) {
// end of data stream, save surrogate high
this._surrogateHigh = char;
continue;
}
code = ((code - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;
char += data.charAt(stringPosition + 1);
}
// surrogate low - already handled above
if (0xDC00 <= code && code <= 0xDFFF) {
continue;
}
// calculate print space // calculate print space
// expensive call, therefore we save width in line buffer // expensive call, therefore we save width in line buffer
chWidth = wcwidth(code); chWidth = wcwidth(code);
// get charset replacement character // get charset replacement character
// charset are only defined for ASCII, therefore we only if (charset) {
// search for an replacement char if code < 127 char = charset[char] || char;
if (code < 127 && charset) { code = char.charCodeAt(0);
const ch = charset[char];
if (ch) {
code = ch.charCodeAt(0);
char = ch;
}
} }
if (screenReaderMode) { if (screenReaderMode) {
@@ -355,22 +374,18 @@ export class InputHandler extends Disposable implements IInputHandler {
// since they always follow a cell consuming char // since they always follow a cell consuming char
// therefore we can test for buffer.x to avoid overflow left // therefore we can test for buffer.x to avoid overflow left
if (!chWidth && buffer.x) { if (!chWidth && buffer.x) {
const chMinusOne = bufferRow.get(buffer.x - 1); if (bufferRow[buffer.x - 1]) {
if (chMinusOne) { if (!bufferRow[buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) {
if (!chMinusOne[CHAR_DATA_WIDTH_INDEX]) {
// found empty cell after fullwidth, need to go 2 cells back // found empty cell after fullwidth, need to go 2 cells back
// it is save to step 2 cells back here // it is save to step 2 cells back here
// since an empty cell is only set by fullwidth chars // since an empty cell is only set by fullwidth chars
const chMinusTwo = bufferRow.get(buffer.x - 2); if (bufferRow[buffer.x - 2]) {
if (chMinusTwo) { bufferRow[buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char;
chMinusTwo[CHAR_DATA_CHAR_INDEX] += char; bufferRow[buffer.x - 2][CHAR_DATA_CODE_INDEX] = code;
chMinusTwo[CHAR_DATA_CODE_INDEX] = code;
bufferRow.set(buffer.x - 2, chMinusTwo); // must be set explicitly now
} }
} else { } else {
chMinusOne[CHAR_DATA_CHAR_INDEX] += char; bufferRow[buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char;
chMinusOne[CHAR_DATA_CODE_INDEX] = code; bufferRow[buffer.x - 1][CHAR_DATA_CODE_INDEX] = code;
bufferRow.set(buffer.x - 1, chMinusOne); // must be set explicitly now
} }
} }
continue; continue;
@@ -378,9 +393,6 @@ export class InputHandler extends Disposable implements IInputHandler {
// goto next line if ch would overflow // goto next line if ch would overflow
// TODO: needs a global min terminal width of 2 // TODO: needs a global min terminal width of 2
// FIXME: additionally ensure chWidth fits into a line
// --> maybe forbid cols<xy at higher level as it would
// introduce a bad runtime penalty here
if (buffer.x + chWidth - 1 >= cols) { if (buffer.x + chWidth - 1 >= cols) {
// autowrap - DECAWM // autowrap - DECAWM
// automatically wraps to the beginning of the next line // automatically wraps to the beginning of the next line
@@ -393,7 +405,7 @@ export class InputHandler extends Disposable implements IInputHandler {
} else { } else {
// The line already exists (eg. the initial viewport), mark it as a // The line already exists (eg. the initial viewport), mark it as a
// wrapped line // wrapped line
buffer.lines.get(buffer.y).isWrapped = true; (<any>buffer.lines.get(buffer.y)).isWrapped = true;
} }
// row changed, get it again // row changed, get it again
bufferRow = buffer.lines.get(buffer.y + buffer.ybase); bufferRow = buffer.lines.get(buffer.y + buffer.ybase);
@@ -408,40 +420,36 @@ export class InputHandler extends Disposable implements IInputHandler {
} }
// insert mode: move characters to right // insert mode: move characters to right
// To achieve insert, we remove cells from the right
// and insert empty ones at cursor position
if (insertMode) { if (insertMode) {
// right shift cells according to the width // do this twice for a fullwidth char
bufferRow.insertCells(buffer.x, chWidth, [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); for (let moves = 0; moves < chWidth; ++moves) {
// test last cell - since the last cell has only room for // remove last cell
// a halfwidth char any fullwidth shifted there is lost // if it's width is 0, we have to adjust the second last cell as well
// and will be set to eraseChar const removed = bufferRow.pop();
const lastCell = bufferRow.get(cols - 1); if (removed[CHAR_DATA_WIDTH_INDEX] === 0
if (lastCell[CHAR_DATA_WIDTH_INDEX] === 2) { && bufferRow[this._terminal.cols - 2]
bufferRow.set(cols - 1, [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); && 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, ' ', 1, 32 /* ' '.charCodeAt(0) */ ]);
} }
} }
// write current char to buffer and advance cursor // 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 // fullwidth char - also set next cell to placeholder stub and advance cursor
// for graphemes bigger than fullwidth we can simply loop to zero if (chWidth === 2) {
// we already made sure above, that buffer.x + chWidth will not overflow right bufferRow[buffer.x++] = [curAttr, '', 0, undefined];
if (chWidth > 0) {
while (--chWidth) {
bufferRow.set(buffer.x++, [curAttr, '', 0, undefined]);
}
} }
} }
this._terminal.updateRange(buffer.y); this._terminal.updateRange(buffer.y);
} }
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable {
return this._parser.addCsiHandler(flag, callback);
}
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
return this._parser.addOscHandler(ident, callback);
}
/** /**
* BEL * BEL
* Bell (Ctrl-G). * Bell (Ctrl-G).
@@ -458,7 +466,7 @@ export class InputHandler extends Disposable implements IInputHandler {
// make buffer local for faster access // make buffer local for faster access
const buffer = this._terminal.buffer; const buffer = this._terminal.buffer;
if (this._terminal.options.convertEol) { if (this._terminal.convertEol) {
buffer.x = 0; buffer.x = 0;
} }
buffer.y++; buffer.y++;
@@ -531,12 +539,20 @@ export class InputHandler extends Disposable implements IInputHandler {
* Insert Ps (Blank) Character(s) (default = 1) (ICH). * Insert Ps (Blank) Character(s) (default = 1) (ICH).
*/ */
public insertChars(params: number[]): void { public insertChars(params: number[]): void {
this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).insertCells( let param = params[0];
this._terminal.buffer.x, if (param < 1) param = 1;
params[0] || 1,
[this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE] // make buffer local for faster access
); const buffer = this._terminal.buffer;
this._terminal.updateRange(this._terminal.buffer.y);
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();
}
} }
/** /**
@@ -696,34 +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, clearWrap: boolean = false): void {
const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + y);
line.replaceCells(
start,
end,
[this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
);
if (clearWrap) {
line.isWrapped = false;
}
}
/**
* Helper method to reset cells in a terminal row.
* The cell gets replaced with the eraseChar of the terminal and the isWrapped property is set to false.
* @param y row index
*/
private _resetBufferLine(y: number): void {
this._eraseInBufferLine(y, 0, this._terminal.cols, true);
}
/** /**
* CSI Ps J Erase in Display (ED). * CSI Ps J Erase in Display (ED).
* Ps = 0 -> Erase Below (default). * Ps = 0 -> Erase Below (default).
@@ -740,35 +728,22 @@ export class InputHandler extends Disposable implements IInputHandler {
let j; let j;
switch (params[0]) { switch (params[0]) {
case 0: case 0:
j = this._terminal.buffer.y; this._terminal.eraseRight(this._terminal.buffer.x, this._terminal.buffer.y);
this._terminal.updateRange(j); j = this._terminal.buffer.y + 1;
this._eraseInBufferLine(j++, this._terminal.buffer.x, this._terminal.cols, this._terminal.buffer.x === 0);
for (; j < this._terminal.rows; j++) { for (; j < this._terminal.rows; j++) {
this._resetBufferLine(j); this._terminal.eraseLine(j);
} }
this._terminal.updateRange(j);
break; break;
case 1: case 1:
this._terminal.eraseLeft(this._terminal.buffer.x, this._terminal.buffer.y);
j = this._terminal.buffer.y; j = this._terminal.buffer.y;
this._terminal.updateRange(j);
// Deleted front part of line and everything before. This line will no longer be wrapped.
this._eraseInBufferLine(j, 0, this._terminal.buffer.x + 1, true);
if (this._terminal.buffer.x + 1 >= this._terminal.cols) {
// Deleted entire previous line. This next line can no longer be wrapped.
this._terminal.buffer.lines.get(j + 1).isWrapped = false;
}
while (j--) { while (j--) {
this._resetBufferLine(j); this._terminal.eraseLine(j);
} }
this._terminal.updateRange(0);
break; break;
case 2: case 2:
j = this._terminal.rows; j = this._terminal.rows;
this._terminal.updateRange(j - 1); while (j--) this._terminal.eraseLine(j);
while (j--) {
this._resetBufferLine(j);
}
this._terminal.updateRange(0);
break; break;
case 3: case 3:
// Clear scrollback (everything not in viewport) // Clear scrollback (everything not in viewport)
@@ -798,16 +773,15 @@ export class InputHandler extends Disposable implements IInputHandler {
public eraseInLine(params: number[]): void { public eraseInLine(params: number[]): void {
switch (params[0]) { switch (params[0]) {
case 0: case 0:
this._eraseInBufferLine(this._terminal.buffer.y, this._terminal.buffer.x, this._terminal.cols); this._terminal.eraseRight(this._terminal.buffer.x, this._terminal.buffer.y);
break; break;
case 1: 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; break;
case 2: case 2:
this._eraseInBufferLine(this._terminal.buffer.y, 0, this._terminal.cols); this._terminal.eraseLine(this._terminal.buffer.y);
break; break;
} }
this._terminal.updateRange(this._terminal.buffer.y);
} }
/** /**
@@ -831,7 +805,7 @@ export class InputHandler extends Disposable implements IInputHandler {
// test: echo -e '\e[44m\e[1L\e[0m' // test: echo -e '\e[44m\e[1L\e[0m'
// blankLine(true) - xterm/linux behavior // blankLine(true) - xterm/linux behavior
buffer.lines.splice(scrollBottomAbsolute - 1, 1); buffer.lines.splice(scrollBottomAbsolute - 1, 1);
buffer.lines.splice(row, 0, buffer.getBlankLine(this._terminal.eraseAttr())); buffer.lines.splice(row, 0, this._terminal.blankLine(true));
} }
// this.maxRange(); // this.maxRange();
@@ -861,7 +835,7 @@ export class InputHandler extends Disposable implements IInputHandler {
// test: echo -e '\e[44m\e[1M\e[0m' // test: echo -e '\e[44m\e[1M\e[0m'
// blankLine(true) - xterm/linux behavior // blankLine(true) - xterm/linux behavior
buffer.lines.splice(row, 1); buffer.lines.splice(row, 1);
buffer.lines.splice(j, 0, buffer.getBlankLine(this._terminal.eraseAttr())); buffer.lines.splice(j, 0, this._terminal.blankLine(true));
} }
// this.maxRange(); // this.maxRange();
@@ -874,12 +848,22 @@ export class InputHandler extends Disposable implements IInputHandler {
* Delete Ps Character(s) (default = 1) (DCH). * Delete Ps Character(s) (default = 1) (DCH).
*/ */
public deleteChars(params: number[]): void { public deleteChars(params: number[]): void {
this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).deleteCells( let param: number = params[0];
this._terminal.buffer.x, if (param < 1) {
params[0] || 1, param = 1;
[this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE] }
);
this._terminal.updateRange(this._terminal.buffer.y); // 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);
} }
/** /**
@@ -893,7 +877,7 @@ export class InputHandler extends Disposable implements IInputHandler {
while (param--) { while (param--) {
buffer.lines.splice(buffer.ybase + buffer.scrollTop, 1); buffer.lines.splice(buffer.ybase + buffer.scrollTop, 1);
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(DEFAULT_ATTR)); buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, this._terminal.blankLine());
} }
// this.maxRange(); // this.maxRange();
this._terminal.updateRange(buffer.scrollTop); this._terminal.updateRange(buffer.scrollTop);
@@ -912,7 +896,7 @@ export class InputHandler extends Disposable implements IInputHandler {
while (param--) { while (param--) {
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1); buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1);
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(DEFAULT_ATTR)); buffer.lines.splice(buffer.ybase + buffer.scrollTop, 0, this._terminal.blankLine());
} }
// this.maxRange(); // this.maxRange();
this._terminal.updateRange(buffer.scrollTop); this._terminal.updateRange(buffer.scrollTop);
@@ -925,11 +909,21 @@ export class InputHandler extends Disposable implements IInputHandler {
* Erase Ps Character(s) (default = 1) (ECH). * Erase Ps Character(s) (default = 1) (ECH).
*/ */
public eraseChars(params: number[]): void { public eraseChars(params: number[]): void {
this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).replaceCells( let param = params[0];
this._terminal.buffer.x, if (param < 1) {
this._terminal.buffer.x + (params[0] || 1), param = 1;
[this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE] }
);
// make buffer local for faster access
const buffer = this._terminal.buffer;
const row = buffer.y + buffer.ybase;
let j = buffer.x;
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm
while (param-- && j < this._terminal.cols) {
buffer.lines.get(row)[j++] = ch;
}
} }
/** /**
@@ -981,14 +975,17 @@ export class InputHandler extends Disposable implements IInputHandler {
* CSI Ps b Repeat the preceding graphic character Ps times (REP). * CSI Ps b Repeat the preceding graphic character Ps times (REP).
*/ */
public repeatPrecedingCharacter(params: number[]): void { public repeatPrecedingCharacter(params: number[]): void {
let param = params[0] || 1;
// make buffer local for faster access // make buffer local for faster access
const buffer = this._terminal.buffer; const buffer = this._terminal.buffer;
const line = buffer.lines.get(buffer.ybase + buffer.y); const line = buffer.lines.get(buffer.ybase + buffer.y);
line.replaceCells(buffer.x, const ch = line[buffer.x - 1] || [DEFAULT_ATTR, ' ', 1, 32];
buffer.x + (params[0] || 1),
line.get(buffer.x - 1) || [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE] while (param--) {
); line[buffer.x++] = ch;
// FIXME: no updateRange here? }
} }
/** /**
@@ -1035,24 +1032,24 @@ export class InputHandler extends Disposable implements IInputHandler {
if (!collect) { if (!collect) {
if (this._terminal.is('xterm') || this._terminal.is('rxvt-unicode') || this._terminal.is('screen')) { 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')) { } else if (this._terminal.is('linux')) {
this._terminal.handler(C0.ESC + '[?6c'); this._terminal.send(C0.ESC + '[?6c');
} }
} else if (collect === '>') { } else if (collect === '>') {
// xterm and urxvt // xterm and urxvt
// seem to spit this // seem to spit this
// out around ~370 times (?). // out around ~370 times (?).
if (this._terminal.is('xterm')) { 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')) { } 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')) { } else if (this._terminal.is('linux')) {
// not supported by linux console. // not supported by linux console.
// linux console echoes parameters. // linux console echoes parameters.
this._terminal.handler(params[0] + 'c'); this._terminal.send(params[0] + 'c');
} else if (this._terminal.is('screen')) { } else if (this._terminal.is('screen')) {
this._terminal.handler(C0.ESC + '[>83;40003;0c'); this._terminal.send(C0.ESC + '[>83;40003;0c');
} }
} }
} }
@@ -1261,9 +1258,7 @@ export class InputHandler extends Disposable implements IInputHandler {
case 66: case 66:
this._terminal.log('Serial port requested application keypad.'); this._terminal.log('Serial port requested application keypad.');
this._terminal.applicationKeypad = true; this._terminal.applicationKeypad = true;
if (this._terminal.viewport) { this._terminal.viewport.syncScrollArea();
this._terminal.viewport.syncScrollArea();
}
break; break;
case 9: // X10 Mouse case 9: // X10 Mouse
// no release, no motion, no wheel, no modifiers. // no release, no motion, no wheel, no modifiers.
@@ -1281,12 +1276,8 @@ export class InputHandler extends Disposable implements IInputHandler {
this._terminal.vt200Mouse = params[0] === 1000; this._terminal.vt200Mouse = params[0] === 1000;
this._terminal.normalMouse = params[0] > 1000; this._terminal.normalMouse = params[0] > 1000;
this._terminal.mouseEvents = true; this._terminal.mouseEvents = true;
if (this._terminal.element) { this._terminal.element.classList.add('enable-mouse-events');
this._terminal.element.classList.add('enable-mouse-events'); this._terminal.selectionManager.disable();
}
if (this._terminal.selectionManager) {
this._terminal.selectionManager.disable();
}
this._terminal.log('Binding to mouse events.'); this._terminal.log('Binding to mouse events.');
break; break;
case 1004: // send focusin/focusout events case 1004: // send focusin/focusout events
@@ -1316,19 +1307,14 @@ export class InputHandler extends Disposable implements IInputHandler {
case 25: // show cursor case 25: // show cursor
this._terminal.cursorHidden = false; this._terminal.cursorHidden = false;
break; break;
case 1048: // alt screen cursor
this.saveCursor(params);
break;
case 1049: // alt screen buffer cursor case 1049: // alt screen buffer cursor
this.saveCursor(params); // TODO: Not sure if we need to save/restore after switching the buffer
// this.saveCursor(params);
// FALL-THROUGH // FALL-THROUGH
case 47: // alt screen buffer case 47: // alt screen buffer
case 1047: // alt screen buffer case 1047: // alt screen buffer
this._terminal.buffers.activateAltBuffer(this._terminal.eraseAttr()); this._terminal.buffers.activateAltBuffer();
this._terminal.refresh(0, this._terminal.rows - 1); this._terminal.viewport.syncScrollArea();
if (this._terminal.viewport) {
this._terminal.viewport.syncScrollArea();
}
this._terminal.showCursor(); this._terminal.showCursor();
break; break;
case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste) case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste)
@@ -1461,9 +1447,7 @@ export class InputHandler extends Disposable implements IInputHandler {
case 66: case 66:
this._terminal.log('Switching back to normal keypad.'); this._terminal.log('Switching back to normal keypad.');
this._terminal.applicationKeypad = false; this._terminal.applicationKeypad = false;
if (this._terminal.viewport) { this._terminal.viewport.syncScrollArea();
this._terminal.viewport.syncScrollArea();
}
break; break;
case 9: // X10 Mouse case 9: // X10 Mouse
case 1000: // vt200 mouse case 1000: // vt200 mouse
@@ -1473,12 +1457,8 @@ export class InputHandler extends Disposable implements IInputHandler {
this._terminal.vt200Mouse = false; this._terminal.vt200Mouse = false;
this._terminal.normalMouse = false; this._terminal.normalMouse = false;
this._terminal.mouseEvents = false; this._terminal.mouseEvents = false;
if (this._terminal.element) { this._terminal.element.classList.remove('enable-mouse-events');
this._terminal.element.classList.remove('enable-mouse-events'); this._terminal.selectionManager.enable();
}
if (this._terminal.selectionManager) {
this._terminal.selectionManager.enable();
}
break; break;
case 1004: // send focusin/focusout events case 1004: // send focusin/focusout events
this._terminal.sendFocus = false; this._terminal.sendFocus = false;
@@ -1495,22 +1475,18 @@ export class InputHandler extends Disposable implements IInputHandler {
case 25: // hide cursor case 25: // hide cursor
this._terminal.cursorHidden = true; this._terminal.cursorHidden = true;
break; break;
case 1048: // alt screen cursor
this.restoreCursor(params);
break;
case 1049: // alt screen buffer cursor case 1049: // alt screen buffer cursor
// FALL-THROUGH // FALL-THROUGH
case 47: // normal screen buffer case 47: // normal screen buffer
case 1047: // normal screen buffer - clearing it first case 1047: // normal screen buffer - clearing it first
// Ensure the selection manager has the correct buffer // Ensure the selection manager has the correct buffer
this._terminal.buffers.activateNormalBuffer(); this._terminal.buffers.activateNormalBuffer();
if (params[0] === 1049) { // TODO: Not sure if we need to save/restore after switching the buffer
this.restoreCursor(params); // if (params[0] === 1049) {
} // this.restoreCursor(params);
// }
this._terminal.refresh(0, this._terminal.rows - 1); this._terminal.refresh(0, this._terminal.rows - 1);
if (this._terminal.viewport) { this._terminal.viewport.syncScrollArea();
this._terminal.viewport.syncScrollArea();
}
this._terminal.showCursor(); this._terminal.showCursor();
break; break;
case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste) case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste)
@@ -1648,9 +1624,6 @@ export class InputHandler extends Disposable implements IInputHandler {
// not bold nor faint // not bold nor faint
flags &= ~FLAGS.BOLD; flags &= ~FLAGS.BOLD;
flags &= ~FLAGS.DIM; flags &= ~FLAGS.DIM;
} else if (p === 23) {
// not italic
flags &= ~FLAGS.ITALIC;
} else if (p === 24) { } else if (p === 24) {
// not underlined // not underlined
flags &= ~FLAGS.UNDERLINE; flags &= ~FLAGS.UNDERLINE;
@@ -1739,13 +1712,15 @@ export class InputHandler extends Disposable implements IInputHandler {
switch (params[0]) { switch (params[0]) {
case 5: case 5:
// status report // status report
this._terminal.emit('data', `${C0.ESC}[0n`); this._terminal.send(C0.ESC + '[0n');
break; break;
case 6: case 6:
// cursor position // cursor position
const y = this._terminal.buffer.y + 1; this._terminal.send(C0.ESC + '['
const x = this._terminal.buffer.x + 1; + (this._terminal.buffer.y + 1)
this._terminal.emit('data', `${C0.ESC}[${y};${x}R`); + ';'
+ (this._terminal.buffer.x + 1)
+ 'R');
break; break;
} }
} else if (collect === '?') { } else if (collect === '?') {
@@ -1754,25 +1729,27 @@ export class InputHandler extends Disposable implements IInputHandler {
switch (params[0]) { switch (params[0]) {
case 6: case 6:
// cursor position // cursor position
const y = this._terminal.buffer.y + 1; this._terminal.send(C0.ESC + '[?'
const x = this._terminal.buffer.x + 1; + (this._terminal.buffer.y + 1)
this._terminal.emit('data', `${C0.ESC}[?${y};${x}R`); + ';'
+ (this._terminal.buffer.x + 1)
+ 'R');
break; break;
case 15: case 15:
// no printer // no printer
// this.handler(C0.ESC + '[?11n'); // this.send(C0.ESC + '[?11n');
break; break;
case 25: case 25:
// dont support user defined keys // dont support user defined keys
// this.handler(C0.ESC + '[?21n'); // this.send(C0.ESC + '[?21n');
break; break;
case 26: case 26:
// north american keyboard // north american keyboard
// this.handler(C0.ESC + '[?27;1;0;0n'); // this.send(C0.ESC + '[?27;1;0;0n');
break; break;
case 53: case 53:
// no dec locator/mouse // no dec locator/mouse
// this.handler(C0.ESC + '[?50n'); // this.send(C0.ESC + '[?50n');
break; break;
} }
} }
@@ -1789,9 +1766,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._terminal.originMode = false; this._terminal.originMode = false;
this._terminal.wraparoundMode = true; // defaults: xterm - true, vt100 - false this._terminal.wraparoundMode = true; // defaults: xterm - true, vt100 - false
this._terminal.applicationKeypad = false; // ? this._terminal.applicationKeypad = false; // ?
if (this._terminal.viewport) { this._terminal.viewport.syncScrollArea();
this._terminal.viewport.syncScrollArea();
}
this._terminal.applicationCursor = false; this._terminal.applicationCursor = false;
this._terminal.buffer.scrollTop = 0; this._terminal.buffer.scrollTop = 0;
this._terminal.buffer.scrollBottom = this._terminal.rows - 1; this._terminal.buffer.scrollBottom = this._terminal.rows - 1;
@@ -1858,7 +1833,7 @@ export class InputHandler extends Disposable implements IInputHandler {
public saveCursor(params: number[]): void { public saveCursor(params: number[]): void {
this._terminal.buffer.savedX = this._terminal.buffer.x; this._terminal.buffer.savedX = this._terminal.buffer.x;
this._terminal.buffer.savedY = this._terminal.buffer.y; this._terminal.buffer.savedY = this._terminal.buffer.y;
this._terminal.buffer.savedCurAttr = this._terminal.curAttr; this._terminal.savedCurAttr = this._terminal.curAttr;
} }
@@ -1870,7 +1845,7 @@ export class InputHandler extends Disposable implements IInputHandler {
public restoreCursor(params: number[]): void { public restoreCursor(params: number[]): void {
this._terminal.buffer.x = this._terminal.buffer.savedX || 0; this._terminal.buffer.x = this._terminal.buffer.savedX || 0;
this._terminal.buffer.y = this._terminal.buffer.savedY || 0; this._terminal.buffer.y = this._terminal.buffer.savedY || 0;
this._terminal.curAttr = this._terminal.buffer.savedCurAttr || DEFAULT_ATTR; this._terminal.curAttr = this._terminal.savedCurAttr || DEFAULT_ATTR;
} }
+11 -108
View File
@@ -5,11 +5,10 @@
import { assert } from 'chai'; import { assert } from 'chai';
import { IMouseZoneManager, IMouseZone } from './ui/Types'; import { IMouseZoneManager, IMouseZone } from './ui/Types';
import { ILinkMatcher, ITerminal, IBufferLine } from './Types'; import { ILinkMatcher, LineData, ITerminal } from './Types';
import { Linkifier } from './Linkifier'; import { Linkifier } from './Linkifier';
import { MockBuffer, MockTerminal, TestTerminal } from './ui/TestUtils.test'; import { MockBuffer, MockTerminal } from './utils/TestUtils.test';
import { CircularList } from './common/CircularList'; import { CircularList } from './common/CircularList';
import { BufferLine } from './BufferLine';
class TestLinkifier extends Linkifier { class TestLinkifier extends Linkifier {
constructor(terminal: ITerminal) { constructor(terminal: ITerminal) {
@@ -41,19 +40,18 @@ describe('Linkifier', () => {
beforeEach(() => { beforeEach(() => {
terminal = new MockTerminal(); terminal = new MockTerminal();
(terminal as any).cols = 100; terminal.cols = 100;
(terminal as any).rows = 10;
terminal.buffer = new MockBuffer(); terminal.buffer = new MockBuffer();
(<MockBuffer>terminal.buffer).setLines(new CircularList<IBufferLine>(20)); (<MockBuffer>terminal.buffer).setLines(new CircularList<LineData>(20));
terminal.buffer.ydisp = 0; terminal.buffer.ydisp = 0;
linkifier = new TestLinkifier(terminal); linkifier = new TestLinkifier(terminal);
mouseZoneManager = new TestMouseZoneManager(); mouseZoneManager = new TestMouseZoneManager();
}); });
function stringToRow(text: string): IBufferLine { function stringToRow(text: string): LineData {
const result = new BufferLine(text.length); const result: LineData = [];
for (let i = 0; i < text.length; i++) { for (let i = 0; i < text.length; i++) {
result.set(i, [0, text.charAt(i), 1, text.charCodeAt(i)]); result.push([0, text.charAt(i), 1, text.charCodeAt(i)]);
} }
return result; return result;
} }
@@ -65,7 +63,6 @@ describe('Linkifier', () => {
function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, links: {x: number, length: number}[], done: MochaDone): void { function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, links: {x: number, length: number}[], done: MochaDone): void {
addRow(rowText); addRow(rowText);
linkifier.registerLinkMatcher(linkMatcherRegex, () => {}); linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
(terminal as any).rows = terminal.buffer.lines.length - 1;
linkifier.linkifyRows(); linkifier.linkifyRows();
// Allow linkify to happen // Allow linkify to happen
setTimeout(() => { setTimeout(() => {
@@ -142,19 +139,19 @@ describe('Linkifier', () => {
}); });
describe('multi-line links', () => { describe('multi-line links', () => {
it('should match links that start on line 1/2 of a wrapped line and end on the last character of line 1/2', done => { it('should match links that start on line 1/2 of a wrapped line and end on the last character of line 1/2', done => {
(terminal as any).cols = 4; terminal.cols = 4;
assertLinkifiesMultiLineLink('12345', /1234/, [{x1: 0, x2: 4, y1: 0, y2: 0}], done); assertLinkifiesMultiLineLink('12345', /1234/, [{x1: 0, x2: 4, y1: 0, y2: 0}], done);
}); });
it('should match links that start on line 1/2 of a wrapped line and wrap to line 2/2', done => { it('should match links that start on line 1/2 of a wrapped line and wrap to line 2/2', done => {
(terminal as any).cols = 4; terminal.cols = 4;
assertLinkifiesMultiLineLink('12345', /12345/, [{x1: 0, x2: 1, y1: 0, y2: 1}], done); assertLinkifiesMultiLineLink('12345', /12345/, [{x1: 0, x2: 1, y1: 0, y2: 1}], done);
}); });
it('should match links that start and end on line 2/2 of a wrapped line', done => { it('should match links that start and end on line 2/2 of a wrapped line', done => {
(terminal as any).cols = 4; terminal.cols = 4;
assertLinkifiesMultiLineLink('12345678', /5678/, [{x1: 0, x2: 4, y1: 1, y2: 1}], done); assertLinkifiesMultiLineLink('12345678', /5678/, [{x1: 0, x2: 4, y1: 1, y2: 1}], done);
}); });
it('should match links that start on line 2/3 of a wrapped line and wrap to line 3/3', done => { it('should match links that start on line 2/3 of a wrapped line and wrap to line 3/3', done => {
(terminal as any).cols = 4; terminal.cols = 4;
assertLinkifiesMultiLineLink('123456789', /56789/, [{x1: 0, x2: 1, y1: 1, y2: 2}], done); assertLinkifiesMultiLineLink('123456789', /56789/, [{x1: 0, x2: 1, y1: 1, y2: 2}], done);
}); });
}); });
@@ -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 -111
View File
@@ -4,11 +4,9 @@
*/ */
import { IMouseZoneManager } from './ui/Types'; 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 { MouseZone } from './ui/MouseZoneManager';
import { EventEmitter } from './common/EventEmitter'; import { EventEmitter } from './EventEmitter';
import { CHAR_DATA_ATTR_INDEX } from './Buffer';
import { getStringCellWidth } from './CharWidth';
/** /**
* The Linkifier applies links to rows shortly after they have been refreshed. * The Linkifier applies links to rows shortly after they have been refreshed.
@@ -21,19 +19,12 @@ export class Linkifier extends EventEmitter implements ILinkifier {
*/ */
protected static readonly TIME_BEFORE_LINKIFY = 200; protected static readonly TIME_BEFORE_LINKIFY = 200;
/**
* Limit of the unwrapping line expansion (overscan) at the top and bottom
* of the actual viewport in ASCII characters.
* A limit of 2000 should match most sane urls.
*/
protected static readonly OVERSCAN_CHAR_LIMIT = 2000;
protected _linkMatchers: ILinkMatcher[] = []; protected _linkMatchers: ILinkMatcher[] = [];
private _mouseZoneManager: IMouseZoneManager; private _mouseZoneManager: IMouseZoneManager;
private _rowsTimeoutId: number; private _rowsTimeoutId: number;
private _nextLinkMatcherId = 0; private _nextLinkMatcherId = 0;
private _rowsToLinkify: { start: number, end: number }; private _rowsToLinkify: {start: number, end: number};
constructor( constructor(
protected _terminal: ITerminal protected _terminal: ITerminal
@@ -88,37 +79,9 @@ export class Linkifier extends EventEmitter implements ILinkifier {
*/ */
private _linkifyRows(): void { private _linkifyRows(): void {
this._rowsTimeoutId = null; this._rowsTimeoutId = null;
const buffer = this._terminal.buffer; for (let i = this._rowsToLinkify.start; i <= this._rowsToLinkify.end; i++) {
this._linkifyRow(i);
// Ensure the start row exists
const absoluteRowIndexStart = buffer.ydisp + this._rowsToLinkify.start;
if (absoluteRowIndexStart >= buffer.lines.length) {
return;
} }
// Invalidate bad end row values (if a resize happened)
const absoluteRowIndexEnd = buffer.ydisp + Math.min(this._rowsToLinkify.end, this._terminal.rows) + 1;
// Iterate over the range of unwrapped content strings within start..end
// (excluding).
// _doLinkifyRow gets full unwrapped lines with the start row as buffer offset
// for every matcher.
// The unwrapping is needed to also match content that got wrapped across
// several buffer lines. To avoid a worst case scenario where the whole buffer
// contains just a single unwrapped string we limit this line expansion beyond
// the viewport to +OVERSCAN_CHAR_LIMIT chars (overscan) at top and bottom.
// This comes with the tradeoff that matches longer than OVERSCAN_CHAR_LIMIT
// chars will not match anymore at the viewport borders.
const overscanLineLimit = Math.ceil(Linkifier.OVERSCAN_CHAR_LIMIT / this._terminal.cols);
const iterator = this._terminal.buffer.iterator(
false, absoluteRowIndexStart, absoluteRowIndexEnd, overscanLineLimit, overscanLineLimit);
while (iterator.hasNext()) {
const lineData: IBufferStringIteratorResult = iterator.next();
for (let i = 0; i < this._linkMatchers.length; i++) {
this._doLinkifyRow(lineData.range.first, lineData.content, this._linkMatchers[i]);
}
}
this._rowsToLinkify.start = null; this._rowsToLinkify.start = null;
this._rowsToLinkify.end = null; this._rowsToLinkify.end = null;
} }
@@ -190,68 +153,81 @@ export class Linkifier extends EventEmitter implements ILinkifier {
} }
/** /**
* Linkifies a row given a specific handler. * Linkifies a row.
* @param rowIndex The row index to linkify (absolute index). * @param rowIndex The index of the row to linkify.
* @param text string content of the unwrapped row.
* @param matcher The link matcher for this line.
*/ */
private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher): void { private _linkifyRow(rowIndex: number): void {
// clone regex to do a global search on text // Ensure the row exists
const rex = new RegExp(matcher.regex.source, matcher.regex.flags + 'g'); let absoluteRowIndex = this._terminal.buffer.ydisp + rowIndex;
let match; if (absoluteRowIndex >= this._terminal.buffer.lines.length) {
let stringIndex = -1; return;
while ((match = rex.exec(text)) !== null) { }
const uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex];
if (!uri) { if ((<any>this._terminal.buffer.lines.get(absoluteRowIndex)).isWrapped) {
// something matched but does not comply with the given matchIndex // Only attempt to linkify rows that start in the viewport
// since this is most likely a bug the regex itself we simply do nothing here if (rowIndex !== 0) {
// DEBUG: print match and throw return;
if ((<any>this._terminal).debug) { }
console.log({match, matcher}); // If the first row is wrapped, backtrack to find the origin row and linkify that
throw new Error('match found without corresponding matchIndex'); 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 // Recursively check for links in the rest of the text
// therefore we cannot use match.index directly, instead we search the position const remainingStartIndex = index + uri.length;
// of the match group in text again const remainingText = text.substr(remainingStartIndex);
// also correct regex and string search offsets for the next loop run if (remainingText.length > 0) {
stringIndex = text.indexOf(uri, stringIndex + 1); this._doLinkifyRow(rowIndex, remainingText, matcher, offset + remainingStartIndex);
rex.lastIndex = stringIndex + uri.length;
if (stringIndex < 0) {
// invalid stringIndex (should not have happened)
break;
}
// get the buffer index as [absolute row, col] for the match
const bufferIndex = this._terminal.buffer.stringIndexToBufferIndex(rowIndex, stringIndex);
if (bufferIndex[0] < 0) {
// invalid bufferIndex (should not have happened)
break;
}
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);
}
} }
} }
@@ -261,14 +237,12 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* @param y The row the link is on. * @param y The row the link is on.
* @param uri The URI of the link. * @param uri The URI of the link.
* @param matcher The link matcher for the link. * @param matcher The link matcher for the link.
* @param fg The link color for hover event.
*/ */
private _addLink(x: number, y: number, uri: string, matcher: ILinkMatcher, fg: number): void { private _addLink(x: number, y: number, uri: string, matcher: ILinkMatcher): void {
const width = getStringCellWidth(uri);
const x1 = x % this._terminal.cols; const x1 = x % this._terminal.cols;
const y1 = y + Math.floor(x / this._terminal.cols); const y1 = y + Math.floor(x / this._terminal.cols);
let x2 = (x1 + width) % this._terminal.cols; let x2 = (x1 + uri.length) % this._terminal.cols;
let y2 = y1 + Math.floor((x1 + width) / this._terminal.cols); let y2 = y1 + Math.floor((x1 + uri.length) / this._terminal.cols);
if (x2 === 0) { if (x2 === 0) {
x2 = this._terminal.cols; x2 = this._terminal.cols;
y2--; y2--;
@@ -286,17 +260,17 @@ export class Linkifier extends EventEmitter implements ILinkifier {
window.open(uri, '_blank'); window.open(uri, '_blank');
}, },
e => { e => {
this.emit(LinkHoverEventTypes.HOVER, this._createLinkHoverEvent(x1, y1, x2, y2, fg)); this.emit(LinkHoverEventTypes.HOVER, this._createLinkHoverEvent(x1, y1, x2, y2));
this._terminal.element.classList.add('xterm-cursor-pointer'); this._terminal.element.classList.add('xterm-cursor-pointer');
}, },
e => { e => {
this.emit(LinkHoverEventTypes.TOOLTIP, this._createLinkHoverEvent(x1, y1, x2, y2, fg)); this.emit(LinkHoverEventTypes.TOOLTIP, this._createLinkHoverEvent(x1, y1, x2, y2));
if (matcher.hoverTooltipCallback) { if (matcher.hoverTooltipCallback) {
matcher.hoverTooltipCallback(e, uri); 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'); this._terminal.element.classList.remove('xterm-cursor-pointer');
if (matcher.hoverLeaveCallback) { if (matcher.hoverLeaveCallback) {
matcher.hoverLeaveCallback(); matcher.hoverLeaveCallback();
@@ -311,7 +285,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
)); ));
} }
private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number, fg: number): ILinkHoverEvent { private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number): ILinkHoverEvent {
return { x1, y1, x2, y2, cols: this._terminal.cols, fg }; return { x1, y1, x2, y2, cols: this._terminal.cols };
} }
} }
+12 -67
View File
@@ -8,9 +8,8 @@ import { CharMeasure } from './ui/CharMeasure';
import { SelectionManager, SelectionMode } from './SelectionManager'; import { SelectionManager, SelectionMode } from './SelectionManager';
import { SelectionModel } from './SelectionModel'; import { SelectionModel } from './SelectionModel';
import { BufferSet } from './BufferSet'; import { BufferSet } from './BufferSet';
import { ITerminal, IBuffer, IBufferLine } from './Types'; import { LineData, CharData, ITerminal, IBuffer } from './Types';
import { MockTerminal } from './ui/TestUtils.test'; import { MockTerminal } from './utils/TestUtils.test';
import { BufferLine } from './BufferLine';
class TestMockTerminal extends MockTerminal { class TestMockTerminal extends MockTerminal {
emit(event: string, data: any): void {} emit(event: string, data: any): void {}
@@ -30,7 +29,6 @@ class TestSelectionManager extends SelectionManager {
public selectLineAt(line: number): void { this._selectLineAt(line); } public selectLineAt(line: number): void { this._selectLineAt(line); }
public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords, true); } public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords, true); }
public areCoordsInSelection(coords: [number, number], start: [number, number], end: [number, number]): boolean { return this._areCoordsInSelection(coords, start, end); }
// Disable DOM interaction // Disable DOM interaction
public enable(): void {} public enable(): void {}
@@ -45,8 +43,8 @@ describe('SelectionManager', () => {
beforeEach(() => { beforeEach(() => {
terminal = new TestMockTerminal(); terminal = new TestMockTerminal();
(terminal as any).cols = 80; terminal.cols = 80;
(terminal as any).rows = 2; terminal.rows = 2;
terminal.options.scrollback = 100; terminal.options.scrollback = 100;
terminal.buffers = new BufferSet(terminal); terminal.buffers = new BufferSet(terminal);
terminal.buffer = terminal.buffers.active; terminal.buffer = terminal.buffers.active;
@@ -54,18 +52,16 @@ describe('SelectionManager', () => {
selectionManager = new TestSelectionManager(terminal, null); selectionManager = new TestSelectionManager(terminal, null);
}); });
function stringToRow(text: string): IBufferLine { function stringToRow(text: string): LineData {
const result = new BufferLine(text.length); const result: LineData = [];
for (let i = 0; i < text.length; i++) { for (let i = 0; i < text.length; i++) {
result.set(i, [0, text.charAt(i), 1, text.charCodeAt(i)]); result.push([0, text.charAt(i), 1, text.charCodeAt(i)]);
} }
return result; return result;
} }
function stringArrayToRow(chars: string[]): IBufferLine { function stringArrayToRow(chars: string[]): LineData {
const line = new BufferLine(chars.length); return chars.map(c => <CharData>[0, c, 1, c.charCodeAt(0)]);
chars.map((c, idx) => line.set(idx, [0, c, 1, c.charCodeAt(0)]));
return line;
} }
describe('_selectWordAt', () => { describe('_selectWordAt', () => {
@@ -101,7 +97,7 @@ describe('SelectionManager', () => {
}); });
it('should expand selection for wide characters', () => { it('should expand selection for wide characters', () => {
// Wide characters use a special format // Wide characters use a special format
const data: [number, string, number, number][] = [ buffer.lines.set(0, [
[null, '中', 2, '中'.charCodeAt(0)], [null, '中', 2, '中'.charCodeAt(0)],
[null, '', 0, null], [null, '', 0, null],
[null, '文', 2, '文'.charCodeAt(0)], [null, '文', 2, '文'.charCodeAt(0)],
@@ -117,10 +113,7 @@ describe('SelectionManager', () => {
[null, 'f', 1, 'f'.charCodeAt(0)], [null, 'f', 1, 'f'.charCodeAt(0)],
[null, 'o', 1, 'o'.charCodeAt(0)], [null, 'o', 1, 'o'.charCodeAt(0)],
[null, 'o', 1, 'o'.charCodeAt(0)] [null, 'o', 1, 'o'.charCodeAt(0)]
]; ]);
const line = new BufferLine(data.length);
for (let i = 0; i < data.length; ++i) line.set(i, data[i]);
buffer.lines.set(0, line);
// Ensure wide characters take up 2 columns // Ensure wide characters take up 2 columns
selectionManager.selectWordAt([0, 0]); selectionManager.selectWordAt([0, 0]);
assert.equal(selectionManager.selectionText, '中文'); assert.equal(selectionManager.selectionText, '中文');
@@ -190,42 +183,6 @@ describe('SelectionManager', () => {
selectionManager.selectWordAt([15, 0]); selectionManager.selectWordAt([15, 0]);
assert.equal(selectionManager.selectionText, 'ij"'); 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', () => { describe('emoji', () => {
it('should treat a single emoji as a word when wrapped in spaces', () => { 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 buffer.lines.set(0, stringToRow(' ⚽ a')); // The a is here to prevent the space being trimmed in selectionText
@@ -346,7 +303,7 @@ describe('SelectionManager', () => {
it('should select the entire wrapped line', () => { it('should select the entire wrapped line', () => {
buffer.lines.set(0, stringToRow('foo')); buffer.lines.set(0, stringToRow('foo'));
const line2 = stringToRow('bar'); const line2 = stringToRow('bar');
line2.isWrapped = true; (<any>line2).isWrapped = true;
buffer.lines.set(1, line2); buffer.lines.set(1, line2);
selectionManager.selectLineAt(0); selectionManager.selectLineAt(0);
assert.equal(selectionManager.selectionText, 'foobar', 'The selected text is correct'); assert.equal(selectionManager.selectionText, 'foobar', 'The selected text is correct');
@@ -479,17 +436,5 @@ describe('SelectionManager', () => {
assert.equal(selectionManager.selectionText, 'a\n😁\nc'); assert.equal(selectionManager.selectionText, 'a\n😁\nc');
}); });
}); });
describe('_areCoordsInSelection', () => {
it('should return whether coords are in the selection', () => {
assert.isFalse(selectionManager.areCoordsInSelection([0, 0], [2, 0], [2, 1]));
assert.isFalse(selectionManager.areCoordsInSelection([1, 0], [2, 0], [2, 1]));
assert.isTrue(selectionManager.areCoordsInSelection([2, 0], [2, 0], [2, 1]));
assert.isTrue(selectionManager.areCoordsInSelection([10, 0], [2, 0], [2, 1]));
assert.isTrue(selectionManager.areCoordsInSelection([0, 1], [2, 0], [2, 1]));
assert.isTrue(selectionManager.areCoordsInSelection([1, 1], [2, 0], [2, 1]));
assert.isFalse(selectionManager.areCoordsInSelection([2, 1], [2, 0], [2, 1]));
});
});
}); });
+38 -108
View File
@@ -3,14 +3,13 @@
* @license MIT * @license MIT
*/ */
import { ITerminal, ISelectionManager, IBuffer, CharData, IBufferLine } from './Types'; import { ITerminal, ISelectionManager, IBuffer, CharData, XtermListener } from './Types';
import { XtermListener } from './common/Types'; import { MouseHelper } from './utils/MouseHelper';
import { MouseHelper } from './ui/MouseHelper'; import * as Browser from './shared/utils/Browser';
import * as Browser from './core/Platform';
import { CharMeasure } from './ui/CharMeasure'; import { CharMeasure } from './ui/CharMeasure';
import { EventEmitter } from './common/EventEmitter'; import { EventEmitter } from './EventEmitter';
import { SelectionModel } from './SelectionModel'; import { SelectionModel } from './SelectionModel';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_CODE_INDEX } from './Buffer'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
import { AltClickHandler } from './handlers/AltClickHandler'; import { AltClickHandler } from './handlers/AltClickHandler';
/** /**
@@ -198,14 +197,14 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
} }
} else { } else {
// Get first row // Get first row
const startRowEndCol = start[1] === end[1] ? end[0] : undefined; const startRowEndCol = start[1] === end[1] ? end[0] : null;
result.push(this._buffer.translateBufferLineToString(start[1], true, start[0], startRowEndCol)); result.push(this._buffer.translateBufferLineToString(start[1], true, start[0], startRowEndCol));
// Get middle rows // Get middle rows
for (let i = start[1] + 1; i <= end[1] - 1; i++) { for (let i = start[1] + 1; i <= end[1] - 1; i++) {
const bufferLine = this._buffer.lines.get(i); const bufferLine = this._buffer.lines.get(i);
const lineText = this._buffer.translateBufferLineToString(i, true); const lineText = this._buffer.translateBufferLineToString(i, true);
if (bufferLine.isWrapped) { if ((<any>bufferLine).isWrapped) {
result[result.length - 1] += lineText; result[result.length - 1] += lineText;
} else { } else {
result.push(lineText); result.push(lineText);
@@ -216,7 +215,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
if (start[1] !== end[1]) { if (start[1] !== end[1]) {
const bufferLine = this._buffer.lines.get(end[1]); const bufferLine = this._buffer.lines.get(end[1]);
const lineText = this._buffer.translateBufferLineToString(end[1], true, 0, end[0]); const lineText = this._buffer.translateBufferLineToString(end[1], true, 0, end[0]);
if (bufferLine.isWrapped) { if ((<any>bufferLine).isWrapped) {
result[result.length - 1] += lineText; result[result.length - 1] += lineText;
} else { } else {
result.push(lineText); result.push(lineText);
@@ -289,14 +288,9 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
return false; return false;
} }
return this._areCoordsInSelection(coords, start, end);
}
protected _areCoordsInSelection(coords: [number, number], start: [number, number], end: [number, number]): boolean {
return (coords[1] > start[1] && coords[1] < end[1]) || return (coords[1] > start[1] && coords[1] < end[1]) ||
(start[1] === end[1] && coords[1] === start[1] && coords[0] >= start[0] && coords[0] < end[0]) || (start[1] === end[1] && coords[1] === start[1] && coords[0] > start[0] && coords[0] < end[0]) ||
(start[1] < end[1] && coords[1] === end[1] && coords[0] < end[0]) || (start[1] < end[1] && coords[1] === end[1] && coords[0] < end[0]);
(start[1] < end[1] && coords[1] === start[1] && coords[0] >= start[0]);
} }
/** /**
@@ -347,7 +341,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param event The mouse event. * @param event The mouse event.
*/ */
private _getMouseBufferCoords(event: MouseEvent): [number, number] { private _getMouseBufferCoords(event: MouseEvent): [number, number] {
const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.screenElement, this._charMeasure, this._terminal.cols, this._terminal.rows, true); const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.screenElement, this._charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows, true);
if (!coords) { if (!coords) {
return null; return null;
} }
@@ -457,10 +451,8 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* Removes the listeners that are registered when mousedown is triggered. * Removes the listeners that are registered when mousedown is triggered.
*/ */
private _removeMouseDownListeners(): void { private _removeMouseDownListeners(): void {
if (this._terminal.element.ownerDocument) { this._terminal.element.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener);
this._terminal.element.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener); this._terminal.element.ownerDocument.removeEventListener('mouseup', this._mouseUpListener);
this._terminal.element.ownerDocument.removeEventListener('mouseup', this._mouseUpListener);
}
clearInterval(this._dragScrollIntervalTimer); clearInterval(this._dragScrollIntervalTimer);
this._dragScrollIntervalTimer = null; this._dragScrollIntervalTimer = null;
} }
@@ -506,7 +498,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
// If the mouse is over the second half of a wide character, adjust the // If the mouse is over the second half of a wide character, adjust the
// selection to cover the whole character // selection to cover the whole character
const char = line.get(this._model.selectionStart[0]); const char = line[this._model.selectionStart[0]];
if (char[CHAR_DATA_WIDTH_INDEX] === 0) { if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
this._model.selectionStart[0]++; this._model.selectionStart[0]++;
} }
@@ -552,7 +544,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
*/ */
private _onMouseMove(event: MouseEvent): void { private _onMouseMove(event: MouseEvent): void {
// If the mousemove listener is active it means that a selection is // If the mousemove listener is active it means that a selection is
// currently being made, we should stop propagation to prevent mouse events // currently being made, we should stop propogation to prevent mouse events
// to be sent to the pty. // to be sent to the pty.
event.stopImmediatePropagation(); event.stopImmediatePropagation();
@@ -582,21 +574,18 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
this._dragScrollAmount = this._getMouseEventScrollAmount(event); this._dragScrollAmount = this._getMouseEventScrollAmount(event);
// If the cursor was above or below the viewport, make sure it's at the // 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 // start or end of the viewport respectively.
// NOT in column select mode. if (this._dragScrollAmount > 0) {
if (this._activeSelectionMode !== SelectionMode.COLUMN) { this._model.selectionEnd[0] = this._terminal.cols;
if (this._dragScrollAmount > 0) { } else if (this._dragScrollAmount < 0) {
this._model.selectionEnd[0] = this._terminal.cols; this._model.selectionEnd[0] = 0;
} 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 // 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 // selection. Note that selections at the very end of the line will never
// have a character. // have a character.
if (this._model.selectionEnd[1] < this._buffer.lines.length) { if (this._model.selectionEnd[1] < this._buffer.lines.length) {
const char = this._buffer.lines.get(this._model.selectionEnd[1]).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) { if (char && char[CHAR_DATA_WIDTH_INDEX] === 0) {
this._model.selectionEnd[0]++; this._model.selectionEnd[0]++;
} }
@@ -618,19 +607,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
if (this._dragScrollAmount) { if (this._dragScrollAmount) {
this._terminal.scrollLines(this._dragScrollAmount, false); this._terminal.scrollLines(this._dragScrollAmount, false);
// Re-evaluate selection // 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._dragScrollAmount > 0) {
if (this._activeSelectionMode !== SelectionMode.COLUMN) { this._model.selectionEnd = [this._terminal.cols - 1, Math.min(this._terminal.buffer.ydisp + this._terminal.rows, this._terminal.buffer.lines.length - 1)];
this._model.selectionEnd[0] = this._terminal.cols;
}
this._model.selectionEnd[1] = Math.min(this._terminal.buffer.ydisp + this._terminal.rows, this._terminal.buffer.lines.length - 1);
} else { } else {
if (this._activeSelectionMode !== SelectionMode.COLUMN) { this._model.selectionEnd = [0, this._terminal.buffer.ydisp];
this._model.selectionEnd[0] = 0;
}
this._model.selectionEnd[1] = this._terminal.buffer.ydisp;
} }
this.refresh(); this.refresh();
} }
@@ -667,10 +647,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* latter takes into account wide characters. * latter takes into account wide characters.
* @param coords The coordinates to find the 2 index for. * @param coords The coordinates to find the 2 index for.
*/ */
private _convertViewportColToCharacterIndex(bufferLine: IBufferLine, coords: [number, number]): number { private _convertViewportColToCharacterIndex(bufferLine: any, coords: [number, number]): number {
let charIndex = coords[0]; let charIndex = coords[0];
for (let i = 0; coords[0] >= i; i++) { for (let i = 0; coords[0] >= i; i++) {
const char = bufferLine.get(i); const char = bufferLine[i];
if (char[CHAR_DATA_WIDTH_INDEX] === 0) { if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
// Wide characters aren't included in the line string so decrement the // Wide characters aren't included in the line string so decrement the
// index so the index is back on the wide character. // index so the index is back on the wide character.
@@ -697,7 +677,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* Gets positional information for the word at the coordinated specified. * Gets positional information for the word at the coordinated specified.
* @param coords The coordinates to get the word at. * @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) // Ensure coords are within viewport (eg. not within scroll bar)
if (coords[0] >= this._terminal.cols) { if (coords[0] >= this._terminal.cols) {
return null; return null;
@@ -739,24 +719,24 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
// Consider the initial position, skip it and increment the wide char // Consider the initial position, skip it and increment the wide char
// variable // variable
if (bufferLine.get(startCol)[CHAR_DATA_WIDTH_INDEX] === 0) { if (bufferLine[startCol][CHAR_DATA_WIDTH_INDEX] === 0) {
leftWideCharCount++; leftWideCharCount++;
startCol--; startCol--;
} }
if (bufferLine.get(endCol)[CHAR_DATA_WIDTH_INDEX] === 2) { if (bufferLine[endCol][CHAR_DATA_WIDTH_INDEX] === 2) {
rightWideCharCount++; rightWideCharCount++;
endCol++; endCol++;
} }
// Adjust the end index for characters whose length are > 1 (emojis) // Adjust the end index for characters whose length are > 1 (emojis)
if (bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length > 1) { if (bufferLine[endCol][CHAR_DATA_CHAR_INDEX].length > 1) {
rightLongCharOffset += bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length - 1; rightLongCharOffset += bufferLine[endCol][CHAR_DATA_CHAR_INDEX].length - 1;
endIndex += bufferLine.get(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 // Expand the string in both directions until a space is hit
while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.get(startCol - 1))) { while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine[startCol - 1])) {
const char = bufferLine.get(startCol - 1); const char = bufferLine[startCol - 1];
if (char[CHAR_DATA_WIDTH_INDEX] === 0) { if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
// If the next character is a wide char, record it and skip the column // If the next character is a wide char, record it and skip the column
leftWideCharCount++; leftWideCharCount++;
@@ -770,8 +750,8 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
startIndex--; startIndex--;
startCol--; startCol--;
} }
while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.get(endCol + 1))) { while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine[endCol + 1])) {
const char = bufferLine.get(endCol + 1); const char = bufferLine[endCol + 1];
if (char[CHAR_DATA_WIDTH_INDEX] === 2) { if (char[CHAR_DATA_WIDTH_INDEX] === 2) {
// If the next character is a wide char, record it and skip the column // If the next character is a wide char, record it and skip the column
rightWideCharCount++; rightWideCharCount++;
@@ -792,7 +772,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
// Calculate the start _column_, converting the the string indexes back to // Calculate the start _column_, converting the the string indexes back to
// column coordinates. // column coordinates.
let start = const start =
startIndex // The index of the selection's start char in the line string startIndex // The index of the selection's start char in the line string
+ charOffset // The difference between the initial char's column and index + charOffset // The difference between the initial char's column and index
- leftWideCharCount // The number of wide chars left of the initial char - leftWideCharCount // The number of wide chars left of the initial char
@@ -800,7 +780,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
// Calculate the length in _columns_, converting the the string indexes back // Calculate the length in _columns_, converting the the string indexes back
// to column coordinates. // 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 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 - startIndex // The index of the selection's start char in the line string
+ leftWideCharCount // The number of wide chars left of the initial char + leftWideCharCount // The number of wide chars left of the initial char
@@ -812,34 +792,6 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
return null; 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 }; return { start, length };
} }
@@ -851,11 +803,6 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
protected _selectWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): void { protected _selectWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): void {
const wordPosition = this._getWordAt(coords, allowWhitespaceOnlySelection); const wordPosition = this._getWordAt(coords, allowWhitespaceOnlySelection);
if (wordPosition) { 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.selectionStart = [wordPosition.start, coords[1]];
this._model.selectionStartLength = wordPosition.length; this._model.selectionStartLength = wordPosition.length;
} }
@@ -868,24 +815,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
private _selectToWordAt(coords: [number, number]): void { private _selectToWordAt(coords: [number, number]): void {
const wordPosition = this._getWordAt(coords, true); const wordPosition = this._getWordAt(coords, true);
if (wordPosition) { if (wordPosition) {
let endRow = coords[1]; this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : (wordPosition.start + wordPosition.length), 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];
} }
} }
+3 -8
View File
@@ -7,7 +7,7 @@ import { assert } from 'chai';
import { ITerminal } from './Types'; import { ITerminal } from './Types';
import { SelectionModel } from './SelectionModel'; import { SelectionModel } from './SelectionModel';
import { BufferSet } from './BufferSet'; import { BufferSet } from './BufferSet';
import { MockTerminal } from './ui/TestUtils.test'; import { MockTerminal } from './utils/TestUtils.test';
class TestSelectionModel extends SelectionModel { class TestSelectionModel extends SelectionModel {
constructor( constructor(
@@ -23,8 +23,8 @@ describe('SelectionManager', () => {
beforeEach(() => { beforeEach(() => {
terminal = new MockTerminal(); terminal = new MockTerminal();
(terminal as any).cols = 80; terminal.cols = 80;
(terminal as any).rows = 2; terminal.rows = 2;
terminal.options.scrollback = 10; terminal.options.scrollback = 10;
terminal.buffers = new BufferSet(terminal); terminal.buffers = new BufferSet(terminal);
terminal.buffer = terminal.buffers.active; terminal.buffer = terminal.buffers.active;
@@ -128,11 +128,6 @@ describe('SelectionManager', () => {
model.selectionEnd = [3, 2]; model.selectionEnd = [3, 2];
assert.deepEqual(model.finalSelectionEnd, [4, 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', () => { it('should return selection end if selection end is after selection start + length', () => {
model.selectionStart = [2, 2]; model.selectionStart = [2, 2];
model.selectionStartLength = 2; model.selectionStartLength = 2;
+2 -6
View File
@@ -76,13 +76,9 @@ export class SelectionModel {
return null; 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()) { if (!this.selectionEnd || this.areSelectionValuesReversed()) {
const startPlusLength = this.selectionStart[0] + this.selectionStartLength; return [this.selectionStart[0] + this.selectionStartLength, this.selectionStart[1]];
if (startPlusLength > this._terminal.cols) {
return [startPlusLength % this._terminal.cols, this.selectionStart[1] + Math.floor(startPlusLength / this._terminal.cols)];
}
return [startPlusLength, this.selectionStart[1]];
} }
// Ensure the the word/line is selected after a double/triple click // Ensure the the word/line is selected after a double/triple click
+16 -22
View File
@@ -12,19 +12,7 @@ import { ITerminal, ISoundManager } from './Types';
export const DEFAULT_BELL_SOUND = 'data:audio/wav;base64,UklGRigBAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQBAADpAFgCwAMlBZoG/wdmCcoKRAypDQ8PbRDBEQQTOxRtFYcWlBePGIUZXhoiG88bcBz7HHIdzh0WHlMeZx51HmkeUx4WHs8dah0AHXwc3hs9G4saxRnyGBIYGBcQFv8U4RPAEoYRQBACD70NWwwHC6gJOwjWBloF7gOBAhABkf8b/qv8R/ve+Xf4Ife79W/0JfPZ8Z/wde9N7ijtE+wU6xvqM+lb6H7nw+YX5mrlxuQz5Mzje+Ma49fioeKD4nXiYeJy4pHitOL04j/jn+MN5IPkFOWs5U3mDefM55/ogOl36m7rdOyE7abuyu8D8Unyj/Pg9D/2qfcb+Yn6/vuK/Qj/lAAlAg=='; export const DEFAULT_BELL_SOUND = 'data:audio/wav;base64,UklGRigBAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQBAADpAFgCwAMlBZoG/wdmCcoKRAypDQ8PbRDBEQQTOxRtFYcWlBePGIUZXhoiG88bcBz7HHIdzh0WHlMeZx51HmkeUx4WHs8dah0AHXwc3hs9G4saxRnyGBIYGBcQFv8U4RPAEoYRQBACD70NWwwHC6gJOwjWBloF7gOBAhABkf8b/qv8R/ve+Xf4Ife79W/0JfPZ8Z/wde9N7ijtE+wU6xvqM+lb6H7nw+YX5mrlxuQz5Mzje+Ma49fioeKD4nXiYeJy4pHitOL04j/jn+MN5IPkFOWs5U3mDefM55/ogOl36m7rdOyE7abuyu8D8Unyj/Pg9D/2qfcb+Yn6/vuK/Qj/lAAlAg==';
export class SoundManager implements ISoundManager { export class SoundManager implements ISoundManager {
private static _audioContext: AudioContext; private _audioContext: AudioContext;
static get audioContext(): AudioContext | null {
if (!SoundManager._audioContext) {
const audioContextCtor: typeof AudioContext = (<any>window).AudioContext || (<any>window).webkitAudioContext;
if (!audioContextCtor) {
console.warn('Web Audio API is not supported by this browser. Consider upgrading to the latest version');
return null;
}
SoundManager._audioContext = new audioContextCtor();
}
return SoundManager._audioContext;
}
constructor( constructor(
private _terminal: ITerminal private _terminal: ITerminal
@@ -32,16 +20,22 @@ export class SoundManager implements ISoundManager {
} }
public playBellSound(): void { public playBellSound(): void {
const ctx = SoundManager.audioContext; const audioContextCtor: typeof AudioContext = (<any>window).AudioContext || (<any>window).webkitAudioContext;
if (!ctx) { if (!this._audioContext && audioContextCtor) {
return; this._audioContext = new audioContextCtor();
}
if (this._audioContext) {
const bellAudioSource = this._audioContext.createBufferSource();
const context = this._audioContext;
this._audioContext.decodeAudioData(this._base64ToArrayBuffer(this._removeMimeType(this._terminal.options.bellSound)), (buffer) => {
bellAudioSource.buffer = buffer;
bellAudioSource.connect(context.destination);
bellAudioSource.start(0);
});
} else {
console.warn('Sorry, but the Web Audio API is not supported by your browser. Please, consider upgrading to the latest version');
} }
const bellAudioSource = ctx.createBufferSource();
ctx.decodeAudioData(this._base64ToArrayBuffer(this._removeMimeType(this._terminal.options.bellSound)), (buffer) => {
bellAudioSource.buffer = buffer;
bellAudioSource.connect(ctx.destination);
bellAudioSource.start(0);
});
} }
private _base64ToArrayBuffer(base64: string): ArrayBuffer { private _base64ToArrayBuffer(base64: string): ArrayBuffer {
+4 -3
View File
@@ -13,7 +13,7 @@ import * as path from 'path';
import * as pty from 'node-pty'; import * as pty from 'node-pty';
import { assert } from 'chai'; import { assert } from 'chai';
import { Terminal } from './Terminal'; import { Terminal } from './Terminal';
import { CHAR_DATA_CHAR_INDEX, WHITESPACE_CELL_CHAR } from './Buffer'; import { CHAR_DATA_CHAR_INDEX } from './Buffer';
import { IViewport } from './Types'; import { IViewport } from './Types';
class TestTerminal extends Terminal { class TestTerminal extends Terminal {
@@ -67,7 +67,7 @@ function terminalToString(term: Terminal): string {
for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) { for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) {
lineText = ''; lineText = '';
for (let cell = 0; cell < term.cols; ++cell) { for (let cell = 0; cell < term.cols; ++cell) {
lineText += term.buffer.lines.get(line).get(cell)[CHAR_DATA_CHAR_INDEX] || WHITESPACE_CELL_CHAR; lineText += term.buffer.lines.get(line)[cell][CHAR_DATA_CHAR_INDEX];
} }
// rtrim empty cells as xterm does // rtrim empty cells as xterm does
lineText = lineText.replace(/\s+$/, ''); lineText = lineText.replace(/\s+$/, '');
@@ -105,7 +105,6 @@ if (os.platform() !== 'win32') {
// omit stack trace for escape sequence files // omit stack trace for escape sequence files
Error.stackTraceLimit = 0; Error.stackTraceLimit = 0;
const files = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '..')}); const files = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '..')});
// for (let i = 0; i < files.length; ++i) console.debug(i, files[i]);
// only successful tests for now // only successful tests for now
const skip = [ const skip = [
10, 16, 17, 19, 32, 33, 34, 35, 36, 39, 10, 16, 17, 19, 32, 33, 34, 35, 36, 39,
@@ -164,5 +163,7 @@ describe('typings', () => {
const fixtureDir = path.join(__dirname, '..', 'fixtures', 'typings-test'); const fixtureDir = path.join(__dirname, '..', 'fixtures', 'typings-test');
const result = cp.spawnSync(tsc, { cwd: fixtureDir }); const result = cp.spawnSync(tsc, { cwd: fixtureDir });
assert.equal(result.status, 0, `build did not succeed:\nstdout: ${result.stdout.toString()}\nstderr: ${result.stderr.toString()}\n`); assert.equal(result.status, 0, `build did not succeed:\nstdout: ${result.stdout.toString()}\nstderr: ${result.stderr.toString()}\n`);
// Clean up
fs.unlinkSync(path.join(fixtureDir, 'typings-test.js'));
}); });
}); });
+150 -268
View File
@@ -5,8 +5,8 @@
import { assert, expect } from 'chai'; import { assert, expect } from 'chai';
import { Terminal } from './Terminal'; import { Terminal } from './Terminal';
import { MockViewport, MockCompositionHelper, MockRenderer } from './ui/TestUtils.test'; import { MockViewport, MockCompositionHelper, MockRenderer } from './utils/TestUtils.test';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, DEFAULT_ATTR } from './Buffer'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer';
const INIT_COLS = 80; const INIT_COLS = 80;
const INIT_ROWS = 24; const INIT_ROWS = 24;
@@ -25,7 +25,7 @@ describe('term.js addons', () => {
beforeEach(() => { beforeEach(() => {
term = new TestTerminal(termOptions); term = new TestTerminal(termOptions);
term.refresh = () => { }; term.refresh = () => {};
(<any>term).renderer = new MockRenderer(); (<any>term).renderer = new MockRenderer();
term.viewport = new MockViewport(); term.viewport = new MockViewport();
(<any>term)._compositionHelper = new MockCompositionHelper(); (<any>term)._compositionHelper = new MockCompositionHelper();
@@ -36,8 +36,8 @@ describe('term.js addons', () => {
}; };
(<any>term).element = { (<any>term).element = {
classList: { classList: {
toggle: () => { }, toggle: () => {},
remove: () => { } remove: () => {}
} }
}; };
}); });
@@ -67,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', () => { describe('attachCustomKeyEventHandler', () => {
const evKeyDown = <KeyboardEvent>{ const evKeyDown = <KeyboardEvent>{
preventDefault: () => { }, preventDefault: () => {},
stopPropagation: () => { }, stopPropagation: () => {},
type: 'keydown', type: 'keydown',
keyCode: 77 keyCode: 77
}; };
const evKeyPress = <KeyboardEvent>{ const evKeyPress = <KeyboardEvent>{
preventDefault: () => { }, preventDefault: () => {},
stopPropagation: () => { }, stopPropagation: () => {},
type: 'keypress', type: 'keypress',
keyCode: 77 keyCode: 77
}; };
beforeEach(() => { beforeEach(() => {
term.handler = () => { }; term.handler = () => {};
term.showCursor = () => { }; term.showCursor = () => {};
term.clearSelection = () => { }; term.clearSelection = () => {};
}); });
it('should process the keydown/keypress event based on what the handler returns', () => { it('should process the keydown/keypress event based on what the handler returns', () => {
@@ -259,7 +141,7 @@ describe('term.js addons', () => {
assert.equal(term.buffer.lines.length, term.rows); assert.equal(term.buffer.lines.length, term.rows);
assert.deepEqual(term.buffer.lines.get(0), promptLine); assert.deepEqual(term.buffer.lines.get(0), promptLine);
for (let i = 1; i < term.rows; i++) { for (let i = 1; i < term.rows; i++) {
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR)); assert.deepEqual(term.buffer.lines.get(i), term.blankLine());
} }
}); });
it('should clear a buffer larger than rows', () => { it('should clear a buffer larger than rows', () => {
@@ -276,7 +158,7 @@ describe('term.js addons', () => {
assert.equal(term.buffer.lines.length, term.rows); assert.equal(term.buffer.lines.length, term.rows);
assert.deepEqual(term.buffer.lines.get(0), promptLine); assert.deepEqual(term.buffer.lines.get(0), promptLine);
for (let i = 1; i < term.rows; i++) { for (let i = 1; i < term.rows; i++) {
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR)); assert.deepEqual(term.buffer.lines.get(i), term.blankLine());
} }
}); });
it('should not break the prompt when cleared twice', () => { it('should not break the prompt when cleared twice', () => {
@@ -289,7 +171,7 @@ describe('term.js addons', () => {
assert.equal(term.buffer.lines.length, term.rows); assert.equal(term.buffer.lines.length, term.rows);
assert.deepEqual(term.buffer.lines.get(0), promptLine); assert.deepEqual(term.buffer.lines.get(0), promptLine);
for (let i = 1; i < term.rows; i++) { for (let i = 1; i < term.rows; i++) {
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR)); assert.deepEqual(term.buffer.lines.get(i), term.blankLine());
} }
}); });
}); });
@@ -422,8 +304,8 @@ describe('term.js addons', () => {
type: 'keydown', type: 'keydown',
key: 'a', key: 'a',
keyCode: 65, keyCode: 65,
preventDefault: () => { }, preventDefault: () => {},
stopPropagation: () => { } stopPropagation: () => {}
}; };
term.buffer.ydisp = 0; term.buffer.ydisp = 0;
@@ -455,62 +337,62 @@ describe('term.js addons', () => {
describe('scroll() function', () => { describe('scroll() function', () => {
describe('when scrollback > 0', () => { describe('when scrollback > 0', () => {
it('should create a new line and scroll', () => { it('should create a new line and scroll', () => {
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(INIT_ROWS - 1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); 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.buffer.y = INIT_ROWS - 1; // Move cursor to last line
term.scroll(); term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS + 1); assert.equal(term.buffer.lines.length, INIT_ROWS + 1);
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a'); assert.equal(term.buffer.lines.get(0)[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 - 1)[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(INIT_ROWS)[0][CHAR_DATA_CHAR_INDEX], ' ');
}); });
it('should properly scroll inside a scroll region (scrollTop set)', () => { it('should properly scroll inside a scroll region (scrollTop set)', () => {
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]); term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
term.buffer.scrollTop = 1; term.buffer.scrollTop = 1;
term.scroll(); term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS); assert.equal(term.buffer.lines.length, INIT_ROWS);
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a'); assert.equal(term.buffer.lines.get(0)[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(1)[0][CHAR_DATA_CHAR_INDEX], 'c');
}); });
it('should properly scroll inside a scroll region (scrollBottom set)', () => { it('should properly scroll inside a scroll region (scrollBottom set)', () => {
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]); term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
term.buffer.lines.get(3).set(0, [0, 'd', 0, 'd'.charCodeAt(0)]); term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd';
term.buffer.lines.get(4).set(0, [0, 'e', 0, 'e'.charCodeAt(0)]); term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e';
term.buffer.y = 3; term.buffer.y = 3;
term.buffer.scrollBottom = 3; term.buffer.scrollBottom = 3;
term.scroll(); term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS + 1); assert.equal(term.buffer.lines.length, INIT_ROWS + 1);
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a', '\'a\' should be pushed to the scrollback'); 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).get(0)[CHAR_DATA_CHAR_INDEX], 'b'); assert.equal(term.buffer.lines.get(1)[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(2)[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(3)[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(4)[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(5)[0][CHAR_DATA_CHAR_INDEX], 'e');
}); });
it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => { it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => {
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]); term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
term.buffer.lines.get(3).set(0, [0, 'd', 0, 'd'.charCodeAt(0)]); term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd';
term.buffer.lines.get(4).set(0, [0, 'e', 0, 'e'.charCodeAt(0)]); term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e';
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
term.buffer.scrollTop = 1; term.buffer.scrollTop = 1;
term.buffer.scrollBottom = 3; term.buffer.scrollBottom = 3;
term.scroll(); term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS); assert.equal(term.buffer.lines.length, INIT_ROWS);
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a'); assert.equal(term.buffer.lines.get(0)[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(1)[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(2)[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(3)[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(4)[0][CHAR_DATA_CHAR_INDEX], 'e');
}); });
}); });
@@ -521,65 +403,65 @@ describe('term.js addons', () => {
}); });
it('should create a new line and shift everything up', () => { it('should create a new line and shift everything up', () => {
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
term.buffer.lines.get(INIT_ROWS - 1).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]); term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX] = 'c';
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
assert.equal(term.buffer.lines.length, INIT_ROWS); assert.equal(term.buffer.lines.length, INIT_ROWS);
term.scroll(); term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS); assert.equal(term.buffer.lines.length, INIT_ROWS);
// 'a' gets pushed out of buffer // 'a' gets pushed out of buffer
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'b'); assert.equal(term.buffer.lines.get(0)[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(1)[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 - 2)[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(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX], ' ');
}); });
it('should properly scroll inside a scroll region (scrollTop set)', () => { it('should properly scroll inside a scroll region (scrollTop set)', () => {
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]); term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
term.buffer.scrollTop = 1; term.buffer.scrollTop = 1;
term.scroll(); term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS); assert.equal(term.buffer.lines.length, INIT_ROWS);
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a'); assert.equal(term.buffer.lines.get(0)[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(1)[0][CHAR_DATA_CHAR_INDEX], 'c');
}); });
it('should properly scroll inside a scroll region (scrollBottom set)', () => { it('should properly scroll inside a scroll region (scrollBottom set)', () => {
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]); term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
term.buffer.lines.get(3).set(0, [0, 'd', 0, 'd'.charCodeAt(0)]); term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd';
term.buffer.lines.get(4).set(0, [0, 'e', 0, 'e'.charCodeAt(0)]); term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e';
term.buffer.y = 3; term.buffer.y = 3;
term.buffer.scrollBottom = 3; term.buffer.scrollBottom = 3;
term.scroll(); term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS); assert.equal(term.buffer.lines.length, INIT_ROWS);
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'b'); assert.equal(term.buffer.lines.get(0)[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(1)[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(2)[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(3)[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(4)[0][CHAR_DATA_CHAR_INDEX], 'e');
}); });
it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => { it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => {
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]); term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
term.buffer.lines.get(3).set(0, [0, 'd', 0, 'd'.charCodeAt(0)]); term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd';
term.buffer.lines.get(4).set(0, [0, 'e', 0, 'e'.charCodeAt(0)]); term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e';
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
term.buffer.scrollTop = 1; term.buffer.scrollTop = 1;
term.buffer.scrollBottom = 3; term.buffer.scrollBottom = 3;
term.scroll(); term.scroll();
assert.equal(term.buffer.lines.length, INIT_ROWS); assert.equal(term.buffer.lines.length, INIT_ROWS);
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a'); assert.equal(term.buffer.lines.get(0)[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(1)[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(2)[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(3)[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(4)[0][CHAR_DATA_CHAR_INDEX], 'e');
}); });
}); });
}); });
@@ -590,9 +472,9 @@ describe('term.js addons', () => {
let evKeyPress: any; let evKeyPress: any;
beforeEach(() => { beforeEach(() => {
term.handler = () => { }; term.handler = () => {};
term.showCursor = () => { }; term.showCursor = () => {};
term.clearSelection = () => { }; term.clearSelection = () => {};
// term.compositionHelper = { // term.compositionHelper = {
// isComposing: false, // isComposing: false,
// keydown: { // keydown: {
@@ -602,15 +484,15 @@ describe('term.js addons', () => {
// } // }
// }; // };
evKeyDown = { evKeyDown = {
preventDefault: () => { }, preventDefault: () => {},
stopPropagation: () => { }, stopPropagation: () => {},
type: 'keydown', type: 'keydown',
altKey: null, altKey: null,
keyCode: null keyCode: null
}; };
evKeyPress = { evKeyPress = {
preventDefault: () => { }, preventDefault: () => {},
stopPropagation: () => { }, stopPropagation: () => {},
type: 'keypress', type: 'keypress',
altKey: null, altKey: null,
charCode: null, charCode: null,
@@ -772,11 +654,11 @@ describe('term.js addons', () => {
const high = String.fromCharCode(0xD800); const high = String.fromCharCode(0xD800);
for (let i = 0xDC00; i <= 0xDCFF; ++i) { for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.write(high + String.fromCharCode(i)); term.write(high + String.fromCharCode(i));
const tchar = term.buffer.lines.get(0).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]).eql(high + String.fromCharCode(i));
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql(''); expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' ');
term.reset(); term.reset();
} }
}); });
@@ -785,9 +667,9 @@ describe('term.js addons', () => {
for (let i = 0xDC00; i <= 0xDCFF; ++i) { for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.buffer.x = term.cols - 1; term.buffer.x = term.cols - 1;
term.write(high + String.fromCharCode(i)); term.write(high + String.fromCharCode(i));
expect(term.buffer.lines.get(0).get(term.buffer.x - 1)[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); expect(term.buffer.lines.get(0)[term.buffer.x - 1][CHAR_DATA_CHAR_INDEX]).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(0)[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(1)[0][CHAR_DATA_CHAR_INDEX]).eql(' ');
term.reset(); term.reset();
} }
}); });
@@ -797,10 +679,10 @@ describe('term.js addons', () => {
term.buffer.x = term.cols - 1; term.buffer.x = term.cols - 1;
term.wraparoundMode = true; term.wraparoundMode = true;
term.write('a' + high + String.fromCharCode(i)); term.write('a' + high + String.fromCharCode(i));
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('a'); expect(term.buffer.lines.get(0)[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)[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)[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(1)[1][CHAR_DATA_CHAR_INDEX]).eql(' ');
term.reset(); term.reset();
} }
}); });
@@ -811,9 +693,9 @@ describe('term.js addons', () => {
term.wraparoundMode = false; term.wraparoundMode = false;
term.write('a' + high + String.fromCharCode(i)); term.write('a' + high + String.fromCharCode(i));
// auto wraparound mode should cut off the rest of the line // auto wraparound mode should cut off the rest of the line
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('a'); expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX]).eql('a');
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX].length).eql(1); expect(term.buffer.lines.get(0)[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(1)[1][CHAR_DATA_CHAR_INDEX]).eql(' ');
term.reset(); term.reset();
} }
}); });
@@ -822,11 +704,11 @@ describe('term.js addons', () => {
for (let i = 0xDC00; i <= 0xDCFF; ++i) { for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.write(high); term.write(high);
term.write(String.fromCharCode(i)); term.write(String.fromCharCode(i));
const tchar = term.buffer.lines.get(0).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]).eql(high + String.fromCharCode(i));
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql(''); expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' ');
term.reset(); term.reset();
} }
}); });
@@ -835,30 +717,30 @@ describe('term.js addons', () => {
describe('unicode - combining characters', () => { describe('unicode - combining characters', () => {
it('café', () => { it('café', () => {
term.write('cafe\u0301'); term.write('cafe\u0301');
expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_CHAR_INDEX]).eql('e\u0301'); expect(term.buffer.lines.get(0)[3][CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_CHAR_INDEX].length).eql(2); expect(term.buffer.lines.get(0)[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_WIDTH_INDEX]).eql(1);
}); });
it('café - end of line', () => { it('café - end of line', () => {
term.buffer.x = term.cols - 1 - 3; term.buffer.x = term.cols - 1 - 3;
term.write('cafe\u0301'); term.write('cafe\u0301');
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('e\u0301'); expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX].length).eql(2); expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_WIDTH_INDEX]).eql(1); expect(term.buffer.lines.get(0)[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)[1][CHAR_DATA_CHAR_INDEX]).eql(' ');
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX].length).eql(0); expect(term.buffer.lines.get(0)[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)[1][CHAR_DATA_WIDTH_INDEX]).eql(1);
}); });
it('multiple combined é', () => { it('multiple combined é', () => {
term.wraparoundMode = true; term.wraparoundMode = true;
term.write(Array(100).join('e\u0301')); term.write(Array(100).join('e\u0301'));
for (let i = 0; i < term.cols; ++i) { for (let i = 0; i < term.cols; ++i) {
const tchar = term.buffer.lines.get(0).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]).eql('e\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
} }
const tchar = term.buffer.lines.get(1).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]).eql('e\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
@@ -867,12 +749,12 @@ describe('term.js addons', () => {
term.wraparoundMode = true; term.wraparoundMode = true;
term.write(Array(100).join('\uD800\uDC00\u0301')); term.write(Array(100).join('\uD800\uDC00\u0301'));
for (let i = 0; i < term.cols; ++i) { for (let i = 0; i < term.cols; ++i) {
const tchar = term.buffer.lines.get(0).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]).eql('\uD800\uDC00\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
} }
const tchar = term.buffer.lines.get(1).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]).eql('\uD800\uDC00\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
@@ -895,7 +777,7 @@ describe('term.js addons', () => {
term.wraparoundMode = true; term.wraparoundMode = true;
term.write(Array(50).join('¥')); term.write(Array(50).join('¥'));
for (let i = 0; i < term.cols; ++i) { for (let i = 0; i < term.cols; ++i) {
const tchar = term.buffer.lines.get(0).get(i); const tchar = term.buffer.lines.get(0)[i];
if (i % 2) { if (i % 2) {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
@@ -906,7 +788,7 @@ describe('term.js addons', () => {
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
} }
} }
const tchar = term.buffer.lines.get(1).get(0); const tchar = term.buffer.lines.get(1)[0];
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥'); expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
@@ -916,7 +798,7 @@ describe('term.js addons', () => {
term.buffer.x = 1; term.buffer.x = 1;
term.write(Array(50).join('¥')); term.write(Array(50).join('¥'));
for (let i = 1; i < term.cols - 1; ++i) { for (let i = 1; i < term.cols - 1; ++i) {
const tchar = term.buffer.lines.get(0).get(i); const tchar = term.buffer.lines.get(0)[i];
if (!(i % 2)) { if (!(i % 2)) {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
@@ -927,11 +809,11 @@ describe('term.js addons', () => {
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
} }
} }
let tchar = term.buffer.lines.get(0).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]).eql(' ');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
tchar = term.buffer.lines.get(1).get(0); tchar = term.buffer.lines.get(1)[0];
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥'); expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
@@ -941,7 +823,7 @@ describe('term.js addons', () => {
term.buffer.x = 1; term.buffer.x = 1;
term.write(Array(50).join('¥\u0301')); term.write(Array(50).join('¥\u0301'));
for (let i = 1; i < term.cols - 1; ++i) { for (let i = 1; i < term.cols - 1; ++i) {
const tchar = term.buffer.lines.get(0).get(i); const tchar = term.buffer.lines.get(0)[i];
if (!(i % 2)) { if (!(i % 2)) {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
@@ -952,11 +834,11 @@ describe('term.js addons', () => {
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
} }
} }
let tchar = term.buffer.lines.get(0).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]).eql(' ');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
tchar = term.buffer.lines.get(1).get(0); tchar = term.buffer.lines.get(1)[0];
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301'); expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
@@ -965,7 +847,7 @@ describe('term.js addons', () => {
term.wraparoundMode = true; term.wraparoundMode = true;
term.write(Array(50).join('¥\u0301')); term.write(Array(50).join('¥\u0301'));
for (let i = 0; i < term.cols; ++i) { for (let i = 0; i < term.cols; ++i) {
const tchar = term.buffer.lines.get(0).get(i); const tchar = term.buffer.lines.get(0)[i];
if (i % 2) { if (i % 2) {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
@@ -976,7 +858,7 @@ describe('term.js addons', () => {
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
} }
} }
const tchar = term.buffer.lines.get(1).get(0); const tchar = term.buffer.lines.get(1)[0];
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301'); expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
@@ -986,7 +868,7 @@ describe('term.js addons', () => {
term.buffer.x = 1; term.buffer.x = 1;
term.write(Array(50).join('\ud843\ude6d\u0301')); term.write(Array(50).join('\ud843\ude6d\u0301'));
for (let i = 1; i < term.cols - 1; ++i) { for (let i = 1; i < term.cols - 1; ++i) {
const tchar = term.buffer.lines.get(0).get(i); const tchar = term.buffer.lines.get(0)[i];
if (!(i % 2)) { if (!(i % 2)) {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
@@ -997,11 +879,11 @@ describe('term.js addons', () => {
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
} }
} }
let tchar = term.buffer.lines.get(0).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]).eql(' ');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
tchar = term.buffer.lines.get(1).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]).eql('\ud843\ude6d\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
@@ -1010,7 +892,7 @@ describe('term.js addons', () => {
term.wraparoundMode = true; term.wraparoundMode = true;
term.write(Array(50).join('\ud843\ude6d\u0301')); term.write(Array(50).join('\ud843\ude6d\u0301'));
for (let i = 0; i < term.cols; ++i) { for (let i = 0; i < term.cols; ++i) {
const tchar = term.buffer.lines.get(0).get(i); const tchar = term.buffer.lines.get(0)[i];
if (i % 2) { if (i % 2) {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
@@ -1021,7 +903,7 @@ describe('term.js addons', () => {
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
} }
} }
const tchar = term.buffer.lines.get(1).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]).eql('\ud843\ude6d\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
@@ -1036,10 +918,10 @@ describe('term.js addons', () => {
term.insertMode = true; term.insertMode = true;
term.write('abcde'); term.write('abcde');
expect(term.buffer.lines.get(0).length).eql(term.cols); expect(term.buffer.lines.get(0).length).eql(term.cols);
expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('a'); expect(term.buffer.lines.get(0)[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)[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)[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)[79][CHAR_DATA_CHAR_INDEX]).eql('4');
}); });
it('fullwidth - insert', () => { it('fullwidth - insert', () => {
term.write(Array(9).join('0123456789').slice(-80)); term.write(Array(9).join('0123456789').slice(-80));
@@ -1048,11 +930,11 @@ describe('term.js addons', () => {
term.insertMode = true; term.insertMode = true;
term.write('¥¥¥'); term.write('¥¥¥');
expect(term.buffer.lines.get(0).length).eql(term.cols); expect(term.buffer.lines.get(0).length).eql(term.cols);
expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('¥'); expect(term.buffer.lines.get(0)[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)[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)[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)[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)[79][CHAR_DATA_CHAR_INDEX]).eql('3');
}); });
it('fullwidth - right border', () => { it('fullwidth - right border', () => {
term.write(Array(41).join('¥')); term.write(Array(41).join('¥'));
@@ -1061,14 +943,14 @@ describe('term.js addons', () => {
term.insertMode = true; term.insertMode = true;
term.write('a'); term.write('a');
expect(term.buffer.lines.get(0).length).eql(term.cols); expect(term.buffer.lines.get(0).length).eql(term.cols);
expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('a'); expect(term.buffer.lines.get(0)[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)[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)[79][CHAR_DATA_CHAR_INDEX]).eql(' '); // fullwidth char got replaced
term.write('b'); term.write('b');
expect(term.buffer.lines.get(0).length).eql(term.cols); expect(term.buffer.lines.get(0).length).eql(term.cols);
expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('b'); expect(term.buffer.lines.get(0)[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)[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)[79][CHAR_DATA_CHAR_INDEX]).eql(''); // empty cell after fullwidth
}); });
}); });
}); });
+177 -181
View File
@@ -21,25 +21,26 @@
* http://linux.die.net/man/7/urxvt * http://linux.die.net/man/7/urxvt
*/ */
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, CharacterJoinerHandler, IBufferLine } from './Types'; import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, LineData } from './Types';
import { IMouseZoneManager } from './ui/Types'; import { IMouseZoneManager } from './ui/Types';
import { IRenderer } from './renderer/Types'; import { IRenderer } from './renderer/Types';
import { BufferSet } from './BufferSet'; import { BufferSet } from './BufferSet';
import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, CHAR_DATA_ATTR_INDEX } from './Buffer'; import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR } from './Buffer';
import { CompositionHelper } from './CompositionHelper'; import { CompositionHelper } from './CompositionHelper';
import { EventEmitter } from './common/EventEmitter'; import { EventEmitter } from './EventEmitter';
import { Viewport } from './Viewport'; import { Viewport } from './Viewport';
import { rightClickHandler, moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from './ui/Clipboard'; import { rightClickHandler, moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from './handlers/Clipboard';
import { C0 } from './common/data/EscapeSequences'; import { C0 } from './common/data/EscapeSequences';
import { InputHandler } from './InputHandler'; import { InputHandler } from './InputHandler';
import { Renderer } from './renderer/Renderer'; import { Renderer } from './renderer/Renderer';
import { Linkifier } from './Linkifier'; import { Linkifier } from './Linkifier';
import { SelectionManager } from './SelectionManager'; import { SelectionManager } from './SelectionManager';
import { CharMeasure } from './ui/CharMeasure'; import { CharMeasure } from './ui/CharMeasure';
import * as Browser from './core/Platform'; import * as Browser from './shared/utils/Browser';
import { addDisposableDomListener } from './ui/Lifecycle'; import { addDisposableDomListener } from './ui/Lifecycle';
import * as Strings from './Strings'; import * as Strings from './Strings';
import { MouseHelper } from './ui/MouseHelper'; import { MouseHelper } from './utils/MouseHelper';
import { clone } from './utils/Clone';
import { DEFAULT_BELL_SOUND, SoundManager } from './SoundManager'; import { DEFAULT_BELL_SOUND, SoundManager } from './SoundManager';
import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager'; import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager';
import { MouseZoneManager } from './ui/MouseZoneManager'; import { MouseZoneManager } from './ui/MouseZoneManager';
@@ -51,7 +52,6 @@ import { DomRenderer } from './renderer/dom/DomRenderer';
import { IKeyboardEvent } from './common/Types'; import { IKeyboardEvent } from './common/Types';
import { evaluateKeyboardEvent } from './core/input/Keyboard'; import { evaluateKeyboardEvent } from './core/input/Keyboard';
import { KeyboardResultType, ICharset } from './core/Types'; import { KeyboardResultType, ICharset } from './core/Types';
import { clone } from './common/Clone';
// Let it work inside Node.js for automated testing purposes. // Let it work inside Node.js for automated testing purposes.
const document = (typeof window !== 'undefined') ? window.document : null; const document = (typeof window !== 'undefined') ? window.document : null;
@@ -64,20 +64,15 @@ const document = (typeof window !== 'undefined') ? window.document : null;
const WRITE_BUFFER_PAUSE_THRESHOLD = 5; const WRITE_BUFFER_PAUSE_THRESHOLD = 5;
/** /**
* The max number of ms to spend on writes before allowing the renderer to * The number of writes to perform in a single batch before allowing the
* catch up with a 0ms setTimeout. A value of < 33 to keep us close to * renderer to catch up with a 0ms setTimeout.
* 30fps, and a value of < 16 to try to run at 60fps. Of course, the real FPS
* depends on the time it takes for the renderer to draw the frame.
*/ */
const WRITE_TIMEOUT_MS = 12; const WRITE_BATCH_SIZE = 300;
const MINIMUM_COLS = 2; // Less than 2 can mess with wide chars
const MINIMUM_ROWS = 1;
/** /**
* The set of options that only have an effect when set in the Terminal constructor. * 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 = { const DEFAULT_OPTIONS: ITerminalOptions = {
cols: 80, cols: 80,
@@ -138,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 // TODO: This can be changed to an enum or boolean, 0 and 1 seem to be the only options
public cursorState: number; public cursorState: number;
public cursorHidden: boolean; public cursorHidden: boolean;
public convertEol: boolean;
private _sendDataQueue: string;
private _customKeyEventHandler: CustomKeyEventHandler; private _customKeyEventHandler: CustomKeyEventHandler;
// modes // modes
@@ -174,6 +171,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
public savedCols: number; public savedCols: number;
public curAttr: number; public curAttr: number;
public savedCurAttr: number;
public params: (string | number)[]; public params: (string | number)[];
public currentParam: string | number; public currentParam: string | number;
@@ -209,10 +207,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
public mouseHelper: MouseHelper; public mouseHelper: MouseHelper;
private _accessibilityManager: AccessibilityManager; private _accessibilityManager: AccessibilityManager;
private _screenDprMonitor: ScreenDprMonitor; private _screenDprMonitor: ScreenDprMonitor;
private _theme: ITheme;
// bufferline to clone/copy from for new blank lines
private _blankLine: IBufferLine = null;
public cols: number; public cols: number;
public rows: number; public rows: number;
@@ -220,7 +214,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/** /**
* Creates a new `Terminal` object. * 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 * - `cursorBlink` (boolean): Whether the terminal cursor blinks
* - `cols` (number): The number of columns of the terminal (horizontal size) * - `cols` (number): The number of columns of the terminal (horizontal size)
* - `rows` (number): The number of rows of the terminal (vertical size) * - `rows` (number): The number of rows of the terminal (vertical size)
@@ -257,7 +251,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
private _setup(): void { private _setup(): void {
Object.keys(DEFAULT_OPTIONS).forEach((key) => { Object.keys(DEFAULT_OPTIONS).forEach((key) => {
if (this.options[key] === null || this.options[key] === undefined) { if (this.options[key] == null) {
this.options[key] = DEFAULT_OPTIONS[key]; this.options[key] = DEFAULT_OPTIONS[key];
} }
}); });
@@ -267,8 +261,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
// TODO: WHy not document.body? // TODO: WHy not document.body?
this._parent = document ? document.body : null; this._parent = document ? document.body : null;
this.cols = Math.max(this.options.cols, MINIMUM_COLS); this.cols = this.options.cols;
this.rows = Math.max(this.options.rows, MINIMUM_ROWS); this.rows = this.options.rows;
if (this.options.handler) { if (this.options.handler) {
this.on('data', this.options.handler); this.on('data', this.options.handler);
@@ -276,6 +270,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.cursorState = 0; this.cursorState = 0;
this.cursorHidden = false; this.cursorHidden = false;
this._sendDataQueue = '';
this._customKeyEventHandler = null; this._customKeyEventHandler = null;
// modes // modes
@@ -348,12 +343,12 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
} }
public get isFocused(): boolean { public get isFocused(): boolean {
return document.activeElement === this.textarea && document.hasFocus(); return document.activeElement === this.textarea;
} }
/** /**
* Retrieves an option's value from the terminal. * Retrieves an option's value from the terminal.
* @param key The option key. * @param {string} key The option key.
*/ */
public getOption(key: string): any { public getOption(key: string): any {
if (!(key in DEFAULT_OPTIONS)) { if (!(key in DEFAULT_OPTIONS)) {
@@ -365,8 +360,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/** /**
* Sets an option on the terminal. * Sets an option on the terminal.
* @param key The option key. * @param {string} key The option key.
* @param value The option value. * @param {any} value The option value.
*/ */
public setOption(key: string, value: any): void { public setOption(key: string, value: any): void {
if (!(key in DEFAULT_OPTIONS)) { if (!(key in DEFAULT_OPTIONS)) {
@@ -375,9 +370,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
if (CONSTRUCTOR_ONLY_OPTIONS.indexOf(key) !== -1) { if (CONSTRUCTOR_ONLY_OPTIONS.indexOf(key) !== -1) {
console.error(`Option "${key}" can only be set in the constructor`); console.error(`Option "${key}" can only be set in the constructor`);
} }
if (this.options[key] === value) {
return;
}
switch (key) { switch (key) {
case 'bellStyle': case 'bellStyle':
if (!value) { if (!value) {
@@ -404,11 +396,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
console.warn(`${key} cannot be less than 1, value: ${value}`); console.warn(`${key} cannot be less than 1, value: ${value}`);
return; return;
} }
case 'rendererType':
if (!value) {
value = 'canvas';
}
break;
case 'tabStopWidth': case 'tabStopWidth':
if (value < 1) { if (value < 1) {
console.warn(`${key} cannot be less than 1, value: ${value}`); console.warn(`${key} cannot be less than 1, value: ${value}`);
@@ -468,20 +455,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.renderer.onResize(this.cols, this.rows); this.renderer.onResize(this.cols, this.rows);
this.refresh(0, this.rows - 1); this.refresh(0, this.rows - 1);
} }
break;
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);
}
this.mouseHelper.setRenderer(this.renderer);
break;
case 'scrollback': case 'scrollback':
this.buffers.resize(this.cols, this.rows); this.buffers.resize(this.cols, this.rows);
if (this.viewport) { if (this.viewport) {
@@ -511,11 +484,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/** /**
* Binds the desired focus behavior on a given terminal object. * Binds the desired focus behavior on a given terminal object.
*/ */
private _onTextAreaFocus(ev: KeyboardEvent): void { private _onTextAreaFocus(): void {
if (this.sendFocus) { if (this.sendFocus) {
this.handler(C0.ESC + '[I'); this.send(C0.ESC + '[I');
} }
this.updateCursorStyle(ev);
this.element.classList.add('focus'); this.element.classList.add('focus');
this.showCursor(); this.showCursor();
this.emit('focus'); this.emit('focus');
@@ -538,7 +510,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.textarea.value = ''; this.textarea.value = '';
this.refresh(this.buffer.y, this.buffer.y); this.refresh(this.buffer.y, this.buffer.y);
if (this.sendFocus) { if (this.sendFocus) {
this.handler(C0.ESC + '[O'); this.send(C0.ESC + '[O');
} }
this.element.classList.remove('focus'); this.element.classList.remove('focus');
this.emit('blur'); this.emit('blur');
@@ -568,12 +540,12 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
// Firefox doesn't appear to fire the contextmenu event on right click // Firefox doesn't appear to fire the contextmenu event on right click
this.register(addDisposableDomListener(this.element, 'mousedown', (event: MouseEvent) => { this.register(addDisposableDomListener(this.element, 'mousedown', (event: MouseEvent) => {
if (event.button === 2) { if (event.button === 2) {
rightClickHandler(event, this, this.selectionManager, this.options.rightClickSelectsWord); rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord);
} }
})); }));
} else { } else {
this.register(addDisposableDomListener(this.element, 'contextmenu', (event: MouseEvent) => { this.register(addDisposableDomListener(this.element, 'contextmenu', (event: MouseEvent) => {
rightClickHandler(event, this, this.selectionManager, this.options.rightClickSelectsWord); rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord);
})); }));
} }
@@ -585,7 +557,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
// that the regular click event doesn't fire for the middle mouse button. // that the regular click event doesn't fire for the middle mouse button.
this.register(addDisposableDomListener(this.element, 'auxclick', (event: MouseEvent) => { this.register(addDisposableDomListener(this.element, 'auxclick', (event: MouseEvent) => {
if (event.button === 1) { if (event.button === 1) {
moveTextAreaUnderMouseCursor(event, this); moveTextAreaUnderMouseCursor(event, this.textarea);
} }
})); }));
} }
@@ -630,7 +602,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/** /**
* Opens the terminal within an element. * 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 { public open(parent: HTMLElement): void {
this._parent = parent || this._parent; this._parent = parent || this._parent;
@@ -688,7 +660,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.textarea.setAttribute('autocapitalize', 'off'); this.textarea.setAttribute('autocapitalize', 'off');
this.textarea.setAttribute('spellcheck', 'false'); this.textarea.setAttribute('spellcheck', 'false');
this.textarea.tabIndex = 0; this.textarea.tabIndex = 0;
this.register(addDisposableDomListener(this.textarea, 'focus', (ev: KeyboardEvent) => this._onTextAreaFocus(ev))); this.register(addDisposableDomListener(this.textarea, 'focus', () => this._onTextAreaFocus()));
this.register(addDisposableDomListener(this.textarea, 'blur', () => this._onTextAreaBlur())); this.register(addDisposableDomListener(this.textarea, 'blur', () => this._onTextAreaBlur()));
this._helperContainer.appendChild(this.textarea); this._helperContainer.appendChild(this.textarea);
@@ -702,8 +674,12 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
// Performance: Add viewport and helper elements from the fragment // Performance: Add viewport and helper elements from the fragment
this.element.appendChild(fragment); this.element.appendChild(fragment);
this._setupRenderer(); switch (this.options.rendererType) {
this._theme = this.options.theme; 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.options.theme = null;
this.viewport = new Viewport(this, this._viewportElement, this._viewportScrollArea, this.charMeasure); this.viewport = new Viewport(this, this._viewportElement, this._viewportScrollArea, this.charMeasure);
this.viewport.onThemeChanged(this.renderer.colorManager.colors); this.viewport.onThemeChanged(this.renderer.colorManager.colors);
@@ -738,13 +714,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this.selectionManager.refresh())); this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this.selectionManager.refresh()));
this.mouseHelper = new MouseHelper(this.renderer); this.mouseHelper = new MouseHelper(this.renderer);
// apply mouse event classes set by escape codes before terminal was attached
this.element.classList.toggle('enable-mouse-events', this.mouseEvents);
if (this.mouseEvents) {
this.selectionManager.disable();
} else {
this.selectionManager.enable();
}
if (this.options.screenReaderMode) { if (this.options.screenReaderMode) {
// Note that this must be done *after* the renderer is created in order to // Note that this must be done *after* the renderer is created in order to
@@ -767,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. * 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 { private _setTheme(theme: ITheme): void {
this._theme = theme;
const colors = this.renderer.setTheme(theme); const colors = this.renderer.setTheme(theme);
if (this.viewport) { if (this.viewport) {
this.viewport.onThemeChanged(colors); this.viewport.onThemeChanged(colors);
@@ -814,7 +773,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
button = getButton(ev); button = getButton(ev);
// get mouse coordinates // get mouse coordinates
pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.charMeasure, self.cols, self.rows); pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.charMeasure, self.options.lineHeight, self.cols, self.rows);
if (!pos) return; if (!pos) return;
sendEvent(button, pos); sendEvent(button, pos);
@@ -840,7 +799,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
// ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7< // ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7<
function sendMove(ev: MouseEvent): void { function sendMove(ev: MouseEvent): void {
let button = pressed; let button = pressed;
const pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.charMeasure, self.cols, self.rows); const pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.charMeasure, self.options.lineHeight, self.cols, self.rows);
if (!pos) return; if (!pos) return;
// buttons marked as motions // buttons marked as motions
@@ -901,7 +860,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
else if (button === 3) return; else if (button === 3) return;
else data += '0'; else data += '0';
data += '~[' + pos.x + ',' + pos.y + ']\r'; data += '~[' + pos.x + ',' + pos.y + ']\r';
self.handler(data); self.send(data);
return; return;
} }
@@ -914,7 +873,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
else if (button === 1) button = 4; else if (button === 1) button = 4;
else if (button === 2) button = 6; else if (button === 2) button = 6;
else if (button === 3) button = 3; else if (button === 3) button = 3;
self.handler(C0.ESC + '[' self.send(C0.ESC + '['
+ button + button
+ ';' + ';'
+ (button === 3 ? 4 : 0) + (button === 3 ? 4 : 0)
@@ -934,14 +893,14 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
pos.y -= 32; pos.y -= 32;
pos.x++; pos.x++;
pos.y++; pos.y++;
self.handler(C0.ESC + '[' + button + ';' + pos.x + ';' + pos.y + 'M'); self.send(C0.ESC + '[' + button + ';' + pos.x + ';' + pos.y + 'M');
return; return;
} }
if (self.sgrMouse) { if (self.sgrMouse) {
pos.x -= 32; pos.x -= 32;
pos.y -= 32; pos.y -= 32;
self.handler(C0.ESC + '[<' self.send(C0.ESC + '[<'
+ (((button & 3) === 3 ? button & ~3 : button) - 32) + (((button & 3) === 3 ? button & ~3 : button) - 32)
+ ';' + ';'
+ pos.x + pos.x
@@ -957,7 +916,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
encode(data, pos.x); encode(data, pos.x);
encode(data, pos.y); 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 { function getButton(ev: MouseEvent): number {
@@ -976,9 +935,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
// 1, and 2 - with 64 added // 1, and 2 - with 64 added
switch ((<any>ev).overrideType || ev.type) { switch ((<any>ev).overrideType || ev.type) {
case 'mousedown': case 'mousedown':
button = ev.button !== null && ev.button !== undefined button = ev.button != null
? +ev.button ? +ev.button
: ev.which !== null && ev.which !== undefined : ev.which != null
? ev.which - 1 ? ev.which - 1
: null; : null;
@@ -995,7 +954,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
: 65; : 65;
break; break;
case 'wheel': case 'wheel':
button = (<WheelEvent>ev).deltaY < 0 button = (<WheelEvent>ev).wheelDeltaY > 0
? 64 ? 64
: 65; : 65;
break; break;
@@ -1106,7 +1065,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
for (let i = 0; i < Math.abs(amount); i++) { for (let i = 0; i < Math.abs(amount); i++) {
data += sequence; data += sequence;
} }
this.handler(data); this.send(data);
} }
return; return;
} }
@@ -1139,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 * Tells the renderer to refresh terminal content between two rows (inclusive) at the next
* opportunity. * opportunity.
* @param start The row to start from (between 0 and this.rows - 1). * @param {number} 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} end The row to end at (between start and this.rows - 1).
*/ */
public refresh(start: number, end: number): void { public refresh(start: number, end: number): void {
if (this.renderer) { if (this.renderer) {
@@ -1150,8 +1109,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/** /**
* Queues linkification for the specified rows. * Queues linkification for the specified rows.
* @param start The row to start from (between 0 and this.rows - 1). * @param {number} 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} end The row to end at (between start and this.rows - 1).
*/ */
private _queueLinkification(start: number, end: number): void { private _queueLinkification(start: number, end: number): void {
if (this.linkifier) { if (this.linkifier) {
@@ -1164,9 +1123,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
*/ */
public updateCursorStyle(ev: KeyboardEvent): void { public updateCursorStyle(ev: KeyboardEvent): void {
if (this.selectionManager && this.selectionManager.shouldColumnSelect(ev)) { if (this.selectionManager && this.selectionManager.shouldColumnSelect(ev)) {
this.element.classList.add('column-select'); this.element.classList.add('xterm-cursor-crosshair');
} else { } else {
this.element.classList.remove('column-select'); this.element.classList.remove('xterm-cursor-crosshair');
} }
} }
@@ -1184,31 +1143,20 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
* Scroll the terminal down 1 row, creating a blank line. * Scroll the terminal down 1 row, creating a blank line.
* @param isWrapped Whether the new line is wrapped from the previous line. * @param isWrapped Whether the new line is wrapped from the previous line.
*/ */
public scroll(isWrapped: boolean = false): void { public scroll(isWrapped?: boolean): void {
let newLine: IBufferLine; const newLine = this.blankLine(undefined, isWrapped);
newLine = this._blankLine;
if (!newLine || newLine.length !== this.cols || newLine.get(0)[CHAR_DATA_ATTR_INDEX] !== this.eraseAttr()) {
newLine = this.buffer.getBlankLine(this.eraseAttr(), isWrapped);
this._blankLine = newLine;
}
newLine.isWrapped = isWrapped;
const topRow = this.buffer.ybase + this.buffer.scrollTop; const topRow = this.buffer.ybase + this.buffer.scrollTop;
const bottomRow = this.buffer.ybase + this.buffer.scrollBottom; const bottomRow = this.buffer.ybase + this.buffer.scrollBottom;
if (this.buffer.scrollTop === 0) { if (this.buffer.scrollTop === 0) {
// Determine whether the buffer is going to be trimmed after insertion. // Determine whether the buffer is going to be trimmed after insertion.
const willBufferBeTrimmed = this.buffer.lines.isFull; const willBufferBeTrimmed = this.buffer.lines.length === this.buffer.lines.maxLength;
// Insert the line using the fastest method // Insert the line using the fastest method
if (bottomRow === this.buffer.lines.length - 1) { if (bottomRow === this.buffer.lines.length - 1) {
if (willBufferBeTrimmed) { this.buffer.lines.push(newLine);
this.buffer.lines.recycle().copyFrom(newLine);
} else {
this.buffer.lines.push(newLine.clone());
}
} else { } else {
this.buffer.lines.splice(bottomRow + 1, 0, newLine.clone()); this.buffer.lines.splice(bottomRow + 1, 0, newLine);
} }
// Only adjust ybase and ydisp when the buffer is not trimmed // Only adjust ybase and ydisp when the buffer is not trimmed
@@ -1230,7 +1178,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
// scrollback, instead we can just shift them in-place. // scrollback, instead we can just shift them in-place.
const scrollRegionHeight = bottomRow - topRow + 1/*as it's zero-based*/; const scrollRegionHeight = bottomRow - topRow + 1/*as it's zero-based*/;
this.buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1); this.buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1);
this.buffer.lines.set(bottomRow, newLine.clone()); this.buffer.lines.set(bottomRow, newLine);
} }
// Move the viewport to the bottom of the buffer unless the user is // Move the viewport to the bottom of the buffer unless the user is
@@ -1254,8 +1202,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/** /**
* Scroll the display of the terminal * Scroll the display of the terminal
* @param disp The number of lines to scroll down (negative scroll up). * @param {number} 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 {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 * to avoid unwanted events being handled by the viewport when the event was triggered from the
* viewport originally. * viewport originally.
*/ */
@@ -1286,7 +1234,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/** /**
* Scroll the display of the terminal by a number of pages. * 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 { public scrollPages(pageCount: number): void {
this.scrollLines(pageCount * (this.rows - 1)); this.scrollLines(pageCount * (this.rows - 1));
@@ -1315,14 +1263,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/** /**
* Writes text to the terminal. * 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 { public write(data: string): void {
// Ensure the terminal isn't disposed
if (this._isDisposed) {
return;
}
// Ignore falsy data values (including the empty string) // Ignore falsy data values (including the empty string)
if (!data) { if (!data) {
return; return;
@@ -1336,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) { if (this.options.useFlowControl && !this._xoffSentToCatchUp && this.writeBuffer.length >= WRITE_BUFFER_PAUSE_THRESHOLD) {
// XOFF - stop pty pipe // XOFF - stop pty pipe
// XON will be triggered by emulator before processing data chunk // XON will be triggered by emulator before processing data chunk
this.handler(C0.DC3); this.send(C0.DC3);
this._xoffSentToCatchUp = true; this._xoffSentToCatchUp = true;
} }
@@ -1350,21 +1293,15 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
} }
} }
protected _innerWrite(bufferOffset: number = 0): void { protected _innerWrite(): void {
// Ensure the terminal isn't disposed const writeBatch = this.writeBuffer.splice(0, WRITE_BATCH_SIZE);
if (this._isDisposed) { while (writeBatch.length > 0) {
this.writeBuffer = []; const data = writeBatch.shift();
}
const startTime = Date.now();
while (this.writeBuffer.length > bufferOffset) {
const data = this.writeBuffer[bufferOffset];
bufferOffset++;
// If XOFF was sent in order to catch up with the pty process, resume it if // If XOFF was sent in order to catch up with the pty process, resume it if
// we reached the end of the writeBuffer to allow more data to come in. // the writeBuffer is empty to allow more data to come in.
if (this._xoffSentToCatchUp && this.writeBuffer.length === bufferOffset) { if (this._xoffSentToCatchUp && writeBatch.length === 0 && this.writeBuffer.length === 0) {
this.handler(C0.DC1); this.send(C0.DC1);
this._xoffSentToCatchUp = false; this._xoffSentToCatchUp = false;
} }
@@ -1381,23 +1318,18 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.updateRange(this.buffer.y); this.updateRange(this.buffer.y);
this.refresh(this._refreshStart, this._refreshEnd); this.refresh(this._refreshStart, this._refreshEnd);
if (Date.now() - startTime >= WRITE_TIMEOUT_MS) {
break;
}
} }
if (this.writeBuffer.length > bufferOffset) { if (this.writeBuffer.length > 0) {
// Allow renderer to catch up before processing the next batch // Allow renderer to catch up before processing the next batch
setTimeout(() => this._innerWrite(bufferOffset), 0); setTimeout(() => this._innerWrite(), 0);
} else { } else {
this._writeInProgress = false; this._writeInProgress = false;
this.writeBuffer = [];
} }
} }
/** /**
* Writes text to the terminal, followed by a break line character (\n). * 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 { public writeln(data: string): void {
this.write(data + '\r\n'); this.write(data + '\r\n');
@@ -1409,22 +1341,13 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
* processed by the terminal and what keys should not. * processed by the terminal and what keys should not.
* @param customKeyEventHandler The custom KeyboardEvent handler to attach. * @param customKeyEventHandler The custom KeyboardEvent handler to attach.
* This is a function that takes a KeyboardEvent, allowing consumers to stop * This is a function that takes a KeyboardEvent, allowing consumers to stop
* propagation and/or prevent the default action. The function returns whether * propogation and/or prevent the default action. The function returns whether
* the event should be processed by xterm.js. * the event should be processed by xterm.js.
*/ */
public attachCustomKeyEventHandler(customKeyEventHandler: CustomKeyEventHandler): void { public attachCustomKeyEventHandler(customKeyEventHandler: CustomKeyEventHandler): void {
this._customKeyEventHandler = customKeyEventHandler; this._customKeyEventHandler = customKeyEventHandler;
} }
/** Add handler for CSI escape sequence. See xterm.d.ts for details. */
public addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable {
return this._inputHandler.addCsiHandler(flag, callback);
}
/** Add handler for OSC escape sequence. See xterm.d.ts for details. */
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
return this._inputHandler.addOscHandler(ident, callback);
}
/** /**
* Registers a link matcher, allowing custom link patterns to be matched and * Registers a link matcher, allowing custom link patterns to be matched and
* handled. * handled.
@@ -1451,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[] { public get markers(): IMarker[] {
return this.buffer.markers; return this.buffer.markers;
} }
@@ -1519,7 +1430,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
* Handle a keydown event * Handle a keydown event
* Key Resources: * Key Resources:
* - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent * - 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 { protected _keyDown(event: KeyboardEvent): boolean {
if (this._customKeyEventHandler && this._customKeyEventHandler(event) === false) { if (this._customKeyEventHandler && this._customKeyEventHandler(event) === false) {
@@ -1616,7 +1527,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
* Handle a keypress event. * Handle a keypress event.
* Key Resources: * Key Resources:
* - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent * - 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 { protected _keyPress(ev: KeyboardEvent): boolean {
let key; let key;
@@ -1629,7 +1540,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
if (ev.charCode) { if (ev.charCode) {
key = ev.charCode; key = ev.charCode;
} else if (ev.which === null || ev.which === undefined) { } else if (ev.which == null) {
key = ev.keyCode; key = ev.keyCode;
} else if (ev.which !== 0 && ev.charCode !== 0) { } else if (ev.which !== 0 && ev.charCode !== 0) {
key = ev.which; key = ev.which;
@@ -1653,6 +1564,21 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
return true; 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. * Ring the bell.
* Note: We could do sweet things with webaudio here * Note: We could do sweet things with webaudio here
@@ -1693,8 +1619,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/** /**
* Resizes the terminal. * Resizes the terminal.
* *
* @param x The number of columns to resize to. * @param {number} x The number of columns to resize to.
* @param y The number of rows to resize to. * @param {number} y The number of rows to resize to.
*/ */
public resize(x: number, y: number): void { public resize(x: number, y: number): void {
if (isNaN(x) || isNaN(y)) { if (isNaN(x) || isNaN(y)) {
@@ -1709,8 +1635,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
return; return;
} }
if (x < MINIMUM_COLS) x = MINIMUM_COLS; if (x < 1) x = 1;
if (y < MINIMUM_ROWS) y = MINIMUM_ROWS; if (y < 1) y = 1;
this.buffers.resize(x, y); this.buffers.resize(x, y);
@@ -1728,7 +1654,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/** /**
* Updates the range of rows to refresh * 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 { public updateRange(y: number): void {
if (y < this._refreshStart) this._refreshStart = y; if (y < this._refreshStart) this._refreshStart = y;
@@ -1749,6 +1675,41 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this._refreshEnd = this.rows - 1; this._refreshEnd = this.rows - 1;
} }
/**
* Erase in the identified line everything from "x" to the end of the line (right).
* @param {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. * Clear the entire buffer, making the prompt line the new first line.
*/ */
@@ -1763,21 +1724,56 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this.buffer.ybase = 0; this.buffer.ybase = 0;
this.buffer.y = 0; this.buffer.y = 0;
for (let i = 1; i < this.rows; i++) { for (let i = 1; i < this.rows; i++) {
this.buffer.lines.push(this.buffer.getBlankLine(DEFAULT_ATTR)); this.buffer.lines.push(this.blankLine());
} }
this.refresh(0, this.rows - 1); this.refresh(0, this.rows - 1);
this.emit('scroll', this.buffer.ydisp); this.emit('scroll', this.buffer.ydisp);
} }
/**
* Erase all content in the given line
* @param {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. * If cur return the back color xterm feature attribute. Else return default attribute.
* @param cur * @param cur
*/ */
public ch(cur?: boolean): CharData { public ch(cur?: boolean): CharData {
if (cur) { 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) */];
} }
/** /**
@@ -1790,7 +1786,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/** /**
* Emit the 'data' event and populate the given data. * 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 { public handler(data: string): void {
// Prevents all events to pty process if stdin is disabled // Prevents all events to pty process if stdin is disabled
@@ -1812,7 +1808,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
/** /**
* Emit the 'title' event and populate the given title. * 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 { public handleTitle(title: string): void {
/** /**
@@ -1855,7 +1851,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
// blankLine(true) is xterm/linux behavior // blankLine(true) is xterm/linux behavior
const scrollRegionHeight = this.buffer.scrollBottom - this.buffer.scrollTop; const scrollRegionHeight = this.buffer.scrollBottom - this.buffer.scrollTop;
this.buffer.lines.shiftElements(this.buffer.y + this.buffer.ybase, scrollRegionHeight, 1); this.buffer.lines.shiftElements(this.buffer.y + this.buffer.ybase, scrollRegionHeight, 1);
this.buffer.lines.set(this.buffer.y + this.buffer.ybase, this.buffer.getBlankLine(this.eraseAttr())); this.buffer.lines.set(this.buffer.y + this.buffer.ybase, this.blankLine(true));
this.updateRange(this.buffer.scrollTop); this.updateRange(this.buffer.scrollTop);
this.updateRange(this.buffer.scrollBottom); this.updateRange(this.buffer.scrollBottom);
} else { } else {
@@ -1904,7 +1900,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
public matchColor(r1: number, g1: number, b1: number): number { public matchColor(r1: number, g1: number, b1: number): number {
const hash = (r1 << 16) | (g1 << 8) | b1; const hash = (r1 << 16) | (g1 << 8) | b1;
if (matchColorCache[hash] !== null && matchColorCache[hash] !== undefined) { if (matchColorCache[hash] != null) {
return matchColorCache[hash]; return matchColorCache[hash];
} }
+36 -67
View File
@@ -7,21 +7,17 @@ import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions,
import { IColorSet, IRenderer } from './renderer/Types'; import { IColorSet, IRenderer } from './renderer/Types';
import { IMouseZoneManager } from './ui/Types'; import { IMouseZoneManager } from './ui/Types';
import { ICharset } from './core/Types'; import { ICharset } from './core/Types';
import { ICircularList } from './common/Types';
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
export type XtermListener = (...args: any[]) => void;
export type CharData = [number, string, number, number]; export type CharData = [number, string, number, number];
export type LineData = CharData[]; export type LineData = CharData[];
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void; export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void;
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => 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 { export const enum LinkHoverEventTypes {
HOVER = 'linkhover', HOVER = 'linkhover',
TOOLTIP = 'linktooltip', TOOLTIP = 'linktooltip',
@@ -67,11 +63,17 @@ export interface IInputHandlingTerminal extends IEventEmitter {
bell(): void; bell(): void;
focus(): void; focus(): void;
convertEol: boolean;
updateRange(y: number): void; updateRange(y: number): void;
scroll(isWrapped?: boolean): void; scroll(isWrapped?: boolean): void;
setgLevel(g: number): void; setgLevel(g: number): void;
eraseAttr(): number; eraseAttr(): number;
eraseRight(x: number, y: number): void;
eraseLine(y: number): void;
eraseLeft(x: number, y: number): void;
blankLine(cur?: boolean, isWrapped?: boolean): LineData;
is(term: string): boolean; is(term: string): boolean;
send(data: string): void;
setgCharset(g: number, charset: ICharset): void; setgCharset(g: number, charset: ICharset): void;
resize(x: number, y: number): void; resize(x: number, y: number): void;
log(text: string, data?: any): void; log(text: string, data?: any): void;
@@ -82,10 +84,6 @@ export interface IInputHandlingTerminal extends IEventEmitter {
error(text: string, data?: any): void; error(text: string, data?: any): void;
setOption(key: string, value: any): void; setOption(key: string, value: any): void;
tabSet(): void; tabSet(): void;
handler(data: string): void;
handleTitle(title: string): void;
index(): void;
reverseIndex(): void;
} }
export interface IViewport extends IDisposable { export interface IViewport extends IDisposable {
@@ -111,7 +109,7 @@ export interface ICompositionHelper {
*/ */
export interface IInputHandler { export interface IInputHandler {
parse(data: string): void; parse(data: string): void;
print(data: Uint32Array, start: number, end: number): void; print(data: string, start: number, end: number): void;
/** C0 BEL */ bell(): void; /** C0 BEL */ bell(): void;
/** C0 LF */ lineFeed(): void; /** C0 LF */ lineFeed(): void;
@@ -200,7 +198,6 @@ export interface ILinkHoverEvent {
x2: number; x2: number;
y2: number; y2: number;
cols: number; cols: number;
fg: number;
} }
export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor { export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
@@ -226,10 +223,12 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce
* @param data The data to populate in the event. * @param data The data to populate in the event.
*/ */
handler(data: string): void; handler(data: string): void;
send(data: string): void;
scrollLines(disp: number, suppressScrollEvent?: boolean): void; scrollLines(disp: number, suppressScrollEvent?: boolean): void;
cancel(ev: Event, force?: boolean): boolean | void; cancel(ev: Event, force?: boolean): boolean | void;
log(text: string): void; log(text: string): void;
showCursor(): void; showCursor(): void;
blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData;
} }
export interface IBufferAccessor { export interface IBufferAccessor {
@@ -237,7 +236,7 @@ export interface IBufferAccessor {
} }
export interface IElementAccessor { export interface IElementAccessor {
readonly element: HTMLElement; element: HTMLElement;
} }
export interface ILinkifierAccessor { export interface ILinkifierAccessor {
@@ -245,8 +244,8 @@ export interface ILinkifierAccessor {
} }
export interface IMouseHelper { export interface IMouseHelper {
getCoords(event: { clientX: number, clientY: number }, element: HTMLElement, charMeasure: ICharMeasure, colCount: number, rowCount: number, isSelection?: boolean): [number, number]; getCoords(event: { pageX: number, pageY: number }, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number];
getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, colCount: number, rowCount: number): { x: number, y: number }; getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number };
} }
export interface ICharMeasure { export interface ICharMeasure {
@@ -267,18 +266,8 @@ export interface ITerminalOptions extends IPublicTerminalOptions {
useFlowControl?: boolean; useFlowControl?: boolean;
} }
export interface IBufferStringIteratorResult {
range: {first: number, last: number};
content: string;
}
export interface IBufferStringIterator {
hasNext(): boolean;
next(): IBufferStringIteratorResult;
}
export interface IBuffer { export interface IBuffer {
readonly lines: ICircularList<IBufferLine>; readonly lines: ICircularList<LineData>;
ydisp: number; ydisp: number;
ybase: number; ybase: number;
y: number; y: number;
@@ -289,15 +278,11 @@ export interface IBuffer {
hasScrollback: boolean; hasScrollback: boolean;
savedY: number; savedY: number;
savedX: number; savedX: number;
savedCurAttr: number;
isCursorInViewport: boolean; isCursorInViewport: boolean;
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string; translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string;
getWrappedRangeForLine(y: number): { first: number, last: number }; getWrappedRangeForLine(y: number): { first: number, last: number };
nextStop(x?: number): number; nextStop(x?: number): number;
prevStop(x?: number): number; prevStop(x?: number): number;
getBlankLine(attr: number, isWrapped?: boolean): IBufferLine;
stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[];
iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator;
} }
export interface IBufferSet extends IEventEmitter { export interface IBufferSet extends IEventEmitter {
@@ -306,7 +291,20 @@ export interface IBufferSet extends IEventEmitter {
active: IBuffer; active: IBuffer;
activateNormalBuffer(): void; activateNormalBuffer(): void;
activateAltBuffer(fillAttr?: number): void; activateAltBuffer(): void;
}
export interface ICircularList<T> extends IEventEmitter {
length: number;
maxLength: number;
get(index: number): T;
set(index: number, value: T): void;
push(value: T): void;
pop(): T;
splice(start: number, deleteCount: number, ...items: T[]): void;
trimStart(count: number): void;
shiftElements(start: number, count: number, offset: number): void;
} }
export interface ISelectionManager { export interface ISelectionManager {
@@ -452,26 +450,18 @@ export interface IParsingState {
* DCS handler signature for EscapeSequenceParser. * DCS handler signature for EscapeSequenceParser.
* EscapeSequenceParser handles DCS commands via separate * EscapeSequenceParser handles DCS commands via separate
* subparsers that get hook/unhooked and can handle * subparsers that get hook/unhooked and can handle
* arbitrary amount of data. * arbitrary amount of print data.
*
* On entering a DSC sequence `hook` is called by * On entering a DSC sequence `hook` is called by
* `EscapeSequenceParser`. Use it to initialize or reset * `EscapeSequenceParser`. Use it to initialize or reset
* states needed to handle the current DCS sequence. * states needed to handle the current DCS sequence.
* Note: A DCS parser is only instantiated once, therefore
* you cannot rely on the ctor to reinitialize state.
*
* EscapeSequenceParser will call `put` several times if the * EscapeSequenceParser will call `put` several times if the
* parsed data got split, therefore you might have to collect * parsed string got splitted, therefore you might have to collect
* `data` until `unhook` is called. * `data` until `unhook` is called. `unhook` marks the end
* Note: `data` is borrowed, if you cannot process the data * of the current DCS sequence.
* in chunks you have to copy it, doing otherwise will lead to
* data losses or corruption.
*
* `unhook` marks the end of the current DCS sequence.
*/ */
export interface IDcsHandler { export interface IDcsHandler {
hook(collect: string, params: number[], flag: number): void; hook(collect: string, params: number[], flag: number): void;
put(data: Uint32Array, start: number, end: number): void; put(data: string, start: number, end: number): void;
unhook(): void; unhook(): void;
} }
@@ -488,9 +478,9 @@ export interface IEscapeSequenceParser extends IDisposable {
* Parse string `data`. * Parse string `data`.
* @param data The data to parse. * @param data The data to parse.
*/ */
parse(data: Uint32Array, length: number): void; parse(data: string): void;
setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void; setPrintHandler(callback: (data: string, start: number, end: number) => void): void;
clearPrintHandler(): void; clearPrintHandler(): void;
setExecuteHandler(flag: string, callback: () => void): void; setExecuteHandler(flag: string, callback: () => void): void;
@@ -500,8 +490,6 @@ export interface IEscapeSequenceParser extends IDisposable {
setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void; setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void;
clearCsiHandler(flag: string): void; clearCsiHandler(flag: string): void;
setCsiHandlerFallback(callback: (collect: string, params: number[], flag: number) => void): void; setCsiHandlerFallback(callback: (collect: string, params: number[], flag: number) => void): void;
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable;
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
setEscHandler(collectAndFlag: string, callback: () => void): void; setEscHandler(collectAndFlag: string, callback: () => void): void;
clearEscHandler(collectAndFlag: string): void; clearEscHandler(collectAndFlag: string): void;
@@ -518,22 +506,3 @@ export interface IEscapeSequenceParser extends IDisposable {
setErrorHandler(callback: (state: IParsingState) => IParsingState): void; setErrorHandler(callback: (state: IParsingState) => IParsingState): void;
clearErrorHandler(): void; clearErrorHandler(): void;
} }
/**
* Interface for a line in the terminal buffer.
*/
export interface IBufferLine {
length: number;
isWrapped: boolean;
get(index: number): CharData;
set(index: number, value: CharData): void;
insertCells(pos: number, n: number, ch: CharData): void;
deleteCells(pos: number, n: number, fill: CharData): void;
replaceCells(start: number, end: number, fill: CharData): void;
resize(cols: number, fill: CharData): void;
fill(fillCharData: CharData): void;
copyFrom(line: IBufferLine): void;
clone(): IBufferLine;
getTrimmedLength(): number;
translateToString(trimRight?: boolean, startCol?: number, endCol?: number): string;
}
+14 -56
View File
@@ -22,16 +22,12 @@ export class Viewport extends Disposable implements IViewport {
private _lastRecordedViewportHeight: number = 0; private _lastRecordedViewportHeight: number = 0;
private _lastRecordedBufferHeight: number = 0; private _lastRecordedBufferHeight: number = 0;
private _lastTouchY: number; private _lastTouchY: number;
private _lastScrollTop: number = 0;
// Stores a partial line amount when scrolling, this is used to keep track of how much of a line // Stores a partial line amount when scrolling, this is used to keep track of how much of a line
// is scrolled so we can "scroll" over partial lines and feel natural on touchpads. This is a // is scrolled so we can "scroll" over partial lines and feel natural on touchpads. This is a
// quick fix and could have a more robust solution in place that reset the value when needed. // quick fix and could have a more robust solution in place that reset the value when needed.
private _wheelPartialScroll: number = 0; private _wheelPartialScroll: number = 0;
private _refreshAnimationFrame: number | null = null;
private _ignoreNextScrollEvent: boolean = false;
/** /**
* Creates a new Viewport. * Creates a new Viewport.
* @param _terminal The terminal this viewport belongs to. * @param _terminal The terminal this viewport belongs to.
@@ -66,12 +62,6 @@ export class Viewport extends Disposable implements IViewport {
* necessary. * necessary.
*/ */
private _refresh(): void { private _refresh(): void {
if (this._refreshAnimationFrame === null) {
this._refreshAnimationFrame = requestAnimationFrame(() => this._innerRefresh());
}
}
private _innerRefresh(): void {
if (this._charMeasure.height > 0) { if (this._charMeasure.height > 0) {
this._currentRowHeight = this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio; this._currentRowHeight = this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio;
this._lastRecordedViewportHeight = this._viewportElement.offsetHeight; this._lastRecordedViewportHeight = this._viewportElement.offsetHeight;
@@ -81,53 +71,30 @@ export class Viewport extends Disposable implements IViewport {
this._scrollArea.style.height = this._lastRecordedBufferHeight + 'px'; this._scrollArea.style.height = this._lastRecordedBufferHeight + 'px';
} }
} }
// Sync scrollTop
const scrollTop = this._terminal.buffer.ydisp * this._currentRowHeight;
if (this._viewportElement.scrollTop !== scrollTop) {
// Ignore the next scroll event which will be triggered by setting the scrollTop as we do not
// want this event to scroll the terminal
this._ignoreNextScrollEvent = true;
this._viewportElement.scrollTop = scrollTop;
}
this._refreshAnimationFrame = null;
} }
/** /**
* Updates dimensions and synchronizes the scroll area if necessary. * Updates dimensions and synchronizes the scroll area if necessary.
*/ */
public syncScrollArea(): void { public syncScrollArea(): void {
// If buffer height changed
if (this._lastRecordedBufferLength !== this._terminal.buffer.lines.length) { if (this._lastRecordedBufferLength !== this._terminal.buffer.lines.length) {
// If buffer height changed
this._lastRecordedBufferLength = this._terminal.buffer.lines.length; this._lastRecordedBufferLength = this._terminal.buffer.lines.length;
this._refresh(); this._refresh();
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 // Sync scrollTop
if (this._lastRecordedViewportHeight !== (<any>this._terminal).renderer.dimensions.canvasHeight) { const scrollTop = this._terminal.buffer.ydisp * this._currentRowHeight;
this._refresh(); if (this._viewportElement.scrollTop !== scrollTop) {
return; this._viewportElement.scrollTop = scrollTop;
}
// 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;
} }
} }
@@ -137,22 +104,13 @@ export class Viewport extends Disposable implements IViewport {
* @param ev The scroll event. * @param ev The scroll event.
*/ */
private _onScroll(ev: Event): void { private _onScroll(ev: Event): void {
// Record current scroll top position
this._lastScrollTop = this._viewportElement.scrollTop;
// Don't attempt to scroll if the element is not visible, otherwise scrollTop will be corrupt // Don't attempt to scroll if the element is not visible, otherwise scrollTop will be corrupt
// which causes the terminal to scroll the buffer to the top // which causes the terminal to scroll the buffer to the top
if (!this._viewportElement.offsetParent) { if (!this._viewportElement.offsetParent) {
return; return;
} }
// Ignore the event if it was flagged to ignore (when the source of the event is from Viewport) const newRow = Math.round(this._viewportElement.scrollTop / this._currentRowHeight);
if (this._ignoreNextScrollEvent) {
this._ignoreNextScrollEvent = false;
return;
}
const newRow = Math.round(this._lastScrollTop / this._currentRowHeight);
const diff = newRow - this._terminal.buffer.ydisp; const diff = newRow - this._terminal.buffer.ydisp;
this._terminal.scrollLines(diff, true); this._terminal.scrollLines(diff, true);
} }
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT * @license MIT
*/ */
import { assert } from 'chai'; import { assert, expect } from 'chai';
import * as attach from './attach'; import * as attach from './attach';
+1 -1
View File
@@ -53,7 +53,7 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean
const fileReader = new FileReader(); const fileReader = new FileReader();
fileReader.addEventListener('load', () => { fileReader.addEventListener('load', () => {
str = myTextDecoder.decode(fileReader.result); str = myTextDecoder.decode(this.result);
displayData(str); displayData(str);
}); });
fileReader.readAsArrayBuffer(ev.data); fileReader.readAsArrayBuffer(ev.data);
-4
View File
@@ -2,10 +2,6 @@
"compilerOptions": { "compilerOptions": {
"module": "commonjs", "module": "commonjs",
"target": "es5", "target": "es5",
"lib": [
"dom",
"es6",
],
"rootDir": ".", "rootDir": ".",
"outDir": "../../../lib/addons/attach/", "outDir": "../../../lib/addons/attach/",
"sourceMap": true, "sourceMap": true,
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT * @license MIT
*/ */
import { assert } from 'chai'; import { assert, expect } from 'chai';
import * as fit from './fit'; import * as fit from './fit';
+1 -8
View File
@@ -2,19 +2,12 @@
"compilerOptions": { "compilerOptions": {
"module": "commonjs", "module": "commonjs",
"target": "es5", "target": "es5",
"lib": [
"dom",
"es5"
],
"rootDir": ".", "rootDir": ".",
"outDir": "../../../lib/addons/fit/", "outDir": "../../../lib/addons/fit/",
"sourceMap": true, "sourceMap": true,
"removeComments": true, "removeComments": true,
"declaration": true, "declaration": true,
"preserveWatchOutput": true, "preserveWatchOutput": true
"types": [
"../../node_modules/@types/mocha"
]
}, },
"include": [ "include": [
"**/*.ts", "**/*.ts",
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT * @license MIT
*/ */
import { assert } from 'chai'; import { assert, expect } from 'chai';
import * as fullscreen from './fullscreen'; import * as fullscreen from './fullscreen';
+5 -7
View File
@@ -11,19 +11,17 @@ import { Terminal } from 'xterm';
* @param fullscreen Toggle fullscreen on (true) or off (false) * @param fullscreen Toggle fullscreen on (true) or off (false)
*/ */
export function toggleFullScreen(term: Terminal, fullscreen: boolean): void { export function toggleFullScreen(term: Terminal, fullscreen: boolean): void {
let fn: (...tokens: string[]) => void; let fn: string;
if (typeof fullscreen === 'undefined') { if (typeof fullscreen === 'undefined') {
fn = (term.element.classList.contains('fullscreen')) ? fn = (term.element.classList.contains('fullscreen')) ? 'remove' : 'add';
term.element.classList.remove : term.element.classList.add;
} else if (!fullscreen) { } else if (!fullscreen) {
fn = term.element.classList.remove; fn = 'remove';
} else { } else {
fn = term.element.classList.add; fn = 'add';
} }
fn = fn.bind(term.element.classList); term.element.classList[fn]('fullscreen');
fn('fullscreen');
} }
export function apply(terminalConstructor: typeof Terminal): void { export function apply(terminalConstructor: typeof Terminal): void {
+1 -8
View File
@@ -2,19 +2,12 @@
"compilerOptions": { "compilerOptions": {
"module": "commonjs", "module": "commonjs",
"target": "es5", "target": "es5",
"lib": [
"dom",
"es5"
],
"rootDir": ".", "rootDir": ".",
"outDir": "../../../lib/addons/fullscreen/", "outDir": "../../../lib/addons/fullscreen/",
"sourceMap": true, "sourceMap": true,
"removeComments": true, "removeComments": true,
"declaration": true, "declaration": true,
"preserveWatchOutput": true, "preserveWatchOutput": true
"types": [
"../../node_modules/@types/mocha"
]
}, },
"include": [ "include": [
"**/*.ts", "**/*.ts",
+2 -19
View File
@@ -17,23 +17,6 @@ export interface ISearchAddonTerminal extends Terminal {
} }
export interface ISearchHelper { export interface ISearchHelper {
findNext(term: string, searchOptions: ISearchOptions): boolean; findNext(term: string): boolean;
findPrevious(term: string, searchOptions: ISearchOptions): boolean; findPrevious(term: string): boolean;
}
export interface ISearchOptions {
regex?: boolean;
wholeWord?: boolean;
caseSensitive?: boolean;
/**
* Use this when you want the selection to expand if it still matches as the
* user types. Note that this only affects findNext.
*/
incremental?: boolean;
}
export interface ISearchResult {
term: string;
col: number;
row: number;
} }
+57 -205
View File
@@ -3,76 +3,56 @@
* @license MIT * @license MIT
*/ */
import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult } from './Interfaces'; import { ISearchHelper, ISearchAddonTerminal } from './Interfaces';
const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?'; interface ISearchResult {
const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs term: string;
col: number;
row: number;
}
/** /**
* A class that knows how to search the terminal and how to display the results. * A class that knows how to search the terminal and how to display the results.
*/ */
export class SearchHelper implements ISearchHelper { export class SearchHelper implements ISearchHelper {
/**
* translateBufferLineToStringWithWrap is a fairly expensive call.
* We memoize the calls into an array that has a time based ttl.
* _linesCache is also invalidated when the terminal cursor moves.
*/
private _linesCache: string[] = null;
private _linesCacheTimeoutId = 0;
constructor(private _terminal: ISearchAddonTerminal) { constructor(private _terminal: ISearchAddonTerminal) {
this._destroyLinesCache = this._destroyLinesCache.bind(this); // 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 * Find the next instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing. * doesn't exist, do nothing.
* @param term The search term. * @param term Tne search term.
* @param searchOptions Search options.
* @return Whether a result was found. * @return Whether a result was found.
*/ */
public findNext(term: string, searchOptions?: ISearchOptions): boolean { public findNext(term: string): boolean {
const selectionManager = this._terminal._core.selectionManager;
const {incremental} = searchOptions;
let result: ISearchResult;
if (!term || term.length === 0) { if (!term || term.length === 0) {
selectionManager.clearSelection();
return false; return false;
} }
let startCol: number = 0; let result: ISearchResult;
let startRow = this._terminal._core.buffer.ydisp; let startRow = this._terminal._core.buffer.ydisp;
if (this._terminal._core.selectionManager.selectionEnd) {
if (selectionManager.selectionEnd) {
// Start from the selection end if there is a selection // Start from the selection end if there is a selection
// For incremental search, use existing row startRow = this._terminal._core.selectionManager.selectionEnd[1];
if (this._terminal.getSelection().length !== 0) { }
startRow = incremental ? selectionManager.selectionStart[1] : selectionManager.selectionEnd[1];
startCol = incremental ? selectionManager.selectionStart[0] : selectionManager.selectionEnd[0]; // 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);
if (result) {
break;
} }
} }
this._initLinesCache(); // Search from the top to the current ydisp
// Search startRow
result = this._findInLine(term, startRow, startCol, searchOptions);
// Search from startRow + 1 to end
if (!result) { if (!result) {
for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) { for (let y = 0; y < startRow; y++) {
result = this._findInLine(term, y, 0, searchOptions); result = this._findInLine(term, y);
if (result) {
break;
}
}
}
// Search from the top to the startRow (search the whole startRow again in
// case startCol > 0)
if (!result) {
for (let y = 0; y <= startRow; y++) {
result = this._findInLine(term, y, 0, searchOptions);
if (result) { if (result) {
break; break;
} }
@@ -86,52 +66,34 @@ export class SearchHelper implements ISearchHelper {
/** /**
* Find the previous instance of the term, then scroll to and select it. If it * Find the previous instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing. * doesn't exist, do nothing.
* @param term The search term. * @param term Tne search term.
* @param searchOptions Search options.
* @return Whether a result was found. * @return Whether a result was found.
*/ */
public findPrevious(term: string, searchOptions?: ISearchOptions): boolean { public findPrevious(term: string): boolean {
const selectionManager = this._terminal._core.selectionManager;
let result: ISearchResult;
if (!term || term.length === 0) { if (!term || term.length === 0) {
selectionManager.clearSelection();
return false; return false;
} }
const isReverseSearch = true; let result: ISearchResult;
let startRow = this._terminal._core.buffer.ydisp; let startRow = this._terminal._core.buffer.ydisp;
let startCol: number = this._terminal._core.buffer.lines.get(startRow).length; if (this._terminal._core.selectionManager.selectionStart) {
// Start from the selection end if there is a selection
startRow = this._terminal._core.selectionManager.selectionStart[1];
}
if (selectionManager.selectionStart) { // Search from ydisp + 1 to end
// Start from the selection start if there is a selection for (let y = startRow - 1; y >= 0; y--) {
if (this._terminal.getSelection().length !== 0) { result = this._findInLine(term, y);
startRow = selectionManager.selectionStart[1]; if (result) {
startCol = selectionManager.selectionStart[0]; break;
} }
} }
this._initLinesCache(); // Search from the top to the current ydisp
// Search startRow
result = this._findInLine(term, startRow, startCol, searchOptions, isReverseSearch);
// Search from startRow - 1 to top
if (!result) { if (!result) {
for (let y = startRow - 1; y >= 0; y--) { for (let y = this._terminal._core.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) {
result = this._findInLine(term, y, this._terminal._core.buffer.lines.get(y).length, searchOptions, isReverseSearch); result = this._findInLine(term, y);
if (result) {
break;
}
}
}
// Search from the bottom to startRow (search the whole startRow again in
// case startCol > 0)
if (!result) {
const searchFrom = this._terminal._core.buffer.ybase + this._terminal.rows - 1;
for (let y = searchFrom; y >= startRow; y--) {
result = this._findInLine(term, y, this._terminal._core.buffer.lines.get(y).length, searchOptions, isReverseSearch);
if (result) { if (result) {
break; break;
} }
@@ -143,147 +105,38 @@ export class SearchHelper implements ISearchHelper {
} }
/** /**
* Sets up a line cache with a ttl * Searches a line for a search term.
*/ * @param term Tne search term.
private _initLinesCache(): void { * @param y The line to search.
if (!this._linesCache) {
this._linesCache = new Array(this._terminal._core.buffer.length);
this._terminal.on('cursormove', this._destroyLinesCache);
}
window.clearTimeout(this._linesCacheTimeoutId);
this._linesCacheTimeoutId = window.setTimeout(() => this._destroyLinesCache(), LINES_CACHE_TIME_TO_LIVE);
}
private _destroyLinesCache(): void {
this._linesCache = null;
this._terminal.off('cursormove', this._destroyLinesCache);
if (this._linesCacheTimeoutId) {
window.clearTimeout(this._linesCacheTimeoutId);
this._linesCacheTimeoutId = 0;
}
}
/**
* 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) || (NON_WORD_CHARACTERS.indexOf(line[searchIndex - 1]) !== -1)) &&
(((searchIndex + term.length) === line.length) || (NON_WORD_CHARACTERS.indexOf(line[searchIndex + term.length]) !== -1)));
}
/**
* Searches a line for a search term. Takes the provided terminal line and searches the text line, which may contain
* subsequent terminal lines if the text is wrapped. If the provided line number is part of a wrapped text line that
* started on an earlier line then it is skipped since it will be properly searched when the terminal line that the
* text starts on is searched.
* @param term The search term.
* @param row The line to start the search from.
* @param col The column to start the search from.
* @param searchOptions Search options.
* @return The search result if it was found. * @return The search result if it was found.
*/ */
protected _findInLine(term: string, row: number, col: number, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult { private _findInLine(term: string, y: number): ISearchResult {
if (this._terminal._core.buffer.lines.get(row).isWrapped) { const lowerStringLine = this._terminal._core.buffer.translateBufferLineToString(y, true).toLowerCase();
return; const lowerTerm = term.toLowerCase();
} let searchIndex = lowerStringLine.indexOf(lowerTerm);
if (searchIndex >= 0) {
let stringLine = this._linesCache ? this._linesCache[row] : void 0; const line = this._terminal._core.buffer.lines.get(y);
if (stringLine === void 0) { for (let i = 0; i < searchIndex; i++) {
stringLine = this.translateBufferLineToStringWithWrap(row, true); const charData = line[i];
if (this._linesCache) {
this._linesCache[row] = stringLine;
}
}
const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();
const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
let resultIndex = -1;
if (searchOptions.regex) {
const searchRegex = RegExp(searchTerm, 'g');
let foundTerm: RegExpExecArray;
if (isReverseSearch) {
// This loop will get the resultIndex of the _last_ regex match in the range 0..col
while (foundTerm = searchRegex.exec(searchStringLine.slice(0, col))) {
resultIndex = searchRegex.lastIndex - foundTerm[0].length;
term = foundTerm[0];
searchRegex.lastIndex -= (term.length - 1);
}
} else {
foundTerm = searchRegex.exec(searchStringLine.slice(col));
if (foundTerm && foundTerm[0].length > 0) {
resultIndex = col + (searchRegex.lastIndex - foundTerm[0].length);
term = foundTerm[0];
}
}
} else {
if (isReverseSearch) {
if (col - searchTerm.length >= 0) {
resultIndex = searchStringLine.lastIndexOf(searchTerm, col - searchTerm.length);
}
} else {
resultIndex = searchStringLine.indexOf(searchTerm, col);
}
}
if (resultIndex >= 0) {
// Adjust the row number and search index if needed since a "line" of text can span multiple rows
if (resultIndex >= this._terminal.cols) {
row += Math.floor(resultIndex / this._terminal.cols);
resultIndex = resultIndex % this._terminal.cols;
}
if (searchOptions.wholeWord && !this._isWholeWord(resultIndex, searchStringLine, term)) {
return;
}
const line = this._terminal._core.buffer.lines.get(row);
for (let i = 0; i < resultIndex; i++) {
const charData = line.get(i);
// Adjust the searchIndex to normalize emoji into single chars // Adjust the searchIndex to normalize emoji into single chars
const char = charData[1/*CHAR_DATA_CHAR_INDEX*/]; const char = charData[1/*CHAR_DATA_CHAR_INDEX*/];
if (char.length > 1) { if (char.length > 1) {
resultIndex -= char.length - 1; searchIndex -= char.length - 1;
} }
// Adjust the searchIndex for empty characters following wide unicode // Adjust the searchIndex for empty characters following wide unicode
// chars (eg. CJK) // chars (eg. CJK)
const charWidth = charData[2/*CHAR_DATA_WIDTH_INDEX*/]; const charWidth = charData[2/*CHAR_DATA_WIDTH_INDEX*/];
if (charWidth === 0) { if (charWidth === 0) {
resultIndex++; searchIndex++;
} }
} }
return { return {
term, term,
col: resultIndex, col: searchIndex,
row row: y
}; };
} }
} }
/**
* 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).substring(0, this._terminal.cols);
lineIndex++;
} while (lineWrapsToNext);
return lineString;
}
/** /**
* Selects and scrolls to a result. * Selects and scrolls to a result.
@@ -292,7 +145,6 @@ export class SearchHelper implements ISearchHelper {
*/ */
private _selectResult(result: ISearchResult): boolean { private _selectResult(result: ISearchResult): boolean {
if (!result) { if (!result) {
this._terminal.clearSelection();
return false; return false;
} }
this._terminal._core.selectionManager.setSelection(result.col, result.row, result.term.length); this._terminal._core.selectionManager.setSelection(result.col, result.row, result.term.length);
-313
View File
@@ -1,313 +0,0 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
declare var require: any;
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, rowNumber: number, searchOptions?: ISearchOptions): ISearchResult {
return this._findInLine(term, rowNumber, 0, searchOptions);
}
public findFromIndex(term: string, row: number, col: number, searchOptions?: ISearchOptions, isReverseSearch?: boolean): ISearchResult {
return this._findInLine(term, row, col, searchOptions, isReverseSearch);
}
}
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'});
term.core.resize(9, 5);
const hello0Resize = term.searchHelper.findInLine('Hello', 0);
expect(hello0Resize).eql({col: 8, row: 0, term: 'Hello'});
});
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});
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);
});
it('should find multiple matches in line', function(): void {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 20, rows: 5});
term.core.write('helloooo helloooo\r\naaaAAaaAAA');
term.pushWriteData();
const searchOptions = {
regex: false,
wholeWord: false,
caseSensitive: false
};
const find0 = term.searchHelper.findFromIndex('hello', 0, 0, searchOptions);
const find1 = term.searchHelper.findFromIndex('hello', 0, find0.col + find0.term.length, searchOptions);
const find2 = term.searchHelper.findFromIndex('aaaa', 1, 0, searchOptions);
const find3 = term.searchHelper.findFromIndex('aaaa', 1, find2.col + find2.term.length, searchOptions);
const find4 = term.searchHelper.findFromIndex('aaaa', 1, find3.col + find3.term.length, searchOptions);
expect(find0).eql({col: 0, row: 0, term: 'hello'});
expect(find1).eql({col: 9, row: 0, term: 'hello'});
expect(find2).eql({col: 0, row: 1, term: 'aaaa'});
expect(find3).eql({col: 4, row: 1, term: 'aaaa'});
expect(find4).eql(undefined);
});
it('should find multiple matches in line - reverse search', function(): void {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 20, rows: 5});
term.core.write('it is what it is');
term.pushWriteData();
const searchOptions = {
regex: false,
wholeWord: false,
caseSensitive: false
};
const isReverseSearch = true;
const find0 = term.searchHelper.findFromIndex('is', 0, 16, searchOptions, isReverseSearch);
const find1 = term.searchHelper.findFromIndex('is', 0, find0.col, searchOptions, isReverseSearch);
const find2 = term.searchHelper.findFromIndex('it', 0, 16, searchOptions, isReverseSearch);
const find3 = term.searchHelper.findFromIndex('it', 0, find2.col, searchOptions, isReverseSearch);
expect(find0).eql({col: 14, row: 0, term: 'is'});
expect(find1).eql({col: 3, row: 0, term: 'is'});
expect(find2).eql({col: 11, row: 0, term: 'it'});
expect(find3).eql({col: 0, row: 0, term: 'it'});
});
it('should find multiple matches in line - reverse search with regex', function(): void {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 20, rows: 5});
term.core.write('zzzABCzzzzABCABC');
term.pushWriteData();
const searchOptions = {
regex: true,
wholeWord: false,
caseSensitive: true
};
const isReverseSearch = true;
const find0 = term.searchHelper.findFromIndex('[A-Z]{3}', 0, 16, searchOptions, isReverseSearch);
const find1 = term.searchHelper.findFromIndex('[A-Z]{3}', 0, find0.col, searchOptions, isReverseSearch);
const find2 = term.searchHelper.findFromIndex('[A-Z]{3}', 0, find1.col, searchOptions, isReverseSearch);
const find3 = term.searchHelper.findFromIndex('[A-Z]{3}', 0, find2.col, searchOptions, isReverseSearch);
expect(find0).eql({col: 13, row: 0, term: 'ABC'});
expect(find1).eql({col: 10, row: 0, term: 'ABC'});
expect(find2).eql({col: 3, row: 0, term: 'ABC'});
expect(find3).eql(undefined);
});
});
});
+11 -13
View File
@@ -5,44 +5,42 @@
import { SearchHelper } from './SearchHelper'; import { SearchHelper } from './SearchHelper';
import { Terminal } from 'xterm'; 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 * Find the next instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing. * doesn't exist, do nothing.
* @param term The search term. * @param term Tne search term.
* @param searchOptions Search options
* @return Whether a result was found. * @return Whether a result was found.
*/ */
export function findNext(terminal: Terminal, term: string, searchOptions: ISearchOptions = {}): boolean { export function findNext(terminal: Terminal, term: string): boolean {
const addonTerminal = <ISearchAddonTerminal>terminal; const addonTerminal = <ISearchAddonTerminal>terminal;
if (!addonTerminal.__searchHelper) { if (!addonTerminal.__searchHelper) {
addonTerminal.__searchHelper = new SearchHelper(addonTerminal); addonTerminal.__searchHelper = new SearchHelper(addonTerminal);
} }
return addonTerminal.__searchHelper.findNext(term, searchOptions); return addonTerminal.__searchHelper.findNext(term);
} }
/** /**
* Find the previous instance of the term, then scroll to and select it. If it * Find the previous instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing. * doesn't exist, do nothing.
* @param term The search term. * @param term Tne search term.
* @param searchOptions Search options
* @return Whether a result was found. * @return Whether a result was found.
*/ */
export function findPrevious(terminal: Terminal, term: string, searchOptions: ISearchOptions): boolean { export function findPrevious(terminal: Terminal, term: string): boolean {
const addonTerminal = <ISearchAddonTerminal>terminal; const addonTerminal = <ISearchAddonTerminal>terminal;
if (!addonTerminal.__searchHelper) { if (!addonTerminal.__searchHelper) {
addonTerminal.__searchHelper = new SearchHelper(addonTerminal); addonTerminal.__searchHelper = new SearchHelper(addonTerminal);
} }
return addonTerminal.__searchHelper.findPrevious(term, searchOptions); return addonTerminal.__searchHelper.findPrevious(term);
} }
export function apply(terminalConstructor: typeof Terminal): void { export function apply(terminalConstructor: typeof Terminal): void {
(<any>terminalConstructor.prototype).findNext = function(term: string, searchOptions: ISearchOptions): boolean { (<any>terminalConstructor.prototype).findNext = function(term: string): boolean {
return findNext(this, term, searchOptions); return findNext(this, term);
}; };
(<any>terminalConstructor.prototype).findPrevious = function(term: string, searchOptions: ISearchOptions): boolean { (<any>terminalConstructor.prototype).findPrevious = function(term: string): boolean {
return findPrevious(this, term, searchOptions); return findPrevious(this, term);
}; };
} }
+1 -8
View File
@@ -2,19 +2,12 @@
"compilerOptions": { "compilerOptions": {
"module": "commonjs", "module": "commonjs",
"target": "es5", "target": "es5",
"lib": [
"dom",
"es5"
],
"rootDir": ".", "rootDir": ".",
"outDir": "../../../lib/addons/search/", "outDir": "../../../lib/addons/search/",
"sourceMap": true, "sourceMap": true,
"removeComments": true, "removeComments": true,
"declaration": true, "declaration": true,
"preserveWatchOutput": true, "preserveWatchOutput": true
"types": [
"../../node_modules/@types/mocha"
]
}, },
"include": [ "include": [
"**/*.ts", "**/*.ts",
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT * @license MIT
*/ */
import { assert } from 'chai'; import { assert, expect } from 'chai';
import * as terminado from './terminado'; import * as terminado from './terminado';
+1 -7
View File
@@ -2,18 +2,12 @@
"compilerOptions": { "compilerOptions": {
"module": "commonjs", "module": "commonjs",
"target": "es5", "target": "es5",
"lib": [
"es5"
],
"rootDir": ".", "rootDir": ".",
"outDir": "../../../lib/addons/terminado/", "outDir": "../../../lib/addons/terminado/",
"sourceMap": true, "sourceMap": true,
"removeComments": true, "removeComments": true,
"declaration": true, "declaration": true,
"preserveWatchOutput": true, "preserveWatchOutput": true
"types": [
"../../node_modules/@types/mocha"
]
}, },
"include": [ "include": [
"**/*.ts", "**/*.ts",
+1 -8
View File
@@ -2,19 +2,12 @@
"compilerOptions": { "compilerOptions": {
"module": "commonjs", "module": "commonjs",
"target": "es5", "target": "es5",
"lib": [
"dom",
"es5",
],
"rootDir": ".", "rootDir": ".",
"outDir": "../../../lib/addons/webLinks/", "outDir": "../../../lib/addons/webLinks/",
"sourceMap": true, "sourceMap": true,
"removeComments": true, "removeComments": true,
"declaration": true, "declaration": true,
"preserveWatchOutput": true, "preserveWatchOutput": true
"types": [
"../../node_modules/@types/mocha"
]
}, },
"include": [ "include": [
"**/*.ts", "**/*.ts",
+1 -49
View File
@@ -3,7 +3,7 @@
* @license MIT * @license MIT
*/ */
import { assert } from 'chai'; import { assert, expect } from 'chai';
import * as webLinks from './webLinks'; import * as webLinks from './webLinks';
@@ -39,52 +39,4 @@ describe('webLinks addon', () => {
assert.equal(uri, 'http://foo.com/a~b#c~d?e~f'); assert.equal(uri, 'http://foo.com/a~b#c~d?e~f');
}); });
it('should allow : character in URI path', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
const row = ' http://foo.com/colon:test ';
const match = row.match(term.regex);
const uri = match[term.options.matchIndex];
assert.equal(uri, 'http://foo.com/colon:test');
});
it('should not allow : character at the end of a URI path', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
const row = ' http://foo.com/colon:test: ';
const match = row.match(term.regex);
const uri = match[term.options.matchIndex];
assert.equal(uri, 'http://foo.com/colon:test');
});
it('should not allow " character at the end of a URI enclosed with ""', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
const row = '"http://foo.com/"';
const match = row.match(term.regex);
const uri = match[term.options.matchIndex];
assert.equal(uri, 'http://foo.com/');
});
it('should not allow \' character at the end of a URI enclosed with \'\'', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
const row = '\'http://foo.com/\'';
const match = row.match(term.regex);
const uri = match[term.options.matchIndex];
assert.equal(uri, 'http://foo.com/');
});
}); });
+1 -1
View File
@@ -14,7 +14,7 @@ const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
const localHostClause = '(localhost)'; const localHostClause = '(localhost)';
const portClause = '(:\\d{1,5})'; const portClause = '(:\\d{1,5})';
const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?'; const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
const pathClause = '(\\/[\\/\\w\\.\\-%~:]*)*([^:"\'\\s])'; const pathClause = '(\\/[\\/\\w\\.\\-%~]*)*';
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*'; const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?'; const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?'; const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
+1 -7
View File
@@ -2,18 +2,12 @@
"compilerOptions": { "compilerOptions": {
"module": "commonjs", "module": "commonjs",
"target": "es5", "target": "es5",
"lib": [
"es5"
],
"rootDir": ".", "rootDir": ".",
"outDir": "../../../lib/addons/winptyCompat/", "outDir": "../../../lib/addons/winptyCompat/",
"sourceMap": true, "sourceMap": true,
"removeComments": true, "removeComments": true,
"declaration": true, "declaration": true,
"preserveWatchOutput": true, "preserveWatchOutput": true
"types": [
"../../node_modules/@types/mocha"
]
}, },
"include": [ "include": [
"**/*.ts", "**/*.ts",
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT * @license MIT
*/ */
import { assert } from 'chai'; import { assert, expect } from 'chai';
import * as winptyCompat from './winptyCompat'; import * as winptyCompat from './winptyCompat';
+3 -9
View File
@@ -6,10 +6,6 @@
import { Terminal } from 'xterm'; import { Terminal } from 'xterm';
import { IWinptyCompatAddonTerminal } from './Interfaces'; import { IWinptyCompatAddonTerminal } from './Interfaces';
const CHAR_DATA_CODE_INDEX = 3;
const NULL_CELL_CODE = 0;
const WHITESPACE_CELL_CODE = 32;
export function winptyCompatInit(terminal: Terminal): void { export function winptyCompatInit(terminal: Terminal): void {
const addonTerminal = <IWinptyCompatAddonTerminal>terminal; const addonTerminal = <IWinptyCompatAddonTerminal>terminal;
@@ -19,8 +15,6 @@ export function winptyCompatInit(terminal: Terminal): void {
return; return;
} }
(addonTerminal._core as any).isWinptyCompatEnabled = true;
// Winpty does not support wraparound mode which means that lines will never // Winpty does not support wraparound mode which means that lines will never
// be marked as wrapped. This causes issues for things like copying a line // be marked as wrapped. This causes issues for things like copying a line
// retaining the wrapped new line characters or if consumers are listening // retaining the wrapped new line characters or if consumers are listening
@@ -33,11 +27,11 @@ export function winptyCompatInit(terminal: Terminal): void {
// wrapped. // wrapped.
addonTerminal.on('linefeed', () => { addonTerminal.on('linefeed', () => {
const line = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y - 1); const line = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y - 1);
const lastChar = line.get(addonTerminal.cols - 1); const lastChar = line[addonTerminal.cols - 1];
if (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE && lastChar[CHAR_DATA_CODE_INDEX] !== WHITESPACE_CELL_CODE) { if (lastChar[3] !== 32 /* ' ' */) {
const nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y); const nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y);
nextLine.isWrapped = true; (<any>nextLine).isWrapped = true;
} }
}); });
} }
+1 -7
View File
@@ -2,18 +2,12 @@
"compilerOptions": { "compilerOptions": {
"module": "commonjs", "module": "commonjs",
"target": "es5", "target": "es5",
"lib": [
"es5"
],
"rootDir": ".", "rootDir": ".",
"outDir": "../../../lib/addons/zmodem/", "outDir": "../../../lib/addons/zmodem/",
"sourceMap": true, "sourceMap": true,
"removeComments": true, "removeComments": true,
"declaration": true, "declaration": true,
"preserveWatchOutput": true, "preserveWatchOutput": true
"types": [
"../../node_modules/@types/mocha"
]
}, },
"include": [ "include": [
"**/*.ts", "**/*.ts",
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT * @license MIT
*/ */
import { assert } from 'chai'; import { assert, expect } from 'chai';
import * as zmodem from './zmodem'; import * as zmodem from './zmodem';
+2 -2
View File
@@ -34,7 +34,7 @@ import { Terminal } from 'xterm';
* via `detach()` and a re-`attach()`.) * via `detach()` and a re-`attach()`.)
*/ */
let zmodem: any; let zmodem;
export interface IZmodemOptions { export interface IZmodemOptions {
noTerminalWriteOutsideSession?: boolean; noTerminalWriteOutsideSession?: boolean;
@@ -44,7 +44,7 @@ function zmodemAttach(ws: WebSocket, opts: IZmodemOptions = {}): void {
const term = this; const term = this;
const senderFunc = (octets: ArrayLike<number>) => ws.send(new Uint8Array(octets)); const senderFunc = (octets: ArrayLike<number>) => ws.send(new Uint8Array(octets));
let zsentry: any; let zsentry;
function shouldWrite(): boolean { function shouldWrite(): boolean {
return !!zsentry.get_confirmed_session() || !opts.noTerminalWriteOutsideSession; return !!zsentry.get_confirmed_session() || !opts.noTerminalWriteOutsideSession;
+33 -59
View File
@@ -3,25 +3,15 @@
* @license MIT * @license MIT
*/ */
import { EventEmitter } from './EventEmitter'; import { EventEmitter } from '../EventEmitter';
import { ICircularList } from './Types'; import { ICircularList } from '../Types';
export interface IInsertEvent {
index: number;
amount: number;
}
export interface IDeleteEvent {
index: number;
amount: number;
}
/** /**
* Represents a circular list; a list with a maximum size that wraps around when push is called, * Represents a circular list; a list with a maximum size that wraps around when push is called,
* overriding values at the start of the list. * overriding values at the start of the list.
*/ */
export class CircularList<T> extends EventEmitter implements ICircularList<T> { export class CircularList<T> extends EventEmitter implements ICircularList<T> {
protected _array: (T | undefined)[]; protected _array: T[];
private _startIndex: number; private _startIndex: number;
private _length: number; private _length: number;
@@ -46,7 +36,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
// Reconstruct array, starting at index 0. Only transfer values from the // Reconstruct array, starting at index 0. Only transfer values from the
// indexes 0 to length. // indexes 0 to length.
const newArray = new Array<T | undefined>(newMaxLength); const newArray = new Array<T>(newMaxLength);
for (let i = 0; i < Math.min(newMaxLength, this.length); i++) { for (let i = 0; i < Math.min(newMaxLength, this.length); i++) {
newArray[i] = this._array[this._getCyclicIndex(i)]; newArray[i] = this._array[this._getCyclicIndex(i)];
} }
@@ -76,7 +66,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
* @param index The index of the value to get. * @param index The index of the value to get.
* @return The value corresponding to the index. * @return The value corresponding to the index.
*/ */
public get(index: number): T | undefined { public get(index: number): T {
return this._array[this._getCyclicIndex(index)]; return this._array[this._getCyclicIndex(index)];
} }
@@ -88,7 +78,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
* @param index The index to set. * @param index The index to set.
* @param value The value to set. * @param value The value to set.
*/ */
public set(index: number, value: T | undefined): void { public set(index: number, value: T): void {
this._array[this._getCyclicIndex(index)] = value; this._array[this._getCyclicIndex(index)] = value;
} }
@@ -100,39 +90,21 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
public push(value: T): void { public push(value: T): void {
this._array[this._getCyclicIndex(this._length)] = value; this._array[this._getCyclicIndex(this._length)] = value;
if (this._length === this._maxLength) { if (this._length === this._maxLength) {
this._startIndex = ++this._startIndex % this._maxLength; this._startIndex++;
this.emitMayRemoveListeners('trim', 1); if (this._startIndex === this._maxLength) {
this._startIndex = 0;
}
this.emit('trim', 1);
} else { } else {
this._length++; this._length++;
} }
} }
/**
* Advance ringbuffer index and return current element for recycling.
* Note: The buffer must be full for this method to work.
* @throws When the buffer is not full.
*/
public recycle(): T {
if (this._length !== this._maxLength) {
throw new Error('Can only recycle when the buffer is full');
}
this._startIndex = ++this._startIndex % this._maxLength;
this.emitMayRemoveListeners('trim', 1);
return this._array[this._getCyclicIndex(this._length - 1)]!;
}
/**
* Ringbuffer is at max length.
*/
public get isFull(): boolean {
return this._length === this._maxLength;
}
/** /**
* Removes and returns the last value on the list. * Removes and returns the last value on the list.
* @return The popped value. * @return The popped value.
*/ */
public pop(): T | undefined { public pop(): T {
return this._array[this._getCyclicIndex(this._length-- - 1)]; return this._array[this._getCyclicIndex(this._length-- - 1)];
} }
@@ -154,22 +126,24 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
this._length -= deleteCount; this._length -= deleteCount;
} }
// Add items if (items && items.length) {
for (let i = this._length - 1; i >= start; i--) { // Add items
this._array[this._getCyclicIndex(i + items.length)] = this._array[this._getCyclicIndex(i)]; for (let i = this._length - 1; i >= start; i--) {
} this._array[this._getCyclicIndex(i + items.length)] = this._array[this._getCyclicIndex(i)];
for (let i = 0; i < items.length; i++) { }
this._array[this._getCyclicIndex(start + i)] = items[i]; for (let i = 0; i < items.length; i++) {
} this._array[this._getCyclicIndex(start + i)] = items[i];
}
// Adjust length as needed // Adjust length as needed
if (this._length + items.length > this._maxLength) { if (this._length + items.length > this.maxLength) {
const countToTrim = (this._length + items.length) - this._maxLength; const countToTrim = (this._length + items.length) - this.maxLength;
this._startIndex += countToTrim; this._startIndex += countToTrim;
this._length = this._maxLength; this._length = this.maxLength;
this.emitMayRemoveListeners('trim', countToTrim); this.emit('trim', countToTrim);
} else { } else {
this._length += items.length; this._length += items.length;
}
} }
} }
@@ -183,7 +157,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
} }
this._startIndex += count; this._startIndex += count;
this._length -= count; this._length -= count;
this.emitMayRemoveListeners('trim', count); this.emit('trim', count);
} }
public shiftElements(start: number, count: number, offset: number): void { public shiftElements(start: number, count: number, offset: number): void {
@@ -204,10 +178,10 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
const expandListBy = (start + count + offset) - this._length; const expandListBy = (start + count + offset) - this._length;
if (expandListBy > 0) { if (expandListBy > 0) {
this._length += expandListBy; this._length += expandListBy;
while (this._length > this._maxLength) { while (this._length > this.maxLength) {
this._length--; this._length--;
this._startIndex++; this._startIndex++;
this.emitMayRemoveListeners('trim', 1); this.emit('trim', 1);
} }
} }
} else { } else {
@@ -224,6 +198,6 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
* @returns The cyclic index. * @returns The cyclic index.
*/ */
private _getCyclicIndex(index: number): number { private _getCyclicIndex(index: number): number {
return (this._startIndex + index) % this._maxLength; return (this._startIndex + index) % this.maxLength;
} }
} }
-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 { export abstract class Disposable implements IDisposable {
protected _disposables: IDisposable[] = []; protected _disposables: IDisposable[] = [];
protected _isDisposed: boolean = false;
constructor() { constructor() {
} }
@@ -20,7 +19,6 @@ export abstract class Disposable implements IDisposable {
* Disposes the object, triggering the `dispose` method on all registered IDisposables. * Disposes the object, triggering the `dispose` method on all registered IDisposables.
*/ */
public dispose(): void { public dispose(): void {
this._isDisposed = true;
this._disposables.forEach(d => d.dispose()); this._disposables.forEach(d => d.dispose());
this._disposables.length = 0; this._disposables.length = 0;
} }
@@ -32,16 +30,4 @@ export abstract class Disposable implements IDisposable {
public register<T extends IDisposable>(d: T): void { public register<T extends IDisposable>(d: T): void {
this._disposables.push(d); 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);
}
}
} }
-97
View File
@@ -1,97 +0,0 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { fillFallback, concat } from './TypedArrayUtils';
type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray
| Int8Array | Int16Array | Int32Array
| Float32Array | Float64Array;
function deepEquals(a: TypedArray, b: TypedArray): void {
assert.equal(a.length, b.length);
for (let i = 0; i < a.length; ++i) {
assert.equal(a[i], b[i]);
}
}
describe('polyfill conformance tests', function(): void {
describe('TypedArray.fill', function(): void {
it('should work with all typed array types', function(): void {
const u81 = new Uint8Array(5);
const u82 = new Uint8Array(5);
deepEquals(fillFallback(u81, 2), u82.fill(2));
deepEquals(fillFallback(u81, -1), u82.fill(-1));
const u161 = new Uint16Array(5);
const u162 = new Uint16Array(5);
deepEquals(fillFallback(u161, 2), u162.fill(2));
deepEquals(fillFallback(u161, 65535), u162.fill(65535));
deepEquals(fillFallback(u161, -1), u162.fill(-1));
const u321 = new Uint32Array(5);
const u322 = new Uint32Array(5);
deepEquals(fillFallback(u321, 2), u322.fill(2));
deepEquals(fillFallback(u321, 65537), u322.fill(65537));
deepEquals(fillFallback(u321, -1), u322.fill(-1));
const i81 = new Int8Array(5);
const i82 = new Int8Array(5);
deepEquals(fillFallback(i81, 2), i82.fill(2));
deepEquals(fillFallback(i81, -1), i82.fill(-1));
const i161 = new Int16Array(5);
const i162 = new Int16Array(5);
deepEquals(fillFallback(i161, 2), i162.fill(2));
deepEquals(fillFallback(i161, 65535), i162.fill(65535));
deepEquals(fillFallback(i161, -1), i162.fill(-1));
const i321 = new Int32Array(5);
const i322 = new Int32Array(5);
deepEquals(fillFallback(i321, 2), i322.fill(2));
deepEquals(fillFallback(i321, 65537), i322.fill(65537));
deepEquals(fillFallback(i321, -1), i322.fill(-1));
const f321 = new Float32Array(5);
const f322 = new Float32Array(5);
deepEquals(fillFallback(f321, 1.2345), f322.fill(1.2345));
const f641 = new Float64Array(5);
const f642 = new Float64Array(5);
deepEquals(fillFallback(f641, 1.2345), f642.fill(1.2345));
const u8Clamped1 = new Uint8ClampedArray(5);
const u8Clamped2 = new Uint8ClampedArray(5);
deepEquals(fillFallback(u8Clamped1, 2), u8Clamped2.fill(2));
deepEquals(fillFallback(u8Clamped1, 257), u8Clamped2.fill(257));
});
it('start offset', function(): void {
for (let i = -2; i < 10; ++i) {
const u81 = new Uint8Array(5);
const u83 = new Uint8Array(5);
deepEquals(fillFallback(u81, 2, i), u83.fill(2, i));
deepEquals(fillFallback(u81, -1, i), u83.fill(-1, i));
}
});
it('end offset', function(): void {
for (let i = -2; i < 10; ++i) {
const u81 = new Uint8Array(5);
const u83 = new Uint8Array(5);
deepEquals(fillFallback(u81, 2, 0, i), u83.fill(2, 0, i));
deepEquals(fillFallback(u81, -1, 0, i), u83.fill(-1, 0, i));
}
});
it('start/end offset', function(): void {
for (let i = -2; i < 10; ++i) {
for (let j = -2; j < 10; ++j) {
const u81 = new Uint8Array(5);
const u83 = new Uint8Array(5);
deepEquals(fillFallback(u81, 2, i, j), u83.fill(2, i, j));
deepEquals(fillFallback(u81, -1, i, j), u83.fill(-1, i, j));
}
}
});
});
});
describe('typed array convenience functions', () => {
it('concat', () => {
const a = new Uint8Array([1, 2, 3, 4, 5]);
const b = new Uint8Array([6, 7, 8, 9, 0]);
const merged = concat(a, b);
deepEquals(merged, new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 0]));
});
});
-52
View File
@@ -1,52 +0,0 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
export type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray
| Int8Array | Int16Array | Int32Array
| Float32Array | Float64Array;
/**
* polyfill for TypedArray.fill
* This is needed to support .fill in all safari versions and IE 11.
*/
export function fill<T extends TypedArray>(array: T, value: number, start?: number, end?: number): T {
// all modern engines that support .fill
if (array.fill) {
return array.fill(value, start, end) as T;
}
return fillFallback(array, value, start, end);
}
export function fillFallback<T extends TypedArray>(array: T, value: number, start: number = 0, end: number = array.length): T {
// safari and IE 11
// since IE 11 does not support Array.prototype.fill either
// we cannot use the suggested polyfill from MDN
// instead we simply fall back to looping
if (start >= array.length) {
return array;
}
start = (array.length + start) % array.length;
if (end >= array.length) {
end = array.length;
} else {
end = (array.length + end) % array.length;
}
for (let i = start; i < end; ++i) {
array[i] = value;
}
return array;
}
/**
* Concat two typed arrays `a` and `b`.
* Returns a new typed array.
*/
export function concat<T extends TypedArray>(a: T, b: T): T {
const result = new (a.constructor as any)(a.length + b.length);
result.set(a);
result.set(b, a.length);
return result;
}
-19
View File
@@ -3,10 +3,6 @@
* @license MIT * @license MIT
*/ */
import { IEventEmitter } from 'xterm';
export type XtermListener = (...args: any[]) => void;
/** /**
* A keyboard event interface which does not depend on the DOM, KeyboardEvent implicitly extends * A keyboard event interface which does not depend on the DOM, KeyboardEvent implicitly extends
* this event. * this event.
@@ -20,18 +16,3 @@ export interface IKeyboardEvent {
key: string; key: string;
type: string; type: string;
} }
export interface ICircularList<T> extends IEventEmitter {
length: number;
maxLength: number;
isFull: boolean;
get(index: number): T | undefined;
set(index: number, value: T): void;
push(value: T): void;
recycle(): T | undefined;
pop(): T | undefined;
splice(start: number, deleteCount: number, ...items: T[]): void;
trimStart(count: number): void;
shiftElements(start: number, count: number, offset: number): void;
}
-19
View File
@@ -1,19 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"es5"
],
"rootDir": ".",
"noEmit": true,
"strict": true,
"pretty": true,
"types": [
"../../node_modules/@types/mocha",
"../../"
]
},
"include": [
"./**/*"
]
}
+2 -2
View File
@@ -10,12 +10,12 @@ import { ICharset } from '../Types';
* to be represented within the terminal with only 8-bit encoding. See ISO 2022 * to be represented within the terminal with only 8-bit encoding. See ISO 2022
* for a discussion on character sets. Only VT100 character sets are supported. * for a discussion on character sets. Only VT100 character sets are supported.
*/ */
export const CHARSETS: { [key: string]: ICharset | null } = {}; export const CHARSETS: { [key: string]: ICharset } = {};
/** /**
* The default character set, US. * The default character set, US.
*/ */
export const DEFAULT_CHARSET: ICharset | null = CHARSETS['B']; export const DEFAULT_CHARSET: ICharset = CHARSETS['B'];
/** /**
* DEC Special Character and Line Drawing Set. * DEC Special Character and Line Drawing Set.
+2 -14
View File
@@ -2,7 +2,6 @@
import { assert } from 'chai'; import { assert } from 'chai';
import { evaluateKeyboardEvent } from './Keyboard'; import { evaluateKeyboardEvent } from './Keyboard';
import { IKeyboardResult } from '../Types'; import { IKeyboardResult } from '../Types';
import { IKeyboardEvent } from '../../common/Types';
/** /**
* A helper function for testing which allows passing in a partial event and defaults will be filled * A helper function for testing which allows passing in a partial event and defaults will be filled
@@ -21,12 +20,12 @@ function testEvaluateKeyboardEvent(partialEvent: {
isMac?: boolean; isMac?: boolean;
macOptionIsMeta?: boolean; macOptionIsMeta?: boolean;
} = {}): IKeyboardResult { } = {}): IKeyboardResult {
const event: IKeyboardEvent = { const event = {
altKey: partialEvent.altKey || false, altKey: partialEvent.altKey || false,
ctrlKey: partialEvent.ctrlKey || false, ctrlKey: partialEvent.ctrlKey || false,
shiftKey: partialEvent.shiftKey || false, shiftKey: partialEvent.shiftKey || false,
metaKey: partialEvent.metaKey || false, metaKey: partialEvent.metaKey || false,
keyCode: partialEvent.keyCode !== undefined ? partialEvent.keyCode : 0, keyCode: partialEvent.keyCode !== undefined ? partialEvent.keyCode : undefined,
key: partialEvent.key || '', key: partialEvent.key || '',
type: partialEvent.type || '' type: partialEvent.type || ''
}; };
@@ -281,16 +280,5 @@ describe('Keyboard', () => {
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputDownArrow' }).key, '\x1b[B'); assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputDownArrow' }).key, '\x1b[B');
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputDownArrow' }, { applicationCursorMode: true }).key, '\x1bOB'); assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputDownArrow' }, { applicationCursorMode: true }).key, '\x1bOB');
}); });
it('should handle lowercase letters', () => {
assert.equal(testEvaluateKeyboardEvent({ keyCode: 65, key: 'a' }).key, 'a');
assert.equal(testEvaluateKeyboardEvent({ keyCode: 189, key: '-' }).key, '-');
});
it('should handle uppercase letters', () => {
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 65, key: 'A' }).key, 'A');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 49, key: '!' }).key, '!');
});
}); });
}); });
+1 -4
View File
@@ -44,7 +44,7 @@ export function evaluateKeyboardEvent(
): IKeyboardResult { ): IKeyboardResult {
const result: IKeyboardResult = { const result: IKeyboardResult = {
type: KeyboardResultType.SEND_KEY, type: KeyboardResultType.SEND_KEY,
// Whether to cancel event propagation (NOTE: this may not be needed since the event is // Whether to cancel event propogation (NOTE: this may not be needed since the event is
// canceled at the end of keyDown // canceled at the end of keyDown
cancel: false, cancel: false,
// The new key even to emit // The new key even to emit
@@ -349,9 +349,6 @@ export function evaluateKeyboardEvent(
if (ev.keyCode === 65) { // cmd + a if (ev.keyCode === 65) { // cmd + a
result.type = KeyboardResultType.SELECT_ALL; result.type = KeyboardResultType.SELECT_ALL;
} }
} else if (ev.key && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.keyCode >= 48 && ev.key.length === 1) {
// Include only keys that that result in a _single_ character; don't include num lock, volume up, etc.
result.key = ev.key;
} }
break; break;
} }
-67
View File
@@ -1,67 +0,0 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { StringToUtf32, stringFromCodePoint, utf32ToString } from './TextDecoder';
describe('text encodings', () => {
it('stringFromCodePoint/utf32ToString', () => {
const s = 'abcdefg';
const data = new Uint32Array(s.length);
for (let i = 0; i < s.length; ++i) {
data[i] = s.charCodeAt(i);
assert.equal(stringFromCodePoint(data[i]), s[i]);
}
assert.equal(utf32ToString(data), s);
});
describe('StringToUtf32 Decoder', () => {
describe('full codepoint test', () => {
it('0..65535', () => {
const decoder = new StringToUtf32();
const target = new Uint32Array(5);
for (let i = 0; i < 65536; ++i) {
// skip surrogate pairs
if (i >= 0xD800 && i <= 0xDFFF) {
continue;
}
const length = decoder.decode(String.fromCharCode(i), target);
assert.equal(length, 1);
assert.equal(target[0], i);
assert.equal(utf32ToString(target, 0, length), String.fromCharCode(i));
decoder.clear();
}
});
it('65536..0x10FFFF (surrogates)', function(): void {
this.timeout(20000);
const decoder = new StringToUtf32();
const target = new Uint32Array(5);
for (let i = 65536; i < 0x10FFFF; ++i) {
const codePoint = i - 0x10000;
const s = String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00);
const length = decoder.decode(s, target);
assert.equal(length, 1);
assert.equal(target[0], i);
assert.equal(utf32ToString(target, 0, length), s);
decoder.clear();
}
});
});
describe('stream handling', () => {
it('surrogates mixed advance by 1', () => {
const decoder = new StringToUtf32();
const target = new Uint32Array(5);
const input = 'Ä€𝄞Ö𝄞€Ü𝄞€';
let decoded = '';
for (let i = 0; i < input.length; ++i) {
const written = decoder.decode(input[i], target);
decoded += utf32ToString(target, written);
}
assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€');
});
});
});
});
-109
View File
@@ -1,109 +0,0 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
/**
* StringToUtf32 - decodes UTF16 sequences into UTF32 codepoints.
* To keep the decoder in line with JS strings it handles single surrogates as UCS2.
*/
export class StringToUtf32 {
private _interim: number = 0;
/**
* Clears interim and resets decoder to clean state.
*/
public clear(): void {
this._interim = 0;
}
/**
* Decode JS string to UTF32 codepoints.
* The methods assumes stream input and will store partly transmitted
* surrogate pairs and decode them with the next data chunk.
* Note: The method does no bound checks for target, therefore make sure
* the provided input data does not exceed the size of `target`.
* Returns the number of written codepoints in `target`.
*/
decode(input: string, target: Uint32Array): number {
const length = input.length;
if (!length) {
return 0;
}
let size = 0;
let startPos = 0;
// handle leftover surrogate high
if (this._interim) {
const second = input.charCodeAt(startPos++);
if (0xDC00 <= second && second <= 0xDFFF) {
target[size++] = (this._interim - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;
} else {
// illegal codepoint (USC2 handling)
target[size++] = this._interim;
target[size++] = second;
}
this._interim = 0;
}
for (let i = startPos; i < length; ++i) {
const code = input.charCodeAt(i);
// surrogate pair first
if (0xD800 <= code && code <= 0xDBFF) {
if (++i >= length) {
this._interim = code;
return size;
}
const second = input.charCodeAt(i);
if (0xDC00 <= second && second <= 0xDFFF) {
target[size++] = (code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;
} else {
// illegal codepoint (USC2 handling)
target[size++] = code;
target[size++] = second;
}
continue;
}
target[size++] = code;
}
return size;
}
}
/**
* Convert UTF32 codepoint into JS string.
*/
export function stringFromCodePoint(codePoint: number): string {
if (codePoint > 0xFFFF) {
// UTF32 to UTF16 conversion (see comments in utf32ToString)
codePoint -= 0x10000;
return String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00);
}
return String.fromCharCode(codePoint);
}
/**
* Convert UTF32 char codes into JS string.
* Basically the same as `stringFromCodePoint` but for multiple codepoints
* in a loop (which is a lot faster).
*/
export function utf32ToString(data: Uint32Array, start: number = 0, end: number = data.length): string {
let result = '';
for (let i = start; i < end; ++i) {
let codepoint = data[i];
if (codepoint > 0xFFFF) {
// JS string are encoded as UTF16, thus a non BMP codepoint gets converted into a surrogate pair
// conversion rules:
// - subtract 0x10000 from code point, leaving a 20 bit number
// - add high 10 bits to 0xD800 --> first surrogate
// - add low 10 bits to 0xDC00 --> second surrogate
codepoint -= 0x10000;
result += String.fromCharCode((codepoint >> 10) + 0xD800) + String.fromCharCode((codepoint % 0x400) + 0xDC00);
} else {
result += String.fromCharCode(codepoint);
}
}
return result;
}
-20
View File
@@ -1,20 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"es5"
],
"rootDir": ".",
"noEmit": true,
"strict": true,
"pretty": true,
"types": [
"../../node_modules/@types/mocha",
"../../"
]
},
"include": [
"./**/*",
"../common/**/*"
]
}
+7 -7
View File
@@ -3,8 +3,7 @@
* @license MIT * @license MIT
*/ */
import { ITerminal, IBufferLine } from '../Types'; import { ITerminal, ICircularList, LineData } from '../Types';
import { ICircularList } from '../common/Types';
import { C0 } from '../common/data/EscapeSequences'; import { C0 } from '../common/data/EscapeSequences';
const enum Direction { const enum Direction {
@@ -19,7 +18,7 @@ export class AltClickHandler {
private _startCol: number; private _startCol: number;
private _endRow: number; private _endRow: number;
private _endCol: number; private _endCol: number;
private _lines: ICircularList<IBufferLine>; private _lines: ICircularList<LineData>;
constructor( constructor(
private _mouseEvent: MouseEvent, private _mouseEvent: MouseEvent,
@@ -33,6 +32,7 @@ export class AltClickHandler {
this._mouseEvent, this._mouseEvent,
this._terminal.element, this._terminal.element,
this._terminal.charMeasure, this._terminal.charMeasure,
this._terminal.options.lineHeight,
this._terminal.cols, this._terminal.cols,
this._terminal.rows, this._terminal.rows,
false false
@@ -50,7 +50,7 @@ export class AltClickHandler {
*/ */
public move(): void { public move(): void {
if (this._mouseEvent.altKey && this._endCol !== undefined && this._endRow !== undefined) { if (this._mouseEvent.altKey && this._endCol !== undefined && this._endRow !== undefined) {
this._terminal.handler(this._arrowSequences()); this._terminal.send(this._arrowSequences());
} }
} }
@@ -138,7 +138,7 @@ export class AltClickHandler {
for (let i = 0; i < Math.abs(startRow - endRow); i++) { for (let i = 0; i < Math.abs(startRow - endRow); i++) {
const direction = this._verticalDirection() === Direction.UP ? -1 : 1; const direction = this._verticalDirection() === Direction.UP ? -1 : 1;
if (this._lines.get(startRow + (direction * i)).isWrapped) { if ((<any>this._lines.get(startRow + (direction * i))).isWrapped) {
wrappedRows++; wrappedRows++;
} }
} }
@@ -152,12 +152,12 @@ export class AltClickHandler {
*/ */
private _wrappedRowsForRow(currentRow: number): number { private _wrappedRowsForRow(currentRow: number): number {
let rowCount = 0; 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) { while (lineWraps && currentRow >= 0 && currentRow < this._terminal.rows) {
rowCount++; rowCount++;
currentRow--; currentRow--;
lineWraps = this._lines.get(currentRow).isWrapped; lineWraps = (<any>this._lines.get(currentRow)).isWrapped;
} }
return rowCount; return rowCount;
@@ -35,7 +35,7 @@ export function bracketTextForPaste(text: string, bracketedPasteMode: boolean):
/** /**
* Binds copy functionality to the given terminal. * 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 { export function copyHandler(ev: ClipboardEvent, term: ITerminal, selectionManager: ISelectionManager): void {
if (term.browser.isMSIE) { 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. * Redirect the clipboard's data to the terminal's input handler.
* @param ev The original paste event to be handled * @param {ClipboardEvent} ev The original paste event to be handled
* @param term The terminal on which to apply the handled paste event * @param {Terminal} term The terminal on which to apply the handled paste event
*/ */
export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void { export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void {
ev.stopPropagation(); ev.stopPropagation();
@@ -85,32 +85,26 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void {
* @param ev The original right click event to be handled. * @param ev The original right click event to be handled.
* @param textarea The terminal's textarea. * @param textarea The terminal's textarea.
*/ */
export function moveTextAreaUnderMouseCursor(ev: MouseEvent, term: ITerminal): void { export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextAreaElement): void {
// Calculate textarea position relative to the screen element
const pos = term.screenElement.getBoundingClientRect();
const left = ev.clientX - pos.left - 10;
const top = ev.clientY - pos.top - 10;
// Bring textarea at the cursor position // Bring textarea at the cursor position
term.textarea.style.position = 'absolute'; textarea.style.position = 'fixed';
term.textarea.style.width = '20px'; textarea.style.width = '20px';
term.textarea.style.height = '20px'; textarea.style.height = '20px';
term.textarea.style.left = `${left}px`; textarea.style.left = (ev.clientX - 10) + 'px';
term.textarea.style.top = `${top}px`; textarea.style.top = (ev.clientY - 10) + 'px';
term.textarea.style.zIndex = '1000'; textarea.style.zIndex = '1000';
term.textarea.focus(); textarea.focus();
// Reset the terminal textarea's styling // Reset the terminal textarea's styling
// Timeout needs to be long enough for click event to be handled. // Timeout needs to be long enough for click event to be handled.
setTimeout(() => { setTimeout(() => {
term.textarea.style.position = null; textarea.style.position = null;
term.textarea.style.width = null; textarea.style.width = null;
term.textarea.style.height = null; textarea.style.height = null;
term.textarea.style.left = null; textarea.style.left = null;
term.textarea.style.top = null; textarea.style.top = null;
term.textarea.style.zIndex = null; textarea.style.zIndex = null;
}, 200); }, 200);
} }
@@ -121,14 +115,14 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, term: ITerminal): v
* @param selectionManager The terminal's selection manager. * @param selectionManager The terminal's selection manager.
* @param shouldSelectWord If true and there is no selection the current word will be selected * @param shouldSelectWord If true and there is no selection the current word will be selected
*/ */
export function rightClickHandler(ev: MouseEvent, term: ITerminal, selectionManager: ISelectionManager, shouldSelectWord: boolean): void { export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager, shouldSelectWord: boolean): void {
moveTextAreaUnderMouseCursor(ev, term); moveTextAreaUnderMouseCursor(ev, textarea);
if (shouldSelectWord && !selectionManager.isClickInSelection(ev)) { if (shouldSelectWord && !selectionManager.isClickInSelection(ev)) {
selectionManager.selectWordAtCursor(ev); selectionManager.selectWordAtCursor(ev);
} }
// Get textarea ready to copy from the context menu // Get textarea ready to copy from the context menu
term.textarea.value = selectionManager.selectionText; textarea.value = selectionManager.selectionText;
term.textarea.select(); textarea.select();
} }
-12
View File
@@ -59,24 +59,12 @@ export class Terminal implements ITerminalApi {
public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void { public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
this._core.attachCustomKeyEventHandler(customKeyEventHandler); this._core.attachCustomKeyEventHandler(customKeyEventHandler);
} }
public addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable {
return this._core.addCsiHandler(flag, callback);
}
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
return this._core.addOscHandler(ident, callback);
}
public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number { public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number {
return this._core.registerLinkMatcher(regex, handler, options); return this._core.registerLinkMatcher(regex, handler, options);
} }
public deregisterLinkMatcher(matcherId: number): void { public deregisterLinkMatcher(matcherId: number): void {
this._core.deregisterLinkMatcher(matcherId); 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 { public addMarker(cursorYOffset: number): IMarker {
return this._core.addMarker(cursorYOffset); return this._core.addMarker(cursorYOffset);
} }
+20 -49
View File
@@ -5,11 +5,10 @@
import { IRenderLayer, IColorSet, IRenderDimensions } from './Types'; import { IRenderLayer, IColorSet, IRenderDimensions } from './Types';
import { CharData, ITerminal } from '../Types'; import { CharData, ITerminal } from '../Types';
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, IGlyphIdentifier } from './atlas/Types'; import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from './atlas/Types';
import BaseCharAtlas from './atlas/BaseCharAtlas'; import BaseCharAtlas from './atlas/BaseCharAtlas';
import { acquireCharAtlas } from './atlas/CharAtlasCache'; import { acquireCharAtlas } from './atlas/CharAtlasCache';
import { CHAR_DATA_CHAR_INDEX } from '../Buffer'; import { CHAR_DATA_CHAR_INDEX } from '../Buffer';
import { is256Color } from './atlas/CharAtlasUtils';
export abstract class BaseRenderLayer implements IRenderLayer { export abstract class BaseRenderLayer implements IRenderLayer {
private _canvas: HTMLCanvasElement; private _canvas: HTMLCanvasElement;
@@ -23,19 +22,6 @@ export abstract class BaseRenderLayer implements IRenderLayer {
protected _charAtlas: BaseCharAtlas; protected _charAtlas: BaseCharAtlas;
/**
* An object that's reused when drawing glyphs in order to reduce GC.
*/
private _currentGlyphIdentifier: IGlyphIdentifier = {
chars: '',
code: 0,
bg: 0,
fg: 0,
bold: false,
dim: false,
italic: false
};
constructor( constructor(
private _container: HTMLElement, private _container: HTMLElement,
id: string, id: string,
@@ -50,13 +36,6 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this._container.appendChild(this._canvas); this._container.appendChild(this._canvas);
} }
public dispose(): void {
this._container.removeChild(this._canvas);
if (this._charAtlas) {
this._charAtlas.dispose();
}
}
private _initCanvas(): void { private _initCanvas(): void {
this._ctx = this._canvas.getContext('2d', {alpha: this._alpha}); this._ctx = this._canvas.getContext('2d', {alpha: this._alpha});
// Draw the background if this is an opaque layer // Draw the background if this is an opaque layer
@@ -236,21 +215,21 @@ export abstract class BaseRenderLayer implements IRenderLayer {
*/ */
protected fillCharTrueColor(terminal: ITerminal, charData: CharData, x: number, y: number): void { protected fillCharTrueColor(terminal: ITerminal, charData: CharData, x: number, y: number): void {
this._ctx.font = this._getFont(terminal, false, false); this._ctx.font = this._getFont(terminal, false, false);
this._ctx.textBaseline = 'middle'; this._ctx.textBaseline = 'top';
this._clipRow(terminal, y); this._clipRow(terminal, y);
this._ctx.fillText( this._ctx.fillText(
charData[CHAR_DATA_CHAR_INDEX], charData[CHAR_DATA_CHAR_INDEX],
x * this._scaledCellWidth + this._scaledCharLeft, x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2); y * this._scaledCellHeight + this._scaledCharTop);
} }
/** /**
* Draws one or more characters at a cell. If possible this will draw using * Draws a character at a cell. If possible this will draw using the character
* the character atlas to reduce draw time. * atlas to reduce draw time.
* @param terminal The terminal. * @param terminal The terminal.
* @param chars The character or characters. * @param char The character.
* @param code The character code. * @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 x The column to draw at.
* @param y The row to draw at. * @param y The row to draw at.
* @param fg The foreground color, in the format stored within the attributes. * @param fg The foreground color, in the format stored within the attributes.
@@ -258,48 +237,40 @@ export abstract class BaseRenderLayer implements IRenderLayer {
* This is used to validate whether a cached image can be used. * This is used to validate whether a cached image can be used.
* @param bold Whether the text is bold. * @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 { 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 !== INVERTED_DEFAULT_COLOR; const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && bold && fg < 8;
fg += drawInBrightColor ? 8 : 0; fg += drawInBrightColor ? 8 : 0;
this._currentGlyphIdentifier.chars = chars;
this._currentGlyphIdentifier.code = code;
this._currentGlyphIdentifier.bg = bg;
this._currentGlyphIdentifier.fg = fg;
this._currentGlyphIdentifier.bold = bold && terminal.options.enableBold;
this._currentGlyphIdentifier.dim = dim;
this._currentGlyphIdentifier.italic = italic;
const atlasDidDraw = this._charAtlas && this._charAtlas.draw( const atlasDidDraw = this._charAtlas && this._charAtlas.draw(
this._ctx, this._ctx,
this._currentGlyphIdentifier, {char, code, bg, fg, bold: bold && terminal.options.enableBold, dim, italic},
x * this._scaledCellWidth + this._scaledCharLeft, x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop y * this._scaledCellHeight + this._scaledCharTop
); );
if (!atlasDidDraw) { 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 * Draws a character at a cell. The character will be clipped to
* clipped to ensure that they fit with the cell(s), including the cell to the * ensure that it fits with the cell, including the cell to the right if it's
* right if the last character is a wide character. * a wide character.
* @param terminal The terminal. * @param terminal The terminal.
* @param chars The character. * @param char The character.
* @param width The width of the character. * @param width The width of the character.
* @param fg The foreground color, in the format stored within the attributes. * @param fg The foreground color, in the format stored within the attributes.
* @param x The column to draw at. * @param x The column to draw at.
* @param y The row 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.save();
this._ctx.font = this._getFont(terminal, bold, italic); this._ctx.font = this._getFont(terminal, bold, italic);
this._ctx.textBaseline = 'middle'; this._ctx.textBaseline = 'top';
if (fg === INVERTED_DEFAULT_COLOR) { if (fg === INVERTED_DEFAULT_COLOR) {
this._ctx.fillStyle = this._colors.background.css; this._ctx.fillStyle = this._colors.background.css;
} else if (is256Color(fg)) { } else if (fg < 256) {
// 256 color support // 256 color support
this._ctx.fillStyle = this._colors.ansi[fg].css; this._ctx.fillStyle = this._colors.ansi[fg].css;
} else { } else {
@@ -314,9 +285,9 @@ export abstract class BaseRenderLayer implements IRenderLayer {
} }
// Draw the character // Draw the character
this._ctx.fillText( this._ctx.fillText(
chars, char,
x * this._scaledCellWidth + this._scaledCharLeft, x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2); y * this._scaledCellHeight + this._scaledCharTop);
this._ctx.restore(); this._ctx.restore();
} }
@@ -1,296 +0,0 @@
import { assert } from 'chai';
import { MockTerminal, MockBuffer } from '../ui/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(0));
lines.set(5, lineData([['a', 0x11111111], [' -> b -> c -> '], ['d', 0x22222222]]));
const line6 = lineData([['wi']]);
line6.resize(line6.length + 1, [0, '¥', 2, '¥'.charCodeAt(0)]);
line6.resize(line6.length + 1, [0, '', 0, null]);
let sub = lineData([['deemo']]);
let oldSize = line6.length;
line6.resize(oldSize + sub.length, [0, '', 0, 0]);
for (let i = 0; i < sub.length; ++i) line6.set(i + oldSize, sub.get(i));
line6.resize(line6.length + 1, [0, '\xf0\x9f\x98\x81', 1, 128513]);
line6.resize(line6.length + 1, [0, ' ', 1, ' '.charCodeAt(0)]);
sub = lineData([['jiabc']]);
oldSize = line6.length;
line6.resize(oldSize + sub.length, [0, '', 0, 0]);
for (let i = 0; i < sub.length; ++i) line6.set(i + oldSize, 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(0);
for (let i = 0; i < data.length; ++i) {
const line = data[i][0];
const attr = <number>(data[i][1] || 0);
const offset = tline.length;
tline.resize(tline.length + line.split('').length, [0, '', 0, 0]);
line.split('').map((char, idx) => tline.set(idx + offset, [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;
}
}
+2 -1
View File
@@ -3,7 +3,8 @@
* @license MIT * @license MIT
*/ */
import { IColorManager, IColor, IColorSet } from './Types'; import { IColorManager } from './Types';
import { IColor, IColorSet } from '../shared/Types';
import { ITheme } from 'xterm'; import { ITheme } from 'xterm';
const DEFAULT_FOREGROUND = fromHex('#ffffff'); const DEFAULT_FOREGROUND = fromHex('#ffffff');
+1 -1
View File
@@ -127,7 +127,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
return; return;
} }
const charData = terminal.buffer.lines.get(cursorY).get(terminal.buffer.x); const charData = terminal.buffer.lines.get(cursorY)[terminal.buffer.x];
if (!charData) { if (!charData) {
return; return;
} }
+1 -11
View File
@@ -6,8 +6,6 @@
import { ILinkHoverEvent, ITerminal, ILinkifierAccessor, LinkHoverEventTypes } from '../Types'; import { ILinkHoverEvent, ITerminal, ILinkifierAccessor, LinkHoverEventTypes } from '../Types';
import { IColorSet, IRenderDimensions } from './Types'; import { IColorSet, IRenderDimensions } from './Types';
import { BaseRenderLayer } from './BaseRenderLayer'; import { BaseRenderLayer } from './BaseRenderLayer';
import { INVERTED_DEFAULT_COLOR } from './atlas/Types';
import { is256Color } from './atlas/CharAtlasUtils';
export class LinkRenderLayer extends BaseRenderLayer { export class LinkRenderLayer extends BaseRenderLayer {
private _state: ILinkHoverEvent = null; private _state: ILinkHoverEvent = null;
@@ -41,15 +39,7 @@ export class LinkRenderLayer extends BaseRenderLayer {
} }
private _onLinkHover(e: ILinkHoverEvent): void { private _onLinkHover(e: ILinkHoverEvent): void {
if (e.fg === INVERTED_DEFAULT_COLOR) { this._ctx.fillStyle = this._colors.foreground.css;
this._ctx.fillStyle = this._colors.background.css;
} else if (is256Color(e.fg)) {
// 256 color support
this._ctx.fillStyle = this._colors.ansi[e.fg].css;
} else {
this._ctx.fillStyle = this._colors.foreground.css;
}
if (e.y1 === e.y2) { if (e.y1 === e.y2) {
// Single line link // Single line link
this.fillBottomLineAtCells(e.x1, e.y1, e.x2 - e.x1); this.fillBottomLineAtCells(e.x1, e.y1, e.x2 - e.x1);

Some files were not shown because too many files have changed in this diff Show More