diff --git a/.gitignore b/.gitignore index 427146147..cd350bb8b 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,6 @@ npm-debug.log .env build/ .DS_Store -fixtures/typings-test/*.js package-lock.json # Keep bundled code out of Git diff --git a/.npmignore b/.npmignore index 1b9f6b628..856b84ab3 100644 --- a/.npmignore +++ b/.npmignore @@ -50,3 +50,4 @@ fixtures/ demo/ .devcontainer/ out/ +addons/ diff --git a/.vscode/launch.json b/.vscode/launch.json index f0bc9be85..5e481d5ce 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -13,10 +13,13 @@ "runtimeArgs": [ "--colors", "--recursive", - "${workspaceRoot}/lib/**/*.test.js" + "${workspaceRoot}/out/**/*.test.js" ], + "env": { + "NODE_PATH": "${workspaceRoot}/out" + }, "sourceMaps": true, - "outFiles": [ "${workspaceRoot}/lib/**/*.js" ], + "outFiles": [ "${workspaceRoot}/out/**/*.js" ], "internalConsoleOptions": "openOnSessionStart" }, { @@ -31,7 +34,7 @@ "runtimeArgs": [ "--colors", "--recursive", - "${workspaceRoot}/lib/**/*.api.js" + "${workspaceRoot}/out/**/*.api.js" ], "sourceMaps": true, "outFiles": [ "${workspaceRoot}/lib/**/*.js" ], diff --git a/README.md b/README.md index 0f4303555..4707f195f 100644 --- a/README.md +++ b/README.md @@ -56,43 +56,38 @@ import { Terminal } from 'xterm'; ### Addons -Addons are JavaScript modules that extend the `Terminal` prototype with new methods and attributes to provide additional functionality. There are a handful available in the main repository in the `src/addons` directory and you can even write your own by using the [public API](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts). +⚠️ *This section describes the new addon format introduced in v3.14.0, see [here](https://github.com/xtermjs/xterm.js/blob/3.14.2/README.md#addons) for the instructions on the old format* -To use an addon, just import the JavaScript module and pass it to `Terminal`'s `applyAddon` method: +Addons are separate modules that extend the `Terminal` by building on the [xterm.js API](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts). To use an addon you first need to install it in your project: -```javascript -import { Terminal } from 'xterm'; -import * as fit from 'xterm/lib/addons/fit/fit'; - -Terminal.applyAddon(fit); - -var xterm = new Terminal(); // Instantiate the terminal -xterm.fit(); // Use the `fit` method, provided by the `fit` addon +```bash +npm i -S xterm-addon-web-links ``` -You will also need to include the addon's CSS file if it has one in the folder. +Then import the addon, instantiate it and call `Terminal.loadAddon`: -#### Importing Addons in TypeScript - -There are currently no typings for addons if they are accessed via extending Terminal prototype, so you will need to upcast if using TypeScript, eg. `(xterm as any).fit()`. Alternatively, you can import the addon function and enhance the terminal on demand. This has better typing support and is friendly to treeshaking. - -```typescript +```ts import { Terminal } from 'xterm'; -import { fit } from 'xterm/lib/addons/fit/fit'; -const xterm = new Terminal(); +import { WebLinksAddon } from 'xterm-addon-web-links'; -fit(xterm); // Fit the terminal when necessary +const terminal = new Terminal(); +// Load WebLinksAddon on terminal, this is all that's needed to get web links +// working in the terminal. +terminal.loadAddon(new WebLinksAddon()); ``` +The xterm.js team maintains the following addons but they can be built by anyone: + +- [`xterm-addon-attach`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-attach): Attaches to a server running a process via a websocket +- [`xterm-addon-fit`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-fit): Fits the terminal to the containing element +- [`xterm-addon-search`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-search): Adds search functionality +- [`xterm-addon-web-links`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-web-links): Adds web link detection and interaction + ## Browser Support -Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Here is a list of the versions we aim to support: +Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Specifically the latest versions of *Chrome*, *Edge*, *Firefox* and *Safari*. -- Chrome latest -- Edge latest -- Firefox latest -- Safari latest -- IE11 +We also partially support *Intenet Explorer 11*, meaning xterm.js should work for the most part, but we reserve the right to not provide workarounds specifically for it unless it's absolutely necessary to get the basic input/output flow 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. diff --git a/addons/xterm-addon-attach/.gitignore b/addons/xterm-addon-attach/.gitignore new file mode 100644 index 000000000..3063f07d5 --- /dev/null +++ b/addons/xterm-addon-attach/.gitignore @@ -0,0 +1,2 @@ +lib +node_modules diff --git a/addons/xterm-addon-attach/.npmignore b/addons/xterm-addon-attach/.npmignore new file mode 100644 index 000000000..e8fc8237a --- /dev/null +++ b/addons/xterm-addon-attach/.npmignore @@ -0,0 +1,4 @@ +**/*.api.js +**/*.api.ts +tsconfig.json +.yarnrc diff --git a/addons/xterm-addon-attach/LICENSE b/addons/xterm-addon-attach/LICENSE new file mode 100644 index 000000000..e597698cc --- /dev/null +++ b/addons/xterm-addon-attach/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2017, The xterm.js authors (https://github.com/xtermjs/xterm.js) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/addons/xterm-addon-attach/package.json b/addons/xterm-addon-attach/package.json new file mode 100644 index 000000000..1d72b1441 --- /dev/null +++ b/addons/xterm-addon-attach/package.json @@ -0,0 +1,20 @@ +{ + "name": "xterm-addon-attach", + "version": "0.1.0-beta11", + "author": { + "name": "The xterm.js authors", + "url": "https://xtermjs.org/" + }, + "main": "lib/xterm-addon-attach.js", + "types": "typings/xterm-addon-attach.d.ts", + "license": "MIT", + "scripts": { + "build": "../../node_modules/.bin/tsc -p src", + "prepackage": "npm run build", + "package": "../../node_modules/.bin/webpack", + "prepublishOnly": "npm run package" + }, + "peerDependencies": { + "xterm": "^3.14.0" + } +} diff --git a/addons/xterm-addon-attach/src/AttachAddon.api.ts b/addons/xterm-addon-attach/src/AttachAddon.api.ts new file mode 100644 index 000000000..945824a20 --- /dev/null +++ b/addons/xterm-addon-attach/src/AttachAddon.api.ts @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import * as puppeteer from 'puppeteer'; +import { assert } from 'chai'; +import { ITerminalOptions } from 'xterm'; +import WebSocket = require('ws'); + +const APP = 'http://127.0.0.1:3000/test'; + +let browser: puppeteer.Browser; +let page: puppeteer.Page; +const width = 800; +const height = 600; + +describe('AttachAddon', () => { + before(async function(): Promise { + this.timeout(20000); + browser = await puppeteer.launch({ + headless: process.argv.indexOf('--headless') !== -1, + slowMo: 80, + args: [`--window-size=${width},${height}`] + }); + page = (await browser.pages())[0]; + await page.setViewport({ width, height }); + }); + + after(async () => { + await browser.close(); + }); + + beforeEach(async function(): Promise { + this.timeout(20000); + await page.goto(APP); + }); + + it('string', async function(): Promise { + this.timeout(20000); + await openTerminal({ rendererType: 'dom' }); + const port = 8080; + const server = new WebSocket.Server({ port }); + server.on('connection', socket => socket.send('foo')); + await page.evaluate(`window.term.loadAddon(new window.AttachAddon(new WebSocket('ws://localhost:${port}')))`); + assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foo'); + server.close(); + }); + + it('utf8', async function(): Promise { + this.timeout(20000); + await openTerminal({ rendererType: 'dom' }); + const port = 8080; + const server = new WebSocket.Server({ port }); + const data = new Uint8Array([102, 111, 111]); + server.on('connection', socket => socket.send(data)); + await page.evaluate(`window.term.loadAddon(new window.AttachAddon(new WebSocket('ws://localhost:${port}'), { inputUtf8: true }))`); + assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foo'); + server.close(); + }); +}); + +async function openTerminal(options: ITerminalOptions = {}): Promise { + await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`); + await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`); + if (options.rendererType === 'dom') { + await page.waitForSelector('.xterm-rows'); + } else { + await page.waitForSelector('.xterm-text-layer'); + } +} diff --git a/addons/xterm-addon-attach/src/AttachAddon.ts b/addons/xterm-addon-attach/src/AttachAddon.ts new file mode 100644 index 000000000..9dc45ecbd --- /dev/null +++ b/addons/xterm-addon-attach/src/AttachAddon.ts @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2014, 2019 The xterm.js authors. All rights reserved. + * @license MIT + * + * Implements the attach method, that attaches the terminal to a WebSocket stream. + */ + +import { Terminal, IDisposable, ITerminalAddon } from 'xterm'; + +interface IAttachOptions { + bidirectional?: boolean; + inputUtf8?: boolean; +} + +export class AttachAddon implements ITerminalAddon { + private _socket: WebSocket; + private _bidirectional: boolean; + private _utf8: boolean; + private _disposables: IDisposable[] = []; + + constructor(socket: WebSocket, options?: IAttachOptions) { + this._socket = socket; + // always set binary type to arraybuffer, we do not handle blobs + this._socket.binaryType = 'arraybuffer'; + this._bidirectional = (options && options.bidirectional === false) ? false : true; + this._utf8 = !!(options && options.inputUtf8); + } + + public activate(terminal: Terminal): void { + if (this._utf8) { + this._disposables.push(addSocketListener(this._socket, 'message', + (ev: MessageEvent | Event | CloseEvent) => terminal.writeUtf8(new Uint8Array((ev as any).data as ArrayBuffer)))); + } else { + this._disposables.push(addSocketListener(this._socket, 'message', + (ev: MessageEvent | Event | CloseEvent) => terminal.write((ev as any).data as string))); + } + + if (this._bidirectional) { + this._disposables.push(terminal.onData(data => this._sendData(data))); + } + + this._disposables.push(addSocketListener(this._socket, 'close', () => this.dispose())); + this._disposables.push(addSocketListener(this._socket, 'error', () => this.dispose())); + } + + public dispose(): void { + this._disposables.forEach(d => d.dispose()); + } + + private _sendData(data: string): void { + // TODO: do something better than just swallowing + // the data if the socket is not in a working condition + if (this._socket.readyState !== 1) { + return; + } + this._socket.send(data); + } +} + +function addSocketListener(socket: WebSocket, type: string, handler: (this: WebSocket, ev: MessageEvent | Event | CloseEvent) => any): IDisposable { + socket.addEventListener(type, handler); + return { + dispose: () => { + if (!handler) { + // Already disposed + return; + } + socket.removeEventListener(type, handler); + } + }; +} diff --git a/src/addons/webLinks/tsconfig.json b/addons/xterm-addon-attach/src/tsconfig.json similarity index 54% rename from src/addons/webLinks/tsconfig.json rename to addons/xterm-addon-attach/src/tsconfig.json index 9c4f11760..d875aa530 100644 --- a/src/addons/webLinks/tsconfig.json +++ b/addons/xterm-addon-attach/src/tsconfig.json @@ -1,22 +1,19 @@ { "compilerOptions": { "module": "commonjs", - "target": "es5", + "target": "es2015", "lib": [ "dom", - "es5", + "es2015" ], "rootDir": ".", - "outDir": "../../../lib/addons/webLinks/", + "outDir": "../out", "sourceMap": true, "removeComments": true, - "declaration": true, - "types": [ - "../../node_modules/@types/mocha" - ] + "strict": true }, "include": [ - "**/*.ts", + "./**/*", "../../../typings/xterm.d.ts" ] } diff --git a/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts b/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts new file mode 100644 index 000000000..8c6671408 --- /dev/null +++ b/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { Terminal, ILinkMatcherOptions, ITerminalAddon } from 'xterm'; + +declare module 'xterm-addon-attach' { + export interface IAttachOptions { + /** + * Whether input should be written to the backend. Defaults to `true`. + */ + bidirectional?: boolean; + + /** + * Whether to use UTF8 binary transport for incoming messages. Defaults to `false`. + * Note: This must be in line with the server side of the websocket. + * Always send string messages from the backend if this options is false, + * otherwise always binary UTF8 data. + */ + inputUtf8?: boolean; + } + + export class AttachAddon implements ITerminalAddon { + constructor(socket: WebSocket, options?: IAttachOptions); + public activate(terminal: Terminal): void; + public dispose(): void; + } +} diff --git a/addons/xterm-addon-attach/webpack.config.js b/addons/xterm-addon-attach/webpack.config.js new file mode 100644 index 000000000..65996f19c --- /dev/null +++ b/addons/xterm-addon-attach/webpack.config.js @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +const path = require('path'); + +const addonName = 'AttachAddon'; +const mainFile = 'xterm-addon-attach.js'; + +module.exports = { + entry: `./out/${addonName}.js`, + devtool: 'source-map', + module: { + rules: [ + { + test: /\.js$/, + use: ["source-map-loader"], + enforce: "pre", + exclude: /node_modules/ + } + ] + }, + output: { + filename: mainFile, + path: path.resolve('./lib'), + library: addonName, + libraryTarget: 'umd' + }, + mode: 'production' +}; diff --git a/addons/xterm-addon-fit/.gitignore b/addons/xterm-addon-fit/.gitignore new file mode 100644 index 000000000..3063f07d5 --- /dev/null +++ b/addons/xterm-addon-fit/.gitignore @@ -0,0 +1,2 @@ +lib +node_modules diff --git a/addons/xterm-addon-fit/.npmignore b/addons/xterm-addon-fit/.npmignore new file mode 100644 index 000000000..e8fc8237a --- /dev/null +++ b/addons/xterm-addon-fit/.npmignore @@ -0,0 +1,4 @@ +**/*.api.js +**/*.api.ts +tsconfig.json +.yarnrc diff --git a/addons/xterm-addon-fit/LICENSE b/addons/xterm-addon-fit/LICENSE new file mode 100644 index 000000000..8f1789258 --- /dev/null +++ b/addons/xterm-addon-fit/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2019, The xterm.js authors (https://github.com/xtermjs/xterm.js) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/addons/xterm-addon-fit/package.json b/addons/xterm-addon-fit/package.json new file mode 100644 index 000000000..4ee8e1b72 --- /dev/null +++ b/addons/xterm-addon-fit/package.json @@ -0,0 +1,20 @@ +{ + "name": "xterm-addon-fit", + "version": "0.1.0-beta3", + "author": { + "name": "The xterm.js authors", + "url": "https://xtermjs.org/" + }, + "main": "lib/xterm-addon-fit.js", + "types": "typings/xterm-addon-fit.d.ts", + "license": "MIT", + "scripts": { + "build": "../../node_modules/.bin/tsc -p src", + "prepackage": "npm run build", + "package": "../../node_modules/.bin/webpack", + "prepublishOnly": "npm run package" + }, + "peerDependencies": { + "xterm": "^3.14.0" + } +} diff --git a/addons/xterm-addon-fit/src/FitAddon.ts b/addons/xterm-addon-fit/src/FitAddon.ts new file mode 100644 index 000000000..f23bd161d --- /dev/null +++ b/addons/xterm-addon-fit/src/FitAddon.ts @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { Terminal, ITerminalAddon } from 'xterm'; + +interface ITerminalDimensions { + /** + * The number of rows in the terminal. + */ + rows: number; + + /** + * The number of columns in the terminal. + */ + cols: number; +} + +export class FitAddon implements ITerminalAddon { + private _terminal: Terminal | undefined; + + constructor() {} + + public activate(terminal: Terminal): void { + this._terminal = terminal; + } + + public dispose(): void {} + + public fit(): void { + const dims = this.proposeDimensions(); + if (!dims || !this._terminal) { + return; + } + + // TODO: Remove reliance on private API + const core = (this._terminal)._core; + + // Force a full render + if (this._terminal.rows !== dims.rows || this._terminal.cols !== dims.cols) { + core._renderService.clear(); + this._terminal.resize(dims.cols, dims.rows); + } + } + + public proposeDimensions(): ITerminalDimensions | undefined { + if (!this._terminal) { + return undefined; + } + + if (!this._terminal.element.parentElement) { + return undefined; + } + + // TODO: Remove reliance on private API + const core = (this._terminal)._core; + + const parentElementStyle = window.getComputedStyle(this._terminal.element.parentElement); + const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')); + const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width'))); + const elementStyle = window.getComputedStyle(this._terminal.element); + const elementPadding = { + top: parseInt(elementStyle.getPropertyValue('padding-top')), + bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')), + right: parseInt(elementStyle.getPropertyValue('padding-right')), + left: parseInt(elementStyle.getPropertyValue('padding-left')) + }; + const elementPaddingVer = elementPadding.top + elementPadding.bottom; + const elementPaddingHor = elementPadding.right + elementPadding.left; + const availableHeight = parentElementHeight - elementPaddingVer; + const availableWidth = parentElementWidth - elementPaddingHor - core.viewport.scrollBarWidth; + const geometry = { + cols: Math.floor(availableWidth / core._renderService.dimensions.actualCellWidth), + rows: Math.floor(availableHeight / core._renderService.dimensions.actualCellHeight) + }; + return geometry; + } +} diff --git a/src/addons/fit/tsconfig.json b/addons/xterm-addon-fit/src/tsconfig.json similarity index 60% rename from src/addons/fit/tsconfig.json rename to addons/xterm-addon-fit/src/tsconfig.json index 3458d23a7..5539aa564 100644 --- a/src/addons/fit/tsconfig.json +++ b/addons/xterm-addon-fit/src/tsconfig.json @@ -4,19 +4,16 @@ "target": "es5", "lib": [ "dom", - "es5" + "es2015" ], "rootDir": ".", - "outDir": "../../../lib/addons/fit/", + "outDir": "../out", "sourceMap": true, "removeComments": true, - "declaration": true, - "types": [ - "../../node_modules/@types/mocha" - ] + "strict": true }, "include": [ - "**/*.ts", + "./**/*", "../../../typings/xterm.d.ts" ] } diff --git a/addons/xterm-addon-fit/typings/xterm-addon-fit.d.ts b/addons/xterm-addon-fit/typings/xterm-addon-fit.d.ts new file mode 100644 index 000000000..c3b0b4af0 --- /dev/null +++ b/addons/xterm-addon-fit/typings/xterm-addon-fit.d.ts @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { Terminal, ITerminalAddon } from 'xterm'; + +declare module 'xterm-addon-fit' { + /** + * An xterm.js addon that enables resizing the terminal to the dimensions of + * its containing element. + */ + export class FitAddon implements ITerminalAddon { + /** + * Creates a new fit addon. + */ + constructor(); + + /** + * Activates the addon + * @param terminal The terminal the addon is being loaded in. + */ + public activate(terminal: Terminal): void; + + /** + * Disposes the addon. + */ + public dispose(): void; + + /** + * Resizes the terminal to the dimensions of its containing element. + */ + public fit(): void; + + /** + * Gets the proposed dimensions that will be used for a fit. + */ + public proposeDimensions(): ITerminalDimensions; + } + + /** + * Reprepresents the dimensions of a terminal. + */ + export interface ITerminalDimensions { + /** + * The number of rows in the terminal. + */ + rows: number; + + /** + * The number of columns in the terminal. + */ + cols: number; + } +} diff --git a/addons/xterm-addon-fit/webpack.config.js b/addons/xterm-addon-fit/webpack.config.js new file mode 100644 index 000000000..4b5421504 --- /dev/null +++ b/addons/xterm-addon-fit/webpack.config.js @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +const path = require('path'); + +const addonName = 'FitAddon'; +const mainFile = 'xterm-addon-fit.js'; + +module.exports = { + entry: `./out/${addonName}.js`, + devtool: 'source-map', + module: { + rules: [ + { + test: /\.js$/, + use: ["source-map-loader"], + enforce: "pre", + exclude: /node_modules/ + } + ] + }, + output: { + filename: mainFile, + path: path.resolve('./lib'), + library: addonName, + libraryTarget: 'umd' + }, + mode: 'production' +}; diff --git a/addons/xterm-addon-search/.gitignore b/addons/xterm-addon-search/.gitignore new file mode 100644 index 000000000..a9f4ed545 --- /dev/null +++ b/addons/xterm-addon-search/.gitignore @@ -0,0 +1,2 @@ +lib +node_modules \ No newline at end of file diff --git a/addons/xterm-addon-search/.npmignore b/addons/xterm-addon-search/.npmignore new file mode 100644 index 000000000..e8fc8237a --- /dev/null +++ b/addons/xterm-addon-search/.npmignore @@ -0,0 +1,4 @@ +**/*.api.js +**/*.api.ts +tsconfig.json +.yarnrc diff --git a/addons/xterm-addon-search/LICENSE b/addons/xterm-addon-search/LICENSE new file mode 100644 index 000000000..e597698cc --- /dev/null +++ b/addons/xterm-addon-search/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2017, The xterm.js authors (https://github.com/xtermjs/xterm.js) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/addons/xterm-addon-search/package.json b/addons/xterm-addon-search/package.json new file mode 100644 index 000000000..02cb6695b --- /dev/null +++ b/addons/xterm-addon-search/package.json @@ -0,0 +1,20 @@ +{ + "name": "xterm-addon-search", + "version": "0.1.0-beta6", + "author": { + "name": "The xterm.js authors", + "url": "https://xtermjs.org/" + }, + "main": "lib/xterm-addon-search.js", + "types": "typings/xterm-addon-search.d.ts", + "license": "MIT", + "scripts": { + "build": "../../node_modules/.bin/tsc -p src", + "prepackage": "npm run build", + "package": "../../node_modules/.bin/webpack", + "prepublishOnly": "npm run package" + }, + "peerDependencies": { + "xterm": "^3.14.0" + } +} diff --git a/src/addons/search/SearchHelper.ts b/addons/xterm-addon-search/src/SearchAddon.ts similarity index 74% rename from src/addons/search/SearchHelper.ts rename to addons/xterm-addon-search/src/SearchAddon.ts index e23b14a10..57f115f8e 100644 --- a/src/addons/search/SearchHelper.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -3,30 +3,43 @@ * @license MIT */ -import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult } from './Interfaces'; -import { IDisposable } from 'xterm'; +import { Terminal, IDisposable, ITerminalAddon } from 'xterm'; + +export interface ISearchOptions { + regex?: boolean; + wholeWord?: boolean; + caseSensitive?: boolean; + incremental?: boolean; +} + +export interface ISearchResult { + term: string; + col: number; + row: number; +} 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 { +export class SearchAddon implements ITerminalAddon { + private _terminal: Terminal | undefined; + /** * 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 _linesCache: string[] | undefined; private _linesCacheTimeoutId = 0; private _cursorMoveListener: IDisposable | undefined; private _resizeListener: IDisposable | undefined; - constructor(private _terminal: ISearchAddonTerminal) { - this._destroyLinesCache = this._destroyLinesCache.bind(this); + public activate(terminal: Terminal): void { + this._terminal = terminal; } + public dispose(): void {} + /** * Find the next instance of the term, then scroll to and select it. If it * doesn't exist, do nothing. @@ -35,8 +48,9 @@ export class SearchHelper implements ISearchHelper { * @return Whether a result was found. */ public findNext(term: string, searchOptions?: ISearchOptions): boolean { - const {incremental} = searchOptions; - let result: ISearchResult; + if (!this._terminal) { + throw new Error('Cannot use addon until it has been loaded'); + } if (!term || term.length === 0) { this._terminal.clearSelection(); @@ -47,9 +61,10 @@ export class SearchHelper implements ISearchHelper { let startRow = this._terminal.buffer.viewportY; if (this._terminal.hasSelection()) { + const incremental = searchOptions ? searchOptions.incremental : false; // Start from the selection end if there is a selection // For incremental search, use existing row - const currentSelection = this._terminal.getSelectionPosition(); + const currentSelection = this._terminal.getSelectionPosition()!; startRow = incremental ? currentSelection.startRow : currentSelection.endRow; startCol = incremental ? currentSelection.startColumn : currentSelection.endColumn; } @@ -62,13 +77,14 @@ export class SearchHelper implements ISearchHelper { let cumulativeCols = startCol; // If startRow is wrapped row, scan for unwrapped row above. // So we can start matching on wrapped line from long unwrapped line. - while (this._terminal.buffer.getLine(findingRow).isWrapped) { - findingRow--; + let currentLine = this._terminal.buffer.getLine(findingRow); + while (currentLine && currentLine.isWrapped) { cumulativeCols += this._terminal.cols; + currentLine = this._terminal.buffer.getLine(--findingRow); } // Search startRow - result = this._findInLine(term, findingRow, cumulativeCols, searchOptions); + let result = this._findInLine(term, findingRow, cumulativeCols, searchOptions); // Search from startRow + 1 to end if (!result) { @@ -107,7 +123,9 @@ export class SearchHelper implements ISearchHelper { * @return Whether a result was found. */ public findPrevious(term: string, searchOptions?: ISearchOptions): boolean { - let result: ISearchResult; + if (!this._terminal) { + throw new Error('Cannot use addon until it has been loaded'); + } if (!term || term.length === 0) { this._terminal.clearSelection(); @@ -120,7 +138,7 @@ export class SearchHelper implements ISearchHelper { if (this._terminal.hasSelection()) { // Start from the selection start if there is a selection - const currentSelection = this._terminal.getSelectionPosition(); + const currentSelection = this._terminal.getSelectionPosition()!; startRow = currentSelection.startRow; startCol = currentSelection.startColumn; } @@ -128,14 +146,14 @@ export class SearchHelper implements ISearchHelper { this._initLinesCache(); // Search startRow - result = this._findInLine(term, startRow, startCol, searchOptions, isReverseSearch); + let result = this._findInLine(term, startRow, startCol, searchOptions, isReverseSearch); // Search from startRow - 1 to top if (!result) { // If the line is wrapped line, increase number of columns that is needed to be scanned // Se we can scan on wrapped line from unwrapped line let cumulativeCols = this._terminal.cols; - if (this._terminal.buffer.getLine(startRow).isWrapped) { + if (this._terminal.buffer.getLine(startRow)!.isWrapped) { cumulativeCols += startCol; } for (let y = startRow - 1; y >= 0; y--) { @@ -145,7 +163,8 @@ export class SearchHelper implements ISearchHelper { } // If the current line is wrapped line, increase scanning range, // preparing for scanning on unwrapped line - if (this._terminal.buffer.getLine(y).isWrapped) { + const line = this._terminal.buffer.getLine(y); + if (line && line.isWrapped) { cumulativeCols += this._terminal.cols; } else { cumulativeCols = this._terminal.cols; @@ -163,7 +182,8 @@ export class SearchHelper implements ISearchHelper { if (result) { break; } - if (this._terminal.buffer.getLine(y).isWrapped) { + const line = this._terminal.buffer.getLine(y); + if (line && line.isWrapped) { cumulativeCols += this._terminal.cols; } else { cumulativeCols = this._terminal.cols; @@ -179,10 +199,11 @@ export class SearchHelper implements ISearchHelper { * Sets up a line cache with a ttl */ private _initLinesCache(): void { + const terminal = this._terminal!; if (!this._linesCache) { - this._linesCache = new Array(this._terminal.buffer.length); - this._cursorMoveListener = this._terminal.onCursorMove(() => this._destroyLinesCache()); - this._resizeListener = this._terminal.onResize(() => this._destroyLinesCache()); + this._linesCache = new Array(terminal.buffer.length); + this._cursorMoveListener = terminal.onCursorMove(() => this._destroyLinesCache()); + this._resizeListener = terminal.onResize(() => this._destroyLinesCache()); } window.clearTimeout(this._linesCacheTimeoutId); @@ -190,7 +211,7 @@ export class SearchHelper implements ISearchHelper { } private _destroyLinesCache(): void { - this._linesCache = null; + this._linesCache = undefined; if (this._cursorMoveListener) { this._cursorMoveListener.dispose(); this._cursorMoveListener = undefined; @@ -227,15 +248,17 @@ export class SearchHelper implements ISearchHelper { * @param searchOptions Search options. * @return The search result if it was found. */ - protected _findInLine(term: string, row: number, col: number, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult { + protected _findInLine(term: string, row: number, col: number, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined { + const terminal = this._terminal!; // Ignore wrapped lines, only consider on unwrapped line (first row of command string). - if (this._terminal.buffer.getLine(row).isWrapped) { + const firstLine = terminal.buffer.getLine(row); + if (firstLine && firstLine.isWrapped) { return; } let stringLine = this._linesCache ? this._linesCache[row] : void 0; if (stringLine === void 0) { - stringLine = this.translateBufferLineToStringWithWrap(row, true); + stringLine = this._translateBufferLineToStringWithWrap(row, true); if (this._linesCache) { this._linesCache[row] = stringLine; } @@ -247,7 +270,7 @@ export class SearchHelper implements ISearchHelper { let resultIndex = -1; if (searchOptions.regex) { const searchRegex = RegExp(searchTerm, 'g'); - let foundTerm: RegExpExecArray; + let foundTerm: RegExpExecArray | null; if (isReverseSearch) { // This loop will get the resultIndex of the _last_ regex match in the range 0..col while (foundTerm = searchRegex.exec(searchStringLine.slice(0, col))) { @@ -274,28 +297,33 @@ export class SearchHelper implements ISearchHelper { if (resultIndex >= 0) { // Adjust the row number and search index if needed since a "line" of text can span multiple rows - if (resultIndex >= this._terminal.cols) { - row += Math.floor(resultIndex / this._terminal.cols); - resultIndex = resultIndex % this._terminal.cols; + if (resultIndex >= terminal.cols) { + row += Math.floor(resultIndex / terminal.cols); + resultIndex = resultIndex % terminal.cols; } if (searchOptions.wholeWord && !this._isWholeWord(resultIndex, searchStringLine, term)) { return; } - const line = this._terminal.buffer.getLine(row); + const line = terminal.buffer.getLine(row); - for (let i = 0; i < resultIndex; i++) { - const cell = line.getCell(i); - // Adjust the searchIndex to normalize emoji into single chars - const char = cell.char; - if (char.length > 1) { - resultIndex -= char.length - 1; - } - // Adjust the searchIndex for empty characters following wide unicode - // chars (eg. CJK) - const charWidth = cell.width; - if (charWidth === 0) { - resultIndex++; + if (line) { + for (let i = 0; i < resultIndex; i++) { + const cell = line.getCell(i); + if (!cell) { + break; + } + // Adjust the searchIndex to normalize emoji into single chars + const char = cell.char; + if (char.length > 1) { + resultIndex -= char.length - 1; + } + // Adjust the searchIndex for empty characters following wide unicode + // chars (eg. CJK) + const charWidth = cell.width; + if (charWidth === 0) { + resultIndex++; + } } } return { @@ -305,6 +333,7 @@ export class SearchHelper implements ISearchHelper { }; } } + /** * Translates a buffer line to a string, including subsequent lines if they are wraps. * Wide characters will count as two columns in the resulting string. This @@ -313,14 +342,19 @@ export class SearchHelper implements ISearchHelper { * @param line The line being translated. * @param trimRight Whether to trim whitespace to the right. */ - public translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): string { + private _translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): string { + const terminal = this._terminal!; let lineString = ''; let lineWrapsToNext: boolean; do { - const nextLine = this._terminal.buffer.getLine(lineIndex + 1); + const nextLine = terminal.buffer.getLine(lineIndex + 1); lineWrapsToNext = nextLine ? nextLine.isWrapped : false; - lineString += this._terminal.buffer.getLine(lineIndex).translateToString(!lineWrapsToNext && trimRight).substring(0, this._terminal.cols); + const line = terminal.buffer.getLine(lineIndex); + if (!line) { + break; + } + lineString += line.translateToString(!lineWrapsToNext && trimRight).substring(0, terminal.cols); lineIndex++; } while (lineWrapsToNext); @@ -332,13 +366,14 @@ export class SearchHelper implements ISearchHelper { * @param result The result to select. * @return Whethera result was selected. */ - private _selectResult(result: ISearchResult): boolean { + private _selectResult(result: ISearchResult | undefined): boolean { + const terminal = this._terminal!; if (!result) { - this._terminal.clearSelection(); + terminal.clearSelection(); return false; } - this._terminal.select(result.col, result.row, result.term.length); - this._terminal.scrollLines(result.row - this._terminal.buffer.viewportY); + terminal.select(result.col, result.row, result.term.length); + terminal.scrollLines(result.row - terminal.buffer.viewportY); return true; } } diff --git a/src/addons/attach/tsconfig.json b/addons/xterm-addon-search/src/tsconfig.json similarity index 74% rename from src/addons/attach/tsconfig.json rename to addons/xterm-addon-search/src/tsconfig.json index 2f39102ca..1cb6d4d7e 100644 --- a/src/addons/attach/tsconfig.json +++ b/addons/xterm-addon-search/src/tsconfig.json @@ -7,13 +7,13 @@ "es6", ], "rootDir": ".", - "outDir": "../../../lib/addons/attach/", + "outDir": "../out", "sourceMap": true, "removeComments": true, - "declaration": true + "strict": true }, "include": [ - "**/*.ts", + "./**/*", "../../../typings/xterm.d.ts" ] } diff --git a/addons/xterm-addon-search/typings/xterm-addon-search.d.ts b/addons/xterm-addon-search/typings/xterm-addon-search.d.ts new file mode 100644 index 000000000..94bc52971 --- /dev/null +++ b/addons/xterm-addon-search/typings/xterm-addon-search.d.ts @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { Terminal, ILinkMatcherOptions, IDisposable, ITerminalAddon } from 'xterm'; + +declare module 'xterm-addon-search' { + /** + * Options for a search. + */ + export interface ISearchOptions { + /** + * Whether the search term is a regex. + */ + regex?: boolean; + + /** + * Whether to search for a whole word, the result is only valid if it's + * suppounded in "non-word" characters such as `_`, `(`, `)` or space. + */ + wholeWord?: boolean; + + /** + * Whether the search is case sensitive. + */ + caseSensitive?: boolean; + + /** + * Whether to do an indcremental search, this will expand the selection if it + * still matches the term the user typed. Note that this only affects + * `findNext`, not `findPrevious`. + */ + incremental?: boolean; + } + + /** + * An xterm.js addon that provides search functionality. + */ + export class SearchAddon implements ITerminalAddon { + /** + * Activates the addon + * @param terminal The terminal the addon is being loaded in. + */ + public activate(terminal: Terminal): void; + + /** + * Disposes the addon. + */ + public dispose(): void; + + /** + * Search forwards for the next result that matches the search term and + * options. + * @param term The search term. + * @param searchOptions The options for the search. + */ + public findNext(term: string, searchOptions?: ISearchOptions): boolean; + + /** + * Search backwards for the previous result that matches the search term and + * options. + * @param term The search term. + * @param searchOptions The options for the search. + */ + public findPrevious(term: string, searchOptions?: ISearchOptions): boolean; + } +} diff --git a/addons/xterm-addon-search/webpack.config.js b/addons/xterm-addon-search/webpack.config.js new file mode 100644 index 000000000..726dceb6f --- /dev/null +++ b/addons/xterm-addon-search/webpack.config.js @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +const path = require('path'); + +const addonName = 'SearchAddon'; +const mainFile = 'xterm-addon-search.js'; + +module.exports = { + entry: `./out/${addonName}.js`, + devtool: 'source-map', + module: { + rules: [ + { + test: /\.js$/, + use: ["source-map-loader"], + enforce: "pre", + exclude: /node_modules/ + } + ] + }, + output: { + filename: mainFile, + path: path.resolve('./lib'), + library: addonName, + libraryTarget: 'umd' + }, + mode: 'production' +}; diff --git a/addons/xterm-addon-web-links/.gitignore b/addons/xterm-addon-web-links/.gitignore new file mode 100644 index 000000000..3063f07d5 --- /dev/null +++ b/addons/xterm-addon-web-links/.gitignore @@ -0,0 +1,2 @@ +lib +node_modules diff --git a/addons/xterm-addon-web-links/.npmignore b/addons/xterm-addon-web-links/.npmignore new file mode 100644 index 000000000..e8fc8237a --- /dev/null +++ b/addons/xterm-addon-web-links/.npmignore @@ -0,0 +1,4 @@ +**/*.api.js +**/*.api.ts +tsconfig.json +.yarnrc diff --git a/addons/xterm-addon-web-links/LICENSE b/addons/xterm-addon-web-links/LICENSE new file mode 100644 index 000000000..e597698cc --- /dev/null +++ b/addons/xterm-addon-web-links/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2017, The xterm.js authors (https://github.com/xtermjs/xterm.js) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/addons/xterm-addon-web-links/README.md b/addons/xterm-addon-web-links/README.md new file mode 100644 index 000000000..dc9e1c326 --- /dev/null +++ b/addons/xterm-addon-web-links/README.md @@ -0,0 +1,23 @@ +## xterm-addon-web-links + +[![Build Status](https://dev.azure.com/xtermjs/xterm-addon-web-links/_apis/build/status/xtermjs.xterm-addon-web-links?branchName=master)](https://dev.azure.com/xtermjs/xterm-addon-web-links/_build/latest?definitionId=5&branchName=master) + +An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enabled web links. This addon requires xterm.js 3.14+. + +### Install + +```bash +npm install --save xterm-addon-web-links +``` + +### Usage + +```ts +import { Terminal } from 'xterm'; +import { WebLinksAddon } from 'xterm-addon-web-links'; + +const terminal = new Terminal(); +terminal.loadAddon(new WebLinksAddon()); +``` + +You can also specify a custom handler and options, see the [API](https://github.com/xtermjs/xterm-addon-web-links/blob/master/typings/web-links.d.ts) for more details. diff --git a/addons/xterm-addon-web-links/package.json b/addons/xterm-addon-web-links/package.json new file mode 100644 index 000000000..a764a200f --- /dev/null +++ b/addons/xterm-addon-web-links/package.json @@ -0,0 +1,20 @@ +{ + "name": "xterm-addon-web-links", + "version": "0.1.0-beta10", + "author": { + "name": "The xterm.js authors", + "url": "https://xtermjs.org/" + }, + "main": "lib/xterm-addon-web-links.js", + "types": "typings/xterm-addon-web-links.d.ts", + "license": "MIT", + "scripts": { + "build": "../../node_modules/.bin/tsc -p src", + "prepackage": "npm run build", + "package": "../../node_modules/.bin/webpack", + "prepublishOnly": "npm run package" + }, + "peerDependencies": { + "xterm": "^3.14.0" + } +} diff --git a/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts b/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts new file mode 100644 index 000000000..2c0044bef --- /dev/null +++ b/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import * as puppeteer from 'puppeteer'; +import { assert } from 'chai'; +import { ITerminalOptions } from 'xterm'; + +const APP = 'http://127.0.0.1:3000/test'; + +let browser: puppeteer.Browser; +let page: puppeteer.Page; +const width = 800; +const height = 600; + +describe('WebLinksAddon', () => { + before(async function(): Promise { + this.timeout(10000); + browser = await puppeteer.launch({ + headless: process.argv.indexOf('--headless') !== -1, + slowMo: 80, + args: [`--window-size=${width},${height}`] + }); + page = (await browser.pages())[0]; + await page.setViewport({ width, height }); + }); + + after(async () => { + await browser.close(); + }); + + beforeEach(async function(): Promise { + this.timeout(5000); + await page.goto(APP); + }); + + it('.com', async function(): Promise { + this.timeout(20000); + await testHostName('foo.com'); + }); + + it('.com.au', async function(): Promise { + this.timeout(20000); + await testHostName('foo.com.au'); + }); + + it('.io', async function(): Promise { + this.timeout(20000); + await testHostName('foo.io'); + }); +}); + +async function testHostName(hostname: string): Promise { + await openTerminal({ rendererType: 'dom' }); + await page.evaluate(`window.term.loadAddon(new window.WebLinksAddon())`); + await page.evaluate(` + window.term.writeln(' http://${hostname} '); + window.term.writeln(' http://${hostname}/a~b#c~d?e~f '); + window.term.writeln(' http://${hostname}/colon:test '); + window.term.writeln(' http://${hostname}/colon:test: '); + window.term.writeln('"http://${hostname}/"'); + window.term.writeln('\\'http://${hostname}/\\''); + window.term.writeln('http://${hostname}/subpath/+/id'); + `); + assert.equal(await getLinkAtCell(3, 1), `http://${hostname}`); + assert.equal(await getLinkAtCell(3, 2), `http://${hostname}/a~b#c~d?e~f`); + assert.equal(await getLinkAtCell(3, 3), `http://${hostname}/colon:test`); + assert.equal(await getLinkAtCell(3, 4), `http://${hostname}/colon:test`); + assert.equal(await getLinkAtCell(2, 5), `http://${hostname}/`); + assert.equal(await getLinkAtCell(2, 6), `http://${hostname}/`); + assert.equal(await getLinkAtCell(1, 7), `http://${hostname}/subpath/+/id`); +} + +async function openTerminal(options: ITerminalOptions = {}): Promise { + await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`); + await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`); + if (options.rendererType === 'dom') { + await page.waitForSelector('.xterm-rows'); + } else { + await page.waitForSelector('.xterm-text-layer'); + } +} + +async function getLinkAtCell(col: number, row: number): Promise { + const rowSelector = `.xterm-rows > :nth-child(${row})`; + await page.hover(`${rowSelector} > :nth-child(${col})`); + return await page.evaluate(`Array.prototype.reduce.call(document.querySelectorAll('${rowSelector} > span[style]'), (a, b) => a + b.textContent, '');`); +} diff --git a/src/addons/webLinks/webLinks.ts b/addons/xterm-addon-web-links/src/WebLinksAddon.ts similarity index 62% rename from src/addons/webLinks/webLinks.ts rename to addons/xterm-addon-web-links/src/WebLinksAddon.ts index 8a0fec097..26d5904b9 100644 --- a/src/addons/webLinks/webLinks.ts +++ b/addons/xterm-addon-web-links/src/WebLinksAddon.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal, ILinkMatcherOptions } from 'xterm'; +import { Terminal, ILinkMatcherOptions, ITerminalAddon } from 'xterm'; const protocolClause = '(https?:\\/\\/)'; const domainCharacterSet = '[\\da-z\\.-]+'; @@ -29,19 +29,25 @@ function handleLink(event: MouseEvent, uri: string): void { window.open(uri, '_blank'); } -/** - * Initialize the web links addon, registering the link matcher. - * @param term The terminal to use web links within. - * @param handler A custom handler to use. - * @param options Custom options to use, matchIndex will always be ignored. - */ -export function webLinksInit(term: Terminal, handler: (event: MouseEvent, uri: string) => void = handleLink, options: ILinkMatcherOptions = {}): void { - options.matchIndex = 1; - term.registerLinkMatcher(strictUrlRegex, handler, options); -} +export class WebLinksAddon implements ITerminalAddon { + private _linkMatcherId: number | undefined; + private _terminal: Terminal | undefined; -export function apply(terminalConstructor: typeof Terminal): void { - (terminalConstructor.prototype).webLinksInit = function (handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): void { - webLinksInit(this, handler, options); - }; + constructor( + private _handler: (event: MouseEvent, uri: string) => void = handleLink, + private _options: ILinkMatcherOptions = {} + ) { + this._options.matchIndex = 1; + } + + public activate(terminal: Terminal): void { + this._terminal = terminal; + this._linkMatcherId = this._terminal.registerLinkMatcher(strictUrlRegex, this._handler, this._options); + } + + public dispose(): void { + if (this._linkMatcherId !== undefined && this._terminal !== undefined) { + this._terminal.deregisterLinkMatcher(this._linkMatcherId); + } + } } diff --git a/src/addons/search/tsconfig.json b/addons/xterm-addon-web-links/src/tsconfig.json similarity index 59% rename from src/addons/search/tsconfig.json rename to addons/xterm-addon-web-links/src/tsconfig.json index 6a1611a51..5539aa564 100644 --- a/src/addons/search/tsconfig.json +++ b/addons/xterm-addon-web-links/src/tsconfig.json @@ -4,19 +4,16 @@ "target": "es5", "lib": [ "dom", - "es5" + "es2015" ], "rootDir": ".", - "outDir": "../../../lib/addons/search/", + "outDir": "../out", "sourceMap": true, "removeComments": true, - "declaration": true, - "types": [ - "../../node_modules/@types/mocha" - ] + "strict": true }, "include": [ - "**/*.ts", + "./**/*", "../../../typings/xterm.d.ts" ] } diff --git a/addons/xterm-addon-web-links/typings/xterm-addon-web-links.d.ts b/addons/xterm-addon-web-links/typings/xterm-addon-web-links.d.ts new file mode 100644 index 000000000..1c53bcdeb --- /dev/null +++ b/addons/xterm-addon-web-links/typings/xterm-addon-web-links.d.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + + +import { Terminal, ILinkMatcherOptions, ITerminalAddon } from 'xterm'; + +declare module 'xterm-addon-web-links' { + /** + * An xterm.js addon that enables web links. + */ + export class WebLinksAddon implements ITerminalAddon { + /** + * Creates a new web links addon. + * @param handler The callback when the link is called. + * @param options Options for the link matcher. + */ + constructor(handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions); + + /** + * Activates the addon + * @param terminal The terminal the addon is being loaded in. + */ + public activate(terminal: Terminal): void; + + /** + * Disposes the addon. + */ + public dispose(): void; + } +} diff --git a/addons/xterm-addon-web-links/webpack.config.js b/addons/xterm-addon-web-links/webpack.config.js new file mode 100644 index 000000000..fd87d07e8 --- /dev/null +++ b/addons/xterm-addon-web-links/webpack.config.js @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +const path = require('path'); + +const addonName = 'WebLinksAddon'; +const mainFile = 'xterm-addon-web-links.js'; + +module.exports = { + entry: `./out/${addonName}.js`, + devtool: 'source-map', + module: { + rules: [ + { + test: /\.js$/, + use: ["source-map-loader"], + enforce: "pre", + exclude: /node_modules/ + } + ] + }, + output: { + filename: mainFile, + path: path.resolve('./lib'), + library: addonName, + libraryTarget: 'umd' + }, + mode: 'production' +}; diff --git a/bin/test.js b/bin/test.js index 42e1a4733..89f0d61e5 100644 --- a/bin/test.js +++ b/bin/test.js @@ -10,16 +10,23 @@ const path = require('path'); const env = { ...process.env }; env.NODE_PATH = path.resolve(__dirname, '../out'); -const args = [ +let testFiles = [ './out/*test.js', - './out/**/*test.js', - './out/*integration.js', - './out/**/*integration.js', - './lib/**/*test.js' + './out/**/*test.js' ]; -cp.spawnSync(path.resolve(__dirname, '../node_modules/.bin/mocha'), args, { - cwd: path.resolve(__dirname, '..'), - env, - stdio: 'inherit' -}); +// ability to inject particular test files via +// yarn test [testFileA testFileB ...] +if (process.argv.length > 2) { + testFiles = process.argv.slice(2); +} + +cp.spawnSync( + path.resolve(__dirname, '../node_modules/.bin/mocha'), + testFiles, + { + cwd: path.resolve(__dirname, '..'), + env, + stdio: 'inherit' + } +); diff --git a/demo/client.ts b/demo/client.ts index 10d27c864..e0d55d57a 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -9,14 +9,17 @@ // Use tsc version (yarn watch) import { Terminal } from '../out/public/Terminal'; +import { AttachAddon } from '../addons/xterm-addon-attach/out/AttachAddon'; +import { FitAddon } from '../addons/xterm-addon-fit/out/FitAddon'; +import { SearchAddon, ISearchOptions } from '../addons/xterm-addon-search/out/SearchAddon'; +import { WebLinksAddon } from '../addons/xterm-addon-web-links/out/WebLinksAddon'; + // Use webpacked version (yarn package) // import { Terminal } from '../lib/xterm'; - -import { AttachAddon } from 'xterm-addon-attach'; -import { SearchAddon, ISearchOptions } from 'xterm-addon-search'; -import { WebLinksAddon } from 'xterm-addon-web-links'; - -import * as fit from '../lib/addons/fit/fit'; +// import { AttachAddon } from 'xterm-addon-attach'; +// import { FitAddon } from 'xterm-addon-fit'; +// import { SearchAddon, ISearchOptions } from 'xterm-addon-search'; +// import { WebLinksAddon } from 'xterm-addon-web-links'; // Pulling in the module's types relies on the above, it's looks a // little weird here as we're importing "this" module @@ -25,12 +28,15 @@ import { Terminal as TerminalType, ITerminalOptions } from 'xterm'; export interface IWindowWithTerminal extends Window { term: TerminalType; Terminal?: typeof TerminalType; + AttachAddon?: typeof AttachAddon; + FitAddon?: typeof FitAddon; + SearchAddon?: typeof SearchAddon; + WebLinksAddon?: typeof WebLinksAddon; } declare let window: IWindowWithTerminal; -Terminal.applyAddon(fit); - let term; +let fitAddon: FitAddon; let searchAddon: SearchAddon; let protocol; let socketURL; @@ -74,6 +80,10 @@ const disposeRecreateButtonHandler = () => { if (document.location.pathname === '/test') { window.Terminal = Terminal; + window.AttachAddon = AttachAddon; + window.FitAddon = FitAddon; + window.SearchAddon = SearchAddon; + window.WebLinksAddon = WebLinksAddon; } else { createTerminal(); document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler); @@ -95,6 +105,8 @@ function createTerminal(): void { typedTerm.loadAddon(new WebLinksAddon()); searchAddon = new SearchAddon(); typedTerm.loadAddon(searchAddon); + fitAddon = new FitAddon(); + typedTerm.loadAddon(fitAddon); window.term = term; // Expose `term` to window for debugging purposes term.onResize((size: { cols: number, rows: number }) => { @@ -111,7 +123,7 @@ function createTerminal(): void { socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/'; term.open(terminalContainer); - term.fit(); + fitAddon.fit(); term.focus(); addDomListener(paddingElement, 'change', setPadding); @@ -299,9 +311,9 @@ function addDomListener(element: HTMLElement, type: string, handler: (...args: a function updateTerminalSize(): void { const cols = parseInt((document.getElementById(`opt-cols`)).value, 10); const rows = parseInt((document.getElementById(`opt-rows`)).value, 10); - const width = (cols * term._core._renderCoordinator.dimensions.actualCellWidth + term._core.viewport.scrollBarWidth).toString() + 'px'; - const height = (rows * term._core._renderCoordinator.dimensions.actualCellHeight).toString() + 'px'; + const width = (cols * term._core._renderService.dimensions.actualCellWidth + term._core.viewport.scrollBarWidth).toString() + 'px'; + const height = (rows * term._core._renderService.dimensions.actualCellHeight).toString() + 'px'; terminalContainer.style.width = width; terminalContainer.style.height = height; - term.fit(); + fitAddon.fit(); } diff --git a/demo/server.js b/demo/server.js index 4b3f1bdab..3b170813b 100644 --- a/demo/server.js +++ b/demo/server.js @@ -40,7 +40,7 @@ function startServer() { var cols = parseInt(req.query.cols), rows = parseInt(req.query.rows), term = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : 'bash', [], { - name: 'xterm-color', + name: 'xterm-256color', cols: cols || 80, rows: rows || 24, cwd: process.env.PWD, diff --git a/demo/start.js b/demo/start.js index c9b90c8b6..a054e939c 100644 --- a/demo/start.js +++ b/demo/start.js @@ -43,12 +43,15 @@ const clientConfig = { ] }, resolve: { - modules: [path.resolve(__dirname, '..'), 'node_modules'], + modules: [ + 'node_modules', + path.resolve(__dirname, '..'), + path.resolve(__dirname, '../addons') + ], extensions: [ '.tsx', '.ts', '.js' ], alias: { common: path.resolve('./out/common'), - core: path.resolve('./out/core'), - ui: path.resolve('./out/ui') + browser: path.resolve('./out/browser') } }, output: { diff --git a/demo/test.html b/demo/test.html index 275c542de..c14ca4bcc 100644 --- a/demo/test.html +++ b/demo/test.html @@ -2,8 +2,8 @@ xterm.js integration test fixture - - + +
diff --git a/demo/tsconfig.json b/demo/tsconfig.json index 2edfe2f7c..2bf76f670 100644 --- a/demo/tsconfig.json +++ b/demo/tsconfig.json @@ -1,9 +1,16 @@ { "compilerOptions": { "module": "commonjs", - "target": "es5", + "target": "es2015", "rootDir": ".", - "sourceMap": true + "sourceMap": true, + "baseUrl": ".", + "paths": { + "xterm-addon-attach": ["../addons/xterm-addon-attach"], + "xterm-addon-fit": ["../addons/xterm-addon-fit"], + "xterm-addon-search": ["../addons/xterm-addon-search"], + "xterm-addon-web-links": ["../addons/xterm-addon-web-links"] + } }, "include": [ "client.ts", diff --git a/fixtures/typings-test/tsconfig.json b/fixtures/typings-test/tsconfig.json deleted file mode 100644 index 9cd56129f..000000000 --- a/fixtures/typings-test/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "files": [ - "typings-test.ts" - ], - "compilerOptions": { - "module": "commonjs", - "target": "es5", - "lib": [ - "dom", - "es6" - ], - "noEmit": true - } -} diff --git a/fixtures/typings-test/typings-test.ts b/fixtures/typings-test/typings-test.ts deleted file mode 100644 index 04f54920b..000000000 --- a/fixtures/typings-test/typings-test.ts +++ /dev/null @@ -1,249 +0,0 @@ -/** - * @license MIT - */ - -/// - -import { Terminal, IDisposable } from 'xterm'; - -namespace constructor { - { - new Terminal(); - new Terminal({}); - new Terminal({ - cols: 1, - rows: 1 - }); - new Terminal({ - 'cols': 1, - 'cursorBlink': true, - 'cursorStyle': 'block', - 'disableStdin': false, - 'rows': 1, - 'scrollback': 10, - 'tabStopWidth': 2 - }); - } -} - -namespace properties { - { - const t: Terminal = new Terminal(); - const element: HTMLElement = t.element; - const textarea: HTMLTextAreaElement = t.textarea; - } -} - -namespace static_methods { - { - Terminal.applyAddon({}); - Terminal.applyAddon({}); - Terminal.applyAddon({}); - Terminal.applyAddon({}); - Terminal.applyAddon({}); - Terminal.applyAddon({}); - } -} - -namespace methods_core { - { - const t: Terminal = new Terminal(); - t.blur(); - t.focus(); - t.clear(); - t.refresh(0, 1); - t.reset(); - t.resize(1, 1); - t.write('foo'); - t.writeln('foo'); - } - { - const t: Terminal = new Terminal(); - // no arg - t.on('blur', () => {}); - t.on('focus', () => {}); - t.on('linefeed', () => {}); - t.on('selection', () => {}); - // args - t.on('data', () => {}); - t.on('data', (data: string) => console.log(data)); - t.on('key', () => {}); - t.on('key', (key: string) => console.log(key, event)); - t.on('key', (key: string, event: KeyboardEvent) => console.log(key, event)); - t.on('keydown', () => {}); - t.on('keydown', (event: KeyboardEvent) => console.log(event)); - t.on('keypress', () => {}); - t.on('keypress', (event: KeyboardEvent) => console.log(event)); - t.on('refresh', () => {}); - t.on('refresh', (data: {start: number, end: number}) => console.log(data)); - t.on('resize', () => {}); - t.on('resize', (data: {cols: number, rows: number}) => console.log(data)); - t.on('scroll', () => {}); - t.on('scroll', (ydisp: number) => console.log(ydisp)); - t.on('title', () => {}); - t.on('title', (title: string) => console.log(title)); - } - { - const t: Terminal = new Terminal(); - // no arg - t.off('blur', () => {}); - t.off('focus', () => {}); - t.off('linefeed', () => {}); - t.off('selection', () => {}); - // args - t.off('data', () => {}); - t.off('data', (data: string) => console.log(data)); - t.off('key', () => {}); - t.off('key', (key: string) => console.log(key, event)); - t.off('key', (key: string, event: KeyboardEvent) => console.log(key, event)); - t.off('keydown', () => {}); - t.off('keydown', (event: KeyboardEvent) => console.log(event)); - t.off('keypress', () => {}); - t.off('keypress', (event: KeyboardEvent) => console.log(event)); - t.off('refresh', () => {}); - t.off('refresh', (data: {element: HTMLElement, start: number, end: number}) => console.log(data)); - t.off('resize', () => {}); - t.off('resize', (data: {terminal: Terminal, cols: number, rows: number}) => console.log(data)); - t.off('scroll', () => {}); - t.off('scroll', (ydisp: number) => console.log(ydisp)); - t.off('title', () => {}); - t.off('title', (title: string) => console.log(title)); - } - { - const t: Terminal = new Terminal(); - const e: HTMLElement = null; - t.open(e); - } - { - const t: Terminal = new Terminal(); - t.attachCustomKeyEventHandler((e: KeyboardEvent) => true); - t.attachCustomKeyEventHandler((e: KeyboardEvent) => false); - const d1: IDisposable = t.addCsiHandler('x', - (params: number[], collect: string): boolean => params[0] === 1); - d1.dispose(); - const d2: IDisposable = t.addOscHandler(199, - (data: string): boolean => true); - d2.dispose(); - } - namespace options { - { - const t: Terminal = new Terminal(); - const r01: string = t.getOption('cursorStyle'); - const r02: string = t.getOption('termName'); - const r03: boolean = t.getOption('cancelEvents'); - const r04: boolean = t.getOption('convertEol'); - const r05: boolean = t.getOption('cursorBlink'); - const r06: boolean = t.getOption('debug'); - const r07: boolean = t.getOption('disableStdin'); - const r08: boolean = t.getOption('popOnBell'); - const r09: boolean = t.getOption('screenKeys'); - const r10: boolean = t.getOption('useFlowControl'); - const r11: boolean = t.getOption('visualBell'); - const r12: string[] = t.getOption('colors'); - const r13: number = t.getOption('cols'); - const r14: number = t.getOption('rows'); - const r15: number = t.getOption('tabStopWidth'); - const r16: number = t.getOption('scrollback'); - const r18: (data: string) => void = t.getOption('handler'); - const r19: string = t.getOption('bellSound'); - const r20: string = t.getOption('bellStyle'); - const r22: number = t.getOption('letterSpacing'); - const r23: boolean = t.getOption('macOptionIsMeta'); - const r24: string = t.getOption('fontWeight'); - const r25: string = t.getOption('fontWeightBold'); - const r26: boolean = t.getOption('allowTransparency'); - const r27: boolean = t.getOption('rightClickSelectsWord'); - const r28: boolean = t.getOption('windowsMode'); - } - { - const t: Terminal = new Terminal(); - t.setOption('cursorStyle', 'bar'); - t.setOption('cursorStyle', 'block'); - t.setOption('cursorStyle', 'underline'); - t.setOption('termName', 'foo'); - t.setOption('cancelEvents', true); - t.setOption('convertEol', true); - t.setOption('cursorBlink', true); - t.setOption('debug', true); - t.setOption('disableStdin', true); - t.setOption('fontWeight', 'normal'); - t.setOption('fontWeight', 'bold'); - t.setOption('fontWeightBold', 'normal'); - t.setOption('fontWeightBold', 'bold'); - t.setOption('popOnBell', true); - t.setOption('screenKeys', true); - t.setOption('useFlowControl', true); - t.setOption('allowTransparency', true); - t.setOption('visualBell', true); - t.setOption('windowsMode', true); - t.setOption('colors', ['a', 'b']); - t.setOption('letterSpacing', 1); - t.setOption('cols', 1); - t.setOption('rows', 1); - t.setOption('tabStopWidth', 1); - t.setOption('scrollback', 1); - t.setOption('handler', (data: string) => console.log(data)); - t.setOption('bellSound', 'foo'); - t.setOption('bellStyle', 'none'); - // t.setOption('bellStyle', 'visual'); - t.setOption('bellStyle', 'sound'); - // t.setOption('bellStyle', 'both'); - t.setOption('fontSize', 1); - t.setOption('lineHeight', 1); - t.setOption('fontFamily', 'foo'); - t.setOption('theme', {background: '#ff0000'}); - t.setOption('macOptionIsMeta', true); - t.setOption('rightClickSelectsWord', false); - } - } - namespace scrolling { - { - const t: Terminal = new Terminal(); - t.scrollLines(-1); - t.scrollLines(1); - t.scrollLines(-1); - t.scrollLines(1); - t.scrollToTop(); - t.scrollToBottom(); - } - } - namespace selection { - { - const t: Terminal = new Terminal(); - const r1: boolean = t.hasSelection(); - const r2: string = t.getSelection(); - t.clearSelection(); - t.selectAll(); - } - } -} - -namespace methods_experimental { - { - const t: Terminal = new Terminal(); - t.registerLinkMatcher(/foo/, () => {}); - t.registerLinkMatcher(new RegExp('foo'), () => {}); - t.registerLinkMatcher(/foo/, () => {}, {}); - t.registerLinkMatcher(/foo/, (event: MouseEvent, uri: string) => { - console.log(event, uri); - return void 0; - }, {}); - t.registerLinkMatcher(/foo/, () => true, {}); - t.registerLinkMatcher(/foo/, () => false, {}); - t.registerLinkMatcher(/foo/, () => true, { - matchIndex: 1 - }); - t.registerLinkMatcher(/foo/, () => true, { - matchIndex: 1, - priority: 1, - validationCallback: (uri: string, callback: (isValid: boolean) => void) => { - console.log(uri, callback); - }, - tooltipCallback: (e: MouseEvent, uri: string) => { - console.log(e, uri); - }, - leaveCallback: () => {} - }); - t.deregisterLinkMatcher(1); - } -} diff --git a/package.json b/package.json index 0dd8e8f78..d291f1471 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,25 @@ "description": "Full xterm terminal, in your browser", "version": "3.14.0", "main": "lib/xterm.js", + "style": "css/xterm.css", "types": "typings/xterm.d.ts", "repository": "https://github.com/xtermjs/xterm.js", "license": "MIT", + "scripts": { + "prepackage": "npm run build", + "package": "webpack", + "start": "node demo/start", + "lint": "tslint 'src/**/*.ts' './demo/**/*.ts' './addons/**/*.ts'", + "test": "npm run test-unit", + "posttest": "npm run lint", + "test-api": "mocha \"**/*.api.js\"", + "test-unit": "node ./bin/test.js", + "build": "tsc -b ./tsconfig.all.json", + "prepare": "npm run build", + "prepublishOnly": "npm run package", + "watch": "tsc -b -w ./tsconfig.all.json --preserveWatchOutput", + "clean": "rm -rf lib out addons/*/lib" + }, "devDependencies": { "@types/chai": "^3.4.34", "@types/glob": "^5.0.35", @@ -15,6 +31,7 @@ "@types/puppeteer": "^1.12.4", "@types/utf8": "^2.1.6", "@types/webpack": "^4.4.11", + "@types/ws": "^6.0.1", "chai": "3.5.0", "express": "4.13.4", "express-ws": "2.0.0-rc.1", @@ -31,25 +48,6 @@ "utf8": "^3.0.0", "webpack": "^4.17.1", "webpack-cli": "^3.1.0", - "xterm-addon-attach": "0.1.0-beta8", - "xterm-addon-search": "0.1.0-beta4", - "xterm-addon-web-links": "0.1.0-beta6" - }, - "scripts": { - "prepackage": "npm run build", - "package": "webpack", - "start": "node demo/start", - "start-debug": "node --inspect-brk demo/start", - "start-zmodem": "node demo/zmodem/app", - "lint": "tslint 'src/**/*.ts' './demo/**/*.ts'", - "test-unit": "node ./bin/test.js", - "test": "npm run test-unit", - "posttest": "npm run lint", - "test-api": "mocha \"**/*.api.js\"", - "build": "tsc -b ./src/tsconfig.all.json", - "prepare": "npm run build", - "prepublishOnly": "npm run package", - "coveralls": "nyc report --reporter=text-lcov | coveralls", - "watch": "tsc -b -w ./src/tsconfig.all.json --preserveWatchOutput" + "ws": "^7.0.0" } } diff --git a/src/AccessibilityManager.ts b/src/AccessibilityManager.ts index a2a6209c3..9124a4775 100644 --- a/src/AccessibilityManager.ts +++ b/src/AccessibilityManager.ts @@ -3,14 +3,15 @@ * @license MIT */ -import * as Strings from './Strings'; -import { ITerminal, IBuffer } from './Types'; +import * as Strings from './browser/LocalizableStrings'; +import { ITerminal } from './Types'; +import { IBuffer } from 'common/buffer/Types'; import { isMac } from 'common/Platform'; -import { RenderDebouncer } from 'ui/RenderDebouncer'; -import { addDisposableDomListener } from 'ui/Lifecycle'; +import { RenderDebouncer } from 'browser/RenderDebouncer'; +import { addDisposableDomListener } from 'browser/Lifecycle'; import { Disposable } from 'common/Lifecycle'; -import { ScreenDprMonitor } from 'ui/ScreenDprMonitor'; -import { IRenderDimensions } from './renderer/Types'; +import { ScreenDprMonitor } from 'browser/ScreenDprMonitor'; +import { IRenderDimensions } from 'browser/renderer/Types'; const MAX_ROWS_TO_READ = 20; @@ -84,11 +85,11 @@ export class AccessibilityManager extends Disposable { this.register(this._terminal.onRender(e => this._refreshRows(e.start, e.end))); this.register(this._terminal.onScroll(() => this._refreshRows())); // Line feed is an issue as the prompt won't be read out after a command is run - this.register(this._terminal.addDisposableListener('a11y.char', (char) => this._onChar(char))); + this.register(this._terminal.onA11yChar(char => this._onChar(char))); this.register(this._terminal.onLineFeed(() => this._onChar('\n'))); - this.register(this._terminal.addDisposableListener('a11y.tab', spaceCount => this._onTab(spaceCount))); + this.register(this._terminal.onA11yTab(spaceCount => this._onTab(spaceCount))); this.register(this._terminal.onKey(e => this._onKey(e.key))); - this.register(this._terminal.addDisposableListener('blur', () => this._clearLiveRegion())); + this.register(this._terminal.onBlur(() => this._clearLiveRegion())); this._screenDprMonitor = new ScreenDprMonitor(); this.register(this._screenDprMonitor); diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts deleted file mode 100644 index e1572889e..000000000 --- a/src/Buffer.test.ts +++ /dev/null @@ -1,1403 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { assert, expect } from 'chai'; -import { ITerminal } from './Types'; -import { Buffer } from './Buffer'; -import { CircularList } from 'common/CircularList'; -import { MockTerminal, TestTerminal } from './TestUtils.test'; -import { BufferLine, CellData, DEFAULT_ATTR_DATA } from 'core/buffer/BufferLine'; - -const INIT_COLS = 80; -const INIT_ROWS = 24; - -describe('Buffer', () => { - let terminal: ITerminal; - let buffer: Buffer; - - beforeEach(() => { - terminal = new MockTerminal(); - (terminal as any).cols = INIT_COLS; - (terminal as any).rows = INIT_ROWS; - terminal.options.scrollback = 1000; - buffer = new Buffer(terminal, true); - }); - - describe('constructor', () => { - it('should create a CircularList with max length equal to rows + scrollback, for its lines', () => { - assert.instanceOf(buffer.lines, CircularList); - assert.equal(buffer.lines.maxLength, terminal.rows + terminal.options.scrollback); - }); - it('should set the Buffer\'s scrollBottom value equal to the terminal\'s rows -1', () => { - assert.equal(buffer.scrollBottom, terminal.rows - 1); - }); - }); - - describe('fillViewportRows', () => { - it('should fill the buffer with blank lines based on the size of the viewport', () => { - const blankLineChar = buffer.getBlankLine(DEFAULT_ATTR_DATA).loadCell(0, new CellData()).getAsCharData(); - buffer.fillViewportRows(); - assert.equal(buffer.lines.length, INIT_ROWS); - for (let y = 0; y < INIT_ROWS; y++) { - assert.equal(buffer.lines.get(y).length, INIT_COLS); - for (let x = 0; x < INIT_COLS; x++) { - assert.deepEqual(buffer.lines.get(y).loadCell(x, new CellData()).getAsCharData(), blankLineChar); - } - } - }); - }); - - describe('getWrappedRangeForLine', () => { - describe('non-wrapped', () => { - it('should return a single row for the first row', () => { - buffer.fillViewportRows(); - assert.deepEqual(buffer.getWrappedRangeForLine(0), { first: 0, last: 0 }); - }); - it('should return a single row for a middle row', () => { - buffer.fillViewportRows(); - assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 12, last: 12 }); - }); - it('should return a single row for the last row', () => { - buffer.fillViewportRows(); - assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 1), { first: 23, last: 23 }); - }); - }); - describe('wrapped', () => { - it('should return a range for the first row', () => { - buffer.fillViewportRows(); - buffer.lines.get(1).isWrapped = true; - assert.deepEqual(buffer.getWrappedRangeForLine(0), { first: 0, last: 1 }); - }); - it('should return a range for a middle row wrapping upwards', () => { - buffer.fillViewportRows(); - buffer.lines.get(12).isWrapped = true; - assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 11, last: 12 }); - }); - it('should return a range for a middle row wrapping downwards', () => { - buffer.fillViewportRows(); - buffer.lines.get(13).isWrapped = true; - assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 12, last: 13 }); - }); - it('should return a range for a middle row wrapping both ways', () => { - buffer.fillViewportRows(); - buffer.lines.get(11).isWrapped = true; - buffer.lines.get(12).isWrapped = true; - buffer.lines.get(13).isWrapped = true; - buffer.lines.get(14).isWrapped = true; - assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 10, last: 14 }); - }); - it('should return a range for the last row', () => { - buffer.fillViewportRows(); - buffer.lines.get(23).isWrapped = true; - assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 1), { first: 22, last: 23 }); - }); - it('should return a range for a row that wraps upward to first row', () => { - buffer.fillViewportRows(); - buffer.lines.get(1).isWrapped = true; - assert.deepEqual(buffer.getWrappedRangeForLine(1), { first: 0, last: 1 }); - }); - it('should return a range for a row that wraps downward to last row', () => { - buffer.fillViewportRows(); - buffer.lines.get(buffer.lines.length - 1).isWrapped = true; - assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 2), { first: 22, last: 23 }); - }); - }); - }); - - describe('resize', () => { - describe('column size is reduced', () => { - it('should trim the data in the buffer', () => { - buffer.fillViewportRows(); - buffer.resize(INIT_COLS / 2, INIT_ROWS); - assert.equal(buffer.lines.length, INIT_ROWS); - for (let i = 0; i < INIT_ROWS; i++) { - assert.equal(buffer.lines.get(i).length, INIT_COLS / 2); - } - }); - }); - - describe('column size is increased', () => { - it('should add pad columns', () => { - buffer.fillViewportRows(); - buffer.resize(INIT_COLS + 10, INIT_ROWS); - assert.equal(buffer.lines.length, INIT_ROWS); - for (let i = 0; i < INIT_ROWS; i++) { - assert.equal(buffer.lines.get(i).length, INIT_COLS + 10); - } - }); - }); - - describe('row size reduced', () => { - it('should trim blank lines from the end', () => { - buffer.fillViewportRows(); - buffer.resize(INIT_COLS, INIT_ROWS - 10); - assert.equal(buffer.lines.length, INIT_ROWS - 10); - }); - - it('should move the viewport down when it\'s at the end', () => { - buffer.fillViewportRows(); - // Set cursor y to have 5 blank lines below it - buffer.y = INIT_ROWS - 5 - 1; - buffer.resize(INIT_COLS, INIT_ROWS - 10); - // Trim 5 rows - assert.equal(buffer.lines.length, INIT_ROWS - 5); - // Shift the viewport down 5 rows - assert.equal(buffer.ydisp, 5); - assert.equal(buffer.ybase, 5); - }); - - describe('no scrollback', () => { - it('should trim from the top of the buffer when the cursor reaches the bottom', () => { - terminal.options.scrollback = 0; - buffer = new Buffer(terminal, true); - assert.equal(buffer.lines.maxLength, INIT_ROWS); - buffer.y = INIT_ROWS - 1; - buffer.fillViewportRows(); - let chData = buffer.lines.get(5).loadCell(0, new CellData()).getAsCharData(); - chData[1] = 'a'; - buffer.lines.get(5).setCell(0, CellData.fromCharData(chData)); - chData = buffer.lines.get(INIT_ROWS - 1).loadCell(0, new CellData()).getAsCharData(); - chData[1] = 'b'; - buffer.lines.get(INIT_ROWS - 1).setCell(0, CellData.fromCharData(chData)); - buffer.resize(INIT_COLS, INIT_ROWS - 5); - assert.equal(buffer.lines.get(0).loadCell(0, new CellData()).getAsCharData()[1], 'a'); - assert.equal(buffer.lines.get(INIT_ROWS - 1 - 5).loadCell(0, new CellData()).getAsCharData()[1], 'b'); - }); - }); - }); - - describe('row size increased', () => { - describe('empty buffer', () => { - it('should add blank lines to end', () => { - buffer.fillViewportRows(); - assert.equal(buffer.ydisp, 0); - buffer.resize(INIT_COLS, INIT_ROWS + 10); - assert.equal(buffer.ydisp, 0); - assert.equal(buffer.lines.length, INIT_ROWS + 10); - }); - }); - - describe('filled buffer', () => { - it('should show more of the buffer above', () => { - buffer.fillViewportRows(); - // Create 10 extra blank lines - for (let i = 0; i < 10; i++) { - buffer.lines.push(buffer.getBlankLine(DEFAULT_ATTR_DATA)); - } - // Set cursor to the bottom of the buffer - buffer.y = INIT_ROWS - 1; - // Scroll down 10 lines - buffer.ybase = 10; - buffer.ydisp = 10; - assert.equal(buffer.lines.length, INIT_ROWS + 10); - buffer.resize(INIT_COLS, INIT_ROWS + 5); - // Should be should 5 more lines - assert.equal(buffer.ydisp, 5); - assert.equal(buffer.ybase, 5); - // Should not trim the buffer - assert.equal(buffer.lines.length, INIT_ROWS + 10); - }); - - it('should show more of the buffer below when the viewport is at the top of the buffer', () => { - buffer.fillViewportRows(); - // Create 10 extra blank lines - for (let i = 0; i < 10; i++) { - buffer.lines.push(buffer.getBlankLine(DEFAULT_ATTR_DATA)); - } - // Set cursor to the bottom of the buffer - buffer.y = INIT_ROWS - 1; - // Scroll down 10 lines - buffer.ybase = 10; - buffer.ydisp = 0; - assert.equal(buffer.lines.length, INIT_ROWS + 10); - buffer.resize(INIT_COLS, INIT_ROWS + 5); - // The viewport should remain at the top - assert.equal(buffer.ydisp, 0); - // The buffer ybase should move up 5 lines - assert.equal(buffer.ybase, 5); - // Should not trim the buffer - assert.equal(buffer.lines.length, INIT_ROWS + 10); - }); - }); - }); - - describe('row and column increased', () => { - it('should resize properly', () => { - buffer.fillViewportRows(); - buffer.resize(INIT_COLS + 5, INIT_ROWS + 5); - assert.equal(buffer.lines.length, INIT_ROWS + 5); - for (let i = 0; i < INIT_ROWS + 5; i++) { - assert.equal(buffer.lines.get(i).length, INIT_COLS + 5); - } - }); - }); - - describe('reflow', () => { - it('should not wrap empty lines', () => { - buffer.fillViewportRows(); - assert.equal(buffer.lines.length, INIT_ROWS); - buffer.resize(INIT_COLS - 5, INIT_ROWS); - assert.equal(buffer.lines.length, INIT_ROWS); - }); - it('should shrink row length', () => { - buffer.fillViewportRows(); - buffer.resize(5, 10); - assert.equal(buffer.lines.length, 10); - assert.equal(buffer.lines.get(0).length, 5); - assert.equal(buffer.lines.get(1).length, 5); - assert.equal(buffer.lines.get(2).length, 5); - assert.equal(buffer.lines.get(3).length, 5); - assert.equal(buffer.lines.get(4).length, 5); - assert.equal(buffer.lines.get(5).length, 5); - assert.equal(buffer.lines.get(6).length, 5); - assert.equal(buffer.lines.get(7).length, 5); - assert.equal(buffer.lines.get(8).length, 5); - assert.equal(buffer.lines.get(9).length, 5); - }); - it('should wrap and unwrap lines', () => { - buffer.fillViewportRows(); - buffer.resize(5, 10); - const firstLine = buffer.lines.get(0); - for (let i = 0; i < 5; i++) { - const code = 'a'.charCodeAt(0) + i; - const char = String.fromCharCode(code); - firstLine.set(i, [null, char, 1, code]); - } - buffer.y = 1; - assert.equal(buffer.lines.get(0).length, 5); - assert.equal(buffer.lines.get(0).translateToString(), 'abcde'); - buffer.resize(1, 10); - assert.equal(buffer.lines.length, 10); - assert.equal(buffer.lines.get(0).translateToString(), 'a'); - assert.equal(buffer.lines.get(1).translateToString(), 'b'); - assert.equal(buffer.lines.get(2).translateToString(), 'c'); - assert.equal(buffer.lines.get(3).translateToString(), 'd'); - assert.equal(buffer.lines.get(4).translateToString(), 'e'); - assert.equal(buffer.lines.get(5).translateToString(), ' '); - assert.equal(buffer.lines.get(6).translateToString(), ' '); - assert.equal(buffer.lines.get(7).translateToString(), ' '); - assert.equal(buffer.lines.get(8).translateToString(), ' '); - assert.equal(buffer.lines.get(9).translateToString(), ' '); - buffer.resize(5, 10); - assert.equal(buffer.lines.length, 10); - assert.equal(buffer.lines.get(0).translateToString(), 'abcde'); - assert.equal(buffer.lines.get(1).translateToString(), ' '); - assert.equal(buffer.lines.get(2).translateToString(), ' '); - assert.equal(buffer.lines.get(3).translateToString(), ' '); - assert.equal(buffer.lines.get(4).translateToString(), ' '); - assert.equal(buffer.lines.get(5).translateToString(), ' '); - assert.equal(buffer.lines.get(6).translateToString(), ' '); - assert.equal(buffer.lines.get(7).translateToString(), ' '); - assert.equal(buffer.lines.get(8).translateToString(), ' '); - assert.equal(buffer.lines.get(9).translateToString(), ' '); - }); - it('should discard parts of wrapped lines that go out of the scrollback', () => { - buffer.fillViewportRows(); - terminal.options.scrollback = 1; - buffer.resize(10, 5); - const lastLine = buffer.lines.get(3); - for (let i = 0; i < 10; i++) { - const code = 'a'.charCodeAt(0) + i; - const char = String.fromCharCode(code); - lastLine.set(i, [null, char, 1, code]); - } - assert.equal(buffer.lines.length, 5); - buffer.y = 4; - buffer.resize(2, 5); - assert.equal(buffer.y, 4); - assert.equal(buffer.ybase, 1); - assert.equal(buffer.lines.length, 6); - assert.equal(buffer.lines.get(0).translateToString(), 'ab'); - assert.equal(buffer.lines.get(1).translateToString(), 'cd'); - assert.equal(buffer.lines.get(2).translateToString(), 'ef'); - assert.equal(buffer.lines.get(3).translateToString(), 'gh'); - assert.equal(buffer.lines.get(4).translateToString(), 'ij'); - assert.equal(buffer.lines.get(5).translateToString(), ' '); - buffer.resize(1, 5); - assert.equal(buffer.y, 4); - assert.equal(buffer.ybase, 1); - assert.equal(buffer.lines.length, 6); - assert.equal(buffer.lines.get(0).translateToString(), 'f'); - assert.equal(buffer.lines.get(1).translateToString(), 'g'); - assert.equal(buffer.lines.get(2).translateToString(), 'h'); - assert.equal(buffer.lines.get(3).translateToString(), 'i'); - assert.equal(buffer.lines.get(4).translateToString(), 'j'); - assert.equal(buffer.lines.get(5).translateToString(), ' '); - buffer.resize(10, 5); - assert.equal(buffer.y, 1); - assert.equal(buffer.ybase, 0); - assert.equal(buffer.lines.length, 5); - assert.equal(buffer.lines.get(0).translateToString(), 'fghij '); - assert.equal(buffer.lines.get(1).translateToString(), ' '); - assert.equal(buffer.lines.get(2).translateToString(), ' '); - assert.equal(buffer.lines.get(3).translateToString(), ' '); - assert.equal(buffer.lines.get(4).translateToString(), ' '); - }); - it('should remove the correct amount of rows when reflowing larger', () => { - // This is a regression test to ensure that successive wrapped lines that are getting - // 3+ lines removed on a reflow actually remove the right lines - buffer.fillViewportRows(); - buffer.resize(10, 10); - buffer.y = 2; - const firstLine = buffer.lines.get(0); - const secondLine = buffer.lines.get(1); - for (let i = 0; i < 10; i++) { - const code = 'a'.charCodeAt(0) + i; - const char = String.fromCharCode(code); - firstLine.set(i, [null, char, 1, code]); - } - for (let i = 0; i < 10; i++) { - const code = '0'.charCodeAt(0) + i; - const char = String.fromCharCode(code); - secondLine.set(i, [null, char, 1, code]); - } - assert.equal(buffer.lines.length, 10); - assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij'); - assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); - for (let i = 2; i < 10; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - buffer.resize(2, 10); - assert.equal(buffer.ybase, 1); - assert.equal(buffer.lines.length, 11); - assert.equal(buffer.lines.get(0).translateToString(), 'ab'); - assert.equal(buffer.lines.get(1).translateToString(), 'cd'); - assert.equal(buffer.lines.get(2).translateToString(), 'ef'); - assert.equal(buffer.lines.get(3).translateToString(), 'gh'); - assert.equal(buffer.lines.get(4).translateToString(), 'ij'); - assert.equal(buffer.lines.get(5).translateToString(), '01'); - assert.equal(buffer.lines.get(6).translateToString(), '23'); - assert.equal(buffer.lines.get(7).translateToString(), '45'); - assert.equal(buffer.lines.get(8).translateToString(), '67'); - assert.equal(buffer.lines.get(9).translateToString(), '89'); - assert.equal(buffer.lines.get(10).translateToString(), ' '); - buffer.resize(10, 10); - assert.equal(buffer.ybase, 0); - assert.equal(buffer.lines.length, 10); - assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij'); - assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); - for (let i = 2; i < 10; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - }); - it('should transfer combined char data over to reflowed lines', () => { - buffer.fillViewportRows(); - buffer.resize(4, 3); - buffer.y = 2; - const firstLine = buffer.lines.get(0); - firstLine.set(0, [ null, 'a', 1, 'a'.charCodeAt(0) ]); - firstLine.set(1, [ null, 'b', 1, 'b'.charCodeAt(0) ]); - firstLine.set(2, [ null, 'c', 1, 'c'.charCodeAt(0) ]); - firstLine.set(3, [ null, '😁', 1, '😁'.charCodeAt(0) ]); - assert.equal(buffer.lines.length, 3); - assert.equal(buffer.lines.get(0).translateToString(), 'abc😁'); - assert.equal(buffer.lines.get(1).translateToString(), ' '); - buffer.resize(2, 3); - assert.equal(buffer.lines.get(0).translateToString(), 'ab'); - assert.equal(buffer.lines.get(1).translateToString(), 'c😁'); - }); - it('should adjust markers when reflowing', () => { - buffer.fillViewportRows(); - buffer.resize(10, 16); - for (let i = 0; i < 10; i++) { - const code = 'a'.charCodeAt(0) + i; - const char = String.fromCharCode(code); - buffer.lines.get(0).set(i, [null, char, 1, code]); - } - for (let i = 0; i < 10; i++) { - const code = '0'.charCodeAt(0) + i; - const char = String.fromCharCode(code); - buffer.lines.get(1).set(i, [null, char, 1, code]); - } - for (let i = 0; i < 10; i++) { - const code = 'k'.charCodeAt(0) + i; - const char = String.fromCharCode(code); - buffer.lines.get(2).set(i, [null, char, 1, code]); - } - buffer.y = 3; - // Buffer: - // abcdefghij - // 0123456789 - // abcdefghij - const firstMarker = buffer.addMarker(0); - const secondMarker = buffer.addMarker(1); - const thirdMarker = buffer.addMarker(2); - assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij'); - assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); - assert.equal(buffer.lines.get(2).translateToString(), 'klmnopqrst'); - assert.equal(firstMarker.line, 0); - assert.equal(secondMarker.line, 1); - assert.equal(thirdMarker.line, 2); - buffer.resize(2, 16); - assert.equal(buffer.lines.get(0).translateToString(), 'ab'); - assert.equal(buffer.lines.get(1).translateToString(), 'cd'); - assert.equal(buffer.lines.get(2).translateToString(), 'ef'); - assert.equal(buffer.lines.get(3).translateToString(), 'gh'); - assert.equal(buffer.lines.get(4).translateToString(), 'ij'); - assert.equal(buffer.lines.get(5).translateToString(), '01'); - assert.equal(buffer.lines.get(6).translateToString(), '23'); - assert.equal(buffer.lines.get(7).translateToString(), '45'); - assert.equal(buffer.lines.get(8).translateToString(), '67'); - assert.equal(buffer.lines.get(9).translateToString(), '89'); - assert.equal(buffer.lines.get(10).translateToString(), 'kl'); - assert.equal(buffer.lines.get(11).translateToString(), 'mn'); - assert.equal(buffer.lines.get(12).translateToString(), 'op'); - assert.equal(buffer.lines.get(13).translateToString(), 'qr'); - assert.equal(buffer.lines.get(14).translateToString(), 'st'); - assert.equal(firstMarker.line, 0, 'first marker should remain unchanged'); - assert.equal(secondMarker.line, 5, 'second marker should be shifted since the first line wrapped'); - assert.equal(thirdMarker.line, 10, 'third marker should be shifted since the first and second lines wrapped'); - buffer.resize(10, 16); - assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij'); - assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); - assert.equal(buffer.lines.get(2).translateToString(), 'klmnopqrst'); - assert.equal(firstMarker.line, 0, 'first marker should remain unchanged'); - assert.equal(secondMarker.line, 1, 'second marker should be restored to it\'s original line'); - assert.equal(thirdMarker.line, 2, 'third marker should be restored to it\'s original line'); - assert.equal(firstMarker.isDisposed, false); - assert.equal(secondMarker.isDisposed, false); - assert.equal(thirdMarker.isDisposed, false); - }); - it('should dispose markers whose rows are trimmed during a reflow', () => { - buffer.fillViewportRows(); - terminal.options.scrollback = 1; - buffer.resize(10, 11); - for (let i = 0; i < 10; i++) { - const code = 'a'.charCodeAt(0) + i; - const char = String.fromCharCode(code); - buffer.lines.get(0).set(i, [null, char, 1, code]); - } - for (let i = 0; i < 10; i++) { - const code = '0'.charCodeAt(0) + i; - const char = String.fromCharCode(code); - buffer.lines.get(1).set(i, [null, char, 1, code]); - } - for (let i = 0; i < 10; i++) { - const code = 'k'.charCodeAt(0) + i; - const char = String.fromCharCode(code); - buffer.lines.get(2).set(i, [null, char, 1, code]); - } - buffer.y = 10; - // Buffer: - // abcdefghij - // 0123456789 - // abcdefghij - const firstMarker = buffer.addMarker(0); - const secondMarker = buffer.addMarker(1); - const thirdMarker = buffer.addMarker(2); - buffer.y = 3; - assert.equal(buffer.lines.get(0).translateToString(), 'abcdefghij'); - assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); - assert.equal(buffer.lines.get(2).translateToString(), 'klmnopqrst'); - assert.equal(firstMarker.line, 0); - assert.equal(secondMarker.line, 1); - assert.equal(thirdMarker.line, 2); - buffer.resize(2, 11); - assert.equal(buffer.lines.get(0).translateToString(), 'ij'); - assert.equal(buffer.lines.get(1).translateToString(), '01'); - assert.equal(buffer.lines.get(2).translateToString(), '23'); - assert.equal(buffer.lines.get(3).translateToString(), '45'); - assert.equal(buffer.lines.get(4).translateToString(), '67'); - assert.equal(buffer.lines.get(5).translateToString(), '89'); - assert.equal(buffer.lines.get(6).translateToString(), 'kl'); - assert.equal(buffer.lines.get(7).translateToString(), 'mn'); - assert.equal(buffer.lines.get(8).translateToString(), 'op'); - assert.equal(buffer.lines.get(9).translateToString(), 'qr'); - assert.equal(buffer.lines.get(10).translateToString(), 'st'); - assert.equal(secondMarker.line, 1, 'second marker should remain the same as it was shifted 4 and trimmed 4'); - assert.equal(thirdMarker.line, 6, 'third marker should be shifted since the first and second lines wrapped'); - assert.equal(firstMarker.isDisposed, true, 'first marker was trimmed'); - assert.equal(secondMarker.isDisposed, false); - assert.equal(thirdMarker.isDisposed, false); - buffer.resize(10, 11); - assert.equal(buffer.lines.get(0).translateToString(), 'ij '); - assert.equal(buffer.lines.get(1).translateToString(), '0123456789'); - assert.equal(buffer.lines.get(2).translateToString(), 'klmnopqrst'); - assert.equal(secondMarker.line, 1, 'second marker should be restored'); - assert.equal(thirdMarker.line, 2, 'third marker should be restored'); - }); - it('should correctly reflow wrapped lines that end in null space (via tab char)', () => { - buffer.fillViewportRows(); - buffer.resize(4, 10); - buffer.y = 2; - buffer.lines.get(0).set(0, [null, 'a', 1, 'a'.charCodeAt(0)]); - buffer.lines.get(0).set(1, [null, 'b', 1, 'b'.charCodeAt(0)]); - buffer.lines.get(1).set(0, [null, 'c', 1, 'c'.charCodeAt(0)]); - buffer.lines.get(1).set(1, [null, 'd', 1, 'd'.charCodeAt(0)]); - buffer.lines.get(1).isWrapped = true; - // Buffer: - // "ab " (wrapped) - // "cd" - buffer.resize(5, 10); - assert.equal(buffer.ybase, 0); - assert.equal(buffer.lines.length, 10); - assert.equal(buffer.lines.get(0).translateToString(true), 'ab c'); - assert.equal(buffer.lines.get(1).translateToString(false), 'd '); - buffer.resize(6, 10); - assert.equal(buffer.ybase, 0); - assert.equal(buffer.lines.length, 10); - assert.equal(buffer.lines.get(0).translateToString(true), 'ab cd'); - assert.equal(buffer.lines.get(1).translateToString(false), ' '); - }); - it('should wrap wide characters correctly when reflowing larger', () => { - buffer.fillViewportRows(); - buffer.resize(12, 10); - buffer.y = 2; - for (let i = 0; i < 12; i += 4) { - buffer.lines.get(0).set(i, [null, '汉', 2, '汉'.charCodeAt(0)]); - buffer.lines.get(1).set(i, [null, '汉', 2, '汉'.charCodeAt(0)]); - } - for (let i = 2; i < 12; i += 4) { - buffer.lines.get(0).set(i, [null, '语', 2, '语'.charCodeAt(0)]); - buffer.lines.get(1).set(i, [null, '语', 2, '语'.charCodeAt(0)]); - } - for (let i = 1; i < 12; i += 2) { - buffer.lines.get(0).set(i, [null, '', 0, undefined]); - buffer.lines.get(1).set(i, [null, '', 0, undefined]); - } - buffer.lines.get(1).isWrapped = true; - // Buffer: - // 汉语汉语汉语 (wrapped) - // 汉语汉语汉语 - assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉语'); - assert.equal(buffer.lines.get(1).translateToString(true), '汉语汉语汉语'); - buffer.resize(13, 10); - assert.equal(buffer.ybase, 0); - assert.equal(buffer.lines.length, 10); - assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉语'); - assert.equal(buffer.lines.get(0).translateToString(false), '汉语汉语汉语 '); - assert.equal(buffer.lines.get(1).translateToString(true), '汉语汉语汉语'); - assert.equal(buffer.lines.get(1).translateToString(false), '汉语汉语汉语 '); - buffer.resize(14, 10); - assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉语汉'); - assert.equal(buffer.lines.get(0).translateToString(false), '汉语汉语汉语汉'); - assert.equal(buffer.lines.get(1).translateToString(true), '语汉语汉语'); - assert.equal(buffer.lines.get(1).translateToString(false), '语汉语汉语 '); - }); - it('should correctly reflow wrapped lines that end in null space (via tab char)', () => { - buffer.fillViewportRows(); - buffer.resize(4, 10); - buffer.y = 2; - buffer.lines.get(0).set(0, [null, 'a', 1, 'a'.charCodeAt(0)]); - buffer.lines.get(0).set(1, [null, 'b', 1, 'b'.charCodeAt(0)]); - buffer.lines.get(1).set(0, [null, 'c', 1, 'c'.charCodeAt(0)]); - buffer.lines.get(1).set(1, [null, 'd', 1, 'd'.charCodeAt(0)]); - buffer.lines.get(1).isWrapped = true; - // Buffer: - // "ab " (wrapped) - // "cd" - buffer.resize(3, 10); - assert.equal(buffer.y, 2); - assert.equal(buffer.ybase, 0); - assert.equal(buffer.lines.length, 10); - assert.equal(buffer.lines.get(0).translateToString(false), 'ab '); - assert.equal(buffer.lines.get(1).translateToString(false), ' cd'); - buffer.resize(2, 10); - assert.equal(buffer.y, 3); - assert.equal(buffer.ybase, 0); - assert.equal(buffer.lines.length, 10); - assert.equal(buffer.lines.get(0).translateToString(false), 'ab'); - assert.equal(buffer.lines.get(1).translateToString(false), ' '); - assert.equal(buffer.lines.get(2).translateToString(false), 'cd'); - }); - it('should wrap wide characters correctly when reflowing smaller', () => { - buffer.fillViewportRows(); - buffer.resize(12, 10); - buffer.y = 2; - for (let i = 0; i < 12; i += 4) { - buffer.lines.get(0).set(i, [null, '汉', 2, '汉'.charCodeAt(0)]); - buffer.lines.get(1).set(i, [null, '汉', 2, '汉'.charCodeAt(0)]); - } - for (let i = 2; i < 12; i += 4) { - buffer.lines.get(0).set(i, [null, '语', 2, '语'.charCodeAt(0)]); - buffer.lines.get(1).set(i, [null, '语', 2, '语'.charCodeAt(0)]); - } - for (let i = 1; i < 12; i += 2) { - buffer.lines.get(0).set(i, [null, '', 0, undefined]); - buffer.lines.get(1).set(i, [null, '', 0, undefined]); - } - buffer.lines.get(1).isWrapped = true; - // Buffer: - // 汉语汉语汉语 (wrapped) - // 汉语汉语汉语 - assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉语'); - assert.equal(buffer.lines.get(1).translateToString(true), '汉语汉语汉语'); - buffer.resize(11, 10); - assert.equal(buffer.ybase, 0); - assert.equal(buffer.lines.length, 10); - assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉'); - assert.equal(buffer.lines.get(1).translateToString(true), '语汉语汉语'); - assert.equal(buffer.lines.get(2).translateToString(true), '汉语'); - buffer.resize(10, 10); - assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语汉'); - assert.equal(buffer.lines.get(1).translateToString(true), '语汉语汉语'); - assert.equal(buffer.lines.get(2).translateToString(true), '汉语'); - buffer.resize(9, 10); - assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语'); - assert.equal(buffer.lines.get(1).translateToString(true), '汉语汉语'); - assert.equal(buffer.lines.get(2).translateToString(true), '汉语汉语'); - buffer.resize(8, 10); - assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉语'); - assert.equal(buffer.lines.get(1).translateToString(true), '汉语汉语'); - assert.equal(buffer.lines.get(2).translateToString(true), '汉语汉语'); - buffer.resize(7, 10); - assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉'); - assert.equal(buffer.lines.get(1).translateToString(true), '语汉语'); - assert.equal(buffer.lines.get(2).translateToString(true), '汉语汉'); - assert.equal(buffer.lines.get(3).translateToString(true), '语汉语'); - buffer.resize(6, 10); - assert.equal(buffer.lines.get(0).translateToString(true), '汉语汉'); - assert.equal(buffer.lines.get(1).translateToString(true), '语汉语'); - assert.equal(buffer.lines.get(2).translateToString(true), '汉语汉'); - assert.equal(buffer.lines.get(3).translateToString(true), '语汉语'); - }); - - describe('reflowLarger cases', () => { - beforeEach(() => { - // Setup buffer state: - // 'ab' - // 'cd' (wrapped) - // 'ef' - // 'gh' (wrapped) - // 'ij' - // 'kl' (wrapped) - // ' ' - // ' ' - // ' ' - // ' ' - buffer.fillViewportRows(); - buffer.resize(2, 10); - buffer.lines.get(0).set(0, [null, 'a', 1, 'a'.charCodeAt(0)]); - buffer.lines.get(0).set(1, [null, 'b', 1, 'b'.charCodeAt(0)]); - buffer.lines.get(1).set(0, [null, 'c', 1, 'c'.charCodeAt(0)]); - buffer.lines.get(1).set(1, [null, 'd', 1, 'd'.charCodeAt(0)]); - buffer.lines.get(1).isWrapped = true; - buffer.lines.get(2).set(0, [null, 'e', 1, 'e'.charCodeAt(0)]); - buffer.lines.get(2).set(1, [null, 'f', 1, 'f'.charCodeAt(0)]); - buffer.lines.get(3).set(0, [null, 'g', 1, 'g'.charCodeAt(0)]); - buffer.lines.get(3).set(1, [null, 'h', 1, 'h'.charCodeAt(0)]); - buffer.lines.get(3).isWrapped = true; - buffer.lines.get(4).set(0, [null, 'i', 1, 'i'.charCodeAt(0)]); - buffer.lines.get(4).set(1, [null, 'j', 1, 'j'.charCodeAt(0)]); - buffer.lines.get(5).set(0, [null, 'k', 1, 'k'.charCodeAt(0)]); - buffer.lines.get(5).set(1, [null, 'l', 1, 'l'.charCodeAt(0)]); - buffer.lines.get(5).isWrapped = true; - }); - describe('viewport not yet filled', () => { - it('should move the cursor up and add empty lines', () => { - buffer.y = 6; - buffer.resize(4, 10); - assert.equal(buffer.y, 3); - assert.equal(buffer.ydisp, 0); - assert.equal(buffer.ybase, 0); - assert.equal(buffer.lines.length, 10); - assert.equal(buffer.lines.get(0).translateToString(), 'abcd'); - assert.equal(buffer.lines.get(1).translateToString(), 'efgh'); - assert.equal(buffer.lines.get(2).translateToString(), 'ijkl'); - for (let i = 3; i < 10; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - const wrappedLines: number[] = []; - for (let i = 0; i < buffer.lines.length; i++) { - assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); - } - }); - }); - describe('viewport filled, scrollback remaining', () => { - beforeEach(() => { - buffer.y = 9; - }); - describe('ybase === 0', () => { - it('should move the cursor up and add empty lines', () => { - buffer.resize(4, 10); - assert.equal(buffer.y, 6); - assert.equal(buffer.ydisp, 0); - assert.equal(buffer.ybase, 0); - assert.equal(buffer.lines.length, 10); - assert.equal(buffer.lines.get(0).translateToString(), 'abcd'); - assert.equal(buffer.lines.get(1).translateToString(), 'efgh'); - assert.equal(buffer.lines.get(2).translateToString(), 'ijkl'); - for (let i = 3; i < 10; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - const wrappedLines: number[] = []; - for (let i = 0; i < buffer.lines.length; i++) { - assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); - } - }); - }); - describe('ybase !== 0', () => { - beforeEach(() => { - // Add 10 empty rows to start - for (let i = 0; i < 10; i++) { - buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); - } - buffer.ybase = 10; - }); - describe('&& ydisp === ybase', () => { - it('should adjust the viewport and keep ydisp = ybase', () => { - buffer.ydisp = 10; - buffer.resize(4, 10); - assert.equal(buffer.y, 9); - assert.equal(buffer.ydisp, 7); - assert.equal(buffer.ybase, 7); - assert.equal(buffer.lines.length, 17); - for (let i = 0; i < 10; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - assert.equal(buffer.lines.get(10).translateToString(), 'abcd'); - assert.equal(buffer.lines.get(11).translateToString(), 'efgh'); - assert.equal(buffer.lines.get(12).translateToString(), 'ijkl'); - for (let i = 13; i < 17; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - const wrappedLines: number[] = []; - for (let i = 0; i < buffer.lines.length; i++) { - assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); - } - }); - }); - describe('&& ydisp !== ybase', () => { - it('should keep ydisp at the same value', () => { - buffer.ydisp = 5; - buffer.resize(4, 10); - assert.equal(buffer.y, 9); - assert.equal(buffer.ydisp, 5); - assert.equal(buffer.ybase, 7); - assert.equal(buffer.lines.length, 17); - for (let i = 0; i < 10; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - assert.equal(buffer.lines.get(10).translateToString(), 'abcd'); - assert.equal(buffer.lines.get(11).translateToString(), 'efgh'); - assert.equal(buffer.lines.get(12).translateToString(), 'ijkl'); - for (let i = 13; i < 17; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - const wrappedLines: number[] = []; - for (let i = 0; i < buffer.lines.length; i++) { - assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); - } - }); - }); - }); - }); - describe('viewport filled, no scrollback remaining', () => { - // ybase === 0 doesn't make sense here as scrollback=0 isn't really supported - describe('ybase !== 0', () => { - beforeEach(() => { - terminal.options.scrollback = 10; - // Add 10 empty rows to start - for (let i = 0; i < 10; i++) { - buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); - } - buffer.y = 9; - buffer.ybase = 10; - }); - describe('&& ydisp === ybase', () => { - it('should trim lines and keep ydisp = ybase', () => { - buffer.ydisp = 10; - buffer.resize(4, 10); - assert.equal(buffer.y, 9); - assert.equal(buffer.ydisp, 7); - assert.equal(buffer.ybase, 7); - assert.equal(buffer.lines.length, 17); - for (let i = 0; i < 10; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - assert.equal(buffer.lines.get(10).translateToString(), 'abcd'); - assert.equal(buffer.lines.get(11).translateToString(), 'efgh'); - assert.equal(buffer.lines.get(12).translateToString(), 'ijkl'); - for (let i = 13; i < 17; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - const wrappedLines: number[] = []; - for (let i = 0; i < buffer.lines.length; i++) { - assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); - } - }); - }); - describe('&& ydisp !== ybase', () => { - it('should trim lines and not change ydisp', () => { - buffer.ydisp = 5; - buffer.resize(4, 10); - assert.equal(buffer.y, 9); - assert.equal(buffer.ydisp, 5); - assert.equal(buffer.ybase, 7); - assert.equal(buffer.lines.length, 17); - for (let i = 0; i < 10; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - assert.equal(buffer.lines.get(10).translateToString(), 'abcd'); - assert.equal(buffer.lines.get(11).translateToString(), 'efgh'); - assert.equal(buffer.lines.get(12).translateToString(), 'ijkl'); - for (let i = 13; i < 17; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - const wrappedLines: number[] = []; - for (let i = 0; i < buffer.lines.length; i++) { - assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); - } - }); - }); - }); - }); - }); - describe('reflowSmaller cases', () => { - beforeEach(() => { - // Setup buffer state: - // 'abcd' - // 'efgh' (wrapped) - // 'ijkl' - // ' ' - // ' ' - // ' ' - // ' ' - // ' ' - // ' ' - // ' ' - buffer.fillViewportRows(); - buffer.resize(4, 10); - buffer.lines.get(0).set(0, [null, 'a', 1, 'a'.charCodeAt(0)]); - buffer.lines.get(0).set(1, [null, 'b', 1, 'b'.charCodeAt(0)]); - buffer.lines.get(0).set(2, [null, 'c', 1, 'c'.charCodeAt(0)]); - buffer.lines.get(0).set(3, [null, 'd', 1, 'd'.charCodeAt(0)]); - buffer.lines.get(1).set(0, [null, 'e', 1, 'e'.charCodeAt(0)]); - buffer.lines.get(1).set(1, [null, 'f', 1, 'f'.charCodeAt(0)]); - buffer.lines.get(1).set(2, [null, 'g', 1, 'g'.charCodeAt(0)]); - buffer.lines.get(1).set(3, [null, 'h', 1, 'h'.charCodeAt(0)]); - buffer.lines.get(2).set(0, [null, 'i', 1, 'i'.charCodeAt(0)]); - buffer.lines.get(2).set(1, [null, 'j', 1, 'j'.charCodeAt(0)]); - buffer.lines.get(2).set(2, [null, 'k', 1, 'k'.charCodeAt(0)]); - buffer.lines.get(2).set(3, [null, 'l', 1, 'l'.charCodeAt(0)]); - }); - describe('viewport not yet filled', () => { - it('should move the cursor down', () => { - buffer.y = 3; - buffer.resize(2, 10); - assert.equal(buffer.y, 6); - assert.equal(buffer.ydisp, 0); - assert.equal(buffer.ybase, 0); - assert.equal(buffer.lines.length, 10); - assert.equal(buffer.lines.get(0).translateToString(), 'ab'); - assert.equal(buffer.lines.get(1).translateToString(), 'cd'); - assert.equal(buffer.lines.get(2).translateToString(), 'ef'); - assert.equal(buffer.lines.get(3).translateToString(), 'gh'); - assert.equal(buffer.lines.get(4).translateToString(), 'ij'); - assert.equal(buffer.lines.get(5).translateToString(), 'kl'); - for (let i = 6; i < 10; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - const wrappedLines = [1, 3, 5]; - for (let i = 0; i < buffer.lines.length; i++) { - assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); - } - }); - }); - describe('viewport filled, scrollback remaining', () => { - beforeEach(() => { - buffer.y = 9; - }); - describe('ybase === 0', () => { - it('should trim the top', () => { - buffer.resize(2, 10); - assert.equal(buffer.y, 9); - assert.equal(buffer.ydisp, 3); - assert.equal(buffer.ybase, 3); - assert.equal(buffer.lines.length, 13); - assert.equal(buffer.lines.get(0).translateToString(), 'ab'); - assert.equal(buffer.lines.get(1).translateToString(), 'cd'); - assert.equal(buffer.lines.get(2).translateToString(), 'ef'); - assert.equal(buffer.lines.get(3).translateToString(), 'gh'); - assert.equal(buffer.lines.get(4).translateToString(), 'ij'); - assert.equal(buffer.lines.get(5).translateToString(), 'kl'); - for (let i = 6; i < 13; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - const wrappedLines = [1, 3, 5]; - for (let i = 0; i < buffer.lines.length; i++) { - assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); - } - }); - }); - describe('ybase !== 0', () => { - beforeEach(() => { - // Add 10 empty rows to start - for (let i = 0; i < 10; i++) { - buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); - } - buffer.ybase = 10; - }); - describe('&& ydisp === ybase', () => { - it('should adjust the viewport and keep ydisp = ybase', () => { - buffer.ydisp = 10; - buffer.resize(2, 10); - assert.equal(buffer.ydisp, 13); - assert.equal(buffer.ybase, 13); - assert.equal(buffer.lines.length, 23); - for (let i = 0; i < 10; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - assert.equal(buffer.lines.get(10).translateToString(), 'ab'); - assert.equal(buffer.lines.get(11).translateToString(), 'cd'); - assert.equal(buffer.lines.get(12).translateToString(), 'ef'); - assert.equal(buffer.lines.get(13).translateToString(), 'gh'); - assert.equal(buffer.lines.get(14).translateToString(), 'ij'); - assert.equal(buffer.lines.get(15).translateToString(), 'kl'); - for (let i = 16; i < 23; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - const wrappedLines = [11, 13, 15]; - for (let i = 0; i < buffer.lines.length; i++) { - assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); - } - }); - }); - describe('&& ydisp !== ybase', () => { - it('should keep ydisp at the same value', () => { - buffer.ydisp = 5; - buffer.resize(2, 10); - assert.equal(buffer.ydisp, 5); - assert.equal(buffer.ybase, 13); - assert.equal(buffer.lines.length, 23); - for (let i = 0; i < 10; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - assert.equal(buffer.lines.get(10).translateToString(), 'ab'); - assert.equal(buffer.lines.get(11).translateToString(), 'cd'); - assert.equal(buffer.lines.get(12).translateToString(), 'ef'); - assert.equal(buffer.lines.get(13).translateToString(), 'gh'); - assert.equal(buffer.lines.get(14).translateToString(), 'ij'); - assert.equal(buffer.lines.get(15).translateToString(), 'kl'); - for (let i = 16; i < 23; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - const wrappedLines = [11, 13, 15]; - for (let i = 0; i < buffer.lines.length; i++) { - assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); - } - }); - }); - }); - }); - describe('viewport filled, no scrollback remaining', () => { - // ybase === 0 doesn't make sense here as scrollback=0 isn't really supported - describe('ybase !== 0', () => { - beforeEach(() => { - terminal.options.scrollback = 10; - // Add 10 empty rows to start - for (let i = 0; i < 10; i++) { - buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); - } - buffer.ybase = 10; - }); - describe('&& ydisp === ybase', () => { - it('should trim lines and keep ydisp = ybase', () => { - buffer.ydisp = 10; - buffer.y = 13; - buffer.resize(2, 10); - assert.equal(buffer.ydisp, 10); - assert.equal(buffer.ybase, 10); - assert.equal(buffer.lines.length, 20); - for (let i = 0; i < 7; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - assert.equal(buffer.lines.get(7).translateToString(), 'ab'); - assert.equal(buffer.lines.get(8).translateToString(), 'cd'); - assert.equal(buffer.lines.get(9).translateToString(), 'ef'); - assert.equal(buffer.lines.get(10).translateToString(), 'gh'); - assert.equal(buffer.lines.get(11).translateToString(), 'ij'); - assert.equal(buffer.lines.get(12).translateToString(), 'kl'); - for (let i = 13; i < 20; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - const wrappedLines = [8, 10, 12]; - for (let i = 0; i < buffer.lines.length; i++) { - assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); - } - }); - }); - describe('&& ydisp !== ybase', () => { - it('should trim lines and not change ydisp', () => { - buffer.ydisp = 5; - buffer.y = 13; - buffer.resize(2, 10); - assert.equal(buffer.ydisp, 5); - assert.equal(buffer.ybase, 10); - assert.equal(buffer.lines.length, 20); - for (let i = 0; i < 7; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - assert.equal(buffer.lines.get(7).translateToString(), 'ab'); - assert.equal(buffer.lines.get(8).translateToString(), 'cd'); - assert.equal(buffer.lines.get(9).translateToString(), 'ef'); - assert.equal(buffer.lines.get(10).translateToString(), 'gh'); - assert.equal(buffer.lines.get(11).translateToString(), 'ij'); - assert.equal(buffer.lines.get(12).translateToString(), 'kl'); - for (let i = 13; i < 20; i++) { - assert.equal(buffer.lines.get(i).translateToString(), ' '); - } - const wrappedLines = [8, 10, 12]; - for (let i = 0; i < buffer.lines.length; i++) { - assert.equal(buffer.lines.get(i).isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); - } - }); - }); - }); - }); - }); - }); - }); - - describe('buffer marked to have no scrollback', () => { - it('should always have a scrollback of 0', () => { - assert.equal(terminal.options.scrollback, 1000); - // Test size on initialization - buffer = new Buffer(terminal, false); - buffer.fillViewportRows(); - assert.equal(buffer.lines.maxLength, INIT_ROWS); - // Test size on buffer increase - buffer.resize(INIT_COLS, INIT_ROWS * 2); - assert.equal(buffer.lines.maxLength, INIT_ROWS * 2); - // Test size on buffer decrease - buffer.resize(INIT_COLS, INIT_ROWS / 2); - assert.equal(buffer.lines.maxLength, INIT_ROWS / 2); - }); - }); - - describe('addMarker', () => { - it('should adjust a marker line when the buffer is trimmed', () => { - terminal.options.scrollback = 0; - buffer = new Buffer(terminal, true); - buffer.fillViewportRows(); - const marker = buffer.addMarker(buffer.lines.length - 1); - assert.equal(marker.line, buffer.lines.length - 1); - buffer.lines.onTrimEmitter.fire(1); - assert.equal(marker.line, buffer.lines.length - 2); - }); - it('should dispose of a marker if it is trimmed off the buffer', () => { - terminal.options.scrollback = 0; - buffer = new Buffer(terminal, true); - buffer.fillViewportRows(); - assert.equal(buffer.markers.length, 0); - const marker = buffer.addMarker(0); - assert.equal(marker.isDisposed, false); - assert.equal(buffer.markers.length, 1); - buffer.lines.onTrimEmitter.fire(1); - assert.equal(marker.isDisposed, true); - assert.equal(buffer.markers.length, 0); - }); - }); - - describe ('translateBufferLineToString', () => { - it('should handle selecting a section of ascii text', () => { - const line = new BufferLine(4); - line.setCell(0, CellData.fromCharData([ null, 'a', 1, 'a'.charCodeAt(0)])); - line.setCell(1, CellData.fromCharData([ null, 'b', 1, 'b'.charCodeAt(0)])); - line.setCell(2, CellData.fromCharData([ null, 'c', 1, 'c'.charCodeAt(0)])); - line.setCell(3, CellData.fromCharData([ null, 'd', 1, 'd'.charCodeAt(0)])); - buffer.lines.set(0, line); - - const str = buffer.translateBufferLineToString(0, true, 0, 2); - assert.equal(str, 'ab'); - }); - - it('should handle a cut-off double width character by including it', () => { - const line = new BufferLine(3); - line.setCell(0, CellData.fromCharData([ null, '語', 2, 35486 ])); - line.setCell(1, CellData.fromCharData([ null, '', 0, null])); - line.setCell(2, CellData.fromCharData([ null, 'a', 1, 'a'.charCodeAt(0)])); - buffer.lines.set(0, line); - - const str1 = buffer.translateBufferLineToString(0, true, 0, 1); - assert.equal(str1, '語'); - }); - - it('should handle a zero width character in the middle of the string by not including it', () => { - const line = new BufferLine(3); - line.setCell(0, CellData.fromCharData([ null, '語', 2, '語'.charCodeAt(0) ])); - line.setCell(1, CellData.fromCharData([ null, '', 0, null])); - line.setCell(2, CellData.fromCharData([ null, 'a', 1, 'a'.charCodeAt(0)])); - buffer.lines.set(0, line); - - const str0 = buffer.translateBufferLineToString(0, true, 0, 1); - assert.equal(str0, '語'); - - const str1 = buffer.translateBufferLineToString(0, true, 0, 2); - assert.equal(str1, '語'); - - const str2 = buffer.translateBufferLineToString(0, true, 0, 3); - assert.equal(str2, '語a'); - }); - - it('should handle single width emojis', () => { - const line = new BufferLine(2); - line.setCell(0, CellData.fromCharData([ null, '😁', 1, '😁'.charCodeAt(0) ])); - line.setCell(1, CellData.fromCharData([ null, 'a', 1, 'a'.charCodeAt(0)])); - buffer.lines.set(0, line); - - const str1 = buffer.translateBufferLineToString(0, true, 0, 1); - assert.equal(str1, '😁'); - - const str2 = buffer.translateBufferLineToString(0, true, 0, 2); - assert.equal(str2, '😁a'); - }); - - it('should handle double width emojis', () => { - const line = new BufferLine(2); - line.setCell(0, CellData.fromCharData([ null, '😁', 2, '😁'.charCodeAt(0) ])); - line.setCell(1, CellData.fromCharData([ null, '', 0, null])); - buffer.lines.set(0, line); - - const str1 = buffer.translateBufferLineToString(0, true, 0, 1); - assert.equal(str1, '😁'); - - const str2 = buffer.translateBufferLineToString(0, true, 0, 2); - assert.equal(str2, '😁'); - - const line2 = new BufferLine(3); - line2.setCell(0, CellData.fromCharData([ null, '😁', 2, '😁'.charCodeAt(0) ])); - line2.setCell(1, CellData.fromCharData([ null, '', 0, null])); - line2.setCell(2, CellData.fromCharData([ null, 'a', 1, 'a'.charCodeAt(0)])); - buffer.lines.set(0, line2); - - const str3 = buffer.translateBufferLineToString(0, true, 0, 3); - assert.equal(str3, '😁a'); - }); - }); - describe('stringIndexToBufferIndex', () => { - let terminal: TestTerminal; - - beforeEach(() => { - terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5}); - }); - - it('multiline ascii', () => { - const input = 'This is ASCII text spanning multiple lines.'; - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 0; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); - } - }); - - it('combining e\u0301 in a sentence', () => { - const input = 'Sitting in the cafe\u0301 drinking coffee.'; - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 0; i < 19; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); - } - // string index 18 & 19 point to combining char e\u0301 ---> same buffer Index - assert.deepEqual( - terminal.buffer.stringIndexToBufferIndex(0, 18), - terminal.buffer.stringIndexToBufferIndex(0, 19)); - // after the combining char every string index has an offset of -1 - for (let i = 19; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex); - } - }); - - it('multiline combining e\u0301', () => { - const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301'; - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - // every buffer cell index contains 2 string indices - for (let i = 0; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex); - } - }); - - it('surrogate char in a sentence', () => { - const input = 'The 𝄞 is a clef widely used in modern notation.'; - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 0; i < 5; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); - } - // string index 4 & 5 point to surrogate char 𝄞 ---> same buffer Index - assert.deepEqual( - terminal.buffer.stringIndexToBufferIndex(0, 4), - terminal.buffer.stringIndexToBufferIndex(0, 5)); - // after the combining char every string index has an offset of -1 - for (let i = 5; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex); - } - }); - - it('multiline surrogate char', () => { - const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞'; - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - // every buffer cell index contains 2 string indices - for (let i = 0; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex); - } - }); - - it('surrogate char with combining', () => { - // eye of Ra with acute accent - string length of 3 - const input = '𓂀\u0301 - the eye hiroglyph with an acute accent.'; - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - // index 0..2 should map to 0 - assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 1)); - assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 2)); - for (let i = 2; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([((i - 2) / terminal.cols) | 0, (i - 2) % terminal.cols], bufferIndex); - } - }); - - it('multiline surrogate with combining', () => { - const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301'; - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - // every buffer cell index contains 3 string indices - for (let i = 0; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([(((i / 3) | 0) / terminal.cols) | 0, ((i / 3) | 0) % terminal.cols], bufferIndex); - } - }); - - it('fullwidth chars', () => { - const input = 'These 123 are some fat numbers.'; - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 0; i < 6; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); - } - // string index 6, 7, 8 take 2 cells - assert.deepEqual([0, 8], terminal.buffer.stringIndexToBufferIndex(0, 7)); - assert.deepEqual([1, 0], terminal.buffer.stringIndexToBufferIndex(0, 8)); - // rest of the string has offset of +3 - for (let i = 9; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([((i + 3) / terminal.cols) | 0, (i + 3) % terminal.cols], bufferIndex); - } - }); - - it('multiline fullwidth chars', () => { - const input = '12345678901234567890'; - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 9; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([((i << 1) / terminal.cols) | 0, (i << 1) % terminal.cols], bufferIndex); - } - }); - - it('fullwidth combining with emoji - match emoji cell', () => { - const input = 'Lots of ¥\u0301 make me 😃.'; - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - const stringIndex = s.match(/😃/).index; - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, stringIndex); - assert(terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars(), '😃'); - }); - - it('multiline fullwidth chars with offset 1 (currently tests for broken behavior)', () => { - const input = 'a12345678901234567890'; - // the 'a' at the beginning moves all fullwidth chars one to the right - // now the end of the line contains a dangling empty cell since - // the next fullwidth char has to wrap early - // the dangling last cell is wrongly added in the string - // --> fixable after resolving #1685 - terminal.writeSync(input); - 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, true); - const j = (i - 0) << 1; - assert.deepEqual([(j / terminal.cols) | 0, j % terminal.cols], bufferIndex); - } - }); - - it('test fully wrapped buffer up to last char', () => { - const input = Array(6).join('1234567890'); - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 0; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); - assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars()); - } - }); - - it('test fully wrapped buffer up to last char with full width odd', () => { - const input = 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301' - + 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301'; - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 0; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); - assert.equal( - (!(i % 3)) - ? input[i] - : (i % 3 === 1) - ? input.substr(i, 2) - : input.substr(i - 1, 2), - terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars()); - } - }); - - it('should handle \t in lines correctly', () => { - const input = '\thttps://google.de'; - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(s, Array(terminal.options.tabStopWidth + 1).join(' ') + 'https://google.de'); - }); - }); - describe('BufferStringIterator', function(): void { - it('iterator does not overflow buffer limits', function(): void { - const terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5}); - const data = [ - 'aaaaaaaaaa', - 'aaaaaaaaa\n', - 'aaaaaaaaaa', - 'aaaaaaaaa\n', - 'aaaaaaaaaa', - 'aaaaaaaaaa', - 'aaaaaaaaaa', - 'aaaaaaaaa\n', - 'aaaaaaaaaa', - 'aaaaaaaaaa' - ]; - terminal.writeSync(data.join('')); - // brute force test with insane values - expect(() => { - for (let overscan = 0; overscan < 20; ++overscan) { - for (let start = -10; start < 20; ++start) { - for (let end = -10; end < 20; ++end) { - const it = terminal.buffer.iterator(false, start, end, overscan, overscan); - while (it.hasNext()) { - it.next(); - } - } - } - } - }).to.not.throw(); - }); - }); -}); diff --git a/src/CharMeasure.test.ts b/src/CharMeasure.test.ts deleted file mode 100644 index 5fd17eb24..000000000 --- a/src/CharMeasure.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) 2016 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import jsdom = require('jsdom'); -import { ICharMeasure } from './Types'; -import { assert } from 'chai'; -import { CharMeasure } from './CharMeasure'; - -describe('CharMeasure', () => { - let dom: jsdom.JSDOM; - let window: Window; - let document: Document; - let container: HTMLElement; - let charMeasure: ICharMeasure; - - beforeEach(() => { - dom = new jsdom.JSDOM(''); - window = dom.window; - document = window.document; - container = document.createElement('div'); - document.body.appendChild(container); - charMeasure = new CharMeasure(document, container); - }); - - describe('measure', () => { - it('should have _measureElement', () => { - assert.isDefined((charMeasure)._measureElement, 'new CharMeasure() should have created _measureElement'); - }); - - it('should be performed sync', () => { - // Mock getBoundingClientRect since jsdom doesn't have a layout engine - (charMeasure)._measureElement.getBoundingClientRect = () => { - return { width: 1, height: 1 }; - }; - charMeasure.measure({}); - assert.equal(charMeasure.height, 1); - assert.equal(charMeasure.width, 1); - }); - - it('should NOT do a measure when the parent is hidden', done => { - charMeasure.measure({}); - setTimeout(() => { - const firstWidth = charMeasure.width; - container.style.display = 'none'; - container.style.fontSize = '2em'; - charMeasure.measure({}); - assert.equal(charMeasure.width, firstWidth); - done(); - }, 0); - }); - }); -}); diff --git a/src/CharMeasure.ts b/src/CharMeasure.ts deleted file mode 100644 index 9ef22b63d..000000000 --- a/src/CharMeasure.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Copyright (c) 2016 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { ICharMeasure, ITerminalOptions } from './Types'; -import { EventEmitter2, IEvent } from 'common/EventEmitter2'; - -/** - * Utility class that measures the size of a character. Measurements are done in - * the DOM rather than with a canvas context because support for extracting the - * height of characters is patchy across browsers. - */ -export class CharMeasure implements ICharMeasure { - private _document: Document; - private _parentElement: HTMLElement; - private _measureElement: HTMLElement; - private _width: number; - private _height: number; - - private _onCharSizeChanged = new EventEmitter2(); - public get onCharSizeChanged(): IEvent { return this._onCharSizeChanged.event; } - - constructor(document: Document, parentElement: HTMLElement) { - this._document = document; - this._parentElement = parentElement; - this._measureElement = this._document.createElement('span'); - this._measureElement.classList.add('xterm-char-measure-element'); - this._measureElement.textContent = 'W'; - this._measureElement.setAttribute('aria-hidden', 'true'); - this._parentElement.appendChild(this._measureElement); - } - - public get width(): number { - return this._width; - } - - public get height(): number { - return this._height; - } - - public measure(options: ITerminalOptions): void { - this._measureElement.style.fontFamily = options.fontFamily; - this._measureElement.style.fontSize = `${options.fontSize}px`; - const geometry = this._measureElement.getBoundingClientRect(); - // The element is likely currently display:none, we should retain the - // previous value. - if (geometry.width === 0 || geometry.height === 0) { - return; - } - const adjustedHeight = Math.ceil(geometry.height); - if (this._width !== geometry.width || this._height !== adjustedHeight) { - this._width = geometry.width; - this._height = adjustedHeight; - this._onCharSizeChanged.fire(); - } - } -} diff --git a/src/Clipboard.ts b/src/Clipboard.ts index 7a32badcd..9461afa1f 100644 --- a/src/Clipboard.ts +++ b/src/Clipboard.ts @@ -5,15 +5,6 @@ import { ITerminal, ISelectionManager } from './Types'; -interface IWindow extends Window { - clipboardData?: { - getData(format: string): string; - setData(format: string, data: string): void; - }; -} - -declare var window: IWindow; - /** * Prepares text to be pasted into the terminal by normalizing the line endings * @param text The pasted text that needs processing before inserting into the terminal @@ -38,12 +29,7 @@ export function bracketTextForPaste(text: string, bracketedPasteMode: boolean): * @param ev The original copy event to be handled */ export function copyHandler(ev: ClipboardEvent, term: ITerminal, selectionManager: ISelectionManager): void { - if (term.browser.isMSIE) { - window.clipboardData.setData('Text', selectionManager.selectionText); - } else { - ev.clipboardData.setData('text/plain', selectionManager.selectionText); - } - + ev.clipboardData.setData('text/plain', selectionManager.selectionText); // Prevent or the original text will be copied. ev.preventDefault(); } @@ -63,20 +49,12 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void { text = bracketTextForPaste(text, term.bracketedPasteMode); term.handler(text); term.textarea.value = ''; - term.emit('paste', text); term.cancel(ev); }; - if (term.browser.isMSIE) { - if (window.clipboardData) { - text = window.clipboardData.getData('Text'); - dispatchPaste(text); - } - } else { - if (ev.clipboardData) { - text = ev.clipboardData.getData('text/plain'); - dispatchPaste(text); - } + if (ev.clipboardData) { + text = ev.clipboardData.getData('text/plain'); + dispatchPaste(text); } } diff --git a/src/CompositionHelper.test.ts b/src/CompositionHelper.test.ts index 053a0c294..2d28f55d5 100644 --- a/src/CompositionHelper.test.ts +++ b/src/CompositionHelper.test.ts @@ -6,6 +6,7 @@ import { assert } from 'chai'; import { CompositionHelper } from './CompositionHelper'; import { ITerminal } from './Types'; +import { MockCharSizeService } from 'browser/TestUtils.test'; describe('CompositionHelper', () => { let terminal: ITerminal; @@ -48,16 +49,12 @@ describe('CompositionHelper', () => { buffer: { isCursorInViewport: true }, - charMeasure: { - height: 10, - width: 10 - }, options: { lineHeight: 1 } } as any; handledText = ''; - compositionHelper = new CompositionHelper(textarea, compositionView, terminal); + compositionHelper = new CompositionHelper(textarea, compositionView, terminal, new MockCharSizeService(10, 10)); }); describe('Input', () => { diff --git a/src/CompositionHelper.ts b/src/CompositionHelper.ts index 840bef550..2b2d3042f 100644 --- a/src/CompositionHelper.ts +++ b/src/CompositionHelper.ts @@ -4,6 +4,7 @@ */ import { ITerminal } from './Types'; +import { ICharSizeService } from 'browser/services/Services'; interface IPosition { start: number; @@ -42,7 +43,8 @@ export class CompositionHelper { constructor( private _textarea: HTMLTextAreaElement, private _compositionView: HTMLElement, - private _terminal: ITerminal + private _terminal: ITerminal, + private _charSizeService: ICharSizeService ) { this._isComposing = false; this._isSendingComposition = false; @@ -195,9 +197,9 @@ export class CompositionHelper { } if (this._terminal.buffer.isCursorInViewport) { - const cellHeight = Math.ceil(this._terminal.charMeasure.height * this._terminal.options.lineHeight); + const cellHeight = Math.ceil(this._charSizeService.height * this._terminal.options.lineHeight); const cursorTop = this._terminal.buffer.y * cellHeight; - const cursorLeft = this._terminal.buffer.x * this._terminal.charMeasure.width; + const cursorLeft = this._terminal.buffer.x * this._charSizeService.width; this._compositionView.style.left = cursorLeft + 'px'; this._compositionView.style.top = cursorTop + 'px'; diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index f3a217a13..3e5cef57c 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -7,8 +7,8 @@ import { assert, expect } from 'chai'; import { InputHandler } from './InputHandler'; import { MockInputHandlingTerminal, TestTerminal } from './TestUtils.test'; import { Terminal } from './Terminal'; -import { IBufferLine } from 'core/Types'; -import { CellData, Attributes, AttributeData, DEFAULT_ATTR_DATA } from 'core/buffer/BufferLine'; +import { IBufferLine } from 'common/Types'; +import { CellData, Attributes, AttributeData, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; describe('InputHandler', () => { describe('save and restore cursor', () => { diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 1a4ef1878..5ea20caf4 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -6,16 +6,16 @@ import { IInputHandler, IInputHandlingTerminal } from './Types'; import { C0, C1 } from 'common/data/EscapeSequences'; -import { CHARSETS, DEFAULT_CHARSET } from 'core/data/Charsets'; -import { wcwidth } from './CharWidth'; -import { EscapeSequenceParser } from 'core/parser/EscapeSequenceParser'; +import { CHARSETS, DEFAULT_CHARSET } from 'common/data/Charsets'; +import { wcwidth } from 'common/CharWidth'; +import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser'; import { IDisposable } from 'xterm'; import { Disposable } from 'common/Lifecycle'; import { concat } from 'common/TypedArrayUtils'; -import { StringToUtf32, stringFromCodePoint, utf32ToString, Utf8ToUtf32 } from 'core/input/TextDecoder'; -import { CellData, Attributes, FgFlags, BgFlags, AttributeData, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR_DATA } from 'core/buffer/BufferLine'; -import { EventEmitter2, IEvent } from 'common/EventEmitter2'; -import { IParsingState, IDcsHandler, IEscapeSequenceParser } from 'core/parser/Types'; +import { StringToUtf32, stringFromCodePoint, utf32ToString, Utf8ToUtf32 } from 'common/input/TextDecoder'; +import { CellData, Attributes, FgFlags, BgFlags, AttributeData, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { IParsingState, IDcsHandler, IEscapeSequenceParser } from 'common/parser/Types'; /** * Map collect to glevel. Used in `selectCharset`. @@ -108,13 +108,13 @@ export class InputHandler extends Disposable implements IInputHandler { private _utf8Decoder: Utf8ToUtf32 = new Utf8ToUtf32(); private _workCell: CellData = new CellData(); - private _onCursorMove = new EventEmitter2(); + private _onCursorMove = new EventEmitter(); public get onCursorMove(): IEvent { return this._onCursorMove.event; } - private _onData = new EventEmitter2(); + private _onData = new EventEmitter(); public get onData(): IEvent { return this._onData.event; } - private _onLineFeed = new EventEmitter2(); + private _onLineFeed = new EventEmitter(); public get onLineFeed(): IEvent { return this._onLineFeed.event; } - private _onScroll = new EventEmitter2(); + private _onScroll = new EventEmitter(); public get onScroll(): IEvent { return this._onScroll.event; } constructor( @@ -342,7 +342,7 @@ export class InputHandler extends Disposable implements IInputHandler { buffer = this._terminal.buffer; if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) { - this._terminal.emit('cursormove'); + this._onCursorMove.fire(); } } @@ -377,7 +377,7 @@ export class InputHandler extends Disposable implements IInputHandler { } if (screenReaderMode) { - this._terminal.emit('a11y.char', stringFromCodePoint(code)); + this._terminal.onA11yCharEmitter.fire(stringFromCodePoint(code)); } // insert combining char at last cursor position @@ -453,6 +453,20 @@ export class InputHandler extends Disposable implements IInputHandler { } } } + // store last char in Parser.precedingCodepoint for REP to work correctly + // This needs to check whether: + // - fullwidth + surrogates: reset + // - combining: only base char gets carried on (bug in xterm?) + if (end) { + bufferRow.loadCell(buffer.x - 1, this._workCell); + if (this._workCell.getWidth() === 2 || this._workCell.getCode() > 0xFFFF) { + this._parser.precedingCodepoint = 0; + } else if (this._workCell.isCombined()) { + this._parser.precedingCodepoint = this._workCell.getChars().charCodeAt(0); + } else { + this._parser.precedingCodepoint = this._workCell.content; + } + } this._terminal.updateRange(buffer.y); } @@ -528,7 +542,7 @@ export class InputHandler extends Disposable implements IInputHandler { const originalX = this._terminal.buffer.x; this._terminal.buffer.x = this._terminal.buffer.nextStop(); if (this._terminal.options.screenReaderMode) { - this._terminal.emit('a11y.tab', this._terminal.buffer.x - originalX); + this._terminal.onA11yTabEmitter.fire(this._terminal.buffer.x - originalX); } } @@ -1004,17 +1018,37 @@ export class InputHandler extends Disposable implements IInputHandler { /** * CSI Ps b Repeat the preceding graphic character Ps times (REP). + * From ECMA 48 (@see http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-048.pdf) + * Notation: (Pn) + * Representation: CSI Pn 06/02 + * Parameter default value: Pn = 1 + * REP is used to indicate that the preceding character in the data stream, + * if it is a graphic character (represented by one or more bit combinations) including SPACE, + * is to be repeated n times, where n equals the value of Pn. + * If the character preceding REP is a control function or part of a control function, + * the effect of REP is not defined by this Standard. + * + * Since we propagate the terminal as xterm-256color we have to follow xterm's behavior: + * - fullwidth + surrogate chars are ignored + * - for combining chars only the base char gets repeated + * - text attrs are applied normally + * - wrap around is respected + * - any valid sequence resets the carried forward char + * + * Note: To get reset on a valid sequence working correctly without much runtime penalty, + * the preceding codepoint is stored on the parser in `this.print` and reset during `parser.parse`. */ public repeatPrecedingCharacter(params: number[]): void { - // make buffer local for faster access - const buffer = this._terminal.buffer; - const line = buffer.lines.get(buffer.ybase + buffer.y); - line.loadCell(buffer.x - 1, this._workCell); - line.replaceCells(buffer.x, - buffer.x + (params[0] || 1), - (this._workCell.content !== undefined) ? this._workCell : buffer.getNullCell(DEFAULT_ATTR_DATA) - ); - // FIXME: no updateRange here? + if (!this._parser.precedingCodepoint) { + return; + } + // call print to insert the chars and handle correct wrapping + const length = params[0] || 1; + const data = new Uint32Array(length); + for (let i = 0; i < length; ++i) { + data[i] = this._parser.precedingCodepoint; + } + this.print(data, 0, data.length); } /** @@ -1818,7 +1852,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.applicationCursor = false; this._terminal.buffer.scrollTop = 0; this._terminal.buffer.scrollBottom = this._terminal.rows - 1; - this._terminal.curAttrData = DEFAULT_ATTR_DATA; + this._terminal.curAttrData = DEFAULT_ATTR_DATA.clone(); this._terminal.buffer.x = this._terminal.buffer.y = 0; // ? this._terminal.charset = null; this._terminal.glevel = 0; // ?? diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index e02105d85..8b7e71a02 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -5,11 +5,11 @@ import { assert } from 'chai'; import { IMouseZoneManager, IMouseZone, ILinkMatcher, ITerminal } from './Types'; -import { IBufferLine } from 'core/Types'; +import { IBufferLine } from 'common/Types'; import { Linkifier } from './Linkifier'; import { MockBuffer, MockTerminal, TestTerminal } from './TestUtils.test'; import { CircularList } from 'common/CircularList'; -import { BufferLine, CellData } from 'core/buffer/BufferLine'; +import { BufferLine, CellData } from 'common/buffer/BufferLine'; class TestLinkifier extends Linkifier { constructor(terminal: ITerminal) { diff --git a/src/Linkifier.ts b/src/Linkifier.ts index ae9a0734b..c11849ded 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -3,10 +3,11 @@ * @license MIT */ -import { ILinkifierEvent, ILinkMatcher, LinkMatcherHandler, ILinkMatcherOptions, ILinkifier, ITerminal, IBufferStringIteratorResult, IMouseZoneManager } from './Types'; +import { ILinkifierEvent, ILinkMatcher, LinkMatcherHandler, ILinkMatcherOptions, ILinkifier, ITerminal, IMouseZoneManager } from './Types'; +import { IBufferStringIteratorResult } from 'common/buffer/Types'; import { MouseZone } from './MouseZoneManager'; -import { getStringCellWidth } from './CharWidth'; -import { EventEmitter2, IEvent } from 'common/EventEmitter2'; +import { getStringCellWidth } from 'common/CharWidth'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; /** * The Linkifier applies links to rows shortly after they have been refreshed. @@ -33,11 +34,11 @@ export class Linkifier implements ILinkifier { private _nextLinkMatcherId = 0; private _rowsToLinkify: { start: number, end: number }; - private _onLinkHover = new EventEmitter2(); + private _onLinkHover = new EventEmitter(); public get onLinkHover(): IEvent { return this._onLinkHover.event; } - private _onLinkLeave = new EventEmitter2(); + private _onLinkLeave = new EventEmitter(); public get onLinkLeave(): IEvent { return this._onLinkLeave.event; } - private _onLinkTooltip = new EventEmitter2(); + private _onLinkTooltip = new EventEmitter(); public get onLinkTooltip(): IEvent { return this._onLinkTooltip.event; } constructor( diff --git a/src/MouseHelper.test.ts b/src/MouseHelper.test.ts deleted file mode 100644 index 0925d49f2..000000000 --- a/src/MouseHelper.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import jsdom = require('jsdom'); -import { assert } from 'chai'; -import { MouseHelper } from './MouseHelper'; -import { MockCharMeasure, MockRenderer } from './TestUtils.test'; - -const CHAR_WIDTH = 10; -const CHAR_HEIGHT = 20; - -describe('MouseHelper.getCoords', () => { - let dom: jsdom.JSDOM; - let window: Window; - let document: Document; - let mouseHelper: MouseHelper; - - let charMeasure: MockCharMeasure; - - beforeEach(() => { - dom = new jsdom.JSDOM(''); - window = dom.window; - document = window.document; - charMeasure = new MockCharMeasure(); - charMeasure.width = CHAR_WIDTH; - charMeasure.height = CHAR_HEIGHT; - const renderer = new MockRenderer(); - renderer.dimensions = { - actualCellWidth: CHAR_WIDTH, - actualCellHeight: CHAR_HEIGHT - }; - mouseHelper = new MouseHelper(renderer as any); - }); - - describe('when charMeasure is not initialized', () => { - it('should return null', () => { - charMeasure = new MockCharMeasure(); - assert.equal(mouseHelper.getCoords({ clientX: 0, clientY: 0 }, document.createElement('div'), charMeasure, 10, 10), null); - }); - }); - - it('should return the cell that was clicked', () => { - let coords: [number, number]; - coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH / 2, clientY: CHAR_HEIGHT / 2 }, document.createElement('div'), charMeasure, 10, 10); - assert.deepEqual(coords, [1, 1]); - coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 10, 10); - assert.deepEqual(coords, [1, 1]); - coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT + 1 }, document.createElement('div'), charMeasure, 10, 10); - assert.deepEqual(coords, [1, 2]); - coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH + 1, clientY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 10, 10); - assert.deepEqual(coords, [2, 1]); - }); - - it('should ensure the coordinates are returned within the terminal bounds', () => { - let coords: [number, number]; - coords = mouseHelper.getCoords({ clientX: -1, clientY: -1 }, document.createElement('div'), charMeasure, 10, 10); - assert.deepEqual(coords, [1, 1]); - // Event are double the cols/rows - coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH * 20, clientY: CHAR_HEIGHT * 20 }, document.createElement('div'), charMeasure, 10, 10); - assert.deepEqual(coords, [10, 10], 'coordinates should never come back as larger than the terminal'); - }); -}); diff --git a/src/MouseZoneManager.ts b/src/MouseZoneManager.ts index 737bfc91a..b2ee9b14e 100644 --- a/src/MouseZoneManager.ts +++ b/src/MouseZoneManager.ts @@ -5,7 +5,7 @@ import { ITerminal, IMouseZoneManager, IMouseZone } from './Types'; import { Disposable } from 'common/Lifecycle'; -import { addDisposableDomListener } from 'ui/Lifecycle'; +import { addDisposableDomListener } from 'browser/Lifecycle'; const HOVER_DURATION = 500; @@ -203,7 +203,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { } private _findZoneEventAt(e: MouseEvent): IMouseZone { - const coords = this._terminal.mouseHelper.getCoords(e, this._terminal.screenElement, this._terminal.charMeasure, this._terminal.cols, this._terminal.rows); + const coords = this._terminal.mouseHelper.getCoords(e, this._terminal.screenElement, this._terminal.cols, this._terminal.rows); if (!coords) { return null; } diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index 77a1612f4..5dc692af1 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -4,14 +4,17 @@ */ import { assert } from 'chai'; -import { CharMeasure } from './CharMeasure'; import { SelectionManager, SelectionMode } from './SelectionManager'; import { SelectionModel } from './SelectionModel'; -import { BufferSet } from './BufferSet'; -import { ITerminal, IBuffer } from './Types'; -import { IBufferLine } from 'core/Types'; +import { BufferSet } from 'common/buffer/BufferSet'; +import { ITerminal } from './Types'; +import { IBuffer } from 'common/buffer/Types'; +import { IBufferLine } from 'common/Types'; import { MockTerminal } from './TestUtils.test'; -import { BufferLine, CellData } from 'core/buffer/BufferLine'; +import { MockOptionsService, MockBufferService } from 'common/TestUtils.test'; +import { BufferLine, CellData } from 'common/buffer/BufferLine'; +import { IBufferService } from 'common/services/Services'; +import { MockCharSizeService } from 'browser/TestUtils.test'; class TestMockTerminal extends MockTerminal { emit(event: string, data: any): void {} @@ -20,9 +23,9 @@ class TestMockTerminal extends MockTerminal { class TestSelectionManager extends SelectionManager { constructor( terminal: ITerminal, - charMeasure: CharMeasure + bufferService: IBufferService ) { - super(terminal, charMeasure); + super(terminal, new MockCharSizeService(10, 10), bufferService); } public get model(): SelectionModel { return this._model; } @@ -42,17 +45,21 @@ class TestSelectionManager extends SelectionManager { describe('SelectionManager', () => { let terminal: ITerminal; let buffer: IBuffer; + let bufferService: IBufferService; let selectionManager: TestSelectionManager; beforeEach(() => { terminal = new TestMockTerminal(); - (terminal as any).cols = 80; - (terminal as any).rows = 2; - terminal.options.scrollback = 100; - terminal.buffers = new BufferSet(terminal); + bufferService = new MockBufferService(20, 20); + terminal.buffers = new BufferSet( + new MockOptionsService({ scrollback: 100 }), + bufferService + ); + terminal.cols = 20; + terminal.rows = 20; terminal.buffer = terminal.buffers.active; buffer = terminal.buffer; - selectionManager = new TestSelectionManager(terminal, null); + selectionManager = new TestSelectionManager(terminal, bufferService); }); function stringToRow(text: string): IBufferLine { @@ -192,36 +199,36 @@ describe('SelectionManager', () => { assert.equal(selectionManager.selectionText, 'ij"'); }); it('should expand upwards or downards for wrapped lines', () => { - buffer.lines.set(0, stringToRow(' foo')); - buffer.lines.set(1, stringToRow('bar ')); + buffer.lines.set(0, stringToRow(' foo')); + buffer.lines.set(1, stringToRow('bar ')); buffer.lines.get(1).isWrapped = true; selectionManager.selectWordAt([1, 1]); assert.equal(selectionManager.selectionText, 'foobar'); selectionManager.model.clearSelection(); - selectionManager.selectWordAt([78, 0]); + selectionManager.selectWordAt([18, 0]); assert.equal(selectionManager.selectionText, 'foobar'); }); it('should expand both upwards and downwards for word wrapped over many lines', () => { - const expectedText = 'fooaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccbar'; - buffer.lines.set(0, stringToRow(' foo')); - buffer.lines.set(1, stringToRow('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')); - buffer.lines.set(2, stringToRow('bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb')); - buffer.lines.set(3, stringToRow('cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc')); - buffer.lines.set(4, stringToRow('bar ')); + const expectedText = 'fooaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbccccccccccccccccccccbar'; + buffer.lines.set(0, stringToRow(' foo')); + buffer.lines.set(1, stringToRow('aaaaaaaaaaaaaaaaaaaa')); + buffer.lines.set(2, stringToRow('bbbbbbbbbbbbbbbbbbbb')); + buffer.lines.set(3, stringToRow('cccccccccccccccccccc')); + buffer.lines.set(4, stringToRow('bar ')); buffer.lines.get(1).isWrapped = true; buffer.lines.get(2).isWrapped = true; buffer.lines.get(3).isWrapped = true; buffer.lines.get(4).isWrapped = true; - selectionManager.selectWordAt([78, 0]); + selectionManager.selectWordAt([18, 0]); assert.equal(selectionManager.selectionText, expectedText); selectionManager.model.clearSelection(); - selectionManager.selectWordAt([40, 1]); + selectionManager.selectWordAt([10, 1]); assert.equal(selectionManager.selectionText, expectedText); selectionManager.model.clearSelection(); - selectionManager.selectWordAt([40, 2]); + selectionManager.selectWordAt([10, 2]); assert.equal(selectionManager.selectionText, expectedText); selectionManager.model.clearSelection(); - selectionManager.selectWordAt([40, 3]); + selectionManager.selectWordAt([10, 3]); assert.equal(selectionManager.selectionText, expectedText); selectionManager.model.clearSelection(); selectionManager.selectWordAt([1, 4]); @@ -342,7 +349,7 @@ describe('SelectionManager', () => { selectionManager.selectLineAt(0); assert.equal(selectionManager.selectionText, 'foo bar', 'The selected text is correct'); assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]); - assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 0], 'The actual selection spans the entire column'); + assert.deepEqual(selectionManager.model.finalSelectionEnd, [bufferService.cols, 0], 'The actual selection spans the entire column'); }); it('should select the entire wrapped line', () => { buffer.lines.set(0, stringToRow('foo')); @@ -352,7 +359,7 @@ describe('SelectionManager', () => { selectionManager.selectLineAt(0); assert.equal(selectionManager.selectionText, 'foobar', 'The selected text is correct'); assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]); - assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 1], 'The actual selection spans the entire column'); + assert.deepEqual(selectionManager.model.finalSelectionEnd, [bufferService.cols, 1], 'The actual selection spans the entire column'); }); }); @@ -365,7 +372,7 @@ describe('SelectionManager', () => { buffer.lines.set(3, stringToRow('4')); buffer.lines.set(4, stringToRow('5')); selectionManager.selectAll(); - terminal.buffer.ybase = buffer.lines.length - terminal.rows; + terminal.buffer.ybase = buffer.lines.length - bufferService.rows; assert.equal(selectionManager.selectionText, '1\n2\n3\n4\n5'); }); }); @@ -378,7 +385,7 @@ describe('SelectionManager', () => { buffer.lines.set(2, stringToRow('3')); selectionManager.selectLines(1, 1); assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 1]); - assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 1]); + assert.deepEqual(selectionManager.model.finalSelectionEnd, [bufferService.cols, 1]); }); it('should select multiple lines', () => { buffer.lines.length = 5; @@ -389,7 +396,7 @@ describe('SelectionManager', () => { buffer.lines.set(4, stringToRow('5')); selectionManager.selectLines(1, 3); assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 1]); - assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 3]); + assert.deepEqual(selectionManager.model.finalSelectionEnd, [bufferService.cols, 3]); }); it('should select the to the start when requesting a negative row', () => { buffer.lines.length = 2; @@ -397,7 +404,7 @@ describe('SelectionManager', () => { buffer.lines.set(1, stringToRow('2')); selectionManager.selectLines(-1, 0); assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]); - assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 0]); + assert.deepEqual(selectionManager.model.finalSelectionEnd, [bufferService.cols, 0]); }); it('should select the to the end when requesting beyond the final row', () => { buffer.lines.length = 2; @@ -405,7 +412,7 @@ describe('SelectionManager', () => { buffer.lines.set(1, stringToRow('2')); selectionManager.selectLines(1, 2); assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 1]); - assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 1]); + assert.deepEqual(selectionManager.model.finalSelectionEnd, [bufferService.cols, 1]); }); }); diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 21283b8fa..4b6a0de0b 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -3,16 +3,18 @@ * @license MIT */ -import { ITerminal, ISelectionManager, IBuffer, ISelectionRedrawRequestEvent } from './Types'; -import { IBufferLine } from 'core/Types'; -import { MouseHelper } from './MouseHelper'; +import { ITerminal, ISelectionManager, ISelectionRedrawRequestEvent } from './Types'; +import { IBuffer } from 'common/buffer/Types'; +import { IBufferLine } from 'common/Types'; +import { MouseHelper } from 'browser/input/MouseHelper'; import * as Browser from 'common/Platform'; -import { CharMeasure } from './CharMeasure'; import { SelectionModel } from './SelectionModel'; import { AltClickHandler } from './handlers/AltClickHandler'; -import { CellData } from 'core/buffer/BufferLine'; +import { CellData } from 'common/buffer/BufferLine'; import { IDisposable } from 'xterm'; -import { EventEmitter2, IEvent } from 'common/EventEmitter2'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { ICharSizeService } from 'browser/services/Services'; +import { IBufferService } from 'common/services/Services'; /** * The number of pixels the mouse needs to be above or below the viewport in @@ -108,21 +110,22 @@ export class SelectionManager implements ISelectionManager { private _mouseDownTimeStamp: number; - private _onLinuxMouseSelection = new EventEmitter2(); + private _onLinuxMouseSelection = new EventEmitter(); public get onLinuxMouseSelection(): IEvent { return this._onLinuxMouseSelection.event; } - private _onRedrawRequest = new EventEmitter2(); + private _onRedrawRequest = new EventEmitter(); public get onRedrawRequest(): IEvent { return this._onRedrawRequest.event; } - private _onSelectionChange = new EventEmitter2(); + private _onSelectionChange = new EventEmitter(); public get onSelectionChange(): IEvent { return this._onSelectionChange.event; } constructor( private _terminal: ITerminal, - private _charMeasure: CharMeasure + private _charSizeService: ICharSizeService, + bufferService: IBufferService ) { this._initListeners(); this.enable(); - this._model = new SelectionModel(_terminal); + this._model = new SelectionModel(_terminal, bufferService); this._activeSelectionMode = SelectionMode.NORMAL; } @@ -354,7 +357,7 @@ export class SelectionManager implements ISelectionManager { * @param event The mouse event. */ private _getMouseBufferCoords(event: MouseEvent): [number, number] { - const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.screenElement, this._charMeasure, this._terminal.cols, this._terminal.rows, true); + const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.screenElement, this._terminal.cols, this._terminal.rows, true); if (!coords) { return null; } @@ -375,7 +378,7 @@ export class SelectionManager implements ISelectionManager { */ private _getMouseEventScrollAmount(event: MouseEvent): number { let offset = MouseHelper.getCoordsRelativeToElement(event, this._terminal.screenElement)[1]; - const terminalHeight = this._terminal.rows * Math.ceil(this._charMeasure.height * this._terminal.options.lineHeight); + const terminalHeight = this._terminal.rows * Math.ceil(this._charSizeService.height * this._terminal.options.lineHeight); if (offset >= 0 && offset <= terminalHeight) { return 0; } diff --git a/src/SelectionModel.test.ts b/src/SelectionModel.test.ts index c2b261a98..605ba9e8f 100644 --- a/src/SelectionModel.test.ts +++ b/src/SelectionModel.test.ts @@ -6,14 +6,17 @@ import { assert } from 'chai'; import { ITerminal } from './Types'; import { SelectionModel } from './SelectionModel'; -import { BufferSet } from './BufferSet'; +import { BufferSet } from 'common/buffer/BufferSet'; import { MockTerminal } from './TestUtils.test'; +import { MockOptionsService, MockBufferService } from 'common/TestUtils.test'; +import { IBufferService } from 'common/services/Services'; class TestSelectionModel extends SelectionModel { constructor( - terminal: ITerminal + terminal: ITerminal, + bufferService: IBufferService ) { - super(terminal); + super(terminal, bufferService); } } @@ -23,13 +26,14 @@ describe('SelectionManager', () => { beforeEach(() => { terminal = new MockTerminal(); - (terminal as any).cols = 80; - (terminal as any).rows = 2; - terminal.options.scrollback = 10; - terminal.buffers = new BufferSet(terminal); + const bufferService = new MockBufferService(80, 2); + terminal.buffers = new BufferSet( + new MockOptionsService({ scrollback: 10 }), + bufferService + ); terminal.buffer = terminal.buffers.active; - model = new TestSelectionModel(terminal); + model = new TestSelectionModel(terminal, bufferService); }); describe('clearSelection', () => { diff --git a/src/SelectionModel.ts b/src/SelectionModel.ts index f87667f24..44cd4cac4 100644 --- a/src/SelectionModel.ts +++ b/src/SelectionModel.ts @@ -4,6 +4,7 @@ */ import { ITerminal } from './Types'; +import { IBufferService } from 'common/services/Services'; /** * Represents a selection within the buffer. This model only cares about column @@ -33,7 +34,8 @@ export class SelectionModel { public selectionEnd: [number, number]; constructor( - private _terminal: ITerminal + private _terminal: ITerminal, + private _bufferService: IBufferService ) { this.clearSelection(); } @@ -69,7 +71,7 @@ export class SelectionModel { */ public get finalSelectionEnd(): [number, number] { if (this.isSelectAllActive) { - return [this._terminal.cols, this._terminal.buffer.ybase + this._terminal.rows - 1]; + return [this._bufferService.cols, this._terminal.buffer.ybase + this._bufferService.rows - 1]; } if (!this.selectionStart) { @@ -79,8 +81,8 @@ export class SelectionModel { // Use the selection start + length if the end doesn't exist or they're reversed if (!this.selectionEnd || this.areSelectionValuesReversed()) { const startPlusLength = this.selectionStart[0] + this.selectionStartLength; - if (startPlusLength > this._terminal.cols) { - return [startPlusLength % this._terminal.cols, this.selectionStart[1] + Math.floor(startPlusLength / this._terminal.cols)]; + if (startPlusLength > this._bufferService.cols) { + return [startPlusLength % this._bufferService.cols, this.selectionStart[1] + Math.floor(startPlusLength / this._bufferService.cols)]; } return [startPlusLength, this.selectionStart[1]]; } diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index beb9e9210..36886da18 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -6,7 +6,7 @@ import { assert, expect } from 'chai'; import { Terminal } from './Terminal'; import { MockViewport, MockCompositionHelper, MockRenderer } from './TestUtils.test'; -import { CellData, DEFAULT_ATTR_DATA } from 'core/buffer/BufferLine'; +import { CellData, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; const INIT_COLS = 80; const INIT_ROWS = 24; @@ -57,20 +57,20 @@ describe('Terminal', () => { term.handler('fake'); }); it('should fire the onCursorMove event', (done) => { - term.on('cursormove', () => done()); + term.onCursorMove(() => done()); term.write('foo'); }); it('should fire the onLineFeed event', (done) => { - term.on('linefeed', () => done()); + term.onLineFeed(() => done()); term.write('\n'); }); it('should fire a scroll event when scrollback is created', (done) => { - term.on('scroll', () => done()); + term.onScroll(() => done()); term.write('\n'.repeat(INIT_ROWS)); }); it('should fire a scroll event when scrollback is cleared', (done) => { term.write('\n'.repeat(INIT_ROWS)); - term.on('scroll', () => done()); + term.onScroll(() => done()); term.clear(); }); it('should fire a key event after a keypress DOM event', (done) => { @@ -126,158 +126,6 @@ describe('Terminal', () => { }); }); - describe('on', () => { - beforeEach(() => { - term.on('key', () => { }); - term.on('keypress', () => { }); - term.on('keydown', () => { }); - }); - - describe('data', () => { - it('should emit a data event', (done) => { - term.on('data', () => { - done(); - }); - - term.handler('fake'); - }); - }); - - describe('cursormove', () => { - it('should emit a cursormove event', (done) => { - term.on('cursormove', () => { - done(); - }); - term.write('foo'); - }); - }); - - describe('linefeed', () => { - it('should emit a linefeed event', (done) => { - term.on('linefeed', () => { - done(); - }); - term.write('\n'); - }); - }); - - describe('scroll', () => { - it('should emit a scroll event when scrollback is created', (done) => { - term.on('scroll', () => { - done(); - }); - term.write('\n'.repeat(INIT_ROWS)); - }); - it('should emit a scroll event when scrollback is cleared', (done) => { - term.write('\n'.repeat(INIT_ROWS)); - term.on('scroll', () => { - done(); - }); - term.clear(); - }); - }); - - describe(`keypress (including 'key' event)`, () => { - it('should receive a string and event object', (done) => { - let steps = 0; - - const finish = () => { - if ((++steps) === 2) { - done(); - } - }; - - const evKeyPress = { - preventDefault: () => { }, - stopPropagation: () => { }, - type: 'keypress', - keyCode: 13 - }; - - term.on('keypress', (key, event) => { - assert.equal(typeof key, 'string'); - expect(event).to.be.an.instanceof(Object); - finish(); - }); - - term.on('key', (key, event) => { - assert.equal(typeof key, 'string'); - expect(event).to.be.an.instanceof(Object); - finish(); - }); - - term.keyPress(evKeyPress); - }); - }); - - describe(`keydown (including 'key' event)`, () => { - it(`should receive an event object for 'keydown' and a string and event object for 'key'`, (done) => { - let steps = 0; - - const finish = () => { - if ((++steps) === 2) { - done(); - } - }; - - const evKeyDown = { - preventDefault: () => { }, - stopPropagation: () => { }, - type: 'keydown', - keyCode: 13 - }; - - term.on('keydown', (event) => { - expect(event).to.be.an.instanceof(Object); - finish(); - }); - - term.on('key', (key, event) => { - assert.equal(typeof key, 'string'); - expect(event).to.be.an.instanceof(Object); - finish(); - }); - - term.keyDown(evKeyDown); - }); - }); - - describe('resize', () => { - it('should receive an object: {cols: number, rows: number}', (done) => { - term.on('resize', (data) => { - expect(data).to.have.keys(['cols', 'rows']); - assert.equal(typeof data.cols, 'number'); - assert.equal(typeof data.rows, 'number'); - done(); - }); - - term.resize(1, 1); - }); - }); - - describe('scroll', () => { - it('should receive a number', (done) => { - term.on('scroll', (ydisp) => { - assert.equal(typeof ydisp, 'number'); - done(); - }); - - term.scroll(); - }); - }); - - describe('title', () => { - it('should receive a string', (done) => { - term.on('title', (title) => { - assert.equal(typeof title, 'string'); - done(); - }); - - term.handleTitle('title'); - }); - }); - }); - describe('attachCustomKeyEventHandler', () => { const evKeyDown = { preventDefault: () => { }, @@ -379,10 +227,10 @@ describe('Terminal', () => { describe('scrollLines', () => { let startYDisp: number; beforeEach(() => { - for (let i = 0; i < term.rows * 2; i++) { + for (let i = 0; i < INIT_ROWS * 2; i++) { term.writeln('test'); } - startYDisp = term.rows + 1; + startYDisp = INIT_ROWS + 1; }); it('should scroll a single line', () => { assert.equal(term.buffer.ydisp, startYDisp); @@ -741,10 +589,10 @@ describe('Terminal', () => { it('should emit key with alt + key on keyPress', (done) => { const keys = ['@', '@', '\\', '\\', '|', '|']; - term.on('keypress', (key) => { - if (key) { - const index = keys.indexOf(key); - assert(index !== -1, 'Emitted wrong key: ' + key); + term.onKey(e => { + if (e.key) { + const index = keys.indexOf(e.key); + assert(index !== -1, 'Emitted wrong key: ' + e.key); keys.splice(index, 1); } if (keys.length === 0) done(); @@ -807,10 +655,10 @@ describe('Terminal', () => { it('should emit key with alt + ctrl + key on keyPress', (done) => { const keys = ['@', '@', '\\', '\\', '|', '|']; - term.on('keypress', (key) => { - if (key) { - const index = keys.indexOf(key); - assert(index !== -1, 'Emitted wrong key: ' + key); + term.onKey(e => { + if (e.key) { + const index = keys.indexOf(e.key); + assert(index !== -1, 'Emitted wrong key: ' + e.key); keys.splice(index, 1); } if (keys.length === 0) done(); diff --git a/src/Terminal.ts b/src/Terminal.ts index 8cb1a6a48..7addeee66 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -21,12 +21,9 @@ * http://linux.die.net/man/7/urxvt */ -import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharacterJoinerHandler, IMouseZoneManager } from './Types'; -import { IRenderer } from './renderer/Types'; -import { BufferSet } from './BufferSet'; -import { Buffer } from './Buffer'; +import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, IMouseZoneManager } from './Types'; +import { IRenderer, CharacterJoinerHandler } from 'browser/renderer/Types'; import { CompositionHelper } from './CompositionHelper'; -import { EventEmitter } from 'common/EventEmitter'; import { Viewport } from './Viewport'; import { rightClickHandler, moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from './Clipboard'; import { C0 } from 'common/data/EscapeSequences'; @@ -34,27 +31,30 @@ import { InputHandler } from './InputHandler'; import { Renderer } from './renderer/Renderer'; import { Linkifier } from './Linkifier'; import { SelectionManager } from './SelectionManager'; -import { CharMeasure } from './CharMeasure'; import * as Browser from 'common/Platform'; -import { addDisposableDomListener } from 'ui/Lifecycle'; -import * as Strings from './Strings'; -import { MouseHelper } from './MouseHelper'; +import { addDisposableDomListener } from 'browser/Lifecycle'; +import * as Strings from './browser/LocalizableStrings'; +import { MouseHelper } from 'browser/input/MouseHelper'; import { SoundManager } from './SoundManager'; import { MouseZoneManager } from './MouseZoneManager'; import { AccessibilityManager } from './AccessibilityManager'; import { ITheme, IMarker, IDisposable, ISelectionPosition } from 'xterm'; import { removeTerminalFromCache } from './renderer/atlas/CharAtlasCache'; import { DomRenderer } from './renderer/dom/DomRenderer'; -import { IKeyboardEvent } from 'common/Types'; -import { evaluateKeyboardEvent } from 'core/input/Keyboard'; -import { KeyboardResultType, ICharset, IBufferLine, IAttributeData } from 'core/Types'; -import { EventEmitter2, IEvent } from 'common/EventEmitter2'; -import { Attributes, DEFAULT_ATTR_DATA } from 'core/buffer/BufferLine'; +import { IKeyboardEvent, KeyboardResultType, ICharset, IBufferLine, IAttributeData } from 'common/Types'; +import { evaluateKeyboardEvent } from 'common/input/Keyboard'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { Attributes, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { applyWindowsMode } from './WindowsMode'; -import { ColorManager } from 'ui/ColorManager'; -import { RenderCoordinator } from './renderer/RenderCoordinator'; -import { IOptionsService } from 'common/options/Types'; -import { OptionsService } from 'common/options/OptionsService'; +import { ColorManager } from 'browser/ColorManager'; +import { RenderService } from 'browser/services/RenderService'; +import { IOptionsService, IBufferService } from 'common/services/Services'; +import { OptionsService } from 'common/services/OptionsService'; +import { ICharSizeService } from 'browser/services/Services'; +import { CharSizeService } from 'browser/services/CharSizeService'; +import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService'; +import { Disposable } from 'common/Lifecycle'; +import { IBufferSet, IBuffer } from 'common/buffer/Types'; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -75,10 +75,7 @@ const WRITE_BUFFER_PAUSE_THRESHOLD = 5; const WRITE_TIMEOUT_MS = 12; const WRITE_BUFFER_LENGTH_THRESHOLD = 50; -const MINIMUM_COLS = 2; // Less than 2 can mess with wide chars -const MINIMUM_ROWS = 1; - -export class Terminal extends EventEmitter implements ITerminal, IDisposable, IInputHandlingTerminal { +export class Terminal extends Disposable implements ITerminal, IDisposable, IInputHandlingTerminal { public textarea: HTMLTextAreaElement; public element: HTMLElement; public screenElement: HTMLElement; @@ -107,9 +104,14 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II private _customKeyEventHandler: CustomKeyEventHandler; - // services + // common services + private _bufferService: IBufferService; public optionsService: IOptionsService; + // browser services + private _charSizeService: ICharSizeService; + private _renderService: RenderService; + // modes public applicationKeypad: boolean; public applicationCursor: boolean; @@ -169,13 +171,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II private _inputHandler: InputHandler; public soundManager: SoundManager; - private _renderCoordinator: RenderCoordinator; public selectionManager: SelectionManager; public linkifier: ILinkifier; - public buffers: BufferSet; public viewport: IViewport; private _compositionHelper: ICompositionHelper; - public charMeasure: CharMeasure; private _mouseZoneManager: IMouseZoneManager; public mouseHelper: MouseHelper; private _accessibilityManager: AccessibilityManager; @@ -186,28 +185,37 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // bufferline to clone/copy from for new blank lines private _blankLine: IBufferLine = null; - public cols: number; - public rows: number; + public get cols(): number { return this._bufferService.cols; } + public get rows(): number { return this._bufferService.rows; } - private _onCursorMove = new EventEmitter2(); + private _onCursorMove = new EventEmitter(); public get onCursorMove(): IEvent { return this._onCursorMove.event; } - private _onData = new EventEmitter2(); + private _onData = new EventEmitter(); public get onData(): IEvent { return this._onData.event; } - private _onKey = new EventEmitter2<{ key: string, domEvent: KeyboardEvent }>(); + private _onKey = new EventEmitter<{ key: string, domEvent: KeyboardEvent }>(); public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._onKey.event; } - private _onLineFeed = new EventEmitter2(); + private _onLineFeed = new EventEmitter(); public get onLineFeed(): IEvent { return this._onLineFeed.event; } - private _onRender = new EventEmitter2<{ start: number, end: number }>(); + private _onRender = new EventEmitter<{ start: number, end: number }>(); public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; } - private _onResize = new EventEmitter2<{ cols: number, rows: number }>(); + private _onResize = new EventEmitter<{ cols: number, rows: number }>(); public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; } - private _onScroll = new EventEmitter2(); + private _onScroll = new EventEmitter(); public get onScroll(): IEvent { return this._onScroll.event; } - private _onSelectionChange = new EventEmitter2(); + private _onSelectionChange = new EventEmitter(); public get onSelectionChange(): IEvent { return this._onSelectionChange.event; } - private _onTitleChange = new EventEmitter2(); + private _onTitleChange = new EventEmitter(); public get onTitleChange(): IEvent { return this._onTitleChange.event; } + private _onFocus = new EventEmitter(); + public get onFocus(): IEvent { return this._onFocus.event; } + private _onBlur = new EventEmitter(); + public get onBlur(): IEvent { return this._onBlur.event; } + public onA11yCharEmitter = new EventEmitter(); + public get onA11yChar(): IEvent { return this.onA11yCharEmitter.event; } + public onA11yTabEmitter = new EventEmitter(); + public get onA11yTab(): IEvent { return this.onA11yTabEmitter.event; } + /** * Creates a new `Terminal` object. * @@ -224,23 +232,12 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II options: ITerminalOptions = {} ) { super(); + + // Setup and initialize common services this.optionsService = new OptionsService(options); + this._bufferService = new BufferService(this.optionsService); this._setupOptionsListeners(); - - // this.options = clone(options); this._setup(); - - // TODO: Remove these in v4 - // Fire old style events from new emitters - this.onCursorMove(() => this.emit('cursormove')); - this.onData(e => this.emit('data', e)); - this.onKey(e => this.emit('key', e.key, e.domEvent)); - this.onLineFeed(() => this.emit('linefeed')); - this.onRender(e => this.emit('refresh', e)); - this.onResize(e => this.emit('resize', e)); - this.onSelectionChange(() => this.emit('selection')); - this.onScroll(e => this.emit('scroll', e)); - this.onTitleChange(e => this.emit('title', e)); } public dispose(): void { @@ -261,9 +258,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II private _setup(): void { this._parent = document ? document.body : null; - this.cols = Math.max(this.options.cols, MINIMUM_COLS); - this.rows = Math.max(this.options.rows, MINIMUM_ROWS); - this.cursorState = 0; this.cursorHidden = false; this._customKeyEventHandler = null; @@ -310,8 +304,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this._mouseZoneManager = this._mouseZoneManager || null; this.soundManager = this.soundManager || new SoundManager(this); - // Create the terminal's buffers and set the current buffer - this.buffers = new BufferSet(this); if (this.selectionManager) { this.selectionManager.clearSelection(); this.selectionManager.initBuffersListeners(); @@ -325,10 +317,14 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II /** * Convenience property to active buffer. */ - public get buffer(): Buffer { + public get buffer(): IBuffer { return this.buffers.active; } + public get buffers(): IBufferSet { + return this._bufferService.buffers; + } + /** * back_color_erase feature for xterm. */ @@ -358,9 +354,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II case 'fontFamily': case 'fontSize': // When the font changes the size of the cells may change which requires a renderer clear - if (this._renderCoordinator) { - this._renderCoordinator.clear(); - this.charMeasure.measure(this.options); + if (this._renderService) { + this._renderService.clear(); + } + if (this._charSizeService) { + this._charSizeService.measure(); } break; case 'drawBoldTextInBrightColors': @@ -369,15 +367,15 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II case 'fontWeight': case 'fontWeightBold': // When the font changes the size of the cells may change which requires a renderer clear - if (this._renderCoordinator) { - this._renderCoordinator.clear(); - this._renderCoordinator.onResize(this.cols, this.rows); + if (this._renderService) { + this._renderService.clear(); + this._renderService.onResize(this.cols, this.rows); this.refresh(0, this.rows - 1); } break; case 'rendererType': - if (this._renderCoordinator) { - this._renderCoordinator.setRenderer(this._createRenderer()); + if (this._renderService) { + this._renderService.setRenderer(this._createRenderer()); } break; case 'scrollback': @@ -388,8 +386,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II break; case 'screenReaderMode': if (this.optionsService.options.screenReaderMode) { - if (!this._accessibilityManager && this._renderCoordinator) { - this._accessibilityManager = new AccessibilityManager(this, this._renderCoordinator.dimensions); + if (!this._accessibilityManager && this._renderService) { + this._accessibilityManager = new AccessibilityManager(this, this._renderService.dimensions); } } else { if (this._accessibilityManager) { @@ -440,7 +438,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.updateCursorStyle(ev); this.element.classList.add('focus'); this.showCursor(); - this.emit('focus'); + this._onFocus.fire(); } /** @@ -463,7 +461,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.handler(C0.ESC + '[O'); } this.element.classList.remove('focus'); - this.emit('blur'); + this._onBlur.fire(); } /** @@ -609,13 +607,13 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.register(addDisposableDomListener(this.textarea, 'blur', () => this._onTextAreaBlur())); this._helperContainer.appendChild(this.textarea); + this._charSizeService = new CharSizeService(this._document, this._helperContainer, this.optionsService); + this._compositionView = document.createElement('div'); this._compositionView.classList.add('composition-view'); - this._compositionHelper = new CompositionHelper(this.textarea, this._compositionView, this); + this._compositionHelper = new CompositionHelper(this.textarea, this._compositionView, this, this._charSizeService); this._helperContainer.appendChild(this._compositionView); - this.charMeasure = new CharMeasure(document, this._helperContainer); - // Performance: Add viewport and helper elements from the fragment this.element.appendChild(fragment); @@ -625,25 +623,24 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this._colorManager.setTheme(this._theme); const renderer = this._createRenderer(); - this._renderCoordinator = new RenderCoordinator(renderer, this.rows, this.screenElement, this.optionsService); - this._renderCoordinator.onRender(e => this._onRender.fire(e)); - this.onResize(e => this._renderCoordinator.resize(e.cols, e.rows)); + this._renderService = new RenderService(renderer, this.rows, this.screenElement, this.optionsService, this._charSizeService); + this._renderService.onRender(e => this._onRender.fire(e)); + this.onResize(e => this._renderService.resize(e.cols, e.rows)); - this.viewport = new Viewport(this, this._viewportElement, this._viewportScrollArea, this.charMeasure, this._renderCoordinator.dimensions); + this.viewport = new Viewport(this, this._viewportElement, this._viewportScrollArea, this._renderService.dimensions, this._charSizeService); this.viewport.onThemeChange(this._colorManager.colors); this.register(this.viewport); - this.register(this.onCursorMove(() => this._renderCoordinator.onCursorMove())); - this.register(this.onResize(() => this._renderCoordinator.onResize(this.cols, this.rows))); - this.register(this.addDisposableListener('blur', () => this._renderCoordinator.onBlur())); - this.register(this.addDisposableListener('focus', () => this._renderCoordinator.onFocus())); - this.register(this.charMeasure.onCharSizeChanged(() => this._renderCoordinator.onCharSizeChanged())); - this.register(this._renderCoordinator.onDimensionsChange(() => this.viewport.syncScrollArea())); + this.register(this.onCursorMove(() => this._renderService.onCursorMove())); + this.register(this.onResize(() => this._renderService.onResize(this.cols, this.rows))); + this.register(this.onBlur(() => this._renderService.onBlur())); + this.register(this.onFocus(() => this._renderService.onFocus())); + this.register(this._renderService.onDimensionsChange(() => this.viewport.syncScrollArea())); - this.selectionManager = new SelectionManager(this, this.charMeasure); + this.selectionManager = new SelectionManager(this, this._charSizeService, this._bufferService); this.register(this.selectionManager.onSelectionChange(() => this._onSelectionChange.fire())); this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e))); - this.register(this.selectionManager.onRedrawRequest(e => this._renderCoordinator.onSelectionChanged(e.start, e.end, e.columnSelectMode))); + this.register(this.selectionManager.onRedrawRequest(e => this._renderService.onSelectionChanged(e.start, e.end, e.columnSelectMode))); this.register(this.selectionManager.onLinuxMouseSelection(text => { // If there's a new selection, put it into the textarea, focus and select it // in order to register it as a selection on the OS. This event is fired @@ -658,7 +655,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II })); this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this.selectionManager.refresh())); - this.mouseHelper = new MouseHelper(this._renderCoordinator); + this.mouseHelper = new MouseHelper(this._renderService, this._charSizeService); // apply mouse event classes set by escape codes before terminal was attached this.element.classList.toggle('enable-mouse-events', this.mouseEvents); if (this.mouseEvents) { @@ -670,12 +667,12 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II if (this.options.screenReaderMode) { // Note that this must be done *after* the renderer is created in order to // ensure the correct order of the dprchange event - this._accessibilityManager = new AccessibilityManager(this, this._renderCoordinator.dimensions); - this._accessibilityManager.register(this._renderCoordinator.onDimensionsChange(e => this._accessibilityManager.setDimensions(e))); + this._accessibilityManager = new AccessibilityManager(this, this._renderService.dimensions); + this._accessibilityManager.register(this._renderService.onDimensionsChange(e => this._accessibilityManager.setDimensions(e))); } // Measure the character size - this.charMeasure.measure(this.options); + this._charSizeService.measure(); // Setup loop that draws to screen this.refresh(0, this.rows - 1); @@ -690,15 +687,15 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } public setRenderer(renderer: IRenderer): void { - this._renderCoordinator.setRenderer(renderer); + this._renderService.setRenderer(renderer); // this._renderCoordinator.onOptionsChanged(); this.refresh(0, this.rows - 1); } private _createRenderer(): IRenderer { switch (this.options.rendererType) { - case 'canvas': return new Renderer(this, this._colorManager.colors); - case 'dom': return new DomRenderer(this, this._colorManager.colors); + case 'canvas': return new Renderer(this, this._colorManager.colors, this._charSizeService); break; + case 'dom': return new DomRenderer(this, this._colorManager.colors, this._charSizeService); break; default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`); } } @@ -710,8 +707,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II private _setTheme(theme: ITheme): void { this._theme = theme; this._colorManager.setTheme(theme); - if (this._renderCoordinator) { - this._renderCoordinator.setColors(this._colorManager.colors); + if (this._renderService) { + this._renderService.setColors(this._colorManager.colors); } if (this.viewport) { this.viewport.onThemeChange(this._colorManager.colors); @@ -744,7 +741,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II button = getButton(ev); // get mouse coordinates - pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.charMeasure, self.cols, self.rows); + pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.cols, self.rows); if (!pos) return; sendEvent(button, pos); @@ -770,7 +767,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7< function sendMove(ev: MouseEvent): void { let button = pressed; - const pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.charMeasure, self.cols, self.rows); + const pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.cols, self.rows); if (!pos) return; // buttons marked as motions @@ -806,12 +803,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // vt300: ^[[ 24(1/3/5)~ [ Cx , Cy ] \r // locator: CSI P e ; P b ; P r ; P c ; P p & w function sendEvent(button: number, pos: {x: number, y: number}): void { - // self.emit('mouse', { - // x: pos.x - 32, - // y: pos.x - 32, - // button: button - // }); - if (self._vt300Mouse) { // NOTE: Unstable. // http://www.vt100.net/docs/vt3xx-gp/chapter15.html @@ -905,10 +896,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II : ev.which !== null && ev.which !== undefined ? ev.which - 1 : null; - - if (Browser.isMSIE) { - button = button === 1 ? 0 : button === 4 ? 1 : button; - } break; case 'mouseup': button = 3; @@ -1067,8 +1054,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II * @param end The row to end at (between start and this.rows - 1). */ public refresh(start: number, end: number): void { - if (this._renderCoordinator) { - this._renderCoordinator.refreshRows(start, end); + if (this._renderService) { + this._renderService.refreshRows(start, end); } } @@ -1463,13 +1450,13 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } public registerCharacterJoiner(handler: CharacterJoinerHandler): number { - const joinerId = this._renderCoordinator.registerCharacterJoiner(handler); + const joinerId = this._renderService.registerCharacterJoiner(handler); this.refresh(0, this.rows - 1); return joinerId; } public deregisterCharacterJoiner(joinerId: number): void { - if (this._renderCoordinator.deregisterCharacterJoiner(joinerId)) { + if (this._renderService.deregisterCharacterJoiner(joinerId)) { this.refresh(0, this.rows - 1); } } @@ -1600,7 +1587,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II return true; } - this.emit('keydown', event); this._onKey.fire({ key: result.key, domEvent: event }); this.showCursor(); this.handler(result.key); @@ -1679,7 +1665,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II key = String.fromCharCode(key); - this.emit('keypress', key, ev); this._onKey.fire({ key, domEvent: ev }); this.showCursor(); this.handler(key); @@ -1692,7 +1677,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II * Note: We could do sweet things with webaudio here */ public bell(): void { - this.emit('bell'); if (this._soundBell()) { this.soundManager.playBellSound(); } @@ -1737,8 +1721,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II if (x === this.cols && y === this.rows) { // Check if we still need to measure the char size (fixes #785). - if (this.charMeasure && (!this.charMeasure.width || !this.charMeasure.height)) { - this.charMeasure.measure(this.options); + if (this._charSizeService && !this._charSizeService.hasValidSize) { + this._charSizeService.measure(); } return; } @@ -1748,12 +1732,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.buffers.resize(x, y); - this.cols = x; - this.rows = y; + this._bufferService.resize(x, y); this.buffers.setupTabStops(this.cols); - if (this.charMeasure) { - this.charMeasure.measure(this.options); + if (this._charSizeService) { + this._charSizeService.measure(); } this.refresh(0, this.rows - 1); @@ -1900,6 +1883,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II const userScrolling = this._userScrolling; this._setup(); + this._bufferService.reset(); // reattach this._customKeyEventHandler = customKeyEventHandler; diff --git a/src/Terminal.integration.ts b/src/Terminal2.test.ts similarity index 87% rename from src/Terminal.integration.ts rename to src/Terminal2.test.ts index 596745866..0fc2f4614 100644 --- a/src/Terminal.integration.ts +++ b/src/Terminal2.test.ts @@ -5,16 +5,14 @@ * This file contains integration tests for xterm.js. */ -import * as cp from 'child_process'; import * as glob from 'glob'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import * as pty from 'node-pty'; -import { assert } from 'chai'; import { Terminal } from './Terminal'; import { IViewport } from './Types'; -import { CellData, WHITESPACE_CELL_CHAR } from 'core/buffer/BufferLine'; +import { CellData, WHITESPACE_CELL_CHAR } from 'common/buffer/BufferLine'; class TestTerminal extends Terminal { innerWrite(): void { this._innerWrite(); } @@ -109,7 +107,7 @@ if (os.platform() !== 'win32') { // for (let i = 0; i < files.length; ++i) console.debug(i, files[i]); // only successful tests for now const skip = [ - 10, 16, 17, 19, 32, 33, 34, 35, 36, 39, + 10, 16, 17, 19, 32, 34, 35, 36, 39, 40, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 54, 55, 56, 57, 58, 59, 60, 61, 63, 68 @@ -154,16 +152,3 @@ if (os.platform() !== 'win32') { } }); } - -describe('typings', () => { - it('should throw no compile errors', function (): void { - this.timeout(20000); - let tsc = path.join(__dirname, '..', 'node_modules', '.bin', 'tsc'); - if (process.platform === 'win32') { - tsc += '.cmd'; - } - const fixtureDir = path.join(__dirname, '..', 'fixtures', 'typings-test'); - const result = cp.spawnSync(tsc, { cwd: fixtureDir }); - assert.equal(result.status, 0, `build did not succeed:\nstdout: ${result.stdout.toString()}\nstderr: ${result.stderr.toString()}\n`); - }); -}); diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index d8cc92592..1446c41ad 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -3,17 +3,18 @@ * @license MIT */ -import { IRenderer, IRenderDimensions } from './renderer/Types'; -import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ILinkifier, IMouseHelper, ILinkMatcherOptions, CharacterJoinerHandler, IBufferStringIterator } from './Types'; -import { IBufferLine, ICellData, IAttributeData } from 'core/Types'; -import { ICircularList, XtermListener } from 'common/Types'; -import { Buffer } from './Buffer'; +import { IRenderer, IRenderDimensions, CharacterJoinerHandler } from 'browser/renderer/Types'; +import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBrowser, ISelectionManager, ITerminalOptions, ILinkifier, ILinkMatcherOptions } from './Types'; +import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types'; +import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener } from 'common/Types'; +import { Buffer } from 'common/buffer/Buffer'; import * as Browser from 'common/Platform'; import { IDisposable, IMarker, IEvent, ISelectionPosition } from 'xterm'; import { Terminal } from './Terminal'; -import { AttributeData } from 'core/buffer/BufferLine'; -import { IColorManager, IColorSet } from 'ui/Types'; -import { IOptionsService } from 'common/options/Types'; +import { AttributeData } from 'common/buffer/BufferLine'; +import { IColorManager, IColorSet, IMouseHelper } from 'browser/Types'; +import { IOptionsService } from 'common/services/Services'; +import { EventEmitter } from 'common/EventEmitter'; export class TestTerminal extends Terminal { writeSync(data: string): void { @@ -26,6 +27,10 @@ export class MockTerminal implements ITerminal { setRenderer(renderer: any): void { throw new Error('Method not implemented.'); } + onBlur: IEvent; + onFocus: IEvent; + onA11yChar: IEvent; + onA11yTab: IEvent; onCursorMove: IEvent; onLineFeed: IEvent; onSelectionChange: IEvent; @@ -33,8 +38,7 @@ export class MockTerminal implements ITerminal { onTitleChange: IEvent; onScroll: IEvent; onKey: IEvent<{ key: string; domEvent: KeyboardEvent; }>; - onRender: IEvent<{ start: number - ; end: number; }>; + onRender: IEvent<{ start: number; end: number; }>; onResize: IEvent<{ cols: number; rows: number; }>; markers: IMarker[]; optionsService: IOptionsService; @@ -131,7 +135,6 @@ export class MockTerminal implements ITerminal { rowContainer: HTMLElement; selectionContainer: HTMLElement; selectionManager: ISelectionManager; - charMeasure: ICharMeasure; textarea: HTMLTextAreaElement; rows: number; cols: number; @@ -185,16 +188,9 @@ export class MockTerminal implements ITerminal { deregisterCharacterJoiner(joinerId: number): void { } } -export class MockCharMeasure implements ICharMeasure { - onCharSizeChanged: IEvent; - width: number; - height: number; - measure(options: ITerminalOptions): void { - throw new Error('Method not implemented.'); - } -} - export class MockInputHandlingTerminal implements IInputHandlingTerminal { + onA11yCharEmitter: EventEmitter; + onA11yTabEmitter: EventEmitter; element: HTMLElement; options: ITerminalOptions = {}; cols: number; @@ -319,6 +315,10 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { } export class MockBuffer implements IBuffer { + markers: IMarker[]; + addMarker(y: number): IMarker { + throw new Error('Method not implemented.'); + } isCursorInViewport: boolean; lines: ICircularList; ydisp: number; diff --git a/src/Types.ts b/src/Types.ts index fa10dcab1..69ed98029 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -3,12 +3,12 @@ * @license MIT */ -import { ITerminalOptions as IPublicTerminalOptions, IEventEmitter, IDisposable, IMarker, ISelectionPosition } from 'xterm'; -import { ICharset, IAttributeData, ICellData, IBufferLine, CharData } from 'core/Types'; -import { ICircularList } from 'common/Types'; -import { IEvent } from 'common/EventEmitter2'; -import { IColorSet } from 'ui/Types'; -import { IOptionsService } from 'common/options/Types'; +import { ITerminalOptions as IPublicTerminalOptions, IDisposable, IMarker, ISelectionPosition } from 'xterm'; +import { ICharset, IAttributeData, CharData } from 'common/Types'; +import { IEvent, EventEmitter } from 'common/EventEmitter'; +import { IColorSet, IMouseHelper } from 'browser/Types'; +import { IOptionsService } from 'common/services/Services'; +import { IBuffer, IBufferSet } from 'common/buffer/Types'; export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; @@ -17,17 +17,12 @@ export type LineData = CharData[]; export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void; export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void; -export type CharacterJoinerHandler = (text: string) => [number, number][]; - -// BufferIndex denotes a position in the buffer: [rowIndex, colIndex] -export type BufferIndex = [number, number]; - /** * This interface encapsulates everything needed from the Terminal by the * InputHandler. This cleanly separates the large amount of methods needed by * InputHandler cleanly from the ITerminal interface. */ -export interface IInputHandlingTerminal extends IEventEmitter { +export interface IInputHandlingTerminal { element: HTMLElement; options: ITerminalOptions; cols: number; @@ -59,6 +54,9 @@ export interface IInputHandlingTerminal extends IEventEmitter { viewport: IViewport; selectionManager: ISelectionManager; + onA11yCharEmitter: EventEmitter; + onA11yTabEmitter: EventEmitter; + bell(): void; focus(): void; updateRange(y: number): void; @@ -199,7 +197,6 @@ export interface ILinkifierEvent { export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor { screenElement: HTMLElement; selectionManager: ISelectionManager; - charMeasure: ICharMeasure; browser: IBrowser; writeBuffer: string[]; cursorHidden: boolean; @@ -215,6 +212,11 @@ export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAcc // TODO: We should remove options once components adopt optionsService options: ITerminalOptions; + onBlur: IEvent; + onFocus: IEvent; + onA11yChar: IEvent; + onA11yTab: IEvent; + handler(data: string): void; scrollLines(disp: number, suppressScrollEvent?: boolean): void; cancel(ev: Event, force?: boolean): boolean | void; @@ -223,7 +225,7 @@ export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAcc } // Portions of the public API that are required by the internal Terminal -export interface IPublicTerminal extends IDisposable, IEventEmitter { +export interface IPublicTerminal extends IDisposable { textarea: HTMLTextAreaElement; rows: number; cols: number; @@ -284,20 +286,6 @@ export interface ILinkifierAccessor { linkifier: ILinkifier; } -export interface IMouseHelper { - getCoords(event: { clientX: number, clientY: number }, element: HTMLElement, charMeasure: ICharMeasure, colCount: number, rowCount: number, isSelection?: boolean): [number, number]; - getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, colCount: number, rowCount: number): { x: number, y: number }; -} - -export interface ICharMeasure { - width: number; - height: number; - - onCharSizeChanged: IEvent; - - measure(options: ITerminalOptions): void; -} - // TODO: The options that are not in the public API should be reviewed export interface ITerminalOptions extends IPublicTerminalOptions { [key: string]: any; @@ -310,52 +298,6 @@ export interface ITerminalOptions extends IPublicTerminalOptions { useFlowControl?: boolean; } -export interface IBufferStringIteratorResult { - range: {first: number, last: number}; - content: string; -} - -export interface IBufferStringIterator { - hasNext(): boolean; - next(): IBufferStringIteratorResult; -} - -export interface IBuffer { - readonly lines: ICircularList; - ydisp: number; - ybase: number; - y: number; - x: number; - tabs: any; - scrollBottom: number; - scrollTop: number; - hasScrollback: boolean; - savedY: number; - savedX: number; - savedCurAttrData: IAttributeData; - isCursorInViewport: boolean; - translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string; - getWrappedRangeForLine(y: number): { first: number, last: number }; - nextStop(x?: number): number; - prevStop(x?: number): number; - getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine; - stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[]; - iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator; - getNullCell(attr?: IAttributeData): ICellData; - getWhitespaceCell(attr?: IAttributeData): ICellData; -} - -export interface IBufferSet { - alt: IBuffer; - normal: IBuffer; - active: IBuffer; - - onBufferActivate: IEvent<{ activeBuffer: IBuffer, inactiveBuffer: IBuffer }>; - - activateNormalBuffer(): void; - activateAltBuffer(fillAttr?: IAttributeData): void; -} - export interface ISelectionManager { selectionText: string; hasSelection: boolean; @@ -425,7 +367,6 @@ export interface IBrowser { userAgent: string; platform: string; isFirefox: boolean; - isMSIE: boolean; isMac: boolean; isIpad: boolean; isIphone: boolean; diff --git a/src/Viewport.ts b/src/Viewport.ts index a7afdb9e3..cd5a282ce 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -4,11 +4,11 @@ */ import { ITerminal, IViewport } from './Types'; -import { CharMeasure } from './CharMeasure'; import { Disposable } from 'common/Lifecycle'; -import { addDisposableDomListener } from 'ui/Lifecycle'; -import { IColorSet } from 'ui/Types'; -import { IRenderDimensions } from './renderer/Types'; +import { addDisposableDomListener } from 'browser/Lifecycle'; +import { IColorSet } from 'browser/Types'; +import { IRenderDimensions } from 'browser/renderer/Types'; +import { ICharSizeService } from 'browser/services/Services'; const FALLBACK_SCROLL_BAR_WIDTH = 15; @@ -33,19 +33,12 @@ export class Viewport extends Disposable implements IViewport { private _refreshAnimationFrame: number | null = null; private _ignoreNextScrollEvent: boolean = false; - /** - * Creates a new Viewport. - * @param _terminal The terminal this viewport belongs to. - * @param _viewportElement The DOM element acting as the viewport. - * @param _scrollArea The DOM element acting as the scroll area. - * @param _charMeasure A DOM element used to measure the character size of. the terminal. - */ constructor( private _terminal: ITerminal, private _viewportElement: HTMLElement, private _scrollArea: HTMLElement, - private _charMeasure: CharMeasure, - private _dimensions: IRenderDimensions + private _dimensions: IRenderDimensions, + private _charSizeService: ICharSizeService ) { super(); @@ -55,7 +48,7 @@ export class Viewport extends Disposable implements IViewport { this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH; this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._onScroll.bind(this))); - // Perform this async to ensure the CharMeasure is ready. + // Perform this async to ensure the ICharSizeService is ready. setTimeout(() => this.syncScrollArea(), 0); } @@ -78,7 +71,7 @@ export class Viewport extends Disposable implements IViewport { } private _innerRefresh(): void { - if (this._charMeasure.height > 0) { + if (this._charSizeService.height > 0) { this._currentRowHeight = this._dimensions.scaledCellHeight / window.devicePixelRatio; this._lastRecordedViewportHeight = this._viewportElement.offsetHeight; const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._dimensions.canvasHeight); diff --git a/src/WindowsMode.ts b/src/WindowsMode.ts index 3bfa2a16d..cee31de13 100644 --- a/src/WindowsMode.ts +++ b/src/WindowsMode.ts @@ -5,7 +5,7 @@ import { IDisposable } from 'xterm'; import { ITerminal } from './Types'; -import { CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CODE } from 'core/buffer/BufferLine'; +import { CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CODE } from 'common/buffer/BufferLine'; export function applyWindowsMode(terminal: ITerminal): IDisposable { // Winpty does not support wraparound mode which means that lines will never diff --git a/src/addons/attach/Interfaces.ts b/src/addons/attach/Interfaces.ts deleted file mode 100644 index 4b2690993..000000000 --- a/src/addons/attach/Interfaces.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) 2018 The xterm.js authors. All rights reserved. - * @license MIT - * - * Implements the attach method, that attaches the terminal to a WebSocket stream. - */ - -import { Terminal, IDisposable } from 'xterm'; - -export interface IAttachAddonTerminal extends Terminal { - _core: { - register(d: T): void; - }; - - __socket?: WebSocket; - __attachSocketBuffer?: string; - __dataListener?: IDisposable; - - __getMessage?(ev: MessageEvent): void; - __flushBuffer?(): void; - __pushToBuffer?(data: string): void; - __sendData?(data: string): void; -} diff --git a/src/addons/attach/attach.test.ts b/src/addons/attach/attach.test.ts deleted file mode 100644 index e280b6568..000000000 --- a/src/addons/attach/attach.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) 2014 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { assert } from 'chai'; - -import * as attach from './attach'; - -class MockTerminal {} - -describe('attach addon', () => { - describe('apply', () => { - it('should do register the `attach` and `detach` methods', () => { - attach.apply(MockTerminal); - assert.equal(typeof (MockTerminal).prototype.attach, 'function'); - assert.equal(typeof (MockTerminal).prototype.detach, 'function'); - }); - }); -}); diff --git a/src/addons/attach/attach.ts b/src/addons/attach/attach.ts deleted file mode 100644 index 2c8a5d4d2..000000000 --- a/src/addons/attach/attach.ts +++ /dev/null @@ -1,157 +0,0 @@ -/** - * Copyright (c) 2014 The xterm.js authors. All rights reserved. - * @license MIT - * - * Implements the attach method, that attaches the terminal to a WebSocket stream. - */ - -import { Terminal, IDisposable } from 'xterm'; -import { IAttachAddonTerminal } from './Interfaces'; - -/** - * Attaches the given terminal to the given socket. - * - * @param term The terminal to be attached to the given socket. - * @param socket The socket to attach the current terminal. - * @param bidirectional Whether the terminal should send data to the socket as well. - * @param buffered Whether the rendering of incoming data should happen instantly or at a maximum - * frequency of 1 rendering per 10ms. - */ -export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean, buffered: boolean): void { - const addonTerminal = term; - bidirectional = (typeof bidirectional === 'undefined') ? true : bidirectional; - addonTerminal.__socket = socket; - - addonTerminal.__flushBuffer = () => { - addonTerminal.write(addonTerminal.__attachSocketBuffer); - addonTerminal.__attachSocketBuffer = null; - }; - - addonTerminal.__pushToBuffer = (data: string) => { - if (addonTerminal.__attachSocketBuffer) { - addonTerminal.__attachSocketBuffer += data; - } else { - addonTerminal.__attachSocketBuffer = data; - setTimeout(addonTerminal.__flushBuffer, 10); - } - }; - - // TODO: This should be typed but there seem to be issues importing the type - let myTextDecoder: any; - - addonTerminal.__getMessage = function(ev: MessageEvent): void { - let str: string; - - if (typeof ev.data === 'object') { - if (!myTextDecoder) { - myTextDecoder = new TextDecoder(); - } - if (ev.data instanceof ArrayBuffer) { - str = myTextDecoder.decode(ev.data); - displayData(str); - } else { - const fileReader = new FileReader(); - - fileReader.addEventListener('load', () => { - str = myTextDecoder.decode(fileReader.result); - displayData(str); - }); - fileReader.readAsArrayBuffer(ev.data); - } - } else if (typeof ev.data === 'string') { - displayData(ev.data); - } else { - throw Error(`Cannot handle "${typeof ev.data}" websocket message.`); - } - }; - - /** - * Push data to buffer or write it in the terminal. - * This is used as a callback for FileReader.onload. - * - * @param str String decoded by FileReader. - * @param data The data of the EventMessage. - */ - function displayData(str?: string, data?: string): void { - if (buffered) { - addonTerminal.__pushToBuffer(str || data); - } else { - addonTerminal.write(str || data); - } - } - - addonTerminal.__sendData = (data: string) => { - if (socket.readyState !== 1) { - return; - } - socket.send(data); - }; - - addonTerminal._core.register(addSocketListener(socket, 'message', addonTerminal.__getMessage)); - - if (bidirectional) { - addonTerminal.__dataListener = addonTerminal.onData(addonTerminal.__sendData); - addonTerminal._core.register(addonTerminal.__dataListener); - } - - addonTerminal._core.register(addSocketListener(socket, 'close', () => detach(addonTerminal, socket))); - addonTerminal._core.register(addSocketListener(socket, 'error', () => detach(addonTerminal, socket))); -} - -function addSocketListener(socket: WebSocket, type: string, handler: (this: WebSocket, ev: Event) => any): IDisposable { - socket.addEventListener(type, handler); - return { - dispose: () => { - if (!handler) { - // Already disposed - return; - } - socket.removeEventListener(type, handler); - handler = null; - } - }; -} - -/** - * Detaches the given terminal from the given socket - * - * @param term The terminal to be detached from the given socket. - * @param socket The socket from which to detach the current terminal. - */ -export function detach(term: Terminal, socket: WebSocket): void { - const addonTerminal = term; - addonTerminal.__dataListener.dispose(); - addonTerminal.__dataListener = undefined; - - socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket; - - if (socket) { - socket.removeEventListener('message', addonTerminal.__getMessage); - } - - delete addonTerminal.__socket; -} - - -export function apply(terminalConstructor: typeof Terminal): void { - /** - * Attaches the current terminal to the given socket - * - * @param socket The socket to attach the current terminal. - * @param bidirectional Whether the terminal should send data to the socket as well. - * @param buffered Whether the rendering of incoming data should happen instantly or at a maximum - * frequency of 1 rendering per 10ms. - */ - (terminalConstructor.prototype).attach = function (socket: WebSocket, bidirectional: boolean, buffered: boolean): void { - attach(this, socket, bidirectional, buffered); - }; - - /** - * Detaches the current terminal from the given socket. - * - * @param socket The socket from which to detach the current terminal. - */ - (terminalConstructor.prototype).detach = function (socket: WebSocket): void { - detach(this, socket); - }; -} diff --git a/src/addons/attach/index.html b/src/addons/attach/index.html deleted file mode 100644 index b6f853be8..000000000 --- a/src/addons/attach/index.html +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - - - - -
- -

- xterm.js: socket attach -

-

- Attach the terminal to a WebSocket terminal stream with ease. Perfect for attaching to your - Docker containers. -

-

- Socket information -

-
- - -
-
- -
- - - \ No newline at end of file diff --git a/src/addons/attach/package.json b/src/addons/attach/package.json deleted file mode 100644 index 9e45068b5..000000000 --- a/src/addons/attach/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "xterm.attach", - "main": "attach.js", - "private": true -} diff --git a/src/addons/fit/README.md b/src/addons/fit/README.md deleted file mode 100644 index 68f015894..000000000 --- a/src/addons/fit/README.md +++ /dev/null @@ -1,3 +0,0 @@ -## fit addon - -The fit addon adjusts the dimensions of the terminal to match best fit its parent element container. `fit` will only work when the element is visible. diff --git a/src/addons/fit/fit.test.ts b/src/addons/fit/fit.test.ts deleted file mode 100644 index 781b50101..000000000 --- a/src/addons/fit/fit.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { assert } from 'chai'; - -import * as fit from './fit'; - -class MockTerminal {} - -describe('fit addon', () => { - describe('apply', () => { - it('should do register the `proposeGeometry` and `fit` methods', () => { - fit.apply(MockTerminal); - assert.equal(typeof (MockTerminal).prototype.proposeGeometry, 'function'); - assert.equal(typeof (MockTerminal).prototype.fit, 'function'); - }); - }); -}); diff --git a/src/addons/fit/fit.ts b/src/addons/fit/fit.ts deleted file mode 100644 index 26f8752c9..000000000 --- a/src/addons/fit/fit.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Copyright (c) 2014 The xterm.js authors. All rights reserved. - * @license MIT - * - * Fit terminal columns and rows to the dimensions of its DOM element. - * - * ## Approach - * - * Rows: Truncate the division of the terminal parent element height by the - * terminal row height. - * Columns: Truncate the division of the terminal parent element width by the - * terminal character width (apply display: inline at the terminal - * row and truncate its width with the current number of columns). - */ - -import { Terminal } from 'xterm'; - -export interface IGeometry { - rows: number; - cols: number; -} - -export function proposeGeometry(term: Terminal): IGeometry { - if (!term.element.parentElement) { - return null; - } - const parentElementStyle = window.getComputedStyle(term.element.parentElement); - const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')); - const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width'))); - const elementStyle = window.getComputedStyle(term.element); - const elementPadding = { - top: parseInt(elementStyle.getPropertyValue('padding-top')), - bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')), - right: parseInt(elementStyle.getPropertyValue('padding-right')), - left: parseInt(elementStyle.getPropertyValue('padding-left')) - }; - const elementPaddingVer = elementPadding.top + elementPadding.bottom; - const elementPaddingHor = elementPadding.right + elementPadding.left; - const availableHeight = parentElementHeight - elementPaddingVer; - const availableWidth = parentElementWidth - elementPaddingHor - (term)._core.viewport.scrollBarWidth; - const geometry = { - cols: Math.floor(availableWidth / (term)._core._renderCoordinator.dimensions.actualCellWidth), - rows: Math.floor(availableHeight / (term)._core._renderCoordinator.dimensions.actualCellHeight) - }; - return geometry; -} - -export function fit(term: Terminal): void { - const geometry = proposeGeometry(term); - if (geometry) { - // Force a full render - if (term.rows !== geometry.rows || term.cols !== geometry.cols) { - (term)._core._renderCoordinator.clear(); - term.resize(geometry.cols, geometry.rows); - } - } -} - -export function apply(terminalConstructor: typeof Terminal): void { - (terminalConstructor.prototype).proposeGeometry = function (): IGeometry { - return proposeGeometry(this); - }; - - (terminalConstructor.prototype).fit = function (): void { - fit(this); - }; -} diff --git a/src/addons/fit/package.json b/src/addons/fit/package.json deleted file mode 100644 index f7cb5bc8c..000000000 --- a/src/addons/fit/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "xterm.fit", - "main": "fit.js", - "private": true -} diff --git a/src/addons/fullscreen/fullscreen.css b/src/addons/fullscreen/fullscreen.css deleted file mode 100644 index 60e8c5114..000000000 --- a/src/addons/fullscreen/fullscreen.css +++ /dev/null @@ -1,10 +0,0 @@ -.xterm.fullscreen { - position: fixed; - top: 0; - bottom: 0; - left: 0; - right: 0; - width: auto; - height: auto; - z-index: 255; -} diff --git a/src/addons/fullscreen/fullscreen.test.ts b/src/addons/fullscreen/fullscreen.test.ts deleted file mode 100644 index 6d41bdfda..000000000 --- a/src/addons/fullscreen/fullscreen.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) 2014 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { assert } from 'chai'; - -import * as fullscreen from './fullscreen'; - -class MockTerminal {} - -describe('fullscreen addon', () => { - describe('apply', () => { - it('should do register the `toggleFullscreen` method', () => { - fullscreen.apply(MockTerminal); - assert.equal(typeof (MockTerminal).prototype.toggleFullScreen, 'function'); - }); - }); -}); diff --git a/src/addons/fullscreen/fullscreen.ts b/src/addons/fullscreen/fullscreen.ts deleted file mode 100644 index 083c00c24..000000000 --- a/src/addons/fullscreen/fullscreen.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * Copyright (c) 2014 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { Terminal } from 'xterm'; - -/** - * Toggle the given terminal's fullscreen mode. - * @param term The terminal to toggle full screen mode - * @param fullscreen Toggle fullscreen on (true) or off (false) - */ -export function toggleFullScreen(term: Terminal, fullscreen: boolean): void { - let fn: (...tokens: string[]) => void; - - if (typeof fullscreen === 'undefined') { - fn = (term.element.classList.contains('fullscreen')) ? - term.element.classList.remove : term.element.classList.add; - } else if (!fullscreen) { - fn = term.element.classList.remove; - } else { - fn = term.element.classList.add; - } - - fn = fn.bind(term.element.classList); - fn('fullscreen'); -} - -export function apply(terminalConstructor: typeof Terminal): void { - (terminalConstructor.prototype).toggleFullScreen = function (fullscreen: boolean): void { - toggleFullScreen(this, fullscreen); - }; -} diff --git a/src/addons/fullscreen/package.json b/src/addons/fullscreen/package.json deleted file mode 100644 index fdaf6880b..000000000 --- a/src/addons/fullscreen/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "xterm.fullscreen", - "main": "fullscreen.js", - "private": true -} diff --git a/src/addons/fullscreen/tsconfig.json b/src/addons/fullscreen/tsconfig.json deleted file mode 100644 index 0c74c25c0..000000000 --- a/src/addons/fullscreen/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "target": "es5", - "lib": [ - "dom", - "es5" - ], - "rootDir": ".", - "outDir": "../../../lib/addons/fullscreen/", - "sourceMap": true, - "removeComments": true, - "declaration": true, - "types": [ - "../../node_modules/@types/mocha" - ] - }, - "include": [ - "**/*.ts", - "../../../typings/xterm.d.ts" - ] -} diff --git a/src/addons/search/Interfaces.ts b/src/addons/search/Interfaces.ts deleted file mode 100644 index 872537ea3..000000000 --- a/src/addons/search/Interfaces.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) 2018 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { Terminal } from 'xterm'; - -export interface ISearchAddonTerminal extends Terminal { - __searchHelper?: ISearchHelper; -} - -export interface ISearchHelper { - findNext(term: string, searchOptions: ISearchOptions): boolean; - findPrevious(term: string, searchOptions: ISearchOptions): boolean; -} - -export interface ISearchOptions { - regex?: boolean; - wholeWord?: boolean; - caseSensitive?: boolean; - /** - * Use this when you want the selection to expand if it still matches as the - * user types. Note that this only affects findNext. - */ - incremental?: boolean; -} - -export interface ISearchResult { - term: string; - col: number; - row: number; -} diff --git a/src/addons/search/package.json b/src/addons/search/package.json deleted file mode 100644 index 552e9332b..000000000 --- a/src/addons/search/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "xterm.search", - "main": "search.js", - "private": true -} diff --git a/src/addons/search/search.test.ts b/src/addons/search/search.test.ts deleted file mode 100644 index 262b475f1..000000000 --- a/src/addons/search/search.test.ts +++ /dev/null @@ -1,358 +0,0 @@ -/** - * Copyright (c) 2018 The xterm.js authors. All rights reserved. - * @license MIT - */ -declare var require: any; - -import { assert, expect } from 'chai'; -import * as search from './search'; -import { SearchHelper } from './SearchHelper'; -import { ISearchOptions, ISearchResult } from './Interfaces'; - -class MockTerminalPlain {} - -class MockTerminal { - private _core: any; - public searchHelper: TestSearchHelper; - public cols: number; - constructor(options: any) { - this._core = new (require('../../../out/Terminal')).Terminal(options); - this.searchHelper = new TestSearchHelper(this as any); - this.cols = options.cols; - } - get core(): any { - return this._core; - } - get buffer(): IBuffer { - // TODO: This is a hacky workaround until we use puppeteer for addon tests - const buffer = this._core.buffer; - return { - cursorY: buffer.y, - cursorX: buffer.x, - viewportY: buffer.ydisp, - baseY: buffer.ybase, - length: buffer.length, - getLine(y: number): IBufferLine { - return { - isWrapped: buffer.lines.get(y) ? buffer.lines.get(y).isWrapped : false, - getCell(x: number): IBufferCell { - return { - char: buffer.lines.get(y).get(x)[1/*CHAR_DATA_CHAR_INDEX*/], - width: buffer.lines.get(y).get(x)[2/*CHAR_DATA_WIDTH_INDEX*/] - }; - }, - translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string { - return buffer.translateBufferLineToString(y, trimRight); - } - }; - } - }; - } - pushWriteData(): void { - this._core._innerWrite(); - } -} - -interface IBuffer { - readonly cursorY: number; - readonly cursorX: number; - readonly viewportY: number; - readonly baseY: number; - readonly length: number; - getLine(y: number): IBufferLine | undefined; -} - -interface IBufferLine { - readonly isWrapped: boolean; - getCell(x: number): IBufferCell; - translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string; -} - -interface IBufferCell { - readonly char: string; - readonly width: number; -} - -class TestSearchHelper extends SearchHelper { - public findInLine(term: string, rowNumber: number, searchOptions?: ISearchOptions): ISearchResult { - return this._findInLine(term, rowNumber, 0, searchOptions); - } - public findFromIndex(term: string, row: number, col: number, searchOptions?: ISearchOptions, isReverseSearch?: boolean): ISearchResult { - return this._findInLine(term, row, col, searchOptions, isReverseSearch); - } -} - -describe('search addon', () => { - describe('apply', () => { - it('should register findNext and findPrevious', () => { - search.apply(MockTerminalPlain); - assert.equal(typeof (MockTerminalPlain).prototype.findNext, 'function'); - assert.equal(typeof (MockTerminalPlain).prototype.findPrevious, 'function'); - }); - }); - describe('find', () => { - it('Searchhelper - should find correct position', () => { - search.apply(MockTerminal); - const term = new MockTerminal({cols: 20, rows: 3}); - term.core.write('Hello World\r\ntest\n123....hello'); - term.pushWriteData(); - const hello0 = term.searchHelper.findInLine('Hello', 0); - const hello1 = term.searchHelper.findInLine('Hello', 1); - const hello2 = term.searchHelper.findInLine('Hello', 2); - expect(hello0).eql({col: 0, row: 0, term: 'Hello'}); - expect(hello1).eql(undefined); - expect(hello2).eql({col: 11, row: 2, term: 'Hello'}); - }); - it('should find search term accross line wrap', () => { - search.apply(MockTerminal); - const term = new MockTerminal({cols: 10, rows: 5}); - term.core.write('texttextHellotext\r\n'); - term.core.write('texttexttextHellotext goodbye'); - term.pushWriteData(); - /* - texttextHe - llotext - texttextte - xtHellotex - t (these spaces included intentionally) - goodbye - */ - - const hello0 = term.searchHelper.findInLine('Hello', 0); - const hello1 = term.searchHelper.findInLine('Hello', 1); - const hello2 = term.searchHelper.findInLine('Hello', 2); - const hello3 = term.searchHelper.findInLine('Hello', 3); - const llo = term.searchHelper.findInLine('llo', 1); - const goodbye = term.searchHelper.findInLine('goodbye', 2); - expect(hello0).eql({col: 8, row: 0, term: 'Hello'}); - expect(hello1).eql(undefined); - expect(hello2).eql({col: 2, row: 3, term: 'Hello'}); - expect(hello3).eql(undefined); - expect(llo).eql(undefined); - expect(goodbye).eql({col: 0, row: 5, term: 'goodbye'}); - term.core.resize(9, 5); - const hello0Resize = term.searchHelper.findInLine('Hello', 0); - expect(hello0Resize).eql({col: 8, row: 0, term: 'Hello'}); - }); - it('should respect search regex', () => { - search.apply(MockTerminal); - const term = new MockTerminal({cols: 10, rows: 4}); - term.core.write('abcdefghijklmnopqrstuvwxyz\r\n~/dev '); - /* - abcdefghij - klmnopqrst - uvwxyz - ~/dev - */ - term.pushWriteData(); - const searchOptions = { - regex: true, - wholeWord: false, - caseSensitive: false - }; - const hello0 = term.searchHelper.findInLine('dee*', 0, searchOptions); - const hello1 = term.searchHelper.findInLine('jkk*', 0, searchOptions); - const hello2 = term.searchHelper.findInLine('mnn*', 1, searchOptions); - const tilda0 = term.searchHelper.findInLine('^~', 3, searchOptions); - const tilda1 = term.searchHelper.findInLine('^[~]', 3, searchOptions); - const tilda2 = term.searchHelper.findInLine('^\\~', 3, searchOptions); - expect(hello0).eql({col: 3, row: 0, term: 'de'}); - expect(hello1).eql({col: 9, row: 0, term: 'jk'}); - expect(hello2).eql(undefined); - expect(tilda0).eql({col: 0, row: 3, term: '~'}); - expect(tilda1).eql({col: 0, row: 3, term: '~'}); - expect(tilda2).eql({col: 0, row: 3, term: '~'}); - }); - it('should not select empty lines', () => { - search.apply(MockTerminal); - const term = new MockTerminal({cols: 20, rows: 3}); - const line = term.searchHelper.findInLine('^.*$', 0, { regex: true }); - expect(line).eql(undefined); - }); - it('should respect case sensitive', function(): void { - search.apply(MockTerminal); - const term = new MockTerminal({cols: 20, rows: 4}); - term.core.write('Hello World\r\n123....hello\r\nmoreTestHello'); - term.pushWriteData(); - const searchOptions = { - regex: false, - wholeWord: false, - caseSensitive: true - }; - const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions); - const hello1 = term.searchHelper.findInLine('Hello', 1, searchOptions); - const hello2 = term.searchHelper.findInLine('Hello', 2, searchOptions); - expect(hello0).eql({col: 0, row: 0, term: 'Hello'}); - expect(hello1).eql(undefined); - expect(hello2).eql({col: 8, row: 2, term: 'Hello'}); - }); - it('should respect case sensitive + regex', function(): void { - search.apply(MockTerminal); - const term = new MockTerminal({cols: 20, rows: 4}); - term.core.write('hellohello\r\nHelloHello'); - term.pushWriteData(); - - /** - * hellohello - * HelloHello - */ - - const searchOptions = { - regex: true, - wholeWord: false, - caseSensitive: true - }; - const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions); - const hello1 = term.searchHelper.findInLine('Hello$', 0, searchOptions); - const hello2 = term.searchHelper.findInLine('Hello', 1, searchOptions); - const hello3 = term.searchHelper.findInLine('Hello$', 1, searchOptions); - expect(hello0).eql(undefined); - expect(hello1).eql(undefined); - expect(hello2).eql({col: 0, row: 1, term: 'Hello'}); - expect(hello3).eql({col: 5, row: 1, term: 'Hello'}); - }); - it('should respect whole-word search option', function(): void { - search.apply(MockTerminal); - const term = new MockTerminal({cols: 20, rows: 5}); - term.core.write('Hello World\r\nWorld Hello\r\nWorldHelloWorld\r\nHelloWorld\r\nWorldHello'); - term.pushWriteData(); - const searchOptions = { - regex: false, - wholeWord: true, - caseSensitive: false - }; - const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions); - const hello1 = term.searchHelper.findInLine('Hello', 1, searchOptions); - const hello2 = term.searchHelper.findInLine('Hello', 2, searchOptions); - const hello3 = term.searchHelper.findInLine('Hello', 3, searchOptions); - const hello4 = term.searchHelper.findInLine('Hello', 4, searchOptions); - expect(hello0).eql({col: 0, row: 0, term: 'Hello'}); - expect(hello1).eql({col: 6, row: 1, term: 'Hello'}); - expect(hello2).eql(undefined); - expect(hello3).eql(undefined); - expect(hello4).eql(undefined); - }); - it('should respect whole-word + case sensitive search options', function(): void { - search.apply(MockTerminal); - const term = new MockTerminal({cols: 20, rows: 5}); - term.core.write('Hello World\r\nHelloWorld'); - term.pushWriteData(); - const searchOptions = { - regex: false, - wholeWord: true, - caseSensitive: true - }; - const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions); - const hello1 = term.searchHelper.findInLine('hello', 0, searchOptions); - const hello2 = term.searchHelper.findInLine('Hello', 1, searchOptions); - const hello3 = term.searchHelper.findInLine('hello', 1, searchOptions); - expect(hello0).eql({col: 0, row: 0, term: 'Hello'}); - expect(hello1).eql(undefined); - expect(hello2).eql(undefined); - expect(hello3).eql(undefined); - }); - it('should respect whole-word + regex search options', function(): void { - search.apply(MockTerminal); - const term = new MockTerminal({cols: 20, rows: 5}); - term.core.write('Hello World Hello\r\nHelloWorldHello'); - term.pushWriteData(); - const searchOptions = { - regex: true, - wholeWord: true, - caseSensitive: false - }; - const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions); - const hello1 = term.searchHelper.findInLine('Hello$', 0, searchOptions); - const hello2 = term.searchHelper.findInLine('Hello', 1, searchOptions); - const hello3 = term.searchHelper.findInLine('Hello$', 1, searchOptions); - expect(hello0).eql({col: 0, row: 0, term: 'hello'}); - expect(hello1).eql({col: 12, row: 0, term: 'hello'}); - expect(hello2).eql(undefined); - expect(hello3).eql(undefined); - }); - it('should respect all search options', function(): void { - search.apply(MockTerminal); - const term = new MockTerminal({cols: 20, rows: 5}); - term.core.write('Hello World Hello\r\nHelloWorldHello'); - term.pushWriteData(); - const searchOptions = { - regex: true, - wholeWord: true, - caseSensitive: true - }; - const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions); - const hello1 = term.searchHelper.findInLine('Hello$', 0, searchOptions); - const hello2 = term.searchHelper.findInLine('hello', 0, searchOptions); - const hello3 = term.searchHelper.findInLine('hello$', 0, searchOptions); - const hello4 = term.searchHelper.findInLine('hello', 1, searchOptions); - const hello5 = term.searchHelper.findInLine('hello$', 1, searchOptions); - expect(hello0).eql({col: 0, row: 0, term: 'Hello'}); - expect(hello1).eql({col: 12, row: 0, term: 'Hello'}); - expect(hello2).eql(undefined); - expect(hello3).eql(undefined); - expect(hello4).eql(undefined); - expect(hello5).eql(undefined); - }); - it('should find multiple matches in line', function(): void { - search.apply(MockTerminal); - const term = new MockTerminal({cols: 20, rows: 5}); - term.core.write('helloooo helloooo\r\naaaAAaaAAA'); - term.pushWriteData(); - const searchOptions = { - regex: false, - wholeWord: false, - caseSensitive: false - }; - const find0 = term.searchHelper.findFromIndex('hello', 0, 0, searchOptions); - const find1 = term.searchHelper.findFromIndex('hello', 0, find0.col + find0.term.length, searchOptions); - const find2 = term.searchHelper.findFromIndex('aaaa', 1, 0, searchOptions); - const find3 = term.searchHelper.findFromIndex('aaaa', 1, find2.col + find2.term.length, searchOptions); - const find4 = term.searchHelper.findFromIndex('aaaa', 1, find3.col + find3.term.length, searchOptions); - expect(find0).eql({col: 0, row: 0, term: 'hello'}); - expect(find1).eql({col: 9, row: 0, term: 'hello'}); - expect(find2).eql({col: 0, row: 1, term: 'aaaa'}); - expect(find3).eql({col: 4, row: 1, term: 'aaaa'}); - expect(find4).eql(undefined); - }); - it('should find multiple matches in line - reverse search', function(): void { - search.apply(MockTerminal); - const term = new MockTerminal({cols: 20, rows: 5}); - term.core.write('it is what it is'); - term.pushWriteData(); - const searchOptions = { - regex: false, - wholeWord: false, - caseSensitive: false - }; - const isReverseSearch = true; - const find0 = term.searchHelper.findFromIndex('is', 0, 16, searchOptions, isReverseSearch); - const find1 = term.searchHelper.findFromIndex('is', 0, find0.col, searchOptions, isReverseSearch); - const find2 = term.searchHelper.findFromIndex('it', 0, 16, searchOptions, isReverseSearch); - const find3 = term.searchHelper.findFromIndex('it', 0, find2.col, searchOptions, isReverseSearch); - expect(find0).eql({col: 14, row: 0, term: 'is'}); - expect(find1).eql({col: 3, row: 0, term: 'is'}); - expect(find2).eql({col: 11, row: 0, term: 'it'}); - expect(find3).eql({col: 0, row: 0, term: 'it'}); - }); - it('should find multiple matches in line - reverse search with regex', function(): void { - search.apply(MockTerminal); - const term = new MockTerminal({cols: 20, rows: 5}); - term.core.write('zzzABCzzzzABCABC'); - term.pushWriteData(); - const searchOptions = { - regex: true, - wholeWord: false, - caseSensitive: true - }; - const isReverseSearch = true; - const find0 = term.searchHelper.findFromIndex('[A-Z]{3}', 0, 16, searchOptions, isReverseSearch); - const find1 = term.searchHelper.findFromIndex('[A-Z]{3}', 0, find0.col, searchOptions, isReverseSearch); - const find2 = term.searchHelper.findFromIndex('[A-Z]{3}', 0, find1.col, searchOptions, isReverseSearch); - const find3 = term.searchHelper.findFromIndex('[A-Z]{3}', 0, find2.col, searchOptions, isReverseSearch); - expect(find0).eql({col: 13, row: 0, term: 'ABC'}); - expect(find1).eql({col: 10, row: 0, term: 'ABC'}); - expect(find2).eql({col: 3, row: 0, term: 'ABC'}); - expect(find3).eql(undefined); - }); - }); -}); diff --git a/src/addons/search/search.ts b/src/addons/search/search.ts deleted file mode 100644 index c274f28a6..000000000 --- a/src/addons/search/search.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { SearchHelper } from './SearchHelper'; -import { Terminal } from 'xterm'; -import { ISearchAddonTerminal, ISearchOptions } from './Interfaces'; - -/** - * Find the next instance of the term, then scroll to and select it. If it - * doesn't exist, do nothing. - * @param term The search term. - * @param searchOptions Search options - * @return Whether a result was found. - */ -export function findNext(terminal: Terminal, term: string, searchOptions: ISearchOptions = {}): boolean { - const addonTerminal = terminal; - if (!addonTerminal.__searchHelper) { - addonTerminal.__searchHelper = new SearchHelper(addonTerminal); - } - return addonTerminal.__searchHelper.findNext(term, searchOptions); -} - -/** - * Find the previous instance of the term, then scroll to and select it. If it - * doesn't exist, do nothing. - * @param term The search term. - * @param searchOptions Search options - * @return Whether a result was found. - */ -export function findPrevious(terminal: Terminal, term: string, searchOptions: ISearchOptions): boolean { - const addonTerminal = terminal; - if (!addonTerminal.__searchHelper) { - addonTerminal.__searchHelper = new SearchHelper(addonTerminal); - } - return addonTerminal.__searchHelper.findPrevious(term, searchOptions); -} - -export function apply(terminalConstructor: typeof Terminal): void { - (terminalConstructor.prototype).findNext = function(term: string, searchOptions: ISearchOptions): boolean { - return findNext(this, term, searchOptions); - }; - - (terminalConstructor.prototype).findPrevious = function(term: string, searchOptions: ISearchOptions): boolean { - return findPrevious(this, term, searchOptions); - }; -} diff --git a/src/addons/terminado/Interfaces.ts b/src/addons/terminado/Interfaces.ts deleted file mode 100644 index dd7b045ca..000000000 --- a/src/addons/terminado/Interfaces.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) 2018 The xterm.js authors. All rights reserved. - * @license MIT - * - * Implements the attach method, that attaches the terminal to a WebSocket stream. - */ - -import { Terminal, IDisposable } from 'xterm'; - -export interface ITerminadoAddonTerminal extends Terminal { - _core: { - register(d: T): void; - }; - - __socket?: WebSocket; - __attachSocketBuffer?: string; - __dataListener?: IDisposable; - - __getMessage?(ev: MessageEvent): void; - __flushBuffer?(): void; - __pushToBuffer?(data: string): void; - __sendData?(data: string): void; - __setSize?(size: {rows: number, cols: number}): void; -} diff --git a/src/addons/terminado/package.json b/src/addons/terminado/package.json deleted file mode 100644 index d69595922..000000000 --- a/src/addons/terminado/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "xterm.terminado", - "main": "terminado.js", - "private": true -} diff --git a/src/addons/terminado/terminado.test.ts b/src/addons/terminado/terminado.test.ts deleted file mode 100644 index e46eafdfa..000000000 --- a/src/addons/terminado/terminado.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { assert } from 'chai'; - -import * as terminado from './terminado'; - -class MockTerminal {} - -describe('terminado addon', () => { - describe('apply', () => { - it('should do register the `terminadoAttach` and `terminadoDetach` methods', () => { - terminado.apply(MockTerminal); - assert.equal(typeof (MockTerminal).prototype.terminadoAttach, 'function'); - assert.equal(typeof (MockTerminal).prototype.terminadoDetach, 'function'); - }); - }); -}); diff --git a/src/addons/terminado/terminado.ts b/src/addons/terminado/terminado.ts deleted file mode 100644 index 9895a07b8..000000000 --- a/src/addons/terminado/terminado.ts +++ /dev/null @@ -1,111 +0,0 @@ -/** - * Copyright (c) 2016 The xterm.js authors. All rights reserved. - * @license MIT - * - * This module provides methods for attaching a terminal to a terminado - * WebSocket stream. - */ - -import { Terminal } from 'xterm'; -import { ITerminadoAddonTerminal } from './Interfaces'; - -/** - * Attaches the given terminal to the given socket. - * - * @param term The terminal to be attached to the given socket. - * @param socket The socket to attach the current terminal. - * @param bidirectional Whether the terminal should send data to the socket as well. - * @param buffered Whether the rendering of incoming data should happen instantly or at a maximum - * frequency of 1 rendering per 10ms. - */ -export function terminadoAttach(term: Terminal, socket: WebSocket, bidirectional: boolean, buffered: boolean): void { - const addonTerminal = term; - bidirectional = (typeof bidirectional === 'undefined') ? true : bidirectional; - addonTerminal.__socket = socket; - - addonTerminal.__flushBuffer = () => { - addonTerminal.write(addonTerminal.__attachSocketBuffer); - addonTerminal.__attachSocketBuffer = null; - }; - - addonTerminal.__pushToBuffer = (data: string) => { - if (addonTerminal.__attachSocketBuffer) { - addonTerminal.__attachSocketBuffer += data; - } else { - addonTerminal.__attachSocketBuffer = data; - setTimeout(addonTerminal.__flushBuffer, 10); - } - }; - - addonTerminal.__getMessage = (ev: MessageEvent) => { - const data = JSON.parse(ev.data); - if (data[0] === 'stdout') { - if (buffered) { - addonTerminal.__pushToBuffer(data[1]); - } else { - addonTerminal.write(data[1]); - } - } - }; - - addonTerminal.__sendData = (data: string) => { - socket.send(JSON.stringify(['stdin', data])); - }; - - addonTerminal.__setSize = (size: {rows: number, cols: number}) => { - socket.send(JSON.stringify(['set_size', size.rows, size.cols])); - }; - - socket.addEventListener('message', addonTerminal.__getMessage); - - if (bidirectional) { - addonTerminal._core.register(addonTerminal.onData(addonTerminal.__sendData)); - } - addonTerminal._core.register(addonTerminal.onResize(addonTerminal.__setSize)); - - socket.addEventListener('close', () => terminadoDetach(addonTerminal, socket)); - socket.addEventListener('error', () => terminadoDetach(addonTerminal, socket)); -} - -/** - * Detaches the given terminal from the given socket - * - * @param term The terminal to be detached from the given socket. - * @param socket The socket from which to detach the current terminal. - */ -export function terminadoDetach(term: Terminal, socket: WebSocket): void { - const addonTerminal = term; - addonTerminal.__dataListener.dispose(); - addonTerminal.__dataListener = undefined; - - socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket; - - if (socket) { - socket.removeEventListener('message', addonTerminal.__getMessage); - } - - delete addonTerminal.__socket; -} - -export function apply(terminalConstructor: typeof Terminal): void { - /** - * Attaches the current terminal to the given socket - * - * @param socket - The socket to attach the current terminal. - * @param bidirectional - Whether the terminal should send data to the socket as well. - * @param buffered - Whether the rendering of incoming data should happen instantly or at a - * maximum frequency of 1 rendering per 10ms. - */ - (terminalConstructor.prototype).terminadoAttach = function (socket: WebSocket, bidirectional: boolean, buffered: boolean): void { - return terminadoAttach(this, socket, bidirectional, buffered); - }; - - /** - * Detaches the current terminal from the given socket. - * - * @param socket The socket from which to detach the current terminal. - */ - (terminalConstructor.prototype).terminadoDetach = function (socket: WebSocket): void { - return terminadoDetach(this, socket); - }; -} diff --git a/src/addons/terminado/tsconfig.json b/src/addons/terminado/tsconfig.json deleted file mode 100644 index e2e194457..000000000 --- a/src/addons/terminado/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "target": "es5", - "lib": [ - "es5" - ], - "rootDir": ".", - "outDir": "../../../lib/addons/terminado/", - "sourceMap": true, - "removeComments": true, - "declaration": true, - "types": [ - "../../node_modules/@types/mocha" - ] - }, - "include": [ - "**/*.ts", - "../../../typings/xterm.d.ts" - ] -} diff --git a/src/addons/webLinks/package.json b/src/addons/webLinks/package.json deleted file mode 100644 index f200cab4a..000000000 --- a/src/addons/webLinks/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "xterm.weblinks", - "main": "weblinks.js", - "private": true -} diff --git a/src/addons/webLinks/webLinks.test.ts b/src/addons/webLinks/webLinks.test.ts deleted file mode 100644 index da5569ab8..000000000 --- a/src/addons/webLinks/webLinks.test.ts +++ /dev/null @@ -1,212 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { assert } from 'chai'; - -import * as webLinks from './webLinks'; - -class MockTerminal { - public regex: RegExp; - public handler: (event: MouseEvent, uri: string) => void; - public options?: any; - - public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: any): number { - this.regex = regex; - this.handler = handler; - this.options = options; - return 0; - } -} - -describe('webLinks addon', () => { - describe('apply', () => { - it('should do register the `webLinksInit` method', () => { - webLinks.apply(MockTerminal); - assert.equal(typeof (MockTerminal).prototype.webLinksInit, 'function'); - }); - }); - - describe('should allow simple URI path', () => { - it('foo.com', () => { - const term = new MockTerminal(); - webLinks.webLinksInit(term); - - const row = ' http://foo.com '; - - const match = row.match(term.regex); - const uri = match[term.options.matchIndex]; - - assert.equal(uri, 'http://foo.com'); - }); - - it('bar.io', () => { - const term = new MockTerminal(); - webLinks.webLinksInit(term); - - const row = ' http://bar.io '; - - const match = row.match(term.regex); - const uri = match[term.options.matchIndex]; - - assert.equal(uri, 'http://bar.io'); - }); - }); - - describe('should allow ~ character in URI path', () => { - it('foo.com', () => { - const term = new MockTerminal(); - webLinks.webLinksInit(term); - - const row = ' http://foo.com/a~b#c~d?e~f '; - - const match = row.match(term.regex); - const uri = match[term.options.matchIndex]; - - assert.equal(uri, 'http://foo.com/a~b#c~d?e~f'); - }); - - it('bar.io', () => { - const term = new MockTerminal(); - webLinks.webLinksInit(term); - - const row = ' http://bar.io/a~b#c~d?e~f '; - - const match = row.match(term.regex); - const uri = match[term.options.matchIndex]; - - assert.equal(uri, 'http://bar.io/a~b#c~d?e~f'); - }); - }); - - describe('should allow : character in URI path', () => { - it('foo.com', () => { - const term = new MockTerminal(); - webLinks.webLinksInit(term); - - const row = ' http://foo.com/colon:test '; - - const match = row.match(term.regex); - const uri = match[term.options.matchIndex]; - - assert.equal(uri, 'http://foo.com/colon:test'); - }); - - it('bar.io', () => { - const term = new MockTerminal(); - webLinks.webLinksInit(term); - - const row = ' http://bar.io/colon:test '; - - const match = row.match(term.regex); - const uri = match[term.options.matchIndex]; - - assert.equal(uri, 'http://bar.io/colon:test'); - }); - }); - - describe('should not allow : character at the end of a URI path', () => { - it('foo.com', () => { - const term = new MockTerminal(); - webLinks.webLinksInit(term); - - const row = ' http://foo.com/colon:test: '; - - const match = row.match(term.regex); - const uri = match[term.options.matchIndex]; - - assert.equal(uri, 'http://foo.com/colon:test'); - }); - - it('bar.io', () => { - const term = new MockTerminal(); - webLinks.webLinksInit(term); - - const row = ' http://bar.io/colon:test: '; - - const match = row.match(term.regex); - const uri = match[term.options.matchIndex]; - - assert.equal(uri, 'http://bar.io/colon:test'); - }); - }); - - describe('should not allow " character at the end of a URI enclosed with ""', () => { - it('foo.com', () => { - const term = new MockTerminal(); - webLinks.webLinksInit(term); - - const row = '"http://foo.com/"'; - - const match = row.match(term.regex); - const uri = match[term.options.matchIndex]; - - assert.equal(uri, 'http://foo.com/'); - }); - - it('bar.io', () => { - const term = new MockTerminal(); - webLinks.webLinksInit(term); - - const row = '"http://bar.io/"'; - - const match = row.match(term.regex); - const uri = match[term.options.matchIndex]; - - assert.equal(uri, 'http://bar.io/'); - }); - }); - - describe('should not allow \' character at the end of a URI enclosed with \'\'', () => { - it('foo.com', () => { - const term = new MockTerminal(); - webLinks.webLinksInit(term); - - const row = '\'http://foo.com/\''; - - const match = row.match(term.regex); - const uri = match[term.options.matchIndex]; - - assert.equal(uri, 'http://foo.com/'); - }); - - it('bar.io', () => { - const term = new MockTerminal(); - webLinks.webLinksInit(term); - - const row = '\'http://bar.io/\''; - - const match = row.match(term.regex); - const uri = match[term.options.matchIndex]; - - assert.equal(uri, 'http://bar.io/'); - }); - }); - - describe('should allow + character in URI path', () => { - it('foo.com', () => { - const term = new MockTerminal(); - webLinks.webLinksInit(term); - - const row = 'http://foo.com/subpath/+/id'; - - const match = row.match(term.regex); - const uri = match[term.options.matchIndex]; - - assert.equal(uri, 'http://foo.com/subpath/+/id'); - }); - - it('bar.io', () => { - const term = new MockTerminal(); - webLinks.webLinksInit(term); - - const row = 'http://bar.io/subpath/+/id'; - - const match = row.match(term.regex); - const uri = match[term.options.matchIndex]; - - assert.equal(uri, 'http://bar.io/subpath/+/id'); - }); - }); -}); diff --git a/src/addons/zmodem/package.json b/src/addons/zmodem/package.json deleted file mode 100644 index 218130ab5..000000000 --- a/src/addons/zmodem/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "xterm.zmodem", - "main": "zmodem.js", - "private": true -} diff --git a/src/addons/zmodem/tsconfig.json b/src/addons/zmodem/tsconfig.json deleted file mode 100644 index 7d821b7c5..000000000 --- a/src/addons/zmodem/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "target": "es5", - "lib": [ - "es5" - ], - "rootDir": ".", - "outDir": "../../../lib/addons/zmodem/", - "sourceMap": true, - "removeComments": true, - "declaration": true, - "types": [ - "../../node_modules/@types/mocha" - ] - }, - "include": [ - "**/*.ts", - "../../../typings/xterm.d.ts" - ] -} diff --git a/src/addons/zmodem/zmodem.test.ts b/src/addons/zmodem/zmodem.test.ts deleted file mode 100644 index d0c7c5fb4..000000000 --- a/src/addons/zmodem/zmodem.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { assert } from 'chai'; - -import * as zmodem from './zmodem'; - -class MockTerminal {} - -describe('zmodem addon', () => { - describe('apply', () => { - it('should do register the `zmodemAttach` method and `zmodemBrowser` attribute', () => { - zmodem.apply(MockTerminal); - assert.equal(typeof (MockTerminal).prototype.zmodemAttach, 'function'); - assert.equal(typeof (MockTerminal).prototype.zmodemBrowser, 'object'); - }); - }); -}); diff --git a/src/addons/zmodem/zmodem.ts b/src/addons/zmodem/zmodem.ts deleted file mode 100644 index 70fc6e98b..000000000 --- a/src/addons/zmodem/zmodem.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { Terminal } from 'xterm'; - -/** - * - * Allow xterm.js to handle ZMODEM uploads and downloads. - * - * This addon is a wrapper around zmodem.js. It adds the following to the - * Terminal class: - * - * - function `zmodemAttach(, )` - creates a Zmodem.Sentry - * on the passed WebSocket object. The Object passed is optional and - * can contain: - * - noTerminalWriteOutsideSession: Suppress writes from the Sentry - * object to the Terminal while there is no active Session. This - * is necessary for compatibility with, for example, the - * `attach.js` addon. - * - * - event `zmodemDetect` - fired on Zmodem.Sentry’s `on_detect` callback. - * Passes the zmodem.js Detection object. - * - * - event `zmodemRetract` - fired on Zmodem.Sentry’s `on_retract` callback. - * - * You’ll need to provide logic to handle uploads and downloads. - * See zmodem.js’s documentation for more details. - * - * **IMPORTANT:** After you confirm() a zmodem.js Detection, if you have - * used the `attach` or `terminado` addons, you’ll need to suspend their - * operation for the duration of the ZMODEM session. (The demo does this - * via `detach()` and a re-`attach()`.) - */ - -let zmodem: any; - -export interface IZmodemOptions { - noTerminalWriteOutsideSession?: boolean; -} - -function zmodemAttach(ws: WebSocket, opts: IZmodemOptions = {}): void { - const term = this; - const senderFunc = (octets: ArrayLike) => ws.send(new Uint8Array(octets)); - - let zsentry: any; - - function shouldWrite(): boolean { - return !!zsentry.get_confirmed_session() || !opts.noTerminalWriteOutsideSession; - } - - zsentry = new zmodem.Sentry({ - to_terminal: (octets: ArrayLike) => { - if (shouldWrite()) { - term.write( - String.fromCharCode.apply(String, octets) - ); - } - }, - sender: senderFunc, - on_retract: () => (term).emit('zmodemRetract'), - on_detect: (detection: any) => (term).emit('zmodemDetect', detection) - }); - - function handleWSMessage(evt: MessageEvent): void { - - // In testing with xterm.js’s demo the first message was - // always text even if the rest were binary. While that - // may be specific to xterm.js’s demo, ultimately we - // should reject anything that isn’t binary. - if (typeof evt.data === 'string') { - if (shouldWrite()) { - term.write(evt.data); - } - } - else { - zsentry.consume(evt.data); - } - } - - ws.binaryType = 'arraybuffer'; - ws.addEventListener('message', handleWSMessage); -} - -export function apply(terminalConstructor: typeof Terminal): void { - zmodem = (typeof window === 'object') ? (window).Zmodem : {Browser: null}; // Nullify browser for tests - - (terminalConstructor.prototype).zmodemAttach = zmodemAttach; - (terminalConstructor.prototype).zmodemBrowser = zmodem.Browser; -} diff --git a/src/ui/ColorManager.test.ts b/src/browser/ColorManager.test.ts similarity index 99% rename from src/ui/ColorManager.test.ts rename to src/browser/ColorManager.test.ts index e4d790926..a213c6166 100644 --- a/src/ui/ColorManager.test.ts +++ b/src/browser/ColorManager.test.ts @@ -5,7 +5,7 @@ import jsdom = require('jsdom'); import { assert } from 'chai'; -import { ColorManager } from 'ui/ColorManager'; +import { ColorManager } from 'browser/ColorManager'; describe('ColorManager', () => { let cm: ColorManager; diff --git a/src/ui/ColorManager.ts b/src/browser/ColorManager.ts similarity index 98% rename from src/ui/ColorManager.ts rename to src/browser/ColorManager.ts index 1252f02f3..4ac8fefe5 100644 --- a/src/ui/ColorManager.ts +++ b/src/browser/ColorManager.ts @@ -3,7 +3,8 @@ * @license MIT */ -import { IColorManager, IColor, IColorSet, ITheme } from 'ui/Types'; +import { IColorManager, IColor, IColorSet } from 'browser/Types'; +import { ITheme } from 'common/services/Services'; const DEFAULT_FOREGROUND = fromHex('#ffffff'); const DEFAULT_BACKGROUND = fromHex('#000000'); diff --git a/src/ui/Lifecycle.ts b/src/browser/Lifecycle.ts similarity index 100% rename from src/ui/Lifecycle.ts rename to src/browser/Lifecycle.ts diff --git a/src/Strings.ts b/src/browser/LocalizableStrings.ts similarity index 100% rename from src/Strings.ts rename to src/browser/LocalizableStrings.ts diff --git a/src/ui/RenderDebouncer.ts b/src/browser/RenderDebouncer.ts similarity index 100% rename from src/ui/RenderDebouncer.ts rename to src/browser/RenderDebouncer.ts diff --git a/src/ui/ScreenDprMonitor.ts b/src/browser/ScreenDprMonitor.ts similarity index 100% rename from src/ui/ScreenDprMonitor.ts rename to src/browser/ScreenDprMonitor.ts diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts new file mode 100644 index 000000000..b286295bf --- /dev/null +++ b/src/browser/TestUtils.test.ts @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IEvent, EventEmitter } from 'common/EventEmitter'; +import { ICharSizeService } from 'browser/services/Services'; + +export class MockCharSizeService implements ICharSizeService { + get hasValidSize(): boolean { return this.width > 0 && this.height > 0; } + onCharSizeChange: IEvent = new EventEmitter().event; + constructor(public width: number, public height: number) {} + measure(): void {} +} diff --git a/src/browser/Types.ts b/src/browser/Types.ts new file mode 100644 index 000000000..a1ea662c7 --- /dev/null +++ b/src/browser/Types.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +export interface IColorManager { + colors: IColorSet; +} + +export interface IColor { + css: string; + rgba: number; // 32-bit int with rgba in each byte +} + +export interface IColorSet { + foreground: IColor; + background: IColor; + cursor: IColor; + cursorAccent: IColor; + selection: IColor; + ansi: IColor[]; +} + +export interface IMouseHelper { + getCoords(event: { clientX: number, clientY: number }, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined; + getRawByteCoords(event: MouseEvent, element: HTMLElement, colCount: number, rowCount: number): { x: number | undefined, y: number | undefined }; +} diff --git a/src/browser/input/MouseHelper.test.ts b/src/browser/input/MouseHelper.test.ts new file mode 100644 index 000000000..5d4b567c5 --- /dev/null +++ b/src/browser/input/MouseHelper.test.ts @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import jsdom = require('jsdom'); +import { assert } from 'chai'; +import { MouseHelper } from 'browser/input/MouseHelper'; +import { MockCharSizeService } from 'browser/TestUtils.test'; + +const CHAR_WIDTH = 10; +const CHAR_HEIGHT = 20; + +describe('MouseHelper.getCoords', () => { + let document: Document; + let mouseHelper: MouseHelper; + + beforeEach(() => { + document = new jsdom.JSDOM('').window.document; + const mockRenderService = { + dimensions: { + actualCellWidth: CHAR_WIDTH, + actualCellHeight: CHAR_HEIGHT + } + }; + mouseHelper = new MouseHelper(mockRenderService as any, new MockCharSizeService(CHAR_WIDTH, CHAR_HEIGHT)); + }); + + it('should return the cell that was clicked', () => { + let coords: [number, number] | undefined; + coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH / 2, clientY: CHAR_HEIGHT / 2 }, document.createElement('div'), 10, 10); + assert.deepEqual(coords, [1, 1]); + coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT }, document.createElement('div'), 10, 10); + assert.deepEqual(coords, [1, 1]); + coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT + 1 }, document.createElement('div'), 10, 10); + assert.deepEqual(coords, [1, 2]); + coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH + 1, clientY: CHAR_HEIGHT }, document.createElement('div'), 10, 10); + assert.deepEqual(coords, [2, 1]); + }); + + it('should ensure the coordinates are returned within the terminal bounds', () => { + let coords: [number, number] | undefined; + coords = mouseHelper.getCoords({ clientX: -1, clientY: -1 }, document.createElement('div'), 10, 10); + assert.deepEqual(coords, [1, 1]); + // Event are double the cols/rows + coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH * 20, clientY: CHAR_HEIGHT * 20 }, document.createElement('div'), 10, 10); + assert.deepEqual(coords, [10, 10], 'coordinates should never come back as larger than the terminal'); + }); +}); diff --git a/src/MouseHelper.ts b/src/browser/input/MouseHelper.ts similarity index 67% rename from src/MouseHelper.ts rename to src/browser/input/MouseHelper.ts index 12905222a..b99757dbc 100644 --- a/src/MouseHelper.ts +++ b/src/browser/input/MouseHelper.ts @@ -3,12 +3,14 @@ * @license MIT */ -import { ICharMeasure, IMouseHelper } from './Types'; -import { RenderCoordinator } from './renderer/RenderCoordinator'; +import { IMouseHelper } from 'browser/Types'; +import { RenderService } from 'browser/services/RenderService'; +import { ICharSizeService } from 'browser/services/Services'; export class MouseHelper implements IMouseHelper { constructor( - private _renderCoordinator: RenderCoordinator + private _renderService: RenderService, + private _charSizeService: ICharSizeService ) { } @@ -23,26 +25,25 @@ export class MouseHelper implements IMouseHelper { * little faster and this function is used in some low level code. * @param event The mouse event. * @param element The terminal's container element. - * @param charMeasure The char measure object used to determine character sizes. * @param colCount The number of columns in the terminal. * @param rowCount The number of rows n the terminal. * @param isSelection Whether the request is for the selection or not. This will * apply an offset to the x value such that the left half of the cell will * select that cell and the right half will select the next cell. */ - public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, charMeasure: ICharMeasure, colCount: number, rowCount: number, isSelection?: boolean): [number, number] { - // Coordinates cannot be measured if charMeasure has not been initialized - if (!charMeasure.width || !charMeasure.height) { - return null; + public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined { + // Coordinates cannot be measured if there are no valid + if (!this._charSizeService.hasValidSize) { + return undefined; } const coords = MouseHelper.getCoordsRelativeToElement(event, element); if (!coords) { - return null; + return undefined; } - coords[0] = Math.ceil((coords[0] + (isSelection ? this._renderCoordinator.dimensions.actualCellWidth / 2 : 0)) / this._renderCoordinator.dimensions.actualCellWidth); - coords[1] = Math.ceil(coords[1] / this._renderCoordinator.dimensions.actualCellHeight); + coords[0] = Math.ceil((coords[0] + (isSelection ? this._renderService.dimensions.actualCellWidth / 2 : 0)) / this._renderService.dimensions.actualCellWidth); + coords[1] = Math.ceil(coords[1] / this._renderService.dimensions.actualCellHeight); // Ensure coordinates are within the terminal viewport. Note that selections // need an addition point of precision to cover the end point (as characters @@ -59,18 +60,15 @@ export class MouseHelper implements IMouseHelper { * as expected by xterm. * @param event The mouse event. * @param element The terminal's container element. - * @param charMeasure The char measure object used to determine character sizes. * @param colCount The number of columns in the terminal. * @param rowCount The number of rows in the terminal. */ - public getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, colCount: number, rowCount: number): { x: number, y: number } { - const coords = this.getCoords(event, element, charMeasure, colCount, rowCount); - let x = coords[0]; - let y = coords[1]; + public getRawByteCoords(event: MouseEvent, element: HTMLElement, colCount: number, rowCount: number): { x: number | undefined, y: number | undefined } { + const coords = this.getCoords(event, element, colCount, rowCount); // xterm sends raw bytes and starts at 32 (SP) for each. - x += 32; - y += 32; + const x = coords ? coords[0] + 32 : undefined; + const y = coords ? coords[1] + 32 : undefined; return { x, y }; } diff --git a/src/browser/renderer/Types.ts b/src/browser/renderer/Types.ts new file mode 100644 index 000000000..e580ff8d4 --- /dev/null +++ b/src/browser/renderer/Types.ts @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IDisposable } from 'common/Types'; +import { IColorSet } from 'browser/Types'; + +export type CharacterJoinerHandler = (text: string) => [number, number][]; + +export interface IRenderDimensions { + scaledCharWidth: number; + scaledCharHeight: number; + scaledCellWidth: number; + scaledCellHeight: number; + scaledCharLeft: number; + scaledCharTop: number; + scaledCanvasWidth: number; + scaledCanvasHeight: number; + canvasWidth: number; + canvasHeight: number; + actualCellWidth: number; + actualCellHeight: number; +} + +/** + * Note that IRenderer implementations should emit the refresh event after + * rendering rows to the screen. + */ +export interface IRenderer extends IDisposable { + readonly dimensions: IRenderDimensions; + + dispose(): void; + setColors(colors: IColorSet): void; + onDevicePixelRatioChange(): void; + onResize(cols: number, rows: number): void; + onCharSizeChanged(): void; + onBlur(): void; + onFocus(): void; + onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void; + onCursorMove(): void; + onOptionsChanged(): void; + clear(): void; + renderRows(start: number, end: number): void; + registerCharacterJoiner(handler: CharacterJoinerHandler): number; + deregisterCharacterJoiner(joinerId: number): boolean; +} diff --git a/src/browser/services/CharSizeService.ts b/src/browser/services/CharSizeService.ts new file mode 100644 index 000000000..42920bfa3 --- /dev/null +++ b/src/browser/services/CharSizeService.ts @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2016 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IOptionsService } from 'common/services/Services'; +import { IEvent, EventEmitter } from 'common/EventEmitter'; +import { ICharSizeService } from 'browser/services/Services'; + +export class CharSizeService implements ICharSizeService { + public width: number = 0; + public height: number = 0; + private _measureStrategy: IMeasureStrategy; + + public get hasValidSize(): boolean { return this.width > 0 && this.height > 0; } + + private _onCharSizeChange = new EventEmitter(); + public get onCharSizeChange(): IEvent { return this._onCharSizeChange.event; } + + constructor( + document: Document, + parentElement: HTMLElement, + private _optionsService: IOptionsService + ) { + this._measureStrategy = new DomMeasureStrategy(document, parentElement, this._optionsService); + } + + public measure(): void { + const result = this._measureStrategy.measure(); + if (result.width !== this.width || result.height !== this.height) { + this.width = result.width; + this.height = result.height; + this._onCharSizeChange.fire(); + } + } +} + +interface IMeasureStrategy { + measure(): IReadonlyMeasureResult; +} + +interface IReadonlyMeasureResult { + readonly width: number; + readonly height: number; +} + +interface IMeasureResult { + width: number; + height: number; +} + +// TODO: For supporting browsers we should also provide a CanvasCharDimensionsProvider that uses ctx.measureText +class DomMeasureStrategy implements IMeasureStrategy { + private _result: IMeasureResult = { width: 0, height: 0 }; + private _measureElement: HTMLElement; + + constructor( + private _document: Document, + private _parentElement: HTMLElement, + private _optionsService: IOptionsService + ) { + this._measureElement = this._document.createElement('span'); + this._measureElement.classList.add('xterm-char-measure-element'); + this._measureElement.textContent = 'W'; + this._measureElement.setAttribute('aria-hidden', 'true'); + this._parentElement.appendChild(this._measureElement); + } + + public measure(): IReadonlyMeasureResult { + this._measureElement.style.fontFamily = this._optionsService.options.fontFamily; + this._measureElement.style.fontSize = `${this._optionsService.options.fontSize}px`; + + // Note that this triggers a synchronous layout + const geometry = this._measureElement.getBoundingClientRect(); + + // If values are 0 then the element is likely currently display:none, in which case we should + // retain the previous value. + if (geometry.width !== 0 && geometry.height !== 0) { + this._result.width = geometry.width; + this._result.height = Math.ceil(geometry.height); + } + + return this._result; + } +} diff --git a/src/renderer/RenderCoordinator.ts b/src/browser/services/RenderService.ts similarity index 82% rename from src/renderer/RenderCoordinator.ts rename to src/browser/services/RenderService.ts index 98c9ae865..5035ed3d8 100644 --- a/src/renderer/RenderCoordinator.ts +++ b/src/browser/services/RenderService.ts @@ -3,17 +3,17 @@ * @license MIT */ -import { IRenderer, IRenderDimensions } from './Types'; -import { RenderDebouncer } from 'ui/RenderDebouncer'; -import { EventEmitter2, IEvent } from 'common/EventEmitter2'; +import { IRenderer, IRenderDimensions, CharacterJoinerHandler } from 'browser/renderer/Types'; +import { RenderDebouncer } from 'browser/RenderDebouncer'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; import { Disposable } from 'common/Lifecycle'; -import { ScreenDprMonitor } from 'ui/ScreenDprMonitor'; -import { addDisposableDomListener } from 'ui/Lifecycle'; -import { IColorSet } from 'ui/Types'; -import { CharacterJoinerHandler } from '../Types'; -import { IOptionsService } from 'common/options/Types'; +import { ScreenDprMonitor } from 'browser/ScreenDprMonitor'; +import { addDisposableDomListener } from 'browser/Lifecycle'; +import { IColorSet } from 'browser/Types'; +import { IOptionsService } from 'common/services/Services'; +import { ICharSizeService, IRenderService } from 'browser/services/Services'; -export class RenderCoordinator extends Disposable { +export class RenderService extends Disposable implements IRenderService { private _renderDebouncer: RenderDebouncer; private _screenDprMonitor: ScreenDprMonitor; @@ -22,11 +22,11 @@ export class RenderCoordinator extends Disposable { private _canvasWidth: number = 0; private _canvasHeight: number = 0; - private _onDimensionsChange = new EventEmitter2(); + private _onDimensionsChange = new EventEmitter(); public get onDimensionsChange(): IEvent { return this._onDimensionsChange.event; } - private _onRender = new EventEmitter2<{ start: number, end: number }>(); + private _onRender = new EventEmitter<{ start: number, end: number }>(); public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; } - private _onRefreshRequest = new EventEmitter2<{ start: number, end: number }>(); + private _onRefreshRequest = new EventEmitter<{ start: number, end: number }>(); public get onRefreshRequest(): IEvent<{ start: number, end: number }> { return this._onRefreshRequest.event; } public get dimensions(): IRenderDimensions { return this._renderer.dimensions; } @@ -35,7 +35,8 @@ export class RenderCoordinator extends Disposable { private _renderer: IRenderer, private _rowCount: number, screenElement: HTMLElement, - optionsService: IOptionsService + optionsService: IOptionsService, + charSizeService: ICharSizeService ) { super(); this._renderDebouncer = new RenderDebouncer((start, end) => this._renderRows(start, end)); @@ -46,6 +47,7 @@ export class RenderCoordinator extends Disposable { this.register(this._screenDprMonitor); this.register(optionsService.onOptionChange(() => this._renderer.onOptionsChanged())); + this.register(charSizeService.onCharSizeChange(() => this.onCharSizeChanged())); // dprchange should handle this case, we need this as well for browsers that don't support the // matchMedia query. diff --git a/src/browser/services/Services.d.ts b/src/browser/services/Services.d.ts new file mode 100644 index 000000000..1916572c3 --- /dev/null +++ b/src/browser/services/Services.d.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IEvent } from 'common/EventEmitter'; +import { IRenderDimensions, IRenderer, CharacterJoinerHandler } from 'browser/renderer/Types'; +import { IColorSet } from 'browser/Types'; + +export interface ICharSizeService { + readonly width: number; + readonly height: number; + readonly hasValidSize: boolean; + + readonly onCharSizeChange: IEvent; + + measure(): void; +} + +export interface IRenderService { + onDimensionsChange: IEvent; + onRender: IEvent<{ start: number, end: number }>; + onRefreshRequest: IEvent<{ start: number, end: number }>; + + dimensions: IRenderDimensions; + + refreshRows(start: number, end: number): void; + resize(cols: number, rows: number): void; + changeOptions(): void; + setRenderer(renderer: IRenderer): void; + setColors(colors: IColorSet): void; + onDevicePixelRatioChange(): void; + onResize(cols: number, rows: number): void; + // TODO: Is this useful when we have onResize? + onCharSizeChanged(): void; + onBlur(): void; + onFocus(): void; + onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void; + onCursorMove(): void; + clear(): void; + registerCharacterJoiner(handler: CharacterJoinerHandler): number; + deregisterCharacterJoiner(joinerId: number): boolean; +} diff --git a/src/ui/tsconfig.json b/src/browser/tsconfig.json similarity index 100% rename from src/ui/tsconfig.json rename to src/browser/tsconfig.json diff --git a/src/CharWidth.test.ts b/src/common/CharWidth.test.ts similarity index 70% rename from src/CharWidth.test.ts rename to src/common/CharWidth.test.ts index 4aec39953..6b2493eef 100644 --- a/src/CharWidth.test.ts +++ b/src/common/CharWidth.test.ts @@ -3,80 +3,8 @@ * @license MIT */ -import { TestTerminal } from './TestUtils.test'; import { assert } from 'chai'; -import { getStringCellWidth, wcwidth } from './CharWidth'; -import { IBuffer } from './Types'; -import { CellData, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from 'core/buffer/BufferLine'; - - -describe('getStringCellWidth', function(): void { - let terminal: TestTerminal; - - beforeEach(() => { - terminal = new TestTerminal({rows: 5, cols: 30}); - }); - - function sumWidths(buffer: IBuffer, start: number, end: number, sentinel: string): number { - let result = 0; - for (let i = start; i < end; ++i) { - const line = buffer.lines.get(i); - for (let j = 0; j < line.length; ++j) { // TODO: change to trimBorder with multiline - const ch = line.loadCell(j, new CellData()).getAsCharData(); - result += ch[CHAR_DATA_WIDTH_INDEX]; - // return on sentinel - if (ch[CHAR_DATA_CHAR_INDEX] === sentinel) { - return result; - } - } - } - return result; - } - - it('ASCII chars', function(): void { - const input = 'This is just ASCII text.#'; - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#')); - }); - it('combining chars', function(): void { - const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301#'; - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#')); - }); - it('surrogate chars', function(): void { - const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞#'; - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#')); - }); - it('surrogate combining chars', function(): void { - const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301#'; - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#')); - }); - it('fullwidth chars', function(): void { - const input = '1234567890#'; - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#')); - }); - it('fullwidth chars offset 1', function(): void { - const input = 'a1234567890#'; - terminal.writeSync(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#')); - }); - // TODO: multiline tests once #1685 is resolved -}); +import { wcwidth } from 'common/CharWidth'; it('wcwidth should match all values from the old implementation', function(): void { // old implementation @@ -197,7 +125,7 @@ it('wcwidth should match all values from the old implementation', function(): vo return 1; } const control = opts.control | 0; - let table: number[] | Uint32Array = null; + let table: number[] | Uint32Array | undefined; function initTable(): number[] | Uint32Array { // lookup table for BMP const CODEPOINTS = 65536; // BMP holds 65536 codepoints diff --git a/src/CharWidth.ts b/src/common/CharWidth.ts similarity index 100% rename from src/CharWidth.ts rename to src/common/CharWidth.ts diff --git a/src/common/CircularList.ts b/src/common/CircularList.ts index 88e714b8d..d4ad3fce6 100644 --- a/src/common/CircularList.ts +++ b/src/common/CircularList.ts @@ -4,7 +4,7 @@ */ import { ICircularList } from 'common/Types'; -import { EventEmitter2, IEvent } from 'common/EventEmitter2'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; export interface IInsertEvent { index: number; @@ -25,11 +25,11 @@ export class CircularList implements ICircularList { private _startIndex: number; private _length: number; - public onDeleteEmitter = new EventEmitter2(); + public onDeleteEmitter = new EventEmitter(); public get onDelete(): IEvent { return this.onDeleteEmitter.event; } - public onInsertEmitter = new EventEmitter2(); + public onInsertEmitter = new EventEmitter(); public get onInsert(): IEvent { return this.onInsertEmitter.event; } - public onTrimEmitter = new EventEmitter2(); + public onTrimEmitter = new EventEmitter(); public get onTrim(): IEvent { return this.onTrimEmitter.event; } constructor( diff --git a/src/common/EventEmitter.test.ts b/src/common/EventEmitter.test.ts index c7f75ba0d..e9013a99c 100644 --- a/src/common/EventEmitter.test.ts +++ b/src/common/EventEmitter.test.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * Copyright (c) 2019 The xterm.js authors. All rights reserved. * @license MIT */ @@ -7,71 +7,25 @@ import { assert } from 'chai'; import { EventEmitter } from 'common/EventEmitter'; describe('EventEmitter', () => { - let eventEmitter: EventEmitter; - - beforeEach(() => { - eventEmitter = new EventEmitter(); + it('should fire listeners multiple times', () => { + const order: string[] = []; + const emitter = new EventEmitter(); + emitter.event(data => order.push(data + 'a')); + emitter.event(data => order.push(data + 'b')); + emitter.fire(1); + emitter.fire(2); + assert.deepEqual(order, [ '1a', '1b', '2a', '2b' ]); }); - describe('emit', () => { - it('should emit events to listeners', () => { - let count1 = 0; - let count2 = 0; - const listener1 = () => count1++; - const listener2 = () => count2++; - eventEmitter.on('test', listener1); - eventEmitter.on('test', listener2); - eventEmitter.emit('test'); - assert.equal(count1, 1); - assert.equal(count2, 1); - eventEmitter.emit('test'); - assert.equal(count1, 2); - assert.equal(count2, 2); - }); - - it('should manage multiple listener types', () => { - let count1 = 0; - let count2 = 0; - const listener1 = () => count1++; - const listener2 = () => count2++; - eventEmitter.on('test', listener1); - eventEmitter.on('foo', listener2); - eventEmitter.emit('test'); - assert.equal(count1, 1); - assert.equal(count2, 0); - eventEmitter.emit('foo'); - assert.equal(count1, 1); - assert.equal(count2, 1); - }); - }); - - describe('listeners', () => { - it('should return listeners for the type requested', () => { - assert.equal(eventEmitter.listeners('test').length, 0); - const listener = () => {}; - eventEmitter.on('test', listener); - assert.deepEqual(eventEmitter.listeners('test'), [listener]); - }); - }); - - describe('off', () => { - it('should remove the specific listener', () => { - const listener1 = () => {}; - const listener2 = () => {}; - eventEmitter.on('foo', listener1); - eventEmitter.on('foo', listener2); - assert.equal(eventEmitter.listeners('foo').length, 2); - eventEmitter.off('foo', listener1); - assert.deepEqual(eventEmitter.listeners('foo'), [listener2]); - }); - }); - - describe('removeAllListeners', () => { - it('should clear all listeners', () => { - eventEmitter.on('foo', () => {}); - assert.equal(eventEmitter.listeners('foo').length, 1); - eventEmitter.removeAllListeners('foo'); - assert.equal(eventEmitter.listeners('foo').length, 0); - }); + it('should not fire listeners once disposed', () => { + const order: string[] = []; + const emitter = new EventEmitter(); + emitter.event(data => order.push(data + 'a')); + const disposeB = emitter.event(data => order.push(data + 'b')); + emitter.event(data => order.push(data + 'c')); + emitter.fire(1); + disposeB.dispose(); + emitter.fire(2); + assert.deepEqual(order, [ '1a', '1b', '1c', '2a', '2c' ]); }); }); diff --git a/src/common/EventEmitter.ts b/src/common/EventEmitter.ts index 266216a44..efc101ce8 100644 --- a/src/common/EventEmitter.ts +++ b/src/common/EventEmitter.ts @@ -1,98 +1,49 @@ /** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * Copyright (c) 2019 The xterm.js authors. All rights reserved. * @license MIT */ -import { IDisposable, IEventEmitter, XtermListener } from 'common/Types'; -import { Disposable } from 'common/Lifecycle'; +import { IDisposable } from 'common/Types'; -export class EventEmitter extends Disposable implements IEventEmitter, IDisposable { - private _events: {[type: string]: XtermListener[]}; +interface IListener { + (e: T): void; +} - constructor() { - super(); - // Restore the previous events if available, this will happen if the - // constructor is called multiple times on the same object (terminal reset). - this._events = (this)._events || {}; - } +export interface IEvent { + (listener: (e: T) => any): IDisposable; +} - public on(type: string, listener: XtermListener): void { - this._events[type] = this._events[type] || []; - this._events[type].push(listener); - } +export class EventEmitter { + private _listeners: IListener[] = []; + private _event?: IEvent; - /** - * Adds a disposable listener to the EventEmitter, returning the disposable. - * @param type The event type. - * @param handler The handler for the listener. - */ - public addDisposableListener(type: string, handler: XtermListener): IDisposable { - // TODO: Rename addDisposableEventListener to more easily disambiguate from Dom listener - this.on(type, handler); - let disposed = false; - return { - dispose: () => { - if (disposed) { - // Already disposed - return; - } - this.off(type, handler); - disposed = true; - } - }; - } - - public off(type: string, listener: XtermListener): void { - if (!this._events[type]) { - return; - } - - const obj = this._events[type]; - let i = obj.length; - - while (i--) { - if (obj[i] === listener) { - obj.splice(i, 1); - return; - } + public get event(): IEvent { + if (!this._event) { + this._event = (listener: (e: T) => any) => { + this._listeners.push(listener); + const disposable = { + dispose: () => { + for (let i = 0; i < this._listeners.length; i++) { + if (this._listeners[i] === listener) { + this._listeners.splice(i, 1); + return; + } + } + } + }; + return disposable; + }; } + return this._event; } - public removeAllListeners(type: string): void { - if (this._events[type]) { - delete this._events[type]; + public fire(data: T): void { + const queue: IListener[] = []; + for (let i = 0; i < this._listeners.length; i++) { + queue.push(this._listeners[i]); } - } - - public emit(type: string, ...args: any[]): void { - if (!this._events[type]) { - return; + for (let i = 0; i < queue.length; i++) { + queue[i].call(undefined, data); } - const obj = this._events[type]; - for (let i = 0; i < obj.length; i++) { - obj[i].apply(this, args); - } - } - - public emitMayRemoveListeners(type: string, ...args: any[]): void { - if (!this._events[type]) { - return; - } - const obj = this._events[type]; - let length = obj.length; - for (let i = 0; i < obj.length; i++) { - obj[i].apply(this, args); - i -= length - obj.length; - length = obj.length; - } - } - - public listeners(type: string): XtermListener[] { - return this._events[type] || []; - } - - public dispose(): void { - super.dispose(); - this._events = {}; } } diff --git a/src/common/EventEmitter2.test.ts b/src/common/EventEmitter2.test.ts deleted file mode 100644 index bd8b5dea0..000000000 --- a/src/common/EventEmitter2.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) 2019 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { assert } from 'chai'; -import { EventEmitter2 } from 'common/EventEmitter2'; - -describe('EventEmitter2', () => { - it('should fire listeners multiple times', () => { - const order: string[] = []; - const emitter = new EventEmitter2(); - emitter.event(data => order.push(data + 'a')); - emitter.event(data => order.push(data + 'b')); - emitter.fire(1); - emitter.fire(2); - assert.deepEqual(order, [ '1a', '1b', '2a', '2b' ]); - }); - - it('should not fire listeners once disposed', () => { - const order: string[] = []; - const emitter = new EventEmitter2(); - emitter.event(data => order.push(data + 'a')); - const disposeB = emitter.event(data => order.push(data + 'b')); - emitter.event(data => order.push(data + 'c')); - emitter.fire(1); - disposeB.dispose(); - emitter.fire(2); - assert.deepEqual(order, [ '1a', '1b', '1c', '2a', '2c' ]); - }); -}); diff --git a/src/common/EventEmitter2.ts b/src/common/EventEmitter2.ts deleted file mode 100644 index fcbef9697..000000000 --- a/src/common/EventEmitter2.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright (c) 2019 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { IDisposable } from 'common/Types'; - -interface IListener { - (e: T): void; -} - -export interface IEvent { - (listener: (e: T) => any): IDisposable; -} - -export class EventEmitter2 { - private _listeners: IListener[] = []; - private _event?: IEvent; - - public get event(): IEvent { - if (!this._event) { - this._event = (listener: (e: T) => any) => { - this._listeners.push(listener); - const disposable = { - dispose: () => { - for (let i = 0; i < this._listeners.length; i++) { - if (this._listeners[i] === listener) { - this._listeners.splice(i, 1); - return; - } - } - } - }; - return disposable; - }; - } - return this._event; - } - - public fire(data: T): void { - const queue: IListener[] = []; - for (let i = 0; i < this._listeners.length; i++) { - queue.push(this._listeners[i]); - } - for (let i = 0; i < queue.length; i++) { - queue[i].call(undefined, data); - } - } -} diff --git a/src/common/Platform.ts b/src/common/Platform.ts index ee82cff4f..87d466c01 100644 --- a/src/common/Platform.ts +++ b/src/common/Platform.ts @@ -19,7 +19,6 @@ const platform = (isNode) ? 'node' : navigator.platform; export const isFirefox = !!~userAgent.indexOf('Firefox'); export const isSafari = /^((?!chrome|android).)*safari/i.test(userAgent); -export const isMSIE = !!~userAgent.indexOf('MSIE') || !!~userAgent.indexOf('Trident'); // Find the users platform. We use this to interpret the meta key // and ISO third level shifts. diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts new file mode 100644 index 000000000..8e03dcad6 --- /dev/null +++ b/src/common/TestUtils.test.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IBufferService, IOptionsService, ITerminalOptions, IPartialTerminalOptions } from 'common/services/Services'; +import { IEvent, EventEmitter } from 'common/EventEmitter'; +import { clone } from 'common/Clone'; +import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; +import { IBufferSet, IBuffer } from 'common/buffer/Types'; + +export class MockBufferService implements IBufferService { + public buffer: IBuffer = {} as any; + public buffers: IBufferSet = {} as any; + constructor( + public cols: number, + public rows: number + ) {} + resize(cols: number, rows: number): void { + this.cols = cols; + this.rows = rows; + } + reset(): void {} +} + +export class MockOptionsService implements IOptionsService { + options: ITerminalOptions = clone(DEFAULT_OPTIONS); + onOptionChange: IEvent = new EventEmitter().event; + constructor(testOptions: IPartialTerminalOptions) { + Object.keys(testOptions).forEach(key => this.options[key] = (testOptions)[key]); + } + setOption(key: string, value: T): void { + throw new Error('Method not implemented.'); + } + getOption(key: string): T { + throw new Error('Method not implemented.'); + } +} diff --git a/src/common/Types.ts b/src/common/Types.ts index e0a1e612b..b29515d4e 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IEvent, EventEmitter2 } from 'common/EventEmitter2'; +import { IEvent, EventEmitter } from 'common/EventEmitter'; import { IDeleteEvent, IInsertEvent } from 'common/CircularList'; export const DEFAULT_COLOR = 256; @@ -40,11 +40,11 @@ export interface ICircularList { maxLength: number; isFull: boolean; - onDeleteEmitter: EventEmitter2; + onDeleteEmitter: EventEmitter; onDelete: IEvent; - onInsertEmitter: EventEmitter2; + onInsertEmitter: EventEmitter; onInsert: IEvent; - onTrimEmitter: EventEmitter2; + onTrimEmitter: EventEmitter; onTrim: IEvent; get(index: number): T | undefined; @@ -56,3 +56,105 @@ export interface ICircularList { trimStart(count: number): void; shiftElements(start: number, count: number, offset: number): void; } + +export const enum KeyboardResultType { + SEND_KEY, + SELECT_ALL, + PAGE_UP, + PAGE_DOWN +} + +export interface IKeyboardResult { + type: KeyboardResultType; + cancel: boolean; + key: string | undefined; +} + +export interface ICharset { + [key: string]: string; +} + +export type CharData = [number, string, number, number]; +export type IColorRGB = [number, number, number]; + +/** Attribute data */ +export interface IAttributeData { + fg: number; + bg: number; + + clone(): IAttributeData; + + // flags + isInverse(): number; + isBold(): number; + isUnderline(): number; + isBlink(): number; + isInvisible(): number; + isItalic(): number; + isDim(): number; + + // color modes + getFgColorMode(): number; + getBgColorMode(): number; + isFgRGB(): boolean; + isBgRGB(): boolean; + isFgPalette(): boolean; + isBgPalette(): boolean; + isFgDefault(): boolean; + isBgDefault(): boolean; + + // colors + getFgColor(): number; + getBgColor(): number; +} + +/** Cell data */ +export interface ICellData extends IAttributeData { + content: number; + combinedData: string; + isCombined(): number; + getWidth(): number; + getChars(): string; + getCode(): number; + setFromCharData(value: CharData): void; + getAsCharData(): CharData; +} + +/** + * Interface for a line in the terminal buffer. + */ +export interface IBufferLine { + length: number; + isWrapped: boolean; + get(index: number): CharData; + set(index: number, value: CharData): void; + loadCell(index: number, cell: ICellData): ICellData; + setCell(index: number, cell: ICellData): void; + setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void; + addCodepointToCell(index: number, codePoint: number): void; + insertCells(pos: number, n: number, ch: ICellData): void; + deleteCells(pos: number, n: number, fill: ICellData): void; + replaceCells(start: number, end: number, fill: ICellData): void; + resize(cols: number, fill: ICellData): void; + fill(fillCellData: ICellData): void; + copyFrom(line: IBufferLine): void; + clone(): IBufferLine; + getTrimmedLength(): number; + translateToString(trimRight?: boolean, startCol?: number, endCol?: number): string; + + /* direct access to cell attrs */ + getWidth(index: number): number; + hasWidth(index: number): number; + getFg(index: number): number; + getBg(index: number): number; + hasContent(index: number): number; + getCodePoint(index: number): number; + isCombined(index: number): number; + getString(index: number): string; +} + +export interface IMarker extends IDisposable { + readonly id: number; + readonly isDisposed: boolean; + readonly line: number; +} diff --git a/src/common/buffer/Buffer.test.ts b/src/common/buffer/Buffer.test.ts new file mode 100644 index 000000000..01516b3cf --- /dev/null +++ b/src/common/buffer/Buffer.test.ts @@ -0,0 +1,1398 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; +import { Buffer } from 'common/buffer/Buffer'; +import { CircularList } from 'common/CircularList'; +import { MockOptionsService, MockBufferService } from 'common/TestUtils.test'; +import { BufferLine, CellData, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; + +const INIT_COLS = 80; +const INIT_ROWS = 24; +const INIT_SCROLLBACK = 1000; + +describe('Buffer', () => { + let optionsService: MockOptionsService; + let bufferService: MockBufferService; + let buffer: Buffer; + + beforeEach(() => { + optionsService = new MockOptionsService({ scrollback: INIT_SCROLLBACK }); + bufferService = new MockBufferService(INIT_COLS, INIT_ROWS); + buffer = new Buffer(true, optionsService, bufferService); + }); + + describe('constructor', () => { + it('should create a CircularList with max length equal to rows + scrollback, for its lines', () => { + assert.instanceOf(buffer.lines, CircularList); + assert.equal(buffer.lines.maxLength, bufferService.rows + INIT_SCROLLBACK); + }); + it('should set the Buffer\'s scrollBottom value equal to the terminal\'s rows -1', () => { + assert.equal(buffer.scrollBottom, bufferService.rows - 1); + }); + }); + + describe('fillViewportRows', () => { + it('should fill the buffer with blank lines based on the size of the viewport', () => { + const blankLineChar = buffer.getBlankLine(DEFAULT_ATTR_DATA).loadCell(0, new CellData()).getAsCharData(); + buffer.fillViewportRows(); + assert.equal(buffer.lines.length, INIT_ROWS); + for (let y = 0; y < INIT_ROWS; y++) { + assert.equal(buffer.lines.get(y)!.length, INIT_COLS); + for (let x = 0; x < INIT_COLS; x++) { + assert.deepEqual(buffer.lines.get(y)!.loadCell(x, new CellData()).getAsCharData(), blankLineChar); + } + } + }); + }); + + describe('getWrappedRangeForLine', () => { + describe('non-wrapped', () => { + it('should return a single row for the first row', () => { + buffer.fillViewportRows(); + assert.deepEqual(buffer.getWrappedRangeForLine(0), { first: 0, last: 0 }); + }); + it('should return a single row for a middle row', () => { + buffer.fillViewportRows(); + assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 12, last: 12 }); + }); + it('should return a single row for the last row', () => { + buffer.fillViewportRows(); + assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 1), { first: 23, last: 23 }); + }); + }); + describe('wrapped', () => { + it('should return a range for the first row', () => { + buffer.fillViewportRows(); + buffer.lines.get(1)!.isWrapped = true; + assert.deepEqual(buffer.getWrappedRangeForLine(0), { first: 0, last: 1 }); + }); + it('should return a range for a middle row wrapping upwards', () => { + buffer.fillViewportRows(); + buffer.lines.get(12)!.isWrapped = true; + assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 11, last: 12 }); + }); + it('should return a range for a middle row wrapping downwards', () => { + buffer.fillViewportRows(); + buffer.lines.get(13)!.isWrapped = true; + assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 12, last: 13 }); + }); + it('should return a range for a middle row wrapping both ways', () => { + buffer.fillViewportRows(); + buffer.lines.get(11)!.isWrapped = true; + buffer.lines.get(12)!.isWrapped = true; + buffer.lines.get(13)!.isWrapped = true; + buffer.lines.get(14)!.isWrapped = true; + assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 10, last: 14 }); + }); + it('should return a range for the last row', () => { + buffer.fillViewportRows(); + buffer.lines.get(23)!.isWrapped = true; + assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 1), { first: 22, last: 23 }); + }); + it('should return a range for a row that wraps upward to first row', () => { + buffer.fillViewportRows(); + buffer.lines.get(1)!.isWrapped = true; + assert.deepEqual(buffer.getWrappedRangeForLine(1), { first: 0, last: 1 }); + }); + it('should return a range for a row that wraps downward to last row', () => { + buffer.fillViewportRows(); + buffer.lines.get(buffer.lines.length - 1)!.isWrapped = true; + assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 2), { first: 22, last: 23 }); + }); + }); + }); + + describe('resize', () => { + describe('column size is reduced', () => { + it('should trim the data in the buffer', () => { + buffer.fillViewportRows(); + buffer.resize(INIT_COLS / 2, INIT_ROWS); + assert.equal(buffer.lines.length, INIT_ROWS); + for (let i = 0; i < INIT_ROWS; i++) { + assert.equal(buffer.lines.get(i)!.length, INIT_COLS / 2); + } + }); + }); + + describe('column size is increased', () => { + it('should add pad columns', () => { + buffer.fillViewportRows(); + buffer.resize(INIT_COLS + 10, INIT_ROWS); + assert.equal(buffer.lines.length, INIT_ROWS); + for (let i = 0; i < INIT_ROWS; i++) { + assert.equal(buffer.lines.get(i)!.length, INIT_COLS + 10); + } + }); + }); + + describe('row size reduced', () => { + it('should trim blank lines from the end', () => { + buffer.fillViewportRows(); + buffer.resize(INIT_COLS, INIT_ROWS - 10); + assert.equal(buffer.lines.length, INIT_ROWS - 10); + }); + + it('should move the viewport down when it\'s at the end', () => { + buffer.fillViewportRows(); + // Set cursor y to have 5 blank lines below it + buffer.y = INIT_ROWS - 5 - 1; + buffer.resize(INIT_COLS, INIT_ROWS - 10); + // Trim 5 rows + assert.equal(buffer.lines.length, INIT_ROWS - 5); + // Shift the viewport down 5 rows + assert.equal(buffer.ydisp, 5); + assert.equal(buffer.ybase, 5); + }); + + describe('no scrollback', () => { + it('should trim from the top of the buffer when the cursor reaches the bottom', () => { + buffer = new Buffer(true, new MockOptionsService({ scrollback: 0 }), bufferService); + assert.equal(buffer.lines.maxLength, INIT_ROWS); + buffer.y = INIT_ROWS - 1; + buffer.fillViewportRows(); + let chData = buffer.lines.get(5)!.loadCell(0, new CellData()).getAsCharData(); + chData[1] = 'a'; + buffer.lines.get(5)!.setCell(0, CellData.fromCharData(chData)); + chData = buffer.lines.get(INIT_ROWS - 1)!.loadCell(0, new CellData()).getAsCharData(); + chData[1] = 'b'; + buffer.lines.get(INIT_ROWS - 1)!.setCell(0, CellData.fromCharData(chData)); + buffer.resize(INIT_COLS, INIT_ROWS - 5); + assert.equal(buffer.lines.get(0)!.loadCell(0, new CellData()).getAsCharData()[1], 'a'); + assert.equal(buffer.lines.get(INIT_ROWS - 1 - 5)!.loadCell(0, new CellData()).getAsCharData()[1], 'b'); + }); + }); + }); + + describe('row size increased', () => { + describe('empty buffer', () => { + it('should add blank lines to end', () => { + buffer.fillViewportRows(); + assert.equal(buffer.ydisp, 0); + buffer.resize(INIT_COLS, INIT_ROWS + 10); + assert.equal(buffer.ydisp, 0); + assert.equal(buffer.lines.length, INIT_ROWS + 10); + }); + }); + + describe('filled buffer', () => { + it('should show more of the buffer above', () => { + buffer.fillViewportRows(); + // Create 10 extra blank lines + for (let i = 0; i < 10; i++) { + buffer.lines.push(buffer.getBlankLine(DEFAULT_ATTR_DATA)); + } + // Set cursor to the bottom of the buffer + buffer.y = INIT_ROWS - 1; + // Scroll down 10 lines + buffer.ybase = 10; + buffer.ydisp = 10; + assert.equal(buffer.lines.length, INIT_ROWS + 10); + buffer.resize(INIT_COLS, INIT_ROWS + 5); + // Should be should 5 more lines + assert.equal(buffer.ydisp, 5); + assert.equal(buffer.ybase, 5); + // Should not trim the buffer + assert.equal(buffer.lines.length, INIT_ROWS + 10); + }); + + it('should show more of the buffer below when the viewport is at the top of the buffer', () => { + buffer.fillViewportRows(); + // Create 10 extra blank lines + for (let i = 0; i < 10; i++) { + buffer.lines.push(buffer.getBlankLine(DEFAULT_ATTR_DATA)); + } + // Set cursor to the bottom of the buffer + buffer.y = INIT_ROWS - 1; + // Scroll down 10 lines + buffer.ybase = 10; + buffer.ydisp = 0; + assert.equal(buffer.lines.length, INIT_ROWS + 10); + buffer.resize(INIT_COLS, INIT_ROWS + 5); + // The viewport should remain at the top + assert.equal(buffer.ydisp, 0); + // The buffer ybase should move up 5 lines + assert.equal(buffer.ybase, 5); + // Should not trim the buffer + assert.equal(buffer.lines.length, INIT_ROWS + 10); + }); + }); + }); + + describe('row and column increased', () => { + it('should resize properly', () => { + buffer.fillViewportRows(); + buffer.resize(INIT_COLS + 5, INIT_ROWS + 5); + assert.equal(buffer.lines.length, INIT_ROWS + 5); + for (let i = 0; i < INIT_ROWS + 5; i++) { + assert.equal(buffer.lines.get(i)!.length, INIT_COLS + 5); + } + }); + }); + + describe('reflow', () => { + it('should not wrap empty lines', () => { + buffer.fillViewportRows(); + assert.equal(buffer.lines.length, INIT_ROWS); + buffer.resize(INIT_COLS - 5, INIT_ROWS); + assert.equal(buffer.lines.length, INIT_ROWS); + }); + it('should shrink row length', () => { + buffer.fillViewportRows(); + buffer.resize(5, 10); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0)!.length, 5); + assert.equal(buffer.lines.get(1)!.length, 5); + assert.equal(buffer.lines.get(2)!.length, 5); + assert.equal(buffer.lines.get(3)!.length, 5); + assert.equal(buffer.lines.get(4)!.length, 5); + assert.equal(buffer.lines.get(5)!.length, 5); + assert.equal(buffer.lines.get(6)!.length, 5); + assert.equal(buffer.lines.get(7)!.length, 5); + assert.equal(buffer.lines.get(8)!.length, 5); + assert.equal(buffer.lines.get(9)!.length, 5); + }); + it('should wrap and unwrap lines', () => { + buffer.fillViewportRows(); + buffer.resize(5, 10); + const firstLine = buffer.lines.get(0)!; + for (let i = 0; i < 5; i++) { + const code = 'a'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + firstLine.set(i, [0, char, 1, code]); + } + buffer.y = 1; + assert.equal(buffer.lines.get(0)!.length, 5); + assert.equal(buffer.lines.get(0)!.translateToString(), 'abcde'); + buffer.resize(1, 10); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0)!.translateToString(), 'a'); + assert.equal(buffer.lines.get(1)!.translateToString(), 'b'); + assert.equal(buffer.lines.get(2)!.translateToString(), 'c'); + assert.equal(buffer.lines.get(3)!.translateToString(), 'd'); + assert.equal(buffer.lines.get(4)!.translateToString(), 'e'); + assert.equal(buffer.lines.get(5)!.translateToString(), ' '); + assert.equal(buffer.lines.get(6)!.translateToString(), ' '); + assert.equal(buffer.lines.get(7)!.translateToString(), ' '); + assert.equal(buffer.lines.get(8)!.translateToString(), ' '); + assert.equal(buffer.lines.get(9)!.translateToString(), ' '); + buffer.resize(5, 10); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0)!.translateToString(), 'abcde'); + assert.equal(buffer.lines.get(1)!.translateToString(), ' '); + assert.equal(buffer.lines.get(2)!.translateToString(), ' '); + assert.equal(buffer.lines.get(3)!.translateToString(), ' '); + assert.equal(buffer.lines.get(4)!.translateToString(), ' '); + assert.equal(buffer.lines.get(5)!.translateToString(), ' '); + assert.equal(buffer.lines.get(6)!.translateToString(), ' '); + assert.equal(buffer.lines.get(7)!.translateToString(), ' '); + assert.equal(buffer.lines.get(8)!.translateToString(), ' '); + assert.equal(buffer.lines.get(9)!.translateToString(), ' '); + }); + it('should discard parts of wrapped lines that go out of the scrollback', () => { + buffer.fillViewportRows(); + optionsService.options.scrollback = 1; + buffer.resize(10, 5); + const lastLine = buffer.lines.get(3)!; + for (let i = 0; i < 10; i++) { + const code = 'a'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + lastLine.set(i, [0, char, 1, code]); + } + assert.equal(buffer.lines.length, 5); + buffer.y = 4; + buffer.resize(2, 5); + assert.equal(buffer.y, 4); + assert.equal(buffer.ybase, 1); + assert.equal(buffer.lines.length, 6); + assert.equal(buffer.lines.get(0)!.translateToString(), 'ab'); + assert.equal(buffer.lines.get(1)!.translateToString(), 'cd'); + assert.equal(buffer.lines.get(2)!.translateToString(), 'ef'); + assert.equal(buffer.lines.get(3)!.translateToString(), 'gh'); + assert.equal(buffer.lines.get(4)!.translateToString(), 'ij'); + assert.equal(buffer.lines.get(5)!.translateToString(), ' '); + buffer.resize(1, 5); + assert.equal(buffer.y, 4); + assert.equal(buffer.ybase, 1); + assert.equal(buffer.lines.length, 6); + assert.equal(buffer.lines.get(0)!.translateToString(), 'f'); + assert.equal(buffer.lines.get(1)!.translateToString(), 'g'); + assert.equal(buffer.lines.get(2)!.translateToString(), 'h'); + assert.equal(buffer.lines.get(3)!.translateToString(), 'i'); + assert.equal(buffer.lines.get(4)!.translateToString(), 'j'); + assert.equal(buffer.lines.get(5)!.translateToString(), ' '); + buffer.resize(10, 5); + assert.equal(buffer.y, 1); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 5); + assert.equal(buffer.lines.get(0)!.translateToString(), 'fghij '); + assert.equal(buffer.lines.get(1)!.translateToString(), ' '); + assert.equal(buffer.lines.get(2)!.translateToString(), ' '); + assert.equal(buffer.lines.get(3)!.translateToString(), ' '); + assert.equal(buffer.lines.get(4)!.translateToString(), ' '); + }); + it('should remove the correct amount of rows when reflowing larger', () => { + // This is a regression test to ensure that successive wrapped lines that are getting + // 3+ lines removed on a reflow actually remove the right lines + buffer.fillViewportRows(); + buffer.resize(10, 10); + buffer.y = 2; + const firstLine = buffer.lines.get(0)!; + const secondLine = buffer.lines.get(1)!; + for (let i = 0; i < 10; i++) { + const code = 'a'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + firstLine.set(i, [0, char, 1, code]); + } + for (let i = 0; i < 10; i++) { + const code = '0'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + secondLine.set(i, [0, char, 1, code]); + } + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0)!.translateToString(), 'abcdefghij'); + assert.equal(buffer.lines.get(1)!.translateToString(), '0123456789'); + for (let i = 2; i < 10; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + buffer.resize(2, 10); + assert.equal(buffer.ybase, 1); + assert.equal(buffer.lines.length, 11); + assert.equal(buffer.lines.get(0)!.translateToString(), 'ab'); + assert.equal(buffer.lines.get(1)!.translateToString(), 'cd'); + assert.equal(buffer.lines.get(2)!.translateToString(), 'ef'); + assert.equal(buffer.lines.get(3)!.translateToString(), 'gh'); + assert.equal(buffer.lines.get(4)!.translateToString(), 'ij'); + assert.equal(buffer.lines.get(5)!.translateToString(), '01'); + assert.equal(buffer.lines.get(6)!.translateToString(), '23'); + assert.equal(buffer.lines.get(7)!.translateToString(), '45'); + assert.equal(buffer.lines.get(8)!.translateToString(), '67'); + assert.equal(buffer.lines.get(9)!.translateToString(), '89'); + assert.equal(buffer.lines.get(10)!.translateToString(), ' '); + buffer.resize(10, 10); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0)!.translateToString(), 'abcdefghij'); + assert.equal(buffer.lines.get(1)!.translateToString(), '0123456789'); + for (let i = 2; i < 10; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + }); + it('should transfer combined char data over to reflowed lines', () => { + buffer.fillViewportRows(); + buffer.resize(4, 3); + buffer.y = 2; + const firstLine = buffer.lines.get(0)!; + firstLine.set(0, [ 0, 'a', 1, 'a'.charCodeAt(0) ]); + firstLine.set(1, [ 0, 'b', 1, 'b'.charCodeAt(0) ]); + firstLine.set(2, [ 0, 'c', 1, 'c'.charCodeAt(0) ]); + firstLine.set(3, [ 0, '😁', 1, '😁'.charCodeAt(0) ]); + assert.equal(buffer.lines.length, 3); + assert.equal(buffer.lines.get(0)!.translateToString(), 'abc😁'); + assert.equal(buffer.lines.get(1)!.translateToString(), ' '); + buffer.resize(2, 3); + assert.equal(buffer.lines.get(0)!.translateToString(), 'ab'); + assert.equal(buffer.lines.get(1)!.translateToString(), 'c😁'); + }); + it('should adjust markers when reflowing', () => { + buffer.fillViewportRows(); + buffer.resize(10, 16); + for (let i = 0; i < 10; i++) { + const code = 'a'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + buffer.lines.get(0)!.set(i, [0, char, 1, code]); + } + for (let i = 0; i < 10; i++) { + const code = '0'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + buffer.lines.get(1)!.set(i, [0, char, 1, code]); + } + for (let i = 0; i < 10; i++) { + const code = 'k'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + buffer.lines.get(2)!.set(i, [0, char, 1, code]); + } + buffer.y = 3; + // Buffer: + // abcdefghij + // 0123456789 + // abcdefghij + const firstMarker = buffer.addMarker(0); + const secondMarker = buffer.addMarker(1); + const thirdMarker = buffer.addMarker(2); + assert.equal(buffer.lines.get(0)!.translateToString(), 'abcdefghij'); + assert.equal(buffer.lines.get(1)!.translateToString(), '0123456789'); + assert.equal(buffer.lines.get(2)!.translateToString(), 'klmnopqrst'); + assert.equal(firstMarker.line, 0); + assert.equal(secondMarker.line, 1); + assert.equal(thirdMarker.line, 2); + buffer.resize(2, 16); + assert.equal(buffer.lines.get(0)!.translateToString(), 'ab'); + assert.equal(buffer.lines.get(1)!.translateToString(), 'cd'); + assert.equal(buffer.lines.get(2)!.translateToString(), 'ef'); + assert.equal(buffer.lines.get(3)!.translateToString(), 'gh'); + assert.equal(buffer.lines.get(4)!.translateToString(), 'ij'); + assert.equal(buffer.lines.get(5)!.translateToString(), '01'); + assert.equal(buffer.lines.get(6)!.translateToString(), '23'); + assert.equal(buffer.lines.get(7)!.translateToString(), '45'); + assert.equal(buffer.lines.get(8)!.translateToString(), '67'); + assert.equal(buffer.lines.get(9)!.translateToString(), '89'); + assert.equal(buffer.lines.get(10)!.translateToString(), 'kl'); + assert.equal(buffer.lines.get(11)!.translateToString(), 'mn'); + assert.equal(buffer.lines.get(12)!.translateToString(), 'op'); + assert.equal(buffer.lines.get(13)!.translateToString(), 'qr'); + assert.equal(buffer.lines.get(14)!.translateToString(), 'st'); + assert.equal(firstMarker.line, 0, 'first marker should remain unchanged'); + assert.equal(secondMarker.line, 5, 'second marker should be shifted since the first line wrapped'); + assert.equal(thirdMarker.line, 10, 'third marker should be shifted since the first and second lines wrapped'); + buffer.resize(10, 16); + assert.equal(buffer.lines.get(0)!.translateToString(), 'abcdefghij'); + assert.equal(buffer.lines.get(1)!.translateToString(), '0123456789'); + assert.equal(buffer.lines.get(2)!.translateToString(), 'klmnopqrst'); + assert.equal(firstMarker.line, 0, 'first marker should remain unchanged'); + assert.equal(secondMarker.line, 1, 'second marker should be restored to it\'s original line'); + assert.equal(thirdMarker.line, 2, 'third marker should be restored to it\'s original line'); + assert.equal(firstMarker.isDisposed, false); + assert.equal(secondMarker.isDisposed, false); + assert.equal(thirdMarker.isDisposed, false); + }); + it('should dispose markers whose rows are trimmed during a reflow', () => { + buffer.fillViewportRows(); + optionsService.options.scrollback = 1; + buffer.resize(10, 11); + for (let i = 0; i < 10; i++) { + const code = 'a'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + buffer.lines.get(0)!.set(i, [0, char, 1, code]); + } + for (let i = 0; i < 10; i++) { + const code = '0'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + buffer.lines.get(1)!.set(i, [0, char, 1, code]); + } + for (let i = 0; i < 10; i++) { + const code = 'k'.charCodeAt(0) + i; + const char = String.fromCharCode(code); + buffer.lines.get(2)!.set(i, [0, char, 1, code]); + } + buffer.y = 10; + // Buffer: + // abcdefghij + // 0123456789 + // abcdefghij + const firstMarker = buffer.addMarker(0); + const secondMarker = buffer.addMarker(1); + const thirdMarker = buffer.addMarker(2); + buffer.y = 3; + assert.equal(buffer.lines.get(0)!.translateToString(), 'abcdefghij'); + assert.equal(buffer.lines.get(1)!.translateToString(), '0123456789'); + assert.equal(buffer.lines.get(2)!.translateToString(), 'klmnopqrst'); + assert.equal(firstMarker.line, 0); + assert.equal(secondMarker.line, 1); + assert.equal(thirdMarker.line, 2); + buffer.resize(2, 11); + assert.equal(buffer.lines.get(0)!.translateToString(), 'ij'); + assert.equal(buffer.lines.get(1)!.translateToString(), '01'); + assert.equal(buffer.lines.get(2)!.translateToString(), '23'); + assert.equal(buffer.lines.get(3)!.translateToString(), '45'); + assert.equal(buffer.lines.get(4)!.translateToString(), '67'); + assert.equal(buffer.lines.get(5)!.translateToString(), '89'); + assert.equal(buffer.lines.get(6)!.translateToString(), 'kl'); + assert.equal(buffer.lines.get(7)!.translateToString(), 'mn'); + assert.equal(buffer.lines.get(8)!.translateToString(), 'op'); + assert.equal(buffer.lines.get(9)!.translateToString(), 'qr'); + assert.equal(buffer.lines.get(10)!.translateToString(), 'st'); + assert.equal(secondMarker.line, 1, 'second marker should remain the same as it was shifted 4 and trimmed 4'); + assert.equal(thirdMarker.line, 6, 'third marker should be shifted since the first and second lines wrapped'); + assert.equal(firstMarker.isDisposed, true, 'first marker was trimmed'); + assert.equal(secondMarker.isDisposed, false); + assert.equal(thirdMarker.isDisposed, false); + buffer.resize(10, 11); + assert.equal(buffer.lines.get(0)!.translateToString(), 'ij '); + assert.equal(buffer.lines.get(1)!.translateToString(), '0123456789'); + assert.equal(buffer.lines.get(2)!.translateToString(), 'klmnopqrst'); + assert.equal(secondMarker.line, 1, 'second marker should be restored'); + assert.equal(thirdMarker.line, 2, 'third marker should be restored'); + }); + it('should correctly reflow wrapped lines that end in 0 space (via tab char)', () => { + buffer.fillViewportRows(); + buffer.resize(4, 10); + buffer.y = 2; + buffer.lines.get(0)!.set(0, [0, 'a', 1, 'a'.charCodeAt(0)]); + buffer.lines.get(0)!.set(1, [0, 'b', 1, 'b'.charCodeAt(0)]); + buffer.lines.get(1)!.set(0, [0, 'c', 1, 'c'.charCodeAt(0)]); + buffer.lines.get(1)!.set(1, [0, 'd', 1, 'd'.charCodeAt(0)]); + buffer.lines.get(1)!.isWrapped = true; + // Buffer: + // "ab " (wrapped) + // "cd" + buffer.resize(5, 10); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0)!.translateToString(true), 'ab c'); + assert.equal(buffer.lines.get(1)!.translateToString(false), 'd '); + buffer.resize(6, 10); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0)!.translateToString(true), 'ab cd'); + assert.equal(buffer.lines.get(1)!.translateToString(false), ' '); + }); + it('should wrap wide characters correctly when reflowing larger', () => { + buffer.fillViewportRows(); + buffer.resize(12, 10); + buffer.y = 2; + for (let i = 0; i < 12; i += 4) { + buffer.lines.get(0)!.set(i, [0, '汉', 2, '汉'.charCodeAt(0)]); + buffer.lines.get(1)!.set(i, [0, '汉', 2, '汉'.charCodeAt(0)]); + } + for (let i = 2; i < 12; i += 4) { + buffer.lines.get(0)!.set(i, [0, '语', 2, '语'.charCodeAt(0)]); + buffer.lines.get(1)!.set(i, [0, '语', 2, '语'.charCodeAt(0)]); + } + for (let i = 1; i < 12; i += 2) { + buffer.lines.get(0)!.set(i, [0, '', 0, 0]); + buffer.lines.get(1)!.set(i, [0, '', 0, 0]); + } + buffer.lines.get(1)!.isWrapped = true; + // Buffer: + // 汉语汉语汉语 (wrapped) + // 汉语汉语汉语 + assert.equal(buffer.lines.get(0)!.translateToString(true), '汉语汉语汉语'); + assert.equal(buffer.lines.get(1)!.translateToString(true), '汉语汉语汉语'); + buffer.resize(13, 10); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0)!.translateToString(true), '汉语汉语汉语'); + assert.equal(buffer.lines.get(0)!.translateToString(false), '汉语汉语汉语 '); + assert.equal(buffer.lines.get(1)!.translateToString(true), '汉语汉语汉语'); + assert.equal(buffer.lines.get(1)!.translateToString(false), '汉语汉语汉语 '); + buffer.resize(14, 10); + assert.equal(buffer.lines.get(0)!.translateToString(true), '汉语汉语汉语汉'); + assert.equal(buffer.lines.get(0)!.translateToString(false), '汉语汉语汉语汉'); + assert.equal(buffer.lines.get(1)!.translateToString(true), '语汉语汉语'); + assert.equal(buffer.lines.get(1)!.translateToString(false), '语汉语汉语 '); + }); + it('should correctly reflow wrapped lines that end in 0 space (via tab char)', () => { + buffer.fillViewportRows(); + buffer.resize(4, 10); + buffer.y = 2; + buffer.lines.get(0)!.set(0, [0, 'a', 1, 'a'.charCodeAt(0)]); + buffer.lines.get(0)!.set(1, [0, 'b', 1, 'b'.charCodeAt(0)]); + buffer.lines.get(1)!.set(0, [0, 'c', 1, 'c'.charCodeAt(0)]); + buffer.lines.get(1)!.set(1, [0, 'd', 1, 'd'.charCodeAt(0)]); + buffer.lines.get(1)!.isWrapped = true; + // Buffer: + // "ab " (wrapped) + // "cd" + buffer.resize(3, 10); + assert.equal(buffer.y, 2); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0)!.translateToString(false), 'ab '); + assert.equal(buffer.lines.get(1)!.translateToString(false), ' cd'); + buffer.resize(2, 10); + assert.equal(buffer.y, 3); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0)!.translateToString(false), 'ab'); + assert.equal(buffer.lines.get(1)!.translateToString(false), ' '); + assert.equal(buffer.lines.get(2)!.translateToString(false), 'cd'); + }); + it('should wrap wide characters correctly when reflowing smaller', () => { + buffer.fillViewportRows(); + buffer.resize(12, 10); + buffer.y = 2; + for (let i = 0; i < 12; i += 4) { + buffer.lines.get(0)!.set(i, [0, '汉', 2, '汉'.charCodeAt(0)]); + buffer.lines.get(1)!.set(i, [0, '汉', 2, '汉'.charCodeAt(0)]); + } + for (let i = 2; i < 12; i += 4) { + buffer.lines.get(0)!.set(i, [0, '语', 2, '语'.charCodeAt(0)]); + buffer.lines.get(1)!.set(i, [0, '语', 2, '语'.charCodeAt(0)]); + } + for (let i = 1; i < 12; i += 2) { + buffer.lines.get(0)!.set(i, [0, '', 0, 0]); + buffer.lines.get(1)!.set(i, [0, '', 0, 0]); + } + buffer.lines.get(1)!.isWrapped = true; + // Buffer: + // 汉语汉语汉语 (wrapped) + // 汉语汉语汉语 + assert.equal(buffer.lines.get(0)!.translateToString(true), '汉语汉语汉语'); + assert.equal(buffer.lines.get(1)!.translateToString(true), '汉语汉语汉语'); + buffer.resize(11, 10); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0)!.translateToString(true), '汉语汉语汉'); + assert.equal(buffer.lines.get(1)!.translateToString(true), '语汉语汉语'); + assert.equal(buffer.lines.get(2)!.translateToString(true), '汉语'); + buffer.resize(10, 10); + assert.equal(buffer.lines.get(0)!.translateToString(true), '汉语汉语汉'); + assert.equal(buffer.lines.get(1)!.translateToString(true), '语汉语汉语'); + assert.equal(buffer.lines.get(2)!.translateToString(true), '汉语'); + buffer.resize(9, 10); + assert.equal(buffer.lines.get(0)!.translateToString(true), '汉语汉语'); + assert.equal(buffer.lines.get(1)!.translateToString(true), '汉语汉语'); + assert.equal(buffer.lines.get(2)!.translateToString(true), '汉语汉语'); + buffer.resize(8, 10); + assert.equal(buffer.lines.get(0)!.translateToString(true), '汉语汉语'); + assert.equal(buffer.lines.get(1)!.translateToString(true), '汉语汉语'); + assert.equal(buffer.lines.get(2)!.translateToString(true), '汉语汉语'); + buffer.resize(7, 10); + assert.equal(buffer.lines.get(0)!.translateToString(true), '汉语汉'); + assert.equal(buffer.lines.get(1)!.translateToString(true), '语汉语'); + assert.equal(buffer.lines.get(2)!.translateToString(true), '汉语汉'); + assert.equal(buffer.lines.get(3)!.translateToString(true), '语汉语'); + buffer.resize(6, 10); + assert.equal(buffer.lines.get(0)!.translateToString(true), '汉语汉'); + assert.equal(buffer.lines.get(1)!.translateToString(true), '语汉语'); + assert.equal(buffer.lines.get(2)!.translateToString(true), '汉语汉'); + assert.equal(buffer.lines.get(3)!.translateToString(true), '语汉语'); + }); + + describe('reflowLarger cases', () => { + beforeEach(() => { + // Setup buffer state: + // 'ab' + // 'cd' (wrapped) + // 'ef' + // 'gh' (wrapped) + // 'ij' + // 'kl' (wrapped) + // ' ' + // ' ' + // ' ' + // ' ' + buffer.fillViewportRows(); + buffer.resize(2, 10); + buffer.lines.get(0)!.set(0, [0, 'a', 1, 'a'.charCodeAt(0)]); + buffer.lines.get(0)!.set(1, [0, 'b', 1, 'b'.charCodeAt(0)]); + buffer.lines.get(1)!.set(0, [0, 'c', 1, 'c'.charCodeAt(0)]); + buffer.lines.get(1)!.set(1, [0, 'd', 1, 'd'.charCodeAt(0)]); + buffer.lines.get(1)!.isWrapped = true; + buffer.lines.get(2)!.set(0, [0, 'e', 1, 'e'.charCodeAt(0)]); + buffer.lines.get(2)!.set(1, [0, 'f', 1, 'f'.charCodeAt(0)]); + buffer.lines.get(3)!.set(0, [0, 'g', 1, 'g'.charCodeAt(0)]); + buffer.lines.get(3)!.set(1, [0, 'h', 1, 'h'.charCodeAt(0)]); + buffer.lines.get(3)!.isWrapped = true; + buffer.lines.get(4)!.set(0, [0, 'i', 1, 'i'.charCodeAt(0)]); + buffer.lines.get(4)!.set(1, [0, 'j', 1, 'j'.charCodeAt(0)]); + buffer.lines.get(5)!.set(0, [0, 'k', 1, 'k'.charCodeAt(0)]); + buffer.lines.get(5)!.set(1, [0, 'l', 1, 'l'.charCodeAt(0)]); + buffer.lines.get(5)!.isWrapped = true; + }); + describe('viewport not yet filled', () => { + it('should move the cursor up and add empty lines', () => { + buffer.y = 6; + buffer.resize(4, 10); + assert.equal(buffer.y, 3); + assert.equal(buffer.ydisp, 0); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0)!.translateToString(), 'abcd'); + assert.equal(buffer.lines.get(1)!.translateToString(), 'efgh'); + assert.equal(buffer.lines.get(2)!.translateToString(), 'ijkl'); + for (let i = 3; i < 10; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + const wrappedLines: number[] = []; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i)!.isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('viewport filled, scrollback remaining', () => { + beforeEach(() => { + buffer.y = 9; + }); + describe('ybase === 0', () => { + it('should move the cursor up and add empty lines', () => { + buffer.resize(4, 10); + assert.equal(buffer.y, 6); + assert.equal(buffer.ydisp, 0); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0)!.translateToString(), 'abcd'); + assert.equal(buffer.lines.get(1)!.translateToString(), 'efgh'); + assert.equal(buffer.lines.get(2)!.translateToString(), 'ijkl'); + for (let i = 3; i < 10; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + const wrappedLines: number[] = []; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i)!.isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('ybase !== 0', () => { + beforeEach(() => { + // Add 10 empty rows to start + for (let i = 0; i < 10; i++) { + buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); + } + buffer.ybase = 10; + }); + describe('&& ydisp === ybase', () => { + it('should adjust the viewport and keep ydisp = ybase', () => { + buffer.ydisp = 10; + buffer.resize(4, 10); + assert.equal(buffer.y, 9); + assert.equal(buffer.ydisp, 7); + assert.equal(buffer.ybase, 7); + assert.equal(buffer.lines.length, 17); + for (let i = 0; i < 10; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + assert.equal(buffer.lines.get(10)!.translateToString(), 'abcd'); + assert.equal(buffer.lines.get(11)!.translateToString(), 'efgh'); + assert.equal(buffer.lines.get(12)!.translateToString(), 'ijkl'); + for (let i = 13; i < 17; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + const wrappedLines: number[] = []; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i)!.isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('&& ydisp !== ybase', () => { + it('should keep ydisp at the same value', () => { + buffer.ydisp = 5; + buffer.resize(4, 10); + assert.equal(buffer.y, 9); + assert.equal(buffer.ydisp, 5); + assert.equal(buffer.ybase, 7); + assert.equal(buffer.lines.length, 17); + for (let i = 0; i < 10; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + assert.equal(buffer.lines.get(10)!.translateToString(), 'abcd'); + assert.equal(buffer.lines.get(11)!.translateToString(), 'efgh'); + assert.equal(buffer.lines.get(12)!.translateToString(), 'ijkl'); + for (let i = 13; i < 17; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + const wrappedLines: number[] = []; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i)!.isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + }); + }); + describe('viewport filled, no scrollback remaining', () => { + // ybase === 0 doesn't make sense here as scrollback=0 isn't really supported + describe('ybase !== 0', () => { + beforeEach(() => { + optionsService.options.scrollback = 10; + // Add 10 empty rows to start + for (let i = 0; i < 10; i++) { + buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); + } + buffer.y = 9; + buffer.ybase = 10; + }); + describe('&& ydisp === ybase', () => { + it('should trim lines and keep ydisp = ybase', () => { + buffer.ydisp = 10; + buffer.resize(4, 10); + assert.equal(buffer.y, 9); + assert.equal(buffer.ydisp, 7); + assert.equal(buffer.ybase, 7); + assert.equal(buffer.lines.length, 17); + for (let i = 0; i < 10; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + assert.equal(buffer.lines.get(10)!.translateToString(), 'abcd'); + assert.equal(buffer.lines.get(11)!.translateToString(), 'efgh'); + assert.equal(buffer.lines.get(12)!.translateToString(), 'ijkl'); + for (let i = 13; i < 17; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + const wrappedLines: number[] = []; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i)!.isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('&& ydisp !== ybase', () => { + it('should trim lines and not change ydisp', () => { + buffer.ydisp = 5; + buffer.resize(4, 10); + assert.equal(buffer.y, 9); + assert.equal(buffer.ydisp, 5); + assert.equal(buffer.ybase, 7); + assert.equal(buffer.lines.length, 17); + for (let i = 0; i < 10; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + assert.equal(buffer.lines.get(10)!.translateToString(), 'abcd'); + assert.equal(buffer.lines.get(11)!.translateToString(), 'efgh'); + assert.equal(buffer.lines.get(12)!.translateToString(), 'ijkl'); + for (let i = 13; i < 17; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + const wrappedLines: number[] = []; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i)!.isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + }); + }); + }); + describe('reflowSmaller cases', () => { + beforeEach(() => { + // Setup buffer state: + // 'abcd' + // 'efgh' (wrapped) + // 'ijkl' + // ' ' + // ' ' + // ' ' + // ' ' + // ' ' + // ' ' + // ' ' + buffer.fillViewportRows(); + buffer.resize(4, 10); + buffer.lines.get(0)!.set(0, [0, 'a', 1, 'a'.charCodeAt(0)]); + buffer.lines.get(0)!.set(1, [0, 'b', 1, 'b'.charCodeAt(0)]); + buffer.lines.get(0)!.set(2, [0, 'c', 1, 'c'.charCodeAt(0)]); + buffer.lines.get(0)!.set(3, [0, 'd', 1, 'd'.charCodeAt(0)]); + buffer.lines.get(1)!.set(0, [0, 'e', 1, 'e'.charCodeAt(0)]); + buffer.lines.get(1)!.set(1, [0, 'f', 1, 'f'.charCodeAt(0)]); + buffer.lines.get(1)!.set(2, [0, 'g', 1, 'g'.charCodeAt(0)]); + buffer.lines.get(1)!.set(3, [0, 'h', 1, 'h'.charCodeAt(0)]); + buffer.lines.get(2)!.set(0, [0, 'i', 1, 'i'.charCodeAt(0)]); + buffer.lines.get(2)!.set(1, [0, 'j', 1, 'j'.charCodeAt(0)]); + buffer.lines.get(2)!.set(2, [0, 'k', 1, 'k'.charCodeAt(0)]); + buffer.lines.get(2)!.set(3, [0, 'l', 1, 'l'.charCodeAt(0)]); + }); + describe('viewport not yet filled', () => { + it('should move the cursor down', () => { + buffer.y = 3; + buffer.resize(2, 10); + assert.equal(buffer.y, 6); + assert.equal(buffer.ydisp, 0); + assert.equal(buffer.ybase, 0); + assert.equal(buffer.lines.length, 10); + assert.equal(buffer.lines.get(0)!.translateToString(), 'ab'); + assert.equal(buffer.lines.get(1)!.translateToString(), 'cd'); + assert.equal(buffer.lines.get(2)!.translateToString(), 'ef'); + assert.equal(buffer.lines.get(3)!.translateToString(), 'gh'); + assert.equal(buffer.lines.get(4)!.translateToString(), 'ij'); + assert.equal(buffer.lines.get(5)!.translateToString(), 'kl'); + for (let i = 6; i < 10; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + const wrappedLines = [1, 3, 5]; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i)!.isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('viewport filled, scrollback remaining', () => { + beforeEach(() => { + buffer.y = 9; + }); + describe('ybase === 0', () => { + it('should trim the top', () => { + buffer.resize(2, 10); + assert.equal(buffer.y, 9); + assert.equal(buffer.ydisp, 3); + assert.equal(buffer.ybase, 3); + assert.equal(buffer.lines.length, 13); + assert.equal(buffer.lines.get(0)!.translateToString(), 'ab'); + assert.equal(buffer.lines.get(1)!.translateToString(), 'cd'); + assert.equal(buffer.lines.get(2)!.translateToString(), 'ef'); + assert.equal(buffer.lines.get(3)!.translateToString(), 'gh'); + assert.equal(buffer.lines.get(4)!.translateToString(), 'ij'); + assert.equal(buffer.lines.get(5)!.translateToString(), 'kl'); + for (let i = 6; i < 13; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + const wrappedLines = [1, 3, 5]; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i)!.isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('ybase !== 0', () => { + beforeEach(() => { + // Add 10 empty rows to start + for (let i = 0; i < 10; i++) { + buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); + } + buffer.ybase = 10; + }); + describe('&& ydisp === ybase', () => { + it('should adjust the viewport and keep ydisp = ybase', () => { + buffer.ydisp = 10; + buffer.resize(2, 10); + assert.equal(buffer.ydisp, 13); + assert.equal(buffer.ybase, 13); + assert.equal(buffer.lines.length, 23); + for (let i = 0; i < 10; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + assert.equal(buffer.lines.get(10)!.translateToString(), 'ab'); + assert.equal(buffer.lines.get(11)!.translateToString(), 'cd'); + assert.equal(buffer.lines.get(12)!.translateToString(), 'ef'); + assert.equal(buffer.lines.get(13)!.translateToString(), 'gh'); + assert.equal(buffer.lines.get(14)!.translateToString(), 'ij'); + assert.equal(buffer.lines.get(15)!.translateToString(), 'kl'); + for (let i = 16; i < 23; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + const wrappedLines = [11, 13, 15]; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i)!.isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('&& ydisp !== ybase', () => { + it('should keep ydisp at the same value', () => { + buffer.ydisp = 5; + buffer.resize(2, 10); + assert.equal(buffer.ydisp, 5); + assert.equal(buffer.ybase, 13); + assert.equal(buffer.lines.length, 23); + for (let i = 0; i < 10; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + assert.equal(buffer.lines.get(10)!.translateToString(), 'ab'); + assert.equal(buffer.lines.get(11)!.translateToString(), 'cd'); + assert.equal(buffer.lines.get(12)!.translateToString(), 'ef'); + assert.equal(buffer.lines.get(13)!.translateToString(), 'gh'); + assert.equal(buffer.lines.get(14)!.translateToString(), 'ij'); + assert.equal(buffer.lines.get(15)!.translateToString(), 'kl'); + for (let i = 16; i < 23; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + const wrappedLines = [11, 13, 15]; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i)!.isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + }); + }); + describe('viewport filled, no scrollback remaining', () => { + // ybase === 0 doesn't make sense here as scrollback=0 isn't really supported + describe('ybase !== 0', () => { + beforeEach(() => { + optionsService.options.scrollback = 10; + // Add 10 empty rows to start + for (let i = 0; i < 10; i++) { + buffer.lines.splice(0, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); + } + buffer.ybase = 10; + }); + describe('&& ydisp === ybase', () => { + it('should trim lines and keep ydisp = ybase', () => { + buffer.ydisp = 10; + buffer.y = 13; + buffer.resize(2, 10); + assert.equal(buffer.ydisp, 10); + assert.equal(buffer.ybase, 10); + assert.equal(buffer.lines.length, 20); + for (let i = 0; i < 7; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + assert.equal(buffer.lines.get(7)!.translateToString(), 'ab'); + assert.equal(buffer.lines.get(8)!.translateToString(), 'cd'); + assert.equal(buffer.lines.get(9)!.translateToString(), 'ef'); + assert.equal(buffer.lines.get(10)!.translateToString(), 'gh'); + assert.equal(buffer.lines.get(11)!.translateToString(), 'ij'); + assert.equal(buffer.lines.get(12)!.translateToString(), 'kl'); + for (let i = 13; i < 20; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + const wrappedLines = [8, 10, 12]; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i)!.isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + describe('&& ydisp !== ybase', () => { + it('should trim lines and not change ydisp', () => { + buffer.ydisp = 5; + buffer.y = 13; + buffer.resize(2, 10); + assert.equal(buffer.ydisp, 5); + assert.equal(buffer.ybase, 10); + assert.equal(buffer.lines.length, 20); + for (let i = 0; i < 7; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + assert.equal(buffer.lines.get(7)!.translateToString(), 'ab'); + assert.equal(buffer.lines.get(8)!.translateToString(), 'cd'); + assert.equal(buffer.lines.get(9)!.translateToString(), 'ef'); + assert.equal(buffer.lines.get(10)!.translateToString(), 'gh'); + assert.equal(buffer.lines.get(11)!.translateToString(), 'ij'); + assert.equal(buffer.lines.get(12)!.translateToString(), 'kl'); + for (let i = 13; i < 20; i++) { + assert.equal(buffer.lines.get(i)!.translateToString(), ' '); + } + const wrappedLines = [8, 10, 12]; + for (let i = 0; i < buffer.lines.length; i++) { + assert.equal(buffer.lines.get(i)!.isWrapped, wrappedLines.indexOf(i) !== -1, `line ${i} isWrapped must equal ${wrappedLines.indexOf(i) !== -1}`); + } + }); + }); + }); + }); + }); + }); + }); + + describe('buffer marked to have no scrollback', () => { + it('should always have a scrollback of 0', () => { + // Test size on initialization + buffer = new Buffer(false, new MockOptionsService({ scrollback: 1000 }), bufferService); + buffer.fillViewportRows(); + assert.equal(buffer.lines.maxLength, INIT_ROWS); + // Test size on buffer increase + buffer.resize(INIT_COLS, INIT_ROWS * 2); + assert.equal(buffer.lines.maxLength, INIT_ROWS * 2); + // Test size on buffer decrease + buffer.resize(INIT_COLS, INIT_ROWS / 2); + assert.equal(buffer.lines.maxLength, INIT_ROWS / 2); + }); + }); + + describe('addMarker', () => { + it('should adjust a marker line when the buffer is trimmed', () => { + buffer = new Buffer(true, new MockOptionsService({ scrollback: 0 }), bufferService); + buffer.fillViewportRows(); + const marker = buffer.addMarker(buffer.lines.length - 1); + assert.equal(marker.line, buffer.lines.length - 1); + buffer.lines.onTrimEmitter.fire(1); + assert.equal(marker.line, buffer.lines.length - 2); + }); + it('should dispose of a marker if it is trimmed off the buffer', () => { + buffer = new Buffer(true, new MockOptionsService({ scrollback: 0 }), bufferService); + buffer.fillViewportRows(); + assert.equal(buffer.markers.length, 0); + const marker = buffer.addMarker(0); + assert.equal(marker.isDisposed, false); + assert.equal(buffer.markers.length, 1); + buffer.lines.onTrimEmitter.fire(1); + assert.equal(marker.isDisposed, true); + assert.equal(buffer.markers.length, 0); + }); + }); + + describe ('translateBufferLineToString', () => { + it('should handle selecting a section of ascii text', () => { + const line = new BufferLine(4); + line.setCell(0, CellData.fromCharData([ 0, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(1, CellData.fromCharData([ 0, 'b', 1, 'b'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([ 0, 'c', 1, 'c'.charCodeAt(0)])); + line.setCell(3, CellData.fromCharData([ 0, 'd', 1, 'd'.charCodeAt(0)])); + buffer.lines.set(0, line); + + const str = buffer.translateBufferLineToString(0, true, 0, 2); + assert.equal(str, 'ab'); + }); + + it('should handle a cut-off double width character by including it', () => { + const line = new BufferLine(3); + line.setCell(0, CellData.fromCharData([ 0, '語', 2, 35486 ])); + line.setCell(1, CellData.fromCharData([ 0, '', 0, 0])); + line.setCell(2, CellData.fromCharData([ 0, 'a', 1, 'a'.charCodeAt(0)])); + buffer.lines.set(0, line); + + const str1 = buffer.translateBufferLineToString(0, true, 0, 1); + assert.equal(str1, '語'); + }); + + it('should handle a zero width character in the middle of the string by not including it', () => { + const line = new BufferLine(3); + line.setCell(0, CellData.fromCharData([ 0, '語', 2, '語'.charCodeAt(0) ])); + line.setCell(1, CellData.fromCharData([ 0, '', 0, 0])); + line.setCell(2, CellData.fromCharData([ 0, 'a', 1, 'a'.charCodeAt(0)])); + buffer.lines.set(0, line); + + const str0 = buffer.translateBufferLineToString(0, true, 0, 1); + assert.equal(str0, '語'); + + const str1 = buffer.translateBufferLineToString(0, true, 0, 2); + assert.equal(str1, '語'); + + const str2 = buffer.translateBufferLineToString(0, true, 0, 3); + assert.equal(str2, '語a'); + }); + + it('should handle single width emojis', () => { + const line = new BufferLine(2); + line.setCell(0, CellData.fromCharData([ 0, '😁', 1, '😁'.charCodeAt(0) ])); + line.setCell(1, CellData.fromCharData([ 0, 'a', 1, 'a'.charCodeAt(0)])); + buffer.lines.set(0, line); + + const str1 = buffer.translateBufferLineToString(0, true, 0, 1); + assert.equal(str1, '😁'); + + const str2 = buffer.translateBufferLineToString(0, true, 0, 2); + assert.equal(str2, '😁a'); + }); + + it('should handle double width emojis', () => { + const line = new BufferLine(2); + line.setCell(0, CellData.fromCharData([ 0, '😁', 2, '😁'.charCodeAt(0) ])); + line.setCell(1, CellData.fromCharData([ 0, '', 0, 0])); + buffer.lines.set(0, line); + + const str1 = buffer.translateBufferLineToString(0, true, 0, 1); + assert.equal(str1, '😁'); + + const str2 = buffer.translateBufferLineToString(0, true, 0, 2); + assert.equal(str2, '😁'); + + const line2 = new BufferLine(3); + line2.setCell(0, CellData.fromCharData([ 0, '😁', 2, '😁'.charCodeAt(0) ])); + line2.setCell(1, CellData.fromCharData([ 0, '', 0, 0])); + line2.setCell(2, CellData.fromCharData([ 0, 'a', 1, 'a'.charCodeAt(0)])); + buffer.lines.set(0, line2); + + const str3 = buffer.translateBufferLineToString(0, true, 0, 3); + assert.equal(str3, '😁a'); + }); + }); + // describe('stringIndexToBufferIndex', () => { + // let terminal: TestTerminal; + + // beforeEach(() => { + // terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5}); + // }); + + // it('multiline ascii', () => { + // const input = 'This is ASCII text spanning multiple lines.'; + // terminal.writeSync(input); + // const s = terminal.buffer.iterator(true).next().content; + // assert.equal(input, s); + // for (let i = 0; i < input.length; ++i) { + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + // assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); + // } + // }); + + // it('combining e\u0301 in a sentence', () => { + // const input = 'Sitting in the cafe\u0301 drinking coffee.'; + // terminal.writeSync(input); + // const s = terminal.buffer.iterator(true).next().content; + // assert.equal(input, s); + // for (let i = 0; i < 19; ++i) { + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + // assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); + // } + // // string index 18 & 19 point to combining char e\u0301 ---> same buffer Index + // assert.deepEqual( + // terminal.buffer.stringIndexToBufferIndex(0, 18), + // terminal.buffer.stringIndexToBufferIndex(0, 19)); + // // after the combining char every string index has an offset of -1 + // for (let i = 19; i < input.length; ++i) { + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + // assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex); + // } + // }); + + // it('multiline combining e\u0301', () => { + // const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301'; + // terminal.writeSync(input); + // const s = terminal.buffer.iterator(true).next().content; + // assert.equal(input, s); + // // every buffer cell index contains 2 string indices + // for (let i = 0; i < input.length; ++i) { + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + // assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex); + // } + // }); + + // it('surrogate char in a sentence', () => { + // const input = 'The 𝄞 is a clef widely used in modern notation.'; + // terminal.writeSync(input); + // const s = terminal.buffer.iterator(true).next().content; + // assert.equal(input, s); + // for (let i = 0; i < 5; ++i) { + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + // assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); + // } + // // string index 4 & 5 point to surrogate char 𝄞 ---> same buffer Index + // assert.deepEqual( + // terminal.buffer.stringIndexToBufferIndex(0, 4), + // terminal.buffer.stringIndexToBufferIndex(0, 5)); + // // after the combining char every string index has an offset of -1 + // for (let i = 5; i < input.length; ++i) { + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + // assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex); + // } + // }); + + // it('multiline surrogate char', () => { + // const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞'; + // terminal.writeSync(input); + // const s = terminal.buffer.iterator(true).next().content; + // assert.equal(input, s); + // // every buffer cell index contains 2 string indices + // for (let i = 0; i < input.length; ++i) { + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + // assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex); + // } + // }); + + // it('surrogate char with combining', () => { + // // eye of Ra with acute accent - string length of 3 + // const input = '𓂀\u0301 - the eye hiroglyph with an acute accent.'; + // terminal.writeSync(input); + // const s = terminal.buffer.iterator(true).next().content; + // assert.equal(input, s); + // // index 0..2 should map to 0 + // assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 1)); + // assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 2)); + // for (let i = 2; i < input.length; ++i) { + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + // assert.deepEqual([((i - 2) / terminal.cols) | 0, (i - 2) % terminal.cols], bufferIndex); + // } + // }); + + // it('multiline surrogate with combining', () => { + // const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301'; + // terminal.writeSync(input); + // const s = terminal.buffer.iterator(true).next().content; + // assert.equal(input, s); + // // every buffer cell index contains 3 string indices + // for (let i = 0; i < input.length; ++i) { + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + // assert.deepEqual([(((i / 3) | 0) / terminal.cols) | 0, ((i / 3) | 0) % terminal.cols], bufferIndex); + // } + // }); + + // it('fullwidth chars', () => { + // const input = 'These 123 are some fat numbers.'; + // terminal.writeSync(input); + // const s = terminal.buffer.iterator(true).next().content; + // assert.equal(input, s); + // for (let i = 0; i < 6; ++i) { + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + // assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); + // } + // // string index 6, 7, 8 take 2 cells + // assert.deepEqual([0, 8], terminal.buffer.stringIndexToBufferIndex(0, 7)); + // assert.deepEqual([1, 0], terminal.buffer.stringIndexToBufferIndex(0, 8)); + // // rest of the string has offset of +3 + // for (let i = 9; i < input.length; ++i) { + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + // assert.deepEqual([((i + 3) / terminal.cols) | 0, (i + 3) % terminal.cols], bufferIndex); + // } + // }); + + // it('multiline fullwidth chars', () => { + // const input = '12345678901234567890'; + // terminal.writeSync(input); + // const s = terminal.buffer.iterator(true).next().content; + // assert.equal(input, s); + // for (let i = 9; i < input.length; ++i) { + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + // assert.deepEqual([((i << 1) / terminal.cols) | 0, (i << 1) % terminal.cols], bufferIndex); + // } + // }); + + // it('fullwidth combining with emoji - match emoji cell', () => { + // const input = 'Lots of ¥\u0301 make me 😃.'; + // terminal.writeSync(input); + // const s = terminal.buffer.iterator(true).next().content; + // assert.equal(input, s); + // const stringIndex = s.match(/😃/).index; + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, stringIndex); + // assert(terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars(), '😃'); + // }); + + // it('multiline fullwidth chars with offset 1 (currently tests for broken behavior)', () => { + // const input = 'a12345678901234567890'; + // // the 'a' at the beginning moves all fullwidth chars one to the right + // // now the end of the line contains a dangling empty cell since + // // the next fullwidth char has to wrap early + // // the dangling last cell is wrongly added in the string + // // --> fixable after resolving #1685 + // terminal.writeSync(input); + // 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, true); + // const j = (i - 0) << 1; + // assert.deepEqual([(j / terminal.cols) | 0, j % terminal.cols], bufferIndex); + // } + // }); + + // it('test fully wrapped buffer up to last char', () => { + // const input = Array(6).join('1234567890'); + // terminal.writeSync(input); + // const s = terminal.buffer.iterator(true).next().content; + // assert.equal(input, s); + // for (let i = 0; i < input.length; ++i) { + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); + // assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars()); + // } + // }); + + // it('test fully wrapped buffer up to last char with full width odd', () => { + // const input = 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301' + // + 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301'; + // terminal.writeSync(input); + // const s = terminal.buffer.iterator(true).next().content; + // assert.equal(input, s); + // for (let i = 0; i < input.length; ++i) { + // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); + // assert.equal( + // (!(i % 3)) + // ? input[i] + // : (i % 3 === 1) + // ? input.substr(i, 2) + // : input.substr(i - 1, 2), + // terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars()); + // } + // }); + + // it('should handle \t in lines correctly', () => { + // const input = '\thttps://google.de'; + // terminal.writeSync(input); + // const s = terminal.buffer.iterator(true).next().content; + // assert.equal(s, Array(optionsService.options.tabStopWidth + 1).join(' ') + 'https://google.de'); + // }); + // }); + // describe('BufferStringIterator', function(): void { + // it('iterator does not overflow buffer limits', function(): void { + // const terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5}); + // const data = [ + // 'aaaaaaaaaa', + // 'aaaaaaaaa\n', + // 'aaaaaaaaaa', + // 'aaaaaaaaa\n', + // 'aaaaaaaaaa', + // 'aaaaaaaaaa', + // 'aaaaaaaaaa', + // 'aaaaaaaaa\n', + // 'aaaaaaaaaa', + // 'aaaaaaaaaa' + // ]; + // terminal.writeSync(data.join('')); + // // brute force test with insane values + // expect(() => { + // for (let overscan = 0; overscan < 20; ++overscan) { + // for (let start = -10; start < 20; ++start) { + // for (let end = -10; end < 20; ++end) { + // const it = terminal.buffer.iterator(false, start, end, overscan, overscan); + // while (it.hasNext()) { + // it.next(); + // } + // } + // } + // } + // }).to.not.throw(); + // }); + // }); +}); diff --git a/src/Buffer.ts b/src/common/buffer/Buffer.ts similarity index 93% rename from src/Buffer.ts rename to src/common/buffer/Buffer.ts index a0d590a56..1e6edae90 100644 --- a/src/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -4,11 +4,12 @@ */ import { CircularList, IInsertEvent } from 'common/CircularList'; -import { ITerminal, IBuffer, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult } from './Types'; -import { IBufferLine, ICellData, IAttributeData } from 'core/Types'; -import { BufferLine, CellData, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_WIDTH, WHITESPACE_CELL_CODE, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX, DEFAULT_ATTR_DATA } from 'core/buffer/BufferLine'; -import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths, getWrappedLineTrimmedLength } from 'core/buffer/BufferReflow'; -import { Marker } from 'core/buffer/Marker'; +import { IBuffer, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult } from 'common/buffer/Types'; +import { IBufferLine, ICellData, IAttributeData } from 'common/Types'; +import { BufferLine, CellData, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_WIDTH, WHITESPACE_CELL_CODE, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; +import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths, getWrappedLineTrimmedLength } from 'common/buffer/BufferReflow'; +import { Marker } from 'common/buffer/Marker'; +import { IOptionsService, IBufferService } from 'common/services/Services'; export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1 @@ -21,15 +22,16 @@ export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1 */ export class Buffer implements IBuffer { public lines: CircularList; - public ydisp: number; - public ybase: number; - public y: number; - public x: number; + public ydisp: number = 0; + public ybase: number = 0; + public y: number = 0; + public x: number = 0; public scrollBottom: number; public scrollTop: number; + // TODO: Type me public tabs: any; - public savedY: number; - public savedX: number; + public savedY: number = 0; + public savedX: number = 0; public savedCurAttrData = DEFAULT_ATTR_DATA.clone(); public markers: Marker[] = []; private _nullCell: ICellData = CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); @@ -37,19 +39,17 @@ export class Buffer implements IBuffer { private _cols: number; private _rows: number; - /** - * Create a new Buffer. - * @param _terminal The terminal the Buffer will belong to. - * @param _hasScrollback Whether the buffer should respect the scrollback of - * the terminal. - */ constructor( - private _terminal: ITerminal, - private _hasScrollback: boolean + private _hasScrollback: boolean, + private _optionsService: IOptionsService, + private _bufferService: IBufferService ) { - this._cols = this._terminal.cols; - this._rows = this._terminal.rows; - this.clear(); + this._cols = this._bufferService.cols; + this._rows = this._bufferService.rows; + this.lines = new CircularList(this._getCorrectBufferLength(this._rows)); + this.scrollTop = 0; + this.scrollBottom = this._rows - 1; + this.setupTabStops(); } public getNullCell(attr?: IAttributeData): ICellData { @@ -75,7 +75,7 @@ export class Buffer implements IBuffer { } public getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine { - return new BufferLine(this._terminal.cols, this.getNullCell(attr), isWrapped); + return new BufferLine(this._bufferService.cols, this.getNullCell(attr), isWrapped); } public get hasScrollback(): boolean { @@ -98,7 +98,7 @@ export class Buffer implements IBuffer { return rows; } - const correctBufferLength = rows + this._terminal.options.scrollback; + const correctBufferLength = rows + this._optionsService.options.scrollback; return correctBufferLength > MAX_BUFFER_SIZE ? MAX_BUFFER_SIZE : correctBufferLength; } @@ -154,7 +154,7 @@ export class Buffer implements IBuffer { // Deal with columns increasing (reducing needs to happen after reflow) if (this._cols < newCols) { for (let i = 0; i < this.lines.length; i++) { - this.lines.get(i).resize(newCols, nullCell); + this.lines.get(i)!.resize(newCols, nullCell); } } @@ -227,7 +227,7 @@ export class Buffer implements IBuffer { // Trim the end of the line off if cols shrunk if (this._cols > newCols) { for (let i = 0; i < this.lines.length; i++) { - this.lines.get(i).resize(newCols, nullCell); + this.lines.get(i)!.resize(newCols, nullCell); } } } @@ -237,7 +237,7 @@ export class Buffer implements IBuffer { } private get _isReflowEnabled(): boolean { - return this._hasScrollback && !this._terminal.options.windowsMode; + return this._hasScrollback && !this._optionsService.options.windowsMode; } private _reflow(newCols: number, newRows: number): void { @@ -509,11 +509,11 @@ export class Buffer implements IBuffer { let first = y; let last = y; // Scan upwards for wrapped lines - while (first > 0 && this.lines.get(first).isWrapped) { + while (first > 0 && this.lines.get(first)!.isWrapped) { first--; } // Scan downwards for wrapped lines - while (last + 1 < this.lines.length && this.lines.get(last + 1).isWrapped) { + while (last + 1 < this.lines.length && this.lines.get(last + 1)!.isWrapped) { last++; } return { first, last }; @@ -533,7 +533,7 @@ export class Buffer implements IBuffer { i = 0; } - for (; i < this._cols; i += this._terminal.options.tabStopWidth) { + for (; i < this._cols; i += this._optionsService.options.tabStopWidth) { this.tabs[i] = true; } } diff --git a/src/core/buffer/BufferLine.test.ts b/src/common/buffer/BufferLine.test.ts similarity index 100% rename from src/core/buffer/BufferLine.test.ts rename to src/common/buffer/BufferLine.test.ts diff --git a/src/core/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts similarity index 98% rename from src/core/buffer/BufferLine.ts rename to src/common/buffer/BufferLine.ts index 4efef6975..747872804 100644 --- a/src/core/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -3,9 +3,8 @@ * @license MIT */ -import { CharData, IBufferLine, ICellData, IColorRGB, IAttributeData } from 'core/Types'; -import { stringFromCodePoint } from 'core/input/TextDecoder'; -import { DEFAULT_COLOR } from 'common/Types'; +import { DEFAULT_COLOR, CharData, IBufferLine, ICellData, IColorRGB, IAttributeData } from 'common/Types'; +import { stringFromCodePoint } from 'common/input/TextDecoder'; export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0); @@ -229,7 +228,7 @@ export class AttributeData implements IAttributeData { } } -export const DEFAULT_ATTR_DATA = new AttributeData(); +export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData()); /** * CellData - represents a single Cell in the terminal buffer. diff --git a/src/core/buffer/BufferReflow.test.ts b/src/common/buffer/BufferReflow.test.ts similarity index 97% rename from src/core/buffer/BufferReflow.test.ts rename to src/common/buffer/BufferReflow.test.ts index f7d2de738..af908572d 100644 --- a/src/core/buffer/BufferReflow.test.ts +++ b/src/common/buffer/BufferReflow.test.ts @@ -3,8 +3,8 @@ * @license MIT */ import { assert } from 'chai'; -import { BufferLine, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from 'core/buffer/BufferLine'; -import { reflowSmallerGetNewLineLengths } from 'core/buffer/BufferReflow'; +import { BufferLine, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from 'common/buffer/BufferLine'; +import { reflowSmallerGetNewLineLengths } from 'common/buffer/BufferReflow'; describe('BufferReflow', () => { describe('reflowSmallerGetNewLineLengths', () => { diff --git a/src/core/buffer/BufferReflow.ts b/src/common/buffer/BufferReflow.ts similarity index 98% rename from src/core/buffer/BufferReflow.ts rename to src/common/buffer/BufferReflow.ts index d75941e3c..ece9a96ed 100644 --- a/src/core/buffer/BufferReflow.ts +++ b/src/common/buffer/BufferReflow.ts @@ -3,9 +3,9 @@ * @license MIT */ -import { BufferLine } from 'core/buffer/BufferLine'; +import { BufferLine } from 'common/buffer/BufferLine'; import { CircularList } from 'common/CircularList'; -import { IBufferLine, ICellData } from 'core/Types'; +import { IBufferLine, ICellData } from 'common/Types'; export interface INewLayoutResult { layout: number[]; diff --git a/src/BufferSet.test.ts b/src/common/buffer/BufferSet.test.ts similarity index 83% rename from src/BufferSet.test.ts rename to src/common/buffer/BufferSet.test.ts index cdc220b3f..894ebb200 100644 --- a/src/BufferSet.test.ts +++ b/src/common/buffer/BufferSet.test.ts @@ -4,21 +4,18 @@ */ import { assert } from 'chai'; -import { ITerminal } from './Types'; -import { BufferSet } from './BufferSet'; -import { Buffer } from './Buffer'; -import { MockTerminal } from './TestUtils.test'; +import { BufferSet } from 'common/buffer/BufferSet'; +import { Buffer } from 'common/buffer/Buffer'; +import { MockOptionsService, MockBufferService } from 'common/TestUtils.test'; describe('BufferSet', () => { - let terminal: ITerminal; let bufferSet: BufferSet; beforeEach(() => { - terminal = new MockTerminal(); - (terminal as any).cols = 80; - (terminal as any).rows = 24; - terminal.options.scrollback = 1000; - bufferSet = new BufferSet(terminal); + bufferSet = new BufferSet( + new MockOptionsService({ scrollback: 1000 }), + new MockBufferService(80, 24) + ); }); describe('constructor', () => { diff --git a/src/BufferSet.ts b/src/common/buffer/BufferSet.ts similarity index 82% rename from src/BufferSet.ts rename to src/common/buffer/BufferSet.ts index c2aee6a7f..50e6e5050 100644 --- a/src/BufferSet.ts +++ b/src/common/buffer/BufferSet.ts @@ -3,10 +3,11 @@ * @license MIT */ -import { ITerminal, IBufferSet, IBuffer } from './Types'; -import { IAttributeData } from 'core/Types'; -import { Buffer } from './Buffer'; -import { EventEmitter2, IEvent } from 'common/EventEmitter2'; +import { IBuffer, IBufferSet } from 'common/buffer/Types'; +import { IAttributeData } from 'common/Types'; +import { Buffer } from 'common/buffer/Buffer'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { IOptionsService, IBufferService } from 'common/services/Services'; /** * The BufferSet represents the set of two buffers used by xterm terminals (normal and alt) and @@ -18,20 +19,23 @@ export class BufferSet implements IBufferSet { private _activeBuffer: Buffer; - private _onBufferActivate = new EventEmitter2<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}>(); + private _onBufferActivate = new EventEmitter<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}>(); public get onBufferActivate(): IEvent<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}> { return this._onBufferActivate.event; } /** * Create a new BufferSet for the given terminal. * @param _terminal - The terminal the BufferSet will belong to */ - constructor(private _terminal: ITerminal) { - this._normal = new Buffer(this._terminal, true); + constructor( + readonly optionsService: IOptionsService, + readonly bufferService: IBufferService + ) { + this._normal = new Buffer(true, optionsService, bufferService); this._normal.fillViewportRows(); // The alt buffer should never have scrollback. // See http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer - this._alt = new Buffer(this._terminal, false); + this._alt = new Buffer(false, optionsService, bufferService); this._activeBuffer = this._normal; this.setupTabStops(); diff --git a/src/core/buffer/Marker.ts b/src/common/buffer/Marker.ts similarity index 82% rename from src/core/buffer/Marker.ts rename to src/common/buffer/Marker.ts index 8d2071660..7f52ac99c 100644 --- a/src/core/buffer/Marker.ts +++ b/src/common/buffer/Marker.ts @@ -3,9 +3,9 @@ * @license MIT */ -import { EventEmitter2, IEvent } from 'common/EventEmitter2'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; import { Disposable } from 'common/Lifecycle'; -import { IMarker } from 'core/Types'; +import { IMarker } from 'common/Types'; export class Marker extends Disposable implements IMarker { private static _nextId = 1; @@ -15,7 +15,7 @@ export class Marker extends Disposable implements IMarker { public get id(): number { return this._id; } - private _onDispose = new EventEmitter2(); + private _onDispose = new EventEmitter(); public get onDispose(): IEvent { return this._onDispose.event; } constructor( diff --git a/src/common/buffer/Types.ts b/src/common/buffer/Types.ts new file mode 100644 index 000000000..2606df2f4 --- /dev/null +++ b/src/common/buffer/Types.ts @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IAttributeData, ICircularList, IBufferLine, ICellData, IMarker } from 'common/Types'; +import { IEvent } from 'common/EventEmitter'; + +// BufferIndex denotes a position in the buffer: [rowIndex, colIndex] +export type BufferIndex = [number, number]; + +export interface IBufferStringIteratorResult { + range: {first: number, last: number}; + content: string; +} + +export interface IBufferStringIterator { + hasNext(): boolean; + next(): IBufferStringIteratorResult; +} + +export interface IBuffer { + readonly lines: ICircularList; + ydisp: number; + ybase: number; + y: number; + x: number; + tabs: any; + scrollBottom: number; + scrollTop: number; + hasScrollback: boolean; + savedY: number; + savedX: number; + savedCurAttrData: IAttributeData; + isCursorInViewport: boolean; + markers: IMarker[]; + translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string; + getWrappedRangeForLine(y: number): { first: number, last: number }; + nextStop(x?: number): number; + prevStop(x?: number): number; + getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine; + stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[]; + iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator; + getNullCell(attr?: IAttributeData): ICellData; + getWhitespaceCell(attr?: IAttributeData): ICellData; + addMarker(y: number): IMarker; +} + +export interface IBufferSet { + alt: IBuffer; + normal: IBuffer; + active: IBuffer; + + onBufferActivate: IEvent<{ activeBuffer: IBuffer, inactiveBuffer: IBuffer }>; + + activateNormalBuffer(): void; + activateAltBuffer(fillAttr?: IAttributeData): void; + resize(newCols: number, newRows: number): void; + setupTabStops(i?: number): void; +} diff --git a/src/core/data/Charsets.ts b/src/common/data/Charsets.ts similarity index 99% rename from src/core/data/Charsets.ts rename to src/common/data/Charsets.ts index f6ee7297b..56ca67991 100644 --- a/src/core/data/Charsets.ts +++ b/src/common/data/Charsets.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ICharset } from 'core/Types'; +import { ICharset } from 'common/Types'; /** * The character sets supported by the terminal. These enable several languages diff --git a/src/core/input/Keyboard.test.ts b/src/common/input/Keyboard.test.ts similarity index 99% rename from src/core/input/Keyboard.test.ts rename to src/common/input/Keyboard.test.ts index 8a5f105b9..409a31926 100644 --- a/src/core/input/Keyboard.test.ts +++ b/src/common/input/Keyboard.test.ts @@ -1,8 +1,7 @@ import { assert } from 'chai'; -import { evaluateKeyboardEvent } from 'core/input/Keyboard'; -import { IKeyboardResult } from 'core/Types'; -import { IKeyboardEvent } from 'common/Types'; +import { evaluateKeyboardEvent } from 'common/input/Keyboard'; +import { IKeyboardResult, IKeyboardEvent } from 'common/Types'; /** * A helper function for testing which allows passing in a partial event and defaults will be filled diff --git a/src/core/input/Keyboard.ts b/src/common/input/Keyboard.ts similarity index 98% rename from src/core/input/Keyboard.ts rename to src/common/input/Keyboard.ts index c78328da4..3cc0eb764 100644 --- a/src/core/input/Keyboard.ts +++ b/src/common/input/Keyboard.ts @@ -4,8 +4,7 @@ * @license MIT */ -import { IKeyboardEvent } from 'common/Types'; -import { IKeyboardResult, KeyboardResultType } from 'core/Types'; +import { IKeyboardEvent, IKeyboardResult, KeyboardResultType } from 'common/Types'; import { C0 } from 'common/data/EscapeSequences'; // reg + shift key mappings for digits and special chars diff --git a/src/core/input/TextDecoder.test.ts b/src/common/input/TextDecoder.test.ts similarity index 99% rename from src/core/input/TextDecoder.test.ts rename to src/common/input/TextDecoder.test.ts index ba1c04faa..cda74a186 100644 --- a/src/core/input/TextDecoder.test.ts +++ b/src/common/input/TextDecoder.test.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32, utf32ToString } from 'core/input/TextDecoder'; +import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32, utf32ToString } from 'common/input/TextDecoder'; import { encode } from 'utf8'; // convert UTF32 codepoints to string diff --git a/src/core/input/TextDecoder.ts b/src/common/input/TextDecoder.ts similarity index 100% rename from src/core/input/TextDecoder.ts rename to src/common/input/TextDecoder.ts diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts new file mode 100644 index 000000000..18d5c6cf9 --- /dev/null +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -0,0 +1,1356 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ParserState, IDcsHandler, IParsingState } from 'common/parser/Types'; +import { EscapeSequenceParser, TransitionTable, VT500_TRANSITION_TABLE } from 'common/parser/EscapeSequenceParser'; +import * as chai from 'chai'; +import { StringToUtf32, stringFromCodePoint } from 'common/input/TextDecoder'; + +function r(a: number, b: number): string[] { + let c = b - a; + const arr = new Array(c); + while (c--) { + arr[c] = String.fromCharCode(--b); + } + return arr; +} + +// derived parser with access to internal states +class TestEscapeSequenceParser extends EscapeSequenceParser { + public get osc(): string { + return this._osc; + } + public set osc(value: string) { + this._osc = value; + } + public get params(): number[] { + return this._params; + } + public set params(value: number[]) { + this._params = value; + } + public get collect(): string { + return this._collect; + } + public set collect(value: string) { + this._collect = value; + } + public mockActiveDcsHandler(): void { + this._activeDcsHandler = this._dcsHandlerFb; + } +} + +// test object to collect parser actions and compare them with expected values +const testTerminal: any = { + calls: [], + clear: function (): void { + this.calls = []; + }, + compare: function (value: any): void { + chai.expect(this.calls.slice()).eql(value); // weird bug w'o slicing here + }, + print: function (data: Uint32Array, start: number, end: number): void { + let s = ''; + for (let i = start; i < end; ++i) { + s += stringFromCodePoint(data[i]); + } + this.calls.push(['print', s]); + }, + actionOSC: function (s: string): void { + this.calls.push(['osc', s]); + }, + actionExecute: function (flag: string): void { + this.calls.push(['exe', flag]); + }, + actionCSI: function (collect: string, params: number[], flag: string): void { + this.calls.push(['csi', collect, params, flag]); + }, + actionESC: function (collect: string, flag: string): void { + this.calls.push(['esc', collect, flag]); + }, + actionDCSHook: function (collect: string, params: number[], flag: string): void { + this.calls.push(['dcs hook', collect, params, flag]); + }, + actionDCSPrint: function (data: Uint32Array, start: number, end: number): void { + let s = ''; + for (let i = start; i < end; ++i) { + s += stringFromCodePoint(data[i]); + } + this.calls.push(['dcs put', s]); + }, + actionDCSUnhook: function (): void { + this.calls.push(['dcs unhook']); + } +}; + +// dcs handler to map dcs actions into the test object `testTerminal` +class DcsTest implements IDcsHandler { + hook(collect: string, params: number[], flag: number): void { + testTerminal.actionDCSHook(collect, params, String.fromCharCode(flag)); + } + put(data: Uint32Array, start: number, end: number): void { + testTerminal.actionDCSPrint(data, start, end); + } + unhook(): void { + testTerminal.actionDCSUnhook(); + } +} + +const states: number[] = [ + ParserState.GROUND, + ParserState.ESCAPE, + ParserState.ESCAPE_INTERMEDIATE, + ParserState.CSI_ENTRY, + ParserState.CSI_PARAM, + ParserState.CSI_INTERMEDIATE, + ParserState.CSI_IGNORE, + ParserState.SOS_PM_APC_STRING, + ParserState.OSC_STRING, + ParserState.DCS_ENTRY, + ParserState.DCS_PARAM, + ParserState.DCS_IGNORE, + ParserState.DCS_INTERMEDIATE, + ParserState.DCS_PASSTHROUGH +]; +let state: any; + +// parser with Uint8Array based transition table +const testParser = new TestEscapeSequenceParser(); +testParser.setPrintHandler(testTerminal.print.bind(testTerminal)); +testParser.setCsiHandlerFallback((collect: string, params: number[], flag: number) => { + testTerminal.actionCSI(collect, params, String.fromCharCode(flag)); +}); +testParser.setEscHandlerFallback((collect: string, flag: number) => { + testTerminal.actionESC(collect, String.fromCharCode(flag)); +}); +testParser.setExecuteHandlerFallback((code: number) => { + testTerminal.actionExecute(String.fromCharCode(code)); +}); +testParser.setOscHandlerFallback((identifier: number, data: string) => { + if (identifier === -1) testTerminal.actionOSC(data); // handle error condition silently + else testTerminal.actionOSC('' + identifier + ';' + data); +}); +testParser.setDcsHandlerFallback(new DcsTest()); + + +// translate string based parse calls into typed array based +function parse(parser: TestEscapeSequenceParser, data: string): void { + const container = new Uint32Array(data.length); + const decoder = new StringToUtf32(); + parser.parse(container, decoder.decode(data, container)); +} + +describe('EscapeSequenceParser', function (): void { + const parser = testParser; + describe('Parser init and methods', function (): void { + it('constructor', function (): void { + let p: EscapeSequenceParser = new EscapeSequenceParser(); + chai.expect(p.TRANSITIONS).equal(VT500_TRANSITION_TABLE); + p = new EscapeSequenceParser(VT500_TRANSITION_TABLE); + chai.expect(p.TRANSITIONS).equal(VT500_TRANSITION_TABLE); + const tansitions: TransitionTable = new TransitionTable(10); + p = new EscapeSequenceParser(tansitions); + chai.expect(p.TRANSITIONS).equal(tansitions); + }); + it('inital states', function (): void { + chai.expect(parser.initialState).equal(ParserState.GROUND); + chai.expect(parser.currentState).equal(ParserState.GROUND); + chai.expect(parser.osc).equal(''); + chai.expect(parser.params).eql([0]); + chai.expect(parser.collect).equal(''); + }); + it('reset states', function (): void { + parser.currentState = 124; + parser.osc = '#'; + parser.params = [123]; + parser.collect = '#'; + + parser.reset(); + chai.expect(parser.currentState).equal(ParserState.GROUND); + chai.expect(parser.osc).equal(''); + chai.expect(parser.params).eql([0]); + chai.expect(parser.collect).equal(''); + }); + }); + describe('state transitions and actions', function (): void { + it('state GROUND execute action', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes = exes.concat(['\x19']); + exes = exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = ParserState.GROUND; + parse(parser, exes[i]); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state GROUND print action', function (): void { + parser.reset(); + testTerminal.clear(); + const printables = r(0x20, 0x7f); // NOTE: DEL excluded + for (let i = 0; i < printables.length; ++i) { + parser.currentState = ParserState.GROUND; + parse(parser, printables[i]); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare([['print', printables[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans ANYWHERE --> GROUND with actions', function (): void { + const exes = [ + '\x18', '\x1a', + '\x80', '\x81', '\x82', '\x83', '\x84', '\x85', '\x86', '\x87', '\x88', + '\x89', '\x8a', '\x8b', '\x8c', '\x8d', '\x8e', '\x8f', + '\x91', '\x92', '\x93', '\x94', '\x95', '\x96', '\x97', '\x99', '\x9a' + ]; + const exceptions: { [key: number]: { [key: string]: any[] } } = { + 8: { '\x18': [], '\x1a': [] } // simply abort osc state + }; + parser.reset(); + testTerminal.clear(); + for (state in states) { + for (let i = 0; i < exes.length; ++i) { + parser.currentState = state; + parse(parser, exes[i]); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare((state in exceptions ? exceptions[state][exes[i]] : 0) || [['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + parse(parser, '\x9c'); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans ANYWHERE --> ESCAPE with clear', function (): void { + parser.reset(); + for (state in states) { + parser.currentState = state; + parser.osc = '#'; + parser.params = [23]; + parser.collect = '#'; + parse(parser, '\x1b'); + chai.expect(parser.currentState).equal(ParserState.ESCAPE); + chai.expect(parser.osc).equal(''); + chai.expect(parser.params).eql([0]); + chai.expect(parser.collect).equal(''); + parser.reset(); + } + }); + it('state ESCAPE execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes = exes.concat(['\x19']); + exes = exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = ParserState.ESCAPE; + parse(parser, exes[i]); + chai.expect(parser.currentState).equal(ParserState.ESCAPE); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state ESCAPE ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = ParserState.ESCAPE; + parse(parser, '\x7f'); + chai.expect(parser.currentState).equal(ParserState.ESCAPE); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('trans ESCAPE --> GROUND with ecs_dispatch action', function (): void { + parser.reset(); + testTerminal.clear(); + let dispatches = r(0x30, 0x50); + dispatches = dispatches.concat(r(0x51, 0x58)); + dispatches = dispatches.concat(['\x59', '\x5a']); // excluded \x5c + dispatches = dispatches.concat(r(0x60, 0x7f)); + for (let i = 0; i < dispatches.length; ++i) { + parser.currentState = ParserState.ESCAPE; + parse(parser, dispatches[i]); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare([['esc', '', dispatches[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans ESCAPE --> ESCAPE_INTERMEDIATE with collect action', function (): void { + parser.reset(); + const collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.ESCAPE; + parse(parser, collect[i]); + chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); + chai.expect(parser.collect).equal(collect[i]); + parser.reset(); + } + }); + it('state ESCAPE_INTERMEDIATE execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes = exes.concat(['\x19']); + exes = exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = ParserState.ESCAPE_INTERMEDIATE; + parse(parser, exes[i]); + chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state ESCAPE_INTERMEDIATE ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = ParserState.ESCAPE_INTERMEDIATE; + parse(parser, '\x7f'); + chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('state ESCAPE_INTERMEDIATE collect action', function (): void { + parser.reset(); + const collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.ESCAPE_INTERMEDIATE; + parse(parser, collect[i]); + chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); + chai.expect(parser.collect).equal(collect[i]); + parser.reset(); + } + }); + it('trans ESCAPE_INTERMEDIATE --> GROUND with esc_dispatch action', function (): void { + parser.reset(); + testTerminal.clear(); + const collect = r(0x30, 0x7f); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.ESCAPE_INTERMEDIATE; + parse(parser, collect[i]); + chai.expect(parser.currentState).equal(ParserState.GROUND); + // '\x5c' --> ESC + \ (7bit ST) parser does not expose this as it already got handled + testTerminal.compare((collect[i] === '\x5c') ? [] : [['esc', '', collect[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans ANYWHERE/ESCAPE --> CSI_ENTRY with clear', function (): void { + parser.reset(); + // C0 + parser.currentState = ParserState.ESCAPE; + parser.osc = '#'; + parser.params = [123]; + parser.collect = '#'; + parse(parser, '['); + chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); + chai.expect(parser.osc).equal(''); + chai.expect(parser.params).eql([0]); + chai.expect(parser.collect).equal(''); + parser.reset(); + // C1 + for (state in states) { + parser.currentState = state; + parser.osc = '#'; + parser.params = [123]; + parser.collect = '#'; + parse(parser, '\x9b'); + chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); + chai.expect(parser.osc).equal(''); + chai.expect(parser.params).eql([0]); + chai.expect(parser.collect).equal(''); + parser.reset(); + } + }); + it('state CSI_ENTRY execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes = exes.concat(['\x19']); + exes = exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = ParserState.CSI_ENTRY; + parse(parser, exes[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state CSI_ENTRY ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = ParserState.CSI_ENTRY; + parse(parser, '\x7f'); + chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('trans CSI_ENTRY --> GROUND with csi_dispatch action', function (): void { + parser.reset(); + const dispatches = r(0x40, 0x7f); + for (let i = 0; i < dispatches.length; ++i) { + parser.currentState = ParserState.CSI_ENTRY; + parse(parser, dispatches[i]); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare([['csi', '', [0], dispatches[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans CSI_ENTRY --> CSI_PARAM with param/collect actions', function (): void { + parser.reset(); + const params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; + const collect = ['\x3c', '\x3d', '\x3e', '\x3f']; + for (let i = 0; i < params.length; ++i) { + parser.currentState = ParserState.CSI_ENTRY; + parse(parser, params[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); + chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); + parser.reset(); + } + parser.currentState = ParserState.CSI_ENTRY; + parse(parser, '\x3b'); + chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.CSI_ENTRY; + parse(parser, collect[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); + chai.expect(parser.collect).equal(collect[i]); + parser.reset(); + } + }); + it('state CSI_PARAM execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes = exes.concat(['\x19']); + exes = exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = ParserState.CSI_PARAM; + parse(parser, exes[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state CSI_PARAM param action', function (): void { + parser.reset(); + const params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; + for (let i = 0; i < params.length; ++i) { + parser.currentState = ParserState.CSI_PARAM; + parse(parser, params[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); + chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); + parser.reset(); + } + parser.currentState = ParserState.CSI_PARAM; + parse(parser, '\x3b'); + chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + }); + it('state CSI_PARAM ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = ParserState.CSI_PARAM; + parse(parser, '\x7f'); + chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('trans CSI_PARAM --> GROUND with csi_dispatch action', function (): void { + parser.reset(); + const dispatches = r(0x40, 0x7f); + for (let i = 0; i < dispatches.length; ++i) { + parser.currentState = ParserState.CSI_PARAM; + parser.params = [0, 1]; + parse(parser, dispatches[i]); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare([['csi', '', [0, 1], dispatches[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans CSI_ENTRY --> CSI_INTERMEDIATE with collect action', function (): void { + parser.reset(); + const collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.CSI_ENTRY; + parse(parser, collect[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); + chai.expect(parser.collect).equal(collect[i]); + parser.reset(); + } + }); + it('trans CSI_PARAM --> CSI_INTERMEDIATE with collect action', function (): void { + parser.reset(); + const collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.CSI_PARAM; + parse(parser, collect[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); + chai.expect(parser.collect).equal(collect[i]); + parser.reset(); + } + }); + it('state CSI_INTERMEDIATE execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes = exes.concat(['\x19']); + exes = exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = ParserState.CSI_INTERMEDIATE; + parse(parser, exes[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state CSI_INTERMEDIATE collect', function (): void { + parser.reset(); + const collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.CSI_INTERMEDIATE; + parse(parser, collect[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); + chai.expect(parser.collect).equal(collect[i]); + parser.reset(); + } + }); + it('state CSI_INTERMEDIATE ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = ParserState.CSI_INTERMEDIATE; + parse(parser, '\x7f'); + chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('trans CSI_INTERMEDIATE --> GROUND with csi_dispatch action', function (): void { + parser.reset(); + const dispatches = r(0x40, 0x7f); + for (let i = 0; i < dispatches.length; ++i) { + parser.currentState = ParserState.CSI_INTERMEDIATE; + parser.params = [0, 1]; + parse(parser, dispatches[i]); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare([['csi', '', [0, 1], dispatches[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans CSI_ENTRY --> CSI_IGNORE', function (): void { + parser.reset(); + parser.currentState = ParserState.CSI_ENTRY; + parse(parser, '\x3a'); + chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); + parser.reset(); + }); + it('trans CSI_PARAM --> CSI_IGNORE', function (): void { + parser.reset(); + const chars = ['\x3a', '\x3c', '\x3d', '\x3e', '\x3f']; + for (let i = 0; i < chars.length; ++i) { + parser.currentState = ParserState.CSI_PARAM; + parse(parser, '\x3b' + chars[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + } + }); + it('trans CSI_INTERMEDIATE --> CSI_IGNORE', function (): void { + parser.reset(); + const chars = r(0x30, 0x40); + for (let i = 0; i < chars.length; ++i) { + parser.currentState = ParserState.CSI_INTERMEDIATE; + parse(parser, chars[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); + chai.expect(parser.params).eql([0]); + parser.reset(); + } + }); + it('state CSI_IGNORE execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + let exes = r(0x00, 0x18); + exes = exes.concat(['\x19']); + exes = exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = ParserState.CSI_IGNORE; + parse(parser, exes[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state CSI_IGNORE ignore', function (): void { + parser.reset(); + testTerminal.clear(); + let ignored = r(0x20, 0x40); + ignored = ignored.concat(['\x7f']); + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = ParserState.CSI_IGNORE; + parse(parser, ignored[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans CSI_IGNORE --> GROUND', function (): void { + parser.reset(); + const dispatches = r(0x40, 0x7f); + for (let i = 0; i < dispatches.length; ++i) { + parser.currentState = ParserState.CSI_IGNORE; + parser.params = [0, 1]; + parse(parser, dispatches[i]); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans ANYWHERE/ESCAPE --> SOS_PM_APC_STRING', function (): void { + parser.reset(); + // C0 + let initializers = ['\x58', '\x5e', '\x5f']; + for (let i = 0; i < initializers.length; ++i) { + parse(parser, '\x1b' + initializers[i]); + chai.expect(parser.currentState).equal(ParserState.SOS_PM_APC_STRING); + parser.reset(); + } + // C1 + for (state in states) { + parser.currentState = state; + initializers = ['\x98', '\x9e', '\x9f']; + for (let i = 0; i < initializers.length; ++i) { + parse(parser, initializers[i]); + chai.expect(parser.currentState).equal(ParserState.SOS_PM_APC_STRING); + parser.reset(); + } + } + }); + it('state SOS_PM_APC_STRING ignore rules', function (): void { + parser.reset(); + let ignored = r(0x00, 0x18); + ignored = ignored.concat(['\x19']); + ignored = ignored.concat(r(0x1c, 0x20)); + ignored = ignored.concat(r(0x20, 0x80)); + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = ParserState.SOS_PM_APC_STRING; + parse(parser, ignored[i]); + chai.expect(parser.currentState).equal(ParserState.SOS_PM_APC_STRING); + parser.reset(); + } + }); + it('trans ANYWHERE/ESCAPE --> OSC_STRING', function (): void { + parser.reset(); + // C0 + parse(parser, '\x1b]'); + chai.expect(parser.currentState).equal(ParserState.OSC_STRING); + parser.reset(); + // C1 + for (state in states) { + parser.currentState = state; + parse(parser, '\x9d'); + chai.expect(parser.currentState).equal(ParserState.OSC_STRING); + parser.reset(); + } + }); + it('state OSC_STRING ignore rules', function (): void { + parser.reset(); + const ignored = [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', /*'\x07',*/ '\x08', + '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', + '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f']; + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = ParserState.OSC_STRING; + parse(parser, ignored[i]); + chai.expect(parser.currentState).equal(ParserState.OSC_STRING); + chai.expect(parser.osc).equal(''); + parser.reset(); + } + }); + it('state OSC_STRING put action', function (): void { + parser.reset(); + const puts = r(0x20, 0x80); + for (let i = 0; i < puts.length; ++i) { + parser.currentState = ParserState.OSC_STRING; + parse(parser, puts[i]); + chai.expect(parser.currentState).equal(ParserState.OSC_STRING); + chai.expect(parser.osc).equal(puts[i]); + parser.reset(); + } + }); + it('state DCS_ENTRY', function (): void { + parser.reset(); + // C0 + parse(parser, '\x1bP'); + chai.expect(parser.currentState).equal(ParserState.DCS_ENTRY); + parser.reset(); + // C1 + for (state in states) { + parser.currentState = state; + parse(parser, '\x90'); + chai.expect(parser.currentState).equal(ParserState.DCS_ENTRY); + parser.reset(); + } + }); + it('state DCS_ENTRY ignore rules', function (): void { + parser.reset(); + const ignored = [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', + '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', + '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = ParserState.DCS_ENTRY; + parse(parser, ignored[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_ENTRY); + parser.reset(); + } + }); + it('state DCS_ENTRY --> DCS_PARAM with param/collect actions', function (): void { + parser.reset(); + const params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; + const collect = ['\x3c', '\x3d', '\x3e', '\x3f']; + for (let i = 0; i < params.length; ++i) { + parser.currentState = ParserState.DCS_ENTRY; + parse(parser, params[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); + chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); + parser.reset(); + } + parser.currentState = ParserState.DCS_ENTRY; + parse(parser, '\x3b'); + chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.DCS_ENTRY; + parse(parser, collect[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); + chai.expect(parser.collect).equal(collect[i]); + parser.reset(); + } + }); + it('state DCS_PARAM ignore rules', function (): void { + parser.reset(); + const ignored = [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', + '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', + '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = ParserState.DCS_PARAM; + parse(parser, ignored[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); + parser.reset(); + } + }); + it('state DCS_PARAM param action', function (): void { + parser.reset(); + const params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; + for (let i = 0; i < params.length; ++i) { + parser.currentState = ParserState.DCS_PARAM; + parse(parser, params[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); + chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); + parser.reset(); + } + parser.currentState = ParserState.DCS_PARAM; + parse(parser, '\x3b'); + chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + }); + it('trans DCS_ENTRY --> DCS_IGNORE', function (): void { + parser.reset(); + parser.currentState = ParserState.DCS_ENTRY; + parse(parser, '\x3a'); + chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); + parser.reset(); + }); + it('trans DCS_PARAM --> DCS_IGNORE', function (): void { + parser.reset(); + const chars = ['\x3a', '\x3c', '\x3d', '\x3e', '\x3f']; + for (let i = 0; i < chars.length; ++i) { + parser.currentState = ParserState.DCS_PARAM; + parse(parser, '\x3b' + chars[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + } + }); + it('trans DCS_INTERMEDIATE --> DCS_IGNORE', function (): void { + parser.reset(); + const chars = r(0x30, 0x40); + for (let i = 0; i < chars.length; ++i) { + parser.currentState = ParserState.DCS_INTERMEDIATE; + parse(parser, chars[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); + parser.reset(); + } + }); + it('state DCS_IGNORE ignore rules', function (): void { + parser.reset(); + let ignored = [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', + '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', + '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; + ignored = ignored.concat(r(0x20, 0x80)); + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = ParserState.DCS_IGNORE; + parse(parser, ignored[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); + parser.reset(); + } + }); + it('trans DCS_ENTRY --> DCS_INTERMEDIATE with collect action', function (): void { + parser.reset(); + const collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.DCS_ENTRY; + parse(parser, collect[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); + chai.expect(parser.collect).equal(collect[i]); + parser.reset(); + } + }); + it('trans DCS_PARAM --> DCS_INTERMEDIATE with collect action', function (): void { + parser.reset(); + const collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.DCS_PARAM; + parse(parser, collect[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); + chai.expect(parser.collect).equal(collect[i]); + parser.reset(); + } + }); + it('state DCS_INTERMEDIATE ignore rules', function (): void { + parser.reset(); + const ignored = [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', + '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', + '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = ParserState.DCS_INTERMEDIATE; + parse(parser, ignored[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); + parser.reset(); + } + }); + it('state DCS_INTERMEDIATE collect action', function (): void { + parser.reset(); + const collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.DCS_INTERMEDIATE; + parse(parser, collect[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); + chai.expect(parser.collect).equal(collect[i]); + parser.reset(); + } + }); + it('trans DCS_INTERMEDIATE --> DCS_IGNORE', function (): void { + parser.reset(); + const chars = r(0x30, 0x40); + for (let i = 0; i < chars.length; ++i) { + parser.currentState = ParserState.DCS_INTERMEDIATE; + parse(parser, '\x20' + chars[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); + chai.expect(parser.collect).equal('\x20'); + parser.reset(); + } + }); + it('trans DCS_ENTRY --> DCS_PASSTHROUGH with hook', function (): void { + parser.reset(); + testTerminal.clear(); + const collect = r(0x40, 0x7f); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.DCS_ENTRY; + parse(parser, collect[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); + testTerminal.compare([['dcs hook', '', [0], collect[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans DCS_PARAM --> DCS_PASSTHROUGH with hook', function (): void { + parser.reset(); + testTerminal.clear(); + const collect = r(0x40, 0x7f); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.DCS_PARAM; + parse(parser, collect[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); + testTerminal.compare([['dcs hook', '', [0], collect[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans DCS_INTERMEDIATE --> DCS_PASSTHROUGH with hook', function (): void { + parser.reset(); + testTerminal.clear(); + const collect = r(0x40, 0x7f); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.DCS_INTERMEDIATE; + parse(parser, collect[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); + testTerminal.compare([['dcs hook', '', [0], collect[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state DCS_PASSTHROUGH put action', function (): void { + parser.reset(); + testTerminal.clear(); + let puts = r(0x00, 0x18); + puts = puts.concat(['\x19']); + puts = puts.concat(r(0x1c, 0x20)); + puts = puts.concat(r(0x20, 0x7f)); + for (let i = 0; i < puts.length; ++i) { + parser.currentState = ParserState.DCS_PASSTHROUGH; + parser.mockActiveDcsHandler(); + parse(parser, puts[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); + testTerminal.compare([['dcs put', puts[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state DCS_PASSTHROUGH ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = ParserState.DCS_PASSTHROUGH; + parse(parser, '\x7f'); + chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + }); + + function test(s: string, value: any, noReset: any): void { + if (!noReset) { + parser.reset(); + testTerminal.clear(); + } + parse(parser, s); + testTerminal.compare(value); + } + + describe('escape sequence examples', function (): void { + it('CSI with print and execute', function (): void { + test('\x1b[<31;5mHello World! öäü€\nabc', + [ + ['csi', '<', [31, 5], 'm'], + ['print', 'Hello World! öäü€'], + ['exe', '\n'], + ['print', 'abc'] + ], null); + }); + it('OSC', function (): void { + test('\x1b]0;abc123€öäü\x07', [ + ['osc', '0;abc123€öäü'] + ], null); + }); + it('single DCS', function (): void { + test('\x1bP1;2;3+$aäbc;däe\x9c', [ + ['dcs hook', '+$', [1, 2, 3], 'a'], + ['dcs put', 'äbc;däe'], + ['dcs unhook'] + ], null); + }); + it('multi DCS', function (): void { + test('\x1bP1;2;3+$abc;de', [ + ['dcs hook', '+$', [1, 2, 3], 'a'], + ['dcs put', 'bc;de'] + ], null); + testTerminal.clear(); + test('abc\x9c', [ + ['dcs put', 'abc'], + ['dcs unhook'] + ], true); + }); + it('print + DCS(C1)', function (): void { + test('abc\x901;2;3+$abc;de\x9c', [ + ['print', 'abc'], + ['dcs hook', '+$', [1, 2, 3], 'a'], + ['dcs put', 'bc;de'], + ['dcs unhook'] + ], null); + }); + it('print + PM(C1) + print', function (): void { + test('abc\x98123tzf\x9cdefg', [ + ['print', 'abc'], + ['print', 'defg'] + ], null); + }); + it('print + OSC(C1) + print', function (): void { + test('abc\x9d123tzf\x9cdefg', [ + ['print', 'abc'], + ['osc', '123tzf'], + ['print', 'defg'] + ], null); + }); + it('error recovery', function (): void { + test('\x1b[1€abcdefg\x9b<;c', [ + ['print', 'abcdefg'], + ['csi', '<', [0, 0], 'c'] + ], null); + }); + it('7bit ST should be swallowed', function (): void { + test('abc\x9d123tzf\x1b\\defg', [ + ['print', 'abc'], + ['osc', '123tzf'], + ['print', 'defg'] + ], null); + }); + }); + + describe('coverage tests', function (): void { + it('CSI_IGNORE error', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = ParserState.CSI_IGNORE; + parse(parser, '€öäü'); + chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('DCS_IGNORE error', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = ParserState.DCS_IGNORE; + parse(parser, '€öäü'); + chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('DCS_PASSTHROUGH error', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = ParserState.DCS_PASSTHROUGH; + parse(parser, '\x901;2;3+$a€öäü'); + chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); + testTerminal.compare([['dcs hook', '+$', [1, 2, 3], 'a'], ['dcs put', '€öäü']]); + parser.reset(); + testTerminal.clear(); + }); + it('error else of if (code > 159)', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = ParserState.GROUND; + parse(parser, '\x9c'); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + }); + + describe('set/clear handler', function (): void { + const INPUT = '\x1b[1;31mhello \x1b%Gwor\x1bEld!\x1b[0m\r\n$>\x1b]1;foo=bar\x1b\\'; + let parser2: TestEscapeSequenceParser; + let print = ''; + const esc: string[] = []; + const csi: [string, number[], string][] = []; + const exe: string[] = []; + const osc: [number, string][] = []; + const dcs: ([string] | [string, string] | [string, string, number[], number])[] = []; + function clearAccu(): void { + print = ''; + esc.length = 0; + csi.length = 0; + exe.length = 0; + osc.length = 0; + dcs.length = 0; + } + beforeEach(function (): void { + parser2 = new TestEscapeSequenceParser(); + clearAccu(); + }); + it('print handler', function (): void { + parser2.setPrintHandler(function (data: Uint32Array, start: number, end: number): void { + for (let i = start; i < end; ++i) { + print += stringFromCodePoint(data[i]); + } + }); + parse(parser2, INPUT); + chai.expect(print).equal('hello world!$>'); + parser2.clearPrintHandler(); + parser2.clearPrintHandler(); // should not throw + clearAccu(); + parse(parser2, INPUT); + chai.expect(print).equal(''); + }); + it('ESC handler', function (): void { + parser2.setEscHandler('%G', function (): void { + esc.push('%G'); + }); + parser2.setEscHandler('E', function (): void { + esc.push('E'); + }); + parse(parser2, INPUT); + chai.expect(esc).eql(['%G', 'E']); + parser2.clearEscHandler('%G'); + parser2.clearEscHandler('%G'); // should not throw + clearAccu(); + parse(parser2, INPUT); + chai.expect(esc).eql(['E']); + parser2.clearEscHandler('E'); + clearAccu(); + parse(parser2, INPUT); + chai.expect(esc).eql([]); + }); + it('CSI handler', function (): void { + parser2.setCsiHandler('m', function (params: number[], collect: string): void { + csi.push(['m', params, collect]); + }); + parse(parser2, INPUT); + chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); + parser2.clearCsiHandler('m'); + parser2.clearCsiHandler('m'); // should not throw + clearAccu(); + parse(parser2, INPUT); + chai.expect(csi).eql([]); + }); + describe('CSI custom handlers', () => { + it('Prevent fallback', () => { + const csiCustom: [string, number[], string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); + parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); + parse(parser2, INPUT); + chai.expect(csi).eql([], 'Should not fallback to original handler'); + chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); + }); + it('Allow fallback', () => { + const csiCustom: [string, number[], string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); + parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return false; }); + parse(parser2, INPUT); + chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']], 'Should fallback to original handler'); + chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); + }); + it('Multiple custom handlers fallback once', () => { + const csiCustom: [string, number[], string][] = []; + const csiCustom2: [string, number[], string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); + parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); + parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params, collect]); return false; }); + parse(parser2, INPUT); + chai.expect(csi).eql([], 'Should not fallback to original handler'); + chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); + chai.expect(csiCustom2).eql([['m', [1, 31], ''], ['m', [0], '']]); + }); + it('Multiple custom handlers no fallback', () => { + const csiCustom: [string, number[], string][] = []; + const csiCustom2: [string, number[], string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); + parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); + parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params, collect]); return true; }); + parse(parser2, INPUT); + chai.expect(csi).eql([], 'Should not fallback to original handler'); + chai.expect(csiCustom).eql([], 'Should not fallback once'); + chai.expect(csiCustom2).eql([['m', [1, 31], ''], ['m', [0], '']]); + }); + it('Execution order should go from latest handler down to the original', () => { + const order: number[] = []; + parser2.setCsiHandler('m', () => order.push(1)); + parser2.addCsiHandler('m', () => { order.push(2); return false; }); + parser2.addCsiHandler('m', () => { order.push(3); return false; }); + parse(parser2, '\x1b[0m'); + chai.expect(order).eql([3, 2, 1]); + }); + it('Dispose should work', () => { + const csiCustom: [string, number[], string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); + const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); + customHandler.dispose(); + parse(parser2, INPUT); + chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); + chai.expect(csiCustom).eql([], 'Should not use custom handler as it was disposed'); + }); + it('Should not corrupt the parser when dispose is called twice', () => { + const csiCustom: [string, number[], string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); + const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); + customHandler.dispose(); + customHandler.dispose(); + parse(parser2, INPUT); + chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); + chai.expect(csiCustom).eql([], 'Should not use custom handler as it was disposed'); + }); + }); + it('EXECUTE handler', function (): void { + parser2.setExecuteHandler('\n', function (): void { + exe.push('\n'); + }); + parser2.setExecuteHandler('\r', function (): void { + exe.push('\r'); + }); + parse(parser2, INPUT); + chai.expect(exe).eql(['\r', '\n']); + parser2.clearExecuteHandler('\r'); + parser2.clearExecuteHandler('\r'); // should not throw + clearAccu(); + parse(parser2, INPUT); + chai.expect(exe).eql(['\n']); + }); + it('OSC handler', function (): void { + parser2.setOscHandler(1, function (data: string): void { + osc.push([1, data]); + }); + parse(parser2, INPUT); + chai.expect(osc).eql([[1, 'foo=bar']]); + parser2.clearOscHandler(1); + parser2.clearOscHandler(1); // should not throw + clearAccu(); + parse(parser2, INPUT); + chai.expect(osc).eql([]); + }); + describe('OSC custom handlers', () => { + it('Prevent fallback', () => { + const oscCustom: [number, string][] = []; + parser2.setOscHandler(1, data => osc.push([1, data])); + parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); + parse(parser2, INPUT); + chai.expect(osc).eql([], 'Should not fallback to original handler'); + chai.expect(oscCustom).eql([[1, 'foo=bar']]); + }); + it('Allow fallback', () => { + const oscCustom: [number, string][] = []; + parser2.setOscHandler(1, data => osc.push([1, data])); + parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return false; }); + parse(parser2, INPUT); + chai.expect(osc).eql([[1, 'foo=bar']], 'Should fallback to original handler'); + chai.expect(oscCustom).eql([[1, 'foo=bar']]); + }); + it('Multiple custom handlers fallback once', () => { + const oscCustom: [number, string][] = []; + const oscCustom2: [number, string][] = []; + parser2.setOscHandler(1, data => osc.push([1, data])); + parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); + parser2.addOscHandler(1, data => { oscCustom2.push([1, data]); return false; }); + parse(parser2, INPUT); + chai.expect(osc).eql([], 'Should not fallback to original handler'); + chai.expect(oscCustom).eql([[1, 'foo=bar']]); + chai.expect(oscCustom2).eql([[1, 'foo=bar']]); + }); + it('Multiple custom handlers no fallback', () => { + const oscCustom: [number, string][] = []; + const oscCustom2: [number, string][] = []; + parser2.setOscHandler(1, data => osc.push([1, data])); + parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); + parser2.addOscHandler(1, data => { oscCustom2.push([1, data]); return true; }); + parse(parser2, INPUT); + chai.expect(osc).eql([], 'Should not fallback to original handler'); + chai.expect(oscCustom).eql([], 'Should not fallback once'); + chai.expect(oscCustom2).eql([[1, 'foo=bar']]); + }); + it('Execution order should go from latest handler down to the original', () => { + const order: number[] = []; + parser2.setOscHandler(1, () => order.push(1)); + parser2.addOscHandler(1, () => { order.push(2); return false; }); + parser2.addOscHandler(1, () => { order.push(3); return false; }); + parse(parser2, '\x1b]1;foo=bar\x1b\\'); + chai.expect(order).eql([3, 2, 1]); + }); + it('Dispose should work', () => { + const oscCustom: [number, string][] = []; + parser2.setOscHandler(1, data => osc.push([1, data])); + const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); + customHandler.dispose(); + parse(parser2, INPUT); + chai.expect(osc).eql([[1, 'foo=bar']]); + chai.expect(oscCustom).eql([], 'Should not use custom handler as it was disposed'); + }); + it('Should not corrupt the parser when dispose is called twice', () => { + const oscCustom: [number, string][] = []; + parser2.setOscHandler(1, data => osc.push([1, data])); + const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); + customHandler.dispose(); + customHandler.dispose(); + parse(parser2, INPUT); + chai.expect(osc).eql([[1, 'foo=bar']]); + chai.expect(oscCustom).eql([], 'Should not use custom handler as it was disposed'); + }); + }); + it('DCS handler', function (): void { + parser2.setDcsHandler('+p', { + hook: function (collect: string, params: number[], flag: number): void { + dcs.push(['hook', collect, params, flag]); + }, + put: function (data: Uint32Array, start: number, end: number): void { + let s = ''; + for (let i = start; i < end; ++i) { + s += stringFromCodePoint(data[i]); + } + dcs.push(['put', s]); + }, + unhook: function (): void { + dcs.push(['unhook']); + } + }); + parse(parser2, '\x1bP1;2;3+pabc'); + parse(parser2, ';de\x9c'); + chai.expect(dcs).eql([ + ['hook', '+', [1, 2, 3], 'p'.charCodeAt(0)], + ['put', 'abc'], ['put', ';de'], + ['unhook'] + ]); + parser2.clearDcsHandler('+p'); + parser2.clearDcsHandler('+p'); // should not throw + clearAccu(); + parse(parser2, '\x1bP1;2;3+pabc'); + parse(parser2, ';de\x9c'); + chai.expect(dcs).eql([]); + }); + it('ERROR handler', function (): void { + let errorState: IParsingState | null = null; + parser2.setErrorHandler(function (state: IParsingState): IParsingState { + errorState = state; + return state; + }); + parse(parser2, '\x1b[1;2;€;3m'); // faulty escape sequence + chai.expect(errorState).eql({ + position: 6, + code: '€'.charCodeAt(0), + currentState: ParserState.CSI_PARAM, + osc: '', + collect: '', + params: [1, 2, 0], // extra zero here + abort: false + }); + parser2.clearErrorHandler(); + parser2.clearErrorHandler(); // should not throw + errorState = null; + parse(parser2, '\x1b[1;2;a;3m'); + chai.expect(errorState).eql(null); + }); + }); + // TODO: error conditions and error recovery (not implemented yet in parser) +}); diff --git a/src/core/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts similarity index 79% rename from src/core/parser/EscapeSequenceParser.ts rename to src/common/parser/EscapeSequenceParser.ts index 7929b2f06..c8b631d08 100644 --- a/src/core/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -3,10 +3,11 @@ * @license MIT */ -import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from 'core/parser/Types'; +import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from 'common/parser/Types'; import { Disposable } from 'common/Lifecycle'; -import { utf32ToString } from 'core/input/TextDecoder'; +import { utf32ToString } from 'common/input/TextDecoder'; import { IDisposable } from 'common/Types'; +import { fill } from 'common/TypedArrayUtils'; interface IHandlerCollection { [key: string]: T[]; @@ -16,31 +17,33 @@ type CsiHandler = (params: number[], collect: string) => boolean | void; type OscHandler = (data: string) => boolean | void; /** - * Returns an array filled with numbers between the low and high parameters (right exclusive). - * @param low The low number. - * @param high The high number. + * Table values are generated like this: + * index: currentState << TableValue.INDEX_STATE_SHIFT | charCode + * value: action << TableValue.TRANSITION_ACTION_SHIFT | nextState */ -function r(low: number, high: number): number[] { - let c = high - low; - const arr = new Array(c); - while (c--) { - arr[c] = --high; - } - return arr; +const enum TableAccess { + TRANSITION_ACTION_SHIFT = 4, + TRANSITION_STATE_MASK = 15, + INDEX_STATE_SHIFT = 8 } /** * Transition table for EscapeSequenceParser. - * NOTE: data in the underlying table is packed like this: - * currentState << 8 | characterCode --> action << 4 | nextState */ export class TransitionTable { - public table: Uint8Array | number[]; + public table: Uint8Array; constructor(length: number) { - this.table = (typeof Uint8Array === 'undefined') - ? new Array(length) - : new Uint8Array(length); + this.table = new Uint8Array(length); + } + + /** + * Set default transition. + * @param action default action + * @param next default next state + */ + public setDefault(action: ParserAction, next: ParserState): void { + fill(this.table, action << TableAccess.TRANSITION_ACTION_SHIFT | next); } /** @@ -50,8 +53,8 @@ export class TransitionTable { * @param action parser action to be done * @param next next parser state */ - add(code: number, state: ParserState, action: ParserAction, next: ParserState): void { - this.table[state << 8 | code] = (action << 4) | next; + public add(code: number, state: ParserState, action: ParserAction, next: ParserState): void { + this.table[state << TableAccess.INDEX_STATE_SHIFT | code] = action << TableAccess.TRANSITION_ACTION_SHIFT | next; } /** @@ -61,24 +64,18 @@ export class TransitionTable { * @param action parser action to be done * @param next next parser state */ - addMany(codes: number[], state: ParserState, action: ParserAction, next: ParserState): void { + public addMany(codes: number[], state: ParserState, action: ParserAction, next: ParserState): void { for (let i = 0; i < codes.length; i++) { - this.add(codes[i], state, action, next); + this.table[state << TableAccess.INDEX_STATE_SHIFT | codes[i]] = action << TableAccess.TRANSITION_ACTION_SHIFT | next; } } } -/** - * Default definitions for the VT500_TRANSITION_TABLE. - */ -const PRINTABLES = r(0x20, 0x7f); -const EXECUTABLES = r(0x00, 0x18); -EXECUTABLES.push(0x19); -EXECUTABLES.push.apply(EXECUTABLES, r(0x1c, 0x20)); -// Pseudo-character placeholder for printable non-ascii characters. +// Pseudo-character placeholder for printable non-ascii characters (unicode). const NON_ASCII_PRINTABLE = 0xA0; + /** * VT500 compatible transition table. * Taken from https://vt100.net/emu/dec_ansi_parser. @@ -86,16 +83,22 @@ const NON_ASCII_PRINTABLE = 0xA0; export const VT500_TRANSITION_TABLE = (function (): TransitionTable { const table: TransitionTable = new TransitionTable(4095); + // range macro for byte + const BYTE_VALUES = 256; + const blueprint = Array.apply(null, Array(BYTE_VALUES)).map((unused: any, i: number) => i); + const r = (start: number, end: number) => blueprint.slice(start, end); + + // Default definitions. + const PRINTABLES = r(0x20, 0x7f); // 0x20 (SP) included, 0x7F (DEL) excluded + const EXECUTABLES = r(0x00, 0x18); + EXECUTABLES.push(0x19); + EXECUTABLES.push.apply(EXECUTABLES, r(0x1c, 0x20)); + const states: number[] = r(ParserState.GROUND, ParserState.DCS_PASSTHROUGH + 1); let state: any; - // table with default transition - for (state in states) { - // NOTE: table lookup is capped at 0xa0 in parse to keep the table small - for (let code = 0; code <= NON_ASCII_PRINTABLE; ++code) { - table.add(code, state, ParserAction.ERROR, ParserState.GROUND); - } - } + // set default transition + table.setDefault(ParserAction.ERROR, ParserState.GROUND); // printables table.addMany(PRINTABLES, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND); // global anywhere rules @@ -195,7 +198,12 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable { table.addMany(PRINTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH); table.add(0x7f, ParserState.DCS_PASSTHROUGH, ParserAction.IGNORE, ParserState.DCS_PASSTHROUGH); table.addMany([0x1b, 0x9c], ParserState.DCS_PASSTHROUGH, ParserAction.DCS_UNHOOK, ParserState.GROUND); + // special handling of unicode chars + table.add(NON_ASCII_PRINTABLE, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND); table.add(NON_ASCII_PRINTABLE, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING); + table.add(NON_ASCII_PRINTABLE, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE); + table.add(NON_ASCII_PRINTABLE, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE); + table.add(NON_ASCII_PRINTABLE, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH); return table; })(); @@ -223,6 +231,7 @@ class DcsDummy implements IDcsHandler { export class EscapeSequenceParser extends Disposable implements IEscapeSequenceParser { public initialState: number; public currentState: number; + public precedingCodepoint: number; // buffers over several parse calls protected _osc: string; @@ -256,6 +265,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._osc = ''; this._params = [0]; this._collect = ''; + this.precedingCodepoint = 0; // set default fallback handlers and handler lookup containers this._printHandlerFb = (data, start, end): void => { }; @@ -386,19 +396,29 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._params = [0]; this._collect = ''; this._activeDcsHandler = null; + this.precedingCodepoint = 0; } + /** + * Parse UTF32 codepoints in `data` up to `length`. + * Note: For several actions with high data load the parsing is optimized + * by using local read ahead loops with hardcoded conditions to + * avoid costly table lookups. Make sure that any change of table values + * will be reflected in the loop conditions as well. Affected states/actions: + * - GROUND:PRINT + * - CSI_PARAM:PARAM + * - DCS_PARAM:PARAM + * - OSC_STRING:OSC_PUT + * - DCS_PASSTHROUGH:DCS_PUT + */ parse(data: Uint32Array, length: number): void { let code = 0; let transition = 0; - let error = false; let currentState = this.currentState; - let print = -1; - let dcs = -1; let osc = this._osc; let collect = this._collect; let params = this._params; - const table: Uint8Array | number[] = this.TRANSITIONS.table; + const table: Uint8Array = this.TRANSITIONS.table; let dcsHandler: IDcsHandler | null = this._activeDcsHandler; let callback: Function | null = null; @@ -406,92 +426,62 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP for (let i = 0; i < length; ++i) { code = data[i]; - // shortcut for most chars (print action) - if (currentState === ParserState.GROUND && code > 0x1f && code < 0x80) { - print = (~print) ? print : i; - do i++; - while (i < length && data[i] > 0x1f && data[i] < 0x80); - i--; - continue; - } - - // shortcut for CSI params - if (currentState === ParserState.CSI_PARAM && (code > 0x2f && code < 0x39)) { - params[params.length - 1] = params[params.length - 1] * 10 + code - 48; - continue; - } - // normal transition & action lookup - transition = table[currentState << 8 | (code < 0xa0 ? code : NON_ASCII_PRINTABLE)]; - switch (transition >> 4) { + transition = table[currentState << TableAccess.INDEX_STATE_SHIFT | (code < 0xa0 ? code : NON_ASCII_PRINTABLE)]; + switch (transition >> TableAccess.TRANSITION_ACTION_SHIFT) { case ParserAction.PRINT: - print = (~print) ? print : i; + // read ahead with loop unrolling + // Note: 0x20 (SP) is included, 0x7F (DEL) is excluded + for (let j = i + 1; ; ++j) { + if (j >= length || (code = data[j]) < 0x20 || (code > 0x7e && code < NON_ASCII_PRINTABLE)) { + this._printHandler(data, i, j); + i = j - 1; + break; + } + if (++j >= length || (code = data[j]) < 0x20 || (code > 0x7e && code < NON_ASCII_PRINTABLE)) { + this._printHandler(data, i, j); + i = j - 1; + break; + } + if (++j >= length || (code = data[j]) < 0x20 || (code > 0x7e && code < NON_ASCII_PRINTABLE)) { + this._printHandler(data, i, j); + i = j - 1; + break; + } + if (++j >= length || (code = data[j]) < 0x20 || (code > 0x7e && code < NON_ASCII_PRINTABLE)) { + this._printHandler(data, i, j); + i = j - 1; + break; + } + } break; case ParserAction.EXECUTE: - if (~print) { - this._printHandler(data, print, i); - print = -1; - } + this.precedingCodepoint = 0; callback = this._executeHandlers[code]; if (callback) callback(); else this._executeHandlerFb(code); break; case ParserAction.IGNORE: - // handle leftover print or dcs chars - if (~print) { - this._printHandler(data, print, i); - print = -1; - } else if (~dcs) { - if (dcsHandler) { - dcsHandler.put(data, dcs, i); - } - dcs = -1; - } break; case ParserAction.ERROR: - // chars higher than 0x9f are handled by this action - // to keep the transition table small - if (code > 0x9f) { - switch (currentState) { - case ParserState.GROUND: - print = (~print) ? print : i; - break; - case ParserState.CSI_IGNORE: - transition |= ParserState.CSI_IGNORE; - break; - case ParserState.DCS_IGNORE: - transition |= ParserState.DCS_IGNORE; - break; - case ParserState.DCS_PASSTHROUGH: - dcs = (~dcs) ? dcs : i; - transition |= ParserState.DCS_PASSTHROUGH; - break; - default: - error = true; - } - } else { - error = true; - } - // if we end up here a real error happened - if (error) { - const inject: IParsingState = this._errorHandler( - { - position: i, - code, - currentState, - print, - dcs, - osc, - collect, - params, - abort: false - }); - if (inject.abort) return; - // TODO: inject return values - error = false; - } + const inject: IParsingState = this._errorHandler( + { + position: i, + code, + currentState, + osc, + collect, + params, + abort: false + }); + if (inject.abort) return; + // inject values: currently not implemented break; case ParserAction.CSI_DISPATCH: + // dont reset preceding codepoint for REP itself + if (code !== 98) { // 'b' + this.precedingCodepoint = 0; + } // Trigger CSI Handler const handlers = this._csiHandlers[code]; let j = handlers ? handlers.length - 1 : -1; @@ -506,38 +496,48 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } break; case ParserAction.PARAM: - if (code === 0x3b) params.push(0); - else params[params.length - 1] = params[params.length - 1] * 10 + code - 48; + // inner loop: digits (0x30 - 0x39) and ; (0x3b) + do { + if (code === 0x3b) params.push(0); + else params[params.length - 1] = params[params.length - 1] * 10 + code - 48; + } while (++i < length && (code = data[i]) > 0x2f && (code < 0x3a || code === 0x3b)); + i--; break; case ParserAction.COLLECT: collect += String.fromCharCode(code); break; case ParserAction.ESC_DISPATCH: + this.precedingCodepoint = 0; callback = this._escHandlers[collect + String.fromCharCode(code)]; if (callback) callback(collect, code); else this._escHandlerFb(collect, code); break; case ParserAction.CLEAR: - if (~print) { - this._printHandler(data, print, i); - print = -1; - } osc = ''; params = [0]; collect = ''; - dcs = -1; break; case ParserAction.DCS_HOOK: + this.precedingCodepoint = 0; dcsHandler = this._dcsHandlers[collect + String.fromCharCode(code)]; if (!dcsHandler) dcsHandler = this._dcsHandlerFb; dcsHandler.hook(collect, params, code); break; case ParserAction.DCS_PUT: - dcs = (~dcs) ? dcs : i; + // inner loop - exit DCS_PUT: 0x18, 0x1a, 0x1b, 0x7f, 0x80 - 0x9f + // unhook triggered by: 0x1b, 0x9c + for (let j = i + 1; ; ++j) { + if (j >= length || (code = data[j]) === 0x18 || code === 0x1a || code === 0x1b || (code > 0x7f && code < NON_ASCII_PRINTABLE)) { + if (dcsHandler) { + dcsHandler.put(data, i, j); + } + i = j - 1; + break; + } + } break; case ParserAction.DCS_UNHOOK: if (dcsHandler) { - if (~dcs) dcsHandler.put(data, dcs, i); dcsHandler.unhook(); dcsHandler = null; } @@ -545,20 +545,14 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP osc = ''; params = [0]; collect = ''; - dcs = -1; break; case ParserAction.OSC_START: - if (~print) { - this._printHandler(data, print, i); - print = -1; - } osc = ''; break; case ParserAction.OSC_PUT: + // inner loop: 0x20 (SP) included, 0x7F (DEL) included for (let j = i + 1; ; j++) { - if (j >= length - || (code = data[j]) < 0x20 - || (code > 0x7f && code <= 0x9f)) { + if (j >= length || (code = data[j]) < 0x20 || (code > 0x7f && code <= 0x9f)) { osc += utf32ToString(data, i, j); i = j - 1; break; @@ -566,6 +560,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } break; case ParserAction.OSC_END: + this.precedingCodepoint = 0; if (osc && code !== 0x18 && code !== 0x1a) { // NOTE: OSC subparsing is not part of the original parser // we do basic identifier parsing here to offer a jump table for OSC as well @@ -596,17 +591,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP osc = ''; params = [0]; collect = ''; - dcs = -1; break; } - currentState = transition & 15; - } - - // push leftover pushable buffers to terminal - if (currentState === ParserState.GROUND && ~print) { - this._printHandler(data, print, length); - } else if (currentState === ParserState.DCS_PASSTHROUGH && ~dcs && dcsHandler) { - dcsHandler.put(data, dcs, length); + currentState = transition & TableAccess.TRANSITION_STATE_MASK; } // save non pushable buffers diff --git a/src/core/parser/Types.ts b/src/common/parser/Types.ts similarity index 94% rename from src/core/parser/Types.ts rename to src/common/parser/Types.ts index c208496aa..871997029 100644 --- a/src/core/parser/Types.ts +++ b/src/common/parser/Types.ts @@ -61,10 +61,6 @@ export interface IParsingState { code: number; // current parser state currentState: ParserState; - // print buffer start index (-1 for not set) - print: number; - // dcs buffer start index (-1 for not set) - dcs: number; // osc string buffer osc: string; // collect buffer with intermediate characters @@ -106,6 +102,12 @@ export interface IDcsHandler { * EscapeSequenceParser interface. */ export interface IEscapeSequenceParser extends IDisposable { + /** + * Preceding codepoint to get REP working correctly. + * This must be set by the print handler as last action. + * It gets reset by the parser for any valid sequence beside REP itself. + */ + precedingCodepoint: number; /** * Reset the parser to its initial state (handlers are kept). */ diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts new file mode 100644 index 000000000..6ff080617 --- /dev/null +++ b/src/common/services/BufferService.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IBufferService, IOptionsService } from 'common/services/Services'; +import { BufferSet } from 'common/buffer/BufferSet'; +import { IBufferSet, IBuffer } from 'common/buffer/Types'; + +export const MINIMUM_COLS = 2; // Less than 2 can mess with wide chars +export const MINIMUM_ROWS = 1; + +export class BufferService implements IBufferService { + public cols: number; + public rows: number; + public buffers: IBufferSet; + + public get buffer(): IBuffer { return this.buffers.active; } + + constructor( + private _optionsService: IOptionsService + ) { + this.cols = Math.max(_optionsService.options.cols, MINIMUM_COLS); + this.rows = Math.max(_optionsService.options.rows, MINIMUM_ROWS); + this.buffers = new BufferSet(_optionsService, this); + } + + public resize(cols: number, rows: number): void { + this.cols = cols; + this.rows = rows; + } + + public reset(): void { + this.buffers = new BufferSet(this._optionsService, this); + } +} diff --git a/src/common/options/OptionsService.ts b/src/common/services/OptionsService.ts similarity index 94% rename from src/common/options/OptionsService.ts rename to src/common/services/OptionsService.ts index 3523198cd..2d2a08f02 100644 --- a/src/common/options/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -3,8 +3,8 @@ * @license MIT */ -import { IOptionsService, ITerminalOptions, IPartialTerminalOptions } from 'common/options/Types'; -import { EventEmitter2, IEvent } from 'common/EventEmitter2'; +import { IOptionsService, ITerminalOptions, IPartialTerminalOptions } from 'common/services/Services'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; import { isMac } from 'common/Platform'; import { clone } from 'common/Clone'; @@ -15,7 +15,7 @@ import { clone } from 'common/Clone'; export const DEFAULT_BELL_SOUND = 'data:audio/wav;base64,UklGRigBAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQBAADpAFgCwAMlBZoG/wdmCcoKRAypDQ8PbRDBEQQTOxRtFYcWlBePGIUZXhoiG88bcBz7HHIdzh0WHlMeZx51HmkeUx4WHs8dah0AHXwc3hs9G4saxRnyGBIYGBcQFv8U4RPAEoYRQBACD70NWwwHC6gJOwjWBloF7gOBAhABkf8b/qv8R/ve+Xf4Ife79W/0JfPZ8Z/wde9N7ijtE+wU6xvqM+lb6H7nw+YX5mrlxuQz5Mzje+Ma49fioeKD4nXiYeJy4pHitOL04j/jn+MN5IPkFOWs5U3mDefM55/ogOl36m7rdOyE7abuyu8D8Unyj/Pg9D/2qfcb+Yn6/vuK/Qj/lAAlAg=='; // TODO: Freeze? -const DEFAULT_OPTIONS: ITerminalOptions = { +export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({ cols: 80, rows: 24, cursorBlink: false, @@ -47,7 +47,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = { debug: false, cancelEvents: false, useFlowControl: false -}; +}); /** * The set of options that only have an effect when set in the Terminal constructor. @@ -57,7 +57,7 @@ const CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows']; export class OptionsService implements IOptionsService { public options: ITerminalOptions; - private _onOptionChange = new EventEmitter2(); + private _onOptionChange = new EventEmitter(); public get onOptionChange(): IEvent { return this._onOptionChange.event; } constructor(options: IPartialTerminalOptions) { diff --git a/src/common/options/Types.ts b/src/common/services/Services.d.ts similarity index 87% rename from src/common/options/Types.ts rename to src/common/services/Services.d.ts index 5c5c33234..602671ff7 100644 --- a/src/common/options/Types.ts +++ b/src/common/services/Services.d.ts @@ -3,13 +3,26 @@ * @license MIT */ -import { IEvent } from 'common/EventEmitter2'; +import { IEvent } from 'common/EventEmitter'; +import { IBuffer, IBufferSet } from 'common/buffer/Types'; + +export interface IBufferService { + readonly cols: number; + readonly rows: number; + readonly buffer: IBuffer; + readonly buffers: IBufferSet; + + // TODO: Move resize event here + + resize(cols: number, rows: number): void; + reset(): void; +} export interface IOptionsService { - readonly onOptionChange: IEvent; - // TODO: as const? readonly options: ITerminalOptions; + readonly onOptionChange: IEvent; + setOption(key: string, value: T): void; getOption(key: string): T | undefined; } diff --git a/src/core/Types.ts b/src/core/Types.ts deleted file mode 100644 index 39cb172ee..000000000 --- a/src/core/Types.ts +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Copyright (c) 2018 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { IDisposable } from 'common/Types'; - -export const enum KeyboardResultType { - SEND_KEY, - SELECT_ALL, - PAGE_UP, - PAGE_DOWN -} - -export interface IKeyboardResult { - type: KeyboardResultType; - cancel: boolean; - key: string | undefined; -} - -export interface ICharset { - [key: string]: string; -} - -export type CharData = [number, string, number, number]; -export type IColorRGB = [number, number, number]; - -/** Attribute data */ -export interface IAttributeData { - fg: number; - bg: number; - - clone(): IAttributeData; - - // flags - isInverse(): number; - isBold(): number; - isUnderline(): number; - isBlink(): number; - isInvisible(): number; - isItalic(): number; - isDim(): number; - - // color modes - getFgColorMode(): number; - getBgColorMode(): number; - isFgRGB(): boolean; - isBgRGB(): boolean; - isFgPalette(): boolean; - isBgPalette(): boolean; - isFgDefault(): boolean; - isBgDefault(): boolean; - - // colors - getFgColor(): number; - getBgColor(): number; -} - -/** Cell data */ -export interface ICellData extends IAttributeData { - content: number; - combinedData: string; - isCombined(): number; - getWidth(): number; - getChars(): string; - getCode(): number; - setFromCharData(value: CharData): void; - getAsCharData(): CharData; -} - -/** - * Interface for a line in the terminal buffer. - */ -export interface IBufferLine { - length: number; - isWrapped: boolean; - get(index: number): CharData; - set(index: number, value: CharData): void; - loadCell(index: number, cell: ICellData): ICellData; - setCell(index: number, cell: ICellData): void; - setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void; - addCodepointToCell(index: number, codePoint: number): void; - insertCells(pos: number, n: number, ch: ICellData): void; - deleteCells(pos: number, n: number, fill: ICellData): void; - replaceCells(start: number, end: number, fill: ICellData): void; - resize(cols: number, fill: ICellData): void; - fill(fillCellData: ICellData): void; - copyFrom(line: IBufferLine): void; - clone(): IBufferLine; - getTrimmedLength(): number; - translateToString(trimRight?: boolean, startCol?: number, endCol?: number): string; - - /* direct access to cell attrs */ - getWidth(index: number): number; - hasWidth(index: number): number; - getFg(index: number): number; - getBg(index: number): number; - hasContent(index: number): number; - getCodePoint(index: number): number; - isCombined(index: number): number; - getString(index: number): string; -} - -export interface IMarker extends IDisposable { - readonly id: number; - readonly isDisposed: boolean; - readonly line: number; -} diff --git a/src/core/parser/EscapeSequenceParser.test.ts b/src/core/parser/EscapeSequenceParser.test.ts deleted file mode 100644 index 553050c85..000000000 --- a/src/core/parser/EscapeSequenceParser.test.ts +++ /dev/null @@ -1,1406 +0,0 @@ -/** - * Copyright (c) 2018 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { ParserState, IDcsHandler, IParsingState } from 'core/parser/Types'; -import { EscapeSequenceParser, TransitionTable, VT500_TRANSITION_TABLE } from 'core/parser/EscapeSequenceParser'; -import * as chai from 'chai'; -import { StringToUtf32, stringFromCodePoint } from 'core/input/TextDecoder'; - -function r(a: number, b: number): string[] { - let c = b - a; - const arr = new Array(c); - while (c--) { - arr[c] = String.fromCharCode(--b); - } - return arr; -} - -// derived parser with access to internal states -class TestEscapeSequenceParser extends EscapeSequenceParser { - public get osc(): string { - return this._osc; - } - public set osc(value: string) { - this._osc = value; - } - public get params(): number[] { - return this._params; - } - public set params(value: number[]) { - this._params = value; - } - public get collect(): string { - return this._collect; - } - public set collect(value: string) { - this._collect = value; - } - public mockActiveDcsHandler(): void { - this._activeDcsHandler = this._dcsHandlerFb; - } -} - -// test object to collect parser actions and compare them with expected values -const testTerminal: any = { - calls: [], - clear: function (): void { - this.calls = []; - }, - compare: function (value: any): void { - chai.expect(this.calls.slice()).eql(value); // weird bug w'o slicing here - }, - print: function (data: Uint32Array, start: number, end: number): void { - let s = ''; - for (let i = start; i < end; ++i) { - s += stringFromCodePoint(data[i]); - } - this.calls.push(['print', s]); - }, - actionOSC: function (s: string): void { - this.calls.push(['osc', s]); - }, - actionExecute: function (flag: string): void { - this.calls.push(['exe', flag]); - }, - actionCSI: function (collect: string, params: number[], flag: string): void { - this.calls.push(['csi', collect, params, flag]); - }, - actionESC: function (collect: string, flag: string): void { - this.calls.push(['esc', collect, flag]); - }, - actionDCSHook: function (collect: string, params: number[], flag: string): void { - this.calls.push(['dcs hook', collect, params, flag]); - }, - actionDCSPrint: function (data: Uint32Array, start: number, end: number): void { - let s = ''; - for (let i = start; i < end; ++i) { - s += stringFromCodePoint(data[i]); - } - this.calls.push(['dcs put', s]); - }, - actionDCSUnhook: function (): void { - this.calls.push(['dcs unhook']); - } -}; - -// dcs handler to map dcs actions into the test object `testTerminal` -class DcsTest implements IDcsHandler { - hook(collect: string, params: number[], flag: number): void { - testTerminal.actionDCSHook(collect, params, String.fromCharCode(flag)); - } - put(data: Uint32Array, start: number, end: number): void { - testTerminal.actionDCSPrint(data, start, end); - } - unhook(): void { - testTerminal.actionDCSUnhook(); - } -} - -const states: number[] = [ - ParserState.GROUND, - ParserState.ESCAPE, - ParserState.ESCAPE_INTERMEDIATE, - ParserState.CSI_ENTRY, - ParserState.CSI_PARAM, - ParserState.CSI_INTERMEDIATE, - ParserState.CSI_IGNORE, - ParserState.SOS_PM_APC_STRING, - ParserState.OSC_STRING, - ParserState.DCS_ENTRY, - ParserState.DCS_PARAM, - ParserState.DCS_IGNORE, - ParserState.DCS_INTERMEDIATE, - ParserState.DCS_PASSTHROUGH -]; -let state: any; - -// parser with Uint8Array based transition table -const parserUint = new TestEscapeSequenceParser(VT500_TRANSITION_TABLE); -parserUint.setPrintHandler(testTerminal.print.bind(testTerminal)); -parserUint.setCsiHandlerFallback((collect: string, params: number[], flag: number) => { - testTerminal.actionCSI(collect, params, String.fromCharCode(flag)); -}); -parserUint.setEscHandlerFallback((collect: string, flag: number) => { - testTerminal.actionESC(collect, String.fromCharCode(flag)); -}); -parserUint.setExecuteHandlerFallback((code: number) => { - testTerminal.actionExecute(String.fromCharCode(code)); -}); -parserUint.setOscHandlerFallback((identifier: number, data: string) => { - if (identifier === -1) testTerminal.actionOSC(data); // handle error condition silently - else testTerminal.actionOSC('' + identifier + ';' + data); -}); -parserUint.setDcsHandlerFallback(new DcsTest()); - -// array based transition table -const VT500_TRANSITION_TABLE_ARRAY = new TransitionTable(VT500_TRANSITION_TABLE.table.length); -VT500_TRANSITION_TABLE_ARRAY.table = new Array(VT500_TRANSITION_TABLE.table.length); -for (let i = 0; i < VT500_TRANSITION_TABLE.table.length; ++i) { - VT500_TRANSITION_TABLE_ARRAY.table[i] = VT500_TRANSITION_TABLE.table[i]; -} - -// parser with array based transition table -const parserArray = new TestEscapeSequenceParser(VT500_TRANSITION_TABLE_ARRAY); -parserArray.setPrintHandler(testTerminal.print.bind(testTerminal)); -parserArray.setCsiHandlerFallback((collect: string, params: number[], flag: number) => { - testTerminal.actionCSI(collect, params, String.fromCharCode(flag)); -}); -parserArray.setEscHandlerFallback((collect: string, flag: number) => { - testTerminal.actionESC(collect, String.fromCharCode(flag)); -}); -parserArray.setExecuteHandlerFallback((code: number) => { - testTerminal.actionExecute(String.fromCharCode(code)); -}); -parserArray.setOscHandlerFallback((identifier: number, data: string) => { - if (identifier === -1) testTerminal.actionOSC(data); // handle error condition silently - else testTerminal.actionOSC('' + identifier + ';' + data); -}); -parserArray.setDcsHandlerFallback(new DcsTest()); - -interface IRun { - tableType: string; - parser: TestEscapeSequenceParser; -} - -// translate string based parse calls into typed array based -function parse(parser: TestEscapeSequenceParser, data: string): void { - const container = new Uint32Array(data.length); - const decoder = new StringToUtf32(); - parser.parse(container, decoder.decode(data, container)); -} - -describe('EscapeSequenceParser', function (): void { - let parser: TestEscapeSequenceParser; - const runs: IRun[] = [ - { tableType: 'Uint8Array', parser: parserUint }, - { tableType: 'Array', parser: parserArray } - ]; - runs.forEach(function (run: IRun): void { - describe('Parser init and methods / ' + run.tableType, function (): void { - before(function(): void { - parser = run.parser; - }); - it('constructor', function (): void { - let p: EscapeSequenceParser = new EscapeSequenceParser(); - chai.expect(p.TRANSITIONS).equal(VT500_TRANSITION_TABLE); - p = new EscapeSequenceParser(VT500_TRANSITION_TABLE); - chai.expect(p.TRANSITIONS).equal(VT500_TRANSITION_TABLE); - const tansitions: TransitionTable = new TransitionTable(10); - p = new EscapeSequenceParser(tansitions); - chai.expect(p.TRANSITIONS).equal(tansitions); - }); - it('inital states', function (): void { - chai.expect(parser.initialState).equal(ParserState.GROUND); - chai.expect(parser.currentState).equal(ParserState.GROUND); - chai.expect(parser.osc).equal(''); - chai.expect(parser.params).eql([0]); - chai.expect(parser.collect).equal(''); - }); - it('reset states', function (): void { - parser.currentState = 124; - parser.osc = '#'; - parser.params = [123]; - parser.collect = '#'; - - parser.reset(); - chai.expect(parser.currentState).equal(ParserState.GROUND); - chai.expect(parser.osc).equal(''); - chai.expect(parser.params).eql([0]); - chai.expect(parser.collect).equal(''); - }); - }); - }); - runs.forEach(function (run: IRun): void { - describe('state transitions and actions / ' + run.tableType, function (): void { - before(function(): void { - parser = run.parser; - }); - it('state GROUND execute action', function (): void { - parser.reset(); - testTerminal.clear(); - let exes = r(0x00, 0x18); - exes = exes.concat(['\x19']); - exes = exes.concat(r(0x1c, 0x20)); - for (let i = 0; i < exes.length; ++i) { - parser.currentState = ParserState.GROUND; - parse(parser, exes[i]); - chai.expect(parser.currentState).equal(ParserState.GROUND); - testTerminal.compare([['exe', exes[i]]]); - parser.reset(); - testTerminal.clear(); - } - }); - it('state GROUND print action', function (): void { - parser.reset(); - testTerminal.clear(); - const printables = r(0x20, 0x7f); // NOTE: DEL excluded - for (let i = 0; i < printables.length; ++i) { - parser.currentState = ParserState.GROUND; - parse(parser, printables[i]); - chai.expect(parser.currentState).equal(ParserState.GROUND); - testTerminal.compare([['print', printables[i]]]); - parser.reset(); - testTerminal.clear(); - } - }); - it('trans ANYWHERE --> GROUND with actions', function (): void { - const exes = [ - '\x18', '\x1a', - '\x80', '\x81', '\x82', '\x83', '\x84', '\x85', '\x86', '\x87', '\x88', - '\x89', '\x8a', '\x8b', '\x8c', '\x8d', '\x8e', '\x8f', - '\x91', '\x92', '\x93', '\x94', '\x95', '\x96', '\x97', '\x99', '\x9a' - ]; - const exceptions: { [key: number]: { [key: string]: any[] } } = { - 8: { '\x18': [], '\x1a': [] } // simply abort osc state - }; - parser.reset(); - testTerminal.clear(); - for (state in states) { - for (let i = 0; i < exes.length; ++i) { - parser.currentState = state; - parse(parser, exes[i]); - chai.expect(parser.currentState).equal(ParserState.GROUND); - testTerminal.compare((state in exceptions ? exceptions[state][exes[i]] : 0) || [['exe', exes[i]]]); - parser.reset(); - testTerminal.clear(); - } - parse(parser, '\x9c'); - chai.expect(parser.currentState).equal(ParserState.GROUND); - testTerminal.compare([]); - parser.reset(); - testTerminal.clear(); - } - }); - it('trans ANYWHERE --> ESCAPE with clear', function (): void { - parser.reset(); - for (state in states) { - parser.currentState = state; - parser.osc = '#'; - parser.params = [23]; - parser.collect = '#'; - parse(parser, '\x1b'); - chai.expect(parser.currentState).equal(ParserState.ESCAPE); - chai.expect(parser.osc).equal(''); - chai.expect(parser.params).eql([0]); - chai.expect(parser.collect).equal(''); - parser.reset(); - } - }); - it('state ESCAPE execute rules', function (): void { - parser.reset(); - testTerminal.clear(); - let exes = r(0x00, 0x18); - exes = exes.concat(['\x19']); - exes = exes.concat(r(0x1c, 0x20)); - for (let i = 0; i < exes.length; ++i) { - parser.currentState = ParserState.ESCAPE; - parse(parser, exes[i]); - chai.expect(parser.currentState).equal(ParserState.ESCAPE); - testTerminal.compare([['exe', exes[i]]]); - parser.reset(); - testTerminal.clear(); - } - }); - it('state ESCAPE ignore', function (): void { - parser.reset(); - testTerminal.clear(); - parser.currentState = ParserState.ESCAPE; - parse(parser, '\x7f'); - chai.expect(parser.currentState).equal(ParserState.ESCAPE); - testTerminal.compare([]); - parser.reset(); - testTerminal.clear(); - }); - it('trans ESCAPE --> GROUND with ecs_dispatch action', function (): void { - parser.reset(); - testTerminal.clear(); - let dispatches = r(0x30, 0x50); - dispatches = dispatches.concat(r(0x51, 0x58)); - dispatches = dispatches.concat(['\x59', '\x5a']); // excluded \x5c - dispatches = dispatches.concat(r(0x60, 0x7f)); - for (let i = 0; i < dispatches.length; ++i) { - parser.currentState = ParserState.ESCAPE; - parse(parser, dispatches[i]); - chai.expect(parser.currentState).equal(ParserState.GROUND); - testTerminal.compare([['esc', '', dispatches[i]]]); - parser.reset(); - testTerminal.clear(); - } - }); - it('trans ESCAPE --> ESCAPE_INTERMEDIATE with collect action', function (): void { - parser.reset(); - const collect = r(0x20, 0x30); - for (let i = 0; i < collect.length; ++i) { - parser.currentState = ParserState.ESCAPE; - parse(parser, collect[i]); - chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); - chai.expect(parser.collect).equal(collect[i]); - parser.reset(); - } - }); - it('state ESCAPE_INTERMEDIATE execute rules', function (): void { - parser.reset(); - testTerminal.clear(); - let exes = r(0x00, 0x18); - exes = exes.concat(['\x19']); - exes = exes.concat(r(0x1c, 0x20)); - for (let i = 0; i < exes.length; ++i) { - parser.currentState = ParserState.ESCAPE_INTERMEDIATE; - parse(parser, exes[i]); - chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); - testTerminal.compare([['exe', exes[i]]]); - parser.reset(); - testTerminal.clear(); - } - }); - it('state ESCAPE_INTERMEDIATE ignore', function (): void { - parser.reset(); - testTerminal.clear(); - parser.currentState = ParserState.ESCAPE_INTERMEDIATE; - parse(parser, '\x7f'); - chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); - testTerminal.compare([]); - parser.reset(); - testTerminal.clear(); - }); - it('state ESCAPE_INTERMEDIATE collect action', function (): void { - parser.reset(); - const collect = r(0x20, 0x30); - for (let i = 0; i < collect.length; ++i) { - parser.currentState = ParserState.ESCAPE_INTERMEDIATE; - parse(parser, collect[i]); - chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); - chai.expect(parser.collect).equal(collect[i]); - parser.reset(); - } - }); - it('trans ESCAPE_INTERMEDIATE --> GROUND with esc_dispatch action', function (): void { - parser.reset(); - testTerminal.clear(); - const collect = r(0x30, 0x7f); - for (let i = 0; i < collect.length; ++i) { - parser.currentState = ParserState.ESCAPE_INTERMEDIATE; - parse(parser, collect[i]); - chai.expect(parser.currentState).equal(ParserState.GROUND); - // '\x5c' --> ESC + \ (7bit ST) parser does not expose this as it already got handled - testTerminal.compare((collect[i] === '\x5c') ? [] : [['esc', '', collect[i]]]); - parser.reset(); - testTerminal.clear(); - } - }); - it('trans ANYWHERE/ESCAPE --> CSI_ENTRY with clear', function (): void { - parser.reset(); - // C0 - parser.currentState = ParserState.ESCAPE; - parser.osc = '#'; - parser.params = [123]; - parser.collect = '#'; - parse(parser, '['); - chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); - chai.expect(parser.osc).equal(''); - chai.expect(parser.params).eql([0]); - chai.expect(parser.collect).equal(''); - parser.reset(); - // C1 - for (state in states) { - parser.currentState = state; - parser.osc = '#'; - parser.params = [123]; - parser.collect = '#'; - parse(parser, '\x9b'); - chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); - chai.expect(parser.osc).equal(''); - chai.expect(parser.params).eql([0]); - chai.expect(parser.collect).equal(''); - parser.reset(); - } - }); - it('state CSI_ENTRY execute rules', function (): void { - parser.reset(); - testTerminal.clear(); - let exes = r(0x00, 0x18); - exes = exes.concat(['\x19']); - exes = exes.concat(r(0x1c, 0x20)); - for (let i = 0; i < exes.length; ++i) { - parser.currentState = ParserState.CSI_ENTRY; - parse(parser, exes[i]); - chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); - testTerminal.compare([['exe', exes[i]]]); - parser.reset(); - testTerminal.clear(); - } - }); - it('state CSI_ENTRY ignore', function (): void { - parser.reset(); - testTerminal.clear(); - parser.currentState = ParserState.CSI_ENTRY; - parse(parser, '\x7f'); - chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); - testTerminal.compare([]); - parser.reset(); - testTerminal.clear(); - }); - it('trans CSI_ENTRY --> GROUND with csi_dispatch action', function (): void { - parser.reset(); - const dispatches = r(0x40, 0x7f); - for (let i = 0; i < dispatches.length; ++i) { - parser.currentState = ParserState.CSI_ENTRY; - parse(parser, dispatches[i]); - chai.expect(parser.currentState).equal(ParserState.GROUND); - testTerminal.compare([['csi', '', [0], dispatches[i]]]); - parser.reset(); - testTerminal.clear(); - } - }); - it('trans CSI_ENTRY --> CSI_PARAM with param/collect actions', function (): void { - parser.reset(); - const params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; - const collect = ['\x3c', '\x3d', '\x3e', '\x3f']; - for (let i = 0; i < params.length; ++i) { - parser.currentState = ParserState.CSI_ENTRY; - parse(parser, params[i]); - chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); - chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); - parser.reset(); - } - parser.currentState = ParserState.CSI_ENTRY; - parse(parser, '\x3b'); - chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); - chai.expect(parser.params).eql([0, 0]); - parser.reset(); - for (let i = 0; i < collect.length; ++i) { - parser.currentState = ParserState.CSI_ENTRY; - parse(parser, collect[i]); - chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); - chai.expect(parser.collect).equal(collect[i]); - parser.reset(); - } - }); - it('state CSI_PARAM execute rules', function (): void { - parser.reset(); - testTerminal.clear(); - let exes = r(0x00, 0x18); - exes = exes.concat(['\x19']); - exes = exes.concat(r(0x1c, 0x20)); - for (let i = 0; i < exes.length; ++i) { - parser.currentState = ParserState.CSI_PARAM; - parse(parser, exes[i]); - chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); - testTerminal.compare([['exe', exes[i]]]); - parser.reset(); - testTerminal.clear(); - } - }); - it('state CSI_PARAM param action', function (): void { - parser.reset(); - const params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; - for (let i = 0; i < params.length; ++i) { - parser.currentState = ParserState.CSI_PARAM; - parse(parser, params[i]); - chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); - chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); - parser.reset(); - } - parser.currentState = ParserState.CSI_PARAM; - parse(parser, '\x3b'); - chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); - chai.expect(parser.params).eql([0, 0]); - parser.reset(); - }); - it('state CSI_PARAM ignore', function (): void { - parser.reset(); - testTerminal.clear(); - parser.currentState = ParserState.CSI_PARAM; - parse(parser, '\x7f'); - chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); - testTerminal.compare([]); - parser.reset(); - testTerminal.clear(); - }); - it('trans CSI_PARAM --> GROUND with csi_dispatch action', function (): void { - parser.reset(); - const dispatches = r(0x40, 0x7f); - for (let i = 0; i < dispatches.length; ++i) { - parser.currentState = ParserState.CSI_PARAM; - parser.params = [0, 1]; - parse(parser, dispatches[i]); - chai.expect(parser.currentState).equal(ParserState.GROUND); - testTerminal.compare([['csi', '', [0, 1], dispatches[i]]]); - parser.reset(); - testTerminal.clear(); - } - }); - it('trans CSI_ENTRY --> CSI_INTERMEDIATE with collect action', function (): void { - parser.reset(); - const collect = r(0x20, 0x30); - for (let i = 0; i < collect.length; ++i) { - parser.currentState = ParserState.CSI_ENTRY; - parse(parser, collect[i]); - chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); - chai.expect(parser.collect).equal(collect[i]); - parser.reset(); - } - }); - it('trans CSI_PARAM --> CSI_INTERMEDIATE with collect action', function (): void { - parser.reset(); - const collect = r(0x20, 0x30); - for (let i = 0; i < collect.length; ++i) { - parser.currentState = ParserState.CSI_PARAM; - parse(parser, collect[i]); - chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); - chai.expect(parser.collect).equal(collect[i]); - parser.reset(); - } - }); - it('state CSI_INTERMEDIATE execute rules', function (): void { - parser.reset(); - testTerminal.clear(); - let exes = r(0x00, 0x18); - exes = exes.concat(['\x19']); - exes = exes.concat(r(0x1c, 0x20)); - for (let i = 0; i < exes.length; ++i) { - parser.currentState = ParserState.CSI_INTERMEDIATE; - parse(parser, exes[i]); - chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); - testTerminal.compare([['exe', exes[i]]]); - parser.reset(); - testTerminal.clear(); - } - }); - it('state CSI_INTERMEDIATE collect', function (): void { - parser.reset(); - const collect = r(0x20, 0x30); - for (let i = 0; i < collect.length; ++i) { - parser.currentState = ParserState.CSI_INTERMEDIATE; - parse(parser, collect[i]); - chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); - chai.expect(parser.collect).equal(collect[i]); - parser.reset(); - } - }); - it('state CSI_INTERMEDIATE ignore', function (): void { - parser.reset(); - testTerminal.clear(); - parser.currentState = ParserState.CSI_INTERMEDIATE; - parse(parser, '\x7f'); - chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); - testTerminal.compare([]); - parser.reset(); - testTerminal.clear(); - }); - it('trans CSI_INTERMEDIATE --> GROUND with csi_dispatch action', function (): void { - parser.reset(); - const dispatches = r(0x40, 0x7f); - for (let i = 0; i < dispatches.length; ++i) { - parser.currentState = ParserState.CSI_INTERMEDIATE; - parser.params = [0, 1]; - parse(parser, dispatches[i]); - chai.expect(parser.currentState).equal(ParserState.GROUND); - testTerminal.compare([['csi', '', [0, 1], dispatches[i]]]); - parser.reset(); - testTerminal.clear(); - } - }); - it('trans CSI_ENTRY --> CSI_IGNORE', function (): void { - parser.reset(); - parser.currentState = ParserState.CSI_ENTRY; - parse(parser, '\x3a'); - chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); - parser.reset(); - }); - it('trans CSI_PARAM --> CSI_IGNORE', function (): void { - parser.reset(); - const chars = ['\x3a', '\x3c', '\x3d', '\x3e', '\x3f']; - for (let i = 0; i < chars.length; ++i) { - parser.currentState = ParserState.CSI_PARAM; - parse(parser, '\x3b' + chars[i]); - chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); - chai.expect(parser.params).eql([0, 0]); - parser.reset(); - } - }); - it('trans CSI_INTERMEDIATE --> CSI_IGNORE', function (): void { - parser.reset(); - const chars = r(0x30, 0x40); - for (let i = 0; i < chars.length; ++i) { - parser.currentState = ParserState.CSI_INTERMEDIATE; - parse(parser, chars[i]); - chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); - chai.expect(parser.params).eql([0]); - parser.reset(); - } - }); - it('state CSI_IGNORE execute rules', function (): void { - parser.reset(); - testTerminal.clear(); - let exes = r(0x00, 0x18); - exes = exes.concat(['\x19']); - exes = exes.concat(r(0x1c, 0x20)); - for (let i = 0; i < exes.length; ++i) { - parser.currentState = ParserState.CSI_IGNORE; - parse(parser, exes[i]); - chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); - testTerminal.compare([['exe', exes[i]]]); - parser.reset(); - testTerminal.clear(); - } - }); - it('state CSI_IGNORE ignore', function (): void { - parser.reset(); - testTerminal.clear(); - let ignored = r(0x20, 0x40); - ignored = ignored.concat(['\x7f']); - for (let i = 0; i < ignored.length; ++i) { - parser.currentState = ParserState.CSI_IGNORE; - parse(parser, ignored[i]); - chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); - testTerminal.compare([]); - parser.reset(); - testTerminal.clear(); - } - }); - it('trans CSI_IGNORE --> GROUND', function (): void { - parser.reset(); - const dispatches = r(0x40, 0x7f); - for (let i = 0; i < dispatches.length; ++i) { - parser.currentState = ParserState.CSI_IGNORE; - parser.params = [0, 1]; - parse(parser, dispatches[i]); - chai.expect(parser.currentState).equal(ParserState.GROUND); - testTerminal.compare([]); - parser.reset(); - testTerminal.clear(); - } - }); - it('trans ANYWHERE/ESCAPE --> SOS_PM_APC_STRING', function (): void { - parser.reset(); - // C0 - let initializers = ['\x58', '\x5e', '\x5f']; - for (let i = 0; i < initializers.length; ++i) { - parse(parser, '\x1b' + initializers[i]); - chai.expect(parser.currentState).equal(ParserState.SOS_PM_APC_STRING); - parser.reset(); - } - // C1 - for (state in states) { - parser.currentState = state; - initializers = ['\x98', '\x9e', '\x9f']; - for (let i = 0; i < initializers.length; ++i) { - parse(parser, initializers[i]); - chai.expect(parser.currentState).equal(ParserState.SOS_PM_APC_STRING); - parser.reset(); - } - } - }); - it('state SOS_PM_APC_STRING ignore rules', function (): void { - parser.reset(); - let ignored = r(0x00, 0x18); - ignored = ignored.concat(['\x19']); - ignored = ignored.concat(r(0x1c, 0x20)); - ignored = ignored.concat(r(0x20, 0x80)); - for (let i = 0; i < ignored.length; ++i) { - parser.currentState = ParserState.SOS_PM_APC_STRING; - parse(parser, ignored[i]); - chai.expect(parser.currentState).equal(ParserState.SOS_PM_APC_STRING); - parser.reset(); - } - }); - it('trans ANYWHERE/ESCAPE --> OSC_STRING', function (): void { - parser.reset(); - // C0 - parse(parser, '\x1b]'); - chai.expect(parser.currentState).equal(ParserState.OSC_STRING); - parser.reset(); - // C1 - for (state in states) { - parser.currentState = state; - parse(parser, '\x9d'); - chai.expect(parser.currentState).equal(ParserState.OSC_STRING); - parser.reset(); - } - }); - it('state OSC_STRING ignore rules', function (): void { - parser.reset(); - const ignored = [ - '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', /*'\x07',*/ '\x08', - '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', - '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f']; - for (let i = 0; i < ignored.length; ++i) { - parser.currentState = ParserState.OSC_STRING; - parse(parser, ignored[i]); - chai.expect(parser.currentState).equal(ParserState.OSC_STRING); - chai.expect(parser.osc).equal(''); - parser.reset(); - } - }); - it('state OSC_STRING put action', function (): void { - parser.reset(); - const puts = r(0x20, 0x80); - for (let i = 0; i < puts.length; ++i) { - parser.currentState = ParserState.OSC_STRING; - parse(parser, puts[i]); - chai.expect(parser.currentState).equal(ParserState.OSC_STRING); - chai.expect(parser.osc).equal(puts[i]); - parser.reset(); - } - }); - it('state DCS_ENTRY', function (): void { - parser.reset(); - // C0 - parse(parser, '\x1bP'); - chai.expect(parser.currentState).equal(ParserState.DCS_ENTRY); - parser.reset(); - // C1 - for (state in states) { - parser.currentState = state; - parse(parser, '\x90'); - chai.expect(parser.currentState).equal(ParserState.DCS_ENTRY); - parser.reset(); - } - }); - it('state DCS_ENTRY ignore rules', function (): void { - parser.reset(); - const ignored = [ - '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', - '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', - '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; - for (let i = 0; i < ignored.length; ++i) { - parser.currentState = ParserState.DCS_ENTRY; - parse(parser, ignored[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_ENTRY); - parser.reset(); - } - }); - it('state DCS_ENTRY --> DCS_PARAM with param/collect actions', function (): void { - parser.reset(); - const params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; - const collect = ['\x3c', '\x3d', '\x3e', '\x3f']; - for (let i = 0; i < params.length; ++i) { - parser.currentState = ParserState.DCS_ENTRY; - parse(parser, params[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); - chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); - parser.reset(); - } - parser.currentState = ParserState.DCS_ENTRY; - parse(parser, '\x3b'); - chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); - chai.expect(parser.params).eql([0, 0]); - parser.reset(); - for (let i = 0; i < collect.length; ++i) { - parser.currentState = ParserState.DCS_ENTRY; - parse(parser, collect[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); - chai.expect(parser.collect).equal(collect[i]); - parser.reset(); - } - }); - it('state DCS_PARAM ignore rules', function (): void { - parser.reset(); - const ignored = [ - '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', - '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', - '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; - for (let i = 0; i < ignored.length; ++i) { - parser.currentState = ParserState.DCS_PARAM; - parse(parser, ignored[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); - parser.reset(); - } - }); - it('state DCS_PARAM param action', function (): void { - parser.reset(); - const params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; - for (let i = 0; i < params.length; ++i) { - parser.currentState = ParserState.DCS_PARAM; - parse(parser, params[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); - chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); - parser.reset(); - } - parser.currentState = ParserState.DCS_PARAM; - parse(parser, '\x3b'); - chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); - chai.expect(parser.params).eql([0, 0]); - parser.reset(); - }); - it('trans DCS_ENTRY --> DCS_IGNORE', function (): void { - parser.reset(); - parser.currentState = ParserState.DCS_ENTRY; - parse(parser, '\x3a'); - chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); - parser.reset(); - }); - it('trans DCS_PARAM --> DCS_IGNORE', function (): void { - parser.reset(); - const chars = ['\x3a', '\x3c', '\x3d', '\x3e', '\x3f']; - for (let i = 0; i < chars.length; ++i) { - parser.currentState = ParserState.DCS_PARAM; - parse(parser, '\x3b' + chars[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); - chai.expect(parser.params).eql([0, 0]); - parser.reset(); - } - }); - it('trans DCS_INTERMEDIATE --> DCS_IGNORE', function (): void { - parser.reset(); - const chars = r(0x30, 0x40); - for (let i = 0; i < chars.length; ++i) { - parser.currentState = ParserState.DCS_INTERMEDIATE; - parse(parser, chars[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); - parser.reset(); - } - }); - it('state DCS_IGNORE ignore rules', function (): void { - parser.reset(); - let ignored = [ - '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', - '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', - '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; - ignored = ignored.concat(r(0x20, 0x80)); - for (let i = 0; i < ignored.length; ++i) { - parser.currentState = ParserState.DCS_IGNORE; - parse(parser, ignored[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); - parser.reset(); - } - }); - it('trans DCS_ENTRY --> DCS_INTERMEDIATE with collect action', function (): void { - parser.reset(); - const collect = r(0x20, 0x30); - for (let i = 0; i < collect.length; ++i) { - parser.currentState = ParserState.DCS_ENTRY; - parse(parser, collect[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); - chai.expect(parser.collect).equal(collect[i]); - parser.reset(); - } - }); - it('trans DCS_PARAM --> DCS_INTERMEDIATE with collect action', function (): void { - parser.reset(); - const collect = r(0x20, 0x30); - for (let i = 0; i < collect.length; ++i) { - parser.currentState = ParserState.DCS_PARAM; - parse(parser, collect[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); - chai.expect(parser.collect).equal(collect[i]); - parser.reset(); - } - }); - it('state DCS_INTERMEDIATE ignore rules', function (): void { - parser.reset(); - const ignored = [ - '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', - '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', - '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; - for (let i = 0; i < ignored.length; ++i) { - parser.currentState = ParserState.DCS_INTERMEDIATE; - parse(parser, ignored[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); - parser.reset(); - } - }); - it('state DCS_INTERMEDIATE collect action', function (): void { - parser.reset(); - const collect = r(0x20, 0x30); - for (let i = 0; i < collect.length; ++i) { - parser.currentState = ParserState.DCS_INTERMEDIATE; - parse(parser, collect[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); - chai.expect(parser.collect).equal(collect[i]); - parser.reset(); - } - }); - it('trans DCS_INTERMEDIATE --> DCS_IGNORE', function (): void { - parser.reset(); - const chars = r(0x30, 0x40); - for (let i = 0; i < chars.length; ++i) { - parser.currentState = ParserState.DCS_INTERMEDIATE; - parse(parser, '\x20' + chars[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); - chai.expect(parser.collect).equal('\x20'); - parser.reset(); - } - }); - it('trans DCS_ENTRY --> DCS_PASSTHROUGH with hook', function (): void { - parser.reset(); - testTerminal.clear(); - const collect = r(0x40, 0x7f); - for (let i = 0; i < collect.length; ++i) { - parser.currentState = ParserState.DCS_ENTRY; - parse(parser, collect[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); - testTerminal.compare([['dcs hook', '', [0], collect[i]]]); - parser.reset(); - testTerminal.clear(); - } - }); - it('trans DCS_PARAM --> DCS_PASSTHROUGH with hook', function (): void { - parser.reset(); - testTerminal.clear(); - const collect = r(0x40, 0x7f); - for (let i = 0; i < collect.length; ++i) { - parser.currentState = ParserState.DCS_PARAM; - parse(parser, collect[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); - testTerminal.compare([['dcs hook', '', [0], collect[i]]]); - parser.reset(); - testTerminal.clear(); - } - }); - it('trans DCS_INTERMEDIATE --> DCS_PASSTHROUGH with hook', function (): void { - parser.reset(); - testTerminal.clear(); - const collect = r(0x40, 0x7f); - for (let i = 0; i < collect.length; ++i) { - parser.currentState = ParserState.DCS_INTERMEDIATE; - parse(parser, collect[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); - testTerminal.compare([['dcs hook', '', [0], collect[i]]]); - parser.reset(); - testTerminal.clear(); - } - }); - it('state DCS_PASSTHROUGH put action', function (): void { - parser.reset(); - testTerminal.clear(); - let puts = r(0x00, 0x18); - puts = puts.concat(['\x19']); - puts = puts.concat(r(0x1c, 0x20)); - puts = puts.concat(r(0x20, 0x7f)); - for (let i = 0; i < puts.length; ++i) { - parser.currentState = ParserState.DCS_PASSTHROUGH; - parser.mockActiveDcsHandler(); - parse(parser, puts[i]); - chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); - testTerminal.compare([['dcs put', puts[i]]]); - parser.reset(); - testTerminal.clear(); - } - }); - it('state DCS_PASSTHROUGH ignore', function (): void { - parser.reset(); - testTerminal.clear(); - parser.currentState = ParserState.DCS_PASSTHROUGH; - parse(parser, '\x7f'); - chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); - testTerminal.compare([]); - parser.reset(); - testTerminal.clear(); - }); - }); - }); - - runs.forEach(function (run: IRun): void { - let test: Function; - describe('escape sequence examples / ' + run.tableType, function (): void { - before(function(): void { - parser = run.parser; - test = function(s: string, value: any, noReset: any): void { - if (!noReset) { - parser.reset(); - testTerminal.clear(); - } - parse(parser, s); - testTerminal.compare(value); - }; - }); - it('CSI with print and execute', function (): void { - test('\x1b[<31;5mHello World! öäü€\nabc', - [ - ['csi', '<', [31, 5], 'm'], - ['print', 'Hello World! öäü€'], - ['exe', '\n'], - ['print', 'abc'] - ], null); - }); - it('OSC', function (): void { - test('\x1b]0;abc123€öäü\x07', [ - ['osc', '0;abc123€öäü'] - ], null); - }); - it('single DCS', function (): void { - test('\x1bP1;2;3+$abc;de\x9c', [ - ['dcs hook', '+$', [1, 2, 3], 'a'], - ['dcs put', 'bc;de'], - ['dcs unhook'] - ], null); - }); - it('multi DCS', function (): void { - test('\x1bP1;2;3+$abc;de', [ - ['dcs hook', '+$', [1, 2, 3], 'a'], - ['dcs put', 'bc;de'] - ], null); - testTerminal.clear(); - test('abc\x9c', [ - ['dcs put', 'abc'], - ['dcs unhook'] - ], true); - }); - it('print + DCS(C1)', function (): void { - test('abc\x901;2;3+$abc;de\x9c', [ - ['print', 'abc'], - ['dcs hook', '+$', [1, 2, 3], 'a'], - ['dcs put', 'bc;de'], - ['dcs unhook'] - ], null); - }); - it('print + PM(C1) + print', function (): void { - test('abc\x98123tzf\x9cdefg', [ - ['print', 'abc'], - ['print', 'defg'] - ], null); - }); - it('print + OSC(C1) + print', function (): void { - test('abc\x9d123tzf\x9cdefg', [ - ['print', 'abc'], - ['osc', '123tzf'], - ['print', 'defg'] - ], null); - }); - it('error recovery', function (): void { - test('\x1b[1€abcdefg\x9b<;c', [ - ['print', 'abcdefg'], - ['csi', '<', [0, 0], 'c'] - ], null); - }); - it('7bit ST should be swallowed', function(): void { - test('abc\x9d123tzf\x1b\\defg', [ - ['print', 'abc'], - ['osc', '123tzf'], - ['print', 'defg'] - ], null); - }); - }); - }); - - describe('coverage tests', function (): void { - it('CSI_IGNORE error', function (): void { - parser.reset(); - testTerminal.clear(); - parser.currentState = ParserState.CSI_IGNORE; - parse(parser, '€öäü'); - chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); - testTerminal.compare([]); - parser.reset(); - testTerminal.clear(); - }); - it('DCS_IGNORE error', function (): void { - parser.reset(); - testTerminal.clear(); - parser.currentState = ParserState.DCS_IGNORE; - parse(parser, '€öäü'); - chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); - testTerminal.compare([]); - parser.reset(); - testTerminal.clear(); - }); - it('DCS_PASSTHROUGH error', function (): void { - parser.reset(); - testTerminal.clear(); - parser.currentState = ParserState.DCS_PASSTHROUGH; - parse(parser, '\x901;2;3+$a€öäü'); - chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); - testTerminal.compare([['dcs hook', '+$', [1, 2, 3], 'a'], ['dcs put', '€öäü']]); - parser.reset(); - testTerminal.clear(); - }); - it('error else of if (code > 159)', function (): void { - parser.reset(); - testTerminal.clear(); - parser.currentState = ParserState.GROUND; - parse(parser, '\x9c'); - chai.expect(parser.currentState).equal(ParserState.GROUND); - testTerminal.compare([]); - parser.reset(); - testTerminal.clear(); - }); - }); - - describe('set/clear handler', function (): void { - const INPUT = '\x1b[1;31mhello \x1b%Gwor\x1bEld!\x1b[0m\r\n$>\x1b]1;foo=bar\x1b\\'; - let parser2: TestEscapeSequenceParser; - let print = ''; - const esc: string[] = []; - const csi: [string, number[], string][] = []; - const exe: string[] = []; - const osc: [number, string][] = []; - const dcs: ([string] | [string, string] | [string, string, number[], number])[] = []; - function clearAccu(): void { - print = ''; - esc.length = 0; - csi.length = 0; - exe.length = 0; - osc.length = 0; - dcs.length = 0; - } - beforeEach(function (): void { - parser2 = new TestEscapeSequenceParser(); - clearAccu(); - }); - it('print handler', function (): void { - parser2.setPrintHandler(function (data: Uint32Array, start: number, end: number): void { - for (let i = start; i < end; ++i) { - print += stringFromCodePoint(data[i]); - } - }); - parse(parser2, INPUT); - chai.expect(print).equal('hello world!$>'); - parser2.clearPrintHandler(); - parser2.clearPrintHandler(); // should not throw - clearAccu(); - parse(parser2, INPUT); - chai.expect(print).equal(''); - }); - it('ESC handler', function (): void { - parser2.setEscHandler('%G', function (): void { - esc.push('%G'); - }); - parser2.setEscHandler('E', function (): void { - esc.push('E'); - }); - parse(parser2, INPUT); - chai.expect(esc).eql(['%G', 'E']); - parser2.clearEscHandler('%G'); - parser2.clearEscHandler('%G'); // should not throw - clearAccu(); - parse(parser2, INPUT); - chai.expect(esc).eql(['E']); - parser2.clearEscHandler('E'); - clearAccu(); - parse(parser2, INPUT); - chai.expect(esc).eql([]); - }); - it('CSI handler', function (): void { - parser2.setCsiHandler('m', function (params: number[], collect: string): void { - csi.push(['m', params, collect]); - }); - parse(parser2, INPUT); - chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); - parser2.clearCsiHandler('m'); - parser2.clearCsiHandler('m'); // should not throw - clearAccu(); - parse(parser2, INPUT); - chai.expect(csi).eql([]); - }); - describe('CSI custom handlers', () => { - it('Prevent fallback', () => { - const csiCustom: [string, number[], string][] = []; - parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); - parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); - parse(parser2, INPUT); - chai.expect(csi).eql([], 'Should not fallback to original handler'); - chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); - }); - it('Allow fallback', () => { - const csiCustom: [string, number[], string][] = []; - parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); - parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return false; }); - parse(parser2, INPUT); - chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']], 'Should fallback to original handler'); - chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); - }); - it('Multiple custom handlers fallback once', () => { - const csiCustom: [string, number[], string][] = []; - const csiCustom2: [string, number[], string][] = []; - parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); - parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); - parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params, collect]); return false; }); - parse(parser2, INPUT); - chai.expect(csi).eql([], 'Should not fallback to original handler'); - chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); - chai.expect(csiCustom2).eql([['m', [1, 31], ''], ['m', [0], '']]); - }); - it('Multiple custom handlers no fallback', () => { - const csiCustom: [string, number[], string][] = []; - const csiCustom2: [string, number[], string][] = []; - parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); - parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); - parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params, collect]); return true; }); - parse(parser2, INPUT); - chai.expect(csi).eql([], 'Should not fallback to original handler'); - chai.expect(csiCustom).eql([], 'Should not fallback once'); - chai.expect(csiCustom2).eql([['m', [1, 31], ''], ['m', [0], '']]); - }); - it('Execution order should go from latest handler down to the original', () => { - const order: number[] = []; - parser2.setCsiHandler('m', () => order.push(1)); - parser2.addCsiHandler('m', () => { order.push(2); return false; }); - parser2.addCsiHandler('m', () => { order.push(3); return false; }); - parse(parser2, '\x1b[0m'); - chai.expect(order).eql([3, 2, 1]); - }); - it('Dispose should work', () => { - const csiCustom: [string, number[], string][] = []; - parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); - const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); - customHandler.dispose(); - parse(parser2, INPUT); - chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); - chai.expect(csiCustom).eql([], 'Should not use custom handler as it was disposed'); - }); - it('Should not corrupt the parser when dispose is called twice', () => { - const csiCustom: [string, number[], string][] = []; - parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); - const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); - customHandler.dispose(); - customHandler.dispose(); - parse(parser2, INPUT); - chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); - chai.expect(csiCustom).eql([], 'Should not use custom handler as it was disposed'); - }); - }); - it('EXECUTE handler', function (): void { - parser2.setExecuteHandler('\n', function (): void { - exe.push('\n'); - }); - parser2.setExecuteHandler('\r', function (): void { - exe.push('\r'); - }); - parse(parser2, INPUT); - chai.expect(exe).eql(['\r', '\n']); - parser2.clearExecuteHandler('\r'); - parser2.clearExecuteHandler('\r'); // should not throw - clearAccu(); - parse(parser2, INPUT); - chai.expect(exe).eql(['\n']); - }); - it('OSC handler', function (): void { - parser2.setOscHandler(1, function (data: string): void { - osc.push([1, data]); - }); - parse(parser2, INPUT); - chai.expect(osc).eql([[1, 'foo=bar']]); - parser2.clearOscHandler(1); - parser2.clearOscHandler(1); // should not throw - clearAccu(); - parse(parser2, INPUT); - chai.expect(osc).eql([]); - }); - describe('OSC custom handlers', () => { - it('Prevent fallback', () => { - const oscCustom: [number, string][] = []; - parser2.setOscHandler(1, data => osc.push([1, data])); - parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); - parse(parser2, INPUT); - chai.expect(osc).eql([], 'Should not fallback to original handler'); - chai.expect(oscCustom).eql([[1, 'foo=bar']]); - }); - it('Allow fallback', () => { - const oscCustom: [number, string][] = []; - parser2.setOscHandler(1, data => osc.push([1, data])); - parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return false; }); - parse(parser2, INPUT); - chai.expect(osc).eql([[1, 'foo=bar']], 'Should fallback to original handler'); - chai.expect(oscCustom).eql([[1, 'foo=bar']]); - }); - it('Multiple custom handlers fallback once', () => { - const oscCustom: [number, string][] = []; - const oscCustom2: [number, string][] = []; - parser2.setOscHandler(1, data => osc.push([1, data])); - parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); - parser2.addOscHandler(1, data => { oscCustom2.push([1, data]); return false; }); - parse(parser2, INPUT); - chai.expect(osc).eql([], 'Should not fallback to original handler'); - chai.expect(oscCustom).eql([[1, 'foo=bar']]); - chai.expect(oscCustom2).eql([[1, 'foo=bar']]); - }); - it('Multiple custom handlers no fallback', () => { - const oscCustom: [number, string][] = []; - const oscCustom2: [number, string][] = []; - parser2.setOscHandler(1, data => osc.push([1, data])); - parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); - parser2.addOscHandler(1, data => { oscCustom2.push([1, data]); return true; }); - parse(parser2, INPUT); - chai.expect(osc).eql([], 'Should not fallback to original handler'); - chai.expect(oscCustom).eql([], 'Should not fallback once'); - chai.expect(oscCustom2).eql([[1, 'foo=bar']]); - }); - it('Execution order should go from latest handler down to the original', () => { - const order: number[] = []; - parser2.setOscHandler(1, () => order.push(1)); - parser2.addOscHandler(1, () => { order.push(2); return false; }); - parser2.addOscHandler(1, () => { order.push(3); return false; }); - parse(parser2, '\x1b]1;foo=bar\x1b\\'); - chai.expect(order).eql([3, 2, 1]); - }); - it('Dispose should work', () => { - const oscCustom: [number, string][] = []; - parser2.setOscHandler(1, data => osc.push([1, data])); - const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); - customHandler.dispose(); - parse(parser2, INPUT); - chai.expect(osc).eql([[1, 'foo=bar']]); - chai.expect(oscCustom).eql([], 'Should not use custom handler as it was disposed'); - }); - it('Should not corrupt the parser when dispose is called twice', () => { - const oscCustom: [number, string][] = []; - parser2.setOscHandler(1, data => osc.push([1, data])); - const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); - customHandler.dispose(); - customHandler.dispose(); - parse(parser2, INPUT); - chai.expect(osc).eql([[1, 'foo=bar']]); - chai.expect(oscCustom).eql([], 'Should not use custom handler as it was disposed'); - }); - }); - it('DCS handler', function (): void { - parser2.setDcsHandler('+p', { - hook: function (collect: string, params: number[], flag: number): void { - dcs.push(['hook', collect, params, flag]); - }, - put: function (data: Uint32Array, start: number, end: number): void { - let s = ''; - for (let i = start; i < end; ++i) { - s += stringFromCodePoint(data[i]); - } - dcs.push(['put', s]); - }, - unhook: function (): void { - dcs.push(['unhook']); - } - }); - parse(parser2, '\x1bP1;2;3+pabc'); - parse(parser2, ';de\x9c'); - chai.expect(dcs).eql([ - ['hook', '+', [1, 2, 3], 'p'.charCodeAt(0)], - ['put', 'abc'], ['put', ';de'], - ['unhook'] - ]); - parser2.clearDcsHandler('+p'); - parser2.clearDcsHandler('+p'); // should not throw - clearAccu(); - parse(parser2, '\x1bP1;2;3+pabc'); - parse(parser2, ';de\x9c'); - chai.expect(dcs).eql([]); - }); - it('ERROR handler', function (): void { - let errorState: IParsingState | null = null; - parser2.setErrorHandler(function (state: IParsingState): IParsingState { - errorState = state; - return state; - }); - parse(parser2, '\x1b[1;2;€;3m'); // faulty escape sequence - chai.expect(errorState).eql({ - position: 6, - code: '€'.charCodeAt(0), - currentState: ParserState.CSI_PARAM, - print: -1, - dcs: -1, - osc: '', - collect: '', - params: [1, 2, 0], // extra zero here - abort: false - }); - parser2.clearErrorHandler(); - parser2.clearErrorHandler(); // should not throw - errorState = null; - parse(parser2, '\x1b[1;2;a;3m'); - chai.expect(errorState).eql(null); - }); - }); - // TODO: error conditions and error recovery (not implemented yet in parser) -}); diff --git a/src/core/tsconfig.json b/src/core/tsconfig.json deleted file mode 100644 index 5eeac7bc6..000000000 --- a/src/core/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "extends": "../tsconfig-library-base", - "compilerOptions": { - "outDir": "../../out", - "types": [ - "../../node_modules/@types/mocha" - ], - "baseUrl": "..", - "paths": { - "common/*": [ "./common/*" ] - } - }, - "include": [ "./**/*" ], - "references": [ - { "path": "../common" } - ] -} diff --git a/src/handlers/AltClickHandler.ts b/src/handlers/AltClickHandler.ts index 43c26498d..506e6ee18 100644 --- a/src/handlers/AltClickHandler.ts +++ b/src/handlers/AltClickHandler.ts @@ -4,8 +4,7 @@ */ import { ITerminal } from '../Types'; -import { IBufferLine } from 'core/Types'; -import { ICircularList } from 'common/Types'; +import { IBufferLine, ICircularList } from 'common/Types'; import { C0 } from 'common/data/EscapeSequences'; const enum Direction { @@ -33,7 +32,6 @@ export class AltClickHandler { const coordinates = this._terminal.mouseHelper.getCoords( this._mouseEvent, this._terminal.element, - this._terminal.charMeasure, this._terminal.cols, this._terminal.rows, false diff --git a/src/public/Terminal.test.ts b/src/public/Terminal.test.ts deleted file mode 100644 index 6bad5b044..000000000 --- a/src/public/Terminal.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2016 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { assert } from 'chai'; -import { Terminal } from './Terminal'; -import * as attach from '../addons/attach/attach'; - - describe('Terminal', () => { - it('should apply addons with Terminal.applyAddon', () => { - Terminal.applyAddon(attach); - // Test that addon was applied successfully, adding attach to Terminal's - // prototype. - assert.equal(typeof (Terminal).prototype.attach, 'function'); - }); -}); diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 94c4eccf9..a18305765 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -4,11 +4,12 @@ */ import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi } from 'xterm'; -import { ITerminal, IBuffer } from '../Types'; -import { IBufferLine } from 'core/Types'; +import { ITerminal } from '../Types'; +import { IBufferLine } from 'common/Types'; +import { IBuffer } from 'common/buffer/Types'; import { Terminal as TerminalCore } from '../Terminal'; -import * as Strings from '../Strings'; -import { IEvent } from 'common/EventEmitter2'; +import * as Strings from '../browser/LocalizableStrings'; +import { IEvent } from 'common/EventEmitter'; import { AddonManager } from './AddonManager'; import { WebglRendererAddon } from '../renderer/webgl/WebglRendererAddon'; @@ -44,27 +45,6 @@ export class Terminal implements ITerminalApi { public focus(): void { this._core.focus(); } - public on(type: 'blur' | 'focus' | 'linefeed' | 'selection', listener: () => void): void; - public on(type: 'data', listener: (...args: any[]) => void): void; - public on(type: 'key', listener: (key?: string, event?: KeyboardEvent) => void): void; - public on(type: 'keypress' | 'keydown', listener: (event?: KeyboardEvent) => void): void; - public on(type: 'refresh', listener: (data?: { start: number; end: number; }) => void): void; - public on(type: 'resize', listener: (data?: { cols: number; rows: number; }) => void): void; - public on(type: 'scroll', listener: (ydisp?: number) => void): void; - public on(type: 'title', listener: (title?: string) => void): void; - public on(type: string, listener: (...args: any[]) => void): void; - public on(type: any, listener: any): void { - this._core.on(type, listener); - } - public off(type: string, listener: (...args: any[]) => void): void { - this._core.off(type, listener); - } - public emit(type: string, data?: any): void { - this._core.emit(type, data); - } - public addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable { - return this._core.addDisposableListener(type, handler); - } public resize(columns: number, rows: number): void { this._core.resize(columns, rows); } diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index f61df580f..8335f76d9 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -3,15 +3,15 @@ * @license MIT */ -import { IRenderLayer, IRenderDimensions } from './Types'; +import { IRenderLayer } from './Types'; +import { IRenderDimensions } from 'browser/renderer/Types'; import { ITerminal } from '../Types'; -import { ICellData } from 'core/Types'; -import { DEFAULT_COLOR } from 'common/Types'; +import { ICellData, DEFAULT_COLOR } from 'common/Types'; import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, IGlyphIdentifier } from './atlas/Types'; import { BaseCharAtlas } from './atlas/BaseCharAtlas'; import { acquireCharAtlas } from './atlas/CharAtlasCache'; -import { CellData, AttributeData, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from 'core/buffer/BufferLine'; -import { IColorSet } from 'ui/Types'; +import { CellData, AttributeData, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from 'common/buffer/BufferLine'; +import { IColorSet } from 'browser/Types'; export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; diff --git a/src/renderer/CharacterJoinerRegistry.test.ts b/src/renderer/CharacterJoinerRegistry.test.ts index 1ce02b76b..e5ab08eaa 100644 --- a/src/renderer/CharacterJoinerRegistry.test.ts +++ b/src/renderer/CharacterJoinerRegistry.test.ts @@ -10,8 +10,8 @@ import { CircularList } from 'common/CircularList'; import { ICharacterJoinerRegistry } from './Types'; import { CharacterJoinerRegistry } from './CharacterJoinerRegistry'; -import { BufferLine, CellData } from 'core/buffer/BufferLine'; -import { IBufferLine } from 'core/Types'; +import { BufferLine, CellData } from 'common/buffer/BufferLine'; +import { IBufferLine } from 'common/Types'; describe('CharacterJoinerRegistry', () => { let registry: ICharacterJoinerRegistry; diff --git a/src/renderer/CharacterJoinerRegistry.ts b/src/renderer/CharacterJoinerRegistry.ts index 1432e8050..b707d863f 100644 --- a/src/renderer/CharacterJoinerRegistry.ts +++ b/src/renderer/CharacterJoinerRegistry.ts @@ -4,9 +4,9 @@ */ import { ITerminal } from '../Types'; -import { IBufferLine, ICellData, CharData } from 'core/Types'; +import { IBufferLine, ICellData, CharData } from 'common/Types'; import { ICharacterJoinerRegistry, ICharacterJoiner } from './Types'; -import { CellData, Content, AttributeData, WHITESPACE_CELL_CHAR } from 'core/buffer/BufferLine'; +import { CellData, Content, AttributeData, WHITESPACE_CELL_CHAR } from 'common/buffer/BufferLine'; export class JoinedCellData extends AttributeData implements ICellData { private _width: number; diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 4534603ea..9626f772d 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -3,12 +3,12 @@ * @license MIT */ -import { IRenderDimensions } from './Types'; +import { IRenderDimensions } from 'browser/renderer/Types'; import { BaseRenderLayer } from './BaseRenderLayer'; import { ITerminal } from '../Types'; -import { ICellData } from 'core/Types'; -import { CellData } from 'core/buffer/BufferLine'; -import { IColorSet } from 'ui/Types'; +import { ICellData } from 'common/Types'; +import { CellData } from 'common/buffer/BufferLine'; +import { IColorSet } from 'browser/Types'; interface ICursorState { x: number; diff --git a/src/renderer/LinkRenderLayer.ts b/src/renderer/LinkRenderLayer.ts index bfdce861a..668691f63 100644 --- a/src/renderer/LinkRenderLayer.ts +++ b/src/renderer/LinkRenderLayer.ts @@ -4,11 +4,11 @@ */ import { ILinkifierEvent, ITerminal, ILinkifierAccessor } from '../Types'; -import { IRenderDimensions } from './Types'; +import { IRenderDimensions } from 'browser/renderer/Types'; import { BaseRenderLayer } from './BaseRenderLayer'; import { INVERTED_DEFAULT_COLOR } from './atlas/Types'; import { is256Color } from './atlas/CharAtlasUtils'; -import { IColorSet } from 'ui/Types'; +import { IColorSet } from 'browser/Types'; export class LinkRenderLayer extends BaseRenderLayer { private _state: ILinkifierEvent = null; diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 2fa09824a..56e12b579 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -6,12 +6,14 @@ import { TextRenderLayer } from './TextRenderLayer'; import { SelectionRenderLayer } from './SelectionRenderLayer'; import { CursorRenderLayer } from './CursorRenderLayer'; -import { IRenderLayer, IRenderer, IRenderDimensions, ICharacterJoinerRegistry } from './Types'; -import { ITerminal, CharacterJoinerHandler } from '../Types'; +import { IRenderLayer, ICharacterJoinerRegistry } from './Types'; +import { IRenderer, IRenderDimensions, CharacterJoinerHandler } from 'browser/renderer/Types'; +import { ITerminal } from '../Types'; import { LinkRenderLayer } from './LinkRenderLayer'; import { CharacterJoinerRegistry } from '../renderer/CharacterJoinerRegistry'; import { Disposable } from 'common/Lifecycle'; -import { IColorSet } from 'ui/Types'; +import { IColorSet } from 'browser/Types'; +import { ICharSizeService } from 'browser/services/Services'; export class Renderer extends Disposable implements IRenderer { private _renderLayers: IRenderLayer[]; @@ -22,7 +24,8 @@ export class Renderer extends Disposable implements IRenderer { constructor( private _terminal: ITerminal, - private _colors: IColorSet + private _colors: IColorSet, + private _charSizeService: ICharSizeService ) { super(); const allowTransparency = this._terminal.options.allowTransparency; @@ -133,8 +136,7 @@ export class Renderer extends Disposable implements IRenderer { * Recalculates the character and canvas dimensions. */ private _updateDimensions(): void { - // Perform a new measure if the CharMeasure dimensions are not yet available - if (!this._terminal.charMeasure.width || !this._terminal.charMeasure.height) { + if (!this._charSizeService.hasValidSize) { return; } @@ -142,12 +144,12 @@ export class Renderer extends Disposable implements IRenderer { // drawn to an integer grid in order for the CharAtlas "stamps" to not be // blurry. When text is drawn to the grid not using the CharAtlas, it is // clipped to ensure there is no overlap with the next cell. - this.dimensions.scaledCharWidth = Math.floor(this._terminal.charMeasure.width * window.devicePixelRatio); + this.dimensions.scaledCharWidth = Math.floor(this._charSizeService.width * window.devicePixelRatio); // Calculate the scaled character height. Height is ceiled in case // devicePixelRatio is a floating point number in order to ensure there is // enough space to draw the character to the cell. - this.dimensions.scaledCharHeight = Math.ceil(this._terminal.charMeasure.height * window.devicePixelRatio); + this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * window.devicePixelRatio); // Calculate the scaled cell height, if lineHeight is not 1 then the value // will be floored because since lineHeight can never be lower then 1, there @@ -181,10 +183,9 @@ export class Renderer extends Disposable implements IRenderer { // Get the _actual_ dimensions of an individual cell. This needs to be // derived from the canvasWidth/Height calculated above which takes into - // account window.devicePixelRatio. CharMeasure.width/height by itself is - // insufficient when the page is not at 100% zoom level as CharMeasure is - // measured in CSS pixels, but the actual char size on the canvas can - // differ. + // account window.devicePixelRatio. ICharSizeService.width/height by itself + // is insufficient when the page is not at 100% zoom level as it's measured + // in CSS pixels, but the actual char size on the canvas can differ. this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows; this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols; } diff --git a/src/renderer/SelectionRenderLayer.ts b/src/renderer/SelectionRenderLayer.ts index 96bf6ec3a..7c297fd83 100644 --- a/src/renderer/SelectionRenderLayer.ts +++ b/src/renderer/SelectionRenderLayer.ts @@ -4,9 +4,9 @@ */ import { ITerminal } from '../Types'; -import { IRenderDimensions } from './Types'; +import { IRenderDimensions } from 'browser/renderer/Types'; import { BaseRenderLayer } from './BaseRenderLayer'; -import { IColorSet } from 'ui/Types'; +import { IColorSet } from 'browser/Types'; interface ISelectionState { start: [number, number]; diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index 52d8b0f8f..39fcb7fcc 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -3,14 +3,15 @@ * @license MIT */ -import { IRenderDimensions, ICharacterJoinerRegistry } from './Types'; +import { ICharacterJoinerRegistry } from './Types'; +import { IRenderDimensions } from 'browser/renderer/Types'; import { ITerminal } from '../Types'; -import { CharData, ICellData } from 'core/Types'; +import { CharData, ICellData } from 'common/Types'; import { GridCache } from './GridCache'; import { BaseRenderLayer } from './BaseRenderLayer'; -import { CellData, AttributeData, Content, NULL_CELL_CODE } from 'core/buffer/BufferLine'; +import { CellData, AttributeData, Content, NULL_CELL_CODE } from 'common/buffer/BufferLine'; import { JoinedCellData } from './CharacterJoinerRegistry'; -import { IColorSet } from 'ui/Types'; +import { IColorSet } from 'browser/Types'; /** * This CharData looks like a null character, which will forc a clear and render diff --git a/src/renderer/Types.ts b/src/renderer/Types.ts index d8f637ebf..153b68e24 100644 --- a/src/renderer/Types.ts +++ b/src/renderer/Types.ts @@ -3,9 +3,10 @@ * @license MIT */ -import { ITerminal, CharacterJoinerHandler } from '../Types'; +import { ITerminal } from '../Types'; import { IDisposable } from 'xterm'; -import { IColorSet } from 'ui/Types'; +import { IColorSet } from 'browser/Types'; +import { IRenderDimensions, CharacterJoinerHandler } from 'browser/renderer/Types'; /** * Flags used to render terminal text properly. @@ -20,44 +21,6 @@ export const enum FLAGS { ITALIC = 64 } -/** - * Note that IRenderer implementations should emit the refresh event after - * rendering rows to the screen. - */ -export interface IRenderer extends IDisposable { - readonly dimensions: IRenderDimensions; - - dispose(): void; - setColors(colors: IColorSet): void; - onDevicePixelRatioChange(): void; - onResize(cols: number, rows: number): void; - onCharSizeChanged(): void; - onBlur(): void; - onFocus(): void; - onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void; - onCursorMove(): void; - onOptionsChanged(): void; - clear(): void; - renderRows(start: number, end: number): void; - registerCharacterJoiner(handler: CharacterJoinerHandler): number; - deregisterCharacterJoiner(joinerId: number): boolean; -} - -export interface IRenderDimensions { - scaledCharWidth: number; - scaledCharHeight: number; - scaledCellWidth: number; - scaledCellHeight: number; - scaledCharLeft: number; - scaledCharTop: number; - scaledCanvasWidth: number; - scaledCanvasHeight: number; - canvasWidth: number; - canvasHeight: number; - actualCellWidth: number; - actualCellHeight: number; -} - export interface IRenderLayer extends IDisposable { /** * Called when the terminal loses focus. diff --git a/src/renderer/atlas/CharAtlasCache.ts b/src/renderer/atlas/CharAtlasCache.ts index 80c05d6f2..43b5eb839 100644 --- a/src/renderer/atlas/CharAtlasCache.ts +++ b/src/renderer/atlas/CharAtlasCache.ts @@ -8,7 +8,7 @@ import { generateConfig, configEquals } from './CharAtlasUtils'; import { BaseCharAtlas } from './BaseCharAtlas'; import { DynamicCharAtlas } from './DynamicCharAtlas'; import { ICharAtlasConfig } from './Types'; -import { IColorSet } from 'ui/Types'; +import { IColorSet } from 'browser/Types'; interface ICharAtlasCacheEntry { atlas: BaseCharAtlas; diff --git a/src/renderer/atlas/CharAtlasUtils.ts b/src/renderer/atlas/CharAtlasUtils.ts index ab0f32408..b68793f64 100644 --- a/src/renderer/atlas/CharAtlasUtils.ts +++ b/src/renderer/atlas/CharAtlasUtils.ts @@ -6,7 +6,7 @@ import { ITerminal } from '../../Types'; import { ICharAtlasConfig } from './Types'; import { DEFAULT_COLOR } from 'common/Types'; -import { IColorSet } from 'ui/Types'; +import { IColorSet } from 'browser/Types'; export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig { // null out some fields that don't matter diff --git a/src/renderer/atlas/DynamicCharAtlas.ts b/src/renderer/atlas/DynamicCharAtlas.ts index 695162c1c..c8ccefcbd 100644 --- a/src/renderer/atlas/DynamicCharAtlas.ts +++ b/src/renderer/atlas/DynamicCharAtlas.ts @@ -5,10 +5,10 @@ import { DIM_OPACITY, IGlyphIdentifier, INVERTED_DEFAULT_COLOR, ICharAtlasConfig } from './Types'; import { BaseCharAtlas } from './BaseCharAtlas'; -import { DEFAULT_ANSI_COLORS } from 'ui/ColorManager'; +import { DEFAULT_ANSI_COLORS } from 'browser/ColorManager'; import { LRUMap } from './LRUMap'; import { isFirefox, isSafari } from 'common/Platform'; -import { IColor } from 'ui/Types'; +import { IColor } from 'browser/Types'; // In practice we're probably never going to exhaust a texture this large. For debugging purposes, // however, it can be useful to set this to a really tiny value, to verify that LRU eviction works. diff --git a/src/renderer/atlas/Types.ts b/src/renderer/atlas/Types.ts index 5bfd83bf0..2cb1db409 100644 --- a/src/renderer/atlas/Types.ts +++ b/src/renderer/atlas/Types.ts @@ -4,7 +4,7 @@ */ import { FontWeight } from 'xterm'; -import { IColorSet } from 'ui/Types'; +import { IColorSet } from 'browser/Types'; export const INVERTED_DEFAULT_COLOR = 257; export const DIM_OPACITY = 0.5; diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index a19225418..8d370336b 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -3,12 +3,13 @@ * @license MIT */ -import { IRenderer, IRenderDimensions } from '../Types'; -import { ILinkifierEvent, ITerminal, CharacterJoinerHandler } from '../../Types'; +import { IRenderer, IRenderDimensions, CharacterJoinerHandler } from 'browser/renderer/Types'; +import { ILinkifierEvent, ITerminal } from '../../Types'; import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from './DomRendererRowFactory'; import { INVERTED_DEFAULT_COLOR } from '../atlas/Types'; import { Disposable } from 'common/Lifecycle'; -import { IColorSet } from 'ui/Types'; +import { IColorSet } from 'browser/Types'; +import { ICharSizeService } from 'browser/services/Services'; const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-'; const ROW_CONTAINER_CLASS = 'xterm-rows'; @@ -41,7 +42,8 @@ export class DomRenderer extends Disposable implements IRenderer { constructor( private _terminal: ITerminal, - private _colors: IColorSet + private _colors: IColorSet, + private _charSizeService: ICharSizeService ) { super(); @@ -91,8 +93,8 @@ export class DomRenderer extends Disposable implements IRenderer { } private _updateDimensions(): void { - this.dimensions.scaledCharWidth = this._terminal.charMeasure.width * window.devicePixelRatio; - this.dimensions.scaledCharHeight = Math.ceil(this._terminal.charMeasure.height * window.devicePixelRatio); + this.dimensions.scaledCharWidth = this._charSizeService.width * window.devicePixelRatio; + this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * window.devicePixelRatio); this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing); this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight); this.dimensions.scaledCharLeft = 0; diff --git a/src/renderer/dom/DomRendererRowFactory.test.ts b/src/renderer/dom/DomRendererRowFactory.test.ts index e3b26e5dc..28ee0a7ec 100644 --- a/src/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/renderer/dom/DomRendererRowFactory.test.ts @@ -6,9 +6,9 @@ import jsdom = require('jsdom'); import { assert } from 'chai'; import { DomRendererRowFactory } from './DomRendererRowFactory'; -import { BufferLine, CellData, FgFlags, BgFlags, Attributes, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR, DEFAULT_ATTR_DATA } from 'core/buffer/BufferLine'; +import { BufferLine, CellData, FgFlags, BgFlags, Attributes, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { ITerminalOptions } from '../../Types'; -import { IBufferLine } from 'core/Types'; +import { IBufferLine } from 'common/Types'; describe('DomRendererRowFactory', () => { let dom: jsdom.JSDOM; diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index d3685923f..8c31aa444 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -4,9 +4,9 @@ */ import { ITerminalOptions } from '../../Types'; -import { IBufferLine } from '../../core/Types'; +import { IBufferLine } from 'common/Types'; import { INVERTED_DEFAULT_COLOR } from '../atlas/Types'; -import { CellData, AttributeData, NULL_CELL_CODE, WHITESPACE_CELL_CHAR } from '../../core/buffer/BufferLine'; +import { CellData, AttributeData, NULL_CELL_CODE, WHITESPACE_CELL_CHAR } from 'common/buffer/BufferLine'; export const BOLD_CLASS = 'xterm-bold'; export const DIM_CLASS = 'xterm-dim'; diff --git a/src/tsconfig.all.json b/src/tsconfig.all.json deleted file mode 100644 index 2a53ab899..000000000 --- a/src/tsconfig.all.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "files": [], - "include": [], - "references": [ - { "path": "." }, - { "path": "./addons/attach" }, - { "path": "./addons/fit" }, - { "path": "./addons/fullscreen" }, - { "path": "./addons/search" }, - { "path": "./addons/terminado" }, - { "path": "./addons/webLinks" }, - { "path": "./addons/zmodem" } - ] -} diff --git a/src/tsconfig.json b/src/tsconfig.json index a42c83381..975766683 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -14,8 +14,7 @@ "baseUrl": ".", "paths": { "common/*": [ "./common/*" ], - "core/*": [ "./core/*" ], - "ui/*": [ "./ui/*" ] + "browser/*": [ "./browser/*" ] }, "noUnusedLocals": true, @@ -30,7 +29,6 @@ ], "references": [ { "path": "./common" }, - { "path": "./core" }, - { "path": "./ui" } + { "path": "./browser" } ] } diff --git a/src/ui/Types.ts b/src/ui/Types.ts deleted file mode 100644 index b7b0ac215..000000000 --- a/src/ui/Types.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -export interface IColorManager { - colors: IColorSet; -} - -export interface IColor { - css: string; - rgba: number; // 32-bit int with rgba in each byte -} - -export interface IColorSet { - foreground: IColor; - background: IColor; - cursor: IColor; - cursorAccent: IColor; - selection: IColor; - ansi: IColor[]; -} - -export interface ITheme { - foreground?: string; - background?: string; - cursor?: string; - cursorAccent?: string; - selection?: string; - black?: string; - red?: string; - green?: string; - yellow?: string; - blue?: string; - magenta?: string; - cyan?: string; - white?: string; - brightBlack?: string; - brightRed?: string; - brightGreen?: string; - brightYellow?: string; - brightBlue?: string; - brightMagenta?: string; - brightCyan?: string; - brightWhite?: string; -} diff --git a/test/CharWidth.api.ts b/test/CharWidth.api.ts new file mode 100644 index 000000000..035208ccc --- /dev/null +++ b/test/CharWidth.api.ts @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import * as puppeteer from 'puppeteer'; +import { assert } from 'chai'; +import { ITerminalOptions } from 'xterm'; + +const APP = 'http://127.0.0.1:3000/test'; + +let browser: puppeteer.Browser; +let page: puppeteer.Page; +const width = 800; +const height = 600; + +describe('CharWidth Integration Tests', function(): void { + this.timeout(20000); + + before(async function(): Promise { + browser = await puppeteer.launch({ + headless: process.argv.indexOf('--headless') !== -1, + slowMo: 80, + args: [`--window-size=${width},${height}`] + }); + page = (await browser.pages())[0]; + await page.setViewport({ width, height }); + await page.goto(APP); + await openTerminal({ rows: 5, cols: 30 }); + }); + + after(() => { + browser.close(); + }); + + describe('getStringCellWidth', () => { + beforeEach(async () => { + await page.evaluate(`window.term.reset()`); + }); + + it('ASCII chars', async function(): Promise { + const input = 'This is just ASCII text.#'; + await page.evaluate(`window.term.write('${input}')`); + assert.equal(25, await sumWidths(0, 1, '#')); + }); + + it('combining chars', async function(): Promise { + const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301#'; + await page.evaluate(`window.term.write('${input}')`); + assert.equal(10, await sumWidths(0, 1, '#')); + }); + + it('surrogate chars', async function(): Promise { + const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞#'; + await page.evaluate(`window.term.write('${input}')`); + assert.equal(28, await sumWidths(0, 1, '#')); + }); + + it('surrogate combining chars', async function(): Promise { + const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301#'; + await page.evaluate(`window.term.write('${input}')`); + assert.equal(12, await sumWidths(0, 1, '#')); + }); + + it('fullwidth chars', async function(): Promise { + const input = '1234567890#'; + await page.evaluate(`window.term.write('${input}')`); + assert.equal(21, await sumWidths(0, 1, '#')); + }); + + it('fullwidth chars offset 1', async function(): Promise { + const input = 'a1234567890#'; + await page.evaluate(`window.term.write('${input}')`); + assert.equal(22, await sumWidths(0, 1, '#')); + }); + + // TODO: multiline tests once #1685 is resolved + }); +}); + +async function openTerminal(options: ITerminalOptions = {}): Promise { + await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`); + await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`); + if (options.rendererType === 'dom') { + await page.waitForSelector('.xterm-rows'); + } else { + await page.waitForSelector('.xterm-text-layer'); + } +} + +async function sumWidths(start: number, end: number, sentinel: string): Promise { + await page.evaluate(` + (function() { + window.result = 0; + const buffer = window.term.buffer; + for (let i = ${start}; i < ${end}; i++) { + const line = buffer.getLine(i); + let j = 0; + while (true) { + const cell = line.getCell(j++); + if (!cell) { + break; + } + window.result += cell.width; + if (cell.char === '${sentinel}') { + return; + } + } + } + })(); + `); + return await page.evaluate(`window.result`); +} diff --git a/src/InputHandler.api.ts b/test/InputHandler.api.ts similarity index 73% rename from src/InputHandler.api.ts rename to test/InputHandler.api.ts index 0d146d5d9..3a95feca4 100644 --- a/src/InputHandler.api.ts +++ b/test/InputHandler.api.ts @@ -5,7 +5,7 @@ import * as puppeteer from 'puppeteer'; import { assert } from 'chai'; -import { ITerminalOptions } from './Types'; +import { ITerminalOptions } from 'xterm'; const APP = 'http://127.0.0.1:3000/test'; @@ -229,6 +229,68 @@ describe('InputHandler Integration Tests', function(): void { assert.equal(await page.evaluate(`window.result`), '\x1b[?3;4R'); }); }); + + describe('SM: Set Mode', () => { + describe('CSI ? Pm h', () => { + it('Pm = 2004, Set bracketed paste mode', async function(): Promise { + assert.equal(await simulatePaste('foo'), 'foo'); + await page.evaluate(`window.term.write('\x1b[?2004h')`); + assert.equal(await simulatePaste('bar'), '\x1b[200~bar\x1b[201~'); + await page.evaluate(`window.term.write('\x1b[?2004l')`); + assert.equal(await simulatePaste('baz'), 'baz'); + }); + }); + }); + + it('REP: Repeat preceding character, ECMA48 - CSI Ps b', async function(): Promise { + // default to 1 + await page.evaluate(` + window.term.resize(10, 10); + window.term.write('#\x1b[b'); + window.term.writeln(''); + window.term.write('#\x1b[0b'); + window.term.writeln(''); + window.term.write('#\x1b[1b'); + window.term.writeln(''); + window.term.write('#\x1b[5b'); + `); + assert.deepEqual(await getLinesAsArray(4), ['##', '##', '##', '######']); + assert.deepEqual(await getCursor(), {col: 6, row: 3}); + // should not repeat on fullwidth chars + await page.evaluate(` + window.term.reset(); + window.term.write('¥\x1b[10b'); + `); + assert.deepEqual(await getLinesAsArray(1), ['¥']); + // should repeat only base char of combining + await page.evaluate(` + window.term.reset(); + window.term.write('e\u0301\x1b[5b'); + `); + assert.deepEqual(await getLinesAsArray(1), ['e\u0301eeeee']); + // should wrap correctly + await page.evaluate(` + window.term.reset(); + window.term.write('#\x1b[15b'); + `); + assert.deepEqual(await getLinesAsArray(2), ['##########', '######']); + await page.evaluate(` + window.term.reset(); + window.term.write('\x1b[?7l'); // disable wrap around + window.term.write('#\x1b[15b'); + `); + assert.deepEqual(await getLinesAsArray(2), ['##########', '']); + // any successful sequence should reset REP + await page.evaluate(` + window.term.reset(); + window.term.write('\x1b[?7h'); // re-enable wrap around + window.term.write('#\\n\x1b[3b'); + window.term.write('#\\r\x1b[3b'); + window.term.writeln(''); + window.term.write('abcdefg\x1b[3D\x1b[10b#\x1b[3b'); + `); + assert.deepEqual(await getLinesAsArray(3), ['#', ' #', 'abcd####']); + }); }); }); @@ -249,3 +311,24 @@ async function getLinesAsArray(count: number, start: number = 0): Promise { + const id = Math.floor(Math.random() * 1000000); + await page.evaluate(` + (function () { + window.term.onData(e => window.result_${id} = e); + const clipboardData = new DataTransfer(); + clipboardData.setData('text/plain', '${text}'); + window.term.textarea.dispatchEvent(new ClipboardEvent('paste', { clipboardData })); + })(); + `); + return await page.evaluate(`window.result_${id}`); +} + +async function getCursor(): Promise<{col: number, row: number}> { + return page.evaluate(` + (function() { + return {col: term.buffer.cursorX, row: term.buffer.cursorY}; + })(); + `); +} diff --git a/src/public/Terminal.api.ts b/test/Terminal.api.ts similarity index 99% rename from src/public/Terminal.api.ts rename to test/Terminal.api.ts index 75deba5b6..e536e88af 100644 --- a/src/public/Terminal.api.ts +++ b/test/Terminal.api.ts @@ -5,7 +5,7 @@ import * as puppeteer from 'puppeteer'; import { assert } from 'chai'; -import { ITerminalOptions } from '../Types'; +import { ITerminalOptions } from 'xterm'; const APP = 'http://127.0.0.1:3000/test'; diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 000000000..fce9a1baa --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "lib": [ + "dom", + "es6", + ], + "rootDir": ".", + "outDir": "../out/test", + "types": [ + "../node_modules/@types/mocha" + ], + "sourceMap": true, + "removeComments": true, + "pretty": true, + "strict": true + }, + "include": [ + "./**/*", + "../typings/xterm.d.ts" + ] +} diff --git a/tsconfig.all.json b/tsconfig.all.json new file mode 100644 index 000000000..d2670811a --- /dev/null +++ b/tsconfig.all.json @@ -0,0 +1,12 @@ +{ + "files": [], + "include": [], + "references": [ + { "path": "./src" }, + { "path": "./test" }, + { "path": "./addons/xterm-addon-attach/src" }, + { "path": "./addons/xterm-addon-fit/src" }, + { "path": "./addons/xterm-addon-search/src" }, + { "path": "./addons/xterm-addon-web-links/src" } + ] +} diff --git a/tslint.json b/tslint.json index d2daef092..f6f1f46c6 100644 --- a/tslint.json +++ b/tslint.json @@ -95,7 +95,7 @@ {"type": "default", "format": "camelCase", "leadingUnderscore": "forbid"}, {"type": "type", "format": "PascalCase"}, {"type": "class", "format": "PascalCase"}, - {"type": "property", "modifiers": ["const"], "format": "UPPER_CASE"}, + {"type": "property", "modifiers": ["const"], "format": ["camelCase", "UPPER_CASE"]}, {"type": "member", "modifiers": ["protected"], "format": "camelCase", "leadingUnderscore": "allow"}, // TODO: Change allow to require when there aren't many PRs out // {"type": "member", "modifiers": ["protected"], "format": "camelCase", "leadingUnderscore": "require"}, diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 319599b5e..564f49356 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -26,13 +26,14 @@ declare module 'xterm' { export interface ITerminalOptions { /** * Whether background should support non-opaque color. It must be set before - * executing open() method and can't be changed later without excuting it again. - * Warning: Enabling this option can reduce performances somewhat. + * executing the `Terminal.open()` method and can't be changed later without + * executing it again. Note that enabling this can negatively impact + * performance. */ allowTransparency?: boolean; /** - * A data uri of the sound to use for the bell (needs bellStyle = 'sound'). + * A data uri of the sound to use for the bell when `bellStyle = 'sound'`. */ bellSound?: string; @@ -114,9 +115,9 @@ declare module 'xterm' { /** * Whether holding a modifier key will force normal selection behavior, * regardless of whether the terminal is in mouse events mode. This will - * also prevent mouse events from being emitted by the terminal. For example, - * this allows you to use xterm.js' regular selection inside tmux with - * mouse mode enabled. + * also prevent mouse events from being emitted by the terminal. For + * example, this allows you to use xterm.js' regular selection inside tmux + * with mouse mode enabled. */ macOptionClickForcesSelection?: boolean; @@ -149,8 +150,9 @@ declare module 'xterm' { screenReaderMode?: boolean; /** - * The amount of scrollback in the terminal. Scrollback is the amount of rows - * that are retained when lines are scrolled beyond the initial viewport. + * The amount of scrollback in the terminal. Scrollback is the amount of + * rows that are retained when lines are scrolled beyond the initial + * viewport. */ scrollback?: number; @@ -187,7 +189,7 @@ declare module 'xterm' { background?: string, /** The cursor color */ cursor?: string, - /** The accent color of the cursor (used as the foreground color for a block cursor) */ + /** The accent color of the cursor (fg color for a block cursor) */ cursorAccent?: string, /** The selection background color (can be transparent) */ selection?: string, @@ -253,8 +255,8 @@ declare module 'xterm' { leaveCallback?: () => void; /** - * The priority of the link matcher, this defines the order in which the link - * matcher is evaluated relative to others, from highest to lowest. The + * The priority of the link matcher, this defines the order in which the + * link matcher is evaluated relative to others, from highest to lowest. The * default value is 0. */ priority?: number; @@ -268,13 +270,6 @@ declare module 'xterm' { willLinkActivate?: (event: MouseEvent, uri: string) => boolean; } - export interface IEventEmitter { - on(type: string, listener: (...args: any[]) => void): void; - off(type: string, listener: (...args: any[]) => void): void; - emit(type: string, data?: any): void; - addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable; - } - /** * An object that can be disposed via a dispose function. */ @@ -290,22 +285,52 @@ declare module 'xterm' { (listener: (e: T) => any): IDisposable; } + /** + * Represents a specific line in the terminal that is tracked when scrollback + * is trimmed and lines are added or removed. + */ export interface IMarker extends IDisposable { + /** + * A unique identifier for this marker. + */ readonly id: number; + + /** + * Whether this marker is disposed. + */ readonly isDisposed: boolean; + + /** + * The actual line index in the buffer at this point in time. + */ readonly line: number; } + /** + * The set of localizable strings. + */ export interface ILocalizableStrings { + /** + * Announcement for a blank line when `screenReaderMode` is enabled. + */ blankLine: string; + + /** + * The aria label for the underlying input textarea for the terminal. + */ promptLabel: string; + + /** + * Announcement for when line reading is suppressed due to too many lines + * being printed to the terminal when `screenReaderMode` is enabled. + */ tooMuchOutput: string; } /** * The class that represents an xterm.js terminal. */ - export class Terminal implements IEventEmitter, IDisposable { + export class Terminal implements IDisposable { /** * The element containing the terminal. */ @@ -429,96 +454,6 @@ declare module 'xterm' { */ focus(): void; - /** - * Registers an event listener. - * @param type The type of the event. - * @param listener The listener. - * @deprecated use `Terminal.onEvent(listener)` instead. - */ - on(type: 'blur' | 'focus' | 'linefeed' | 'selection', listener: () => void): void; - /** - * Registers an event listener. - * @param type The type of the event. - * @param listener The listener. - * @deprecated use `Terminal.onEvent(listener)` instead. - */ - on(type: 'data', listener: (...args: any[]) => void): void; - /** - * Registers an event listener. - * @param type The type of the event. - * @param listener The listener. - * @deprecated use `Terminal.onEvent(listener)` instead. - */ - on(type: 'key', listener: (key: string, event: KeyboardEvent) => void): void; - /** - * Registers an event listener. - * @param type The type of the event. - * @param listener The listener. - * @deprecated use `Terminal.onEvent(listener)` instead. - */ - on(type: 'keypress' | 'keydown', listener: (event: KeyboardEvent) => void): void; - /** - * Registers an event listener. - * @param type The type of the event. - * @param listener The listener. - * @deprecated use `Terminal.onEvent(listener)` instead. - */ - on(type: 'refresh', listener: (data: {start: number, end: number}) => void): void; - /** - * Registers an event listener. - * @param type The type of the event. - * @param listener The listener. - * @deprecated use `Terminal.onEvent(listener)` instead. - */ - on(type: 'resize', listener: (data: {cols: number, rows: number}) => void): void; - /** - * Registers an event listener. - * @param type The type of the event. - * @param listener The listener. - * @deprecated use `Terminal.onEvent(listener)` instead. - */ - on(type: 'scroll', listener: (ydisp: number) => void): void; - /** - * Registers an event listener. - * @param type The type of the event. - * @param listener The listener. - * @deprecated use `Terminal.onEvent(listener)` instead. - */ - on(type: 'title', listener: (title: string) => void): void; - /** - * Registers an event listener. - * @param type The type of the event. - * @param listener The listener. - * @deprecated use `Terminal.onEvent(listener)` instead. - */ - on(type: string, listener: (...args: any[]) => void): void; - - /** - * Deregisters an event listener. - * @param type The type of the event. - * @param listener The listener. - * @deprecated use `Terminal.onEvent(listener).dispose()` instead. - */ - off(type: 'blur' | 'focus' | 'linefeed' | 'selection' | 'data' | 'key' | 'keypress' | 'keydown' | 'refresh' | 'resize' | 'scroll' | 'title' | string, listener: (...args: any[]) => void): void; - - /** - * Emits an event on the terminal. - * @param type The type of event - * @param data data associated with the event. - * @deprecated This is being removed from the API with no replacement, see - * issue #1505. - */ - emit(type: string, data?: any): void; - - /** - * Adds an event listener to the Terminal, returning an IDisposable that can - * be used to conveniently remove the event listener. - * @param type The type of event. - * @param handler The event handler. - * @deprecated use `Terminal.onEvent(listener)` instead. - */ - addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable; - /** * Resizes the terminal. It's best practice to debounce calls to resize, * this will help ensure that the pty can respond to the resize event @@ -727,9 +662,9 @@ declare module 'xterm' { writeln(data: string): void; /** - * Writes UTF8 data to the terminal. - * This has a slight performance advantage over the string based write method - * due to lesser data conversions needed on the way from the pty to xterm.js. + * Writes UTF8 data to the terminal. This has a slight performance advantage + * over the string based write method due to lesser data conversions needed + * on the way from the pty to xterm.js. * @param data The data to write to the terminal. */ writeUtf8(data: Uint8Array): void; @@ -934,13 +869,13 @@ declare module 'xterm' { */ export interface ITerminalAddon extends IDisposable { /** - * (EXPERIMENTAL) This is called when the addon is activated within xterm.js. + * (EXPERIMENTAL) This is called when the addon is activated. */ activate(terminal: Terminal): void; } /** - * An object representing a selecrtion within the terminal. + * An object representing a selection within the terminal. */ interface ISelectionPosition { /** @@ -964,6 +899,9 @@ declare module 'xterm' { endRow: number; } + /** + * Represents a terminal buffer. + */ interface IBuffer { /** * The y position of the cursor. This ranges between `0` (when the @@ -995,16 +933,21 @@ declare module 'xterm' { readonly length: number; /** - * Gets a line from the buffer, or undefined if the line index does not exist. + * Gets a line from the buffer, or undefined if the line index does not + * exist. * - * Note that the result of this function should be used immediately after calling as when the - * terminal updates it could lead to unexpected behavior. + * Note that the result of this function should be used immediately after + * calling as when the terminal updates it could lead to unexpected + * behavior. * * @param y The line index to get. */ getLine(y: number): IBufferLine | undefined; } + /** + * Represents a line in the terminal's buffer. + */ interface IBufferLine { /** * Whether the line is wrapped from the previous line. @@ -1014,16 +957,17 @@ declare module 'xterm' { /** * Gets a cell from the line, or undefined if the line index does not exist. * - * Note that the result of this function should be used immediately after calling as when the - * terminal updates it could lead to unexpected behavior. + * Note that the result of this function should be used immediately after + * calling as when the terminal updates it could lead to unexpected + * behavior. * * @param x The character index to get. */ - getCell(x: number): IBufferCell; + getCell(x: number): IBufferCell | undefined; /** - * Gets the line as a string. Note that this is gets only the string for the line, not taking - * isWrapped into account. + * Gets the line as a string. Note that this is gets only the string for the + * line, not taking isWrapped into account. * * @param trimRight Whether to trim any whitespace at the right of the line. * @param startColumn The column to start from (inclusive). @@ -1032,6 +976,9 @@ declare module 'xterm' { translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string; } + /** + * Represents a single cell in the terminal's buffer. + */ interface IBufferCell { /** * The character within the cell. diff --git a/webpack.config.js b/webpack.config.js index 5e0a27547..3f55dda35 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -29,8 +29,7 @@ module.exports = { extensions: [ '.js' ], alias: { common: path.resolve('./out/common'), - core: path.resolve('./out/core'), - ui: path.resolve('./out/ui') + browser: path.resolve('./out/browser') } }, output: { diff --git a/yarn.lock b/yarn.lock index a453bb236..2c2be0f3e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -108,6 +108,14 @@ "@types/uglify-js" "*" source-map "^0.6.0" +"@types/ws@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-6.0.1.tgz#ca7a3f3756aa12f62a0a62145ed14c6db25d5a28" + integrity sha512-EzH8k1gyZ4xih/MaZTXwT2xOkPiIMSrhQ9b8wrlX88L0T02eYsddatQlwVFlEPyEqV0ChpdpNnE51QPH6NVT4Q== + dependencies: + "@types/events" "*" + "@types/node" "*" + "@webassemblyjs/ast@1.5.13": version "1.5.13" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.5.13.tgz#81155a570bd5803a30ec31436bc2c9c0ede38f25" @@ -472,7 +480,7 @@ async-each@^1.0.0: resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" integrity sha1-GdOGodntxufByF04iu28xW0zYC0= -async-limiter@~1.0.0: +async-limiter@^1.0.0, async-limiter@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg== @@ -1384,9 +1392,9 @@ esprima@^3.1.3: integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM= esprima@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" - integrity sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw== + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== esrecurse@^4.1.0: version "4.2.1" @@ -2286,7 +2294,7 @@ js-tokens@^3.0.2: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= -js-yaml@3.13.1: +js-yaml@3.13.1, js-yaml@^3.7.0: version "3.13.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== @@ -2294,14 +2302,6 @@ js-yaml@3.13.1: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@^3.7.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" - integrity sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" @@ -4536,6 +4536,13 @@ ws@^6.1.0: dependencies: async-limiter "~1.0.0" +ws@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.0.0.tgz#79351cbc3f784b3c20d0821baf4b4ff809ffbf51" + integrity sha512-cknCal4k0EAOrh1SHHPPWWh4qm93g1IuGGGwBjWkXmCG7LsDtL8w9w+YVfaF+KSVwiHQKDIMsSLBVftKf9d1pg== + dependencies: + async-limiter "^1.0.0" + xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" @@ -4551,21 +4558,6 @@ xtend@^4.0.0, xtend@~4.0.1: resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68= -xterm-addon-attach@0.1.0-beta8: - version "0.1.0-beta8" - resolved "https://registry.yarnpkg.com/xterm-addon-attach/-/xterm-addon-attach-0.1.0-beta8.tgz#e469ed9d6ab7e535d0a9ffae23ef4f2efe58163b" - integrity sha512-HtQuwqnvcR+SwI9/JbBMd//Il+oEeo3rWrIucLLKHT8sB+OAOkdhmo5KIM/hhnovjI040WJ+tTHkDgPFwIJtmw== - -xterm-addon-search@0.1.0-beta4: - version "0.1.0-beta4" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.1.0-beta4.tgz#c73fe058c87f07eaae31baaa92976e927438a396" - integrity sha512-tJgZ1VTRd/DOFUhSFZzybRF8SR1LCEXRYkw/mHzGV5Ba3zhqVdSkN/0J9sjOpX6u21buee2OmTiCMZxq80zfJg== - -xterm-addon-web-links@0.1.0-beta6: - version "0.1.0-beta6" - resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.1.0-beta6.tgz#9b4e862be8928ef455a667745bea479665db6c6b" - integrity sha512-tkVU5wCfBFjXwfOvcbMHoLoMDANztkwSREiKyu2R059kEF+sP67Z33HzxVCXUWFuCmutcx40xR2O0BK68gXZlg== - "y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"