diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e7924027..e8102a2e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,21 +33,21 @@ opening an issue, read these pointers. You can find issues to work on by looking at the [help wanted](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) or [good first issue](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) issues. It's a good idea to comment on the issue saying that you're taking it, just in case someone else comes along and you duplicate work. Once you have your issue, here are the steps to contribute: - Fork [xterm.js](https://github.com/sourcelair/xterm.js/) - ([how to fork a repo](https://help.github.com/articles/fork-a-repo)) -- Get the [xterm.js demo](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo) running -- Make your changes + ([how to fork a repo](https://help.github.com/articles/fork-a-repo)). +- Get the [xterm.js demo](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo) running. +- Make your changes. - If your changes are easy to test or likely to regress, add tests. Tests go into `test`, directory. - Follow the general code style of the rest of the project (see below). - Submit a pull request ([how to create a pull request](https://help.github.com/articles/fork-a-repo)). Don't put more than one feature/fix in a single pull request. -By contributing code to xterm.js you +By contributing code to xterm.js you: - - agree to license the contributed code under xterm.js' [MIT + - Agree to license the contributed code under xterm.js' [MIT license](LICENSE). - - confirm that you have the right to contribute and license the code + - Confirm that you have the right to contribute and license the code in question. (Either you hold all rights on the code, or the rights holder has explicitly granted the right to use it like this, through a compatible open source license or through a direct diff --git a/LICENSE b/LICENSE index 28adbdad..4472336c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2017-2018, The xterm.js authors (https://github.com/xtermjs/xterm.js) +Copyright (c) 2017-2019, The xterm.js authors (https://github.com/xtermjs/xterm.js) Copyright (c) 2014-2016, SourceLair Private Company (https://www.sourcelair.com) Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/) diff --git a/README.md b/README.md index 35ccc9b6..5378f443 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,17 @@ Xterm.js is a front-end component written in TypeScript that lets applications b ## Features -- **Terminal apps just work**: Xterm.js works with most terminal apps such as `bash`, `vim` and `tmux`, this includes support for curses-based apps and mouse event support -- **Perfomant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer -- **Rich unicode support**: Supports CJK, emojis and IMEs -- **Self-contained**: Requires zero dependencies to work -- **Accessible**: Screen reader support can be turned on using the `screenReaderMode` option +- **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) +- Xterm.js is not a terminal application that you can download and use on your computer. +- Xterm.js is not `bash`. Xterm.js can be connected to processes like `bash` and let you interact with them (provide input, receive output). ## Getting Started @@ -41,7 +41,7 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t @@ -106,17 +106,17 @@ Note that some APIs are marked *experimental*, these are added to enable experim ## Real-world uses Xterm.js is used in several world-class applications to provide great terminal experiences. -- [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js -- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js -- [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js +- [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js. +- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js. +- [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js. - [**Katacoda**](https://www.katacoda.com/): Katacoda is an Interactive Learning Platform for software developers, covering the latest Cloud Native technologies. - [**Eclipse Che**](http://www.eclipse.org/che): Developer workspace server, cloud IDE, and Eclipse next-generation IDE. - [**Codenvy**](http://www.codenvy.com): Cloud workspaces for development teams. -- [**CoderPad**](https://coderpad.io): Online interviewing platform for programmers. Run code in many programming languages, with results displayed by `xterm.js`. -- [**WebSSH2**](https://github.com/billchurch/WebSSH2): A web based SSH2 client using `xterm.js`, socket.io, and ssh2. +- [**CoderPad**](https://coderpad.io): Online interviewing platform for programmers. Run code in many programming languages, with results displayed by xterm.js. +- [**WebSSH2**](https://github.com/billchurch/WebSSH2): A web based SSH2 client using xterm.js, socket.io, and ssh2. - [**Spyder Terminal**](https://github.com/spyder-ide/spyder-terminal): A full fledged system terminal embedded on Spyder IDE. - [**Cloud Commander**](https://cloudcmd.io "Cloud Commander"): Orthodox web file manager with console and editor. -- [**Codevolve**](https://www.codevolve.com "Codevolve"): Online platform for interactive coding and web development courses. Live container-backed terminal uses `xterm.js`. +- [**Codevolve**](https://www.codevolve.com "Codevolve"): Online platform for interactive coding and web development courses. Live container-backed terminal uses xterm.js. - [**RStudio**](https://www.rstudio.com/products/RStudio "RStudio"): RStudio is an integrated development environment (IDE) for R. - [**Terminal for Atom**](https://github.com/jsmecham/atom-terminal-tab): A simple terminal for the Atom text editor. - [**Eclipse Orion**](https://orionhub.org): A modern, open source software development environment that runs in the cloud. Code, deploy and run in the cloud. @@ -124,9 +124,8 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**Hexlet**](https://en.hexlet.io): Practical programming courses (JavaScript, PHP, Unix, databases, functional programming). A steady path from the first line of code to the first job. - [**Selenoid UI**](https://github.com/aerokube/selenoid-ui): Simple UI for the scallable golang implementation of Selenium Hub named Selenoid. We use XTerm for streaming logs over websockets from docker containers. - [**Portainer**](https://portainer.io): Simple management UI for Docker. -- [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising `xterm.js`, SJCL & websockets. -- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible -computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages. +- [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising xterm.js, SJCL & websockets. +- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages. - [**Theia**](https://github.com/theia-ide/theia): Theia is a cloud & desktop IDE framework implemented in TypeScript. - [**Opshell**](https://github.com/ricktbaker/opshell) Ops Helper tool to make life easier working with AWS instances across multiple organizations. - [**Proxmox VE**](https://www.proxmox.com/en/proxmox-ve): Proxmox VE is a complete open-source platform for enterprise virtualization. It uses xterm.js for container terminals and the host shell. @@ -138,22 +137,23 @@ computational environment for Jupyter, supporting interactive data science and s - [**Azure Cloud Shell**](https://shell.azure.com): Azure Cloud Shell is a Microsoft-managed admin machine built on Azure, for Azure. - [**atom-xterm**](https://atom.io/packages/atom-xterm): Atom plugin for providing terminals inside your Atom workspace. - [**rtty**](https://github.com/zhaojh329/rtty): A reverse proxy WebTTY. It is composed of the client and the server. -- [**Pisth**](https://github.com/ColdGrub1384/Pisth): An SFTP and SSH client for iOS +- [**Pisth**](https://github.com/ColdGrub1384/Pisth): An SFTP and SSH client for iOS. - [**abstruse**](https://github.com/bleenco/abstruse): Abstruse CI is a continuous integration platform based on Node.JS and Docker. - [**Azure Data Studio**](https://github.com/Microsoft/azuredatastudio): A data management tool that enables working with SQL Server, Azure SQL DB and SQL DW from Windows, macOS and Linux. -- [**FreeMAN**](https://github.com/matthew-matvei/freeman): A free, cross-platform file manager for power users +- [**FreeMAN**](https://github.com/matthew-matvei/freeman): A free, cross-platform file manager for power users. - [**Fluent Terminal**](https://github.com/felixse/FluentTerminal): A terminal emulator based on UWP and web technologies. -- [**Hyper**](https://hyper.is): A terminal built on web technologies +- [**Hyper**](https://hyper.is): A terminal built on web technologies. - [**Diag**](https://diag.ai): A better way to troubleshoot problems faster. Capture, share and reapply troubleshooting knowledge so you can focus on solving problems that matter. - [**GoTTY**](https://github.com/yudai/gotty): A simple command line tool that shares your terminal as a web application based on xterm.js. - [**genact**](https://github.com/svenstaro/genact): A nonsense activity generator. - [**cPanel & WHM**](https://cpanel.com): The hosting platform of choice. -- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js +- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js. - [**SSH Web Client**](https://github.com/roke22/PHP-SSH2-Web-Client): SSH Web Client with PHP. - [**Shellvault**](https://www.shellvault.io): The cloud-based SSH terminal you can access from anywhere. - [**Juno**](http://junolab.org/): A flexible Julia IDE, based on Atom. - [**webssh**](https://github.com/huashengdun/webssh): Web based ssh client. -- [**info-beamer hosted**](https://info-beamer.com): Uses Xterm.js to manage digital signage devices from the web dashboard. +- [**info-beamer hosted**](https://info-beamer.com): Uses xterm.js to manage digital signage devices from the web dashboard. +- [**Jumpserver**](https://github.com/jumpserver/luna): Jumpserver Luna project, Jumpserver is a bastion server project, Luna use xterm.js for web terminal emulation. [And much more...](https://github.com/xtermjs/xterm.js/network/dependents) @@ -163,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. -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). +All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), you can view the [high-level roadmap on the wiki](https://github.com/xtermjs/xterm.js/wiki/Roadmap) and see what we're working on now by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones). ## Contributing diff --git a/docker-compose.yml b/docker-compose.yml index 8e6a2f46..6eefed89 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: volumes: - ./:/usr/src/app ports: - - ${XTERMJS_PORT:3000}:3000 + - ${XTERMJS_PORT:-3000}:3000 command: ["npm", "start"] watch: diff --git a/package.json b/package.json index 5ed3a4b8..c5fad515 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xterm", "description": "Full xterm terminal, in your browser", - "version": "3.10.0", + "version": "3.12.0", "main": "lib/public/Terminal.js", "types": "typings/xterm.d.ts", "repository": "https://github.com/xtermjs/xterm.js", diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index a56fd63a..53de19b7 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -265,6 +265,7 @@ describe('Buffer', () => { 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); @@ -296,7 +297,7 @@ describe('Buffer', () => { buffer.fillViewportRows(); terminal.options.scrollback = 1; buffer.resize(10, 5); - const lastLine = buffer.lines.get(4); + const lastLine = buffer.lines.get(3); for (let i = 0; i < 10; i++) { const code = 'a'.charCodeAt(0) + i; const char = String.fromCharCode(code); @@ -308,27 +309,27 @@ describe('Buffer', () => { assert.equal(buffer.y, 4); assert.equal(buffer.ybase, 1); assert.equal(buffer.lines.length, 6); - assert.equal(buffer.lines.get(0).translateToString(), ' '); - assert.equal(buffer.lines.get(1).translateToString(), 'ab'); - assert.equal(buffer.lines.get(2).translateToString(), 'cd'); - assert.equal(buffer.lines.get(3).translateToString(), 'ef'); - assert.equal(buffer.lines.get(4).translateToString(), 'gh'); - assert.equal(buffer.lines.get(5).translateToString(), 'ij'); + 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(), 'e'); - assert.equal(buffer.lines.get(1).translateToString(), 'f'); - assert.equal(buffer.lines.get(2).translateToString(), 'g'); - assert.equal(buffer.lines.get(3).translateToString(), 'h'); - assert.equal(buffer.lines.get(4).translateToString(), 'i'); - assert.equal(buffer.lines.get(5).translateToString(), 'j'); + 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, 0); + assert.equal(buffer.y, 1); assert.equal(buffer.ybase, 0); assert.equal(buffer.lines.length, 5); - assert.equal(buffer.lines.get(0).translateToString(), 'efghij '); + 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(), ' '); @@ -339,6 +340,7 @@ describe('Buffer', () => { // 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++) { @@ -358,8 +360,8 @@ describe('Buffer', () => { assert.equal(buffer.lines.get(i).translateToString(), ' '); } buffer.resize(2, 10); - assert.equal(buffer.ybase, 0); - assert.equal(buffer.lines.length, 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'); @@ -370,7 +372,10 @@ describe('Buffer', () => { 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++) { @@ -379,22 +384,23 @@ describe('Buffer', () => { }); it('should transfer combined char data over to reflowed lines', () => { buffer.fillViewportRows(); - buffer.resize(4, 2); + 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, 2); + assert.equal(buffer.lines.length, 3); assert.equal(buffer.lines.get(0).translateToString(), 'abc😁'); assert.equal(buffer.lines.get(1).translateToString(), ' '); - buffer.resize(2, 2); + 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, 15); + buffer.resize(10, 16); for (let i = 0; i < 10; i++) { const code = 'a'.charCodeAt(0) + i; const char = String.fromCharCode(code); @@ -410,6 +416,7 @@ describe('Buffer', () => { const char = String.fromCharCode(code); buffer.lines.get(2).set(i, [null, char, 1, code]); } + buffer.y = 3; // Buffer: // abcdefghij // 0123456789 @@ -423,7 +430,7 @@ describe('Buffer', () => { assert.equal(firstMarker.line, 0); assert.equal(secondMarker.line, 1); assert.equal(thirdMarker.line, 2); - buffer.resize(2, 15); + 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'); @@ -442,7 +449,7 @@ describe('Buffer', () => { 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, 15); + 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'); @@ -456,7 +463,7 @@ describe('Buffer', () => { it('should dispose markers whose rows are trimmed during a reflow', () => { buffer.fillViewportRows(); terminal.options.scrollback = 1; - buffer.resize(10, 10); + buffer.resize(10, 11); for (let i = 0; i < 10; i++) { const code = 'a'.charCodeAt(0) + i; const char = String.fromCharCode(code); @@ -472,6 +479,7 @@ describe('Buffer', () => { const char = String.fromCharCode(code); buffer.lines.get(2).set(i, [null, char, 1, code]); } + buffer.y = 10; // Buffer: // abcdefghij // 0123456789 @@ -479,14 +487,14 @@ describe('Buffer', () => { const firstMarker = buffer.addMarker(0); const secondMarker = buffer.addMarker(1); const thirdMarker = buffer.addMarker(2); - buffer.y = 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, 10); + 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'); @@ -503,7 +511,7 @@ describe('Buffer', () => { assert.equal(firstMarker.isDisposed, true, 'first marker was trimmed'); assert.equal(secondMarker.isDisposed, false); assert.equal(thirdMarker.isDisposed, false); - buffer.resize(10, 10); + 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'); @@ -513,6 +521,7 @@ describe('Buffer', () => { 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)]); @@ -547,6 +556,7 @@ describe('Buffer', () => { 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)]); @@ -937,6 +947,7 @@ describe('Buffer', () => { 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); @@ -962,6 +973,7 @@ describe('Buffer', () => { 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); @@ -1266,7 +1278,7 @@ describe('Buffer', () => { 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); + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); const j = (i - 0) << 1; assert.deepEqual([(j / terminal.cols) | 0, j % terminal.cols], bufferIndex); } @@ -1278,7 +1290,7 @@ describe('Buffer', () => { 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); + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX]); } }); @@ -1290,7 +1302,7 @@ describe('Buffer', () => { 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); + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); assert.equal( (!(i % 3)) ? input[i] @@ -1300,6 +1312,13 @@ describe('Buffer', () => { terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX]); } }); + + it('should handle \t in lines correctly', () => { + const input = '\thttps://google.de'; + terminal.writeSync(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(s, Array(terminal.getOption('tabStopWidth') + 1).join(' ') + 'https://google.de'); + }); }); describe('BufferStringIterator', function(): void { it('iterator does not overflow buffer limits', function(): void { diff --git a/src/Buffer.ts b/src/Buffer.ts index 52d9572d..5eea5d9a 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -3,13 +3,13 @@ * @license MIT */ -import { CircularList, IInsertEvent, IDeleteEvent } from './common/CircularList'; -import { CharData, ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult } from './Types'; -import { EventEmitter } from './common/EventEmitter'; import { IMarker } from 'xterm'; import { BufferLine } from './BufferLine'; +import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from './BufferReflow'; +import { CircularList, IDeleteEvent, IInsertEvent } from './common/CircularList'; +import { EventEmitter } from './common/EventEmitter'; import { DEFAULT_COLOR } from './renderer/atlas/Types'; -import { reflowSmallerGetNewLineLengths, reflowLargerGetLinesToRemove, reflowLargerCreateNewLayout, reflowLargerApplyNewLayout } from './BufferReflow'; +import { BufferIndex, CharData, IBuffer, IBufferLine, IBufferStringIterator, IBufferStringIteratorResult, ITerminal } from './Types'; export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0); export const CHAR_DATA_ATTR_INDEX = 0; @@ -211,8 +211,8 @@ export class Buffer implements IBuffer { this.scrollBottom = newRows - 1; - if (this._hasScrollback) { - this._reflow(newCols); + if (this._isReflowEnabled) { + this._reflow(newCols, newRows); // Trim the end of the line off if cols shrunk if (this._cols > newCols) { @@ -226,36 +226,44 @@ export class Buffer implements IBuffer { this._rows = newRows; } - private _reflow(newCols: number): void { + private get _isReflowEnabled(): boolean { + return this._hasScrollback && !(this._terminal as any).isWinptyCompatEnabled; + } + + private _reflow(newCols: number, newRows: number): void { if (this._cols === newCols) { return; } // Iterate through rows, ignore the last one as it cannot be wrapped if (newCols > this._cols) { - this._reflowLarger(newCols); + this._reflowLarger(newCols, newRows); } else { - this._reflowSmaller(newCols); + this._reflowSmaller(newCols, newRows); } } - private _reflowLarger(newCols: number): void { - const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, newCols); + private _reflowLarger(newCols: number, newRows: number): void { + const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, newCols, this.ybase + this.y); if (toRemove.length > 0) { const newLayoutResult = reflowLargerCreateNewLayout(this.lines, toRemove); reflowLargerApplyNewLayout(this.lines, newLayoutResult.layout); - this._reflowLargerAdjustViewport(newCols, newLayoutResult.countRemoved); + this._reflowLargerAdjustViewport(newCols, newRows, newLayoutResult.countRemoved); } } - private _reflowLargerAdjustViewport(newCols: number, countRemoved: number): void { + private _reflowLargerAdjustViewport(newCols: number, newRows: number, countRemoved: number): void { // Adjust viewport based on number of items removed let viewportAdjustments = countRemoved; while (viewportAdjustments-- > 0) { if (this.ybase === 0) { - this.y--; - // Add an extra row at the bottom of the viewport - this.lines.push(new BufferLine(newCols, FILL_CHAR_DATA)); + if (this.y > 0) { + this.y--; + } + if (this.lines.length < newRows) { + // Add an extra row at the bottom of the viewport + this.lines.push(new BufferLine(newCols, FILL_CHAR_DATA)); + } } else { if (this.ydisp === this.ybase) { this.ydisp--; @@ -265,7 +273,7 @@ export class Buffer implements IBuffer { } } - private _reflowSmaller(newCols: number): void { + private _reflowSmaller(newCols: number, newRows: number): void { // Gather all BufferLines that need to be inserted into the Buffer here so that they can be // batched up and only committed once const toInsert = []; @@ -285,6 +293,13 @@ export class Buffer implements IBuffer { wrappedLines.unshift(nextLine); } + // If these lines contain the cursor don't touch them, the program will handle fixing up + // wrapped lines with the cursor + const absoluteY = this.ybase + this.y; + if (absoluteY >= y && absoluteY < y + wrappedLines.length) { + continue; + } + const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength(); const destLineLengths = reflowSmallerGetNewLineLengths(wrappedLines, this._cols, newCols); const linesToAdd = destLineLengths.length - wrappedLines.length; @@ -349,7 +364,7 @@ export class Buffer implements IBuffer { let viewportAdjustments = linesToAdd - trimmedLines; while (viewportAdjustments-- > 0) { if (this.ybase === 0) { - if (this.y < this._rows - 1) { + if (this.y < newRows - 1) { this.y++; this.lines.pop(); } else { @@ -357,10 +372,13 @@ export class Buffer implements IBuffer { this.ydisp++; } } else { - if (this.ybase === this.ydisp) { - this.ydisp++; + // Ensure ybase does not exceed its maximum value + if (this.ybase < Math.min(this.lines.maxLength, this.lines.length + countToInsert) - newRows) { + if (this.ybase === this.ydisp) { + this.ydisp++; + } + this.ybase++; } - this.ybase++; } } } @@ -435,14 +453,19 @@ export class Buffer implements IBuffer { * @param stringIndex index within the string * @param startCol column offset the string was retrieved from */ - public stringIndexToBufferIndex(lineIndex: number, stringIndex: number): BufferIndex { + public stringIndexToBufferIndex(lineIndex: number, stringIndex: number, trimRight: boolean = false): BufferIndex { while (stringIndex) { const line = this.lines.get(lineIndex); if (!line) { return [-1, -1]; } - for (let i = 0; i < line.length; ++i) { - stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length; + const length = (trimRight) ? line.getTrimmedLength() : line.length; + for (let i = 0; i < length; ++i) { + if (line.get(i)[CHAR_DATA_WIDTH_INDEX]) { + // empty cells report a string length of 0, but get replaced + // with a whitespace in translateToString, thus replace with 1 + stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length || 1; + } if (stringIndex < 0) { return [lineIndex, i]; } diff --git a/src/BufferReflow.ts b/src/BufferReflow.ts index 59934e46..24ab69e6 100644 --- a/src/BufferReflow.ts +++ b/src/BufferReflow.ts @@ -3,10 +3,10 @@ * @license MIT */ +import { FILL_CHAR_DATA } from './Buffer'; import { BufferLine } from './BufferLine'; import { CircularList, IDeleteEvent } from './common/CircularList'; import { IBufferLine } from './Types'; -import { FILL_CHAR_DATA } from './Buffer'; export interface INewLayoutResult { layout: number[]; @@ -19,7 +19,7 @@ export interface INewLayoutResult { * @param lines The buffer lines. * @param newCols The columns after resize. */ -export function reflowLargerGetLinesToRemove(lines: CircularList, newCols: number): number[] { +export function reflowLargerGetLinesToRemove(lines: CircularList, newCols: number, bufferAbsoluteY: number): number[] { // Gather all BufferLines that need to be removed from the Buffer here so that they can be // batched up and only committed once const toRemove: number[] = []; @@ -39,6 +39,13 @@ export function reflowLargerGetLinesToRemove(lines: CircularList, n nextLine = lines.get(++i) as BufferLine; } + // If these lines contain the cursor don't touch them, the program will handle fixing up wrapped + // lines with the cursor + if (bufferAbsoluteY >= y && bufferAbsoluteY < i) { + y += wrappedLines.length - 1; + continue; + } + // Copy buffer data to new locations let destLineIndex = 0; let destCol = wrappedLines[destLineIndex].getTrimmedLength(); @@ -64,7 +71,7 @@ export function reflowLargerGetLinesToRemove(lines: CircularList, n } // Make sure the last cell isn't wide, if it is copy it to the current dest - if (destCol === 0) { + if (destCol === 0 && destLineIndex !== 0) { if (wrappedLines[destLineIndex - 1].getWidth(newCols - 1) === 2) { wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[destLineIndex - 1], newCols - 1, destCol++, 1, false); // Null out the end of the last row @@ -166,7 +173,6 @@ export function reflowLargerApplyNewLayout(lines: CircularList, new */ export function reflowSmallerGetNewLineLengths(wrappedLines: BufferLine[], oldCols: number, newCols: number): number[] { const newLineLengths: number[] = []; - const cellsNeeded = wrappedLines.map(l => l.getTrimmedLength()).reduce((p, c) => p + c); // Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and diff --git a/src/CompositionHelper.ts b/src/CompositionHelper.ts index 31ad866b..840bef55 100644 --- a/src/CompositionHelper.ts +++ b/src/CompositionHelper.ts @@ -203,6 +203,8 @@ export class CompositionHelper { this._compositionView.style.top = cursorTop + 'px'; this._compositionView.style.height = cellHeight + 'px'; this._compositionView.style.lineHeight = cellHeight + 'px'; + this._compositionView.style.fontFamily = this._terminal.options.fontFamily; + this._compositionView.style.fontSize = this._terminal.options.fontSize + 'px'; // Sync the textarea to the exact position of the composition view so the IME knows where the // text is. const compositionViewBounds = this._compositionView.getBoundingClientRect(); diff --git a/src/EscapeSequenceParser.test.ts b/src/EscapeSequenceParser.test.ts index 0f1d63cc..135cc393 100644 --- a/src/EscapeSequenceParser.test.ts +++ b/src/EscapeSequenceParser.test.ts @@ -6,6 +6,7 @@ import { ParserState, IDcsHandler, IParsingState } from './Types'; import { EscapeSequenceParser, TransitionTable, VT500_TRANSITION_TABLE } from './EscapeSequenceParser'; import * as chai from 'chai'; +import { StringToUtf32, stringFromCodePoint } from './core/input/TextDecoder'; function r(a: number, b: number): string[] { let c = b - a; @@ -50,8 +51,12 @@ const testTerminal: any = { compare: function (value: any): void { chai.expect(this.calls.slice()).eql(value); // weird bug w'o slicing here }, - print: function (data: string, start: number, end: number): void { - this.calls.push(['print', data.substring(start, end)]); + 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]); @@ -68,8 +73,12 @@ const testTerminal: any = { actionDCSHook: function (collect: string, params: number[], flag: string): void { this.calls.push(['dcs hook', collect, params, flag]); }, - actionDCSPrint: function (data: string, start: number, end: number): void { - this.calls.push(['dcs put', data.substring(start, end)]); + 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']); @@ -81,7 +90,7 @@ class DcsTest implements IDcsHandler { hook(collect: string, params: number[], flag: number): void { testTerminal.actionDCSHook(collect, params, String.fromCharCode(flag)); } - put(data: string, start: number, end: number): void { + put(data: Uint32Array, start: number, end: number): void { testTerminal.actionDCSPrint(data, start, end); } unhook(): void { @@ -155,6 +164,12 @@ interface IRun { 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 | null = null; @@ -205,12 +220,12 @@ describe('EscapeSequenceParser', function (): void { it('state GROUND execute action', function (): void { parser.reset(); testTerminal.clear(); - const exes = r(0x00, 0x18); - exes.concat(['\x19']); - exes.concat(r(0x1c, 0x20)); + 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; - parser.parse(exes[i]); + parse(parser, exes[i]); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare([['exe', exes[i]]]); parser.reset(); @@ -223,7 +238,7 @@ describe('EscapeSequenceParser', function (): void { const printables = r(0x20, 0x7f); // NOTE: DEL excluded for (let i = 0; i < printables.length; ++i) { parser.currentState = ParserState.GROUND; - parser.parse(printables[i]); + parse(parser, printables[i]); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare([['print', printables[i]]]); parser.reset(); @@ -245,13 +260,13 @@ describe('EscapeSequenceParser', function (): void { for (state in states) { for (let i = 0; i < exes.length; ++i) { parser.currentState = state; - parser.parse(exes[i]); + 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(); } - parser.parse('\x9c'); + parse(parser, '\x9c'); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare([]); parser.reset(); @@ -265,7 +280,7 @@ describe('EscapeSequenceParser', function (): void { parser.osc = '#'; parser.params = [23]; parser.collect = '#'; - parser.parse('\x1b'); + parse(parser, '\x1b'); chai.expect(parser.currentState).equal(ParserState.ESCAPE); chai.expect(parser.osc).equal(''); chai.expect(parser.params).eql([0]); @@ -276,12 +291,12 @@ describe('EscapeSequenceParser', function (): void { it('state ESCAPE execute rules', function (): void { parser.reset(); testTerminal.clear(); - const exes = r(0x00, 0x18); - exes.concat(['\x19']); - exes.concat(r(0x1c, 0x20)); + 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; - parser.parse(exes[i]); + parse(parser, exes[i]); chai.expect(parser.currentState).equal(ParserState.ESCAPE); testTerminal.compare([['exe', exes[i]]]); parser.reset(); @@ -292,7 +307,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.ESCAPE; - parser.parse('\x7f'); + parse(parser, '\x7f'); chai.expect(parser.currentState).equal(ParserState.ESCAPE); testTerminal.compare([]); parser.reset(); @@ -301,13 +316,13 @@ describe('EscapeSequenceParser', function (): void { it('trans ESCAPE --> GROUND with ecs_dispatch action', function (): void { parser.reset(); testTerminal.clear(); - const dispatches = r(0x30, 0x50); - dispatches.concat(r(0x51, 0x58)); - dispatches.concat(['\x59', '\x5a', '\x5c']); - dispatches.concat(r(0x60, 0x7f)); + 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; - parser.parse(dispatches[i]); + parse(parser, dispatches[i]); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare([['esc', '', dispatches[i]]]); parser.reset(); @@ -319,7 +334,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.ESCAPE; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); chai.expect(parser.collect).equal(collect[i]); parser.reset(); @@ -328,12 +343,12 @@ describe('EscapeSequenceParser', function (): void { it('state ESCAPE_INTERMEDIATE execute rules', function (): void { parser.reset(); testTerminal.clear(); - const exes = r(0x00, 0x18); - exes.concat(['\x19']); - exes.concat(r(0x1c, 0x20)); + 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; - parser.parse(exes[i]); + parse(parser, exes[i]); chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); testTerminal.compare([['exe', exes[i]]]); parser.reset(); @@ -344,7 +359,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.ESCAPE_INTERMEDIATE; - parser.parse('\x7f'); + parse(parser, '\x7f'); chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); testTerminal.compare([]); parser.reset(); @@ -355,7 +370,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.ESCAPE_INTERMEDIATE; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); chai.expect(parser.collect).equal(collect[i]); parser.reset(); @@ -367,7 +382,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x30, 0x7f); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.ESCAPE_INTERMEDIATE; - parser.parse(collect[i]); + 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]]]); @@ -382,7 +397,7 @@ describe('EscapeSequenceParser', function (): void { parser.osc = '#'; parser.params = [123]; parser.collect = '#'; - parser.parse('['); + parse(parser, '['); chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); chai.expect(parser.osc).equal(''); chai.expect(parser.params).eql([0]); @@ -394,7 +409,7 @@ describe('EscapeSequenceParser', function (): void { parser.osc = '#'; parser.params = [123]; parser.collect = '#'; - parser.parse('\x9b'); + parse(parser, '\x9b'); chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); chai.expect(parser.osc).equal(''); chai.expect(parser.params).eql([0]); @@ -405,12 +420,12 @@ describe('EscapeSequenceParser', function (): void { it('state CSI_ENTRY execute rules', function (): void { parser.reset(); testTerminal.clear(); - const exes = r(0x00, 0x18); - exes.concat(['\x19']); - exes.concat(r(0x1c, 0x20)); + 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; - parser.parse(exes[i]); + parse(parser, exes[i]); chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); testTerminal.compare([['exe', exes[i]]]); parser.reset(); @@ -421,7 +436,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.CSI_ENTRY; - parser.parse('\x7f'); + parse(parser, '\x7f'); chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); testTerminal.compare([]); parser.reset(); @@ -432,7 +447,7 @@ describe('EscapeSequenceParser', function (): void { const dispatches = r(0x40, 0x7f); for (let i = 0; i < dispatches.length; ++i) { parser.currentState = ParserState.CSI_ENTRY; - parser.parse(dispatches[i]); + parse(parser, dispatches[i]); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare([['csi', '', [0], dispatches[i]]]); parser.reset(); @@ -445,19 +460,19 @@ describe('EscapeSequenceParser', function (): void { const collect = ['\x3c', '\x3d', '\x3e', '\x3f']; for (let i = 0; i < params.length; ++i) { parser.currentState = ParserState.CSI_ENTRY; - parser.parse(params[i]); + 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; - parser.parse('\x3b'); + 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; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); chai.expect(parser.collect).equal(collect[i]); parser.reset(); @@ -466,12 +481,12 @@ describe('EscapeSequenceParser', function (): void { it('state CSI_PARAM execute rules', function (): void { parser.reset(); testTerminal.clear(); - const exes = r(0x00, 0x18); - exes.concat(['\x19']); - exes.concat(r(0x1c, 0x20)); + 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; - parser.parse(exes[i]); + parse(parser, exes[i]); chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); testTerminal.compare([['exe', exes[i]]]); parser.reset(); @@ -483,13 +498,13 @@ describe('EscapeSequenceParser', function (): void { 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; - parser.parse(params[i]); + 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; - parser.parse('\x3b'); + parse(parser, '\x3b'); chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); chai.expect(parser.params).eql([0, 0]); parser.reset(); @@ -498,7 +513,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.CSI_PARAM; - parser.parse('\x7f'); + parse(parser, '\x7f'); chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); testTerminal.compare([]); parser.reset(); @@ -510,7 +525,7 @@ describe('EscapeSequenceParser', function (): void { for (let i = 0; i < dispatches.length; ++i) { parser.currentState = ParserState.CSI_PARAM; parser.params = [0, 1]; - parser.parse(dispatches[i]); + parse(parser, dispatches[i]); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare([['csi', '', [0, 1], dispatches[i]]]); parser.reset(); @@ -522,7 +537,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.CSI_ENTRY; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); chai.expect(parser.collect).equal(collect[i]); parser.reset(); @@ -533,7 +548,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.CSI_PARAM; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); chai.expect(parser.collect).equal(collect[i]); parser.reset(); @@ -542,12 +557,12 @@ describe('EscapeSequenceParser', function (): void { it('state CSI_INTERMEDIATE execute rules', function (): void { parser.reset(); testTerminal.clear(); - const exes = r(0x00, 0x18); - exes.concat(['\x19']); - exes.concat(r(0x1c, 0x20)); + 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; - parser.parse(exes[i]); + parse(parser, exes[i]); chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); testTerminal.compare([['exe', exes[i]]]); parser.reset(); @@ -559,7 +574,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.CSI_INTERMEDIATE; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); chai.expect(parser.collect).equal(collect[i]); parser.reset(); @@ -569,7 +584,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.CSI_INTERMEDIATE; - parser.parse('\x7f'); + parse(parser, '\x7f'); chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); testTerminal.compare([]); parser.reset(); @@ -581,7 +596,7 @@ describe('EscapeSequenceParser', function (): void { for (let i = 0; i < dispatches.length; ++i) { parser.currentState = ParserState.CSI_INTERMEDIATE; parser.params = [0, 1]; - parser.parse(dispatches[i]); + parse(parser, dispatches[i]); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare([['csi', '', [0, 1], dispatches[i]]]); parser.reset(); @@ -591,7 +606,7 @@ describe('EscapeSequenceParser', function (): void { it('trans CSI_ENTRY --> CSI_IGNORE', function (): void { parser.reset(); parser.currentState = ParserState.CSI_ENTRY; - parser.parse('\x3a'); + parse(parser, '\x3a'); chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); parser.reset(); }); @@ -600,7 +615,7 @@ describe('EscapeSequenceParser', function (): void { const chars = ['\x3a', '\x3c', '\x3d', '\x3e', '\x3f']; for (let i = 0; i < chars.length; ++i) { parser.currentState = ParserState.CSI_PARAM; - parser.parse('\x3b' + chars[i]); + parse(parser, '\x3b' + chars[i]); chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); chai.expect(parser.params).eql([0, 0]); parser.reset(); @@ -611,7 +626,7 @@ describe('EscapeSequenceParser', function (): void { const chars = r(0x30, 0x40); for (let i = 0; i < chars.length; ++i) { parser.currentState = ParserState.CSI_INTERMEDIATE; - parser.parse(chars[i]); + parse(parser, chars[i]); chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); chai.expect(parser.params).eql([0]); parser.reset(); @@ -620,12 +635,12 @@ describe('EscapeSequenceParser', function (): void { it('state CSI_IGNORE execute rules', function (): void { parser.reset(); testTerminal.clear(); - const exes = r(0x00, 0x18); - exes.concat(['\x19']); - exes.concat(r(0x1c, 0x20)); + 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; - parser.parse(exes[i]); + parse(parser, exes[i]); chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); testTerminal.compare([['exe', exes[i]]]); parser.reset(); @@ -635,11 +650,11 @@ describe('EscapeSequenceParser', function (): void { it('state CSI_IGNORE ignore', function (): void { parser.reset(); testTerminal.clear(); - const ignored = r(0x20, 0x40); - ignored.concat(['\x7f']); + let ignored = r(0x20, 0x40); + ignored = ignored.concat(['\x7f']); for (let i = 0; i < ignored.length; ++i) { parser.currentState = ParserState.CSI_IGNORE; - parser.parse(ignored[i]); + parse(parser, ignored[i]); chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); testTerminal.compare([]); parser.reset(); @@ -652,7 +667,7 @@ describe('EscapeSequenceParser', function (): void { for (let i = 0; i < dispatches.length; ++i) { parser.currentState = ParserState.CSI_IGNORE; parser.params = [0, 1]; - parser.parse(dispatches[i]); + parse(parser, dispatches[i]); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare([]); parser.reset(); @@ -664,7 +679,7 @@ describe('EscapeSequenceParser', function (): void { // C0 let initializers = ['\x58', '\x5e', '\x5f']; for (let i = 0; i < initializers.length; ++i) { - parser.parse('\x1b' + initializers[i]); + parse(parser, '\x1b' + initializers[i]); chai.expect(parser.currentState).equal(ParserState.SOS_PM_APC_STRING); parser.reset(); } @@ -673,7 +688,7 @@ describe('EscapeSequenceParser', function (): void { parser.currentState = state; initializers = ['\x98', '\x9e', '\x9f']; for (let i = 0; i < initializers.length; ++i) { - parser.parse(initializers[i]); + parse(parser, initializers[i]); chai.expect(parser.currentState).equal(ParserState.SOS_PM_APC_STRING); parser.reset(); } @@ -681,13 +696,13 @@ describe('EscapeSequenceParser', function (): void { }); it('state SOS_PM_APC_STRING ignore rules', function (): void { parser.reset(); - const ignored = r(0x00, 0x18); - ignored.concat(['\x19']); - ignored.concat(r(0x1c, 0x20)); - ignored.concat(r(0x20, 0x80)); + 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; - parser.parse(ignored[i]); + parse(parser, ignored[i]); chai.expect(parser.currentState).equal(ParserState.SOS_PM_APC_STRING); parser.reset(); } @@ -695,13 +710,13 @@ describe('EscapeSequenceParser', function (): void { it('trans ANYWHERE/ESCAPE --> OSC_STRING', function (): void { parser.reset(); // C0 - parser.parse('\x1b]'); + parse(parser, '\x1b]'); chai.expect(parser.currentState).equal(ParserState.OSC_STRING); parser.reset(); // C1 for (state in states) { parser.currentState = state; - parser.parse('\x9d'); + parse(parser, '\x9d'); chai.expect(parser.currentState).equal(ParserState.OSC_STRING); parser.reset(); } @@ -714,7 +729,7 @@ describe('EscapeSequenceParser', function (): void { '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f']; for (let i = 0; i < ignored.length; ++i) { parser.currentState = ParserState.OSC_STRING; - parser.parse(ignored[i]); + parse(parser, ignored[i]); chai.expect(parser.currentState).equal(ParserState.OSC_STRING); chai.expect(parser.osc).equal(''); parser.reset(); @@ -725,7 +740,7 @@ describe('EscapeSequenceParser', function (): void { const puts = r(0x20, 0x80); for (let i = 0; i < puts.length; ++i) { parser.currentState = ParserState.OSC_STRING; - parser.parse(puts[i]); + parse(parser, puts[i]); chai.expect(parser.currentState).equal(ParserState.OSC_STRING); chai.expect(parser.osc).equal(puts[i]); parser.reset(); @@ -734,13 +749,13 @@ describe('EscapeSequenceParser', function (): void { it('state DCS_ENTRY', function (): void { parser.reset(); // C0 - parser.parse('\x1bP'); + parse(parser, '\x1bP'); chai.expect(parser.currentState).equal(ParserState.DCS_ENTRY); parser.reset(); // C1 for (state in states) { parser.currentState = state; - parser.parse('\x90'); + parse(parser, '\x90'); chai.expect(parser.currentState).equal(ParserState.DCS_ENTRY); parser.reset(); } @@ -753,7 +768,7 @@ describe('EscapeSequenceParser', function (): void { '\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; - parser.parse(ignored[i]); + parse(parser, ignored[i]); chai.expect(parser.currentState).equal(ParserState.DCS_ENTRY); parser.reset(); } @@ -764,19 +779,19 @@ describe('EscapeSequenceParser', function (): void { const collect = ['\x3c', '\x3d', '\x3e', '\x3f']; for (let i = 0; i < params.length; ++i) { parser.currentState = ParserState.DCS_ENTRY; - parser.parse(params[i]); + 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; - parser.parse('\x3b'); + 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; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); chai.expect(parser.collect).equal(collect[i]); parser.reset(); @@ -790,7 +805,7 @@ describe('EscapeSequenceParser', function (): void { '\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; - parser.parse(ignored[i]); + parse(parser, ignored[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); parser.reset(); } @@ -800,13 +815,13 @@ describe('EscapeSequenceParser', function (): void { 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; - parser.parse(params[i]); + 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; - parser.parse('\x3b'); + parse(parser, '\x3b'); chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); chai.expect(parser.params).eql([0, 0]); parser.reset(); @@ -814,7 +829,7 @@ describe('EscapeSequenceParser', function (): void { it('trans DCS_ENTRY --> DCS_IGNORE', function (): void { parser.reset(); parser.currentState = ParserState.DCS_ENTRY; - parser.parse('\x3a'); + parse(parser, '\x3a'); chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); parser.reset(); }); @@ -823,7 +838,7 @@ describe('EscapeSequenceParser', function (): void { const chars = ['\x3a', '\x3c', '\x3d', '\x3e', '\x3f']; for (let i = 0; i < chars.length; ++i) { parser.currentState = ParserState.DCS_PARAM; - parser.parse('\x3b' + chars[i]); + parse(parser, '\x3b' + chars[i]); chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); chai.expect(parser.params).eql([0, 0]); parser.reset(); @@ -834,21 +849,21 @@ describe('EscapeSequenceParser', function (): void { const chars = r(0x30, 0x40); for (let i = 0; i < chars.length; ++i) { parser.currentState = ParserState.DCS_INTERMEDIATE; - parser.parse(chars[i]); + parse(parser, chars[i]); chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); parser.reset(); } }); it('state DCS_IGNORE ignore rules', function (): void { parser.reset(); - const ignored = [ + 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.concat(r(0x20, 0x80)); + ignored = ignored.concat(r(0x20, 0x80)); for (let i = 0; i < ignored.length; ++i) { parser.currentState = ParserState.DCS_IGNORE; - parser.parse(ignored[i]); + parse(parser, ignored[i]); chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); parser.reset(); } @@ -858,7 +873,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.DCS_ENTRY; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); chai.expect(parser.collect).equal(collect[i]); parser.reset(); @@ -869,7 +884,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.DCS_PARAM; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); chai.expect(parser.collect).equal(collect[i]); parser.reset(); @@ -883,7 +898,7 @@ describe('EscapeSequenceParser', function (): void { '\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; - parser.parse(ignored[i]); + parse(parser, ignored[i]); chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); parser.reset(); } @@ -893,7 +908,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x20, 0x30); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.DCS_INTERMEDIATE; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); chai.expect(parser.collect).equal(collect[i]); parser.reset(); @@ -904,7 +919,7 @@ describe('EscapeSequenceParser', function (): void { const chars = r(0x30, 0x40); for (let i = 0; i < chars.length; ++i) { parser.currentState = ParserState.DCS_INTERMEDIATE; - parser.parse('\x20' + chars[i]); + parse(parser, '\x20' + chars[i]); chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); chai.expect(parser.collect).equal('\x20'); parser.reset(); @@ -916,7 +931,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x40, 0x7f); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.DCS_ENTRY; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); testTerminal.compare([['dcs hook', '', [0], collect[i]]]); parser.reset(); @@ -929,7 +944,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x40, 0x7f); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.DCS_PARAM; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); testTerminal.compare([['dcs hook', '', [0], collect[i]]]); parser.reset(); @@ -942,7 +957,7 @@ describe('EscapeSequenceParser', function (): void { const collect = r(0x40, 0x7f); for (let i = 0; i < collect.length; ++i) { parser.currentState = ParserState.DCS_INTERMEDIATE; - parser.parse(collect[i]); + parse(parser, collect[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); testTerminal.compare([['dcs hook', '', [0], collect[i]]]); parser.reset(); @@ -952,14 +967,14 @@ describe('EscapeSequenceParser', function (): void { it('state DCS_PASSTHROUGH put action', function (): void { parser.reset(); testTerminal.clear(); - const puts = r(0x00, 0x18); - puts.concat(['\x19']); - puts.concat(r(0x1c, 0x20)); - puts.concat(r(0x20, 0x7f)); + 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(); - parser.parse(puts[i]); + parse(parser, puts[i]); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); testTerminal.compare([['dcs put', puts[i]]]); parser.reset(); @@ -970,7 +985,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.DCS_PASSTHROUGH; - parser.parse('\x7f'); + parse(parser, '\x7f'); chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); testTerminal.compare([]); parser.reset(); @@ -989,7 +1004,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); } - parser.parse(s); + parse(parser, s); testTerminal.compare(value); }; }); @@ -1067,7 +1082,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.CSI_IGNORE; - parser.parse('€öäü'); + parse(parser, '€öäü'); chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); testTerminal.compare([]); parser.reset(); @@ -1077,7 +1092,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.DCS_IGNORE; - parser.parse('€öäü'); + parse(parser, '€öäü'); chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); testTerminal.compare([]); parser.reset(); @@ -1087,7 +1102,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.DCS_PASSTHROUGH; - parser.parse('\x901;2;3+$a€öäü'); + 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(); @@ -1097,7 +1112,7 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); testTerminal.clear(); parser.currentState = ParserState.GROUND; - parser.parse('\x9c'); + parse(parser, '\x9c'); chai.expect(parser.currentState).equal(ParserState.GROUND); testTerminal.compare([]); parser.reset(); @@ -1127,15 +1142,17 @@ describe('EscapeSequenceParser', function (): void { clearAccu(); }); it('print handler', function (): void { - parser2.setPrintHandler(function (data: string, start: number, end: number): void { - print += data.substring(start, end); + parser2.setPrintHandler(function (data: Uint32Array, start: number, end: number): void { + for (let i = start; i < end; ++i) { + print += stringFromCodePoint(data[i]); + } }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(print).equal('hello world!$>'); parser2.clearPrintHandler(); parser2.clearPrintHandler(); // should not throw clearAccu(); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(print).equal(''); }); it('ESC handler', function (): void { @@ -1145,28 +1162,28 @@ describe('EscapeSequenceParser', function (): void { parser2.setEscHandler('E', function (): void { esc.push('E'); }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(esc).eql(['%G', 'E']); parser2.clearEscHandler('%G'); parser2.clearEscHandler('%G'); // should not throw clearAccu(); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(esc).eql(['E']); parser2.clearEscHandler('E'); clearAccu(); - parser2.parse(INPUT); + 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]); }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); parser2.clearCsiHandler('m'); parser2.clearCsiHandler('m'); // should not throw clearAccu(); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(csi).eql([]); }); describe('CSI custom handlers', () => { @@ -1174,7 +1191,7 @@ describe('EscapeSequenceParser', function (): void { 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); + parse(parser2, INPUT); chai.expect(csi).eql([], 'Should not fallback to original handler'); chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]); }); @@ -1182,7 +1199,7 @@ describe('EscapeSequenceParser', function (): void { 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); + 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], '']]); }); @@ -1192,7 +1209,7 @@ describe('EscapeSequenceParser', function (): void { 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); + 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], '']]); @@ -1203,7 +1220,7 @@ describe('EscapeSequenceParser', function (): void { 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); + 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], '']]); @@ -1213,7 +1230,7 @@ describe('EscapeSequenceParser', function (): void { 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'); + parse(parser2, '\x1b[0m'); chai.expect(order).eql([3, 2, 1]); }); it('Dispose should work', () => { @@ -1221,7 +1238,7 @@ describe('EscapeSequenceParser', function (): void { 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); + 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'); }); @@ -1231,7 +1248,7 @@ describe('EscapeSequenceParser', function (): void { const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; }); customHandler.dispose(); customHandler.dispose(); - parser2.parse(INPUT); + 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'); }); @@ -1243,24 +1260,24 @@ describe('EscapeSequenceParser', function (): void { parser2.setExecuteHandler('\r', function (): void { exe.push('\r'); }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(exe).eql(['\r', '\n']); parser2.clearExecuteHandler('\r'); parser2.clearExecuteHandler('\r'); // should not throw clearAccu(); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(exe).eql(['\n']); }); it('OSC handler', function (): void { parser2.setOscHandler(1, function (data: string): void { osc.push([1, data]); }); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(osc).eql([[1, 'foo=bar']]); parser2.clearOscHandler(1); parser2.clearOscHandler(1); // should not throw clearAccu(); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(osc).eql([]); }); describe('OSC custom handlers', () => { @@ -1268,7 +1285,7 @@ describe('EscapeSequenceParser', function (): void { 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); + parse(parser2, INPUT); chai.expect(osc).eql([], 'Should not fallback to original handler'); chai.expect(oscCustom).eql([[1, 'foo=bar']]); }); @@ -1276,7 +1293,7 @@ describe('EscapeSequenceParser', function (): void { 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); + parse(parser2, INPUT); chai.expect(osc).eql([[1, 'foo=bar']], 'Should fallback to original handler'); chai.expect(oscCustom).eql([[1, 'foo=bar']]); }); @@ -1286,7 +1303,7 @@ describe('EscapeSequenceParser', function (): void { 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); + 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']]); @@ -1297,7 +1314,7 @@ describe('EscapeSequenceParser', function (): void { 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); + 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']]); @@ -1307,7 +1324,7 @@ describe('EscapeSequenceParser', function (): void { 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\\'); + parse(parser2, '\x1b]1;foo=bar\x1b\\'); chai.expect(order).eql([3, 2, 1]); }); it('Dispose should work', () => { @@ -1315,7 +1332,7 @@ describe('EscapeSequenceParser', function (): void { 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); + parse(parser2, INPUT); chai.expect(osc).eql([[1, 'foo=bar']]); chai.expect(oscCustom).eql([], 'Should not use custom handler as it was disposed'); }); @@ -1325,7 +1342,7 @@ describe('EscapeSequenceParser', function (): void { const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; }); customHandler.dispose(); customHandler.dispose(); - parser2.parse(INPUT); + parse(parser2, INPUT); chai.expect(osc).eql([[1, 'foo=bar']]); chai.expect(oscCustom).eql([], 'Should not use custom handler as it was disposed'); }); @@ -1335,15 +1352,19 @@ describe('EscapeSequenceParser', function (): void { hook: function (collect: string, params: number[], flag: number): void { dcs.push(['hook', collect, params, flag]); }, - put: function (data: string, start: number, end: number): void { - dcs.push(['put', data.substring(start, end)]); + 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']); } }); - parser2.parse('\x1bP1;2;3+pabc'); - parser2.parse(';de\x9c'); + 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'], @@ -1352,8 +1373,8 @@ describe('EscapeSequenceParser', function (): void { parser2.clearDcsHandler('+p'); parser2.clearDcsHandler('+p'); // should not throw clearAccu(); - parser2.parse('\x1bP1;2;3+pabc'); - parser2.parse(';de\x9c'); + parse(parser2, '\x1bP1;2;3+pabc'); + parse(parser2, ';de\x9c'); chai.expect(dcs).eql([]); }); it('ERROR handler', function (): void { @@ -1362,7 +1383,7 @@ describe('EscapeSequenceParser', function (): void { errorState = state; return state; }); - parser2.parse('\x1b[1;2;€;3m'); // faulty escape sequence + parse(parser2, '\x1b[1;2;€;3m'); // faulty escape sequence chai.expect(errorState).eql({ position: 6, code: '€'.charCodeAt(0), @@ -1377,7 +1398,7 @@ describe('EscapeSequenceParser', function (): void { parser2.clearErrorHandler(); parser2.clearErrorHandler(); // should not throw errorState = null; - parser2.parse('\x1b[1;2;a;3m'); + parse(parser2, '\x1b[1;2;a;3m'); chai.expect(errorState).eql(null); }); }); diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts index 6d3de0e0..7b65624d 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -6,6 +6,7 @@ import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from './Types'; import { IDisposable } from 'xterm'; import { Disposable } from './common/Lifecycle'; +import { utf32ToString } from './core/input/TextDecoder'; interface IHandlerCollection { [key: string]: T[]; @@ -134,6 +135,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable { table.addMany(PRINTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); table.addMany(EXECUTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); table.add(0x9c, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.GROUND); + table.add(0x7f, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); // csi entries table.add(0x5b, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.CSI_ENTRY); table.addMany(r(0x40, 0x7f), ParserState.CSI_ENTRY, ParserAction.CSI_DISPATCH, ParserState.GROUND); @@ -202,7 +204,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable { */ class DcsDummy implements IDcsHandler { hook(collect: string, params: number[], flag: number): void { } - put(data: string, start: number, end: number): void { } + put(data: Uint32Array, start: number, end: number): void { } unhook(): void { } } @@ -228,7 +230,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP protected _collect: string; // handler lookup containers - protected _printHandler: (data: string, start: number, end: number) => void; + protected _printHandler: (data: Uint32Array, start: number, end: number) => void; protected _executeHandlers: any; protected _csiHandlers: IHandlerCollection; protected _escHandlers: any; @@ -238,7 +240,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP protected _errorHandler: (state: IParsingState) => IParsingState; // fallback handlers - protected _printHandlerFb: (data: string, start: number, end: number) => void; + protected _printHandlerFb: (data: Uint32Array, start: number, end: number) => void; protected _executeHandlerFb: (code: number) => void; protected _csiHandlerFb: (collect: string, params: number[], flag: number) => void; protected _escHandlerFb: (collect: string, flag: number) => void; @@ -294,7 +296,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._errorHandler = null; } - setPrintHandler(callback: (data: string, start: number, end: number) => void): void { + setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void { this._printHandler = callback; } clearPrintHandler(): void { @@ -397,7 +399,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._activeDcsHandler = null; } - parse(data: string): void { + parse(data: Uint32Array, length: number): void { let code = 0; let transition = 0; let error = false; @@ -412,15 +414,14 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP let callback: Function | null = null; // process input string - const l = data.length; - for (let i = 0; i < l; ++i) { - code = data.charCodeAt(i); + 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 < l && data.charCodeAt(i) > 0x1f && data.charCodeAt(i) < 0x80); + while (i < length && data[i] > 0x1f && data[i] < 0x80); i--; continue; } @@ -563,10 +564,10 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP break; case ParserAction.OSC_PUT: for (let j = i + 1; ; j++) { - if (j >= l - || (code = data.charCodeAt(j)) < 0x20 + if (j >= length + || (code = data[j]) < 0x20 || (code > 0x7f && code <= 0x9f)) { - osc += data.substring(i, j); + osc += utf32ToString(data, i, j); i = j - 1; break; } @@ -610,9 +611,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP // push leftover pushable buffers to terminal if (currentState === ParserState.GROUND && ~print) { - this._printHandler(data, print, data.length); + this._printHandler(data, print, length); } else if (currentState === ParserState.DCS_PASSTHROUGH && ~dcs && dcsHandler) { - dcsHandler.put(data, dcs, data.length); + dcsHandler.put(data, dcs, length); } // save non pushable buffers diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index ac3fbc8b..a7963a2c 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -337,7 +337,9 @@ describe('InputHandler', () => { it('should not cause an infinite loop (regression test)', () => { const term = new Terminal(); const inputHandler = new InputHandler(term); - inputHandler.print(String.fromCharCode(0x200B), 0, 1); + const container = new Uint32Array(10); + container[0] = 0x200B; + inputHandler.print(container, 0, 1); }); }); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 1b3907fd..7405ff9f 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -14,6 +14,8 @@ import { EscapeSequenceParser } from './EscapeSequenceParser'; import { ICharset } from './core/Types'; import { IDisposable } from 'xterm'; import { Disposable } from './common/Lifecycle'; +import { concat } from './common/TypedArrayUtils'; +import { StringToUtf32, stringFromCodePoint, utf32ToString } from './core/input/TextDecoder'; /** * Map collect to glevel. Used in `selectCharset`. @@ -32,21 +34,22 @@ const GLEVEL: {[key: string]: number} = {'(': 0, ')': 1, '*': 2, '+': 3, '-': 1, * Response: DECRPSS (https://vt100.net/docs/vt510-rm/DECRPSS.html) */ class DECRQSS implements IDcsHandler { - private _data: string; + private _data: Uint32Array = new Uint32Array(0); constructor(private _terminal: any) { } hook(collect: string, params: number[], flag: number): void { - // reset data - this._data = ''; + this._data = new Uint32Array(0); } - put(data: string, start: number, end: number): void { - this._data += data.substring(start, end); + put(data: Uint32Array, start: number, end: number): void { + this._data = concat(this._data, data.subarray(start, end)); } unhook(): void { - switch (this._data) { + const data = utf32ToString(this._data); + this._data = new Uint32Array(0); + switch (data) { // valid: DCS 1 $ r Pt ST (xterm) case '"q': // DECSCA return this._terminal.handler(`${C0.ESC}P1$r0"q${C0.ESC}\\`); @@ -66,7 +69,7 @@ class DECRQSS implements IDcsHandler { return this._terminal.handler(`${C0.ESC}P1$r${style} q${C0.ESC}\\`); default: // invalid: DCS 0 $ r Pt ST (xterm) - this._terminal.error('Unknown DCS $q %s', this._data); + this._terminal.error('Unknown DCS $q %s', data); this._terminal.handler(`${C0.ESC}P0$r${C0.ESC}\\`); } } @@ -78,11 +81,17 @@ class DECRQSS implements IDcsHandler { * not supported */ - /** - * DCS + p Pt ST (xterm) - * Set Terminfo Data - * not supported - */ +/** + * DCS + q Pt ST (xterm) + * Request Terminfo String + * not implemented + */ + +/** + * DCS + p Pt ST (xterm) + * Set Terminfo Data + * not supported + */ @@ -94,7 +103,8 @@ class DECRQSS implements IDcsHandler { * each function's header comment. */ export class InputHandler extends Disposable implements IInputHandler { - private _surrogateFirst: string; + private _parseBuffer: Uint32Array = new Uint32Array(4096); + private _stringDecoder: StringToUtf32 = new StringToUtf32(); constructor( protected _terminal: IInputHandlingTerminal, @@ -104,8 +114,6 @@ export class InputHandler extends Disposable implements IInputHandler { this.register(this._parser); - this._surrogateFirst = ''; - /** * custom fallback handlers */ @@ -290,13 +298,13 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.log('data: ' + data); } - // apply leftover surrogate high from last write - if (this._surrogateFirst) { - data = this._surrogateFirst + data; - this._surrogateFirst = ''; + if (this._parseBuffer.length < data.length) { + this._parseBuffer = new Uint32Array(data.length); } - - this._parser.parse(data); + for (let i = 0; i < data.length; ++i) { + this._parseBuffer[i] = data.charCodeAt(i); + } + this._parser.parse(this._parseBuffer, this._stringDecoder.decode(data, this._parseBuffer)); buffer = this._terminal.buffer; if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) { @@ -304,9 +312,9 @@ export class InputHandler extends Disposable implements IInputHandler { } } - public print(data: string, start: number, end: number): void { - let char: string; + public print(data: Uint32Array, start: number, end: number): void { let code: number; + let char: string; let chWidth: number; const buffer: IBuffer = this._terminal.buffer; const charset: ICharset = this._terminal.charset; @@ -318,41 +326,23 @@ export class InputHandler extends Disposable implements IInputHandler { let bufferRow = buffer.lines.get(buffer.y + buffer.ybase); this._terminal.updateRange(buffer.y); - for (let stringPosition = start; stringPosition < end; ++stringPosition) { - char = data.charAt(stringPosition); - code = data.charCodeAt(stringPosition); - - // surrogate pair handling - if (0xD800 <= code && code <= 0xDBFF) { - if (++stringPosition >= end) { - // end of input: - // handle pairs as true UTF-16 and wait for the second part - // since we expect the input comming from a stream there is - // a small chance that the surrogate pair got split - // therefore we dont process the first char here, instead - // it gets added as first char to the next processed chunk - this._surrogateFirst = char; - continue; - } - const second = data.charCodeAt(stringPosition); - // if the second part is in surrogate pair range create the high codepoint - // otherwise fall back to UCS-2 behavior (handle codepoints independently) - if (0xDC00 <= second && second <= 0xDFFF) { - code = (code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000; - char += data.charAt(stringPosition); - } else { - stringPosition--; - } - } + for (let pos = start; pos < end; ++pos) { + code = data[pos]; + char = stringFromCodePoint(code); // calculate print space // expensive call, therefore we save width in line buffer chWidth = wcwidth(code); // get charset replacement character - if (charset) { - char = charset[char] || char; - code = char.charCodeAt(0); + // charset are only defined for ASCII, therefore we only + // search for an replacement char if code < 127 + if (code < 127 && charset) { + const ch = charset[char]; + if (ch) { + code = ch.charCodeAt(0); + char = ch; + } } if (screenReaderMode) { @@ -1294,7 +1284,9 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._terminal.element) { this._terminal.element.classList.add('enable-mouse-events'); } - this._terminal.selectionManager.disable(); + if (this._terminal.selectionManager) { + this._terminal.selectionManager.disable(); + } this._terminal.log('Binding to mouse events.'); break; case 1004: // send focusin/focusout events @@ -1484,7 +1476,9 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._terminal.element) { this._terminal.element.classList.remove('enable-mouse-events'); } - this._terminal.selectionManager.enable(); + if (this._terminal.selectionManager) { + this._terminal.selectionManager.enable(); + } break; case 1004: // send focusin/focusout events this._terminal.sendFocus = false; diff --git a/src/Terminal.ts b/src/Terminal.ts index 4ddd0431..4a85758f 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -64,10 +64,12 @@ const document = (typeof window !== 'undefined') ? window.document : null; const WRITE_BUFFER_PAUSE_THRESHOLD = 5; /** - * The number of writes to perform in a single batch before allowing the - * renderer to catch up with a 0ms setTimeout. + * The max number of ms to spend on writes before allowing the renderer to + * catch up with a 0ms setTimeout. A value of < 33 to keep us close to + * 30fps, and a value of < 16 to try to run at 60fps. Of course, the real FPS + * depends on the time it takes for the renderer to draw the frame. */ -const WRITE_BATCH_SIZE = 300; +const WRITE_TIMEOUT_MS = 12; const MINIMUM_COLS = 2; // Less than 2 can mess with wide chars const MINIMUM_ROWS = 1; @@ -566,12 +568,12 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // Firefox doesn't appear to fire the contextmenu event on right click this.register(addDisposableDomListener(this.element, 'mousedown', (event: MouseEvent) => { if (event.button === 2) { - rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord); + rightClickHandler(event, this, this.selectionManager, this.options.rightClickSelectsWord); } })); } else { this.register(addDisposableDomListener(this.element, 'contextmenu', (event: MouseEvent) => { - rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord); + rightClickHandler(event, this, this.selectionManager, this.options.rightClickSelectsWord); })); } @@ -583,7 +585,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // that the regular click event doesn't fire for the middle mouse button. this.register(addDisposableDomListener(this.element, 'auxclick', (event: MouseEvent) => { if (event.button === 1) { - moveTextAreaUnderMouseCursor(event, this.textarea); + moveTextAreaUnderMouseCursor(event, this); } })); } @@ -738,6 +740,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.mouseHelper = new MouseHelper(this.renderer); // apply mouse event classes set by escape codes before terminal was attached this.element.classList.toggle('enable-mouse-events', this.mouseEvents); + if (this.mouseEvents) { + this.selectionManager.disable(); + } else { + this.selectionManager.enable(); + } if (this.options.screenReaderMode) { // Note that this must be done *after* the renderer is created in order to @@ -854,17 +861,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II if (ch > 127) ch = 127; data.push(ch); } else { - if (ch === 2047) { - data.push(0); + if (ch > 2047) { + data.push(2047); return; } - if (ch < 127) { - data.push(ch); - } else { - if (ch > 2047) ch = 2047; - data.push(0xC0 | (ch >> 6)); - data.push(0x80 | (ch & 0x3F)); - } + data.push(ch); } } @@ -1343,19 +1344,20 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } } - protected _innerWrite(): void { + protected _innerWrite(bufferOffset: number = 0): void { // Ensure the terminal isn't disposed if (this._isDisposed) { this.writeBuffer = []; } - const writeBatch = this.writeBuffer.splice(0, WRITE_BATCH_SIZE); - while (writeBatch.length > 0) { - const data = writeBatch.shift(); + const startTime = Date.now(); + while (this.writeBuffer.length > bufferOffset) { + const data = this.writeBuffer[bufferOffset]; + bufferOffset++; // If XOFF was sent in order to catch up with the pty process, resume it if - // the writeBuffer is empty to allow more data to come in. - if (this._xoffSentToCatchUp && writeBatch.length === 0 && this.writeBuffer.length === 0) { + // we reached the end of the writeBuffer to allow more data to come in. + if (this._xoffSentToCatchUp && this.writeBuffer.length === bufferOffset) { this.handler(C0.DC1); this._xoffSentToCatchUp = false; } @@ -1373,12 +1375,17 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.updateRange(this.buffer.y); this.refresh(this._refreshStart, this._refreshEnd); + + if (Date.now() - startTime >= WRITE_TIMEOUT_MS) { + break; + } } - if (this.writeBuffer.length > 0) { + if (this.writeBuffer.length > bufferOffset) { // Allow renderer to catch up before processing the next batch - setTimeout(() => this._innerWrite(), 0); + setTimeout(() => this._innerWrite(bufferOffset), 0); } else { this._writeInProgress = false; + this.writeBuffer = []; } } diff --git a/src/Types.ts b/src/Types.ts index 31e42339..cc8ff00a 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -111,7 +111,7 @@ export interface ICompositionHelper { */ export interface IInputHandler { parse(data: string): void; - print(data: string, start: number, end: number): void; + print(data: Uint32Array, start: number, end: number): void; /** C0 BEL */ bell(): void; /** C0 LF */ lineFeed(): void; @@ -452,18 +452,26 @@ export interface IParsingState { * DCS handler signature for EscapeSequenceParser. * EscapeSequenceParser handles DCS commands via separate * subparsers that get hook/unhooked and can handle -* arbitrary amount of print data. +* arbitrary amount of data. +* * On entering a DSC sequence `hook` is called by * `EscapeSequenceParser`. Use it to initialize or reset * states needed to handle the current DCS sequence. +* Note: A DCS parser is only instantiated once, therefore +* you cannot rely on the ctor to reinitialize state. +* * EscapeSequenceParser will call `put` several times if the -* parsed string got splitted, therefore you might have to collect -* `data` until `unhook` is called. `unhook` marks the end -* of the current DCS sequence. +* parsed data got split, therefore you might have to collect +* `data` until `unhook` is called. +* Note: `data` is borrowed, if you cannot process the data +* in chunks you have to copy it, doing otherwise will lead to +* data losses or corruption. +* +* `unhook` marks the end of the current DCS sequence. */ export interface IDcsHandler { hook(collect: string, params: number[], flag: number): void; - put(data: string, start: number, end: number): void; + put(data: Uint32Array, start: number, end: number): void; unhook(): void; } @@ -480,9 +488,9 @@ export interface IEscapeSequenceParser extends IDisposable { * Parse string `data`. * @param data The data to parse. */ - parse(data: string): void; + parse(data: Uint32Array, length: number): void; - setPrintHandler(callback: (data: string, start: number, end: number) => void): void; + setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void; clearPrintHandler(): void; setExecuteHandler(flag: string, callback: () => void): void; diff --git a/src/addons/winptyCompat/winptyCompat.ts b/src/addons/winptyCompat/winptyCompat.ts index aec580ed..d162f4e9 100644 --- a/src/addons/winptyCompat/winptyCompat.ts +++ b/src/addons/winptyCompat/winptyCompat.ts @@ -19,6 +19,8 @@ export function winptyCompatInit(terminal: Terminal): void { return; } + (addonTerminal._core as any).isWinptyCompatEnabled = true; + // Winpty does not support wraparound mode which means that lines will never // be marked as wrapped. This causes issues for things like copying a line // retaining the wrapped new line characters or if consumers are listening diff --git a/src/common/TypedArrayUtils.test.ts b/src/common/TypedArrayUtils.test.ts index 69a62abc..99b0fd82 100644 --- a/src/common/TypedArrayUtils.test.ts +++ b/src/common/TypedArrayUtils.test.ts @@ -3,21 +3,20 @@ * @license MIT */ import { assert } from 'chai'; -import { fillFallback } from './TypedArrayUtils'; +import { fillFallback, concat } from './TypedArrayUtils'; type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray | Int8Array | Int16Array | Int32Array | Float32Array | Float64Array; -describe('polyfill conformance tests', function(): void { - - function deepEquals(a: TypedArray, b: TypedArray): void { - assert.equal(a.length, b.length); - for (let i = 0; i < a.length; ++i) { - assert.equal(a[i], b[i]); - } +function deepEquals(a: TypedArray, b: TypedArray): void { + assert.equal(a.length, b.length); + for (let i = 0; i < a.length; ++i) { + assert.equal(a[i], b[i]); } +} +describe('polyfill conformance tests', function(): void { describe('TypedArray.fill', function(): void { it('should work with all typed array types', function(): void { const u81 = new Uint8Array(5); @@ -87,3 +86,12 @@ describe('polyfill conformance tests', function(): void { }); }); }); + +describe('typed array convenience functions', () => { + it('concat', () => { + const a = new Uint8Array([1, 2, 3, 4, 5]); + const b = new Uint8Array([6, 7, 8, 9, 0]); + const merged = concat(a, b); + deepEquals(merged, new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 0])); + }); +}); diff --git a/src/common/TypedArrayUtils.ts b/src/common/TypedArrayUtils.ts index 6e1a3630..54699835 100644 --- a/src/common/TypedArrayUtils.ts +++ b/src/common/TypedArrayUtils.ts @@ -3,15 +3,15 @@ * @license MIT */ +export type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray + | Int8Array | Int16Array | Int32Array + | Float32Array | Float64Array; + + /** * polyfill for TypedArray.fill * This is needed to support .fill in all safari versions and IE 11. */ - -type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray - | Int8Array | Int16Array | Int32Array - | Float32Array | Float64Array; - export function fill(array: T, value: number, start?: number, end?: number): T { // all modern engines that support .fill if (array.fill) { @@ -39,3 +39,14 @@ export function fillFallback(array: T, value: number, star } return array; } + +/** + * Concat two typed arrays `a` and `b`. + * Returns a new typed array. + */ +export function concat(a: T, b: T): T { + const result = new (a.constructor as any)(a.length + b.length); + result.set(a); + result.set(b, a.length); + return result; +} diff --git a/src/core/input/TextDecoder.test.ts b/src/core/input/TextDecoder.test.ts new file mode 100644 index 00000000..12f3099a --- /dev/null +++ b/src/core/input/TextDecoder.test.ts @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; +import { StringToUtf32, stringFromCodePoint, utf32ToString } from './TextDecoder'; + +describe('text encodings', () => { + it('stringFromCodePoint/utf32ToString', () => { + const s = 'abcdefg'; + const data = new Uint32Array(s.length); + for (let i = 0; i < s.length; ++i) { + data[i] = s.charCodeAt(i); + assert.equal(stringFromCodePoint(data[i]), s[i]); + } + assert.equal(utf32ToString(data), s); + }); + + describe('StringToUtf32 Decoder', () => { + describe('full codepoint test', () => { + it('0..65535', () => { + const decoder = new StringToUtf32(); + const target = new Uint32Array(5); + for (let i = 0; i < 65536; ++i) { + // skip surrogate pairs + if (i >= 0xD800 && i <= 0xDFFF) { + continue; + } + const length = decoder.decode(String.fromCharCode(i), target); + assert.equal(length, 1); + assert.equal(target[0], i); + assert.equal(utf32ToString(target, 0, length), String.fromCharCode(i)); + decoder.clear(); + } + }); + it('65536..0x10FFFF (surrogates)', function(): void { + this.timeout(20000); + const decoder = new StringToUtf32(); + const target = new Uint32Array(5); + for (let i = 65536; i < 0x10FFFF; ++i) { + const codePoint = i - 0x10000; + const s = String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00); + const length = decoder.decode(s, target); + assert.equal(length, 1); + assert.equal(target[0], i); + assert.equal(utf32ToString(target, 0, length), s); + decoder.clear(); + } + }); + }); + + describe('stream handling', () => { + it('surrogates mixed advance by 1', () => { + const decoder = new StringToUtf32(); + const target = new Uint32Array(5); + const input = 'Ä€𝄞Ö𝄞€Ü𝄞€'; + let decoded = ''; + for (let i = 0; i < input.length; ++i) { + const written = decoder.decode(input[i], target); + decoded += utf32ToString(target, written); + } + assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€'); + }); + }); + }); +}); diff --git a/src/core/input/TextDecoder.ts b/src/core/input/TextDecoder.ts new file mode 100644 index 00000000..1ff00940 --- /dev/null +++ b/src/core/input/TextDecoder.ts @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +/** + * StringToUtf32 - decodes UTF16 sequences into UTF32 codepoints. + * To keep the decoder in line with JS strings it handles single surrogates as UCS2. + */ +export class StringToUtf32 { + private _interim: number = 0; + + /** + * Clears interim and resets decoder to clean state. + */ + public clear(): void { + this._interim = 0; + } + + /** + * Decode JS string to UTF32 codepoints. + * The methods assumes stream input and will store partly transmitted + * surrogate pairs and decode them with the next data chunk. + * Note: The method does no bound checks for target, therefore make sure + * the provided input data does not exceed the size of `target`. + * Returns the number of written codepoints in `target`. + */ + decode(input: string, target: Uint32Array): number { + const length = input.length; + + if (!length) { + return 0; + } + + let size = 0; + let startPos = 0; + + // handle leftover surrogate high + if (this._interim) { + const second = input.charCodeAt(startPos++); + if (0xDC00 <= second && second <= 0xDFFF) { + target[size++] = (this._interim - 0xD800) * 0x400 + second - 0xDC00 + 0x10000; + } else { + // illegal codepoint (USC2 handling) + target[size++] = this._interim; + target[size++] = second; + } + this._interim = 0; + } + + for (let i = startPos; i < length; ++i) { + const code = input.charCodeAt(i); + // surrogate pair first + if (0xD800 <= code && code <= 0xDBFF) { + if (++i >= length) { + this._interim = code; + return size; + } + const second = input.charCodeAt(i); + if (0xDC00 <= second && second <= 0xDFFF) { + target[size++] = (code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000; + } else { + // illegal codepoint (USC2 handling) + target[size++] = code; + target[size++] = second; + } + continue; + } + target[size++] = code; + } + return size; + } +} + +/** + * Convert UTF32 codepoint into JS string. + */ +export function stringFromCodePoint(codePoint: number): string { + if (codePoint > 0xFFFF) { + // UTF32 to UTF16 conversion (see comments in utf32ToString) + codePoint -= 0x10000; + return String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00); + } + return String.fromCharCode(codePoint); +} + +/** + * Convert UTF32 char codes into JS string. + * Basically the same as `stringFromCodePoint` but for multiple codepoints + * in a loop (which is a lot faster). + */ +export function utf32ToString(data: Uint32Array, start: number = 0, end: number = data.length): string { + let result = ''; + for (let i = start; i < end; ++i) { + let codepoint = data[i]; + if (codepoint > 0xFFFF) { + // JS string are encoded as UTF16, thus a non BMP codepoint gets converted into a surrogate pair + // conversion rules: + // - subtract 0x10000 from code point, leaving a 20 bit number + // - add high 10 bits to 0xD800 --> first surrogate + // - add low 10 bits to 0xDC00 --> second surrogate + codepoint -= 0x10000; + result += String.fromCharCode((codepoint >> 10) + 0xD800) + String.fromCharCode((codepoint % 0x400) + 0xDC00); + } else { + result += String.fromCharCode(codepoint); + } + } + return result; +} diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 3e0b8643..a609d79c 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -241,7 +241,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.fillText( charData[CHAR_DATA_CHAR_INDEX], x * this._scaledCellWidth + this._scaledCharLeft, - (y + 0.5) * this._scaledCellHeight + this._scaledCharTop); + y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2); } /** @@ -316,7 +316,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.fillText( chars, x * this._scaledCellWidth + this._scaledCharLeft, - (y + 0.5) * this._scaledCellHeight + this._scaledCharTop); + y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2); this._ctx.restore(); } diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 02328877..b8ef87aa 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -70,7 +70,7 @@ export class Renderer extends EventEmitter implements IRenderer { // Detect whether IntersectionObserver is detected and enable renderer pause // and resume based on terminal visibility if so if ('IntersectionObserver' in window) { - const observer = new IntersectionObserver(e => this.onIntersectionChange(e[0]), { threshold: 0 }); + const observer = new IntersectionObserver(e => this.onIntersectionChange(e[e.length - 1]), { threshold: 0 }); observer.observe(this._terminal.element); this.register({ dispose: () => observer.disconnect() }); } diff --git a/src/renderer/SelectionRenderLayer.ts b/src/renderer/SelectionRenderLayer.ts index 81782ee8..fc16a2fb 100644 --- a/src/renderer/SelectionRenderLayer.ts +++ b/src/renderer/SelectionRenderLayer.ts @@ -55,6 +55,7 @@ export class SelectionRenderLayer extends BaseRenderLayer { // Selection does not exist if (!start || !end) { + this._clearState(); return; } diff --git a/src/ui/Clipboard.ts b/src/ui/Clipboard.ts index b1acba9d..2570a8b1 100644 --- a/src/ui/Clipboard.ts +++ b/src/ui/Clipboard.ts @@ -85,26 +85,32 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void { * @param ev The original right click event to be handled. * @param textarea The terminal's textarea. */ -export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextAreaElement): void { - // Bring textarea at the cursor position - textarea.style.position = 'fixed'; - textarea.style.width = '20px'; - textarea.style.height = '20px'; - textarea.style.left = (ev.clientX - 10) + 'px'; - textarea.style.top = (ev.clientY - 10) + 'px'; - textarea.style.zIndex = '1000'; +export function moveTextAreaUnderMouseCursor(ev: MouseEvent, term: ITerminal): void { - textarea.focus(); + // Calculate textarea position relative to the screen element + const pos = term.screenElement.getBoundingClientRect(); + const left = ev.clientX - pos.left - 10; + const top = ev.clientY - pos.top - 10; + + // Bring textarea at the cursor position + term.textarea.style.position = 'absolute'; + term.textarea.style.width = '20px'; + term.textarea.style.height = '20px'; + term.textarea.style.left = `${left}px`; + term.textarea.style.top = `${top}px`; + term.textarea.style.zIndex = '1000'; + + term.textarea.focus(); // Reset the terminal textarea's styling // Timeout needs to be long enough for click event to be handled. setTimeout(() => { - textarea.style.position = null; - textarea.style.width = null; - textarea.style.height = null; - textarea.style.left = null; - textarea.style.top = null; - textarea.style.zIndex = null; + term.textarea.style.position = null; + term.textarea.style.width = null; + term.textarea.style.height = null; + term.textarea.style.left = null; + term.textarea.style.top = null; + term.textarea.style.zIndex = null; }, 200); } @@ -115,14 +121,14 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA * @param selectionManager The terminal's selection manager. * @param shouldSelectWord If true and there is no selection the current word will be selected */ -export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager, shouldSelectWord: boolean): void { - moveTextAreaUnderMouseCursor(ev, textarea); +export function rightClickHandler(ev: MouseEvent, term: ITerminal, selectionManager: ISelectionManager, shouldSelectWord: boolean): void { + moveTextAreaUnderMouseCursor(ev, term); if (shouldSelectWord && !selectionManager.isClickInSelection(ev)) { selectionManager.selectWordAtCursor(ev); } // Get textarea ready to copy from the context menu - textarea.value = selectionManager.selectionText; - textarea.select(); + term.textarea.value = selectionManager.selectionText; + term.textarea.select(); } diff --git a/src/ui/MouseZoneManager.ts b/src/ui/MouseZoneManager.ts index a232f5b9..79022723 100644 --- a/src/ui/MouseZoneManager.ts +++ b/src/ui/MouseZoneManager.ts @@ -23,6 +23,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { private _areZonesActive: boolean = false; private _mouseMoveListener: (e: MouseEvent) => any; + private _mouseLeaveListener: (e: MouseEvent) => any; private _clickListener: (e: MouseEvent) => any; private _tooltipTimeout: number = null; @@ -38,6 +39,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { // These events are expensive, only listen to it when mouse zones are active this._mouseMoveListener = e => this._onMouseMove(e); + this._mouseLeaveListener = e => this._onMouseLeave(e); this._clickListener = e => this._onClick(e); } @@ -89,6 +91,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { if (!this._areZonesActive) { this._areZonesActive = true; this._terminal.element.addEventListener('mousemove', this._mouseMoveListener); + this._terminal.element.addEventListener('mouseleave', this._mouseLeaveListener); this._terminal.element.addEventListener('click', this._clickListener); } } @@ -97,6 +100,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { if (this._areZonesActive) { this._areZonesActive = false; this._terminal.element.removeEventListener('mousemove', this._mouseMoveListener); + this._terminal.element.removeEventListener('mouseleave', this._mouseLeaveListener); this._terminal.element.removeEventListener('click', this._clickListener); } } @@ -169,6 +173,18 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { } } + private _onMouseLeave(e: MouseEvent): void { + // Fire the hover end callback and cancel any existing timer if the mouse + // leaves the terminal element + if (this._currentZone) { + this._currentZone.leaveCallback(); + this._currentZone = null; + if (this._tooltipTimeout) { + clearTimeout(this._tooltipTimeout); + } + } + } + private _onClick(e: MouseEvent): void { // Find the active zone and click it if found const zone = this._findZoneEventAt(e); diff --git a/src/xterm.css b/src/xterm.css index 24cd475f..2e47b1a1 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -36,7 +36,6 @@ */ .xterm { - font-family: courier-new, courier, monospace; font-feature-settings: "liga" 0; position: relative; user-select: none; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index dc9ebbae..d813bc5f 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -443,7 +443,9 @@ declare module 'xterm' { addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable; /** - * Resizes the terminal. + * Resizes the terminal. It's best practice to debounce calls to resize, + * this will help ensure that the pty can respond to the resize event + * before another one occurs. * @param x The number of columns to resize to. * @param y The number of rows to resize to. */