diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 10eee3d9..1fe5c763 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -1,8 +1,8 @@ --- name: Question -about: The issue tracker is not for questions. Please ask questions on https://stackoverflow.com/questions/tagged/xtermjs or https://gitter.im/sourcelair/xterm.js. +about: The issue tracker is not for questions. Please ask questions on https://stackoverflow.com/questions/tagged/xtermjs --- 🛑 The issue tracker is not for questions 🛑 -If you have a question, please ask it on https://stackoverflow.com/questions/tagged/xtermjs or https://gitter.im/sourcelair/xterm.js. +If you have a question, please ask it on https://stackoverflow.com/questions/tagged/xtermjs. diff --git a/.gitignore b/.gitignore index 25e93d9b..b50ab2d9 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,6 @@ npm-debug.log /.idea/ .env build/ -.vscode/ .DS_Store fixtures/typings-test/*.js package-lock.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..c7bf7381 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,44 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Unit Tests", + "cwd": "${workspaceRoot}", + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/mocha", + "windows": { + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/mocha.cmd" + }, + "runtimeArgs": [ + "--colors", + "--recursive", + "${workspaceRoot}/lib" + ], + "sourceMaps": true, + "outFiles": [ "${workspaceRoot}/lib/**/*.js" ], + "internalConsoleOptions": "openOnSessionStart" + }, + { + "type": "chrome", + "request": "launch", + "name": "Demo Client", + "url": "http://0.0.0.0:3000", + "windows": { + "url": "http://127.0.0.1:3000" + }, + "webRoot": "${workspaceFolder}/" + }, + { + "type": "node", + "request": "launch", + "name": "Demo Server", + "runtimeExecutable": "npm", + "runtimeArgs": [ + "run", + "start-debug" + ], + "port": 9229 + } + ] +} diff --git a/README.md b/README.md index c654f288..35ccc9b6 100644 --- a/README.md +++ b/README.md @@ -2,23 +2,20 @@ [![Build Status](https://dev.azure.com/xtermjs/xterm.js/_apis/build/status/xtermjs.xterm.js)](https://dev.azure.com/xtermjs/xterm.js/_build/latest?definitionId=3) [![Coverage Status](https://coveralls.io/repos/github/xtermjs/xterm.js/badge.svg?branch=master)](https://coveralls.io/github/xtermjs/xterm.js?branch=master) -[![Gitter](https://badges.gitter.im/sourcelair/xterm.js.svg)](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) -[![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/xterm/badge?style=rounded)](https://www.jsdelivr.com/package/npm/xterm) -Xterm.js is a terminal front-end component written in JavaScript that works in the browser. - -It enables applications to provide fully featured terminals to their users and create great development experiences. +Xterm.js is a front-end component written in TypeScript that lets applications bring fully-featured terminals to their users in the browser. It's used by popular projects such as VS Code, Hyper and Theia. ## Features -- **Text-based application support**: Use xterm.js to work with applications like `bash`, `git` etc. -- **Curses-based application support**: Use xterm.js to work with applications like `vim`, `tmux` etc. -- **Mouse events support**: Xterm.js captures mouse events like click and scroll and passes them to the terminal's back-end controlling process -- **CJK (Chinese, Japanese, Korean) character support**: Xterm.js renders CJK characters seamlessly -- **IME support**: Insert international (including CJK) characters using IME input with your keyboard -- **Self-contained library**: Xterm.js works on its own. It does not require any external libraries like jQuery or React to work -- **Modular, event-based API**: Lets you build addons and themes with ease + +- **Terminal apps just work**: Xterm.js works with most terminal apps such as `bash`, `vim` and `tmux`, this includes support for curses-based apps and mouse event support +- **Perfomant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer +- **Rich unicode support**: Supports CJK, emojis and IMEs +- **Self-contained**: Requires zero dependencies to work +- **Accessible**: Screen reader support can be turned on using the `screenReaderMode` option +- **And much more**: Links, theming, addons, well documented API, etc. ## What xterm.js is not + - Xterm.js is not a terminal application that you can download and use on your computer - Xterm.js is not `bash`. Xterm.js can be connected to processes like `bash` and let you interact with them (provide input, receive output) @@ -30,7 +27,7 @@ First you need to install the module, we ship exclusively through [npm](https:// npm install xterm ``` -To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to the head of your html page. Then create a `
` onto which xterm can attach itself. +To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to the head of your html page. Then create a `
` onto which xterm can attach itself. Finally instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`. ```html @@ -50,33 +47,24 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t ``` -Finally instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`. - ### Importing -The proposed way to load xterm.js is via the ES6 module syntax. +The recommended way to load xterm.js is via the ES6 module syntax: ```javascript import { Terminal } from 'xterm'; ``` -### API - -The full API for xterm.js is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API. - -Note that some APIs are marked *experimental*, these are added so we can experiment with new ideas without committing to support it like a normal semver API. Note that these APIs can change radically between versions so be sure to read release notes if you plan on using experimental APIs. - ### Addons -Addons are JavaScript modules that extend the `Terminal` prototype with new methods and attributes to provide additional functionality. There are a handful available in the main repository in the `src/addons` directory and you can even write your own, by using xterm.js' public API. +Addons are JavaScript modules that extend the `Terminal` prototype with new methods and attributes to provide additional functionality. There are a handful available in the main repository in the `src/addons` directory and you can even write your own by using the [public API](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts). To use an addon, just import the JavaScript module and pass it to `Terminal`'s `applyAddon` method: ```javascript -import { Terminal } from xterm; +import { Terminal } from 'xterm'; import * as fit from 'xterm/lib/addons/fit/fit'; - Terminal.applyAddon(fit); var xterm = new Terminal(); // Instantiate the terminal @@ -87,25 +75,16 @@ You will also need to include the addon's CSS file if it has one in the folder. #### Importing Addons in TypeScript -There are currently no typings for addons if they are accessed via extending Terminal prototype, so you will need to upcast if using TypeScript, eg. `(xterm).fit()`. - -Alternatively, you can import addon function and enhance the terminal on demand. This would have better typing support and is friendly to treeshaking. E.g.: +There are currently no typings for addons if they are accessed via extending Terminal prototype, so you will need to upcast if using TypeScript, eg. `(xterm as any).fit()`. Alternatively, you can import the addon function and enhance the terminal on demand. This has better typing support and is friendly to treeshaking. ```typescript import { Terminal } from 'xterm'; import { fit } from 'xterm/lib/addons/fit/fit'; const xterm = new Terminal(); -// Fit the terminal when necessary: -fit(xterm); +fit(xterm); // Fit the terminal when necessary ``` -#### Third party addons - -There are also the following third party addons available: - -- [xterm-webfont](https://www.npmjs.com/package/xterm-webfont) - ## Browser Support Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Here is a list of the versions we aim to support: @@ -116,11 +95,13 @@ Since xterm.js is typically implemented as a developer tool, only modern browser - Safari latest - IE11 -Xterm.js works seamlessly in Electron apps and may even work on earlier versions of the browsers but these are the browsers we strive to keep working. +Xterm.js works seamlessly in [Electron](https://electronjs.org/) apps and may even work on earlier versions of the browsers, these are the versions we strive to keep working. ## API -The current full API documentation is available in the [TypeScript declaration file on the repository](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), switch the tag (press `w` when viewing the file) to point at the specific version tag you're using. +The full API for xterm.js is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API. + +Note that some APIs are marked *experimental*, these are added to enable experimentation with new ideas without committing to support it like a normal [semver](https://semver.org/) API. Note that these APIs can change radically between versions so be sure to read release notes if you plan on using experimental APIs. ## Real-world uses Xterm.js is used in several world-class applications to provide great terminal experiences. @@ -182,7 +163,7 @@ Do you use xterm.js in your application as well? Please [open a Pull Request](ht Xterm.js follows a monthly release cycle roughly. -The existing releases are available at this GitHub repo's [Releases](https://github.com/sourcelair/xterm.js/releases), while the roadmap is available as [Milestones](https://github.com/sourcelair/xterm.js/milestones). +All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), while a rough roadmap is available by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones). ## Contributing diff --git a/demo/client.ts b/demo/client.ts index 6c69899e..acc83cb8 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -14,6 +14,7 @@ import * as fullscreen from '../lib/addons/fullscreen/fullscreen'; import * as search from '../lib/addons/search/search'; import * as webLinks from '../lib/addons/webLinks/webLinks'; import * as winptyCompat from '../lib/addons/winptyCompat/winptyCompat'; +import { ISearchOptions } from '../lib/addons/search/Interfaces'; // Pulling in the module's types relies on the above, it's looks a // little weird here as we're importing "this" module @@ -50,6 +51,14 @@ function setPadding(): void { term.fit(); } +function getSearchOptions(): ISearchOptions { + return { + regex: (document.getElementById('regex') as HTMLInputElement).checked, + wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked, + caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked + }; +} + createTerminal(); const disposeRecreateButtonHandler = () => { @@ -97,26 +106,15 @@ function createTerminal(): void { addDomListener(paddingElement, 'change', setPadding); - addDomListener(actionElements.findNext, 'keypress', (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - const searchOptions = { - regex: (document.getElementById('regex') as HTMLInputElement).checked, - wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked, - caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked - }; - term.findNext(actionElements.findNext.value, searchOptions); - } + addDomListener(actionElements.findNext, 'keyup', (e) => { + const searchOptions = getSearchOptions(); + searchOptions.incremental = e.key !== `Enter`; + term.findNext(actionElements.findNext.value, searchOptions); }); - addDomListener(actionElements.findPrevious, 'keypress', (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - const searchOptions = { - regex: (document.getElementById('regex') as HTMLInputElement).checked, - wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked, - caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked - }; - term.findPrevious(actionElements.findPrevious.value, searchOptions); + + addDomListener(actionElements.findPrevious, 'keyup', (e) => { + if (e.key === `Enter`) { + term.findPrevious(actionElements.findPrevious.value, getSearchOptions()); } }); diff --git a/demo/server.js b/demo/server.js index ebe2f644..5ff9ca61 100644 --- a/demo/server.js +++ b/demo/server.js @@ -1,100 +1,106 @@ var express = require('express'); -var app = express(); -var expressWs = require('express-ws')(app); +var expressWs = require('express-ws'); var os = require('os'); var pty = require('node-pty'); -var terminals = {}, - logs = {}; +function startServer() { + var app = express(); + expressWs(app); -app.use('/build', express.static(__dirname + '/../build')); + var terminals = {}, + logs = {}; -app.get('/', function(req, res){ - res.sendFile(__dirname + '/index.html'); -}); + app.use('/build', express.static(__dirname + '/../build')); -app.get('/style.css', function(req, res){ - res.sendFile(__dirname + '/style.css'); -}); - -app.get('/dist/client-bundle.js', function(req, res){ - res.sendFile(__dirname + '/dist/client-bundle.js'); -}); - -app.post('/terminals', function (req, res) { - var cols = parseInt(req.query.cols), - rows = parseInt(req.query.rows), - term = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : 'bash', [], { - name: 'xterm-color', - cols: cols || 80, - rows: rows || 24, - cwd: process.env.PWD, - env: process.env - }); - - console.log('Created terminal with PID: ' + term.pid); - terminals[term.pid] = term; - logs[term.pid] = ''; - term.on('data', function(data) { - logs[term.pid] += data; + app.get('/', function(req, res){ + res.sendFile(__dirname + '/index.html'); }); - res.send(term.pid.toString()); - res.end(); -}); -app.post('/terminals/:pid/size', function (req, res) { - var pid = parseInt(req.params.pid), - cols = parseInt(req.query.cols), - rows = parseInt(req.query.rows), - term = terminals[pid]; + app.get('/style.css', function(req, res){ + res.sendFile(__dirname + '/style.css'); + }); - term.resize(cols, rows); - console.log('Resized terminal ' + pid + ' to ' + cols + ' cols and ' + rows + ' rows.'); - res.end(); -}); + app.get('/dist/client-bundle.js', function(req, res){ + res.sendFile(__dirname + '/dist/client-bundle.js'); + }); -app.ws('/terminals/:pid', function (ws, req) { - var term = terminals[parseInt(req.params.pid)]; - console.log('Connected to terminal ' + term.pid); - ws.send(logs[term.pid]); + app.post('/terminals', function (req, res) { + var cols = parseInt(req.query.cols), + rows = parseInt(req.query.rows), + term = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : 'bash', [], { + name: 'xterm-color', + cols: cols || 80, + rows: rows || 24, + cwd: process.env.PWD, + env: process.env + }); - function buffer(socket, timeout) { - let s = ''; - let sender = null; - return (data) => { - s += data; - if (!sender) { - sender = setTimeout(() => { - socket.send(s); - s = ''; - sender = null; - }, timeout); - } - }; - } - const send = buffer(ws, 5); + console.log('Created terminal with PID: ' + term.pid); + terminals[term.pid] = term; + logs[term.pid] = ''; + term.on('data', function(data) { + logs[term.pid] += data; + }); + res.send(term.pid.toString()); + res.end(); + }); - term.on('data', function(data) { - try { - send(data); - } catch (ex) { - // The WebSocket is not open, ignore + app.post('/terminals/:pid/size', function (req, res) { + var pid = parseInt(req.params.pid), + cols = parseInt(req.query.cols), + rows = parseInt(req.query.rows), + term = terminals[pid]; + + term.resize(cols, rows); + console.log('Resized terminal ' + pid + ' to ' + cols + ' cols and ' + rows + ' rows.'); + res.end(); + }); + + app.ws('/terminals/:pid', function (ws, req) { + var term = terminals[parseInt(req.params.pid)]; + console.log('Connected to terminal ' + term.pid); + ws.send(logs[term.pid]); + + function buffer(socket, timeout) { + let s = ''; + let sender = null; + return (data) => { + s += data; + if (!sender) { + sender = setTimeout(() => { + socket.send(s); + s = ''; + sender = null; + }, timeout); + } + }; } - }); - ws.on('message', function(msg) { - term.write(msg); - }); - ws.on('close', function () { - term.kill(); - console.log('Closed terminal ' + term.pid); - // Clean things up - delete terminals[term.pid]; - delete logs[term.pid]; - }); -}); + const send = buffer(ws, 5); -var port = process.env.PORT || 3000, - host = os.platform() === 'win32' ? '127.0.0.1' : '0.0.0.0'; + term.on('data', function(data) { + try { + send(data); + } catch (ex) { + // The WebSocket is not open, ignore + } + }); + ws.on('message', function(msg) { + term.write(msg); + }); + ws.on('close', function () { + term.kill(); + console.log('Closed terminal ' + term.pid); + // Clean things up + delete terminals[term.pid]; + delete logs[term.pid]; + }); + }); -console.log('App listening to http://' + host + ':' + port); -app.listen(port, host); + var port = process.env.PORT || 3000, + host = os.platform() === 'win32' ? '127.0.0.1' : '0.0.0.0'; + + console.log('App listening to http://' + host + ':' + port); + app.listen(port, host); +} + +module.exports = startServer; diff --git a/demo/start.js b/demo/start.js index ad9e9f2f..78f1ff1d 100644 --- a/demo/start.js +++ b/demo/start.js @@ -8,9 +8,9 @@ const cp = require('child_process'); const path = require('path'); const webpack = require('webpack'); +const startServer = require('./server.js'); -// Launch server -cp.spawn('node', [path.resolve(__dirname, 'server.js')], { stdio: 'inherit' }); +startServer(); // Build/watch client source const clientConfig = { @@ -22,6 +22,11 @@ const clientConfig = { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ + }, + { + test: /\.js$/, + use: ["source-map-loader"], + enforce: "pre" } ] }, diff --git a/fixtures/typings-test/typings-test.ts b/fixtures/typings-test/typings-test.ts index 13da6961..87d911c2 100644 --- a/fixtures/typings-test/typings-test.ts +++ b/fixtures/typings-test/typings-test.ts @@ -4,7 +4,7 @@ /// -import { Terminal } from 'xterm'; +import { Terminal, IDisposable } from 'xterm'; namespace constructor { { @@ -119,6 +119,12 @@ namespace methods_core { const t: Terminal = new Terminal(); t.attachCustomKeyEventHandler((e: KeyboardEvent) => true); t.attachCustomKeyEventHandler((e: KeyboardEvent) => false); + const d1: IDisposable = t.addCsiHandler("x", + (params: number[], collect: string): boolean => params[0]===1); + d1.dispose(); + const d2: IDisposable = t.addOscHandler(199, + (data: string): boolean => true); + d2.dispose(); } namespace options { { diff --git a/package.json b/package.json index c5918cb5..0d797f78 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "nodemon": "1.10.2", "nyc": "^11.8.0", "sorcery": "^0.10.0", - "source-map-loader": "^0.2.3", + "source-map-loader": "^0.2.4", "ts-loader": "^4.5.0", "tslint": "^5.9.1", "tslint-consistent-codestyle": "^1.13.0", @@ -47,6 +47,7 @@ }, "scripts": { "start": "node demo/start", + "start-debug": "node --inspect-brk demo/start", "start-zmodem": "node demo/zmodem/app", "lint": "tslint 'src/**/*.ts' './demo/**/*.ts'", "pretest": "npm run layering", diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 6b6adc11..0546dfe8 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -355,7 +355,7 @@ describe('Buffer', () => { let terminal: TestTerminal; beforeEach(() => { - terminal = new TestTerminal({rows: 5, cols: 10}); + terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5}); }); it('multiline ascii', () => { @@ -516,9 +516,38 @@ describe('Buffer', () => { assert.deepEqual([(j / terminal.cols) | 0, j % terminal.cols], bufferIndex); } }); + + it('test fully wrapped buffer up to last char', () => { + const input = Array(6).join('1234567890'); + terminal.writeSync(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 0; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX]); + } + }); + + it('test fully wrapped buffer up to last char with full width odd', () => { + const input = 'aï¿¥\u0301aï¿¥\u0301aï¿¥\u0301aï¿¥\u0301aï¿¥\u0301aï¿¥\u0301aï¿¥\u0301aï¿¥\u0301' + + 'aï¿¥\u0301aï¿¥\u0301aï¿¥\u0301aï¿¥\u0301aï¿¥\u0301aï¿¥\u0301aï¿¥\u0301'; + terminal.writeSync(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 0; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.equal( + (!(i % 3)) + ? input[i] + : (i % 3 === 1) + ? input.substr(i, 2) + : input.substr(i - 1, 2), + terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX]); + } + }); }); describe('BufferStringIterator', function(): void { - it('iterator does not ovrflow buffer limits', function(): void { + it('iterator does not overflow buffer limits', function(): void { const terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5}); const data = [ 'aaaaaaaaaa', diff --git a/src/Buffer.ts b/src/Buffer.ts index 10f1d1b2..74750a8b 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -252,7 +252,7 @@ export class Buffer implements IBuffer { while (stringIndex) { const line = this.lines.get(lineIndex); if (!line) { - [-1, -1]; + return [-1, -1]; } for (let i = 0; i < line.length; ++i) { stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length; @@ -275,7 +275,7 @@ export class Buffer implements IBuffer { * @param startCol The column to start at. * @param endCol The column to end at. */ - public translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol: number = 0, endCol: number = null): string { + public translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol: number = 0, endCol?: number): string { const line = this.lines.get(lineIndex); if (!line) { return ''; diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index 93b2759f..fbf8b051 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -325,5 +325,10 @@ describe('BufferLine', function(): void { chai.expect(line.translateToString(false)).equal(' '); chai.expect(line.translateToString(true)).equal(''); }); + it('should work with endCol=0', () => { + const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE], false); + line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); + chai.expect(line.translateToString(true, 0, 0)).equal(''); + }); }); }); diff --git a/src/BufferLine.ts b/src/BufferLine.ts index bcc3d1bb..3f93af62 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -118,13 +118,12 @@ export class BufferLineJSArray implements IBufferLine { return 0; } - public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = null): string { - let length = endCol || this.length; + public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length): string { if (trimRight) { - length = Math.min(length, this.getTrimmedLength()); + endCol = Math.min(endCol, this.getTrimmedLength()); } let result = ''; - while (startCol < length) { + while (startCol < endCol) { result += this.get(startCol)[CHAR_DATA_CHAR_INDEX] || WHITESPACE_CELL_CHAR; startCol += this.get(startCol)[CHAR_DATA_WIDTH_INDEX] || 1; } @@ -305,13 +304,12 @@ export class BufferLine implements IBufferLine { return 0; } - public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = null): string { - let length = endCol || this.length; + public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length): string { if (trimRight) { - length = Math.min(length, this.getTrimmedLength()); + endCol = Math.min(endCol, this.getTrimmedLength()); } let result = ''; - while (startCol < length) { + while (startCol < endCol) { const stringData = this._data[startCol * CELL_SIZE + Cell.STRING]; result += (stringData & IS_COMBINED_BIT_MASK) ? this._combined[startCol] : (stringData) ? String.fromCharCode(stringData) : WHITESPACE_CELL_CHAR; startCol += this._data[startCol * CELL_SIZE + Cell.WIDTH] || 1; diff --git a/src/EscapeSequenceParser.test.ts b/src/EscapeSequenceParser.test.ts index 3e310b08..011ff2ae 100644 --- a/src/EscapeSequenceParser.test.ts +++ b/src/EscapeSequenceParser.test.ts @@ -1187,6 +1187,73 @@ describe('EscapeSequenceParser', function (): void { 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; }); + parser2.parse(INPUT); + chai.expect(csi).eql([], 'Should not fallback to original handler'); + chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); + }); + it('Allow fallback', () => { + const csiCustom: [string, number[], string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); + parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return false; }); + parser2.parse(INPUT); + chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']], 'Should fallback to original handler'); + chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); + }); + it('Multiple custom handlers fallback once', () => { + const csiCustom: [string, number[], string][] = []; + const csiCustom2: [string, number[], string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); + parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); + parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params, collect]); return false; }); + parser2.parse(INPUT); + chai.expect(csi).eql([], 'Should not fallback to original handler'); + chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); + chai.expect(csiCustom2).eql([['m', [1, 31], ''], ['m', [0], '']]); + }); + it('Multiple custom handlers no fallback', () => { + const csiCustom: [string, number[], string][] = []; + const csiCustom2: [string, number[], string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); + parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); + parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params, collect]); return true; }); + parser2.parse(INPUT); + chai.expect(csi).eql([], 'Should not fallback to original handler'); + chai.expect(csiCustom).eql([], 'Should not fallback once'); + chai.expect(csiCustom2).eql([['m', [1, 31], ''], ['m', [0], '']]); + }); + it('Execution order should go from latest handler down to the original', () => { + const order: number[] = []; + parser2.setCsiHandler('m', () => order.push(1)); + parser2.addCsiHandler('m', () => { order.push(2); return false; }); + parser2.addCsiHandler('m', () => { order.push(3); return false; }); + parser2.parse('\x1b[0m'); + chai.expect(order).eql([3, 2, 1]); + }); + it('Dispose should work', () => { + const csiCustom: [string, number[], string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); + const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); + customHandler.dispose(); + parser2.parse(INPUT); + chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); + chai.expect(csiCustom).eql([], 'Should not use custom handler as it was disposed'); + }); + it('Should not corrupt the parser when dispose is called twice', () => { + const csiCustom: [string, number[], string][] = []; + parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect])); + const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); + customHandler.dispose(); + customHandler.dispose(); + parser2.parse(INPUT); + chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); + chai.expect(csiCustom).eql([], 'Should not use custom handler as it was disposed'); + }); + }); it('EXECUTE handler', function (): void { parser2.setExecuteHandler('\n', function (): void { exe.push('\n'); @@ -1214,6 +1281,73 @@ describe('EscapeSequenceParser', function (): void { 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; }); + parser2.parse(INPUT); + chai.expect(osc).eql([], 'Should not fallback to original handler'); + chai.expect(oscCustom).eql([[1, 'foo=bar']]); + }); + it('Allow fallback', () => { + const oscCustom: [number, string][] = []; + parser2.setOscHandler(1, data => osc.push([1, data])); + parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return false; }); + parser2.parse(INPUT); + chai.expect(osc).eql([[1, 'foo=bar']], 'Should fallback to original handler'); + chai.expect(oscCustom).eql([[1, 'foo=bar']]); + }); + it('Multiple custom handlers fallback once', () => { + const oscCustom: [number, string][] = []; + const oscCustom2: [number, string][] = []; + parser2.setOscHandler(1, data => osc.push([1, data])); + parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); + parser2.addOscHandler(1, data => { oscCustom2.push([1, data]); return false; }); + parser2.parse(INPUT); + chai.expect(osc).eql([], 'Should not fallback to original handler'); + chai.expect(oscCustom).eql([[1, 'foo=bar']]); + chai.expect(oscCustom2).eql([[1, 'foo=bar']]); + }); + it('Multiple custom handlers no fallback', () => { + const oscCustom: [number, string][] = []; + const oscCustom2: [number, string][] = []; + parser2.setOscHandler(1, data => osc.push([1, data])); + parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); + parser2.addOscHandler(1, data => { oscCustom2.push([1, data]); return true; }); + parser2.parse(INPUT); + chai.expect(osc).eql([], 'Should not fallback to original handler'); + chai.expect(oscCustom).eql([], 'Should not fallback once'); + chai.expect(oscCustom2).eql([[1, 'foo=bar']]); + }); + it('Execution order should go from latest handler down to the original', () => { + const order: number[] = []; + parser2.setOscHandler(1, () => order.push(1)); + parser2.addOscHandler(1, () => { order.push(2); return false; }); + parser2.addOscHandler(1, () => { order.push(3); return false; }); + parser2.parse('\x1b]1;foo=bar\x1b\\'); + chai.expect(order).eql([3, 2, 1]); + }); + it('Dispose should work', () => { + const oscCustom: [number, string][] = []; + parser2.setOscHandler(1, data => osc.push([1, data])); + const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); + customHandler.dispose(); + parser2.parse(INPUT); + chai.expect(osc).eql([[1, 'foo=bar']]); + chai.expect(oscCustom).eql([], 'Should not use custom handler as it was disposed'); + }); + it('Should not corrupt the parser when dispose is called twice', () => { + const oscCustom: [number, string][] = []; + parser2.setOscHandler(1, data => osc.push([1, data])); + const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); + customHandler.dispose(); + customHandler.dispose(); + parser2.parse(INPUT); + chai.expect(osc).eql([[1, 'foo=bar']]); + chai.expect(oscCustom).eql([], 'Should not use custom handler as it was disposed'); + }); + }); it('DCS handler', function (): void { parser2.setDcsHandler('+p', { hook: function (collect: string, params: number[], flag: number): void { diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index 0177cdfe..cee9957b 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -4,9 +4,17 @@ */ import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from './Types'; +import { IDisposable } from 'xterm'; import { Disposable } from './common/Lifecycle'; import { utf16ToString } from './common/TypedArrayUtils'; +interface IHandlerCollection { + [key: string]: T[]; +} + +type CsiHandler = (params: number[], collect: string) => boolean | void; +type OscHandler = (data: string) => boolean | void; + /** * Returns an array filled with numbers between the low and high parameters (right exclusive). * @param low The low number. @@ -224,9 +232,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // handler lookup containers protected _printHandler: (data: Uint16Array, start: number, end: number) => void; protected _executeHandlers: any; - protected _csiHandlers: any; + protected _csiHandlers: IHandlerCollection; protected _escHandlers: any; - protected _oscHandlers: any; + protected _oscHandlers: IHandlerCollection; protected _dcsHandlers: any; protected _activeDcsHandler: IDcsHandler | null; protected _errorHandler: (state: IParsingState) => IParsingState; @@ -280,8 +288,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._errorHandlerFb = null; this._printHandler = null; this._executeHandlers = null; - this._csiHandlers = null; this._escHandlers = null; + this._csiHandlers = null; this._oscHandlers = null; this._dcsHandlers = null; this._activeDcsHandler = null; @@ -305,8 +313,24 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._executeHandlerFb = callback; } + addCsiHandler(flag: string, callback: CsiHandler): IDisposable { + const index = flag.charCodeAt(0); + if (this._csiHandlers[index] === undefined) { + this._csiHandlers[index] = []; + } + const handlerList = this._csiHandlers[index]; + handlerList.push(callback); + return { + dispose: () => { + const handlerIndex = handlerList.indexOf(callback); + if (handlerIndex !== -1) { + handlerList.splice(handlerIndex, 1); + } + } + }; + } setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void { - this._csiHandlers[flag.charCodeAt(0)] = callback; + this._csiHandlers[flag.charCodeAt(0)] = [callback]; } clearCsiHandler(flag: string): void { if (this._csiHandlers[flag.charCodeAt(0)]) delete this._csiHandlers[flag.charCodeAt(0)]; @@ -325,8 +349,23 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._escHandlerFb = callback; } + addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + if (this._oscHandlers[ident] === undefined) { + this._oscHandlers[ident] = []; + } + const handlerList = this._oscHandlers[ident]; + handlerList.push(callback); + return { + dispose: () => { + const handlerIndex = handlerList.indexOf(callback); + if (handlerIndex !== -1) { + handlerList.splice(handlerIndex, 1); + } + } + }; + } setOscHandler(ident: number, callback: (data: string) => void): void { - this._oscHandlers[ident] = callback; + this._oscHandlers[ident] = [callback]; } clearOscHandler(ident: number): void { if (this._oscHandlers[ident]) delete this._oscHandlers[ident]; @@ -462,9 +501,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } break; case ParserAction.CSI_DISPATCH: - callback = this._csiHandlers[code]; - if (callback) callback(params, collect); - else this._csiHandlerFb(collect, params, code); + // Trigger CSI Handler + const handlers = this._csiHandlers[code]; + let j = handlers ? handlers.length - 1 : -1; + for (; j >= 0; j--) { + if (handlers[j](params, collect)) { + break; + } + } + if (j < 0) { + this._csiHandlerFb(collect, params, code); + } break; case ParserAction.PARAM: if (code === 0x3b) params.push(0); @@ -539,9 +586,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // or with an explicit NaN OSC handler const identifier = parseInt(osc.substring(0, idx)); const content = osc.substring(idx + 1); - callback = this._oscHandlers[identifier]; - if (callback) callback(content); - else this._oscHandlerFb(identifier, content); + // Trigger OSC Handler + const handlers = this._oscHandlers[identifier]; + let j = handlers ? handlers.length - 1 : -1; + for (; j >= 0; j--) { + if (handlers[j](content)) { + break; + } + } + if (j < 0) { + this._oscHandlerFb(identifier, content); + } } } if (code === 0x1b) transition |= ParserState.ESCAPE; diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 7438cf1e..3a2dab11 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -12,6 +12,7 @@ import { FLAGS } from './renderer/Types'; import { wcwidth } from './CharWidth'; import { EscapeSequenceParser } from './EscapeSequenceParser'; import { ICharset } from './core/Types'; +import { IDisposable } from 'xterm'; import { Disposable } from './common/Lifecycle'; import { concat, utf16ToString } from './common/TypedArrayUtils'; @@ -481,6 +482,13 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.updateRange(buffer.y); } + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { + return this._parser.addCsiHandler(flag, callback); + } + addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + return this._parser.addOscHandler(ident, callback); + } + /** * BEL * Bell (Ctrl-G). diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 2ec3ed40..53247c95 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -219,9 +219,17 @@ export class Linkifier extends EventEmitter implements ILinkifier { // also correct regex and string search offsets for the next loop run stringIndex = text.indexOf(uri, stringIndex + 1); rex.lastIndex = stringIndex + uri.length; + if (stringIndex < 0) { + // invalid stringIndex (should not have happened) + break; + } // get the buffer index as [absolute row, col] for the match const bufferIndex = this._terminal.buffer.stringIndexToBufferIndex(rowIndex, stringIndex); + if (bufferIndex[0] < 0) { + // invalid bufferIndex (should not have happened) + break; + } const line = this._terminal.buffer.lines.get(bufferIndex[0]); const char = line.get(bufferIndex[1]); diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 4bac0400..1aea1cb5 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -198,7 +198,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager } } else { // Get first row - const startRowEndCol = start[1] === end[1] ? end[0] : null; + const startRowEndCol = start[1] === end[1] ? end[0] : undefined; result.push(this._buffer.translateBufferLineToString(start[1], true, start[0], startRowEndCol)); // Get middle rows diff --git a/src/Terminal.ts b/src/Terminal.ts index bc8fb103..bed45e46 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1413,6 +1413,15 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this._customKeyEventHandler = customKeyEventHandler; } + /** Add handler for CSI escape sequence. See xterm.d.ts for details. */ + public addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { + return this._inputHandler.addCsiHandler(flag, callback); + } + /** Add handler for OSC escape sequence. See xterm.d.ts for details. */ + public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + return this._inputHandler.addOscHandler(ident, callback); + } + /** * Registers a link matcher, allowing custom link patterns to be matched and * handled. diff --git a/src/Types.ts b/src/Types.ts index 2cc14183..ca50afd5 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -492,6 +492,8 @@ export interface IEscapeSequenceParser extends IDisposable { setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void; clearCsiHandler(flag: string): void; setCsiHandlerFallback(callback: (collect: string, params: number[], flag: number) => void): void; + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable; + addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; setEscHandler(collectAndFlag: string, callback: () => void): void; clearEscHandler(collectAndFlag: string): void; diff --git a/src/addons/fullscreen/fullscreen.ts b/src/addons/fullscreen/fullscreen.ts index 4d05e904..083c00c2 100644 --- a/src/addons/fullscreen/fullscreen.ts +++ b/src/addons/fullscreen/fullscreen.ts @@ -22,6 +22,7 @@ export function toggleFullScreen(term: Terminal, fullscreen: boolean): void { fn = term.element.classList.add; } + fn = fn.bind(term.element.classList); fn('fullscreen'); } diff --git a/src/addons/search/Interfaces.ts b/src/addons/search/Interfaces.ts index af06c5d1..a1f05895 100644 --- a/src/addons/search/Interfaces.ts +++ b/src/addons/search/Interfaces.ts @@ -25,6 +25,11 @@ 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 { diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 7919932a..96cd845d 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -4,16 +4,24 @@ */ import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult } from './Interfaces'; -const nonWordCharacters = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?'; + +const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?'; +const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs /** * A class that knows how to search the terminal and how to display the results. */ export class SearchHelper implements ISearchHelper { + /** + * translateBufferLineToStringWithWrap is a fairly expensive call. + * We memoize the calls into an array that has a time based ttl. + * _linesCache is also invalidated when the terminal cursor moves. + */ + private _linesCache: string[] = null; + private _linesCacheTimeoutId = 0; + constructor(private _terminal: ISearchAddonTerminal) { - // TODO: Search for multiple instances on 1 line - // TODO: Don't use the actual selection, instead use a "find selection" so multiple instances can be highlighted - // TODO: Highlight other instances in the viewport + this._destroyLinesCache = this._destroyLinesCache.bind(this); } /** @@ -24,32 +32,47 @@ export class SearchHelper implements ISearchHelper { * @return Whether a result was found. */ public findNext(term: string, searchOptions?: ISearchOptions): boolean { + const selectionManager = this._terminal._core.selectionManager; + const {incremental} = searchOptions; + let result: ISearchResult; + if (!term || term.length === 0) { + selectionManager.clearSelection(); return false; } - let result: ISearchResult; - + let startCol: number = 0; let startRow = this._terminal._core.buffer.ydisp; - if (this._terminal._core.selectionManager.selectionEnd) { + + if (selectionManager.selectionEnd) { // Start from the selection end if there is a selection + // For incremental search, use existing row if (this._terminal.getSelection().length !== 0) { - startRow = this._terminal._core.selectionManager.selectionEnd[1]; + startRow = incremental ? selectionManager.selectionStart[1] : selectionManager.selectionEnd[1]; + startCol = incremental ? selectionManager.selectionStart[0] : selectionManager.selectionEnd[0]; } } - // Search from ydisp + 1 to end - for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) { - result = this._findInLine(term, y, searchOptions); - if (result) { - break; - } - } + this._initLinesCache(); - // Search from the top to the current ydisp + // Search startRow + result = this._findInLine(term, startRow, startCol, searchOptions); + + // Search from startRow + 1 to end if (!result) { - for (let y = 0; y < startRow; y++) { - result = this._findInLine(term, y, searchOptions); + for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) { + result = this._findInLine(term, y, 0, searchOptions); + if (result) { + break; + } + } + } + + // Search from the top to the startRow (search the whole startRow again in + // case startCol > 0) + if (!result) { + for (let y = 0; y <= startRow; y++) { + result = this._findInLine(term, y, 0, searchOptions); if (result) { break; } @@ -68,32 +91,47 @@ export class SearchHelper implements ISearchHelper { * @return Whether a result was found. */ public findPrevious(term: string, searchOptions?: ISearchOptions): boolean { + const selectionManager = this._terminal._core.selectionManager; + let result: ISearchResult; + if (!term || term.length === 0) { + selectionManager.clearSelection(); return false; } - let result: ISearchResult; - + const isReverseSearch = true; let startRow = this._terminal._core.buffer.ydisp; - if (this._terminal._core.selectionManager.selectionStart) { - // Start from the selection end if there is a selection + let startCol: number = this._terminal._core.buffer.lines.get(startRow).length; + + if (selectionManager.selectionStart) { + // Start from the selection start if there is a selection if (this._terminal.getSelection().length !== 0) { - startRow = this._terminal._core.selectionManager.selectionStart[1]; + startRow = selectionManager.selectionStart[1]; + startCol = selectionManager.selectionStart[0]; } } - // Search from ydisp + 1 to end - for (let y = startRow - 1; y >= 0; y--) { - result = this._findInLine(term, y, searchOptions); - if (result) { - break; - } - } + this._initLinesCache(); - // Search from the top to the current ydisp + // Search startRow + result = this._findInLine(term, startRow, startCol, searchOptions, isReverseSearch); + + // Search from startRow - 1 to top if (!result) { - for (let y = this._terminal._core.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) { - result = this._findInLine(term, y, searchOptions); + for (let y = startRow - 1; y >= 0; y--) { + result = this._findInLine(term, y, this._terminal._core.buffer.lines.get(y).length, searchOptions, isReverseSearch); + if (result) { + break; + } + } + } + + // Search from the bottom to startRow (search the whole startRow again in + // case startCol > 0) + if (!result) { + const searchFrom = this._terminal._core.buffer.ybase + this._terminal.rows - 1; + for (let y = searchFrom; y >= startRow; y--) { + result = this._findInLine(term, y, this._terminal._core.buffer.lines.get(y).length, searchOptions, isReverseSearch); if (result) { break; } @@ -104,6 +142,28 @@ export class SearchHelper implements ISearchHelper { return this._selectResult(result); } + /** + * Sets up a line cache with a ttl + */ + private _initLinesCache(): void { + if (!this._linesCache) { + this._linesCache = new Array(this._terminal._core.buffer.length); + this._terminal.on('cursormove', this._destroyLinesCache); + } + + window.clearTimeout(this._linesCacheTimeoutId); + this._linesCacheTimeoutId = window.setTimeout(() => this._destroyLinesCache(), LINES_CACHE_TIME_TO_LIVE); + } + + private _destroyLinesCache(): void { + this._linesCache = null; + this._terminal.off('cursormove', this._destroyLinesCache); + if (this._linesCacheTimeoutId) { + window.clearTimeout(this._linesCacheTimeoutId); + this._linesCacheTimeoutId = 0; + } + } + /** * A found substring is a whole word if it doesn't have an alphanumeric character directly adjacent to it. * @param searchIndex starting indext of the potential whole word substring @@ -111,8 +171,8 @@ export class SearchHelper implements ISearchHelper { * @param term the substring that starts at searchIndex */ private _isWholeWord(searchIndex: number, line: string, term: string): boolean { - return (((searchIndex === 0) || (nonWordCharacters.indexOf(line[searchIndex - 1]) !== -1)) && - (((searchIndex + term.length) === line.length) || (nonWordCharacters.indexOf(line[searchIndex + term.length]) !== -1))); + return (((searchIndex === 0) || (NON_WORD_CHARACTERS.indexOf(line[searchIndex - 1]) !== -1)) && + (((searchIndex + term.length) === line.length) || (NON_WORD_CHARACTERS.indexOf(line[searchIndex + term.length]) !== -1))); } /** @@ -121,65 +181,88 @@ export class SearchHelper implements ISearchHelper { * started on an earlier line then it is skipped since it will be properly searched when the terminal line that the * text starts on is searched. * @param term The search term. - * @param y The line to search. + * @param row The line to start the search from. + * @param col The column to start the search from. * @param searchOptions Search options. * @return The search result if it was found. */ - protected _findInLine(term: string, y: number, searchOptions: ISearchOptions = {}): ISearchResult { - if (this._terminal._core.buffer.lines.get(y).isWrapped) { + protected _findInLine(term: string, row: number, col: number, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult { + if (this._terminal._core.buffer.lines.get(row).isWrapped) { return; } - const stringLine = this.translateBufferLineToStringWithWrap(y, true); - const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase(); - const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase(); - let searchIndex = -1; - - if (searchOptions.regex) { - const searchRegex = RegExp(searchTerm, 'g'); - const foundTerm = searchRegex.exec(searchStringLine); - if (foundTerm && foundTerm[0].length > 0) { - searchIndex = searchRegex.lastIndex - foundTerm[0].length; - term = foundTerm[0]; + let stringLine = this._linesCache ? this._linesCache[row] : void 0; + if (stringLine === void 0) { + stringLine = this.translateBufferLineToStringWithWrap(row, true); + if (this._linesCache) { + this._linesCache[row] = stringLine; } - } else { - searchIndex = searchStringLine.indexOf(searchTerm); } - if (searchIndex >= 0) { - // Adjust the row number and search index if needed since a "line" of text can span multiple rows - if (searchIndex >= this._terminal.cols) { - y += Math.floor(searchIndex / this._terminal.cols); - searchIndex = searchIndex % this._terminal.cols; + const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase(); + const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase(); + + let resultIndex = -1; + if (searchOptions.regex) { + const searchRegex = RegExp(searchTerm, 'g'); + let foundTerm: RegExpExecArray; + if (isReverseSearch) { + // This loop will get the resultIndex of the _last_ regex match in the range 0..col + while (foundTerm = searchRegex.exec(searchStringLine.slice(0, col))) { + resultIndex = searchRegex.lastIndex - foundTerm[0].length; + term = foundTerm[0]; + searchRegex.lastIndex -= (term.length - 1); + } + } else { + foundTerm = searchRegex.exec(searchStringLine.slice(col)); + if (foundTerm && foundTerm[0].length > 0) { + resultIndex = col + (searchRegex.lastIndex - foundTerm[0].length); + term = foundTerm[0]; + } } - if (searchOptions.wholeWord && !this._isWholeWord(searchIndex, searchStringLine, term)) { + } else { + if (isReverseSearch) { + if (col - searchTerm.length >= 0) { + resultIndex = searchStringLine.lastIndexOf(searchTerm, col - searchTerm.length); + } + } else { + resultIndex = searchStringLine.indexOf(searchTerm, col); + } + } + + if (resultIndex >= 0) { + // Adjust the row number and search index if needed since a "line" of text can span multiple rows + if (resultIndex >= this._terminal.cols) { + row += Math.floor(resultIndex / this._terminal.cols); + resultIndex = resultIndex % this._terminal.cols; + } + if (searchOptions.wholeWord && !this._isWholeWord(resultIndex, searchStringLine, term)) { return; } - const line = this._terminal._core.buffer.lines.get(y); + const line = this._terminal._core.buffer.lines.get(row); - for (let i = 0; i < searchIndex; i++) { + for (let i = 0; i < resultIndex; i++) { const charData = line.get(i); // Adjust the searchIndex to normalize emoji into single chars const char = charData[1/*CHAR_DATA_CHAR_INDEX*/]; if (char.length > 1) { - searchIndex -= char.length - 1; + resultIndex -= char.length - 1; } // Adjust the searchIndex for empty characters following wide unicode // chars (eg. CJK) const charWidth = charData[2/*CHAR_DATA_WIDTH_INDEX*/]; if (charWidth === 0) { - searchIndex++; + resultIndex++; } } return { term, - col: searchIndex, - row: y + col: resultIndex, + row }; } } - /** * 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 diff --git a/src/addons/search/search.test.ts b/src/addons/search/search.test.ts index a70fd115..6551fa8b 100644 --- a/src/addons/search/search.test.ts +++ b/src/addons/search/search.test.ts @@ -29,8 +29,11 @@ class MockTerminal { } class TestSearchHelper extends SearchHelper { - public findInLine(term: string, y: number, searchOptions?: ISearchOptions): ISearchResult { - return this._findInLine(term, y, searchOptions); + 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); } } @@ -245,5 +248,66 @@ describe('search addon', () => { 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/tsconfig.json b/src/addons/search/tsconfig.json index c42412be..87899cda 100644 --- a/src/addons/search/tsconfig.json +++ b/src/addons/search/tsconfig.json @@ -3,6 +3,7 @@ "module": "commonjs", "target": "es5", "lib": [ + "dom", "es5" ], "rootDir": ".", diff --git a/src/addons/webLinks/webLinks.test.ts b/src/addons/webLinks/webLinks.test.ts index 8ada2510..1e8a4ae7 100644 --- a/src/addons/webLinks/webLinks.test.ts +++ b/src/addons/webLinks/webLinks.test.ts @@ -63,4 +63,28 @@ describe('webLinks addon', () => { assert.equal(uri, 'http://foo.com/colon:test'); }); + + it('should not allow " character at the end of a URI enclosed with ""', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = '"http://foo.com/"'; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://foo.com/'); + }); + + it('should not allow \' character at the end of a URI enclosed with \'\'', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = '\'http://foo.com/\''; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://foo.com/'); + }); }); diff --git a/src/addons/webLinks/webLinks.ts b/src/addons/webLinks/webLinks.ts index 75d79104..f0d69cc5 100644 --- a/src/addons/webLinks/webLinks.ts +++ b/src/addons/webLinks/webLinks.ts @@ -14,7 +14,7 @@ const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})'; const localHostClause = '(localhost)'; const portClause = '(:\\d{1,5})'; const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?'; -const pathClause = '(\\/[\\/\\w\\.\\-%~:]*)*([^:\\s])'; +const pathClause = '(\\/[\\/\\w\\.\\-%~:]*)*([^:"\'\\s])'; const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*'; const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?'; const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?'; diff --git a/src/addons/winptyCompat/winptyCompat.ts b/src/addons/winptyCompat/winptyCompat.ts index 2513dce6..aec580ed 100644 --- a/src/addons/winptyCompat/winptyCompat.ts +++ b/src/addons/winptyCompat/winptyCompat.ts @@ -7,7 +7,8 @@ import { Terminal } from 'xterm'; import { IWinptyCompatAddonTerminal } from './Interfaces'; const CHAR_DATA_CODE_INDEX = 3; -const NULL_CELL_CODE = 32; +const NULL_CELL_CODE = 0; +const WHITESPACE_CELL_CODE = 32; export function winptyCompatInit(terminal: Terminal): void { const addonTerminal = terminal; @@ -32,7 +33,7 @@ export function winptyCompatInit(terminal: Terminal): void { const line = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y - 1); const lastChar = line.get(addonTerminal.cols - 1); - if (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE) { + if (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE && lastChar[CHAR_DATA_CODE_INDEX] !== WHITESPACE_CELL_CODE) { const nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y); nextLine.isWrapped = true; } diff --git a/src/core/input/Keyboard.test.ts b/src/core/input/Keyboard.test.ts index e9846831..a0dc3cbc 100644 --- a/src/core/input/Keyboard.test.ts +++ b/src/core/input/Keyboard.test.ts @@ -281,5 +281,16 @@ describe('Keyboard', () => { assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputDownArrow' }).key, '\x1b[B'); assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputDownArrow' }, { applicationCursorMode: true }).key, '\x1bOB'); }); + + it('should handle lowercase letters', () => { + assert.equal(testEvaluateKeyboardEvent({ keyCode: 65, key: 'a' }).key, 'a'); + assert.equal(testEvaluateKeyboardEvent({ keyCode: 189, key: '-' }).key, '-'); + }); + + it('should handle uppercase letters', () => { + assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 65, key: 'A' }).key, 'A'); + assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 49, key: '!' }).key, '!'); + }); + }); }); diff --git a/src/core/input/Keyboard.ts b/src/core/input/Keyboard.ts index 8c6f3c59..9d86b349 100644 --- a/src/core/input/Keyboard.ts +++ b/src/core/input/Keyboard.ts @@ -349,6 +349,10 @@ export function evaluateKeyboardEvent( if (ev.keyCode === 65) { // cmd + a result.type = KeyboardResultType.SELECT_ALL; } + } else if (ev.key && !ev.ctrlKey && !ev.altKey && !ev.metaKey && + ev.keyCode >= 48 && ev.keyCode !== 144 && ev.keyCode !== 145) { + // Include only keys that that result in a character; don't include num lock and scroll lock + result.key = ev.key; } break; } diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 8ff7cf2b..87fcfaef 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -59,6 +59,12 @@ export class Terminal implements ITerminalApi { public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void { this._core.attachCustomKeyEventHandler(customKeyEventHandler); } + public addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { + return this._core.addCsiHandler(flag, callback); + } + public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + return this._core.addOscHandler(ident, callback); + } public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number { return this._core.registerLinkMatcher(regex, handler, options); } diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 2afdebb5..3e0b8643 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -236,12 +236,12 @@ export abstract class BaseRenderLayer implements IRenderLayer { */ protected fillCharTrueColor(terminal: ITerminal, charData: CharData, x: number, y: number): void { this._ctx.font = this._getFont(terminal, false, false); - this._ctx.textBaseline = 'top'; + this._ctx.textBaseline = 'middle'; this._clipRow(terminal, y); this._ctx.fillText( charData[CHAR_DATA_CHAR_INDEX], x * this._scaledCellWidth + this._scaledCharLeft, - y * this._scaledCellHeight + this._scaledCharTop); + (y + 0.5) * this._scaledCellHeight + this._scaledCharTop); } /** @@ -295,7 +295,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { private _drawUncachedChars(terminal: ITerminal, chars: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean, italic: boolean): void { this._ctx.save(); this._ctx.font = this._getFont(terminal, bold, italic); - this._ctx.textBaseline = 'top'; + this._ctx.textBaseline = 'middle'; if (fg === INVERTED_DEFAULT_COLOR) { this._ctx.fillStyle = this._colors.background.css; @@ -316,7 +316,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.fillText( chars, x * this._scaledCellWidth + this._scaledCharLeft, - y * this._scaledCellHeight + this._scaledCharTop); + (y + 0.5) * this._scaledCellHeight + this._scaledCharTop); this._ctx.restore(); } diff --git a/src/renderer/atlas/CharAtlasGenerator.ts b/src/renderer/atlas/CharAtlasGenerator.ts index e40215cf..cadcce2e 100644 --- a/src/renderer/atlas/CharAtlasGenerator.ts +++ b/src/renderer/atlas/CharAtlasGenerator.ts @@ -29,7 +29,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: ( ctx.save(); ctx.fillStyle = config.colors.foreground.css; ctx.font = getFont(config.fontWeight, config); - ctx.textBaseline = 'top'; + ctx.textBaseline = 'middle'; // Default color for (let i = 0; i < 256; i++) { @@ -37,7 +37,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: ( ctx.beginPath(); ctx.rect(i * cellWidth, 0, cellWidth, cellHeight); ctx.clip(); - ctx.fillText(String.fromCharCode(i), i * cellWidth, 0); + ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight / 2); ctx.restore(); } // Default color bold @@ -48,7 +48,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: ( ctx.beginPath(); ctx.rect(i * cellWidth, cellHeight, cellWidth, cellHeight); ctx.clip(); - ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight); + ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight * 1.5); ctx.restore(); } ctx.restore(); @@ -64,7 +64,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: ( ctx.rect(i * cellWidth, y, cellWidth, cellHeight); ctx.clip(); ctx.fillStyle = config.colors.ansi[colorIndex].css; - ctx.fillText(String.fromCharCode(i), i * cellWidth, y); + ctx.fillText(String.fromCharCode(i), i * cellWidth, y + cellHeight / 2); ctx.restore(); } } @@ -80,7 +80,7 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: ( ctx.rect(i * cellWidth, y, cellWidth, cellHeight); ctx.clip(); ctx.fillStyle = config.colors.ansi[colorIndex].css; - ctx.fillText(String.fromCharCode(i), i * cellWidth, y); + ctx.fillText(String.fromCharCode(i), i * cellWidth, y + cellHeight / 2); ctx.restore(); } } diff --git a/src/renderer/atlas/DynamicCharAtlas.ts b/src/renderer/atlas/DynamicCharAtlas.ts index 72010768..e311c369 100644 --- a/src/renderer/atlas/DynamicCharAtlas.ts +++ b/src/renderer/atlas/DynamicCharAtlas.ts @@ -250,7 +250,7 @@ export default class DynamicCharAtlas extends BaseCharAtlas { const fontStyle = glyph.italic ? 'italic' : ''; this._tmpCtx.font = `${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`; - this._tmpCtx.textBaseline = 'top'; + this._tmpCtx.textBaseline = 'middle'; this._tmpCtx.fillStyle = this._getForegroundColor(glyph).css; @@ -259,7 +259,7 @@ export default class DynamicCharAtlas extends BaseCharAtlas { this._tmpCtx.globalAlpha = DIM_OPACITY; } // Draw the character - this._tmpCtx.fillText(glyph.chars, 0, 0); + this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight / 2); this._tmpCtx.restore(); // clear the background from the character to avoid issues with drawing over the previous diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index a0cefd67..c5ef212d 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -364,9 +364,11 @@ export class DomRenderer extends EventEmitter implements IRenderer { return; } const span = row.children[x]; - span.style.textDecoration = enabled ? 'underline' : 'none'; - x = (x + 1) % cols; - if (x === 0) { + if (span) { + span.style.textDecoration = enabled ? 'underline' : 'none'; + } + if (++x >= cols) { + x = 0; y++; } } diff --git a/src/ui/CharMeasure.ts b/src/ui/CharMeasure.ts index 7d1e5e48..2dfc4eb5 100644 --- a/src/ui/CharMeasure.ts +++ b/src/ui/CharMeasure.ts @@ -38,7 +38,7 @@ export class CharMeasure extends EventEmitter implements ICharMeasure { } public measure(options: ITerminalOptions): void { - this._measureElement.style.fontFamily = options.fontFamily; + this._measureElement.style.fontFamily = options.fontFamily; this._measureElement.style.fontSize = `${options.fontSize}px`; const geometry = this._measureElement.getBoundingClientRect(); // The element is likely currently display:none, we should retain the diff --git a/src/ui/TestUtils.test.ts b/src/ui/TestUtils.test.ts index 10033a33..e6e4aaa3 100644 --- a/src/ui/TestUtils.test.ts +++ b/src/ui/TestUtils.test.ts @@ -54,6 +54,12 @@ export class MockTerminal implements ITerminal { attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void { throw new Error('Method not implemented.'); } + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { + throw new Error('Method not implemented.'); + } + addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + throw new Error('Method not implemented.'); + } registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number { throw new Error('Method not implemented.'); } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 7528bb55..0ceab01d 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -481,6 +481,29 @@ declare module 'xterm' { */ attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void; + /** + * (EXPERIMENTAL) Adds a handler for CSI escape sequences. + * @param flag The flag should be one-character string, which specifies the + * final character (e.g "m" for SGR) of the CSI sequence. + * @param callback The function to handle the escape sequence. The callback + * is called with the numerical params, as well as the special characters + * (e.g. "$" for DECSCPP). Return true if the sequence was handled; false if + * we should try a previous handler (set by addCsiHandler or setCsiHandler). + * The most recently-added handler is tried first. + * @return An IDisposable you can call to remove this handler. + */ + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable; + + /** + * (EXPERIMENTAL) Adds a handler for OSC escape sequences. + * @param ident The number (first parameter) of the sequence. + * @param callback The function to handle the escape sequence. The callback + * is called with OSC data string. Return true if the sequence was handled; + * false if we should try a previous handler (set by addOscHandler or + * setOscHandler). The most recently-added handler is tried first. + * @return An IDisposable you can call to remove this handler. + */ + addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; /** * (EXPERIMENTAL) Registers a link matcher, allowing custom link patterns to * be matched and handled. diff --git a/yarn.lock b/yarn.lock index 7ebdbf3c..5555321d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3839,16 +3839,6 @@ loader-utils@^1.0.2, loader-utils@^1.1.0: emojis-list "^2.0.0" json5 "^0.5.0" -loader-utils@~0.2.2: - version "0.2.17" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348" - integrity sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g= - dependencies: - big.js "^3.1.3" - emojis-list "^2.0.0" - json5 "^0.5.0" - object-assign "^4.0.1" - locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -4479,9 +4469,9 @@ nanomatch@^1.2.9: to-regex "^3.0.1" natives@^1.1.0: - version "1.1.4" - resolved "https://registry.yarnpkg.com/natives/-/natives-1.1.4.tgz#2f0f224fc9a7dd53407c7667c84cf8dbe773de58" - integrity sha512-Q29yeg9aFKwhLVdkTAejM/HvYG0Y1Am1+HUkFQGn5k2j8GS+v60TVmZh6nujpEAj/qql+wGUrlryO8bF+b1jEg== + version "1.1.6" + resolved "https://registry.yarnpkg.com/natives/-/natives-1.1.6.tgz#a603b4a498ab77173612b9ea1acdec4d980f00bb" + integrity sha512-6+TDFewD4yxY14ptjKaS63GVdtKiES1pTPyxn9Jb0rBqPMZ7VcCiooEhPNsr+mqHtMGxa/5c/HhcC4uPEUw/nA== needle@^2.2.1: version "2.2.1" @@ -5972,14 +5962,13 @@ source-list-map@^2.0.0: resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085" integrity sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A== -source-map-loader@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-0.2.3.tgz#d4b0c8cd47d54edce3e6bfa0f523f452b5b0e521" - integrity sha512-MYbFX9DYxmTQFfy2v8FC1XZwpwHKYxg3SK8Wb7VPBKuhDjz8gi9re2819MsG4p49HDyiOSUKlmZ+nQBArW5CGw== +source-map-loader@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-0.2.4.tgz#c18b0dc6e23bf66f6792437557c569a11e072271" + integrity sha512-OU6UJUty+i2JDpTItnizPrlpOIBLmQbWMuBg9q5bVtnHACqw1tn9nNwqJLbv0/00JjnJb/Ee5g5WS5vrRv7zIQ== dependencies: async "^2.5.0" - loader-utils "~0.2.2" - source-map "~0.6.1" + loader-utils "^1.1.0" source-map-resolve@^0.5.0, source-map-resolve@^0.5.1: version "0.5.2"