Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
062b45ce0b | ||
|
|
f3bfcc368c | ||
|
|
d306a96918 | ||
|
|
24eb0130f5 |
@@ -1,8 +1,8 @@
|
||||
---
|
||||
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 🛑
|
||||
|
||||
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.
|
||||
|
||||
@@ -12,6 +12,7 @@ npm-debug.log
|
||||
/.idea/
|
||||
.env
|
||||
build/
|
||||
.vscode/
|
||||
.DS_Store
|
||||
fixtures/typings-test/*.js
|
||||
package-lock.json
|
||||
|
||||
Vendored
-44
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
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>
|
||||
Jeff Principe <princjef@gmail.com>
|
||||
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>
|
||||
Jose Anton <roke22@gmail.com>
|
||||
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>
|
||||
PowerHat <b.raymond@protonmail.com>
|
||||
pro-src <34285059+pro-src@users.noreply.github.com>
|
||||
pro-src <rodneyd.teal@gmail.com>
|
||||
Rick Baker <rick@ricktbaker.com>
|
||||
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>
|
||||
Segev Finer <segev208@gmail.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>
|
||||
+6
-6
@@ -33,21 +33,21 @@ opening an issue, read these pointers.
|
||||
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:
|
||||
|
||||
- Fork [xterm.js](https://github.com/sourcelair/xterm.js/)
|
||||
([how to fork a repo](https://help.github.com/articles/fork-a-repo)).
|
||||
- Get the [xterm.js demo](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo) running.
|
||||
- Make your changes.
|
||||
([how to fork a repo](https://help.github.com/articles/fork-a-repo))
|
||||
- Get the [xterm.js demo](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo) running
|
||||
- Make your changes
|
||||
- If your changes are easy to test or likely to regress, add tests. Tests go into `test`, directory.
|
||||
- Follow the general code style of the rest of the project (see below).
|
||||
- Submit a pull request
|
||||
([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.
|
||||
|
||||
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).
|
||||
|
||||
- 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
|
||||
holder has explicitly granted the right to use it like this,
|
||||
through a compatible open source license or through a direct
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2017-2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)
|
||||
Copyright (c) 2017-2018, The xterm.js authors (https://github.com/xtermjs/xterm.js)
|
||||
Copyright (c) 2014-2016, SourceLair Private Company (https://www.sourcelair.com)
|
||||
Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/)
|
||||
|
||||
|
||||
@@ -2,22 +2,25 @@
|
||||
|
||||
[](https://dev.azure.com/xtermjs/xterm.js/_build/latest?definitionId=3)
|
||||
[](https://coveralls.io/github/xtermjs/xterm.js?branch=master)
|
||||
[](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
[](https://www.jsdelivr.com/package/npm/xterm)
|
||||
|
||||
Xterm.js is a 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
|
||||
|
||||
- **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.
|
||||
- **Perfomant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer.
|
||||
- **Rich unicode support**: Supports CJK, emojis and IMEs.
|
||||
- **Self-contained**: Requires zero dependencies to work.
|
||||
- **Accessible**: Screen reader support can be turned on using the `screenReaderMode` option.
|
||||
- **And much more**: Links, theming, addons, well documented API, etc.
|
||||
- **Text-based application support**: Use xterm.js to work with applications like `bash`, `git` etc.
|
||||
- **Curses-based application support**: Use xterm.js to work with applications like `vim`, `tmux` etc.
|
||||
- **Mouse events support**: Xterm.js captures mouse events like click and scroll and passes them to the terminal's back-end controlling process
|
||||
- **CJK (Chinese, Japanese, Korean) character support**: Xterm.js renders CJK characters seamlessly
|
||||
- **IME support**: Insert international (including CJK) characters using IME input with your keyboard
|
||||
- **Self-contained library**: Xterm.js works on its own. It does not require any external libraries like jQuery or React to work
|
||||
- **Modular, event-based API**: Lets you build addons and themes with ease
|
||||
|
||||
## 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 `bash`. Xterm.js can be connected to processes like `bash` and let you interact with them (provide input, receive output).
|
||||
- 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)
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -27,7 +30,7 @@ First you need to install the module, we ship exclusively through [npm](https://
|
||||
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
|
||||
<!doctype html>
|
||||
@@ -41,30 +44,39 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t
|
||||
<script>
|
||||
var term = new 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>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Finally instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`.
|
||||
|
||||
### 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
|
||||
import { Terminal } from 'xterm';
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
The full API for xterm.js is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API.
|
||||
|
||||
Note that some APIs are marked *experimental*, these are added so we can experiment with new ideas without committing to support it like a normal semver API. Note that these APIs can change radically between versions so be sure to read release notes if you plan on using experimental APIs.
|
||||
|
||||
### Addons
|
||||
|
||||
Addons are JavaScript modules that extend the `Terminal` prototype with new methods and attributes to provide additional functionality. There are a handful available in the main repository in the `src/addons` directory and you can even write your own by using 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:
|
||||
|
||||
```javascript
|
||||
import { Terminal } from 'xterm';
|
||||
import { Terminal } from xterm;
|
||||
import * as fit from 'xterm/lib/addons/fit/fit';
|
||||
|
||||
|
||||
Terminal.applyAddon(fit);
|
||||
|
||||
var xterm = new Terminal(); // Instantiate the terminal
|
||||
@@ -75,16 +87,25 @@ You will also need to include the addon's CSS file if it has one in the folder.
|
||||
|
||||
#### Importing Addons in TypeScript
|
||||
|
||||
There are currently no typings for addons if they are accessed via extending Terminal prototype, so you will need to upcast if using TypeScript, eg. `(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
|
||||
import { Terminal } from 'xterm';
|
||||
import { fit } from 'xterm/lib/addons/fit/fit';
|
||||
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
|
||||
|
||||
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 +116,26 @@ Since xterm.js is typically implemented as a developer tool, only modern browser
|
||||
- Safari latest
|
||||
- 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.
|
||||
The current full API documentation is available in the [TypeScript declaration file on the repository](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), switch the tag (press `w` when viewing the file) to point at the specific version tag you're using.
|
||||
|
||||
## Real-world uses
|
||||
Xterm.js is used in several world-class applications to provide great terminal experiences.
|
||||
|
||||
- [**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.
|
||||
- [**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.
|
||||
- [**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
|
||||
- [**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.
|
||||
- [**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.
|
||||
- [**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.
|
||||
- [**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.
|
||||
- [**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.
|
||||
- [**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.
|
||||
- [**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.
|
||||
@@ -124,8 +143,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.
|
||||
- [**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.
|
||||
- [**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.
|
||||
- [**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.
|
||||
- [**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.
|
||||
- [**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,23 +157,22 @@ 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.
|
||||
- [**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.
|
||||
- [**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.
|
||||
- [**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.
|
||||
- [**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.
|
||||
- [**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.
|
||||
- [**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.
|
||||
- [**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.
|
||||
- [**info-beamer hosted**](https://info-beamer.com): Uses Xterm.js to manage digital signage devices from the web dashboard.
|
||||
|
||||
[And much more...](https://github.com/xtermjs/xterm.js/network/dependents)
|
||||
|
||||
@@ -163,7 +182,7 @@ Do you use xterm.js in your application as well? Please [open a Pull Request](ht
|
||||
|
||||
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
|
||||
|
||||
|
||||
Executable
+7
@@ -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
|
||||
@@ -17,6 +17,9 @@ CURRENT_PACKAGE_JSON_VERSION=$(cat package.json \
|
||||
export BUILD_DIR=dist
|
||||
npm run build
|
||||
|
||||
# Update AUTHORS file
|
||||
sh bin/generate-authors
|
||||
|
||||
# Update version in package.json
|
||||
sed -i "s/\"version\": \"$CURRENT_PACKAGE_JSON_VERSION\"/\"version\": \"$NEW_VERSION\"/g" package.json
|
||||
|
||||
|
||||
+23
-20
@@ -14,7 +14,6 @@ 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
|
||||
@@ -51,14 +50,6 @@ function setPadding(): void {
|
||||
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();
|
||||
|
||||
const disposeRecreateButtonHandler = () => {
|
||||
@@ -106,15 +97,26 @@ function createTerminal(): void {
|
||||
|
||||
addDomListener(paddingElement, 'change', setPadding);
|
||||
|
||||
addDomListener(actionElements.findNext, 'keyup', (e) => {
|
||||
const searchOptions = getSearchOptions();
|
||||
searchOptions.incremental = e.key !== `Enter`;
|
||||
term.findNext(actionElements.findNext.value, searchOptions);
|
||||
addDomListener(actionElements.findNext, 'keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const searchOptions = {
|
||||
regex: (document.getElementById('regex') as HTMLInputElement).checked,
|
||||
wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked,
|
||||
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked
|
||||
};
|
||||
term.findNext(actionElements.findNext.value, searchOptions);
|
||||
}
|
||||
});
|
||||
|
||||
addDomListener(actionElements.findPrevious, 'keyup', (e) => {
|
||||
if (e.key === `Enter`) {
|
||||
term.findPrevious(actionElements.findPrevious.value, getSearchOptions());
|
||||
addDomListener(actionElements.findPrevious, 'keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const searchOptions = {
|
||||
regex: (document.getElementById('regex') as HTMLInputElement).checked,
|
||||
wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked,
|
||||
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked
|
||||
};
|
||||
term.findPrevious(actionElements.findPrevious.value, searchOptions);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -205,7 +207,8 @@ function initOptions(term: TerminalType): void {
|
||||
fontFamily: null,
|
||||
fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
|
||||
fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
|
||||
rendererType: ['dom', 'canvas']
|
||||
rendererType: ['dom', 'canvas'],
|
||||
experimentalBufferLineImpl: ['JsArray', 'TypedArray']
|
||||
};
|
||||
const options = Object.keys((<any>term)._core.options);
|
||||
const booleanOptions = [];
|
||||
@@ -232,7 +235,7 @@ function initOptions(term: TerminalType): void {
|
||||
});
|
||||
html += '</div><div class="option-group">';
|
||||
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">';
|
||||
Object.keys(stringOptions).forEach(o => {
|
||||
@@ -262,7 +265,7 @@ function initOptions(term: TerminalType): void {
|
||||
if (o === 'cols' || o === 'rows') {
|
||||
updateTerminalSize();
|
||||
} else {
|
||||
term.setOption(o, o === 'lineHeight' ? parseFloat(input.value) : parseInt(input.value, 10));
|
||||
term.setOption(o, parseInt(input.value, 10));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+84
-90
@@ -1,106 +1,100 @@
|
||||
var express = require('express');
|
||||
var expressWs = require('express-ws');
|
||||
var app = express();
|
||||
var expressWs = require('express-ws')(app);
|
||||
var os = require('os');
|
||||
var pty = require('node-pty');
|
||||
|
||||
function startServer() {
|
||||
var app = express();
|
||||
expressWs(app);
|
||||
var terminals = {},
|
||||
logs = {};
|
||||
|
||||
var terminals = {},
|
||||
logs = {};
|
||||
app.use('/build', express.static(__dirname + '/../build'));
|
||||
|
||||
app.use('/build', express.static(__dirname + '/../build'));
|
||||
app.get('/', function(req, res){
|
||||
res.sendFile(__dirname + '/index.html');
|
||||
});
|
||||
|
||||
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.get('/style.css', function(req, res){
|
||||
res.sendFile(__dirname + '/style.css');
|
||||
});
|
||||
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];
|
||||
|
||||
app.get('/dist/client-bundle.js', function(req, res){
|
||||
res.sendFile(__dirname + '/dist/client-bundle.js');
|
||||
});
|
||||
term.resize(cols, rows);
|
||||
console.log('Resized terminal ' + pid + ' to ' + cols + ' cols and ' + rows + ' rows.');
|
||||
res.end();
|
||||
});
|
||||
|
||||
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
|
||||
});
|
||||
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]);
|
||||
|
||||
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
|
||||
function buffer(socket, timeout) {
|
||||
let s = '';
|
||||
let sender = null;
|
||||
return (data) => {
|
||||
s += data;
|
||||
if (!sender) {
|
||||
sender = setTimeout(() => {
|
||||
socket.send(s);
|
||||
s = '';
|
||||
sender = null;
|
||||
}, timeout);
|
||||
}
|
||||
});
|
||||
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];
|
||||
});
|
||||
};
|
||||
}
|
||||
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';
|
||||
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;
|
||||
console.log('App listening to http://' + host + ':' + port);
|
||||
app.listen(port, host);
|
||||
|
||||
+3
-7
@@ -5,11 +5,12 @@
|
||||
* This file is the entry point for browserify.
|
||||
*/
|
||||
|
||||
const cp = require('child_process');
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const startServer = require('./server.js');
|
||||
|
||||
startServer();
|
||||
// Launch server
|
||||
cp.spawn('node', [path.resolve(__dirname, 'server.js')], { stdio: 'inherit' });
|
||||
|
||||
// Build/watch client source
|
||||
const clientConfig = {
|
||||
@@ -21,11 +22,6 @@ const clientConfig = {
|
||||
test: /\.tsx?$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: ["source-map-loader"],
|
||||
enforce: "pre"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ services:
|
||||
volumes:
|
||||
- ./:/usr/src/app
|
||||
ports:
|
||||
- ${XTERMJS_PORT:-3000}:3000
|
||||
- ${XTERMJS_PORT:3000}:3000
|
||||
command: ["npm", "start"]
|
||||
|
||||
watch:
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
/// <reference path="../../typings/xterm.d.ts" />
|
||||
|
||||
import { Terminal, IDisposable } from 'xterm';
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
namespace constructor {
|
||||
{
|
||||
@@ -119,12 +119,6 @@ namespace methods_core {
|
||||
const t: Terminal = new Terminal();
|
||||
t.attachCustomKeyEventHandler((e: KeyboardEvent) => true);
|
||||
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 {
|
||||
{
|
||||
|
||||
+4
-3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "xterm",
|
||||
"description": "Full xterm terminal, in your browser",
|
||||
"version": "3.12.0",
|
||||
"version": "3.9.2",
|
||||
"main": "lib/public/Terminal.js",
|
||||
"types": "typings/xterm.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
@@ -28,13 +28,14 @@
|
||||
"gulp-sourcemaps": "1.9.1",
|
||||
"gulp-typescript": "^3.1.3",
|
||||
"gulp-util": "3.0.8",
|
||||
"jsdoc": "3.4.3",
|
||||
"jsdom": "^11.11.0",
|
||||
"merge-stream": "^1.0.1",
|
||||
"node-pty": "0.7.6",
|
||||
"nodemon": "1.10.2",
|
||||
"nyc": "^11.8.0",
|
||||
"sorcery": "^0.10.0",
|
||||
"source-map-loader": "^0.2.4",
|
||||
"source-map-loader": "^0.2.3",
|
||||
"ts-loader": "^4.5.0",
|
||||
"tslint": "^5.9.1",
|
||||
"tslint-consistent-codestyle": "^1.13.0",
|
||||
@@ -47,7 +48,6 @@
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node demo/start",
|
||||
"start-debug": "node --inspect-brk demo/start",
|
||||
"start-zmodem": "node demo/zmodem/app",
|
||||
"lint": "tslint 'src/**/*.ts' './demo/**/*.ts'",
|
||||
"pretest": "npm run layering",
|
||||
@@ -57,6 +57,7 @@
|
||||
"test-suite": "gulp mocha-suite --test",
|
||||
"test-coverage": "nyc -x gulpfile.js -x '**/*test*' npm run mocha",
|
||||
"mocha": "gulp test",
|
||||
"build:docs": "jsdoc -c jsdoc.json",
|
||||
"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\"",
|
||||
"build": "gulp build",
|
||||
|
||||
+11
-813
@@ -7,7 +7,7 @@ import { assert, expect } from 'chai';
|
||||
import { ITerminal } from './Types';
|
||||
import { Buffer, DEFAULT_ATTR, CHAR_DATA_CHAR_INDEX } from './Buffer';
|
||||
import { CircularList } from './common/CircularList';
|
||||
import { MockTerminal, TestTerminal } from './ui/TestUtils.test';
|
||||
import { MockTerminal, TestTerminal } from './utils/TestUtils.test';
|
||||
import { BufferLine } from './BufferLine';
|
||||
|
||||
const INIT_COLS = 80;
|
||||
@@ -19,8 +19,8 @@ describe('Buffer', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
terminal = new MockTerminal();
|
||||
(terminal as any).cols = INIT_COLS;
|
||||
(terminal as any).rows = INIT_ROWS;
|
||||
terminal.cols = INIT_COLS;
|
||||
terminal.rows = INIT_ROWS;
|
||||
terminal.options.scrollback = 1000;
|
||||
buffer = new Buffer(terminal, true);
|
||||
});
|
||||
@@ -108,12 +108,12 @@ describe('Buffer', () => {
|
||||
|
||||
describe('resize', () => {
|
||||
describe('column size is reduced', () => {
|
||||
it('should trim the data in the buffer', () => {
|
||||
it('should not trim the data in the buffer', () => {
|
||||
buffer.fillViewportRows();
|
||||
buffer.resize(INIT_COLS / 2, INIT_ROWS);
|
||||
assert.equal(buffer.lines.length, INIT_ROWS);
|
||||
for (let i = 0; i < INIT_ROWS; i++) {
|
||||
assert.equal(buffer.lines.get(i).length, INIT_COLS / 2);
|
||||
assert.equal(buffer.lines.get(i).length, INIT_COLS);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -233,773 +233,6 @@ describe('Buffer', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('reflow', () => {
|
||||
it('should not wrap empty lines', () => {
|
||||
buffer.fillViewportRows();
|
||||
assert.equal(buffer.lines.length, INIT_ROWS);
|
||||
buffer.resize(INIT_COLS - 5, INIT_ROWS);
|
||||
assert.equal(buffer.lines.length, INIT_ROWS);
|
||||
});
|
||||
it('should shrink row length', () => {
|
||||
buffer.fillViewportRows();
|
||||
buffer.resize(5, 10);
|
||||
assert.equal(buffer.lines.length, 10);
|
||||
assert.equal(buffer.lines.get(0).length, 5);
|
||||
assert.equal(buffer.lines.get(1).length, 5);
|
||||
assert.equal(buffer.lines.get(2).length, 5);
|
||||
assert.equal(buffer.lines.get(3).length, 5);
|
||||
assert.equal(buffer.lines.get(4).length, 5);
|
||||
assert.equal(buffer.lines.get(5).length, 5);
|
||||
assert.equal(buffer.lines.get(6).length, 5);
|
||||
assert.equal(buffer.lines.get(7).length, 5);
|
||||
assert.equal(buffer.lines.get(8).length, 5);
|
||||
assert.equal(buffer.lines.get(9).length, 5);
|
||||
});
|
||||
it('should wrap and unwrap lines', () => {
|
||||
buffer.fillViewportRows();
|
||||
buffer.resize(5, 10);
|
||||
const firstLine = buffer.lines.get(0);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const code = 'a'.charCodeAt(0) + i;
|
||||
const char = String.fromCharCode(code);
|
||||
firstLine.set(i, [null, char, 1, code]);
|
||||
}
|
||||
buffer.y = 1;
|
||||
assert.equal(buffer.lines.get(0).length, 5);
|
||||
assert.equal(buffer.lines.get(0).translateToString(), 'abcde');
|
||||
buffer.resize(1, 10);
|
||||
assert.equal(buffer.lines.length, 10);
|
||||
assert.equal(buffer.lines.get(0).translateToString(), 'a');
|
||||
assert.equal(buffer.lines.get(1).translateToString(), 'b');
|
||||
assert.equal(buffer.lines.get(2).translateToString(), 'c');
|
||||
assert.equal(buffer.lines.get(3).translateToString(), 'd');
|
||||
assert.equal(buffer.lines.get(4).translateToString(), 'e');
|
||||
assert.equal(buffer.lines.get(5).translateToString(), ' ');
|
||||
assert.equal(buffer.lines.get(6).translateToString(), ' ');
|
||||
assert.equal(buffer.lines.get(7).translateToString(), ' ');
|
||||
assert.equal(buffer.lines.get(8).translateToString(), ' ');
|
||||
assert.equal(buffer.lines.get(9).translateToString(), ' ');
|
||||
buffer.resize(5, 10);
|
||||
assert.equal(buffer.lines.length, 10);
|
||||
assert.equal(buffer.lines.get(0).translateToString(), 'abcde');
|
||||
assert.equal(buffer.lines.get(1).translateToString(), ' ');
|
||||
assert.equal(buffer.lines.get(2).translateToString(), ' ');
|
||||
assert.equal(buffer.lines.get(3).translateToString(), ' ');
|
||||
assert.equal(buffer.lines.get(4).translateToString(), ' ');
|
||||
assert.equal(buffer.lines.get(5).translateToString(), ' ');
|
||||
assert.equal(buffer.lines.get(6).translateToString(), ' ');
|
||||
assert.equal(buffer.lines.get(7).translateToString(), ' ');
|
||||
assert.equal(buffer.lines.get(8).translateToString(), ' ');
|
||||
assert.equal(buffer.lines.get(9).translateToString(), ' ');
|
||||
});
|
||||
it('should discard parts of wrapped lines that go out of the scrollback', () => {
|
||||
buffer.fillViewportRows();
|
||||
terminal.options.scrollback = 1;
|
||||
buffer.resize(10, 5);
|
||||
const lastLine = buffer.lines.get(3);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const code = 'a'.charCodeAt(0) + i;
|
||||
const char = String.fromCharCode(code);
|
||||
lastLine.set(i, [null, char, 1, code]);
|
||||
}
|
||||
assert.equal(buffer.lines.length, 5);
|
||||
buffer.y = 4;
|
||||
buffer.resize(2, 5);
|
||||
assert.equal(buffer.y, 4);
|
||||
assert.equal(buffer.ybase, 1);
|
||||
assert.equal(buffer.lines.length, 6);
|
||||
assert.equal(buffer.lines.get(0).translateToString(), 'ab');
|
||||
assert.equal(buffer.lines.get(1).translateToString(), 'cd');
|
||||
assert.equal(buffer.lines.get(2).translateToString(), 'ef');
|
||||
assert.equal(buffer.lines.get(3).translateToString(), 'gh');
|
||||
assert.equal(buffer.lines.get(4).translateToString(), 'ij');
|
||||
assert.equal(buffer.lines.get(5).translateToString(), ' ');
|
||||
buffer.resize(1, 5);
|
||||
assert.equal(buffer.y, 4);
|
||||
assert.equal(buffer.ybase, 1);
|
||||
assert.equal(buffer.lines.length, 6);
|
||||
assert.equal(buffer.lines.get(0).translateToString(), 'f');
|
||||
assert.equal(buffer.lines.get(1).translateToString(), 'g');
|
||||
assert.equal(buffer.lines.get(2).translateToString(), 'h');
|
||||
assert.equal(buffer.lines.get(3).translateToString(), 'i');
|
||||
assert.equal(buffer.lines.get(4).translateToString(), 'j');
|
||||
assert.equal(buffer.lines.get(5).translateToString(), ' ');
|
||||
buffer.resize(10, 5);
|
||||
assert.equal(buffer.y, 1);
|
||||
assert.equal(buffer.ybase, 0);
|
||||
assert.equal(buffer.lines.length, 5);
|
||||
assert.equal(buffer.lines.get(0).translateToString(), 'fghij ');
|
||||
assert.equal(buffer.lines.get(1).translateToString(), ' ');
|
||||
assert.equal(buffer.lines.get(2).translateToString(), ' ');
|
||||
assert.equal(buffer.lines.get(3).translateToString(), ' ');
|
||||
assert.equal(buffer.lines.get(4).translateToString(), ' ');
|
||||
});
|
||||
it('should remove the correct amount of rows when reflowing larger', () => {
|
||||
// This is a regression test to ensure that successive wrapped lines that are getting
|
||||
// 3+ lines removed on a reflow actually remove the right lines
|
||||
buffer.fillViewportRows();
|
||||
buffer.resize(10, 10);
|
||||
buffer.y = 2;
|
||||
const firstLine = buffer.lines.get(0);
|
||||
const secondLine = buffer.lines.get(1);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const code = 'a'.charCodeAt(0) + i;
|
||||
const char = String.fromCharCode(code);
|
||||
firstLine.set(i, [null, char, 1, code]);
|
||||
}
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const code = '0'.charCodeAt(0) + i;
|
||||
const char = String.fromCharCode(code);
|
||||
secondLine.set(i, [null, char, 1, code]);
|
||||
}
|
||||
assert.equal(buffer.lines.length, 10);
|
||||
assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij');
|
||||
assert.equal(buffer.lines.get(1).translateToString(), '0123456789');
|
||||
for (let i = 2; i < 10; i++) {
|
||||
assert.equal(buffer.lines.get(i).translateToString(), ' ');
|
||||
}
|
||||
buffer.resize(2, 10);
|
||||
assert.equal(buffer.ybase, 1);
|
||||
assert.equal(buffer.lines.length, 11);
|
||||
assert.equal(buffer.lines.get(0).translateToString(), 'ab');
|
||||
assert.equal(buffer.lines.get(1).translateToString(), 'cd');
|
||||
assert.equal(buffer.lines.get(2).translateToString(), 'ef');
|
||||
assert.equal(buffer.lines.get(3).translateToString(), 'gh');
|
||||
assert.equal(buffer.lines.get(4).translateToString(), 'ij');
|
||||
assert.equal(buffer.lines.get(5).translateToString(), '01');
|
||||
assert.equal(buffer.lines.get(6).translateToString(), '23');
|
||||
assert.equal(buffer.lines.get(7).translateToString(), '45');
|
||||
assert.equal(buffer.lines.get(8).translateToString(), '67');
|
||||
assert.equal(buffer.lines.get(9).translateToString(), '89');
|
||||
assert.equal(buffer.lines.get(10).translateToString(), ' ');
|
||||
buffer.resize(10, 10);
|
||||
assert.equal(buffer.ybase, 0);
|
||||
assert.equal(buffer.lines.length, 10);
|
||||
assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij');
|
||||
assert.equal(buffer.lines.get(1).translateToString(), '0123456789');
|
||||
for (let i = 2; i < 10; i++) {
|
||||
assert.equal(buffer.lines.get(i).translateToString(), ' ');
|
||||
}
|
||||
});
|
||||
it('should transfer combined char data over to reflowed lines', () => {
|
||||
buffer.fillViewportRows();
|
||||
buffer.resize(4, 3);
|
||||
buffer.y = 2;
|
||||
const firstLine = buffer.lines.get(0);
|
||||
firstLine.set(0, [ null, 'a', 1, 'a'.charCodeAt(0) ]);
|
||||
firstLine.set(1, [ null, 'b', 1, 'b'.charCodeAt(0) ]);
|
||||
firstLine.set(2, [ null, 'c', 1, 'c'.charCodeAt(0) ]);
|
||||
firstLine.set(3, [ null, '😁', 1, '😁'.charCodeAt(0) ]);
|
||||
assert.equal(buffer.lines.length, 3);
|
||||
assert.equal(buffer.lines.get(0).translateToString(), 'abc😁');
|
||||
assert.equal(buffer.lines.get(1).translateToString(), ' ');
|
||||
buffer.resize(2, 3);
|
||||
assert.equal(buffer.lines.get(0).translateToString(), 'ab');
|
||||
assert.equal(buffer.lines.get(1).translateToString(), 'c😁');
|
||||
});
|
||||
it('should adjust markers when reflowing', () => {
|
||||
buffer.fillViewportRows();
|
||||
buffer.resize(10, 16);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const code = 'a'.charCodeAt(0) + i;
|
||||
const char = String.fromCharCode(code);
|
||||
buffer.lines.get(0).set(i, [null, char, 1, code]);
|
||||
}
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const code = '0'.charCodeAt(0) + i;
|
||||
const char = String.fromCharCode(code);
|
||||
buffer.lines.get(1).set(i, [null, char, 1, code]);
|
||||
}
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const code = 'k'.charCodeAt(0) + i;
|
||||
const char = String.fromCharCode(code);
|
||||
buffer.lines.get(2).set(i, [null, char, 1, code]);
|
||||
}
|
||||
buffer.y = 3;
|
||||
// Buffer:
|
||||
// abcdefghij
|
||||
// 0123456789
|
||||
// abcdefghij
|
||||
const firstMarker = buffer.addMarker(0);
|
||||
const secondMarker = buffer.addMarker(1);
|
||||
const thirdMarker = buffer.addMarker(2);
|
||||
assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij');
|
||||
assert.equal(buffer.lines.get(1).translateToString(), '0123456789');
|
||||
assert.equal(buffer.lines.get(2).translateToString(), 'klmnopqrst');
|
||||
assert.equal(firstMarker.line, 0);
|
||||
assert.equal(secondMarker.line, 1);
|
||||
assert.equal(thirdMarker.line, 2);
|
||||
buffer.resize(2, 16);
|
||||
assert.equal(buffer.lines.get(0).translateToString(), 'ab');
|
||||
assert.equal(buffer.lines.get(1).translateToString(), 'cd');
|
||||
assert.equal(buffer.lines.get(2).translateToString(), 'ef');
|
||||
assert.equal(buffer.lines.get(3).translateToString(), 'gh');
|
||||
assert.equal(buffer.lines.get(4).translateToString(), 'ij');
|
||||
assert.equal(buffer.lines.get(5).translateToString(), '01');
|
||||
assert.equal(buffer.lines.get(6).translateToString(), '23');
|
||||
assert.equal(buffer.lines.get(7).translateToString(), '45');
|
||||
assert.equal(buffer.lines.get(8).translateToString(), '67');
|
||||
assert.equal(buffer.lines.get(9).translateToString(), '89');
|
||||
assert.equal(buffer.lines.get(10).translateToString(), 'kl');
|
||||
assert.equal(buffer.lines.get(11).translateToString(), 'mn');
|
||||
assert.equal(buffer.lines.get(12).translateToString(), 'op');
|
||||
assert.equal(buffer.lines.get(13).translateToString(), 'qr');
|
||||
assert.equal(buffer.lines.get(14).translateToString(), 'st');
|
||||
assert.equal(firstMarker.line, 0, 'first marker should remain unchanged');
|
||||
assert.equal(secondMarker.line, 5, 'second marker should be shifted since the first line wrapped');
|
||||
assert.equal(thirdMarker.line, 10, 'third marker should be shifted since the first and second lines wrapped');
|
||||
buffer.resize(10, 16);
|
||||
assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij');
|
||||
assert.equal(buffer.lines.get(1).translateToString(), '0123456789');
|
||||
assert.equal(buffer.lines.get(2).translateToString(), 'klmnopqrst');
|
||||
assert.equal(firstMarker.line, 0, 'first marker should remain unchanged');
|
||||
assert.equal(secondMarker.line, 1, 'second marker should be restored to it\'s original line');
|
||||
assert.equal(thirdMarker.line, 2, 'third marker should be restored to it\'s original line');
|
||||
assert.equal(firstMarker.isDisposed, false);
|
||||
assert.equal(secondMarker.isDisposed, false);
|
||||
assert.equal(thirdMarker.isDisposed, false);
|
||||
});
|
||||
it('should dispose markers whose rows are trimmed during a reflow', () => {
|
||||
buffer.fillViewportRows();
|
||||
terminal.options.scrollback = 1;
|
||||
buffer.resize(10, 11);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const code = 'a'.charCodeAt(0) + i;
|
||||
const char = String.fromCharCode(code);
|
||||
buffer.lines.get(0).set(i, [null, char, 1, code]);
|
||||
}
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const code = '0'.charCodeAt(0) + i;
|
||||
const char = String.fromCharCode(code);
|
||||
buffer.lines.get(1).set(i, [null, char, 1, code]);
|
||||
}
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const code = 'k'.charCodeAt(0) + i;
|
||||
const char = String.fromCharCode(code);
|
||||
buffer.lines.get(2).set(i, [null, char, 1, code]);
|
||||
}
|
||||
buffer.y = 10;
|
||||
// Buffer:
|
||||
// abcdefghij
|
||||
// 0123456789
|
||||
// abcdefghij
|
||||
const firstMarker = buffer.addMarker(0);
|
||||
const secondMarker = buffer.addMarker(1);
|
||||
const thirdMarker = buffer.addMarker(2);
|
||||
buffer.y = 3;
|
||||
assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij');
|
||||
assert.equal(buffer.lines.get(1).translateToString(), '0123456789');
|
||||
assert.equal(buffer.lines.get(2).translateToString(), 'klmnopqrst');
|
||||
assert.equal(firstMarker.line, 0);
|
||||
assert.equal(secondMarker.line, 1);
|
||||
assert.equal(thirdMarker.line, 2);
|
||||
buffer.resize(2, 11);
|
||||
assert.equal(buffer.lines.get(0).translateToString(), 'ij');
|
||||
assert.equal(buffer.lines.get(1).translateToString(), '01');
|
||||
assert.equal(buffer.lines.get(2).translateToString(), '23');
|
||||
assert.equal(buffer.lines.get(3).translateToString(), '45');
|
||||
assert.equal(buffer.lines.get(4).translateToString(), '67');
|
||||
assert.equal(buffer.lines.get(5).translateToString(), '89');
|
||||
assert.equal(buffer.lines.get(6).translateToString(), 'kl');
|
||||
assert.equal(buffer.lines.get(7).translateToString(), 'mn');
|
||||
assert.equal(buffer.lines.get(8).translateToString(), 'op');
|
||||
assert.equal(buffer.lines.get(9).translateToString(), 'qr');
|
||||
assert.equal(buffer.lines.get(10).translateToString(), 'st');
|
||||
assert.equal(secondMarker.line, 1, 'second marker should remain the same as it was shifted 4 and trimmed 4');
|
||||
assert.equal(thirdMarker.line, 6, 'third marker should be shifted since the first and second lines wrapped');
|
||||
assert.equal(firstMarker.isDisposed, true, 'first marker was trimmed');
|
||||
assert.equal(secondMarker.isDisposed, false);
|
||||
assert.equal(thirdMarker.isDisposed, false);
|
||||
buffer.resize(10, 11);
|
||||
assert.equal(buffer.lines.get(0).translateToString(), 'ij ');
|
||||
assert.equal(buffer.lines.get(1).translateToString(), '0123456789');
|
||||
assert.equal(buffer.lines.get(2).translateToString(), 'klmnopqrst');
|
||||
assert.equal(secondMarker.line, 1, 'second marker should be restored');
|
||||
assert.equal(thirdMarker.line, 2, 'third marker should be restored');
|
||||
});
|
||||
it('should wrap wide characters correctly when reflowing larger', () => {
|
||||
buffer.fillViewportRows();
|
||||
buffer.resize(12, 10);
|
||||
buffer.y = 2;
|
||||
for (let i = 0; i < 12; i += 4) {
|
||||
buffer.lines.get(0).set(i, [null, '汉', 2, '汉'.charCodeAt(0)]);
|
||||
buffer.lines.get(1).set(i, [null, '汉', 2, '汉'.charCodeAt(0)]);
|
||||
}
|
||||
for (let i = 2; i < 12; i += 4) {
|
||||
buffer.lines.get(0).set(i, [null, '语', 2, '语'.charCodeAt(0)]);
|
||||
buffer.lines.get(1).set(i, [null, '语', 2, '语'.charCodeAt(0)]);
|
||||
}
|
||||
for (let i = 1; i < 12; i += 2) {
|
||||
buffer.lines.get(0).set(i, [null, '', 0, undefined]);
|
||||
buffer.lines.get(1).set(i, [null, '', 0, undefined]);
|
||||
}
|
||||
buffer.lines.get(1).isWrapped = true;
|
||||
// Buffer:
|
||||
// 汉语汉语汉语 (wrapped)
|
||||
// 汉语汉语汉语
|
||||
assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉语');
|
||||
assert.equal(buffer.lines.get(1).translateToString(true), '汉语汉语汉语');
|
||||
buffer.resize(13, 10);
|
||||
assert.equal(buffer.ybase, 0);
|
||||
assert.equal(buffer.lines.length, 10);
|
||||
assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉语');
|
||||
assert.equal(buffer.lines.get(0).translateToString(false), '汉语汉语汉语 ');
|
||||
assert.equal(buffer.lines.get(1).translateToString(true), '汉语汉语汉语');
|
||||
assert.equal(buffer.lines.get(1).translateToString(false), '汉语汉语汉语 ');
|
||||
buffer.resize(14, 10);
|
||||
assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉语汉');
|
||||
assert.equal(buffer.lines.get(0).translateToString(false), '汉语汉语汉语汉');
|
||||
assert.equal(buffer.lines.get(1).translateToString(true), '语汉语汉语');
|
||||
assert.equal(buffer.lines.get(1).translateToString(false), '语汉语汉语 ');
|
||||
});
|
||||
it('should wrap wide characters correctly when reflowing smaller', () => {
|
||||
buffer.fillViewportRows();
|
||||
buffer.resize(12, 10);
|
||||
buffer.y = 2;
|
||||
for (let i = 0; i < 12; i += 4) {
|
||||
buffer.lines.get(0).set(i, [null, '汉', 2, '汉'.charCodeAt(0)]);
|
||||
buffer.lines.get(1).set(i, [null, '汉', 2, '汉'.charCodeAt(0)]);
|
||||
}
|
||||
for (let i = 2; i < 12; i += 4) {
|
||||
buffer.lines.get(0).set(i, [null, '语', 2, '语'.charCodeAt(0)]);
|
||||
buffer.lines.get(1).set(i, [null, '语', 2, '语'.charCodeAt(0)]);
|
||||
}
|
||||
for (let i = 1; i < 12; i += 2) {
|
||||
buffer.lines.get(0).set(i, [null, '', 0, undefined]);
|
||||
buffer.lines.get(1).set(i, [null, '', 0, undefined]);
|
||||
}
|
||||
buffer.lines.get(1).isWrapped = true;
|
||||
// Buffer:
|
||||
// 汉语汉语汉语 (wrapped)
|
||||
// 汉语汉语汉语
|
||||
assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉语');
|
||||
assert.equal(buffer.lines.get(1).translateToString(true), '汉语汉语汉语');
|
||||
buffer.resize(11, 10);
|
||||
assert.equal(buffer.ybase, 0);
|
||||
assert.equal(buffer.lines.length, 10);
|
||||
assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉');
|
||||
assert.equal(buffer.lines.get(1).translateToString(true), '语汉语汉语');
|
||||
assert.equal(buffer.lines.get(2).translateToString(true), '汉语');
|
||||
buffer.resize(10, 10);
|
||||
assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉');
|
||||
assert.equal(buffer.lines.get(1).translateToString(true), '语汉语汉语');
|
||||
assert.equal(buffer.lines.get(2).translateToString(true), '汉语');
|
||||
buffer.resize(9, 10);
|
||||
assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语');
|
||||
assert.equal(buffer.lines.get(1).translateToString(true), '汉语汉语');
|
||||
assert.equal(buffer.lines.get(2).translateToString(true), '汉语汉语');
|
||||
buffer.resize(8, 10);
|
||||
assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语');
|
||||
assert.equal(buffer.lines.get(1).translateToString(true), '汉语汉语');
|
||||
assert.equal(buffer.lines.get(2).translateToString(true), '汉语汉语');
|
||||
buffer.resize(7, 10);
|
||||
assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉');
|
||||
assert.equal(buffer.lines.get(1).translateToString(true), '语汉语');
|
||||
assert.equal(buffer.lines.get(2).translateToString(true), '汉语汉');
|
||||
assert.equal(buffer.lines.get(3).translateToString(true), '语汉语');
|
||||
buffer.resize(6, 10);
|
||||
assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉');
|
||||
assert.equal(buffer.lines.get(1).translateToString(true), '语汉语');
|
||||
assert.equal(buffer.lines.get(2).translateToString(true), '汉语汉');
|
||||
assert.equal(buffer.lines.get(3).translateToString(true), '语汉语');
|
||||
});
|
||||
|
||||
describe('reflowLarger cases', () => {
|
||||
beforeEach(() => {
|
||||
// Setup buffer state:
|
||||
// 'ab'
|
||||
// 'cd' (wrapped)
|
||||
// 'ef'
|
||||
// 'gh' (wrapped)
|
||||
// 'ij'
|
||||
// 'kl' (wrapped)
|
||||
// ' '
|
||||
// ' '
|
||||
// ' '
|
||||
// ' '
|
||||
buffer.fillViewportRows();
|
||||
buffer.resize(2, 10);
|
||||
buffer.lines.get(0).set(0, [null, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
buffer.lines.get(0).set(1, [null, 'b', 1, 'b'.charCodeAt(0)]);
|
||||
buffer.lines.get(1).set(0, [null, 'c', 1, 'c'.charCodeAt(0)]);
|
||||
buffer.lines.get(1).set(1, [null, 'd', 1, 'd'.charCodeAt(0)]);
|
||||
buffer.lines.get(1).isWrapped = true;
|
||||
buffer.lines.get(2).set(0, [null, 'e', 1, 'e'.charCodeAt(0)]);
|
||||
buffer.lines.get(2).set(1, [null, 'f', 1, 'f'.charCodeAt(0)]);
|
||||
buffer.lines.get(3).set(0, [null, 'g', 1, 'g'.charCodeAt(0)]);
|
||||
buffer.lines.get(3).set(1, [null, 'h', 1, 'h'.charCodeAt(0)]);
|
||||
buffer.lines.get(3).isWrapped = true;
|
||||
buffer.lines.get(4).set(0, [null, 'i', 1, 'i'.charCodeAt(0)]);
|
||||
buffer.lines.get(4).set(1, [null, 'j', 1, 'j'.charCodeAt(0)]);
|
||||
buffer.lines.get(5).set(0, [null, 'k', 1, 'k'.charCodeAt(0)]);
|
||||
buffer.lines.get(5).set(1, [null, 'l', 1, 'l'.charCodeAt(0)]);
|
||||
buffer.lines.get(5).isWrapped = true;
|
||||
});
|
||||
describe('viewport not yet filled', () => {
|
||||
it('should move the cursor up and add empty lines', () => {
|
||||
buffer.y = 6;
|
||||
buffer.resize(4, 10);
|
||||
assert.equal(buffer.y, 3);
|
||||
assert.equal(buffer.ydisp, 0);
|
||||
assert.equal(buffer.ybase, 0);
|
||||
assert.equal(buffer.lines.length, 10);
|
||||
assert.equal(buffer.lines.get(0).translateToString(), 'abcd');
|
||||
assert.equal(buffer.lines.get(1).translateToString(), 'efgh');
|
||||
assert.equal(buffer.lines.get(2).translateToString(), 'ijkl');
|
||||
for (let i = 3; i < 10; i++) {
|
||||
assert.equal(buffer.lines.get(i).translateToString(), ' ');
|
||||
}
|
||||
const wrappedLines: number[] = [];
|
||||
for (let i = 0; i < buffer.lines.length; i++) {
|
||||
assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe('viewport filled, scrollback remaining', () => {
|
||||
beforeEach(() => {
|
||||
buffer.y = 9;
|
||||
});
|
||||
describe('ybase === 0', () => {
|
||||
it('should move the cursor up and add empty lines', () => {
|
||||
buffer.resize(4, 10);
|
||||
assert.equal(buffer.y, 6);
|
||||
assert.equal(buffer.ydisp, 0);
|
||||
assert.equal(buffer.ybase, 0);
|
||||
assert.equal(buffer.lines.length, 10);
|
||||
assert.equal(buffer.lines.get(0).translateToString(), 'abcd');
|
||||
assert.equal(buffer.lines.get(1).translateToString(), 'efgh');
|
||||
assert.equal(buffer.lines.get(2).translateToString(), 'ijkl');
|
||||
for (let i = 3; i < 10; i++) {
|
||||
assert.equal(buffer.lines.get(i).translateToString(), ' ');
|
||||
}
|
||||
const wrappedLines: number[] = [];
|
||||
for (let i = 0; i < buffer.lines.length; i++) {
|
||||
assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe('ybase !== 0', () => {
|
||||
beforeEach(() => {
|
||||
// Add 10 empty rows to start
|
||||
for (let i = 0; i < 10; i++) {
|
||||
buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR));
|
||||
}
|
||||
buffer.ybase = 10;
|
||||
});
|
||||
describe('&& ydisp === ybase', () => {
|
||||
it('should adjust the viewport and keep ydisp = ybase', () => {
|
||||
buffer.ydisp = 10;
|
||||
buffer.resize(4, 10);
|
||||
assert.equal(buffer.y, 9);
|
||||
assert.equal(buffer.ydisp, 7);
|
||||
assert.equal(buffer.ybase, 7);
|
||||
assert.equal(buffer.lines.length, 17);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
assert.equal(buffer.lines.get(i).translateToString(), ' ');
|
||||
}
|
||||
assert.equal(buffer.lines.get(10).translateToString(), 'abcd');
|
||||
assert.equal(buffer.lines.get(11).translateToString(), 'efgh');
|
||||
assert.equal(buffer.lines.get(12).translateToString(), 'ijkl');
|
||||
for (let i = 13; i < 17; i++) {
|
||||
assert.equal(buffer.lines.get(i).translateToString(), ' ');
|
||||
}
|
||||
const wrappedLines: number[] = [];
|
||||
for (let i = 0; i < buffer.lines.length; i++) {
|
||||
assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe('&& ydisp !== ybase', () => {
|
||||
it('should keep ydisp at the same value', () => {
|
||||
buffer.ydisp = 5;
|
||||
buffer.resize(4, 10);
|
||||
assert.equal(buffer.y, 9);
|
||||
assert.equal(buffer.ydisp, 5);
|
||||
assert.equal(buffer.ybase, 7);
|
||||
assert.equal(buffer.lines.length, 17);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
assert.equal(buffer.lines.get(i).translateToString(), ' ');
|
||||
}
|
||||
assert.equal(buffer.lines.get(10).translateToString(), 'abcd');
|
||||
assert.equal(buffer.lines.get(11).translateToString(), 'efgh');
|
||||
assert.equal(buffer.lines.get(12).translateToString(), 'ijkl');
|
||||
for (let i = 13; i < 17; i++) {
|
||||
assert.equal(buffer.lines.get(i).translateToString(), ' ');
|
||||
}
|
||||
const wrappedLines: number[] = [];
|
||||
for (let i = 0; i < buffer.lines.length; i++) {
|
||||
assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('viewport filled, no scrollback remaining', () => {
|
||||
// ybase === 0 doesn't make sense here as scrollback=0 isn't really supported
|
||||
describe('ybase !== 0', () => {
|
||||
beforeEach(() => {
|
||||
terminal.options.scrollback = 10;
|
||||
// Add 10 empty rows to start
|
||||
for (let i = 0; i < 10; i++) {
|
||||
buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR));
|
||||
}
|
||||
buffer.y = 9;
|
||||
buffer.ybase = 10;
|
||||
});
|
||||
describe('&& ydisp === ybase', () => {
|
||||
it('should trim lines and keep ydisp = ybase', () => {
|
||||
buffer.ydisp = 10;
|
||||
buffer.resize(4, 10);
|
||||
assert.equal(buffer.y, 9);
|
||||
assert.equal(buffer.ydisp, 7);
|
||||
assert.equal(buffer.ybase, 7);
|
||||
assert.equal(buffer.lines.length, 17);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
assert.equal(buffer.lines.get(i).translateToString(), ' ');
|
||||
}
|
||||
assert.equal(buffer.lines.get(10).translateToString(), 'abcd');
|
||||
assert.equal(buffer.lines.get(11).translateToString(), 'efgh');
|
||||
assert.equal(buffer.lines.get(12).translateToString(), 'ijkl');
|
||||
for (let i = 13; i < 17; i++) {
|
||||
assert.equal(buffer.lines.get(i).translateToString(), ' ');
|
||||
}
|
||||
const wrappedLines: number[] = [];
|
||||
for (let i = 0; i < buffer.lines.length; i++) {
|
||||
assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe('&& ydisp !== ybase', () => {
|
||||
it('should trim lines and not change ydisp', () => {
|
||||
buffer.ydisp = 5;
|
||||
buffer.resize(4, 10);
|
||||
assert.equal(buffer.y, 9);
|
||||
assert.equal(buffer.ydisp, 5);
|
||||
assert.equal(buffer.ybase, 7);
|
||||
assert.equal(buffer.lines.length, 17);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
assert.equal(buffer.lines.get(i).translateToString(), ' ');
|
||||
}
|
||||
assert.equal(buffer.lines.get(10).translateToString(), 'abcd');
|
||||
assert.equal(buffer.lines.get(11).translateToString(), 'efgh');
|
||||
assert.equal(buffer.lines.get(12).translateToString(), 'ijkl');
|
||||
for (let i = 13; i < 17; i++) {
|
||||
assert.equal(buffer.lines.get(i).translateToString(), ' ');
|
||||
}
|
||||
const wrappedLines: number[] = [];
|
||||
for (let i = 0; i < buffer.lines.length; i++) {
|
||||
assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('reflowSmaller cases', () => {
|
||||
beforeEach(() => {
|
||||
// Setup buffer state:
|
||||
// 'abcd'
|
||||
// 'efgh' (wrapped)
|
||||
// 'ijkl'
|
||||
// ' '
|
||||
// ' '
|
||||
// ' '
|
||||
// ' '
|
||||
// ' '
|
||||
// ' '
|
||||
// ' '
|
||||
buffer.fillViewportRows();
|
||||
buffer.resize(4, 10);
|
||||
buffer.lines.get(0).set(0, [null, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
buffer.lines.get(0).set(1, [null, 'b', 1, 'b'.charCodeAt(0)]);
|
||||
buffer.lines.get(0).set(2, [null, 'c', 1, 'c'.charCodeAt(0)]);
|
||||
buffer.lines.get(0).set(3, [null, 'd', 1, 'd'.charCodeAt(0)]);
|
||||
buffer.lines.get(1).set(0, [null, 'e', 1, 'e'.charCodeAt(0)]);
|
||||
buffer.lines.get(1).set(1, [null, 'f', 1, 'f'.charCodeAt(0)]);
|
||||
buffer.lines.get(1).set(2, [null, 'g', 1, 'g'.charCodeAt(0)]);
|
||||
buffer.lines.get(1).set(3, [null, 'h', 1, 'h'.charCodeAt(0)]);
|
||||
buffer.lines.get(2).set(0, [null, 'i', 1, 'i'.charCodeAt(0)]);
|
||||
buffer.lines.get(2).set(1, [null, 'j', 1, 'j'.charCodeAt(0)]);
|
||||
buffer.lines.get(2).set(2, [null, 'k', 1, 'k'.charCodeAt(0)]);
|
||||
buffer.lines.get(2).set(3, [null, 'l', 1, 'l'.charCodeAt(0)]);
|
||||
});
|
||||
describe('viewport not yet filled', () => {
|
||||
it('should move the cursor down', () => {
|
||||
buffer.y = 3;
|
||||
buffer.resize(2, 10);
|
||||
assert.equal(buffer.y, 6);
|
||||
assert.equal(buffer.ydisp, 0);
|
||||
assert.equal(buffer.ybase, 0);
|
||||
assert.equal(buffer.lines.length, 10);
|
||||
assert.equal(buffer.lines.get(0).translateToString(), 'ab');
|
||||
assert.equal(buffer.lines.get(1).translateToString(), 'cd');
|
||||
assert.equal(buffer.lines.get(2).translateToString(), 'ef');
|
||||
assert.equal(buffer.lines.get(3).translateToString(), 'gh');
|
||||
assert.equal(buffer.lines.get(4).translateToString(), 'ij');
|
||||
assert.equal(buffer.lines.get(5).translateToString(), 'kl');
|
||||
for (let i = 6; i < 10; i++) {
|
||||
assert.equal(buffer.lines.get(i).translateToString(), ' ');
|
||||
}
|
||||
const wrappedLines = [1, 3, 5];
|
||||
for (let i = 0; i < buffer.lines.length; i++) {
|
||||
assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe('viewport filled, scrollback remaining', () => {
|
||||
beforeEach(() => {
|
||||
buffer.y = 9;
|
||||
});
|
||||
describe('ybase === 0', () => {
|
||||
it('should trim the top', () => {
|
||||
buffer.resize(2, 10);
|
||||
assert.equal(buffer.y, 9);
|
||||
assert.equal(buffer.ydisp, 3);
|
||||
assert.equal(buffer.ybase, 3);
|
||||
assert.equal(buffer.lines.length, 13);
|
||||
assert.equal(buffer.lines.get(0).translateToString(), 'ab');
|
||||
assert.equal(buffer.lines.get(1).translateToString(), 'cd');
|
||||
assert.equal(buffer.lines.get(2).translateToString(), 'ef');
|
||||
assert.equal(buffer.lines.get(3).translateToString(), 'gh');
|
||||
assert.equal(buffer.lines.get(4).translateToString(), 'ij');
|
||||
assert.equal(buffer.lines.get(5).translateToString(), 'kl');
|
||||
for (let i = 6; i < 13; i++) {
|
||||
assert.equal(buffer.lines.get(i).translateToString(), ' ');
|
||||
}
|
||||
const wrappedLines = [1, 3, 5];
|
||||
for (let i = 0; i < buffer.lines.length; i++) {
|
||||
assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe('ybase !== 0', () => {
|
||||
beforeEach(() => {
|
||||
// Add 10 empty rows to start
|
||||
for (let i = 0; i < 10; i++) {
|
||||
buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR));
|
||||
}
|
||||
buffer.ybase = 10;
|
||||
});
|
||||
describe('&& ydisp === ybase', () => {
|
||||
it('should adjust the viewport and keep ydisp = ybase', () => {
|
||||
buffer.ydisp = 10;
|
||||
buffer.resize(2, 10);
|
||||
assert.equal(buffer.ydisp, 13);
|
||||
assert.equal(buffer.ybase, 13);
|
||||
assert.equal(buffer.lines.length, 23);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
assert.equal(buffer.lines.get(i).translateToString(), ' ');
|
||||
}
|
||||
assert.equal(buffer.lines.get(10).translateToString(), 'ab');
|
||||
assert.equal(buffer.lines.get(11).translateToString(), 'cd');
|
||||
assert.equal(buffer.lines.get(12).translateToString(), 'ef');
|
||||
assert.equal(buffer.lines.get(13).translateToString(), 'gh');
|
||||
assert.equal(buffer.lines.get(14).translateToString(), 'ij');
|
||||
assert.equal(buffer.lines.get(15).translateToString(), 'kl');
|
||||
for (let i = 16; i < 23; i++) {
|
||||
assert.equal(buffer.lines.get(i).translateToString(), ' ');
|
||||
}
|
||||
const wrappedLines = [11, 13, 15];
|
||||
for (let i = 0; i < buffer.lines.length; i++) {
|
||||
assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe('&& ydisp !== ybase', () => {
|
||||
it('should keep ydisp at the same value', () => {
|
||||
buffer.ydisp = 5;
|
||||
buffer.resize(2, 10);
|
||||
assert.equal(buffer.ydisp, 5);
|
||||
assert.equal(buffer.ybase, 13);
|
||||
assert.equal(buffer.lines.length, 23);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
assert.equal(buffer.lines.get(i).translateToString(), ' ');
|
||||
}
|
||||
assert.equal(buffer.lines.get(10).translateToString(), 'ab');
|
||||
assert.equal(buffer.lines.get(11).translateToString(), 'cd');
|
||||
assert.equal(buffer.lines.get(12).translateToString(), 'ef');
|
||||
assert.equal(buffer.lines.get(13).translateToString(), 'gh');
|
||||
assert.equal(buffer.lines.get(14).translateToString(), 'ij');
|
||||
assert.equal(buffer.lines.get(15).translateToString(), 'kl');
|
||||
for (let i = 16; i < 23; i++) {
|
||||
assert.equal(buffer.lines.get(i).translateToString(), ' ');
|
||||
}
|
||||
const wrappedLines = [11, 13, 15];
|
||||
for (let i = 0; i < buffer.lines.length; i++) {
|
||||
assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('viewport filled, no scrollback remaining', () => {
|
||||
// ybase === 0 doesn't make sense here as scrollback=0 isn't really supported
|
||||
describe('ybase !== 0', () => {
|
||||
beforeEach(() => {
|
||||
terminal.options.scrollback = 10;
|
||||
// Add 10 empty rows to start
|
||||
for (let i = 0; i < 10; i++) {
|
||||
buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR));
|
||||
}
|
||||
buffer.ybase = 10;
|
||||
});
|
||||
describe('&& ydisp === ybase', () => {
|
||||
it('should trim lines and keep ydisp = ybase', () => {
|
||||
buffer.ydisp = 10;
|
||||
buffer.y = 13;
|
||||
buffer.resize(2, 10);
|
||||
assert.equal(buffer.ydisp, 10);
|
||||
assert.equal(buffer.ybase, 10);
|
||||
assert.equal(buffer.lines.length, 20);
|
||||
for (let i = 0; i < 7; i++) {
|
||||
assert.equal(buffer.lines.get(i).translateToString(), ' ');
|
||||
}
|
||||
assert.equal(buffer.lines.get(7).translateToString(), 'ab');
|
||||
assert.equal(buffer.lines.get(8).translateToString(), 'cd');
|
||||
assert.equal(buffer.lines.get(9).translateToString(), 'ef');
|
||||
assert.equal(buffer.lines.get(10).translateToString(), 'gh');
|
||||
assert.equal(buffer.lines.get(11).translateToString(), 'ij');
|
||||
assert.equal(buffer.lines.get(12).translateToString(), 'kl');
|
||||
for (let i = 13; i < 20; i++) {
|
||||
assert.equal(buffer.lines.get(i).translateToString(), ' ');
|
||||
}
|
||||
const wrappedLines = [8, 10, 12];
|
||||
for (let i = 0; i < buffer.lines.length; i++) {
|
||||
assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe('&& ydisp !== ybase', () => {
|
||||
it('should trim lines and not change ydisp', () => {
|
||||
buffer.ydisp = 5;
|
||||
buffer.y = 13;
|
||||
buffer.resize(2, 10);
|
||||
assert.equal(buffer.ydisp, 5);
|
||||
assert.equal(buffer.ybase, 10);
|
||||
assert.equal(buffer.lines.length, 20);
|
||||
for (let i = 0; i < 7; i++) {
|
||||
assert.equal(buffer.lines.get(i).translateToString(), ' ');
|
||||
}
|
||||
assert.equal(buffer.lines.get(7).translateToString(), 'ab');
|
||||
assert.equal(buffer.lines.get(8).translateToString(), 'cd');
|
||||
assert.equal(buffer.lines.get(9).translateToString(), 'ef');
|
||||
assert.equal(buffer.lines.get(10).translateToString(), 'gh');
|
||||
assert.equal(buffer.lines.get(11).translateToString(), 'ij');
|
||||
assert.equal(buffer.lines.get(12).translateToString(), 'kl');
|
||||
for (let i = 13; i < 20; i++) {
|
||||
assert.equal(buffer.lines.get(i).translateToString(), ' ');
|
||||
}
|
||||
const wrappedLines = [8, 10, 12];
|
||||
for (let i = 0; i < buffer.lines.length; i++) {
|
||||
assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('buffer marked to have no scrollback', () => {
|
||||
@@ -1122,7 +355,7 @@ describe('Buffer', () => {
|
||||
let terminal: TestTerminal;
|
||||
|
||||
beforeEach(() => {
|
||||
terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5});
|
||||
terminal = new TestTerminal({rows: 5, cols: 10});
|
||||
});
|
||||
|
||||
it('multiline ascii', () => {
|
||||
@@ -1275,53 +508,18 @@ describe('Buffer', () => {
|
||||
// the dangling last cell is wrongly added in the string
|
||||
// --> fixable after resolving #1685
|
||||
terminal.writeSync(input);
|
||||
const s = terminal.buffer.iterator(true).next().content;
|
||||
assert.equal(input, s);
|
||||
// TODO: reenable after fix
|
||||
// const s = terminal.buffer.contents(true).toArray()[0];
|
||||
// assert.equal(input, s);
|
||||
for (let i = 10; i < input.length; ++i) {
|
||||
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true);
|
||||
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i + 1); // TODO: remove +1 after fix
|
||||
const j = (i - 0) << 1;
|
||||
assert.deepEqual([(j / terminal.cols) | 0, j % terminal.cols], bufferIndex);
|
||||
}
|
||||
});
|
||||
|
||||
it('test fully wrapped buffer up to last char', () => {
|
||||
const input = Array(6).join('1234567890');
|
||||
terminal.writeSync(input);
|
||||
const s = terminal.buffer.iterator(true).next().content;
|
||||
assert.equal(input, s);
|
||||
for (let i = 0; i < input.length; ++i) {
|
||||
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true);
|
||||
assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX]);
|
||||
}
|
||||
});
|
||||
|
||||
it('test fully wrapped buffer up to last char with full width odd', () => {
|
||||
const input = 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301'
|
||||
+ 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301';
|
||||
terminal.writeSync(input);
|
||||
const s = terminal.buffer.iterator(true).next().content;
|
||||
assert.equal(input, s);
|
||||
for (let i = 0; i < input.length; ++i) {
|
||||
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true);
|
||||
assert.equal(
|
||||
(!(i % 3))
|
||||
? input[i]
|
||||
: (i % 3 === 1)
|
||||
? input.substr(i, 2)
|
||||
: input.substr(i - 1, 2),
|
||||
terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX]);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle \t in lines correctly', () => {
|
||||
const input = '\thttps://google.de';
|
||||
terminal.writeSync(input);
|
||||
const s = terminal.buffer.iterator(true).next().content;
|
||||
assert.equal(s, Array(terminal.getOption('tabStopWidth') + 1).join(' ') + 'https://google.de');
|
||||
});
|
||||
});
|
||||
describe('BufferStringIterator', function(): void {
|
||||
it('iterator does not overflow buffer limits', function(): void {
|
||||
it('iterator does not ovrflow buffer limits', function(): void {
|
||||
const terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5});
|
||||
const data = [
|
||||
'aaaaaaaaaa',
|
||||
|
||||
+116
-291
@@ -3,13 +3,12 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IMarker } from 'xterm';
|
||||
import { BufferLine } from './BufferLine';
|
||||
import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from './BufferReflow';
|
||||
import { CircularList, IDeleteEvent, IInsertEvent } from './common/CircularList';
|
||||
import { CircularList } from './common/CircularList';
|
||||
import { CharData, ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult, IBufferLineConstructor } from './Types';
|
||||
import { EventEmitter } from './common/EventEmitter';
|
||||
import { IMarker } from 'xterm';
|
||||
import { BufferLine, BufferLineJSArray } from './BufferLine';
|
||||
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 CHAR_DATA_ATTR_INDEX = 0;
|
||||
@@ -18,15 +17,9 @@ export const CHAR_DATA_WIDTH_INDEX = 2;
|
||||
export const CHAR_DATA_CODE_INDEX = 3;
|
||||
export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1
|
||||
|
||||
export const NULL_CELL_CHAR = '';
|
||||
export const NULL_CELL_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];
|
||||
export const NULL_CELL_CODE = 32;
|
||||
|
||||
/**
|
||||
* This class represents a terminal buffer (an internal state of the terminal), where the
|
||||
@@ -48,8 +41,7 @@ export class Buffer implements IBuffer {
|
||||
public savedX: number;
|
||||
public savedCurAttr: number;
|
||||
public markers: Marker[] = [];
|
||||
private _cols: number;
|
||||
private _rows: number;
|
||||
private _bufferLineConstructor: IBufferLineConstructor;
|
||||
|
||||
/**
|
||||
* Create a new Buffer.
|
||||
@@ -61,24 +53,48 @@ export class Buffer implements IBuffer {
|
||||
private _terminal: ITerminal,
|
||||
private _hasScrollback: boolean
|
||||
) {
|
||||
this._cols = this._terminal.cols;
|
||||
this._rows = this._terminal.rows;
|
||||
this.clear();
|
||||
}
|
||||
|
||||
public setBufferLineFactory(type: string): void {
|
||||
if (type === 'JsArray') {
|
||||
if (this._bufferLineConstructor !== BufferLineJSArray) {
|
||||
this._bufferLineConstructor = BufferLineJSArray;
|
||||
this._recreateLines();
|
||||
}
|
||||
} else {
|
||||
if (this._bufferLineConstructor !== BufferLine) {
|
||||
this._bufferLineConstructor = BufferLine;
|
||||
this._recreateLines();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _recreateLines(): void {
|
||||
if (!this.lines) return;
|
||||
for (let i = 0; i < this.lines.length; ++i) {
|
||||
const oldLine = this.lines.get(i);
|
||||
const newLine = new this._bufferLineConstructor(oldLine.length);
|
||||
for (let j = 0; j < oldLine.length; ++j) {
|
||||
newLine.set(j, oldLine.get(j));
|
||||
}
|
||||
this.lines.set(i, newLine);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
return new this._bufferLineConstructor(this._terminal.cols, fillCharData, isWrapped);
|
||||
}
|
||||
|
||||
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 {
|
||||
const absoluteY = this.ybase + this.y;
|
||||
const relativeY = absoluteY - this.ydisp;
|
||||
return (relativeY >= 0 && relativeY < this._rows);
|
||||
return (relativeY >= 0 && relativeY < this._terminal.rows);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,7 +120,7 @@ export class Buffer implements IBuffer {
|
||||
if (fillAttr === undefined) {
|
||||
fillAttr = DEFAULT_ATTR;
|
||||
}
|
||||
let i = this._rows;
|
||||
let i = this._terminal.rows;
|
||||
while (i--) {
|
||||
this.lines.push(this.getBlankLine(fillAttr));
|
||||
}
|
||||
@@ -115,13 +131,14 @@ export class Buffer implements IBuffer {
|
||||
* Clears the buffer to it's initial state, discarding all previous data.
|
||||
*/
|
||||
public clear(): void {
|
||||
this.setBufferLineFactory(this._terminal.options.experimentalBufferLineImpl);
|
||||
this.ydisp = 0;
|
||||
this.ybase = 0;
|
||||
this.y = 0;
|
||||
this.x = 0;
|
||||
this.lines = new CircularList<IBufferLine>(this._getCorrectBufferLength(this._rows));
|
||||
this.lines = new CircularList<IBufferLine>(this._getCorrectBufferLength(this._terminal.rows));
|
||||
this.scrollTop = 0;
|
||||
this.scrollBottom = this._rows - 1;
|
||||
this.scrollBottom = this._terminal.rows - 1;
|
||||
this.setupTabStops();
|
||||
}
|
||||
|
||||
@@ -141,17 +158,18 @@ export class Buffer implements IBuffer {
|
||||
// The following adjustments should only happen if the buffer has been
|
||||
// initialized/filled.
|
||||
if (this.lines.length > 0) {
|
||||
// Deal with columns increasing (reducing needs to happen after reflow)
|
||||
if (this._cols < newCols) {
|
||||
// Deal with columns increasing (we don't do anything when columns reduce)
|
||||
if (this._terminal.cols < newCols) {
|
||||
const ch: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // does xterm use the default attr?
|
||||
for (let i = 0; i < this.lines.length; i++) {
|
||||
this.lines.get(i).resize(newCols, FILL_CHAR_DATA);
|
||||
this.lines.get(i).resize(newCols, ch);
|
||||
}
|
||||
}
|
||||
|
||||
// Resize rows in both directions as needed
|
||||
let addToY = 0;
|
||||
if (this._rows < newRows) {
|
||||
for (let y = this._rows; y < newRows; y++) {
|
||||
if (this._terminal.rows < newRows) {
|
||||
for (let y = this._terminal.rows; y < newRows; y++) {
|
||||
if (this.lines.length < newRows + this.ybase) {
|
||||
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,
|
||||
@@ -165,12 +183,13 @@ export class Buffer implements IBuffer {
|
||||
} else {
|
||||
// Add a blank line if there is no buffer left at the top to scroll to, or if there
|
||||
// are blank lines after the cursor
|
||||
this.lines.push(new BufferLine(newCols, FILL_CHAR_DATA));
|
||||
const fillCharData: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
|
||||
this.lines.push(new this._bufferLineConstructor(newCols, fillCharData));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else { // (this._rows >= newRows)
|
||||
for (let y = this._rows; y > newRows; y--) {
|
||||
} else { // (this._terminal.rows >= newRows)
|
||||
for (let y = this._terminal.rows; y > newRows; y--) {
|
||||
if (this.lines.length > newRows + this.ybase) {
|
||||
if (this.lines.length > this.ybase + this.y + 1) {
|
||||
// The line is a blank line below the cursor, remove it
|
||||
@@ -210,236 +229,8 @@ export class Buffer implements IBuffer {
|
||||
}
|
||||
|
||||
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
|
||||
@@ -453,19 +244,14 @@ export class Buffer implements IBuffer {
|
||||
* @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 {
|
||||
public stringIndexToBufferIndex(lineIndex: number, stringIndex: number): BufferIndex {
|
||||
while (stringIndex) {
|
||||
const line = this.lines.get(lineIndex);
|
||||
if (!line) {
|
||||
return [-1, -1];
|
||||
[-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;
|
||||
}
|
||||
for (let i = 0; i < line.length; ++i) {
|
||||
stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length;
|
||||
if (stringIndex < 0) {
|
||||
return [lineIndex, i];
|
||||
}
|
||||
@@ -485,12 +271,65 @@ export class Buffer implements IBuffer {
|
||||
* @param startCol The column to start 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);
|
||||
if (!line) {
|
||||
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.get(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 } {
|
||||
@@ -521,7 +360,7 @@ export class Buffer implements IBuffer {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -535,7 +374,7 @@ export class Buffer implements IBuffer {
|
||||
x = this.x;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -546,8 +385,8 @@ export class Buffer implements IBuffer {
|
||||
if (x === null || x === undefined) {
|
||||
x = this.x;
|
||||
}
|
||||
while (!this.tabs[++x] && x < this._cols);
|
||||
return x >= this._cols ? this._cols - 1 : x < 0 ? 0 : x;
|
||||
while (!this.tabs[++x] && x < this._terminal.cols);
|
||||
return x >= this._terminal.cols ? this._terminal.cols - 1 : x < 0 ? 0 : x;
|
||||
}
|
||||
|
||||
public addMarker(y: number): Marker {
|
||||
@@ -560,27 +399,12 @@ export class Buffer implements IBuffer {
|
||||
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)));
|
||||
return marker;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -664,7 +488,8 @@ export class BufferStringIterator implements IBufferStringIterator {
|
||||
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);
|
||||
// TODO: always apply trimRight after fixing #1685
|
||||
result += this._buffer.translateBufferLineToString(i, (this._trimRight) ? i === range.last : false);
|
||||
}
|
||||
this._current = range.last + 1;
|
||||
return {range: range, content: result};
|
||||
|
||||
+47
-149
@@ -5,14 +5,10 @@
|
||||
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';
|
||||
import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } 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) {
|
||||
@@ -145,161 +141,63 @@ describe('BufferLine', function(): void {
|
||||
});
|
||||
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)]);
|
||||
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], true);
|
||||
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)]);
|
||||
line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)], true);
|
||||
chai.expect(line.toArray()).eql(Array(5).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
it('shrink(false) - should not 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)], false);
|
||||
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
it('shrink(false) + shrink(false) - should not apply new size', function(): void {
|
||||
const line = new TestBufferLine(20, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
chai.expect(line.toArray()).eql(Array(20).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
it('shrink(false) + enlarge(false) to smaller than before', function(): void {
|
||||
const line = new TestBufferLine(20, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
line.resize(15, [1, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
chai.expect(line.toArray()).eql(Array(20).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
it('shrink(false) + enlarge(false) to bigger than before', function(): void {
|
||||
const line = new TestBufferLine(20, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
line.resize(25, [1, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
chai.expect(line.toArray()).eql(Array(25).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
it('shrink(false) + resize shrink=true should enforce shrinking', function(): void {
|
||||
const line = new TestBufferLine(20, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], true);
|
||||
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
it('enlarge from 0 length', function(): void {
|
||||
const line = new TestBufferLine(0, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
chai.expect(line.toArray()).eql(Array(10).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)]);
|
||||
line.resize(0, [1, 'a', 0, 'a'.charCodeAt(0)], true);
|
||||
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', () => {
|
||||
it('shrink(false) to 0 and enlarge to different sizes', function(): void {
|
||||
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('');
|
||||
line.resize(0, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
line.resize(7, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
line.resize(7, [1, 'a', 0, 'a'.charCodeAt(0)], true);
|
||||
chai.expect(line.toArray()).eql(Array(7).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+110
-71
@@ -3,8 +3,111 @@
|
||||
* @license MIT
|
||||
*/
|
||||
import { CharData, IBufferLine } from './Types';
|
||||
import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, WHITESPACE_CELL_CHAR } from './Buffer';
|
||||
import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer';
|
||||
|
||||
/**
|
||||
* Class representing a terminal line.
|
||||
*
|
||||
* @deprecated to be removed with one of the next releases
|
||||
*/
|
||||
export class BufferLineJSArray implements IBufferLine {
|
||||
protected _data: CharData[];
|
||||
public isWrapped = false;
|
||||
public length: number;
|
||||
|
||||
constructor(cols: number, fillCharData?: CharData, isWrapped?: boolean) {
|
||||
this._data = [];
|
||||
if (!fillCharData) {
|
||||
fillCharData = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
|
||||
}
|
||||
for (let i = 0; i < cols; i++) {
|
||||
this._push(fillCharData); // Note: the ctor ch is not cloned (resembles old behavior)
|
||||
}
|
||||
if (isWrapped) {
|
||||
this.isWrapped = true;
|
||||
}
|
||||
this.length = this._data.length;
|
||||
}
|
||||
|
||||
private _pop(): CharData | undefined {
|
||||
const data = this._data.pop();
|
||||
this.length = this._data.length;
|
||||
return data;
|
||||
}
|
||||
|
||||
private _push(data: CharData): void {
|
||||
this._data.push(data);
|
||||
this.length = this._data.length;
|
||||
}
|
||||
|
||||
private _splice(start: number, deleteCount: number, ...items: CharData[]): CharData[] {
|
||||
const removed = this._data.splice(start, deleteCount, ...items);
|
||||
this.length = this._data.length;
|
||||
return removed;
|
||||
}
|
||||
|
||||
public get(index: number): CharData {
|
||||
return this._data[index];
|
||||
}
|
||||
|
||||
public set(index: number, data: CharData): void {
|
||||
this._data[index] = data;
|
||||
}
|
||||
|
||||
/** insert n cells ch at pos, right cells are lost (stable length) */
|
||||
public insertCells(pos: number, n: number, ch: CharData): void {
|
||||
while (n--) {
|
||||
this._splice(pos, 0, ch);
|
||||
this._pop();
|
||||
}
|
||||
}
|
||||
|
||||
/** delete n cells at pos, right side is filled with fill (stable length) */
|
||||
public deleteCells(pos: number, n: number, fillCharData: CharData): void {
|
||||
while (n--) {
|
||||
this._splice(pos, 1);
|
||||
this._push(fillCharData);
|
||||
}
|
||||
}
|
||||
|
||||
/** replace cells from pos to pos + n - 1 with fill */
|
||||
public replaceCells(start: number, end: number, fillCharData: CharData): void {
|
||||
while (start < end && start < this.length) {
|
||||
this.set(start++, fillCharData); // Note: fill is not cloned (resembles old behavior)
|
||||
}
|
||||
}
|
||||
|
||||
/** resize line to cols filling new cells with fill */
|
||||
public resize(cols: number, fillCharData: CharData, shrink: boolean = false): void {
|
||||
while (this._data.length < cols) {
|
||||
this._data.push(fillCharData);
|
||||
}
|
||||
if (shrink) {
|
||||
while (this._data.length > cols) {
|
||||
this._data.pop();
|
||||
}
|
||||
}
|
||||
this.length = this._data.length;
|
||||
}
|
||||
|
||||
public fill(fillCharData: CharData): void {
|
||||
for (let i = 0; i < this.length; ++i) {
|
||||
this.set(i, fillCharData);
|
||||
}
|
||||
}
|
||||
|
||||
public copyFrom(line: BufferLineJSArray): void {
|
||||
this._data = line._data.slice(0);
|
||||
this.length = line.length;
|
||||
this.isWrapped = line.isWrapped;
|
||||
}
|
||||
|
||||
public clone(): IBufferLine {
|
||||
const newLine = new BufferLineJSArray(0);
|
||||
newLine.copyFrom(this);
|
||||
return newLine;
|
||||
}
|
||||
}
|
||||
|
||||
/** typed array slots taken by one cell */
|
||||
const CELL_SIZE = 3;
|
||||
@@ -16,9 +119,6 @@ const enum Cell {
|
||||
WIDTH = 2
|
||||
}
|
||||
|
||||
/** single vs. combined char distinction */
|
||||
const IS_COMBINED_BIT_MASK = 0x80000000;
|
||||
|
||||
/**
|
||||
* Typed array based bufferline implementation.
|
||||
*/
|
||||
@@ -44,25 +144,21 @@ export class BufferLine implements IBufferLine {
|
||||
const stringData = this._data[index * CELL_SIZE + Cell.STRING];
|
||||
return [
|
||||
this._data[index * CELL_SIZE + Cell.FLAGS],
|
||||
(stringData & IS_COMBINED_BIT_MASK)
|
||||
(stringData & 0x80000000)
|
||||
? this._combined[index]
|
||||
: (stringData) ? String.fromCharCode(stringData) : '',
|
||||
this._data[index * CELL_SIZE + Cell.WIDTH],
|
||||
(stringData & IS_COMBINED_BIT_MASK)
|
||||
(stringData & 0x80000000)
|
||||
? 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;
|
||||
this._data[index * CELL_SIZE + Cell.STRING] = index | 0x80000000;
|
||||
} else {
|
||||
this._data[index * CELL_SIZE + Cell.STRING] = value[1].charCodeAt(0);
|
||||
}
|
||||
@@ -107,8 +203,8 @@ export class BufferLine implements IBufferLine {
|
||||
}
|
||||
}
|
||||
|
||||
public resize(cols: number, fillCharData: CharData): void {
|
||||
if (cols === this.length) {
|
||||
public resize(cols: number, fillCharData: CharData, shrink: boolean = false): void {
|
||||
if (cols === this.length || (!shrink && cols < this.length)) {
|
||||
return;
|
||||
}
|
||||
if (cols > this.length) {
|
||||
@@ -124,22 +220,13 @@ export class BufferLine implements IBufferLine {
|
||||
for (let i = this.length; i < cols; ++i) {
|
||||
this.set(i, fillCharData);
|
||||
}
|
||||
} else {
|
||||
} else if (shrink) {
|
||||
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;
|
||||
@@ -182,52 +269,4 @@ export class BufferLine implements IBufferLine {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: 汉, 语');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { assert } from 'chai';
|
||||
import { ITerminal } from './Types';
|
||||
import { BufferSet } from './BufferSet';
|
||||
import { Buffer } from './Buffer';
|
||||
import { MockTerminal } from './ui/TestUtils.test';
|
||||
import { MockTerminal } from './utils/TestUtils.test';
|
||||
|
||||
describe('BufferSet', () => {
|
||||
let terminal: ITerminal;
|
||||
@@ -15,8 +15,8 @@ describe('BufferSet', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
terminal = new MockTerminal();
|
||||
(terminal as any).cols = 80;
|
||||
(terminal as any).rows = 24;
|
||||
terminal.cols = 80;
|
||||
terminal.rows = 24;
|
||||
terminal.options.scrollback = 1000;
|
||||
bufferSet = new BufferSet(terminal);
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { TestTerminal } from './ui/TestUtils.test';
|
||||
import { TestTerminal } from './utils/TestUtils.test';
|
||||
import { assert } from 'chai';
|
||||
import { getStringCellWidth, wcwidth } from './CharWidth';
|
||||
import { IBuffer } from './Types';
|
||||
|
||||
@@ -111,17 +111,17 @@ export class CompositionHelper {
|
||||
/**
|
||||
* Finalizes the composition, resuming regular input actions. This is called when a composition
|
||||
* 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
|
||||
* 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.
|
||||
*/
|
||||
private _finalizeComposition(waitForPropagation: boolean): void {
|
||||
private _finalizeComposition(waitForPropogation: boolean): void {
|
||||
this._compositionView.classList.remove('active');
|
||||
this._isComposing = false;
|
||||
this._clearTextareaPosition();
|
||||
|
||||
if (!waitForPropagation) {
|
||||
if (!waitForPropogation) {
|
||||
// Cancel any delayed composition send requests and send the input immediately.
|
||||
this._isSendingComposition = false;
|
||||
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
|
||||
// browsers, use a setTimeout with 0ms time to allow the native compositionend event to
|
||||
// complete. This ensures the correct character is retrieved.
|
||||
// This solution was used because:
|
||||
// complete. This ensures the correct character is retrieved, this solution was used
|
||||
// because:
|
||||
// - 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 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.height = 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
|
||||
// text is.
|
||||
const compositionViewBounds = this._compositionView.getBoundingClientRect();
|
||||
|
||||
+136
-291
File diff suppressed because it is too large
Load Diff
+31
-93
@@ -4,16 +4,7 @@
|
||||
*/
|
||||
|
||||
import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from './Types';
|
||||
import { IDisposable } from 'xterm';
|
||||
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).
|
||||
@@ -76,8 +67,7 @@ const PRINTABLES = r(0x20, 0x7f);
|
||||
const EXECUTABLES = r(0x00, 0x18);
|
||||
EXECUTABLES.push(0x19);
|
||||
EXECUTABLES.push.apply(EXECUTABLES, r(0x1c, 0x20));
|
||||
// Pseudo-character placeholder for printable non-ascii characters.
|
||||
const NON_ASCII_PRINTABLE = 0xA0;
|
||||
const DEFAULT_TRANSITION = ParserAction.ERROR << 4 | ParserState.GROUND;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
let state: any;
|
||||
|
||||
// table with default transition
|
||||
// table with default transition [any] --> DEFAULT_TRANSITION
|
||||
for (state in states) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -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(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(0x7f, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING);
|
||||
// csi entries
|
||||
table.add(0x5b, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.CSI_ENTRY);
|
||||
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.add(0x7f, ParserState.DCS_PASSTHROUGH, ParserAction.IGNORE, ParserState.DCS_PASSTHROUGH);
|
||||
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;
|
||||
})();
|
||||
|
||||
@@ -204,7 +192,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
|
||||
*/
|
||||
class DcsDummy implements IDcsHandler {
|
||||
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 { }
|
||||
}
|
||||
|
||||
@@ -230,17 +218,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
protected _collect: string;
|
||||
|
||||
// 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 _csiHandlers: IHandlerCollection<CsiHandler>;
|
||||
protected _csiHandlers: any;
|
||||
protected _escHandlers: any;
|
||||
protected _oscHandlers: IHandlerCollection<OscHandler>;
|
||||
protected _oscHandlers: any;
|
||||
protected _dcsHandlers: any;
|
||||
protected _activeDcsHandler: IDcsHandler | null;
|
||||
protected _errorHandler: (state: IParsingState) => IParsingState;
|
||||
|
||||
// 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 _csiHandlerFb: (collect: string, params: number[], flag: number) => void;
|
||||
protected _escHandlerFb: (collect: string, flag: number) => void;
|
||||
@@ -288,15 +276,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
this._errorHandlerFb = null;
|
||||
this._printHandler = null;
|
||||
this._executeHandlers = null;
|
||||
this._escHandlers = null;
|
||||
this._csiHandlers = null;
|
||||
this._escHandlers = null;
|
||||
this._oscHandlers = null;
|
||||
this._dcsHandlers = null;
|
||||
this._activeDcsHandler = 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;
|
||||
}
|
||||
clearPrintHandler(): void {
|
||||
@@ -313,24 +301,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
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 {
|
||||
this._csiHandlers[flag.charCodeAt(0)] = [callback];
|
||||
this._csiHandlers[flag.charCodeAt(0)] = callback;
|
||||
}
|
||||
clearCsiHandler(flag: string): void {
|
||||
if (this._csiHandlers[flag.charCodeAt(0)]) delete this._csiHandlers[flag.charCodeAt(0)];
|
||||
@@ -349,23 +321,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
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 {
|
||||
this._oscHandlers[ident] = [callback];
|
||||
this._oscHandlers[ident] = callback;
|
||||
}
|
||||
clearOscHandler(ident: number): void {
|
||||
if (this._oscHandlers[ident]) delete this._oscHandlers[ident];
|
||||
@@ -399,7 +356,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
this._activeDcsHandler = null;
|
||||
}
|
||||
|
||||
parse(data: Uint32Array, length: number): void {
|
||||
parse(data: string): void {
|
||||
let code = 0;
|
||||
let transition = 0;
|
||||
let error = false;
|
||||
@@ -414,14 +371,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
let callback: Function | null = null;
|
||||
|
||||
// process input string
|
||||
for (let i = 0; i < length; ++i) {
|
||||
code = data[i];
|
||||
const l = data.length;
|
||||
for (let i = 0; i < l; ++i) {
|
||||
code = data.charCodeAt(i);
|
||||
|
||||
// shortcut for most chars (print action)
|
||||
if (currentState === ParserState.GROUND && code > 0x1f && code < 0x80) {
|
||||
print = (~print) ? print : i;
|
||||
do i++;
|
||||
while (i < length && data[i] > 0x1f && data[i] < 0x80);
|
||||
while (i < l && data.charCodeAt(i) > 0x1f && data.charCodeAt(i) < 0x80);
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
@@ -433,7 +391,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
}
|
||||
|
||||
// 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) {
|
||||
case ParserAction.PRINT:
|
||||
print = (~print) ? print : i;
|
||||
@@ -465,6 +423,10 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
case ParserState.GROUND:
|
||||
print = (~print) ? print : i;
|
||||
break;
|
||||
case ParserState.OSC_STRING:
|
||||
osc += String.fromCharCode(code);
|
||||
transition |= ParserState.OSC_STRING;
|
||||
break;
|
||||
case ParserState.CSI_IGNORE:
|
||||
transition |= ParserState.CSI_IGNORE;
|
||||
break;
|
||||
@@ -501,17 +463,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
}
|
||||
break;
|
||||
case ParserAction.CSI_DISPATCH:
|
||||
// Trigger CSI Handler
|
||||
const handlers = this._csiHandlers[code];
|
||||
let j = handlers ? handlers.length - 1 : -1;
|
||||
for (; j >= 0; j--) {
|
||||
if (handlers[j](params, collect)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (j < 0) {
|
||||
this._csiHandlerFb(collect, params, code);
|
||||
}
|
||||
callback = this._csiHandlers[code];
|
||||
if (callback) callback(params, collect);
|
||||
else this._csiHandlerFb(collect, params, code);
|
||||
break;
|
||||
case ParserAction.PARAM:
|
||||
if (code === 0x3b) params.push(0);
|
||||
@@ -563,15 +517,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
osc = '';
|
||||
break;
|
||||
case ParserAction.OSC_PUT:
|
||||
for (let j = i + 1; ; j++) {
|
||||
if (j >= length
|
||||
|| (code = data[j]) < 0x20
|
||||
|| (code > 0x7f && code <= 0x9f)) {
|
||||
osc += utf32ToString(data, i, j);
|
||||
i = j - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
osc += data.charAt(i);
|
||||
break;
|
||||
case ParserAction.OSC_END:
|
||||
if (osc && code !== 0x18 && code !== 0x1a) {
|
||||
@@ -586,17 +532,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
// or with an explicit NaN OSC handler
|
||||
const identifier = parseInt(osc.substring(0, idx));
|
||||
const content = osc.substring(idx + 1);
|
||||
// Trigger OSC Handler
|
||||
const handlers = this._oscHandlers[identifier];
|
||||
let j = handlers ? handlers.length - 1 : -1;
|
||||
for (; j >= 0; j--) {
|
||||
if (handlers[j](content)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (j < 0) {
|
||||
this._oscHandlerFb(identifier, content);
|
||||
}
|
||||
callback = this._oscHandlers[identifier];
|
||||
if (callback) callback(content);
|
||||
else this._oscHandlerFb(identifier, content);
|
||||
}
|
||||
}
|
||||
if (code === 0x1b) transition |= ParserState.ESCAPE;
|
||||
@@ -611,9 +549,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
|
||||
// push leftover pushable buffers to terminal
|
||||
if (currentState === ParserState.GROUND && ~print) {
|
||||
this._printHandler(data, print, length);
|
||||
this._printHandler(data, print, data.length);
|
||||
} else if (currentState === ParserState.DCS_PASSTHROUGH && ~dcs && dcsHandler) {
|
||||
dcsHandler.put(data, dcs, length);
|
||||
dcsHandler.put(data, dcs, data.length);
|
||||
}
|
||||
|
||||
// save non pushable buffers
|
||||
|
||||
+311
-141
@@ -5,11 +5,131 @@
|
||||
|
||||
import { assert, expect } from 'chai';
|
||||
import { InputHandler } from './InputHandler';
|
||||
import { MockInputHandlingTerminal } from './ui/TestUtils.test';
|
||||
import { CHAR_DATA_ATTR_INDEX, DEFAULT_ATTR } from './Buffer';
|
||||
import { MockInputHandlingTerminal } from './utils/TestUtils.test';
|
||||
import { NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, DEFAULT_ATTR } from './Buffer';
|
||||
import { Terminal } from './Terminal';
|
||||
import { IBufferLine } from './Types';
|
||||
|
||||
|
||||
// TODO: This and the sections related to this object in associated tests can be
|
||||
// removed safely after InputHandler refactors are finished
|
||||
class OldInputHandler extends InputHandler {
|
||||
public eraseInLine(params: number[]): void {
|
||||
switch (params[0]) {
|
||||
case 0:
|
||||
this.eraseRight(this._terminal.buffer.x, this._terminal.buffer.y);
|
||||
break;
|
||||
case 1:
|
||||
this.eraseLeft(this._terminal.buffer.x, this._terminal.buffer.y);
|
||||
break;
|
||||
case 2:
|
||||
this.eraseLine(this._terminal.buffer.y);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public eraseInDisplay(params: number[]): void {
|
||||
let j;
|
||||
switch (params[0]) {
|
||||
case 0:
|
||||
this.eraseRight(this._terminal.buffer.x, this._terminal.buffer.y);
|
||||
j = this._terminal.buffer.y + 1;
|
||||
for (; j < this._terminal.rows; j++) {
|
||||
this.eraseLine(j);
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
this.eraseLeft(this._terminal.buffer.x, this._terminal.buffer.y);
|
||||
j = this._terminal.buffer.y;
|
||||
while (j--) {
|
||||
this.eraseLine(j);
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
j = this._terminal.rows;
|
||||
while (j--) this.eraseLine(j);
|
||||
break;
|
||||
case 3:
|
||||
// Clear scrollback (everything not in viewport)
|
||||
const scrollBackSize = this._terminal.buffer.lines.length - this._terminal.rows;
|
||||
if (scrollBackSize > 0) {
|
||||
this._terminal.buffer.lines.trimStart(scrollBackSize);
|
||||
this._terminal.buffer.ybase = Math.max(this._terminal.buffer.ybase - scrollBackSize, 0);
|
||||
this._terminal.buffer.ydisp = Math.max(this._terminal.buffer.ydisp - scrollBackSize, 0);
|
||||
// Force a scroll event to refresh viewport
|
||||
this._terminal.emit('scroll', 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase in the identified line everything from "x" to the end of the line (right).
|
||||
* @param x The column from which to start erasing to the end of the line.
|
||||
* @param y The line in which to operate.
|
||||
*/
|
||||
public eraseRight(x: number, y: number): void {
|
||||
const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + y);
|
||||
if (!line) {
|
||||
return;
|
||||
}
|
||||
line.replaceCells(x, this._terminal.cols, [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
this._terminal.updateRange(y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase in the identified line everything from "x" to the start of the line (left).
|
||||
* @param x The column from which to start erasing to the start of the line.
|
||||
* @param y The line in which to operate.
|
||||
*/
|
||||
public eraseLeft(x: number, y: number): void {
|
||||
const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + y);
|
||||
if (!line) {
|
||||
return;
|
||||
}
|
||||
line.replaceCells(0, x + 1, [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
this._terminal.updateRange(y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase all content in the given line
|
||||
* @param y The line to erase all of its contents.
|
||||
*/
|
||||
public eraseLine(y: number): void {
|
||||
this.eraseRight(0, y);
|
||||
}
|
||||
|
||||
public insertChars(params: number[]): void {
|
||||
let param = params[0];
|
||||
if (param < 1) param = 1;
|
||||
|
||||
// make buffer local for faster access
|
||||
const buffer = this._terminal.buffer;
|
||||
|
||||
const row = buffer.y + buffer.ybase;
|
||||
let j = buffer.x;
|
||||
while (param-- && j < this._terminal.cols) {
|
||||
buffer.lines.get(row).insertCells(j++, 1, [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
}
|
||||
}
|
||||
|
||||
public deleteChars(params: number[]): void {
|
||||
let param: number = params[0];
|
||||
if (param < 1) {
|
||||
param = 1;
|
||||
}
|
||||
|
||||
// make buffer local for faster access
|
||||
const buffer = this._terminal.buffer;
|
||||
|
||||
const row = buffer.y + buffer.ybase;
|
||||
while (param--) {
|
||||
buffer.lines.get(row).deleteCells(buffer.x, 1, [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
}
|
||||
this._terminal.updateRange(buffer.y);
|
||||
}
|
||||
}
|
||||
|
||||
describe('InputHandler', () => {
|
||||
describe('save and restore cursor', () => {
|
||||
const terminal = new MockInputHandlingTerminal();
|
||||
@@ -88,258 +208,302 @@ describe('InputHandler', () => {
|
||||
});
|
||||
});
|
||||
describe('regression tests', function(): void {
|
||||
function termContent(term: Terminal, trim: boolean): string[] {
|
||||
function lineContent(line: IBufferLine): string {
|
||||
let content = '';
|
||||
for (let i = 0; i < line.length; ++i) content += line.get(i)[CHAR_DATA_CHAR_INDEX];
|
||||
return content;
|
||||
}
|
||||
|
||||
function termContent(term: Terminal): string[] {
|
||||
const result = [];
|
||||
for (let i = 0; i < term.rows; ++i) result.push(term.buffer.lines.get(i).translateToString(trim));
|
||||
for (let i = 0; i < term.rows; ++i) result.push(lineContent(term.buffer.lines.get(i)));
|
||||
return result;
|
||||
}
|
||||
|
||||
it('insertChars', function(): void {
|
||||
const term = new Terminal();
|
||||
const inputHandler = new InputHandler(term);
|
||||
const oldInputHandler = new OldInputHandler(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');
|
||||
const line1: IBufferLine = term.buffer.lines.get(0); // line for old variant
|
||||
const line2: IBufferLine = term.buffer.lines.get(1); // line for new variant
|
||||
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '1234567890');
|
||||
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '1234567890');
|
||||
|
||||
// insert one char from params = [0]
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 70;
|
||||
oldInputHandler.insertChars([0]);
|
||||
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' 123456789');
|
||||
term.buffer.y = 1;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.insertChars([0]);
|
||||
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' 123456789');
|
||||
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' 123456789');
|
||||
expect(lineContent(line2)).equals(lineContent(line1));
|
||||
|
||||
// insert one char from params = [1]
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 70;
|
||||
oldInputHandler.insertChars([1]);
|
||||
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' 12345678');
|
||||
term.buffer.y = 1;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.insertChars([1]);
|
||||
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' 12345678');
|
||||
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' 12345678');
|
||||
expect(lineContent(line2)).equals(lineContent(line1));
|
||||
|
||||
// insert two chars from params = [2]
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 70;
|
||||
oldInputHandler.insertChars([2]);
|
||||
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' 123456');
|
||||
term.buffer.y = 1;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.insertChars([2]);
|
||||
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' 123456');
|
||||
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' 123456');
|
||||
expect(lineContent(line2)).equals(lineContent(line1));
|
||||
|
||||
// insert 10 chars from params = [10]
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 70;
|
||||
oldInputHandler.insertChars([10]);
|
||||
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' ');
|
||||
term.buffer.y = 1;
|
||||
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'));
|
||||
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' ');
|
||||
expect(lineContent(line2)).equals(lineContent(line1));
|
||||
});
|
||||
it('deleteChars', function(): void {
|
||||
const term = new Terminal();
|
||||
const inputHandler = new InputHandler(term);
|
||||
const oldInputHandler = new OldInputHandler(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');
|
||||
const line1: IBufferLine = term.buffer.lines.get(0); // line for old variant
|
||||
const line2: IBufferLine = term.buffer.lines.get(1); // line for new variant
|
||||
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '1234567890');
|
||||
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '1234567890');
|
||||
|
||||
// delete one char from params = [0]
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 70;
|
||||
oldInputHandler.deleteChars([0]);
|
||||
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '234567890 ');
|
||||
term.buffer.y = 1;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.deleteChars([0]);
|
||||
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + '234567890 ');
|
||||
expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a') + '234567890');
|
||||
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '234567890 ');
|
||||
expect(lineContent(line2)).equals(lineContent(line1));
|
||||
|
||||
// insert one char from params = [1]
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 70;
|
||||
oldInputHandler.deleteChars([1]);
|
||||
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '34567890 ');
|
||||
term.buffer.y = 1;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.deleteChars([1]);
|
||||
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + '34567890 ');
|
||||
expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a') + '34567890');
|
||||
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '34567890 ');
|
||||
expect(lineContent(line2)).equals(lineContent(line1));
|
||||
|
||||
// insert two chars from params = [2]
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 70;
|
||||
oldInputHandler.deleteChars([2]);
|
||||
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '567890 ');
|
||||
term.buffer.y = 1;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.deleteChars([2]);
|
||||
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + '567890 ');
|
||||
expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a') + '567890');
|
||||
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '567890 ');
|
||||
expect(lineContent(line2)).equals(lineContent(line1));
|
||||
|
||||
// insert 10 chars from params = [10]
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 70;
|
||||
oldInputHandler.deleteChars([10]);
|
||||
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' ');
|
||||
term.buffer.y = 1;
|
||||
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'));
|
||||
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' ');
|
||||
expect(lineContent(line2)).equals(lineContent(line1));
|
||||
});
|
||||
it('eraseInLine', function(): void {
|
||||
const term = new Terminal();
|
||||
const inputHandler = new InputHandler(term);
|
||||
const oldInputHandler = new OldInputHandler(term);
|
||||
|
||||
// fill 6 lines to test 3 different states
|
||||
inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||
inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||
inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||
inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||
inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||
inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||
|
||||
// params[0] - right erase
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.eraseInLine([0]);
|
||||
expect(term.buffer.lines.get(0).translateToString(false)).equals(Array(71).join('a') + ' ');
|
||||
|
||||
// params[1] - left erase
|
||||
oldInputHandler.eraseInLine([0]);
|
||||
expect(lineContent(term.buffer.lines.get(0))).equals(Array(71).join('a') + ' ');
|
||||
term.buffer.y = 1;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.eraseInLine([1]);
|
||||
expect(term.buffer.lines.get(1).translateToString(false)).equals(Array(71).join(' ') + ' aaaaaaaaa');
|
||||
inputHandler.eraseInLine([0]);
|
||||
expect(lineContent(term.buffer.lines.get(1))).equals(Array(71).join('a') + ' ');
|
||||
|
||||
// params[1] - left erase
|
||||
term.buffer.y = 2;
|
||||
term.buffer.x = 70;
|
||||
oldInputHandler.eraseInLine([1]);
|
||||
expect(lineContent(term.buffer.lines.get(2))).equals(Array(71).join(' ') + ' aaaaaaaaa');
|
||||
term.buffer.y = 3;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.eraseInLine([1]);
|
||||
expect(lineContent(term.buffer.lines.get(3))).equals(Array(71).join(' ') + ' aaaaaaaaa');
|
||||
|
||||
// params[1] - left erase
|
||||
term.buffer.y = 4;
|
||||
term.buffer.x = 70;
|
||||
oldInputHandler.eraseInLine([2]);
|
||||
expect(lineContent(term.buffer.lines.get(4))).equals(Array(term.cols + 1).join(' '));
|
||||
term.buffer.y = 5;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.eraseInLine([2]);
|
||||
expect(term.buffer.lines.get(2).translateToString(false)).equals(Array(term.cols + 1).join(' '));
|
||||
expect(lineContent(term.buffer.lines.get(5))).equals(Array(term.cols + 1).join(' '));
|
||||
|
||||
});
|
||||
it('eraseInDisplay', function(): void {
|
||||
const term = new Terminal({cols: 80, rows: 7});
|
||||
const inputHandler = new InputHandler(term);
|
||||
const termOld = new Terminal();
|
||||
const inputHandlerOld = new OldInputHandler(termOld);
|
||||
const termNew = new Terminal();
|
||||
const inputHandlerNew = new InputHandler(termNew);
|
||||
|
||||
// fill display with a's
|
||||
for (let i = 0; i < term.rows; ++i) inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||
for (let i = 0; i < termOld.rows; ++i) inputHandlerOld.parse(Array(termOld.cols + 1).join('a'));
|
||||
for (let i = 0; i < termNew.rows; ++i) inputHandlerNew.parse(Array(termOld.cols + 1).join('a'));
|
||||
const data = [];
|
||||
for (let i = 0; i < termOld.rows; ++i) data.push(Array(termOld.cols + 1).join('a'));
|
||||
expect(termContent(termOld)).eql(data);
|
||||
expect(termContent(termOld)).eql(termContent(termNew));
|
||||
|
||||
// params [0] - right and below erase
|
||||
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'),
|
||||
''
|
||||
]);
|
||||
termOld.buffer.y = 5;
|
||||
termOld.buffer.x = 40;
|
||||
inputHandlerOld.eraseInDisplay([0]);
|
||||
termNew.buffer.y = 5;
|
||||
termNew.buffer.x = 40;
|
||||
inputHandlerNew.eraseInDisplay([0]);
|
||||
expect(termContent(termNew)).eql(termContent(termOld));
|
||||
|
||||
// reset
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 0;
|
||||
for (let i = 0; i < term.rows; ++i) inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||
termOld.buffer.y = 0;
|
||||
termOld.buffer.x = 0;
|
||||
termNew.buffer.y = 0;
|
||||
termNew.buffer.x = 0;
|
||||
for (let i = 0; i < termOld.rows; ++i) inputHandlerOld.parse(Array(termOld.cols + 1).join('a'));
|
||||
for (let i = 0; i < termNew.rows; ++i) inputHandlerNew.parse(Array(termOld.cols + 1).join('a'));
|
||||
|
||||
// params [1] - left and above
|
||||
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')
|
||||
]);
|
||||
termOld.buffer.y = 5;
|
||||
termOld.buffer.x = 40;
|
||||
inputHandlerOld.eraseInDisplay([1]);
|
||||
termNew.buffer.y = 5;
|
||||
termNew.buffer.x = 40;
|
||||
inputHandlerNew.eraseInDisplay([1]);
|
||||
expect(termContent(termNew)).eql(termContent(termOld));
|
||||
|
||||
// reset
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 0;
|
||||
for (let i = 0; i < term.rows; ++i) inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||
termOld.buffer.y = 0;
|
||||
termOld.buffer.x = 0;
|
||||
termNew.buffer.y = 0;
|
||||
termNew.buffer.x = 0;
|
||||
for (let i = 0; i < termOld.rows; ++i) inputHandlerOld.parse(Array(termOld.cols + 1).join('a'));
|
||||
for (let i = 0; i < termNew.rows; ++i) inputHandlerNew.parse(Array(termOld.cols + 1).join('a'));
|
||||
|
||||
// params [2] - whole screen
|
||||
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([
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
''
|
||||
]);
|
||||
termOld.buffer.y = 5;
|
||||
termOld.buffer.x = 40;
|
||||
inputHandlerOld.eraseInDisplay([2]);
|
||||
termNew.buffer.y = 5;
|
||||
termNew.buffer.x = 40;
|
||||
inputHandlerNew.eraseInDisplay([2]);
|
||||
expect(termContent(termNew)).eql(termContent(termOld));
|
||||
|
||||
// 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'));
|
||||
termNew.buffer.y = 0;
|
||||
termNew.buffer.x = 0;
|
||||
inputHandlerNew.parse(Array(termNew.cols + 1).join('a')); // line 0
|
||||
inputHandlerNew.parse(Array(termNew.cols + 10).join('a')); // line 1 and 2
|
||||
for (let i = 3; i < termOld.rows; ++i) inputHandlerNew.parse(Array(termNew.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;
|
||||
expect(termNew.buffer.lines.get(2).isWrapped).true;
|
||||
termNew.buffer.y = 2;
|
||||
termNew.buffer.x = 40;
|
||||
inputHandlerNew.eraseInDisplay([1]);
|
||||
expect(termNew.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'));
|
||||
termNew.buffer.y = 0;
|
||||
termNew.buffer.x = 0;
|
||||
inputHandlerNew.parse(Array(termNew.cols + 1).join('a')); // line 0
|
||||
inputHandlerNew.parse(Array(termNew.cols + 10).join('a')); // line 1 and 2
|
||||
for (let i = 3; i < termOld.rows; ++i) inputHandlerNew.parse(Array(termNew.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;
|
||||
expect(termNew.buffer.lines.get(2).isWrapped).true;
|
||||
termNew.buffer.y = 1;
|
||||
termNew.buffer.x = 90; // Cursor is beyond last column
|
||||
inputHandlerNew.eraseInDisplay([1]);
|
||||
expect(termNew.buffer.lines.get(2).isWrapped).false;
|
||||
});
|
||||
});
|
||||
it('convertEol setting', function(): void {
|
||||
// not converting
|
||||
let s = '';
|
||||
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');
|
||||
for (let i = 0; i < termNotConverting.cols; ++i) {
|
||||
s += termNotConverting.buffer.lines.get(0).get(i)[CHAR_DATA_CHAR_INDEX];
|
||||
}
|
||||
expect(s).equals('Hello ');
|
||||
s = '';
|
||||
for (let i = 0; i < termNotConverting.cols; ++i) {
|
||||
s += termNotConverting.buffer.lines.get(1).get(i)[CHAR_DATA_CHAR_INDEX];
|
||||
}
|
||||
expect(s).equals(' World ');
|
||||
|
||||
// converting
|
||||
s = '';
|
||||
const termConverting = new Terminal({cols: 15, rows: 10, convertEol: true});
|
||||
(termConverting as any)._inputHandler.parse('Hello\nWorld');
|
||||
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');
|
||||
for (let i = 0; i < termConverting.cols; ++i) {
|
||||
s += termConverting.buffer.lines.get(0).get(i)[CHAR_DATA_CHAR_INDEX];
|
||||
}
|
||||
expect(s).equals('Hello ');
|
||||
s = '';
|
||||
for (let i = 0; i < termConverting.cols; ++i) {
|
||||
s += termConverting.buffer.lines.get(1).get(i)[CHAR_DATA_CHAR_INDEX];
|
||||
}
|
||||
expect(s).equals('World ');
|
||||
});
|
||||
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);
|
||||
inputHandler.print(String.fromCharCode(0x200B), 0, 1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -347,28 +511,34 @@ describe('InputHandler', () => {
|
||||
let term: Terminal;
|
||||
let handler: InputHandler;
|
||||
|
||||
function lineContent(line: IBufferLine): string {
|
||||
let content = '';
|
||||
for (let i = 0; i < line.length; ++i) content += line.get(i)[CHAR_DATA_CHAR_INDEX];
|
||||
return content;
|
||||
}
|
||||
|
||||
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');
|
||||
expect(lineContent(term.buffer.lines.get(0))).to.equal(Array(term.cols + 1).join(' '));
|
||||
expect(lineContent(term.buffer.lines.get(1))).to.equal(' TEST' + Array(term.cols - 7).join(' '));
|
||||
// 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');
|
||||
expect(lineContent(term.buffer.lines.get(0))).to.equal(Array(term.cols + 1).join(' '));
|
||||
expect(lineContent(term.buffer.lines.get(1))).to.equal(' TEST' + Array(term.cols - 7).join(' '));
|
||||
// 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');
|
||||
expect(lineContent(term.buffer.lines.get(0))).to.equal('TEST' + Array(term.cols - 3).join(' '));
|
||||
expect(lineContent(term.buffer.lines.get(1))).to.equal('JUNK' + Array(term.cols - 3).join(' '));
|
||||
// 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
|
||||
@@ -376,18 +546,18 @@ describe('InputHandler', () => {
|
||||
});
|
||||
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('');
|
||||
expect(lineContent(term.buffer.lines.get(0))).to.equal('TEST' + Array(term.cols - 3).join(' '));
|
||||
expect(lineContent(term.buffer.lines.get(1))).to.equal(Array(term.cols + 1).join(' '));
|
||||
// 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');
|
||||
expect(lineContent(term.buffer.lines.get(0))).to.equal('TEST' + Array(term.cols - 3).join(' '));
|
||||
// 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');
|
||||
expect(lineContent(term.buffer.lines.get(1))).to.equal('TEST' + Array(term.cols - 3).join(' '));
|
||||
// Text color of 'TEST' should be red
|
||||
expect((term.buffer.lines.get(1).get(0)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1);
|
||||
});
|
||||
|
||||
+56
-62
@@ -12,10 +12,7 @@ import { FLAGS } from './renderer/Types';
|
||||
import { wcwidth } from './CharWidth';
|
||||
import { EscapeSequenceParser } from './EscapeSequenceParser';
|
||||
import { ICharset } from './core/Types';
|
||||
import { IDisposable } from 'xterm';
|
||||
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`.
|
||||
@@ -34,22 +31,21 @@ const GLEVEL: {[key: string]: number} = {'(': 0, ')': 1, '*': 2, '+': 3, '-': 1,
|
||||
* Response: DECRPSS (https://vt100.net/docs/vt510-rm/DECRPSS.html)
|
||||
*/
|
||||
class DECRQSS implements IDcsHandler {
|
||||
private _data: Uint32Array = new Uint32Array(0);
|
||||
private _data: string;
|
||||
|
||||
constructor(private _terminal: any) { }
|
||||
|
||||
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 {
|
||||
this._data = concat(this._data, data.subarray(start, end));
|
||||
put(data: string, start: number, end: number): void {
|
||||
this._data += data.substring(start, end);
|
||||
}
|
||||
|
||||
unhook(): void {
|
||||
const data = utf32ToString(this._data);
|
||||
this._data = new Uint32Array(0);
|
||||
switch (data) {
|
||||
switch (this._data) {
|
||||
// valid: DCS 1 $ r Pt ST (xterm)
|
||||
case '"q': // DECSCA
|
||||
return this._terminal.handler(`${C0.ESC}P1$r0"q${C0.ESC}\\`);
|
||||
@@ -69,7 +65,7 @@ class DECRQSS implements IDcsHandler {
|
||||
return this._terminal.handler(`${C0.ESC}P1$r${style} q${C0.ESC}\\`);
|
||||
default:
|
||||
// invalid: DCS 0 $ r Pt ST (xterm)
|
||||
this._terminal.error('Unknown DCS $q %s', data);
|
||||
this._terminal.error('Unknown DCS $q %s', this._data);
|
||||
this._terminal.handler(`${C0.ESC}P0$r${C0.ESC}\\`);
|
||||
}
|
||||
}
|
||||
@@ -81,17 +77,11 @@ class DECRQSS implements IDcsHandler {
|
||||
* not supported
|
||||
*/
|
||||
|
||||
/**
|
||||
* DCS + q Pt ST (xterm)
|
||||
* Request Terminfo String
|
||||
* not implemented
|
||||
*/
|
||||
|
||||
/**
|
||||
* DCS + p Pt ST (xterm)
|
||||
* Set Terminfo Data
|
||||
* not supported
|
||||
*/
|
||||
/**
|
||||
* DCS + p Pt ST (xterm)
|
||||
* Set Terminfo Data
|
||||
* not supported
|
||||
*/
|
||||
|
||||
|
||||
|
||||
@@ -103,8 +93,7 @@ class DECRQSS implements IDcsHandler {
|
||||
* each function's header comment.
|
||||
*/
|
||||
export class InputHandler extends Disposable implements IInputHandler {
|
||||
private _parseBuffer: Uint32Array = new Uint32Array(4096);
|
||||
private _stringDecoder: StringToUtf32 = new StringToUtf32();
|
||||
private _surrogateFirst: string;
|
||||
|
||||
constructor(
|
||||
protected _terminal: IInputHandlingTerminal,
|
||||
@@ -114,6 +103,8 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
|
||||
this.register(this._parser);
|
||||
|
||||
this._surrogateFirst = '';
|
||||
|
||||
/**
|
||||
* custom fallback handlers
|
||||
*/
|
||||
@@ -298,13 +289,13 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._terminal.log('data: ' + data);
|
||||
}
|
||||
|
||||
if (this._parseBuffer.length < data.length) {
|
||||
this._parseBuffer = new Uint32Array(data.length);
|
||||
// apply leftover surrogate high from last write
|
||||
if (this._surrogateFirst) {
|
||||
data = this._surrogateFirst + data;
|
||||
this._surrogateFirst = '';
|
||||
}
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
this._parseBuffer[i] = data.charCodeAt(i);
|
||||
}
|
||||
this._parser.parse(this._parseBuffer, this._stringDecoder.decode(data, this._parseBuffer));
|
||||
|
||||
this._parser.parse(data);
|
||||
|
||||
buffer = this._terminal.buffer;
|
||||
if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) {
|
||||
@@ -312,9 +303,9 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
}
|
||||
}
|
||||
|
||||
public print(data: Uint32Array, start: number, end: number): void {
|
||||
let code: number;
|
||||
public print(data: string, start: number, end: number): void {
|
||||
let char: string;
|
||||
let code: number;
|
||||
let chWidth: number;
|
||||
const buffer: IBuffer = this._terminal.buffer;
|
||||
const charset: ICharset = this._terminal.charset;
|
||||
@@ -326,23 +317,41 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
let bufferRow = buffer.lines.get(buffer.y + buffer.ybase);
|
||||
|
||||
this._terminal.updateRange(buffer.y);
|
||||
for (let pos = start; pos < end; ++pos) {
|
||||
code = data[pos];
|
||||
char = stringFromCodePoint(code);
|
||||
for (let stringPosition = start; stringPosition < end; ++stringPosition) {
|
||||
char = data.charAt(stringPosition);
|
||||
code = data.charCodeAt(stringPosition);
|
||||
|
||||
// surrogate pair handling
|
||||
if (0xD800 <= code && code <= 0xDBFF) {
|
||||
if (++stringPosition >= end) {
|
||||
// end of input:
|
||||
// handle pairs as true UTF-16 and wait for the second part
|
||||
// since we expect the input comming from a stream there is
|
||||
// a small chance that the surrogate pair got split
|
||||
// therefore we dont process the first char here, instead
|
||||
// it gets added as first char to the next processed chunk
|
||||
this._surrogateFirst = char;
|
||||
continue;
|
||||
}
|
||||
const second = data.charCodeAt(stringPosition);
|
||||
// if the second part is in surrogate pair range create the high codepoint
|
||||
// otherwise fall back to UCS-2 behavior (handle codepoints independently)
|
||||
if (0xDC00 <= second && second <= 0xDFFF) {
|
||||
code = (code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;
|
||||
char += data.charAt(stringPosition);
|
||||
} else {
|
||||
stringPosition--;
|
||||
}
|
||||
}
|
||||
|
||||
// calculate print space
|
||||
// expensive call, therefore we save width in line buffer
|
||||
chWidth = wcwidth(code);
|
||||
|
||||
// get charset replacement character
|
||||
// charset are only defined for ASCII, therefore we only
|
||||
// search for an replacement char if code < 127
|
||||
if (code < 127 && charset) {
|
||||
const ch = charset[char];
|
||||
if (ch) {
|
||||
code = ch.charCodeAt(0);
|
||||
char = ch;
|
||||
}
|
||||
if (charset) {
|
||||
char = charset[char] || char;
|
||||
code = char.charCodeAt(0);
|
||||
}
|
||||
|
||||
if (screenReaderMode) {
|
||||
@@ -435,13 +444,6 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
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
|
||||
* Bell (Ctrl-G).
|
||||
@@ -1281,12 +1283,8 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._terminal.vt200Mouse = params[0] === 1000;
|
||||
this._terminal.normalMouse = params[0] > 1000;
|
||||
this._terminal.mouseEvents = true;
|
||||
if (this._terminal.element) {
|
||||
this._terminal.element.classList.add('enable-mouse-events');
|
||||
}
|
||||
if (this._terminal.selectionManager) {
|
||||
this._terminal.selectionManager.disable();
|
||||
}
|
||||
this._terminal.element.classList.add('enable-mouse-events');
|
||||
this._terminal.selectionManager.disable();
|
||||
this._terminal.log('Binding to mouse events.');
|
||||
break;
|
||||
case 1004: // send focusin/focusout events
|
||||
@@ -1473,12 +1471,8 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._terminal.vt200Mouse = false;
|
||||
this._terminal.normalMouse = false;
|
||||
this._terminal.mouseEvents = false;
|
||||
if (this._terminal.element) {
|
||||
this._terminal.element.classList.remove('enable-mouse-events');
|
||||
}
|
||||
if (this._terminal.selectionManager) {
|
||||
this._terminal.selectionManager.enable();
|
||||
}
|
||||
this._terminal.element.classList.remove('enable-mouse-events');
|
||||
this._terminal.selectionManager.enable();
|
||||
break;
|
||||
case 1004: // send focusin/focusout events
|
||||
this._terminal.sendFocus = false;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { assert } from 'chai';
|
||||
import { IMouseZoneManager, IMouseZone } from './ui/Types';
|
||||
import { ILinkMatcher, ITerminal, IBufferLine } from './Types';
|
||||
import { Linkifier } from './Linkifier';
|
||||
import { MockBuffer, MockTerminal, TestTerminal } from './ui/TestUtils.test';
|
||||
import { MockBuffer, MockTerminal, TestTerminal } from './utils/TestUtils.test';
|
||||
import { CircularList } from './common/CircularList';
|
||||
import { BufferLine } from './BufferLine';
|
||||
|
||||
@@ -41,8 +41,8 @@ describe('Linkifier', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
terminal = new MockTerminal();
|
||||
(terminal as any).cols = 100;
|
||||
(terminal as any).rows = 10;
|
||||
terminal.cols = 100;
|
||||
terminal.rows = 10;
|
||||
terminal.buffer = new MockBuffer();
|
||||
(<MockBuffer>terminal.buffer).setLines(new CircularList<IBufferLine>(20));
|
||||
terminal.buffer.ydisp = 0;
|
||||
@@ -65,7 +65,7 @@ describe('Linkifier', () => {
|
||||
function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, links: {x: number, length: number}[], done: MochaDone): void {
|
||||
addRow(rowText);
|
||||
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
|
||||
(terminal as any).rows = terminal.buffer.lines.length - 1;
|
||||
terminal.rows = terminal.buffer.lines.length - 1;
|
||||
linkifier.linkifyRows();
|
||||
// Allow linkify to happen
|
||||
setTimeout(() => {
|
||||
@@ -142,19 +142,19 @@ describe('Linkifier', () => {
|
||||
});
|
||||
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 => {
|
||||
(terminal as any).cols = 4;
|
||||
terminal.cols = 4;
|
||||
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 => {
|
||||
(terminal as any).cols = 4;
|
||||
terminal.cols = 4;
|
||||
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 => {
|
||||
(terminal as any).cols = 4;
|
||||
terminal.cols = 4;
|
||||
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 => {
|
||||
(terminal as any).cols = 4;
|
||||
terminal.cols = 4;
|
||||
assertLinkifiesMultiLineLink('123456789', /56789/, [{x1: 0, x2: 1, y1: 1, y2: 2}], done);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -219,17 +219,9 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
// also correct regex and string search offsets for the next loop run
|
||||
stringIndex = text.indexOf(uri, stringIndex + 1);
|
||||
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]);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { SelectionManager, SelectionMode } from './SelectionManager';
|
||||
import { SelectionModel } from './SelectionModel';
|
||||
import { BufferSet } from './BufferSet';
|
||||
import { ITerminal, IBuffer, IBufferLine } from './Types';
|
||||
import { MockTerminal } from './ui/TestUtils.test';
|
||||
import { MockTerminal } from './utils/TestUtils.test';
|
||||
import { BufferLine } from './BufferLine';
|
||||
|
||||
class TestMockTerminal extends MockTerminal {
|
||||
@@ -45,8 +45,8 @@ describe('SelectionManager', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
terminal = new TestMockTerminal();
|
||||
(terminal as any).cols = 80;
|
||||
(terminal as any).rows = 2;
|
||||
terminal.cols = 80;
|
||||
terminal.rows = 2;
|
||||
terminal.options.scrollback = 100;
|
||||
terminal.buffers = new BufferSet(terminal);
|
||||
terminal.buffer = terminal.buffers.active;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { ITerminal, ISelectionManager, IBuffer, CharData, IBufferLine } from './Types';
|
||||
import { XtermListener } from './common/Types';
|
||||
import { MouseHelper } from './ui/MouseHelper';
|
||||
import { MouseHelper } from './utils/MouseHelper';
|
||||
import * as Browser from './core/Platform';
|
||||
import { CharMeasure } from './ui/CharMeasure';
|
||||
import { EventEmitter } from './common/EventEmitter';
|
||||
@@ -198,7 +198,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
}
|
||||
} else {
|
||||
// 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));
|
||||
|
||||
// Get middle rows
|
||||
@@ -552,7 +552,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
*/
|
||||
private _onMouseMove(event: MouseEvent): void {
|
||||
// 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.
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { assert } from 'chai';
|
||||
import { ITerminal } from './Types';
|
||||
import { SelectionModel } from './SelectionModel';
|
||||
import { BufferSet } from './BufferSet';
|
||||
import { MockTerminal } from './ui/TestUtils.test';
|
||||
import { MockTerminal } from './utils/TestUtils.test';
|
||||
|
||||
class TestSelectionModel extends SelectionModel {
|
||||
constructor(
|
||||
@@ -23,8 +23,8 @@ describe('SelectionManager', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
terminal = new MockTerminal();
|
||||
(terminal as any).cols = 80;
|
||||
(terminal as any).rows = 2;
|
||||
terminal.cols = 80;
|
||||
terminal.rows = 2;
|
||||
terminal.options.scrollback = 10;
|
||||
terminal.buffers = new BufferSet(terminal);
|
||||
terminal.buffer = terminal.buffers.active;
|
||||
|
||||
@@ -13,7 +13,7 @@ import * as path from 'path';
|
||||
import * as pty from 'node-pty';
|
||||
import { assert } from 'chai';
|
||||
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';
|
||||
|
||||
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++) {
|
||||
lineText = '';
|
||||
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).get(cell)[CHAR_DATA_CHAR_INDEX];
|
||||
}
|
||||
// rtrim empty cells as xterm does
|
||||
lineText = lineText.replace(/\s+$/, '');
|
||||
|
||||
+22
-22
@@ -5,7 +5,7 @@
|
||||
|
||||
import { assert, expect } from 'chai';
|
||||
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';
|
||||
|
||||
const INIT_COLS = 80;
|
||||
@@ -462,7 +462,7 @@ describe('term.js addons', () => {
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS + 1);
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS).get(0)[CHAR_DATA_CHAR_INDEX], '');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS).get(0)[CHAR_DATA_CHAR_INDEX], ' ');
|
||||
});
|
||||
|
||||
it('should properly scroll inside a scroll region (scrollTop set)', () => {
|
||||
@@ -491,7 +491,7 @@ describe('term.js addons', () => {
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
|
||||
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
|
||||
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
|
||||
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], '', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(5).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
|
||||
});
|
||||
|
||||
@@ -509,7 +509,7 @@ describe('term.js addons', () => {
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer');
|
||||
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
|
||||
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], '', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
|
||||
});
|
||||
});
|
||||
@@ -530,9 +530,9 @@ describe('term.js addons', () => {
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||
// 'a' gets pushed out of buffer
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], '');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], ' ');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 2).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX], '');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX], ' ');
|
||||
});
|
||||
|
||||
it('should properly scroll inside a scroll region (scrollTop set)', () => {
|
||||
@@ -560,7 +560,7 @@ describe('term.js addons', () => {
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
|
||||
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
|
||||
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], '', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
|
||||
});
|
||||
|
||||
@@ -578,7 +578,7 @@ describe('term.js addons', () => {
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer');
|
||||
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
|
||||
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], '', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
|
||||
});
|
||||
});
|
||||
@@ -776,7 +776,7 @@ describe('term.js addons', () => {
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
term.reset();
|
||||
}
|
||||
});
|
||||
@@ -787,7 +787,7 @@ describe('term.js addons', () => {
|
||||
term.write(high + String.fromCharCode(i));
|
||||
expect(term.buffer.lines.get(0).get(term.buffer.x - 1)[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
||||
expect(term.buffer.lines.get(0).get(term.buffer.x - 1)[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
term.reset();
|
||||
}
|
||||
});
|
||||
@@ -800,7 +800,7 @@ describe('term.js addons', () => {
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('a');
|
||||
expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
||||
expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(term.buffer.lines.get(1).get(1)[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(term.buffer.lines.get(1).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
term.reset();
|
||||
}
|
||||
});
|
||||
@@ -813,7 +813,7 @@ describe('term.js addons', () => {
|
||||
// auto wraparound mode should cut off the rest of the line
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('a');
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(term.buffer.lines.get(1).get(1)[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(term.buffer.lines.get(1).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
term.reset();
|
||||
}
|
||||
});
|
||||
@@ -826,7 +826,7 @@ describe('term.js addons', () => {
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
term.reset();
|
||||
}
|
||||
});
|
||||
@@ -845,8 +845,8 @@ describe('term.js addons', () => {
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
});
|
||||
it('multiple combined é', () => {
|
||||
@@ -928,8 +928,8 @@ describe('term.js addons', () => {
|
||||
}
|
||||
}
|
||||
let tchar = term.buffer.lines.get(0).get(term.cols - 1);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
tchar = term.buffer.lines.get(1).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
@@ -953,8 +953,8 @@ describe('term.js addons', () => {
|
||||
}
|
||||
}
|
||||
let tchar = term.buffer.lines.get(0).get(term.cols - 1);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
tchar = term.buffer.lines.get(1).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301');
|
||||
@@ -998,8 +998,8 @@ describe('term.js addons', () => {
|
||||
}
|
||||
}
|
||||
let tchar = term.buffer.lines.get(0).get(term.cols - 1);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
tchar = term.buffer.lines.get(1).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301');
|
||||
@@ -1063,7 +1063,7 @@ describe('term.js addons', () => {
|
||||
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
||||
expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('a');
|
||||
expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql(''); // fullwidth char got replaced
|
||||
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql(' '); // fullwidth char got replaced
|
||||
term.write('b');
|
||||
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
||||
expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('b');
|
||||
|
||||
+46
-59
@@ -39,7 +39,8 @@ import { CharMeasure } from './ui/CharMeasure';
|
||||
import * as Browser from './core/Platform';
|
||||
import { addDisposableDomListener } from './ui/Lifecycle';
|
||||
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_ANSI_COLORS } from './renderer/ColorManager';
|
||||
import { MouseZoneManager } from './ui/MouseZoneManager';
|
||||
@@ -51,7 +52,6 @@ import { DomRenderer } from './renderer/dom/DomRenderer';
|
||||
import { IKeyboardEvent } from './common/Types';
|
||||
import { evaluateKeyboardEvent } from './core/input/Keyboard';
|
||||
import { KeyboardResultType, ICharset } from './core/Types';
|
||||
import { clone } from './common/Clone';
|
||||
|
||||
// Let it work inside Node.js for automated testing purposes.
|
||||
const document = (typeof window !== 'undefined') ? window.document : null;
|
||||
@@ -64,15 +64,10 @@ const document = (typeof window !== 'undefined') ? window.document : null;
|
||||
const WRITE_BUFFER_PAUSE_THRESHOLD = 5;
|
||||
|
||||
/**
|
||||
* The max number of ms to spend on writes before allowing the renderer to
|
||||
* catch up with a 0ms setTimeout. A value of < 33 to keep us close to
|
||||
* 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.
|
||||
* The number of writes to perform in a single batch before allowing the
|
||||
* renderer to catch up with a 0ms setTimeout.
|
||||
*/
|
||||
const WRITE_TIMEOUT_MS = 12;
|
||||
|
||||
const MINIMUM_COLS = 2; // Less than 2 can mess with wide chars
|
||||
const MINIMUM_ROWS = 1;
|
||||
const WRITE_BATCH_SIZE = 300;
|
||||
|
||||
/**
|
||||
* The set of options that only have an effect when set in the Terminal constructor.
|
||||
@@ -110,7 +105,8 @@ const DEFAULT_OPTIONS: ITerminalOptions = {
|
||||
tabStopWidth: 8,
|
||||
theme: null,
|
||||
rightClickSelectsWord: Browser.isMac,
|
||||
rendererType: 'canvas'
|
||||
rendererType: 'canvas',
|
||||
experimentalBufferLineImpl: 'TypedArray'
|
||||
};
|
||||
|
||||
export class Terminal extends EventEmitter implements ITerminal, IDisposable, IInputHandlingTerminal {
|
||||
@@ -267,8 +263,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
// TODO: WHy not document.body?
|
||||
this._parent = document ? document.body : null;
|
||||
|
||||
this.cols = Math.max(this.options.cols, MINIMUM_COLS);
|
||||
this.rows = Math.max(this.options.rows, MINIMUM_ROWS);
|
||||
this.cols = this.options.cols;
|
||||
this.rows = this.options.rows;
|
||||
|
||||
if (this.options.handler) {
|
||||
this.on('data', this.options.handler);
|
||||
@@ -468,7 +464,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
this.renderer.onResize(this.cols, this.rows);
|
||||
this.refresh(0, this.rows - 1);
|
||||
}
|
||||
break;
|
||||
case 'rendererType':
|
||||
if (this.renderer) {
|
||||
this.unregister(this.renderer);
|
||||
@@ -501,6 +496,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
}
|
||||
break;
|
||||
case 'tabStopWidth': this.buffers.setupTabStops(); break;
|
||||
case 'experimentalBufferLineImpl':
|
||||
this.buffers.normal.setBufferLineFactory(value);
|
||||
this.buffers.alt.setBufferLineFactory(value);
|
||||
this._blankLine = null;
|
||||
break;
|
||||
}
|
||||
// Inform renderer of changes
|
||||
if (this.renderer) {
|
||||
@@ -568,12 +568,12 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
// Firefox doesn't appear to fire the contextmenu event on right click
|
||||
this.register(addDisposableDomListener(this.element, 'mousedown', (event: MouseEvent) => {
|
||||
if (event.button === 2) {
|
||||
rightClickHandler(event, this, this.selectionManager, this.options.rightClickSelectsWord);
|
||||
rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord);
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
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 +585,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
// that the regular click event doesn't fire for the middle mouse button.
|
||||
this.register(addDisposableDomListener(this.element, 'auxclick', (event: MouseEvent) => {
|
||||
if (event.button === 1) {
|
||||
moveTextAreaUnderMouseCursor(event, this);
|
||||
moveTextAreaUnderMouseCursor(event, this.textarea);
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -738,13 +738,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this.selectionManager.refresh()));
|
||||
|
||||
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) {
|
||||
// Note that this must be done *after* the renderer is created in order to
|
||||
@@ -1186,12 +1179,17 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
*/
|
||||
public scroll(isWrapped: boolean = false): void {
|
||||
let newLine: IBufferLine;
|
||||
newLine = this._blankLine;
|
||||
if (!newLine || newLine.length !== this.cols || newLine.get(0)[CHAR_DATA_ATTR_INDEX] !== this.eraseAttr()) {
|
||||
const useRecycling = this.options.experimentalBufferLineImpl !== 'JsArray';
|
||||
if (useRecycling) {
|
||||
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;
|
||||
} else {
|
||||
newLine = this.buffer.getBlankLine(this.eraseAttr(), isWrapped);
|
||||
this._blankLine = newLine;
|
||||
}
|
||||
newLine.isWrapped = isWrapped;
|
||||
|
||||
const topRow = this.buffer.ybase + this.buffer.scrollTop;
|
||||
const bottomRow = this.buffer.ybase + this.buffer.scrollBottom;
|
||||
@@ -1202,13 +1200,17 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
|
||||
// Insert the line using the fastest method
|
||||
if (bottomRow === this.buffer.lines.length - 1) {
|
||||
if (willBufferBeTrimmed) {
|
||||
this.buffer.lines.recycle().copyFrom(newLine);
|
||||
if (useRecycling) {
|
||||
if (willBufferBeTrimmed) {
|
||||
this.buffer.lines.recycle().copyFrom(newLine);
|
||||
} else {
|
||||
this.buffer.lines.push(newLine.clone());
|
||||
}
|
||||
} else {
|
||||
this.buffer.lines.push(newLine.clone());
|
||||
this.buffer.lines.push(newLine);
|
||||
}
|
||||
} else {
|
||||
this.buffer.lines.splice(bottomRow + 1, 0, newLine.clone());
|
||||
this.buffer.lines.splice(bottomRow + 1, 0, (useRecycling) ? newLine.clone() : newLine);
|
||||
}
|
||||
|
||||
// Only adjust ybase and ydisp when the buffer is not trimmed
|
||||
@@ -1230,7 +1232,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
// scrollback, instead we can just shift them in-place.
|
||||
const scrollRegionHeight = bottomRow - topRow + 1/*as it's zero-based*/;
|
||||
this.buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1);
|
||||
this.buffer.lines.set(bottomRow, newLine.clone());
|
||||
this.buffer.lines.set(bottomRow, (useRecycling) ? newLine.clone() : newLine);
|
||||
}
|
||||
|
||||
// Move the viewport to the bottom of the buffer unless the user is
|
||||
@@ -1350,20 +1352,19 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
}
|
||||
}
|
||||
|
||||
protected _innerWrite(bufferOffset: number = 0): void {
|
||||
protected _innerWrite(): void {
|
||||
// Ensure the terminal isn't disposed
|
||||
if (this._isDisposed) {
|
||||
this.writeBuffer = [];
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
while (this.writeBuffer.length > bufferOffset) {
|
||||
const data = this.writeBuffer[bufferOffset];
|
||||
bufferOffset++;
|
||||
const writeBatch = this.writeBuffer.splice(0, WRITE_BATCH_SIZE);
|
||||
while (writeBatch.length > 0) {
|
||||
const data = writeBatch.shift();
|
||||
|
||||
// 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.
|
||||
if (this._xoffSentToCatchUp && this.writeBuffer.length === bufferOffset) {
|
||||
// the writeBuffer is empty to allow more data to come in.
|
||||
if (this._xoffSentToCatchUp && writeBatch.length === 0 && this.writeBuffer.length === 0) {
|
||||
this.handler(C0.DC1);
|
||||
this._xoffSentToCatchUp = false;
|
||||
}
|
||||
@@ -1381,17 +1382,12 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
|
||||
this.updateRange(this.buffer.y);
|
||||
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
|
||||
setTimeout(() => this._innerWrite(bufferOffset), 0);
|
||||
setTimeout(() => this._innerWrite(), 0);
|
||||
} else {
|
||||
this._writeInProgress = false;
|
||||
this.writeBuffer = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1409,22 +1405,13 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
* processed by the terminal and what keys should not.
|
||||
* @param customKeyEventHandler The custom KeyboardEvent handler to attach.
|
||||
* 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.
|
||||
*/
|
||||
public attachCustomKeyEventHandler(customKeyEventHandler: CustomKeyEventHandler): void {
|
||||
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
|
||||
* handled.
|
||||
@@ -1709,8 +1696,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
return;
|
||||
}
|
||||
|
||||
if (x < MINIMUM_COLS) x = MINIMUM_COLS;
|
||||
if (y < MINIMUM_ROWS) y = MINIMUM_ROWS;
|
||||
if (x < 1) x = 1;
|
||||
if (y < 1) y = 1;
|
||||
|
||||
this.buffers.resize(x, y);
|
||||
|
||||
|
||||
+15
-23
@@ -111,7 +111,7 @@ export interface ICompositionHelper {
|
||||
*/
|
||||
export interface IInputHandler {
|
||||
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 LF */ lineFeed(): void;
|
||||
@@ -237,7 +237,7 @@ export interface IBufferAccessor {
|
||||
}
|
||||
|
||||
export interface IElementAccessor {
|
||||
readonly element: HTMLElement;
|
||||
element: HTMLElement;
|
||||
}
|
||||
|
||||
export interface ILinkifierAccessor {
|
||||
@@ -245,7 +245,7 @@ export interface ILinkifierAccessor {
|
||||
}
|
||||
|
||||
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, colCount: number, rowCount: number, isSelection?: boolean): [number, number];
|
||||
getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, colCount: number, rowCount: number): { x: number, y: number };
|
||||
}
|
||||
|
||||
@@ -452,26 +452,18 @@ export interface IParsingState {
|
||||
* DCS handler signature for EscapeSequenceParser.
|
||||
* EscapeSequenceParser handles DCS commands via separate
|
||||
* 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
|
||||
* `EscapeSequenceParser`. Use it to initialize or reset
|
||||
* 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
|
||||
* parsed data got split, therefore you might have to collect
|
||||
* `data` until `unhook` is called.
|
||||
* Note: `data` is borrowed, if you cannot process the data
|
||||
* 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.
|
||||
* parsed string got splitted, therefore you might have to collect
|
||||
* `data` until `unhook` is called. `unhook` marks the end
|
||||
* of the current DCS sequence.
|
||||
*/
|
||||
export interface IDcsHandler {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -488,9 +480,9 @@ export interface IEscapeSequenceParser extends IDisposable {
|
||||
* Parse string `data`.
|
||||
* @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;
|
||||
|
||||
setExecuteHandler(flag: string, callback: () => void): void;
|
||||
@@ -500,8 +492,6 @@ export interface IEscapeSequenceParser extends IDisposable {
|
||||
setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void;
|
||||
clearCsiHandler(flag: string): 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;
|
||||
clearEscHandler(collectAndFlag: string): void;
|
||||
@@ -530,10 +520,12 @@ export interface IBufferLine {
|
||||
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;
|
||||
resize(cols: number, fill: CharData, shrink?: boolean): void;
|
||||
fill(fillCharData: CharData): void;
|
||||
copyFrom(line: IBufferLine): void;
|
||||
clone(): IBufferLine;
|
||||
getTrimmedLength(): number;
|
||||
translateToString(trimRight?: boolean, startCol?: number, endCol?: number): string;
|
||||
}
|
||||
|
||||
export interface IBufferLineConstructor {
|
||||
new(cols: number, fillCharData?: CharData, isWrapped?: boolean): IBufferLine;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ export function toggleFullScreen(term: Terminal, fullscreen: boolean): void {
|
||||
fn = term.element.classList.add;
|
||||
}
|
||||
|
||||
fn = fn.bind(term.element.classList);
|
||||
fn('fullscreen');
|
||||
}
|
||||
|
||||
|
||||
@@ -25,11 +25,6 @@ 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 {
|
||||
|
||||
@@ -4,24 +4,16 @@
|
||||
*/
|
||||
|
||||
import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult } from './Interfaces';
|
||||
|
||||
const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?';
|
||||
const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs
|
||||
const nonWordCharacters = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?';
|
||||
|
||||
/**
|
||||
* A class that knows how to search the terminal and how to display the results.
|
||||
*/
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,47 +24,32 @@ export class SearchHelper implements ISearchHelper {
|
||||
* @return Whether a result was found.
|
||||
*/
|
||||
public findNext(term: string, searchOptions?: ISearchOptions): boolean {
|
||||
const selectionManager = this._terminal._core.selectionManager;
|
||||
const {incremental} = searchOptions;
|
||||
let result: ISearchResult;
|
||||
|
||||
if (!term || term.length === 0) {
|
||||
selectionManager.clearSelection();
|
||||
return false;
|
||||
}
|
||||
|
||||
let startCol: number = 0;
|
||||
let result: ISearchResult;
|
||||
|
||||
let startRow = this._terminal._core.buffer.ydisp;
|
||||
|
||||
if (selectionManager.selectionEnd) {
|
||||
if (this._terminal._core.selectionManager.selectionEnd) {
|
||||
// Start from the selection end if there is a selection
|
||||
// For incremental search, use existing row
|
||||
if (this._terminal.getSelection().length !== 0) {
|
||||
startRow = incremental ? selectionManager.selectionStart[1] : selectionManager.selectionEnd[1];
|
||||
startCol = incremental ? selectionManager.selectionStart[0] : selectionManager.selectionEnd[0];
|
||||
startRow = this._terminal._core.selectionManager.selectionEnd[1];
|
||||
}
|
||||
}
|
||||
|
||||
this._initLinesCache();
|
||||
|
||||
// Search startRow
|
||||
result = this._findInLine(term, startRow, startCol, searchOptions);
|
||||
|
||||
// Search from startRow + 1 to end
|
||||
if (!result) {
|
||||
for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) {
|
||||
result = this._findInLine(term, y, 0, searchOptions);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
// Search from ydisp + 1 to end
|
||||
for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) {
|
||||
result = this._findInLine(term, y, searchOptions);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Search from the top to the startRow (search the whole startRow again in
|
||||
// case startCol > 0)
|
||||
// Search from the top to the current ydisp
|
||||
if (!result) {
|
||||
for (let y = 0; y <= startRow; y++) {
|
||||
result = this._findInLine(term, y, 0, searchOptions);
|
||||
for (let y = 0; y < startRow; y++) {
|
||||
result = this._findInLine(term, y, searchOptions);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
@@ -91,47 +68,32 @@ export class SearchHelper implements ISearchHelper {
|
||||
* @return Whether a result was found.
|
||||
*/
|
||||
public findPrevious(term: string, searchOptions?: ISearchOptions): boolean {
|
||||
const selectionManager = this._terminal._core.selectionManager;
|
||||
let result: ISearchResult;
|
||||
|
||||
if (!term || term.length === 0) {
|
||||
selectionManager.clearSelection();
|
||||
return false;
|
||||
}
|
||||
|
||||
const isReverseSearch = true;
|
||||
let result: ISearchResult;
|
||||
|
||||
let startRow = this._terminal._core.buffer.ydisp;
|
||||
let startCol: number = this._terminal._core.buffer.lines.get(startRow).length;
|
||||
|
||||
if (selectionManager.selectionStart) {
|
||||
// Start from the selection start if there is a selection
|
||||
if (this._terminal._core.selectionManager.selectionStart) {
|
||||
// Start from the selection end if there is a selection
|
||||
if (this._terminal.getSelection().length !== 0) {
|
||||
startRow = selectionManager.selectionStart[1];
|
||||
startCol = selectionManager.selectionStart[0];
|
||||
startRow = this._terminal._core.selectionManager.selectionStart[1];
|
||||
}
|
||||
}
|
||||
|
||||
this._initLinesCache();
|
||||
|
||||
// Search startRow
|
||||
result = this._findInLine(term, startRow, startCol, searchOptions, isReverseSearch);
|
||||
|
||||
// Search from startRow - 1 to top
|
||||
if (!result) {
|
||||
for (let y = startRow - 1; y >= 0; y--) {
|
||||
result = this._findInLine(term, y, this._terminal._core.buffer.lines.get(y).length, searchOptions, isReverseSearch);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
// Search from ydisp + 1 to end
|
||||
for (let y = startRow - 1; y >= 0; y--) {
|
||||
result = this._findInLine(term, y, searchOptions);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Search from the bottom to startRow (search the whole startRow again in
|
||||
// case startCol > 0)
|
||||
// Search from the top to the current ydisp
|
||||
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);
|
||||
for (let y = this._terminal._core.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) {
|
||||
result = this._findInLine(term, y, searchOptions);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
@@ -142,28 +104,6 @@ export class SearchHelper implements ISearchHelper {
|
||||
return this._selectResult(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a line cache with a ttl
|
||||
*/
|
||||
private _initLinesCache(): void {
|
||||
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
|
||||
@@ -171,8 +111,8 @@ export class SearchHelper implements ISearchHelper {
|
||||
* @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)));
|
||||
return (((searchIndex === 0) || (nonWordCharacters.indexOf(line[searchIndex - 1]) !== -1)) &&
|
||||
(((searchIndex + term.length) === line.length) || (nonWordCharacters.indexOf(line[searchIndex + term.length]) !== -1)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,88 +121,65 @@ export class SearchHelper implements ISearchHelper {
|
||||
* 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 y The line to search.
|
||||
* @param searchOptions Search options.
|
||||
* @return The search result if it was found.
|
||||
*/
|
||||
protected _findInLine(term: string, row: number, col: number, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult {
|
||||
if (this._terminal._core.buffer.lines.get(row).isWrapped) {
|
||||
protected _findInLine(term: string, y: number, searchOptions: ISearchOptions = {}): ISearchResult {
|
||||
if (this._terminal._core.buffer.lines.get(y).isWrapped) {
|
||||
return;
|
||||
}
|
||||
|
||||
let stringLine = this._linesCache ? this._linesCache[row] : void 0;
|
||||
if (stringLine === void 0) {
|
||||
stringLine = this.translateBufferLineToStringWithWrap(row, true);
|
||||
if (this._linesCache) {
|
||||
this._linesCache[row] = stringLine;
|
||||
}
|
||||
}
|
||||
|
||||
const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();
|
||||
const stringLine = this.translateBufferLineToStringWithWrap(y, true);
|
||||
const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
|
||||
const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();
|
||||
let searchIndex = -1;
|
||||
|
||||
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];
|
||||
}
|
||||
const foundTerm = searchRegex.exec(searchStringLine);
|
||||
if (foundTerm && foundTerm[0].length > 0) {
|
||||
searchIndex = 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);
|
||||
}
|
||||
searchIndex = searchStringLine.indexOf(searchTerm);
|
||||
}
|
||||
|
||||
if (resultIndex >= 0) {
|
||||
if (searchIndex >= 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 (searchIndex >= this._terminal.cols) {
|
||||
y += Math.floor(searchIndex / this._terminal.cols);
|
||||
searchIndex = searchIndex % this._terminal.cols;
|
||||
}
|
||||
if (searchOptions.wholeWord && !this._isWholeWord(resultIndex, searchStringLine, term)) {
|
||||
if (searchOptions.wholeWord && !this._isWholeWord(searchIndex, searchStringLine, term)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const line = this._terminal._core.buffer.lines.get(row);
|
||||
const line = this._terminal._core.buffer.lines.get(y);
|
||||
|
||||
for (let i = 0; i < resultIndex; i++) {
|
||||
for (let i = 0; i < searchIndex; i++) {
|
||||
const charData = line.get(i);
|
||||
// Adjust the searchIndex to normalize emoji into single chars
|
||||
const char = charData[1/*CHAR_DATA_CHAR_INDEX*/];
|
||||
if (char.length > 1) {
|
||||
resultIndex -= char.length - 1;
|
||||
searchIndex -= char.length - 1;
|
||||
}
|
||||
// Adjust the searchIndex for empty characters following wide unicode
|
||||
// chars (eg. CJK)
|
||||
const charWidth = charData[2/*CHAR_DATA_WIDTH_INDEX*/];
|
||||
if (charWidth === 0) {
|
||||
resultIndex++;
|
||||
searchIndex++;
|
||||
}
|
||||
}
|
||||
return {
|
||||
term,
|
||||
col: resultIndex,
|
||||
row
|
||||
col: searchIndex,
|
||||
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
|
||||
|
||||
@@ -29,11 +29,8 @@ class MockTerminal {
|
||||
}
|
||||
|
||||
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);
|
||||
public findInLine(term: string, y: number, searchOptions?: ISearchOptions): ISearchResult {
|
||||
return this._findInLine(term, y, searchOptions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +118,8 @@ describe('search addon', () => {
|
||||
it('should not select empty lines', () => {
|
||||
search.apply(<any>MockTerminal);
|
||||
const term = new MockTerminal({cols: 20, rows: 3});
|
||||
term.core.write(' ');
|
||||
term.pushWriteData();
|
||||
const line = term.searchHelper.findInLine('^.*$', 0, { regex: true });
|
||||
expect(line).eql(undefined);
|
||||
});
|
||||
@@ -248,66 +247,5 @@ describe('search addon', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"es5"
|
||||
],
|
||||
"rootDir": ".",
|
||||
|
||||
@@ -63,28 +63,4 @@ describe('webLinks addon', () => {
|
||||
|
||||
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/');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
|
||||
const localHostClause = '(localhost)';
|
||||
const portClause = '(:\\d{1,5})';
|
||||
const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
|
||||
const pathClause = '(\\/[\\/\\w\\.\\-%~:]*)*([^:"\'\\s])';
|
||||
const pathClause = '(\\/[\\/\\w\\.\\-%~:]*)*([^:\\s])';
|
||||
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
|
||||
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
|
||||
const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
|
||||
|
||||
@@ -7,8 +7,7 @@ import { Terminal } from 'xterm';
|
||||
import { IWinptyCompatAddonTerminal } from './Interfaces';
|
||||
|
||||
const CHAR_DATA_CODE_INDEX = 3;
|
||||
const NULL_CELL_CODE = 0;
|
||||
const WHITESPACE_CELL_CODE = 32;
|
||||
const NULL_CELL_CODE = 32;
|
||||
|
||||
export function winptyCompatInit(terminal: Terminal): void {
|
||||
const addonTerminal = <IWinptyCompatAddonTerminal>terminal;
|
||||
@@ -19,8 +18,6 @@ export function winptyCompatInit(terminal: Terminal): void {
|
||||
return;
|
||||
}
|
||||
|
||||
(addonTerminal._core as any).isWinptyCompatEnabled = true;
|
||||
|
||||
// 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
|
||||
// retaining the wrapped new line characters or if consumers are listening
|
||||
@@ -35,7 +32,7 @@ export function winptyCompatInit(terminal: Terminal): void {
|
||||
const line = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y - 1);
|
||||
const lastChar = line.get(addonTerminal.cols - 1);
|
||||
|
||||
if (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE && lastChar[CHAR_DATA_CODE_INDEX] !== WHITESPACE_CELL_CODE) {
|
||||
if (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE) {
|
||||
const nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y);
|
||||
nextLine.isWrapped = true;
|
||||
}
|
||||
|
||||
+21
-29
@@ -6,16 +6,6 @@
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
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,
|
||||
* overriding values at the start of the list.
|
||||
@@ -101,7 +91,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
|
||||
this._array[this._getCyclicIndex(this._length)] = value;
|
||||
if (this._length === this._maxLength) {
|
||||
this._startIndex = ++this._startIndex % this._maxLength;
|
||||
this.emitMayRemoveListeners('trim', 1);
|
||||
this.emit('trim', 1);
|
||||
} else {
|
||||
this._length++;
|
||||
}
|
||||
@@ -117,7 +107,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
|
||||
throw new Error('Can only recycle when the buffer is full');
|
||||
}
|
||||
this._startIndex = ++this._startIndex % this._maxLength;
|
||||
this.emitMayRemoveListeners('trim', 1);
|
||||
this.emit('trim', 1);
|
||||
return this._array[this._getCyclicIndex(this._length - 1)]!;
|
||||
}
|
||||
|
||||
@@ -154,22 +144,24 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
|
||||
this._length -= deleteCount;
|
||||
}
|
||||
|
||||
// Add items
|
||||
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];
|
||||
}
|
||||
if (items && items.length) {
|
||||
// Add items
|
||||
for (let i = this._length - 1; i >= start; i--) {
|
||||
this._array[this._getCyclicIndex(i + items.length)] = this._array[this._getCyclicIndex(i)];
|
||||
}
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
this._array[this._getCyclicIndex(start + i)] = items[i];
|
||||
}
|
||||
|
||||
// Adjust length as needed
|
||||
if (this._length + items.length > this._maxLength) {
|
||||
const countToTrim = (this._length + items.length) - this._maxLength;
|
||||
this._startIndex += countToTrim;
|
||||
this._length = this._maxLength;
|
||||
this.emitMayRemoveListeners('trim', countToTrim);
|
||||
} else {
|
||||
this._length += items.length;
|
||||
// Adjust length as needed
|
||||
if (this._length + items.length > this._maxLength) {
|
||||
const countToTrim = (this._length + items.length) - this._maxLength;
|
||||
this._startIndex += countToTrim;
|
||||
this._length = this._maxLength;
|
||||
this.emit('trim', countToTrim);
|
||||
} else {
|
||||
this._length += items.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +175,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
|
||||
}
|
||||
this._startIndex += count;
|
||||
this._length -= count;
|
||||
this.emitMayRemoveListeners('trim', count);
|
||||
this.emit('trim', count);
|
||||
}
|
||||
|
||||
public shiftElements(start: number, count: number, offset: number): void {
|
||||
@@ -207,7 +199,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
|
||||
while (this._length > this._maxLength) {
|
||||
this._length--;
|
||||
this._startIndex++;
|
||||
this.emitMayRemoveListeners('trim', 1);
|
||||
this.emit('trim', 1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -23,7 +23,7 @@ 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 handler The handler for the listener.
|
||||
*/
|
||||
@@ -75,19 +75,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[] {
|
||||
return this._events[type] || [];
|
||||
}
|
||||
|
||||
@@ -3,20 +3,21 @@
|
||||
* @license MIT
|
||||
*/
|
||||
import { assert } from 'chai';
|
||||
import { fillFallback, concat } from './TypedArrayUtils';
|
||||
import { fillFallback } 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 {
|
||||
|
||||
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('TypedArray.fill', function(): void {
|
||||
it('should work with all typed array types', function(): void {
|
||||
const u81 = new Uint8Array(5);
|
||||
@@ -86,12 +87,3 @@ describe('polyfill conformance tests', function(): void {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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]));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
* @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.
|
||||
*/
|
||||
|
||||
type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray
|
||||
| Int8Array | Int16Array | Int32Array
|
||||
| Float32Array | Float64Array;
|
||||
|
||||
export function fill<T extends TypedArray>(array: T, value: number, start?: number, end?: number): T {
|
||||
// all modern engines that support .fill
|
||||
if (array.fill) {
|
||||
@@ -39,14 +39,3 @@ export function fillFallback<T extends TypedArray>(array: T, value: number, star
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -281,16 +281,5 @@ describe('Keyboard', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputDownArrow' }).key, '\x1b[B');
|
||||
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, '!');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,7 +44,7 @@ export function evaluateKeyboardEvent(
|
||||
): IKeyboardResult {
|
||||
const result: IKeyboardResult = {
|
||||
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
|
||||
cancel: false,
|
||||
// The new key even to emit
|
||||
@@ -349,9 +349,6 @@ export function evaluateKeyboardEvent(
|
||||
if (ev.keyCode === 65) { // cmd + a
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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, 'Ä€𝄞Ö𝄞€Ü𝄞€');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -59,12 +59,6 @@ export class Terminal implements ITerminalApi {
|
||||
public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
|
||||
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 {
|
||||
return this._core.registerLinkMatcher(regex, handler, options);
|
||||
}
|
||||
|
||||
@@ -236,12 +236,12 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
*/
|
||||
protected fillCharTrueColor(terminal: ITerminal, charData: CharData, x: number, y: number): void {
|
||||
this._ctx.font = this._getFont(terminal, false, false);
|
||||
this._ctx.textBaseline = 'middle';
|
||||
this._ctx.textBaseline = 'top';
|
||||
this._clipRow(terminal, y);
|
||||
this._ctx.fillText(
|
||||
charData[CHAR_DATA_CHAR_INDEX],
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2);
|
||||
y * this._scaledCellHeight + this._scaledCharTop);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -295,7 +295,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
private _drawUncachedChars(terminal: ITerminal, chars: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean, italic: boolean): void {
|
||||
this._ctx.save();
|
||||
this._ctx.font = this._getFont(terminal, bold, italic);
|
||||
this._ctx.textBaseline = 'middle';
|
||||
this._ctx.textBaseline = 'top';
|
||||
|
||||
if (fg === INVERTED_DEFAULT_COLOR) {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
@@ -316,7 +316,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
this._ctx.fillText(
|
||||
chars,
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2);
|
||||
y * this._scaledCellHeight + this._scaledCharTop);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { MockTerminal, MockBuffer } from '../ui/TestUtils.test';
|
||||
import { MockTerminal, MockBuffer } from '../utils/TestUtils.test';
|
||||
import { CircularList } from '../common/CircularList';
|
||||
|
||||
import { ICharacterJoinerRegistry } from './Types';
|
||||
|
||||
@@ -70,7 +70,7 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
// Detect whether IntersectionObserver is detected and enable renderer pause
|
||||
// and resume based on terminal visibility if so
|
||||
if ('IntersectionObserver' in window) {
|
||||
const observer = new IntersectionObserver(e => this.onIntersectionChange(e[e.length - 1]), { threshold: 0 });
|
||||
const observer = new IntersectionObserver(e => this.onIntersectionChange(e[0]), { threshold: 0 });
|
||||
observer.observe(this._terminal.element);
|
||||
this.register({ dispose: () => observer.disconnect() });
|
||||
}
|
||||
|
||||
@@ -55,7 +55,6 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
||||
|
||||
// Selection does not exist
|
||||
if (!start || !end) {
|
||||
this._clearState();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from '../Buffer';
|
||||
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, NULL_CELL_CODE } from '../Buffer';
|
||||
import { FLAGS, IColorSet, IRenderDimensions, ICharacterJoinerRegistry } from './Types';
|
||||
import { CharData, ITerminal } from '../Types';
|
||||
import { INVERTED_DEFAULT_COLOR, DEFAULT_COLOR } from './atlas/Types';
|
||||
@@ -73,11 +73,11 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
const joinedRanges = joinerRegistry ? joinerRegistry.getJoinedCharacters(row) : [];
|
||||
for (let x = 0; x < terminal.cols; x++) {
|
||||
const charData = line.get(x);
|
||||
let code: number = <number>charData[CHAR_DATA_CODE_INDEX] || WHITESPACE_CELL_CODE;
|
||||
let code: number = <number>charData[CHAR_DATA_CODE_INDEX];
|
||||
|
||||
// Can either represent character(s) for a single cell or multiple cells
|
||||
// if indicated by a character joiner.
|
||||
let chars: string = charData[CHAR_DATA_CHAR_INDEX] || WHITESPACE_CELL_CHAR;
|
||||
let chars: string = charData[CHAR_DATA_CHAR_INDEX];
|
||||
const attr: number = charData[CHAR_DATA_ATTR_INDEX];
|
||||
let width: number = charData[CHAR_DATA_WIDTH_INDEX];
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@ export function acquireCharAtlas(
|
||||
): BaseCharAtlas {
|
||||
const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors);
|
||||
|
||||
// TODO: Currently if a terminal changes configs it will not free the entry reference (until it's disposed)
|
||||
|
||||
// Check to see if the terminal already owns this config
|
||||
for (let i = 0; i < charAtlasCache.length; i++) {
|
||||
const entry = charAtlasCache[i];
|
||||
@@ -52,7 +54,6 @@ export function acquireCharAtlas(
|
||||
}
|
||||
// The configs differ, release the terminal from the entry
|
||||
if (entry.ownedBy.length === 1) {
|
||||
entry.atlas.dispose();
|
||||
charAtlasCache.splice(i, 1);
|
||||
} else {
|
||||
entry.ownedBy.splice(ownedByIndex, 1);
|
||||
@@ -93,7 +94,6 @@ export function removeTerminalFromCache(terminal: ITerminal): void {
|
||||
if (index !== -1) {
|
||||
if (charAtlasCache[i].ownedBy.length === 1) {
|
||||
// Remove the cache entry if it's the only terminal
|
||||
charAtlasCache[i].atlas.dispose();
|
||||
charAtlasCache.splice(i, 1);
|
||||
} else {
|
||||
// Remove the reference from the cache entry
|
||||
|
||||
@@ -29,7 +29,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: (
|
||||
ctx.save();
|
||||
ctx.fillStyle = config.colors.foreground.css;
|
||||
ctx.font = getFont(config.fontWeight, config);
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.textBaseline = 'top';
|
||||
|
||||
// Default color
|
||||
for (let i = 0; i < 256; i++) {
|
||||
@@ -37,7 +37,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: (
|
||||
ctx.beginPath();
|
||||
ctx.rect(i * cellWidth, 0, cellWidth, cellHeight);
|
||||
ctx.clip();
|
||||
ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight / 2);
|
||||
ctx.fillText(String.fromCharCode(i), i * cellWidth, 0);
|
||||
ctx.restore();
|
||||
}
|
||||
// Default color bold
|
||||
@@ -48,7 +48,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: (
|
||||
ctx.beginPath();
|
||||
ctx.rect(i * cellWidth, cellHeight, cellWidth, cellHeight);
|
||||
ctx.clip();
|
||||
ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight * 1.5);
|
||||
ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight);
|
||||
ctx.restore();
|
||||
}
|
||||
ctx.restore();
|
||||
@@ -64,7 +64,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: (
|
||||
ctx.rect(i * cellWidth, y, cellWidth, cellHeight);
|
||||
ctx.clip();
|
||||
ctx.fillStyle = config.colors.ansi[colorIndex].css;
|
||||
ctx.fillText(String.fromCharCode(i), i * cellWidth, y + cellHeight / 2);
|
||||
ctx.fillText(String.fromCharCode(i), i * cellWidth, y);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: (
|
||||
ctx.rect(i * cellWidth, y, cellWidth, cellHeight);
|
||||
ctx.clip();
|
||||
ctx.fillStyle = config.colors.ansi[colorIndex].css;
|
||||
ctx.fillText(String.fromCharCode(i), i * cellWidth, y + cellHeight / 2);
|
||||
ctx.fillText(String.fromCharCode(i), i * cellWidth, y);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,7 +250,7 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
|
||||
const fontStyle = glyph.italic ? 'italic' : '';
|
||||
this._tmpCtx.font =
|
||||
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
|
||||
this._tmpCtx.textBaseline = 'middle';
|
||||
this._tmpCtx.textBaseline = 'top';
|
||||
|
||||
this._tmpCtx.fillStyle = this._getForegroundColor(glyph).css;
|
||||
|
||||
@@ -259,7 +259,7 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
|
||||
this._tmpCtx.globalAlpha = DIM_OPACITY;
|
||||
}
|
||||
// Draw the character
|
||||
this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight / 2);
|
||||
this._tmpCtx.fillText(glyph.chars, 0, 0);
|
||||
this._tmpCtx.restore();
|
||||
|
||||
// clear the background from the character to avoid issues with drawing over the previous
|
||||
|
||||
@@ -364,11 +364,9 @@ export class DomRenderer extends EventEmitter implements IRenderer {
|
||||
return;
|
||||
}
|
||||
const span = <HTMLElement>row.children[x];
|
||||
if (span) {
|
||||
span.style.textDecoration = enabled ? 'underline' : 'none';
|
||||
}
|
||||
if (++x >= cols) {
|
||||
x = 0;
|
||||
span.style.textDecoration = enabled ? 'underline' : 'none';
|
||||
x = (x + 1) % cols;
|
||||
if (x === 0) {
|
||||
y++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CHAR } from '../../Buffer';
|
||||
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, NULL_CELL_CODE } from '../../Buffer';
|
||||
import { FLAGS } from '../Types';
|
||||
import { IBufferLine } from '../../Types';
|
||||
import { DEFAULT_COLOR, INVERTED_DEFAULT_COLOR } from '../atlas/Types';
|
||||
@@ -41,7 +41,7 @@ export class DomRendererRowFactory {
|
||||
|
||||
for (let x = 0; x < lineLength; x++) {
|
||||
const charData = lineData.get(x);
|
||||
const char = charData[CHAR_DATA_CHAR_INDEX] || WHITESPACE_CELL_CHAR;
|
||||
const char = charData[CHAR_DATA_CHAR_INDEX];
|
||||
const attr = charData[CHAR_DATA_ATTR_INDEX];
|
||||
const width = charData[CHAR_DATA_WIDTH_INDEX];
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ export class CharMeasure extends EventEmitter implements ICharMeasure {
|
||||
}
|
||||
|
||||
public measure(options: ITerminalOptions): void {
|
||||
this._measureElement.style.fontFamily = options.fontFamily;
|
||||
this._measureElement.style.fontFamily = options.fontFamily;
|
||||
this._measureElement.style.fontSize = `${options.fontSize}px`;
|
||||
const geometry = this._measureElement.getBoundingClientRect();
|
||||
// The element is likely currently display:none, we should retain the
|
||||
@@ -46,10 +46,9 @@ export class CharMeasure extends EventEmitter implements ICharMeasure {
|
||||
if (geometry.width === 0 || geometry.height === 0) {
|
||||
return;
|
||||
}
|
||||
const adjustedHeight = Math.ceil(geometry.height);
|
||||
if (this._width !== geometry.width || this._height !== adjustedHeight) {
|
||||
if (this._width !== geometry.width || this._height !== geometry.height) {
|
||||
this._width = geometry.width;
|
||||
this._height = adjustedHeight;
|
||||
this._height = Math.ceil(geometry.height);
|
||||
this.emit('charsizechanged');
|
||||
}
|
||||
}
|
||||
|
||||
+18
-24
@@ -85,32 +85,26 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void {
|
||||
* @param ev The original right click event to be handled.
|
||||
* @param textarea The terminal's textarea.
|
||||
*/
|
||||
export function moveTextAreaUnderMouseCursor(ev: MouseEvent, term: ITerminal): 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;
|
||||
|
||||
export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextAreaElement): void {
|
||||
// Bring textarea at the cursor position
|
||||
term.textarea.style.position = 'absolute';
|
||||
term.textarea.style.width = '20px';
|
||||
term.textarea.style.height = '20px';
|
||||
term.textarea.style.left = `${left}px`;
|
||||
term.textarea.style.top = `${top}px`;
|
||||
term.textarea.style.zIndex = '1000';
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.width = '20px';
|
||||
textarea.style.height = '20px';
|
||||
textarea.style.left = (ev.clientX - 10) + 'px';
|
||||
textarea.style.top = (ev.clientY - 10) + 'px';
|
||||
textarea.style.zIndex = '1000';
|
||||
|
||||
term.textarea.focus();
|
||||
textarea.focus();
|
||||
|
||||
// Reset the terminal textarea's styling
|
||||
// Timeout needs to be long enough for click event to be handled.
|
||||
setTimeout(() => {
|
||||
term.textarea.style.position = null;
|
||||
term.textarea.style.width = null;
|
||||
term.textarea.style.height = null;
|
||||
term.textarea.style.left = null;
|
||||
term.textarea.style.top = null;
|
||||
term.textarea.style.zIndex = null;
|
||||
textarea.style.position = null;
|
||||
textarea.style.width = null;
|
||||
textarea.style.height = null;
|
||||
textarea.style.left = null;
|
||||
textarea.style.top = null;
|
||||
textarea.style.zIndex = null;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
@@ -121,14 +115,14 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, term: ITerminal): v
|
||||
* @param selectionManager The terminal's selection manager.
|
||||
* @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 {
|
||||
moveTextAreaUnderMouseCursor(ev, term);
|
||||
export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager, shouldSelectWord: boolean): void {
|
||||
moveTextAreaUnderMouseCursor(ev, textarea);
|
||||
|
||||
if (shouldSelectWord && !selectionManager.isClickInSelection(ev)) {
|
||||
selectionManager.selectWordAtCursor(ev);
|
||||
}
|
||||
|
||||
// Get textarea ready to copy from the context menu
|
||||
term.textarea.value = selectionManager.selectionText;
|
||||
term.textarea.select();
|
||||
textarea.value = selectionManager.selectionText;
|
||||
textarea.select();
|
||||
}
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
import { IDisposable } from 'xterm';
|
||||
|
||||
/**
|
||||
* Adds a disposable listener to a node in the DOM, returning the disposable.
|
||||
* Adds a disposabe listener to a node in the DOM, returning the disposable.
|
||||
* @param type The event type.
|
||||
* @param handler The handler for the listener.
|
||||
*/
|
||||
|
||||
@@ -23,7 +23,6 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
|
||||
|
||||
private _areZonesActive: boolean = false;
|
||||
private _mouseMoveListener: (e: MouseEvent) => any;
|
||||
private _mouseLeaveListener: (e: MouseEvent) => any;
|
||||
private _clickListener: (e: MouseEvent) => any;
|
||||
|
||||
private _tooltipTimeout: number = null;
|
||||
@@ -39,7 +38,6 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
|
||||
|
||||
// These events are expensive, only listen to it when mouse zones are active
|
||||
this._mouseMoveListener = e => this._onMouseMove(e);
|
||||
this._mouseLeaveListener = e => this._onMouseLeave(e);
|
||||
this._clickListener = e => this._onClick(e);
|
||||
}
|
||||
|
||||
@@ -91,7 +89,6 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
|
||||
if (!this._areZonesActive) {
|
||||
this._areZonesActive = true;
|
||||
this._terminal.element.addEventListener('mousemove', this._mouseMoveListener);
|
||||
this._terminal.element.addEventListener('mouseleave', this._mouseLeaveListener);
|
||||
this._terminal.element.addEventListener('click', this._clickListener);
|
||||
}
|
||||
}
|
||||
@@ -100,7 +97,6 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
|
||||
if (this._areZonesActive) {
|
||||
this._areZonesActive = false;
|
||||
this._terminal.element.removeEventListener('mousemove', this._mouseMoveListener);
|
||||
this._terminal.element.removeEventListener('mouseleave', this._mouseLeaveListener);
|
||||
this._terminal.element.removeEventListener('click', this._clickListener);
|
||||
}
|
||||
}
|
||||
@@ -173,18 +169,6 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
|
||||
}
|
||||
}
|
||||
|
||||
private _onMouseLeave(e: MouseEvent): void {
|
||||
// Fire the hover end callback and cancel any existing timer if the mouse
|
||||
// leaves the terminal element
|
||||
if (this._currentZone) {
|
||||
this._currentZone.leaveCallback();
|
||||
this._currentZone = null;
|
||||
if (this._tooltipTimeout) {
|
||||
clearTimeout(this._tooltipTimeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _onClick(e: MouseEvent): void {
|
||||
// Find the active zone and click it if found
|
||||
const zone = this._findZoneEventAt(e);
|
||||
|
||||
@@ -101,7 +101,7 @@ describe('clone', () => {
|
||||
test.a.b.c.d.e.f = 'bar';
|
||||
|
||||
// The values at a greater depth then 5 should not be cloned
|
||||
assert.equal((cloned as any).a.b.c.d.e.f, 'bar');
|
||||
assert.equal(cloned.a.b.c.d.e.f, 'bar');
|
||||
});
|
||||
|
||||
it('should allow an optional maximum depth to be set', () => {
|
||||
@@ -118,7 +118,7 @@ describe('clone', () => {
|
||||
test.a.b.c = 'bar';
|
||||
|
||||
// The values at a greater depth then 2 should not be cloned
|
||||
assert.equal((cloned as any).a.b.c, 'bar');
|
||||
assert.equal(cloned.a.b.c, 'bar');
|
||||
});
|
||||
|
||||
it('should not throw when cloning a recursive reference', () => {
|
||||
@@ -6,7 +6,7 @@
|
||||
/*
|
||||
* A simple utility for cloning values
|
||||
*/
|
||||
export function clone<T>(val: T, depth: number = 5): T | null {
|
||||
export const clone = <T>(val: T, depth: number = 5): T => {
|
||||
if (typeof val !== 'object') {
|
||||
return val;
|
||||
}
|
||||
@@ -25,4 +25,4 @@ export function clone<T>(val: T, depth: number = 5): T | null {
|
||||
}
|
||||
|
||||
return clonedObject as T;
|
||||
}
|
||||
};
|
||||
@@ -37,28 +37,34 @@ describe('MouseHelper.getCoords', () => {
|
||||
describe('when charMeasure is not initialized', () => {
|
||||
it('should return null', () => {
|
||||
charMeasure = new MockCharMeasure();
|
||||
assert.equal(mouseHelper.getCoords({ clientX: 0, clientY: 0 }, document.createElement('div'), charMeasure, 10, 10), null);
|
||||
assert.equal(mouseHelper.getCoords({ pageX: 0, pageY: 0 }, document.createElement('div'), charMeasure, 10, 10), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when pageX/pageY are not supported', () => {
|
||||
it('should return null', () => {
|
||||
assert.equal(mouseHelper.getCoords({ pageX: undefined, pageY: undefined }, document.createElement('div'), charMeasure, 10, 10), null);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the cell that was clicked', () => {
|
||||
let coords: [number, number];
|
||||
coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH / 2, clientY: CHAR_HEIGHT / 2 }, document.createElement('div'), charMeasure, 10, 10);
|
||||
coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH / 2, pageY: CHAR_HEIGHT / 2 }, document.createElement('div'), charMeasure, 10, 10);
|
||||
assert.deepEqual(coords, [1, 1]);
|
||||
coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 10, 10);
|
||||
coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH, pageY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 10, 10);
|
||||
assert.deepEqual(coords, [1, 1]);
|
||||
coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT + 1 }, document.createElement('div'), charMeasure, 10, 10);
|
||||
coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH, pageY: CHAR_HEIGHT + 1 }, document.createElement('div'), charMeasure, 10, 10);
|
||||
assert.deepEqual(coords, [1, 2]);
|
||||
coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH + 1, clientY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 10, 10);
|
||||
coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH + 1, pageY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 10, 10);
|
||||
assert.deepEqual(coords, [2, 1]);
|
||||
});
|
||||
|
||||
it('should ensure the coordinates are returned within the terminal bounds', () => {
|
||||
let coords: [number, number];
|
||||
coords = mouseHelper.getCoords({ clientX: -1, clientY: -1 }, document.createElement('div'), charMeasure, 10, 10);
|
||||
coords = mouseHelper.getCoords({ pageX: -1, pageY: -1 }, document.createElement('div'), charMeasure, 10, 10);
|
||||
assert.deepEqual(coords, [1, 1]);
|
||||
// Event are double the cols/rows
|
||||
coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH * 20, clientY: CHAR_HEIGHT * 20 }, document.createElement('div'), charMeasure, 10, 10);
|
||||
coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH * 20, pageY: CHAR_HEIGHT * 20 }, document.createElement('div'), charMeasure, 10, 10);
|
||||
assert.deepEqual(coords, [10, 10], 'coordinates should never come back as larger than the terminal');
|
||||
});
|
||||
});
|
||||
@@ -3,19 +3,40 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ICharMeasure, IMouseHelper } from '../Types';
|
||||
import { ICharMeasure } from '../Types';
|
||||
import { IRenderer } from '../renderer/Types';
|
||||
|
||||
export class MouseHelper implements IMouseHelper {
|
||||
export class MouseHelper {
|
||||
constructor(private _renderer: IRenderer) {}
|
||||
|
||||
public setRenderer(renderer: IRenderer): void {
|
||||
this._renderer = renderer;
|
||||
}
|
||||
|
||||
public static getCoordsRelativeToElement(event: {clientX: number, clientY: number}, element: HTMLElement): [number, number] {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return [event.clientX - rect.left, event.clientY - rect.top];
|
||||
public static getCoordsRelativeToElement(event: {pageX: number, pageY: number}, element: HTMLElement): [number, number] {
|
||||
// Ignore browsers that don't support MouseEvent.pageX
|
||||
if (event.pageX === null || event.pageX === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const originalElement = element;
|
||||
let x = event.pageX;
|
||||
let y = event.pageY;
|
||||
|
||||
// Converts the coordinates from being relative to the document to being
|
||||
// relative to the terminal.
|
||||
while (element) {
|
||||
x -= element.offsetLeft;
|
||||
y -= element.offsetTop;
|
||||
element = <HTMLElement>element.offsetParent;
|
||||
}
|
||||
element = originalElement;
|
||||
while (element && element !== element.ownerDocument.body) {
|
||||
x += element.scrollLeft;
|
||||
y += element.scrollTop;
|
||||
element = <HTMLElement>element.parentElement;
|
||||
}
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,7 +52,7 @@ export class MouseHelper implements IMouseHelper {
|
||||
* apply an offset to the x value such that the left half of the cell will
|
||||
* select that cell and the right half will select the next cell.
|
||||
*/
|
||||
public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, charMeasure: ICharMeasure, colCount: number, rowCount: number, isSelection?: boolean): [number, number] {
|
||||
public getCoords(event: {pageX: number, pageY: number}, element: HTMLElement, charMeasure: ICharMeasure, colCount: number, rowCount: number, isSelection?: boolean): [number, number] {
|
||||
// Coordinates cannot be measured if charMeasure has not been initialized
|
||||
if (!charMeasure.width || !charMeasure.height) {
|
||||
return null;
|
||||
@@ -54,12 +54,6 @@ export class MockTerminal implements ITerminal {
|
||||
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
@@ -36,6 +36,7 @@
|
||||
*/
|
||||
|
||||
.xterm {
|
||||
font-family: courier-new, courier, monospace;
|
||||
font-feature-settings: "liga" 0;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
|
||||
Vendored
+20
-38
@@ -101,6 +101,17 @@ declare module 'xterm' {
|
||||
*/
|
||||
experimentalCharAtlas?: 'none' | 'static' | 'dynamic';
|
||||
|
||||
/**
|
||||
* (EXPERIMENTAL) Defines which implementation to use for buffer lines.
|
||||
*
|
||||
* - 'JsArray': The default/stable implementation.
|
||||
* - 'TypedArray': The new experimental implementation based on TypedArrays that is expected to
|
||||
* significantly boost performance and memory consumption. Use at your own risk.
|
||||
*
|
||||
* @deprecated This option will be removed in the future.
|
||||
*/
|
||||
experimentalBufferLineImpl?: 'JsArray' | 'TypedArray';
|
||||
|
||||
/**
|
||||
* The font size used to render text.
|
||||
*/
|
||||
@@ -314,32 +325,28 @@ declare module 'xterm' {
|
||||
/**
|
||||
* The element containing the terminal.
|
||||
*/
|
||||
readonly element: HTMLElement;
|
||||
element: HTMLElement;
|
||||
|
||||
/**
|
||||
* The textarea that accepts input for the terminal.
|
||||
*/
|
||||
readonly textarea: HTMLTextAreaElement;
|
||||
textarea: HTMLTextAreaElement;
|
||||
|
||||
/**
|
||||
* The number of rows in the terminal's viewport. Use
|
||||
* `ITerminalOptions.rows` to set this in the constructor and
|
||||
* `Terminal.resize` for when the terminal exists.
|
||||
* The number of rows in the terminal's viewport.
|
||||
*/
|
||||
readonly rows: number;
|
||||
rows: number;
|
||||
|
||||
/**
|
||||
* The number of columns in the terminal's viewport. Use
|
||||
* `ITerminalOptions.cols` to set this in the constructor and
|
||||
* `Terminal.resize` for when the terminal exists.
|
||||
* The number of columns in the terminal's viewport.
|
||||
*/
|
||||
readonly cols: number;
|
||||
cols: number;
|
||||
|
||||
/**
|
||||
* (EXPERIMENTAL) Get all markers registered against the buffer. If the alt
|
||||
* buffer is active this will always return [].
|
||||
*/
|
||||
readonly markers: IMarker[];
|
||||
markers: IMarker[];
|
||||
|
||||
/**
|
||||
* Natural language strings that can be localized.
|
||||
@@ -443,9 +450,7 @@ declare module 'xterm' {
|
||||
addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable;
|
||||
|
||||
/**
|
||||
* Resizes the terminal. It's best practice to debounce calls to resize,
|
||||
* this will help ensure that the pty can respond to the resize event
|
||||
* before another one occurs.
|
||||
* Resizes the terminal.
|
||||
* @param x The number of columns to resize to.
|
||||
* @param y The number of rows to resize to.
|
||||
*/
|
||||
@@ -471,34 +476,11 @@ declare module 'xterm' {
|
||||
* should be processed by the terminal and what keys should not.
|
||||
* @param customKeyEventHandler The custom KeyboardEvent handler to attach.
|
||||
* This is a function that takes a KeyboardEvent, allowing consumers to stop
|
||||
* propagation and/or prevent the default action. The function returns
|
||||
* propogation and/or prevent the default action. The function returns
|
||||
* whether the event should be processed by xterm.js.
|
||||
*/
|
||||
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void;
|
||||
|
||||
/**
|
||||
* (EXPERIMENTAL) Adds a handler for CSI escape sequences.
|
||||
* @param flag The flag should be one-character string, which specifies the
|
||||
* final character (e.g "m" for SGR) of the CSI sequence.
|
||||
* @param callback The function to handle the escape sequence. The callback
|
||||
* is called with the numerical params, as well as the special characters
|
||||
* (e.g. "$" for DECSCPP). Return true if the sequence was handled; false if
|
||||
* we should try a previous handler (set by addCsiHandler or setCsiHandler).
|
||||
* The most recently-added handler is tried first.
|
||||
* @return An IDisposable you can call to remove this handler.
|
||||
*/
|
||||
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable;
|
||||
|
||||
/**
|
||||
* (EXPERIMENTAL) Adds a handler for OSC escape sequences.
|
||||
* @param ident The number (first parameter) of the sequence.
|
||||
* @param callback The function to handle the escape sequence. The callback
|
||||
* is called with OSC data string. Return true if the sequence was handled;
|
||||
* false if we should try a previous handler (set by addOscHandler or
|
||||
* setOscHandler). The most recently-added handler is tried first.
|
||||
* @return An IDisposable you can call to remove this handler.
|
||||
*/
|
||||
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
|
||||
/**
|
||||
* (EXPERIMENTAL) Registers a link matcher, allowing custom link patterns to
|
||||
* be matched and handled.
|
||||
|
||||
@@ -290,6 +290,13 @@ acorn-globals@^4.1.0:
|
||||
dependencies:
|
||||
acorn "^5.0.0"
|
||||
|
||||
acorn-jsx@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b"
|
||||
integrity sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=
|
||||
dependencies:
|
||||
acorn "^3.0.4"
|
||||
|
||||
acorn-node@^1.2.0, acorn-node@^1.3.0, acorn-node@^1.5.2:
|
||||
version "1.5.2"
|
||||
resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.5.2.tgz#2ca723df19d997b05824b69f6c7fb091fc42c322"
|
||||
@@ -304,6 +311,11 @@ acorn@4.X:
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
|
||||
integrity sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=
|
||||
|
||||
acorn@^3.0.4, acorn@^3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
|
||||
integrity sha1-ReN/s56No/JbruP/U2niu18iAXo=
|
||||
|
||||
acorn@^5.0.0, acorn@^5.2.1, acorn@^5.3.0, acorn@^5.7.1:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.1.tgz#f095829297706a7c9776958c0afc8930a9b9d9d8"
|
||||
@@ -737,6 +749,11 @@ bluebird@^3.5.1:
|
||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
|
||||
integrity sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==
|
||||
|
||||
bluebird@~3.4.6:
|
||||
version "3.4.7"
|
||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3"
|
||||
integrity sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=
|
||||
|
||||
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
|
||||
version "4.11.8"
|
||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
|
||||
@@ -1037,6 +1054,13 @@ caseless@~0.12.0:
|
||||
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
|
||||
integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
|
||||
|
||||
catharsis@~0.8.8:
|
||||
version "0.8.9"
|
||||
resolved "https://registry.yarnpkg.com/catharsis/-/catharsis-0.8.9.tgz#98cc890ca652dd2ef0e70b37925310ff9e90fc8b"
|
||||
integrity sha1-mMyJDKZS3S7w5ws3klMQ/56Q/Is=
|
||||
dependencies:
|
||||
underscore-contrib "~0.3.0"
|
||||
|
||||
center-align@^0.1.1:
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad"
|
||||
@@ -1976,7 +2000,7 @@ escape-html@~1.0.3:
|
||||
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
|
||||
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
|
||||
|
||||
escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
|
||||
escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5, escape-string-regexp@~1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
|
||||
@@ -2001,6 +2025,14 @@ eslint-scope@^4.0.0:
|
||||
esrecurse "^4.1.0"
|
||||
estraverse "^4.1.1"
|
||||
|
||||
espree@~3.1.7:
|
||||
version "3.1.7"
|
||||
resolved "https://registry.yarnpkg.com/espree/-/espree-3.1.7.tgz#fd5deec76a97a5120a9cd3a7cb1177a0923b11d2"
|
||||
integrity sha1-/V3ux2qXpRIKnNOnyxF3oJI7EdI=
|
||||
dependencies:
|
||||
acorn "^3.3.0"
|
||||
acorn-jsx "^3.0.0"
|
||||
|
||||
esprima@^3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
|
||||
@@ -3583,11 +3615,34 @@ js-yaml@^3.11.0, js-yaml@^3.7.0:
|
||||
argparse "^1.0.7"
|
||||
esprima "^4.0.0"
|
||||
|
||||
js2xmlparser@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js2xmlparser/-/js2xmlparser-1.0.0.tgz#5a170f2e8d6476ce45405e04823242513782fe30"
|
||||
integrity sha1-WhcPLo1kds5FQF4EgjJCUTeC/jA=
|
||||
|
||||
jsbn@~0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
|
||||
integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
|
||||
|
||||
jsdoc@3.4.3:
|
||||
version "3.4.3"
|
||||
resolved "https://registry.yarnpkg.com/jsdoc/-/jsdoc-3.4.3.tgz#e5740d6145c681f6679e6c17783a88dbdd97ccd3"
|
||||
integrity sha1-5XQNYUXGgfZnnmwXeDqI292XzNM=
|
||||
dependencies:
|
||||
bluebird "~3.4.6"
|
||||
catharsis "~0.8.8"
|
||||
escape-string-regexp "~1.0.5"
|
||||
espree "~3.1.7"
|
||||
js2xmlparser "~1.0.0"
|
||||
klaw "~1.3.0"
|
||||
marked "~0.3.6"
|
||||
mkdirp "~0.5.1"
|
||||
requizzle "~0.2.1"
|
||||
strip-json-comments "~2.0.1"
|
||||
taffydb "2.6.2"
|
||||
underscore "~1.8.3"
|
||||
|
||||
jsdom@^11.11.0:
|
||||
version "11.11.0"
|
||||
resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.11.0.tgz#df486efad41aee96c59ad7a190e2449c7eb1110e"
|
||||
@@ -3725,7 +3780,7 @@ kind-of@^6.0.0, kind-of@^6.0.2:
|
||||
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
|
||||
integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==
|
||||
|
||||
klaw@^1.0.0:
|
||||
klaw@^1.0.0, klaw@~1.3.0:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439"
|
||||
integrity sha1-QIhDO0azsbolnXh4XY6W9zugJDk=
|
||||
@@ -3839,6 +3894,16 @@ loader-utils@^1.0.2, loader-utils@^1.1.0:
|
||||
emojis-list "^2.0.0"
|
||||
json5 "^0.5.0"
|
||||
|
||||
loader-utils@~0.2.2:
|
||||
version "0.2.17"
|
||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348"
|
||||
integrity sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=
|
||||
dependencies:
|
||||
big.js "^3.1.3"
|
||||
emojis-list "^2.0.0"
|
||||
json5 "^0.5.0"
|
||||
object-assign "^4.0.1"
|
||||
|
||||
locate-path@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
|
||||
@@ -4128,6 +4193,11 @@ map-visit@^1.0.0:
|
||||
dependencies:
|
||||
object-visit "^1.0.0"
|
||||
|
||||
marked@~0.3.6:
|
||||
version "0.3.19"
|
||||
resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.19.tgz#5d47f709c4c9fc3c216b6d46127280f40b39d790"
|
||||
integrity sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg==
|
||||
|
||||
matchdep@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/matchdep/-/matchdep-1.0.1.tgz#a57a33804491fbae208aba8f68380437abc2dca5"
|
||||
@@ -4361,7 +4431,7 @@ mixin-deep@^1.2.0:
|
||||
for-in "^1.0.2"
|
||||
is-extendable "^1.0.1"
|
||||
|
||||
mkdirp@0.5.1, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0:
|
||||
mkdirp@0.5.1, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
|
||||
version "0.5.1"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
|
||||
integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
|
||||
@@ -4469,9 +4539,9 @@ nanomatch@^1.2.9:
|
||||
to-regex "^3.0.1"
|
||||
|
||||
natives@^1.1.0:
|
||||
version "1.1.6"
|
||||
resolved "https://registry.yarnpkg.com/natives/-/natives-1.1.6.tgz#a603b4a498ab77173612b9ea1acdec4d980f00bb"
|
||||
integrity sha512-6+TDFewD4yxY14ptjKaS63GVdtKiES1pTPyxn9Jb0rBqPMZ7VcCiooEhPNsr+mqHtMGxa/5c/HhcC4uPEUw/nA==
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/natives/-/natives-1.1.4.tgz#2f0f224fc9a7dd53407c7667c84cf8dbe773de58"
|
||||
integrity sha512-Q29yeg9aFKwhLVdkTAejM/HvYG0Y1Am1+HUkFQGn5k2j8GS+v60TVmZh6nujpEAj/qql+wGUrlryO8bF+b1jEg==
|
||||
|
||||
needle@^2.2.1:
|
||||
version "2.2.1"
|
||||
@@ -5592,6 +5662,13 @@ require-main-filename@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
|
||||
integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=
|
||||
|
||||
requizzle@~0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/requizzle/-/requizzle-0.2.1.tgz#6943c3530c4d9a7e46f1cddd51c158fc670cdbde"
|
||||
integrity sha1-aUPDUwxNmn5G8c3dUcFY/GcM294=
|
||||
dependencies:
|
||||
underscore "~1.6.0"
|
||||
|
||||
resolve-cwd@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
|
||||
@@ -5962,13 +6039,14 @@ source-list-map@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085"
|
||||
integrity sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A==
|
||||
|
||||
source-map-loader@^0.2.4:
|
||||
version "0.2.4"
|
||||
resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-0.2.4.tgz#c18b0dc6e23bf66f6792437557c569a11e072271"
|
||||
integrity sha512-OU6UJUty+i2JDpTItnizPrlpOIBLmQbWMuBg9q5bVtnHACqw1tn9nNwqJLbv0/00JjnJb/Ee5g5WS5vrRv7zIQ==
|
||||
source-map-loader@^0.2.3:
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-0.2.3.tgz#d4b0c8cd47d54edce3e6bfa0f523f452b5b0e521"
|
||||
integrity sha512-MYbFX9DYxmTQFfy2v8FC1XZwpwHKYxg3SK8Wb7VPBKuhDjz8gi9re2819MsG4p49HDyiOSUKlmZ+nQBArW5CGw==
|
||||
dependencies:
|
||||
async "^2.5.0"
|
||||
loader-utils "^1.1.0"
|
||||
loader-utils "~0.2.2"
|
||||
source-map "~0.6.1"
|
||||
|
||||
source-map-resolve@^0.5.0, source-map-resolve@^0.5.1:
|
||||
version "0.5.2"
|
||||
@@ -6346,6 +6424,11 @@ syntax-error@^1.1.1:
|
||||
dependencies:
|
||||
acorn-node "^1.2.0"
|
||||
|
||||
taffydb@2.6.2:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/taffydb/-/taffydb-2.6.2.tgz#7cbcb64b5a141b6a2efc2c5d2c67b4e150b2a268"
|
||||
integrity sha1-fLy2S1oUG2ou/CxdLGe04VCyomg=
|
||||
|
||||
tapable@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.0.0.tgz#cbb639d9002eed9c6b5975eb20598d7936f1f9f2"
|
||||
@@ -6705,6 +6788,23 @@ undefsafe@0.0.3:
|
||||
resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-0.0.3.tgz#ecca3a03e56b9af17385baac812ac83b994a962f"
|
||||
integrity sha1-7Mo6A+VrmvFzhbqsgSrIO5lKli8=
|
||||
|
||||
underscore-contrib@~0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/underscore-contrib/-/underscore-contrib-0.3.0.tgz#665b66c24783f8fa2b18c9f8cbb0e2c7d48c26c7"
|
||||
integrity sha1-ZltmwkeD+PorGMn4y7Dix9SMJsc=
|
||||
dependencies:
|
||||
underscore "1.6.0"
|
||||
|
||||
underscore@1.6.0, underscore@~1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8"
|
||||
integrity sha1-izixDKze9jM3uLJOT/htRa6lKag=
|
||||
|
||||
underscore@~1.8.3:
|
||||
version "1.8.3"
|
||||
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022"
|
||||
integrity sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=
|
||||
|
||||
union-value@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4"
|
||||
|
||||
Reference in New Issue
Block a user