diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 10eee3d9..1fe5c763 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -1,8 +1,8 @@ --- name: Question -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. +about: The issue tracker is not for questions. Please ask questions on https://stackoverflow.com/questions/tagged/xtermjs --- 🛑 The issue tracker is not for questions 🛑 -If you have a question, please ask it on https://stackoverflow.com/questions/tagged/xtermjs or https://gitter.im/sourcelair/xterm.js. +If you have a question, please ask it on https://stackoverflow.com/questions/tagged/xtermjs. diff --git a/AUTHORS b/AUTHORS deleted file mode 100644 index 482e0309..00000000 --- a/AUTHORS +++ /dev/null @@ -1,134 +0,0 @@ -List of xterm.js contributors. Updated before every release. - -7PH -7PH -Adrian Labbé -Aleksandr Andrienko -Aleksandr Andriienko -Alessandro Nadalin -Alexander Olsson -Alexandre Petit-Pas -Alexey Kontsevoy -Andres Mejia -Anish Athalye -Anthony Lapenna -Antonin Stefanutti -Antonis Kalipetis -Anton Skshidlevsky -Anton Yurovskykh -Artem Arbatskiy -Austin Robertson -ayapi -Ben Hall -Benjamin Fischer -Benjamin Raymond -Benjamin Woodruff -Bill Church -Bob Reid -bottleofwater -Brandon Bayer -Brian Mock -Bruno Ribeiro -Bruno Ribeito -Carson Anderson -CHaBou -Christian Budde Christensen -Christof Marti -Christopher Jeffrey -coderaiser -Damien Tournoud -Dan Brown -Daniel Griffen -Daniel Griffen -Daniel Imms -Daniel Risacher -Dan Kaplun -Darin Morrison -dcylabs -Dominik Csapak -Edgar Andrés Margffoy Tuay -Elliot Saba -Exile -Felipe Gasper -Felix <30559812+felixse@users.noreply.github.com> -ficristo -Gary Ritchie -hiro-su -Ian Lewis -imoses -InDieTasten -irokas -Jakob Gillich -Jan Kuri -Jean Bruenn -Jeff Principe -Jeremy Danyow -Jianhui Zhao -Joao Moreno -Joao Moreno -Johannes Zellner -Jon Austin -Jon Kohler -Jon Masters -Jörg Breitbart -Jose Anton -jpoth -Justin Luk -Justin Mecham -Kirill Merkushev -Krasimir Tsonev -Ledion Bitincka -Linus Unnebäck -Luca -Lucian Buzzo -Lukas Drgon -Lukas Geiger -Maël Nison -Marc Dumais -Marek Libra -Markus F.X.J. Oberhumer -Martin Chloride -Martin Koppehel -Martin Wang -Matt Bierner -Matthew James -Michael Irwin -Mikko Karvonen -mofux -muji -Nicolas Ramz -npezza93 -Oleksandr Andriienko -Paris Kasidiaris -Paris Kasidiaris -Peng Xiao -Peter Baumgarten -Philip Olson -PowerHat <31401273+7PH@users.noreply.github.com> -PowerHat -pro-src <34285059+pro-src@users.noreply.github.com> -pro-src -Rick Baker -runarberg -Saad Malik -Samuel Williams -Saswat Das -Saul Costa -Segev Finer -Shuanglei Tao -sitzmar -Steven Silvester -stuicey -Sven-Hendrik Haase -t-amqi -Thanasis Daglis -thomas -Thomas Zilz -Tine Jozelj -Tyler Jewell -Vincent Woo -Viraj Sinha -yutaka -YuviPanda -ZHAO Xudong -赵雪珂 diff --git a/README.md b/README.md index c654f288..35ccc9b6 100644 --- a/README.md +++ b/README.md @@ -2,23 +2,20 @@ [![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 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. +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. ## Features -- **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 + +- **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. ## 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) @@ -30,7 +27,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 `
` onto which xterm can attach itself. +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 `
` onto which xterm can attach itself. Finally instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`. ```html @@ -50,33 +47,24 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t ``` -Finally instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`. - ### Importing -The proposed way to load xterm.js is via the ES6 module syntax. +The recommended 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 xterm.js' public API. +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). 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 @@ -87,25 +75,16 @@ 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).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.: +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. ```typescript import { Terminal } from 'xterm'; import { fit } from 'xterm/lib/addons/fit/fit'; const xterm = new Terminal(); -// Fit the terminal when necessary: -fit(xterm); +fit(xterm); // Fit the terminal when necessary ``` -#### 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: @@ -116,11 +95,13 @@ Since xterm.js is typically implemented as a developer tool, only modern browser - Safari latest - IE11 -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. +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. ## API -The current full API documentation is available in the [TypeScript declaration file on the repository](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), switch the tag (press `w` when viewing the file) to point at the specific version tag you're using. +The full API for xterm.js is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API. + +Note that some APIs are marked *experimental*, these are added to enable experimentation with new ideas without committing to support it like a normal [semver](https://semver.org/) API. Note that these APIs can change radically between versions so be sure to read release notes if you plan on using experimental APIs. ## Real-world uses Xterm.js is used in several world-class applications to provide great terminal experiences. @@ -182,7 +163,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. -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). +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). ## Contributing diff --git a/bin/generate-authors b/bin/generate-authors deleted file mode 100755 index 21446ad4..00000000 --- a/bin/generate-authors +++ /dev/null @@ -1,7 +0,0 @@ -#! /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 diff --git a/bin/prepare-release b/bin/prepare-release index fed7919e..53f0947a 100755 --- a/bin/prepare-release +++ b/bin/prepare-release @@ -17,9 +17,6 @@ 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 diff --git a/demo/client.ts b/demo/client.ts index 6c69899e..d99ff66d 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -14,6 +14,7 @@ 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 above, it's looks a // little weird here as we're importing "this" module @@ -50,6 +51,14 @@ 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 = () => { @@ -97,27 +106,16 @@ function createTerminal(): void { addDomListener(paddingElement, 'change', setPadding); - 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.findNext, 'keyup', (e) => { + const searchOptions = getSearchOptions(); + searchOptions.incremental = e.key !== `Enter`; + term.findNext(actionElements.findNext.value, searchOptions); }); - 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); - } + + addDomListener(actionElements.findPrevious, 'keyup', (e) => { + const searchOptions = getSearchOptions(); + searchOptions.incremental = e.key !== `Enter`; + term.findPrevious(actionElements.findPrevious.value, searchOptions); }); // fit is called within a setTimeout, cols and rows need this. diff --git a/package.json b/package.json index 4aef95af..c5918cb5 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,6 @@ "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", @@ -57,7 +56,6 @@ "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", diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 2561ce36..0546dfe8 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -355,7 +355,7 @@ describe('Buffer', () => { let terminal: TestTerminal; beforeEach(() => { - terminal = new TestTerminal({rows: 5, cols: 10}); + terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5}); }); it('multiline ascii', () => { @@ -508,18 +508,46 @@ describe('Buffer', () => { // the dangling last cell is wrongly added in the string // --> fixable after resolving #1685 terminal.writeSync(input); - // TODO: reenable after fix - // const s = terminal.buffer.contents(true).toArray()[0]; - // assert.equal(input, s); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); for (let i = 10; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i + 1); // TODO: remove +1 after fix + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); 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 ovrflow buffer limits', function(): void { + it('iterator does not overflow buffer limits', function(): void { const terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5}); const data = [ 'aaaaaaaaaa', diff --git a/src/Buffer.ts b/src/Buffer.ts index dd657a2a..74750a8b 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -17,9 +17,13 @@ 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 = 32; +export const NULL_CELL_CODE = 0; + +export const WHITESPACE_CELL_CHAR = ' '; +export const WHITESPACE_CELL_WIDTH = 1; +export const WHITESPACE_CELL_CODE = 32; /** * This class represents a terminal buffer (an internal state of the terminal), where the @@ -248,7 +252,7 @@ export class Buffer implements IBuffer { while (stringIndex) { const line = this.lines.get(lineIndex); if (!line) { - [-1, -1]; + return [-1, -1]; } for (let i = 0; i < line.length; ++i) { stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length; @@ -271,65 +275,12 @@ 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 = null): string { - // Get full line - let lineString = ''; + public translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol: number = 0, endCol?: number): string { const line = this.lines.get(lineIndex); if (!line) { return ''; } - - // 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); + return line.translateToString(trimRight, startCol, endCol); } public getWrappedRangeForLine(y: number): { first: number, last: number } { @@ -488,8 +439,7 @@ 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) { - // TODO: always apply trimRight after fixing #1685 - result += this._buffer.translateBufferLineToString(i, (this._trimRight) ? i === range.last : false); + result += this._buffer.translateBufferLineToString(i, this._trimRight); } this._current = range.last + 1; return {range: range, content: result}; diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index b824cc38..fbf8b051 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -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 } from './Buffer'; +import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR } from './Buffer'; class TestBufferLine extends BufferLine { @@ -200,4 +200,135 @@ 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(''); + }); + }); }); diff --git a/src/BufferLine.ts b/src/BufferLine.ts index a95fe0f4..3f93af62 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -3,7 +3,7 @@ * @license MIT */ import { CharData, IBufferLine } from './Types'; -import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer'; +import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, WHITESPACE_CELL_CHAR } from './Buffer'; /** * Class representing a terminal line. @@ -107,6 +107,28 @@ 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 */ @@ -119,6 +141,9 @@ const enum Cell { WIDTH = 2 } +/** single vs. combined char distinction */ +const IS_COMBINED_BIT_MASK = 0x80000000; + /** * Typed array based bufferline implementation. */ @@ -144,11 +169,11 @@ export class BufferLine implements IBufferLine { const stringData = this._data[index * CELL_SIZE + Cell.STRING]; return [ this._data[index * CELL_SIZE + Cell.FLAGS], - (stringData & 0x80000000) + (stringData & IS_COMBINED_BIT_MASK) ? this._combined[index] : (stringData) ? String.fromCharCode(stringData) : '', this._data[index * CELL_SIZE + Cell.WIDTH], - (stringData & 0x80000000) + (stringData & IS_COMBINED_BIT_MASK) ? this._combined[index].charCodeAt(this._combined[index].length - 1) : stringData ]; @@ -158,7 +183,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 | 0x80000000; + this._data[index * CELL_SIZE + Cell.STRING] = index | IS_COMBINED_BIT_MASK; } else { this._data[index * CELL_SIZE + Cell.STRING] = value[1].charCodeAt(0); } @@ -269,4 +294,26 @@ 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; + } } diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index acc18ea5..ac3fbc8b 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -6,130 +6,10 @@ import { assert, expect } from 'chai'; import { InputHandler } from './InputHandler'; import { MockInputHandlingTerminal } from './ui/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 { 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(); @@ -208,296 +88,250 @@ describe('InputHandler', () => { }); }); describe('regression tests', function(): void { - 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[] { + function termContent(term: Terminal, trim: boolean): string[] { const result = []; - for (let i = 0; i < term.rows; ++i) result.push(lineContent(term.buffer.lines.get(i))); + for (let i = 0; i < term.rows; ++i) result.push(term.buffer.lines.get(i).translateToString(trim)); return result; } it('insertChars', function(): void { const term = new Terminal(); const inputHandler = new InputHandler(term); - 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); // 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'); + const line1: IBufferLine = term.buffer.lines.get(0); + expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + '1234567890'); // insert one char from params = [0] term.buffer.y = 0; term.buffer.x = 70; - 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(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' 123456789'); - expect(lineContent(line2)).equals(lineContent(line1)); + expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' 123456789'); // insert one char from params = [1] term.buffer.y = 0; term.buffer.x = 70; - 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(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' 12345678'); - expect(lineContent(line2)).equals(lineContent(line1)); + expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' 12345678'); // insert two chars from params = [2] term.buffer.y = 0; term.buffer.x = 70; - 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(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' 123456'); - expect(lineContent(line2)).equals(lineContent(line1)); + expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' 123456'); // insert 10 chars from params = [10] term.buffer.y = 0; term.buffer.x = 70; - 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(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' '); - expect(lineContent(line2)).equals(lineContent(line1)); + expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' '); + expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a')); }); it('deleteChars', function(): void { const term = new Terminal(); const inputHandler = new InputHandler(term); - 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); // 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'); + const line1: IBufferLine = term.buffer.lines.get(0); + expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + '1234567890'); // delete one char from params = [0] term.buffer.y = 0; term.buffer.x = 70; - 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(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '234567890 '); - expect(lineContent(line2)).equals(lineContent(line1)); + expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + '234567890 '); + expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a') + '234567890'); // insert one char from params = [1] term.buffer.y = 0; term.buffer.x = 70; - 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(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '34567890 '); - expect(lineContent(line2)).equals(lineContent(line1)); + expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + '34567890 '); + expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a') + '34567890'); // insert two chars from params = [2] term.buffer.y = 0; term.buffer.x = 70; - 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(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '567890 '); - expect(lineContent(line2)).equals(lineContent(line1)); + expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + '567890 '); + expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a') + '567890'); // insert 10 chars from params = [10] term.buffer.y = 0; term.buffer.x = 70; - 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(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' '); - expect(lineContent(line2)).equals(lineContent(line1)); + expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' '); + expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a')); }); it('eraseInLine', function(): void { const term = new Terminal(); const inputHandler = new InputHandler(term); - const oldInputHandler = new OldInputHandler(term); // fill 6 lines to test 3 different states inputHandler.parse(Array(term.cols + 1).join('a')); inputHandler.parse(Array(term.cols + 1).join('a')); inputHandler.parse(Array(term.cols + 1).join('a')); - inputHandler.parse(Array(term.cols + 1).join('a')); - inputHandler.parse(Array(term.cols + 1).join('a')); - inputHandler.parse(Array(term.cols + 1).join('a')); // params[0] - right erase term.buffer.y = 0; term.buffer.x = 70; - oldInputHandler.eraseInLine([0]); - expect(lineContent(term.buffer.lines.get(0))).equals(Array(71).join('a') + ' '); + inputHandler.eraseInLine([0]); + expect(term.buffer.lines.get(0).translateToString(false)).equals(Array(71).join('a') + ' '); + + // params[1] - left erase term.buffer.y = 1; term.buffer.x = 70; - inputHandler.eraseInLine([0]); - expect(lineContent(term.buffer.lines.get(1))).equals(Array(71).join('a') + ' '); + inputHandler.eraseInLine([1]); + expect(term.buffer.lines.get(1).translateToString(false)).equals(Array(71).join(' ') + ' aaaaaaaaa'); // params[1] - left erase term.buffer.y = 2; term.buffer.x = 70; - oldInputHandler.eraseInLine([1]); - expect(lineContent(term.buffer.lines.get(2))).equals(Array(71).join(' ') + ' aaaaaaaaa'); - term.buffer.y = 3; - term.buffer.x = 70; - inputHandler.eraseInLine([1]); - expect(lineContent(term.buffer.lines.get(3))).equals(Array(71).join(' ') + ' aaaaaaaaa'); - - // params[1] - left erase - term.buffer.y = 4; - term.buffer.x = 70; - oldInputHandler.eraseInLine([2]); - expect(lineContent(term.buffer.lines.get(4))).equals(Array(term.cols + 1).join(' ')); - term.buffer.y = 5; - term.buffer.x = 70; inputHandler.eraseInLine([2]); - expect(lineContent(term.buffer.lines.get(5))).equals(Array(term.cols + 1).join(' ')); + expect(term.buffer.lines.get(2).translateToString(false)).equals(Array(term.cols + 1).join(' ')); }); it('eraseInDisplay', function(): void { - const termOld = new Terminal(); - const inputHandlerOld = new OldInputHandler(termOld); - const termNew = new Terminal(); - const inputHandlerNew = new InputHandler(termNew); + const term = new Terminal({cols: 80, rows: 7}); + const inputHandler = new InputHandler(term); // fill display with a's - for (let i = 0; i < termOld.rows; ++i) inputHandlerOld.parse(Array(termOld.cols + 1).join('a')); - for (let i = 0; i < termNew.rows; ++i) inputHandlerNew.parse(Array(termOld.cols + 1).join('a')); - const data = []; - for (let i = 0; i < termOld.rows; ++i) data.push(Array(termOld.cols + 1).join('a')); - expect(termContent(termOld)).eql(data); - expect(termContent(termOld)).eql(termContent(termNew)); + for (let i = 0; i < term.rows; ++i) inputHandler.parse(Array(term.cols + 1).join('a')); // params [0] - right and below erase - termOld.buffer.y = 5; - termOld.buffer.x = 40; - inputHandlerOld.eraseInDisplay([0]); - termNew.buffer.y = 5; - termNew.buffer.x = 40; - inputHandlerNew.eraseInDisplay([0]); - expect(termContent(termNew)).eql(termContent(termOld)); + term.buffer.y = 5; + term.buffer.x = 40; + inputHandler.eraseInDisplay([0]); + expect(termContent(term, false)).eql([ + Array(term.cols + 1).join('a'), + Array(term.cols + 1).join('a'), + Array(term.cols + 1).join('a'), + Array(term.cols + 1).join('a'), + Array(term.cols + 1).join('a'), + Array(40 + 1).join('a') + Array(term.cols - 40 + 1).join(' '), + Array(term.cols + 1).join(' ') + ]); + expect(termContent(term, true)).eql([ + Array(term.cols + 1).join('a'), + Array(term.cols + 1).join('a'), + Array(term.cols + 1).join('a'), + Array(term.cols + 1).join('a'), + Array(term.cols + 1).join('a'), + Array(40 + 1).join('a'), + '' + ]); // reset - 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')); + term.buffer.y = 0; + term.buffer.x = 0; + for (let i = 0; i < term.rows; ++i) inputHandler.parse(Array(term.cols + 1).join('a')); // params [1] - left and above - 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)); + term.buffer.y = 5; + term.buffer.x = 40; + inputHandler.eraseInDisplay([1]); + expect(termContent(term, false)).eql([ + Array(term.cols + 1).join(' '), + Array(term.cols + 1).join(' '), + Array(term.cols + 1).join(' '), + Array(term.cols + 1).join(' '), + Array(term.cols + 1).join(' '), + Array(41 + 1).join(' ') + Array(term.cols - 41 + 1).join('a'), + Array(term.cols + 1).join('a') + ]); + expect(termContent(term, true)).eql([ + '', + '', + '', + '', + '', + Array(41 + 1).join(' ') + Array(term.cols - 41 + 1).join('a'), + Array(term.cols + 1).join('a') + ]); // reset - 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')); + term.buffer.y = 0; + term.buffer.x = 0; + for (let i = 0; i < term.rows; ++i) inputHandler.parse(Array(term.cols + 1).join('a')); // params [2] - whole screen - 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)); + term.buffer.y = 5; + term.buffer.x = 40; + inputHandler.eraseInDisplay([2]); + expect(termContent(term, false)).eql([ + Array(term.cols + 1).join(' '), + Array(term.cols + 1).join(' '), + Array(term.cols + 1).join(' '), + Array(term.cols + 1).join(' '), + Array(term.cols + 1).join(' '), + Array(term.cols + 1).join(' '), + Array(term.cols + 1).join(' ') + ]); + expect(termContent(term, true)).eql([ + '', + '', + '', + '', + '', + '', + '' + ]); // reset and add a wrapped line - 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')); + term.buffer.y = 0; + term.buffer.x = 0; + inputHandler.parse(Array(term.cols + 1).join('a')); // line 0 + inputHandler.parse(Array(term.cols + 10).join('a')); // line 1 and 2 + for (let i = 3; i < term.rows; ++i) inputHandler.parse(Array(term.cols + 1).join('a')); // params[1] left and above with wrap // confirm precondition that line 2 is wrapped - expect(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; + expect(term.buffer.lines.get(2).isWrapped).true; + term.buffer.y = 2; + term.buffer.x = 40; + inputHandler.eraseInDisplay([1]); + expect(term.buffer.lines.get(2).isWrapped).false; // reset and add a wrapped line - 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')); + term.buffer.y = 0; + term.buffer.x = 0; + inputHandler.parse(Array(term.cols + 1).join('a')); // line 0 + inputHandler.parse(Array(term.cols + 10).join('a')); // line 1 and 2 + for (let i = 3; i < term.rows; ++i) inputHandler.parse(Array(term.cols + 1).join('a')); // params[1] left and above with wrap // confirm precondition that line 2 is wrapped - expect(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; + expect(term.buffer.lines.get(2).isWrapped).true; + term.buffer.y = 1; + term.buffer.x = 90; // Cursor is beyond last column + inputHandler.eraseInDisplay([1]); + expect(term.buffer.lines.get(2).isWrapped).false; }); }); it('convertEol setting', function(): void { // not converting - let s = ''; const termNotConverting = new Terminal({cols: 15, rows: 10}); (termNotConverting as any)._inputHandler.parse('Hello\nWorld'); - for (let i = 0; i < termNotConverting.cols; ++i) { - s += termNotConverting.buffer.lines.get(0).get(i)[CHAR_DATA_CHAR_INDEX]; - } - expect(s).equals('Hello '); - s = ''; - for (let i = 0; i < termNotConverting.cols; ++i) { - s += termNotConverting.buffer.lines.get(1).get(i)[CHAR_DATA_CHAR_INDEX]; - } - expect(s).equals(' World '); + expect(termNotConverting.buffer.lines.get(0).translateToString(false)).equals('Hello '); + expect(termNotConverting.buffer.lines.get(1).translateToString(false)).equals(' World '); + expect(termNotConverting.buffer.lines.get(0).translateToString(true)).equals('Hello'); + expect(termNotConverting.buffer.lines.get(1).translateToString(true)).equals(' World'); // converting - s = ''; const termConverting = new Terminal({cols: 15, rows: 10, convertEol: true}); (termConverting as any)._inputHandler.parse('Hello\nWorld'); - for (let i = 0; i < termConverting.cols; ++i) { - s += termConverting.buffer.lines.get(0).get(i)[CHAR_DATA_CHAR_INDEX]; - } - expect(s).equals('Hello '); - s = ''; - for (let i = 0; i < termConverting.cols; ++i) { - s += termConverting.buffer.lines.get(1).get(i)[CHAR_DATA_CHAR_INDEX]; - } - expect(s).equals('World '); + expect(termConverting.buffer.lines.get(0).translateToString(false)).equals('Hello '); + expect(termConverting.buffer.lines.get(1).translateToString(false)).equals('World '); + expect(termConverting.buffer.lines.get(0).translateToString(true)).equals('Hello'); + expect(termConverting.buffer.lines.get(1).translateToString(true)).equals('World'); }); describe('print', () => { it('should not cause an infinite loop (regression test)', () => { @@ -511,34 +345,28 @@ 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(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(' ')); + expect(term.buffer.translateBufferLineToString(0, true)).to.equal(''); + expect(term.buffer.translateBufferLineToString(1, true)).to.equal(' TEST'); // Text color of 'TEST' should be red expect((term.buffer.lines.get(1).get(4)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1); }); it('should handle DECSET/DECRST 1047 (alt screen buffer)', () => { handler.parse('\x1b[?1047h\r\n\x1b[31mJUNK\x1b[?1047lTEST'); - expect(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(' ')); + expect(term.buffer.translateBufferLineToString(0, true)).to.equal(''); + expect(term.buffer.translateBufferLineToString(1, true)).to.equal(' TEST'); // Text color of 'TEST' should be red expect((term.buffer.lines.get(1).get(4)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1); }); it('should handle DECSET/DECRST 1048 (alt screen cursor)', () => { handler.parse('\x1b[?1048h\r\n\x1b[31mJUNK\x1b[?1048lTEST'); - expect(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(' ')); + expect(term.buffer.translateBufferLineToString(0, true)).to.equal('TEST'); + expect(term.buffer.translateBufferLineToString(1, true)).to.equal('JUNK'); // Text color of 'TEST' should be default expect(term.buffer.lines.get(0).get(0)[CHAR_DATA_ATTR_INDEX]).to.equal(DEFAULT_ATTR); // Text color of 'JUNK' should be red @@ -546,18 +374,18 @@ describe('InputHandler', () => { }); it('should handle DECSET/DECRST 1049 (alt screen buffer+cursor)', () => { handler.parse('\x1b[?1049h\r\n\x1b[31mJUNK\x1b[?1049lTEST'); - 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(' ')); + expect(term.buffer.translateBufferLineToString(0, true)).to.equal('TEST'); + expect(term.buffer.translateBufferLineToString(1, true)).to.equal(''); // Text color of 'TEST' should be default expect(term.buffer.lines.get(0).get(0)[CHAR_DATA_ATTR_INDEX]).to.equal(DEFAULT_ATTR); }); it('should handle DECSET/DECRST 1049 - maintains saved cursor for alt buffer', () => { handler.parse('\x1b[?1049h\r\n\x1b[31m\x1b[s\x1b[?1049lTEST'); - expect(lineContent(term.buffer.lines.get(0))).to.equal('TEST' + Array(term.cols - 3).join(' ')); + expect(term.buffer.translateBufferLineToString(0, true)).to.equal('TEST'); // Text color of 'TEST' should be default expect(term.buffer.lines.get(0).get(0)[CHAR_DATA_ATTR_INDEX]).to.equal(DEFAULT_ATTR); handler.parse('\x1b[?1049h\x1b[uTEST'); - expect(lineContent(term.buffer.lines.get(1))).to.equal('TEST' + Array(term.cols - 3).join(' ')); + expect(term.buffer.translateBufferLineToString(1, true)).to.equal('TEST'); // Text color of 'TEST' should be red expect((term.buffer.lines.get(1).get(0)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1); }); diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 2ec3ed40..53247c95 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -219,9 +219,17 @@ 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]); diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 4bac0400..1aea1cb5 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -198,7 +198,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager } } else { // Get first row - const startRowEndCol = start[1] === end[1] ? end[0] : null; + const startRowEndCol = start[1] === end[1] ? end[0] : undefined; result.push(this._buffer.translateBufferLineToString(start[1], true, start[0], startRowEndCol)); // Get middle rows diff --git a/src/Terminal.integration.ts b/src/Terminal.integration.ts index 66fd3502..d2a5cd7c 100644 --- a/src/Terminal.integration.ts +++ b/src/Terminal.integration.ts @@ -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 } from './Buffer'; +import { CHAR_DATA_CHAR_INDEX, WHITESPACE_CELL_CHAR } 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]; + lineText += term.buffer.lines.get(line).get(cell)[CHAR_DATA_CHAR_INDEX] || WHITESPACE_CELL_CHAR; } // rtrim empty cells as xterm does lineText = lineText.replace(/\s+$/, ''); diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 733d2b39..fdc9678b 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -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(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_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(1); + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); 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(1); + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); 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(1); + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); 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'); diff --git a/src/Types.ts b/src/Types.ts index a5aa8add..8ebb28d3 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -524,6 +524,8 @@ 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 { diff --git a/src/addons/fit/tsconfig.json b/src/addons/fit/tsconfig.json index f478e03f..489ccdfe 100644 --- a/src/addons/fit/tsconfig.json +++ b/src/addons/fit/tsconfig.json @@ -13,11 +13,11 @@ "declaration": true, "preserveWatchOutput": true, "types": [ - "../../node_modules/@types/mocha", - "../.." + "../../node_modules/@types/mocha" ] }, "include": [ - "**/*.ts" + "**/*.ts", + "../../../typings/xterm.d.ts" ] } diff --git a/src/addons/fullscreen/fullscreen.ts b/src/addons/fullscreen/fullscreen.ts index 4d05e904..083c00c2 100644 --- a/src/addons/fullscreen/fullscreen.ts +++ b/src/addons/fullscreen/fullscreen.ts @@ -22,6 +22,7 @@ export function toggleFullScreen(term: Terminal, fullscreen: boolean): void { fn = term.element.classList.add; } + fn = fn.bind(term.element.classList); fn('fullscreen'); } diff --git a/src/addons/fullscreen/tsconfig.json b/src/addons/fullscreen/tsconfig.json index 40d0ae71..05e6df68 100644 --- a/src/addons/fullscreen/tsconfig.json +++ b/src/addons/fullscreen/tsconfig.json @@ -13,11 +13,11 @@ "declaration": true, "preserveWatchOutput": true, "types": [ - "../../node_modules/@types/mocha", - "../.." + "../../node_modules/@types/mocha" ] }, "include": [ - "**/*.ts" + "**/*.ts", + "../../../typings/xterm.d.ts" ] } diff --git a/src/addons/search/Interfaces.ts b/src/addons/search/Interfaces.ts index e15224d1..76fe4bc9 100644 --- a/src/addons/search/Interfaces.ts +++ b/src/addons/search/Interfaces.ts @@ -25,6 +25,8 @@ export interface ISearchOptions { regex?: boolean; wholeWord?: boolean; caseSensitive?: boolean; + /** Assume caller implements 'search as you type' where findNext gets called when search input changes */ + incremental?: boolean; } export interface ISearchIndex { diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 5052a07d..408e1f29 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -4,15 +4,24 @@ */ import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult, ISearchIndex } from './Interfaces'; -const nonWordCharacters = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?'; + +const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?'; +const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs + /** * 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) { - // 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 + this._destroyLinesCache = this._destroyLinesCache.bind(this); } /** @@ -23,31 +32,40 @@ 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 result: ISearchResult; - let startRow = this._terminal._core.buffer.ydisp; let startCol: number = 0; - if (this._terminal._core.selectionManager.selectionEnd) { + let startRow = this._terminal._core.buffer.ydisp; + + if (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 = this._terminal._core.selectionManager.selectionEnd[1]; + startRow = incremental ? selectionManager.selectionStart[1] : selectionManager.selectionEnd[1]; + // TODO: Fix for incremental startCol = this._terminal._core.selectionManager.selectionEnd[0]; } } - // Search from ydisp + 1 to end - for (let y = startRow; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) { - result = this._findInLine(term, {row: y, col: startCol}, searchOptions); + this._initLinesCache(); + + // Search from startRow to end + for (let y = incremental ? startRow : startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) { + result = this._findInLine(term, { row: y, col: startCol }, searchOptions); if (result) { break; } startCol = 0; } - // Search from the top to the current ydisp + // Search from the top to the startRow if (!result) { for (let y = 0; y < startRow; y++) { startCol = 0; @@ -70,24 +88,31 @@ export class SearchHelper implements ISearchHelper { * @return Whether a result was found. */ public findPrevious(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; } + 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 (this._terminal._core.selectionManager.selectionStart) { - // Start from the selection end if there is a selection + if (selectionManager.selectionStart) { + // Start from the selection start if there is a selection if (this._terminal.getSelection().length !== 0) { - startRow = this._terminal._core.selectionManager.selectionStart[1]; - startCol = this._terminal._core.selectionManager.selectionStart[0]; + startRow = selectionManager.selectionStart[1]; + startCol = selectionManager.selectionStart[0]; } } - // Search from ydisp + 1 to end - for (let y = startRow; y >= 0; y--) { + this._initLinesCache(); + + // Search from startRow to top + for (let y = incremental ? startRow : startRow - 1; y >= 0; y--) { result = this._findInLine(term, {row: y, col: startCol}, searchOptions, isReverseSearch); if (result) { break; @@ -95,7 +120,7 @@ export class SearchHelper implements ISearchHelper { startCol = y > 0 ? this._terminal._core.buffer.lines.get(y - 1).length : 0; } - // Search from the top to the current ydisp + // Search from the bottom to startRow if (!result) { const searchFrom = this._terminal._core.buffer.ybase + this._terminal.rows - 1; for (let y = searchFrom; y > startRow; y--) { @@ -111,6 +136,28 @@ 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 @@ -118,8 +165,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) || (nonWordCharacters.indexOf(line[searchIndex - 1]) !== -1)) && - (((searchIndex + term.length) === line.length) || (nonWordCharacters.indexOf(line[searchIndex + term.length]) !== -1))); + 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))); } /** @@ -137,7 +184,14 @@ export class SearchHelper implements ISearchHelper { return; } - const stringLine = this.translateBufferLineToStringWithWrap(searchIndex.row, true); + let stringLine = this._linesCache ? this._linesCache[searchIndex.row] : void 0; + if (stringLine === void 0) { + stringLine = this.translateBufferLineToStringWithWrap(searchIndex.row, true); + if (this._linesCache) { + this._linesCache[searchIndex.row] = stringLine; + } + } + const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase(); const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase(); diff --git a/src/addons/search/search.test.ts b/src/addons/search/search.test.ts index 0a38f755..e9c11fb2 100644 --- a/src/addons/search/search.test.ts +++ b/src/addons/search/search.test.ts @@ -121,8 +121,6 @@ describe('search addon', () => { it('should not select empty lines', () => { search.apply(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); }); diff --git a/src/addons/search/tsconfig.json b/src/addons/search/tsconfig.json index c34a0bc5..87899cda 100644 --- a/src/addons/search/tsconfig.json +++ b/src/addons/search/tsconfig.json @@ -3,6 +3,7 @@ "module": "commonjs", "target": "es5", "lib": [ + "dom", "es5" ], "rootDir": ".", @@ -12,11 +13,11 @@ "declaration": true, "preserveWatchOutput": true, "types": [ - "../../node_modules/@types/mocha", - "../.." + "../../node_modules/@types/mocha" ] }, "include": [ - "**/*.ts" + "**/*.ts", + "../../../typings/xterm.d.ts" ] } diff --git a/src/addons/terminado/tsconfig.json b/src/addons/terminado/tsconfig.json index 0e30963d..91c18314 100644 --- a/src/addons/terminado/tsconfig.json +++ b/src/addons/terminado/tsconfig.json @@ -12,11 +12,11 @@ "declaration": true, "preserveWatchOutput": true, "types": [ - "../../node_modules/@types/mocha", - "../.." + "../../node_modules/@types/mocha" ] }, "include": [ - "**/*.ts" + "**/*.ts", + "../../../typings/xterm.d.ts" ] } diff --git a/src/addons/webLinks/tsconfig.json b/src/addons/webLinks/tsconfig.json index 6f9db2cb..18105aa2 100644 --- a/src/addons/webLinks/tsconfig.json +++ b/src/addons/webLinks/tsconfig.json @@ -13,11 +13,11 @@ "declaration": true, "preserveWatchOutput": true, "types": [ - "../../node_modules/@types/mocha", - "../.." + "../../node_modules/@types/mocha" ] }, "include": [ - "**/*.ts" + "**/*.ts", + "../../../typings/xterm.d.ts" ] } diff --git a/src/addons/winptyCompat/tsconfig.json b/src/addons/winptyCompat/tsconfig.json index 6056da08..9fc4d25e 100644 --- a/src/addons/winptyCompat/tsconfig.json +++ b/src/addons/winptyCompat/tsconfig.json @@ -12,11 +12,11 @@ "declaration": true, "preserveWatchOutput": true, "types": [ - "../../node_modules/@types/mocha", - "../.." + "../../node_modules/@types/mocha" ] }, "include": [ - "**/*.ts" + "**/*.ts", + "../../../typings/xterm.d.ts" ] } diff --git a/src/addons/winptyCompat/winptyCompat.ts b/src/addons/winptyCompat/winptyCompat.ts index 2513dce6..aec580ed 100644 --- a/src/addons/winptyCompat/winptyCompat.ts +++ b/src/addons/winptyCompat/winptyCompat.ts @@ -7,7 +7,8 @@ import { Terminal } from 'xterm'; import { IWinptyCompatAddonTerminal } from './Interfaces'; const CHAR_DATA_CODE_INDEX = 3; -const NULL_CELL_CODE = 32; +const NULL_CELL_CODE = 0; +const WHITESPACE_CELL_CODE = 32; export function winptyCompatInit(terminal: Terminal): void { const addonTerminal = terminal; @@ -32,7 +33,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) { + if (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE && lastChar[CHAR_DATA_CODE_INDEX] !== WHITESPACE_CELL_CODE) { const nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y); nextLine.isWrapped = true; } diff --git a/src/addons/zmodem/tsconfig.json b/src/addons/zmodem/tsconfig.json index 5dd438a3..2b49f537 100644 --- a/src/addons/zmodem/tsconfig.json +++ b/src/addons/zmodem/tsconfig.json @@ -12,11 +12,11 @@ "declaration": true, "preserveWatchOutput": true, "types": [ - "../../node_modules/@types/mocha", - "../.." + "../../node_modules/@types/mocha" ] }, "include": [ - "**/*.ts" + "**/*.ts", + "../../../typings/xterm.d.ts" ] } diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index 7b3feed7..ade2dd4c 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -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 } from '../Buffer'; +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 { 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 = charData[CHAR_DATA_CODE_INDEX]; + let code: number = charData[CHAR_DATA_CODE_INDEX] || WHITESPACE_CELL_CODE; // 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]; + let chars: string = charData[CHAR_DATA_CHAR_INDEX] || WHITESPACE_CELL_CHAR; const attr: number = charData[CHAR_DATA_ATTR_INDEX]; let width: number = charData[CHAR_DATA_WIDTH_INDEX]; diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index 54f088d4..8bcde39a 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -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 } from '../../Buffer'; +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 { 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]; + const char = charData[CHAR_DATA_CHAR_INDEX] || WHITESPACE_CELL_CHAR; const attr = charData[CHAR_DATA_ATTR_INDEX]; const width = charData[CHAR_DATA_WIDTH_INDEX]; diff --git a/yarn.lock b/yarn.lock index db31bfe1..01de5ab7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -290,13 +290,6 @@ 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" @@ -311,11 +304,6 @@ 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" @@ -749,11 +737,6 @@ 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" @@ -1054,13 +1037,6 @@ 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" @@ -2000,7 +1976,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.5, escape-string-regexp@^1.0.2, 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= @@ -2025,14 +2001,6 @@ 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" @@ -3615,34 +3583,11 @@ 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" @@ -3780,7 +3725,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.3.0: +klaw@^1.0.0: version "1.3.1" resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" integrity sha1-QIhDO0azsbolnXh4XY6W9zugJDk= @@ -4193,11 +4138,6 @@ 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" @@ -4431,7 +4371,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.1, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= @@ -4539,9 +4479,9 @@ nanomatch@^1.2.9: to-regex "^3.0.1" natives@^1.1.0: - 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== + version "1.1.6" + resolved "https://registry.yarnpkg.com/natives/-/natives-1.1.6.tgz#a603b4a498ab77173612b9ea1acdec4d980f00bb" + integrity sha512-6+TDFewD4yxY14ptjKaS63GVdtKiES1pTPyxn9Jb0rBqPMZ7VcCiooEhPNsr+mqHtMGxa/5c/HhcC4uPEUw/nA== needle@^2.2.1: version "2.2.1" @@ -5662,13 +5602,6 @@ 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" @@ -6424,11 +6357,6 @@ 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" @@ -6788,23 +6716,6 @@ 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"