Compare commits

..
2 Commits
Author SHA1 Message Date
Daniel Imms d306a96918 v3.9.1 2018-12-12 09:35:17 -08:00
Daniel Imms 24eb0130f5 Remove triple slash directive error in addon types
Fixes #1830
2018-12-12 09:34:20 -08:00
58 changed files with 973 additions and 1178 deletions
+2 -2
View File
@@ -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.
+1
View File
@@ -12,6 +12,7 @@ npm-debug.log
/.idea/
.env
build/
.vscode/
.DS_Store
fixtures/typings-test/*.js
package-lock.json
-44
View File
@@ -1,44 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Unit Tests",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/mocha",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/mocha.cmd"
},
"runtimeArgs": [
"--colors",
"--recursive",
"${workspaceRoot}/lib"
],
"sourceMaps": true,
"outFiles": [ "${workspaceRoot}/lib/**/*.js" ],
"internalConsoleOptions": "openOnSessionStart"
},
{
"type": "chrome",
"request": "launch",
"name": "Demo Client",
"url": "http://0.0.0.0:3000",
"windows": {
"url": "http://127.0.0.1:3000"
},
"webRoot": "${workspaceFolder}/"
},
{
"type": "node",
"request": "launch",
"name": "Demo Server",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"start-debug"
],
"port": 9229
}
]
}
+134
View File
@@ -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>
+39 -20
View File
@@ -2,20 +2,23 @@
[![Build Status](https://dev.azure.com/xtermjs/xterm.js/_apis/build/status/xtermjs.xterm.js)](https://dev.azure.com/xtermjs/xterm.js/_build/latest?definitionId=3)
[![Coverage Status](https://coveralls.io/repos/github/xtermjs/xterm.js/badge.svg?branch=master)](https://coveralls.io/github/xtermjs/xterm.js?branch=master)
[![Gitter](https://badges.gitter.im/sourcelair/xterm.js.svg)](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/xterm/badge?style=rounded)](https://www.jsdelivr.com/package/npm/xterm)
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)
@@ -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>
@@ -47,24 +50,33 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t
</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,13 +116,11 @@ 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.
@@ -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), while a rough roadmap is available 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
+7
View File
@@ -0,0 +1,7 @@
#! /usr/bin/env sh
tail --lines=+3 AUTHORS > AUTHORS.tmp
git log --format='%aN <%aE>' >> AUTHORS.tmp
echo "List of xterm.js contributors. Updated before every release.\n" > AUTHORS
sort -u AUTHORS.tmp >> AUTHORS
rm -f AUTHORS.tmp
+3
View File
@@ -17,6 +17,9 @@ CURRENT_PACKAGE_JSON_VERSION=$(cat package.json \
export BUILD_DIR=dist
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
+21 -19
View File
@@ -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);
}
});
@@ -233,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 => {
@@ -263,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
View File
@@ -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);
+2 -7
View File
@@ -8,9 +8,9 @@
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 = {
@@ -22,11 +22,6 @@ const clientConfig = {
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
},
{
test: /\.js$/,
use: ["source-map-loader"],
enforce: "pre"
}
]
},
+1 -7
View File
@@ -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
View File
@@ -1,7 +1,7 @@
{
"name": "xterm",
"description": "Full xterm terminal, in your browser",
"version": "3.10.0",
"version": "3.9.1",
"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",
+7 -35
View File
@@ -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;
@@ -355,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', () => {
@@ -508,46 +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);
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);
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);
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]);
}
});
});
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',
+60 -10
View File
@@ -17,13 +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 NULL_CELL_CODE = 32;
/**
* This class represents a terminal buffer (an internal state of the terminal), where the
@@ -252,7 +248,7 @@ export class Buffer implements IBuffer {
while (stringIndex) {
const line = this.lines.get(lineIndex);
if (!line) {
return [-1, -1];
[-1, -1];
}
for (let i = 0; i < line.length; ++i) {
stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length;
@@ -275,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 } {
@@ -439,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};
+1 -132
View File
@@ -5,7 +5,7 @@
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 {
@@ -200,135 +200,4 @@ describe('BufferLine', function(): void {
chai.expect(line.toArray()).eql(Array(7).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
});
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('');
});
});
});
+4 -51
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { CharData, IBufferLine } from './Types';
import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, WHITESPACE_CELL_CHAR } from './Buffer';
import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer';
/**
* Class representing a terminal line.
@@ -107,28 +107,6 @@ export class BufferLineJSArray implements IBufferLine {
newLine.copyFrom(this);
return newLine;
}
public getTrimmedLength(): number {
for (let i = this.length - 1; i >= 0; --i) {
const ch = this.get(i);
if (ch[CHAR_DATA_CHAR_INDEX] !== '') {
return i + ch[CHAR_DATA_WIDTH_INDEX];
}
}
return 0;
}
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) {
result += this.get(startCol)[CHAR_DATA_CHAR_INDEX] || WHITESPACE_CELL_CHAR;
startCol += this.get(startCol)[CHAR_DATA_WIDTH_INDEX] || 1;
}
return result;
}
}
/** typed array slots taken by one cell */
@@ -141,9 +119,6 @@ const enum Cell {
WIDTH = 2
}
/** single vs. combined char distinction */
const IS_COMBINED_BIT_MASK = 0x80000000;
/**
* Typed array based bufferline implementation.
*/
@@ -169,11 +144,11 @@ 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
];
@@ -183,7 +158,7 @@ export class BufferLine implements IBufferLine {
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);
}
@@ -294,26 +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 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 -1
View File
@@ -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;
+1 -1
View File
@@ -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';
-134
View File
@@ -1169,73 +1169,6 @@ describe('EscapeSequenceParser', function (): void {
parser2.parse(INPUT);
chai.expect(csi).eql([]);
});
describe('CSI custom handlers', () => {
it('Prevent fallback', () => {
const csiCustom: [string, number[], string][] = [];
parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect]));
parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; });
parser2.parse(INPUT);
chai.expect(csi).eql([], 'Should not fallback to original handler');
chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]);
});
it('Allow fallback', () => {
const csiCustom: [string, number[], string][] = [];
parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect]));
parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return false; });
parser2.parse(INPUT);
chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']], 'Should fallback to original handler');
chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]);
});
it('Multiple custom handlers fallback once', () => {
const csiCustom: [string, number[], string][] = [];
const csiCustom2: [string, number[], string][] = [];
parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect]));
parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; });
parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params, collect]); return false; });
parser2.parse(INPUT);
chai.expect(csi).eql([], 'Should not fallback to original handler');
chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]);
chai.expect(csiCustom2).eql([['m', [1, 31], ''], ['m', [0], '']]);
});
it('Multiple custom handlers no fallback', () => {
const csiCustom: [string, number[], string][] = [];
const csiCustom2: [string, number[], string][] = [];
parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect]));
parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; });
parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params, collect]); return true; });
parser2.parse(INPUT);
chai.expect(csi).eql([], 'Should not fallback to original handler');
chai.expect(csiCustom).eql([], 'Should not fallback once');
chai.expect(csiCustom2).eql([['m', [1, 31], ''], ['m', [0], '']]);
});
it('Execution order should go from latest handler down to the original', () => {
const order: number[] = [];
parser2.setCsiHandler('m', () => order.push(1));
parser2.addCsiHandler('m', () => { order.push(2); return false; });
parser2.addCsiHandler('m', () => { order.push(3); return false; });
parser2.parse('\x1b[0m');
chai.expect(order).eql([3, 2, 1]);
});
it('Dispose should work', () => {
const csiCustom: [string, number[], string][] = [];
parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect]));
const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; });
customHandler.dispose();
parser2.parse(INPUT);
chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]);
chai.expect(csiCustom).eql([], 'Should not use custom handler as it was disposed');
});
it('Should not corrupt the parser when dispose is called twice', () => {
const csiCustom: [string, number[], string][] = [];
parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect]));
const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; });
customHandler.dispose();
customHandler.dispose();
parser2.parse(INPUT);
chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]);
chai.expect(csiCustom).eql([], 'Should not use custom handler as it was disposed');
});
});
it('EXECUTE handler', function (): void {
parser2.setExecuteHandler('\n', function (): void {
exe.push('\n');
@@ -1263,73 +1196,6 @@ describe('EscapeSequenceParser', function (): void {
parser2.parse(INPUT);
chai.expect(osc).eql([]);
});
describe('OSC custom handlers', () => {
it('Prevent fallback', () => {
const oscCustom: [number, string][] = [];
parser2.setOscHandler(1, data => osc.push([1, data]));
parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
parser2.parse(INPUT);
chai.expect(osc).eql([], 'Should not fallback to original handler');
chai.expect(oscCustom).eql([[1, 'foo=bar']]);
});
it('Allow fallback', () => {
const oscCustom: [number, string][] = [];
parser2.setOscHandler(1, data => osc.push([1, data]));
parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return false; });
parser2.parse(INPUT);
chai.expect(osc).eql([[1, 'foo=bar']], 'Should fallback to original handler');
chai.expect(oscCustom).eql([[1, 'foo=bar']]);
});
it('Multiple custom handlers fallback once', () => {
const oscCustom: [number, string][] = [];
const oscCustom2: [number, string][] = [];
parser2.setOscHandler(1, data => osc.push([1, data]));
parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
parser2.addOscHandler(1, data => { oscCustom2.push([1, data]); return false; });
parser2.parse(INPUT);
chai.expect(osc).eql([], 'Should not fallback to original handler');
chai.expect(oscCustom).eql([[1, 'foo=bar']]);
chai.expect(oscCustom2).eql([[1, 'foo=bar']]);
});
it('Multiple custom handlers no fallback', () => {
const oscCustom: [number, string][] = [];
const oscCustom2: [number, string][] = [];
parser2.setOscHandler(1, data => osc.push([1, data]));
parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
parser2.addOscHandler(1, data => { oscCustom2.push([1, data]); return true; });
parser2.parse(INPUT);
chai.expect(osc).eql([], 'Should not fallback to original handler');
chai.expect(oscCustom).eql([], 'Should not fallback once');
chai.expect(oscCustom2).eql([[1, 'foo=bar']]);
});
it('Execution order should go from latest handler down to the original', () => {
const order: number[] = [];
parser2.setOscHandler(1, () => order.push(1));
parser2.addOscHandler(1, () => { order.push(2); return false; });
parser2.addOscHandler(1, () => { order.push(3); return false; });
parser2.parse('\x1b]1;foo=bar\x1b\\');
chai.expect(order).eql([3, 2, 1]);
});
it('Dispose should work', () => {
const oscCustom: [number, string][] = [];
parser2.setOscHandler(1, data => osc.push([1, data]));
const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
customHandler.dispose();
parser2.parse(INPUT);
chai.expect(osc).eql([[1, 'foo=bar']]);
chai.expect(oscCustom).eql([], 'Should not use custom handler as it was disposed');
});
it('Should not corrupt the parser when dispose is called twice', () => {
const oscCustom: [number, string][] = [];
parser2.setOscHandler(1, data => osc.push([1, data]));
const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
customHandler.dispose();
customHandler.dispose();
parser2.parse(INPUT);
chai.expect(osc).eql([[1, 'foo=bar']]);
chai.expect(oscCustom).eql([], 'Should not use custom handler as it was disposed');
});
});
it('DCS handler', function (): void {
parser2.setDcsHandler('+p', {
hook: function (collect: string, params: number[], flag: number): void {
+20 -81
View File
@@ -4,16 +4,8 @@
*/
import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from './Types';
import { IDisposable } from 'xterm';
import { Disposable } from './common/Lifecycle';
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).
* @param low The low number.
@@ -75,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.
@@ -88,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);
}
}
@@ -193,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;
})();
@@ -230,9 +220,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
// handler lookup containers
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;
@@ -286,8 +276,8 @@ 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;
@@ -311,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)];
@@ -347,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];
@@ -432,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;
@@ -464,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;
@@ -500,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);
@@ -562,15 +517,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
osc = '';
break;
case ParserAction.OSC_PUT:
for (let j = i + 1; ; j++) {
if (j >= l
|| (code = data.charCodeAt(j)) < 0x20
|| (code > 0x7f && code <= 0x9f)) {
osc += data.substring(i, j);
i = j - 1;
break;
}
}
osc += data.charAt(i);
break;
case ParserAction.OSC_END:
if (osc && code !== 0x18 && code !== 0x1a) {
@@ -585,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;
+310 -138
View File
@@ -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,250 +208,296 @@ 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)', () => {
@@ -345,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
@@ -374,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);
});
-8
View File
@@ -12,7 +12,6 @@ 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';
/**
@@ -466,13 +465,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).
+1 -1
View File
@@ -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';
-8
View File
@@ -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]);
+1 -1
View File
@@ -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 {
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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(
+2 -2
View File
@@ -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
View File
@@ -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');
+2 -12
View File
@@ -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;
@@ -464,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);
@@ -1413,15 +1412,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
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.
+1 -5
View File
@@ -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 };
}
@@ -492,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;
@@ -526,8 +524,6 @@ export interface IBufferLine {
fill(fillCharData: CharData): void;
copyFrom(line: IBufferLine): void;
clone(): IBufferLine;
getTrimmedLength(): number;
translateToString(trimRight?: boolean, startCol?: number, endCol?: number): string;
}
export interface IBufferLineConstructor {
-1
View File
@@ -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');
}
-5
View File
@@ -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 {
+54 -137
View File
@@ -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
+4 -66
View File
@@ -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);
});
});
});
-1
View File
@@ -3,7 +3,6 @@
"module": "commonjs",
"target": "es5",
"lib": [
"dom",
"es5"
],
"rootDir": ".",
-24
View File
@@ -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/');
});
});
+1 -1
View File
@@ -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 + ')?';
+2 -3
View File
@@ -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;
@@ -33,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;
}
-11
View File
@@ -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, '!');
});
});
});
-4
View File
@@ -349,10 +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.keyCode !== 144 && ev.keyCode !== 145) {
// Include only keys that that result in a character; don't include num lock and scroll lock
result.key = ev.key;
}
break;
}
-6
View File
@@ -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);
}
+4 -4
View File
@@ -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 + 0.5) * this._scaledCellHeight + this._scaledCharTop);
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 + 0.5) * this._scaledCellHeight + this._scaledCharTop);
y * this._scaledCellHeight + this._scaledCharTop);
this._ctx.restore();
}
+1 -1
View File
@@ -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';
+3 -3
View File
@@ -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];
+2 -2
View File
@@ -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
+5 -5
View File
@@ -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();
}
}
+2 -2
View File
@@ -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
+3 -5
View File
@@ -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++;
}
}
+2 -2
View File
@@ -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];
+1 -1
View File
@@ -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
@@ -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', () => {
+2 -2
View File
@@ -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.');
}
-23
View File
@@ -481,29 +481,6 @@ declare module 'xterm' {
*/
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.
+111 -11
View File
@@ -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"