diff --git a/.vscode/launch.json b/.vscode/launch.json index c7bf7381..e5bad7b3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -23,10 +23,7 @@ "type": "chrome", "request": "launch", "name": "Demo Client", - "url": "http://0.0.0.0:3000", - "windows": { - "url": "http://127.0.0.1:3000" - }, + "url": "http://127.0.0.1:3000", "webRoot": "${workspaceFolder}/" }, { @@ -38,7 +35,11 @@ "run", "start-debug" ], - "port": 9229 + "port": 9229, + "serverReadyAction": { + "action": "openExternally", + "pattern": "App listening to (http://.*?:[0-9]+)" + } } ] } diff --git a/README.md b/README.md index 5378f443..40de04cf 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**Kubebox**](https://github.com/astefanutti/kubebox): Terminal console for Kubernetes clusters. - [**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. +- [**rtty**](https://github.com/zhaojh329/rtty): Access your terminals from anywhere via the web. - [**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. @@ -154,6 +154,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**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. - [**Jumpserver**](https://github.com/jumpserver/luna): Jumpserver Luna project, Jumpserver is a bastion server project, Luna use xterm.js for web terminal emulation. +- [**LxdMosaic**](https://github.com/turtle0x1/LxdMosaic): Uses xterm.js to give terminal access to containers through LXD [And much more...](https://github.com/xtermjs/xterm.js/network/dependents) diff --git a/demo/client.ts b/demo/client.ts index f83d3af1..b6b4cbdf 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -30,7 +30,10 @@ Terminal.applyAddon(fit); Terminal.applyAddon(fullscreen); Terminal.applyAddon(search); Terminal.applyAddon(webLinks); -Terminal.applyAddon(winptyCompat); +const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0; +if (isWindows) { + Terminal.applyAddon(winptyCompat); +} let term; @@ -99,7 +102,9 @@ function createTerminal(): void { socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/'; term.open(terminalContainer); - term.winptyCompatInit(); + if (isWindows) { + term.winptyCompatInit(); + } term.webLinksInit(); term.fit(); term.focus(); diff --git a/demo/index.html b/demo/index.html index 12f7cb98..370a51ed 100644 --- a/demo/index.html +++ b/demo/index.html @@ -2,8 +2,8 @@ xterm.js demo - - + + diff --git a/demo/server.js b/demo/server.js index 5ff9ca61..758023c7 100644 --- a/demo/server.js +++ b/demo/server.js @@ -10,7 +10,7 @@ function startServer() { var terminals = {}, logs = {}; - app.use('/build', express.static(__dirname + '/../build')); + app.use('/src', express.static(__dirname + '/../src')); app.get('/', function(req, res){ res.sendFile(__dirname + '/index.html'); @@ -99,7 +99,7 @@ function startServer() { var port = process.env.PORT || 3000, host = os.platform() === 'win32' ? '127.0.0.1' : '0.0.0.0'; - console.log('App listening to http://' + host + ':' + port); + console.log('App listening to http://127.0.0.1:' + port); app.listen(port, host); } diff --git a/demo/zmodem/app.js b/demo/zmodem/app.js deleted file mode 100644 index 7124c222..00000000 --- a/demo/zmodem/app.js +++ /dev/null @@ -1,87 +0,0 @@ -var express = require('express'); -var app = express(); -var expressWs = require('express-ws')(app); -var os = require('os'); -var pty = require('node-pty'); - -var terminals = {}, - logs = {}; - -app.use('/build', express.static(__dirname + '/../../build')); -app.use('/demo', express.static(__dirname + '/../../demo')); -app.use('/zmodemjs', express.static(__dirname + '/../../node_modules/zmodem.js/dist')); - -app.get('/', function(req, res){ - res.sendFile(__dirname + '/index.html'); -}); - -app.get('/style.css', function(req, res){ - res.sendFile(__dirname + '../style.css'); -}); - -app.get('/main.js', function(req, res){ - res.sendFile(__dirname + '/main.js'); -}); - -app.post('/terminals', function (req, res) { - var cols = parseInt(req.query.cols), - rows = parseInt(req.query.rows), - term = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : 'bash', [], { - encoding: null, - name: 'xterm-color', - cols: cols || 80, - rows: rows || 24, - cwd: process.env.PWD, - env: process.env - }); - - console.log('Created terminal with PID: ' + term.pid); - terminals[term.pid] = term; - logs[term.pid] = ''; - term.on('data', function(data) { - logs[term.pid] += data; - }); - res.send(term.pid.toString()); - res.end(); -}); - -app.post('/terminals/:pid/size', function (req, res) { - var pid = parseInt(req.params.pid), - cols = parseInt(req.query.cols), - rows = parseInt(req.query.rows), - term = terminals[pid]; - - term.resize(cols, rows); - console.log('Resized terminal ' + pid + ' to ' + cols + ' cols and ' + rows + ' rows.'); - res.end(); -}); - -app.ws('/terminals/:pid', function (ws, req) { - var term = terminals[parseInt(req.params.pid)]; - console.log('Connected to terminal ' + term.pid); - ws.send(logs[term.pid]); - - term.on('data', function(data) { - try { - ws.send(data); - } catch (ex) { - // The WebSocket is not open, ignore - } - }); - ws.on('message', function(msg) { - term.write(msg); - }); - ws.on('close', function () { - term.kill(); - console.log('Closed terminal ' + term.pid); - // Clean things up - delete terminals[term.pid]; - delete logs[term.pid]; - }); -}); - -var port = process.env.PORT || 3000, - host = os.platform() === 'win32' ? '127.0.0.1' : '0.0.0.0'; - -console.log('App listening to http://' + host + ':' + port); -app.listen(port, host); diff --git a/demo/zmodem/index.html b/demo/zmodem/index.html deleted file mode 100644 index aee7742a..00000000 --- a/demo/zmodem/index.html +++ /dev/null @@ -1,128 +0,0 @@ - - - - xterm.js demo - - - - - - - - - - - - - - - - -

xterm.js: xterm, in the browser

- -
- -
- - - - - - - - - -
- -
-

Actions

-

- - -

-
-
-

Options

-

- -

-

- -

-

- -

-

- -

-

- -

-

- -

-
-

Size

-
-
- - -
-
- - -
-
-
-
-

Attention: The demo is a barebones implementation and is designed for xterm.js evaluation purposes only. Exposing the demo to the public as is would introduce security risks for the host.

-

* ZMODEM file transfers are supported via an addon. To try it out, install lrzsz onto the remote peer, then run rz to send from your browser or sz <file> to send from the remote peer.

- - - diff --git a/demo/zmodem/main.js b/demo/zmodem/main.js deleted file mode 100644 index 619ef2b8..00000000 --- a/demo/zmodem/main.js +++ /dev/null @@ -1,388 +0,0 @@ -"use strict"; - -var term, - protocol, - socketURL, - socket, - pid; - -Terminal.applyAddon(fit); -Terminal.applyAddon(attach); -Terminal.applyAddon(zmodem); -Terminal.applyAddon(search); - -var terminalContainer = document.getElementById('terminal-container'), - actionElements = { - findNext: document.querySelector('#find-next'), - findPrevious: document.querySelector('#find-previous') - }, - optionElements = { - cursorBlink: document.querySelector('#option-cursor-blink'), - cursorStyle: document.querySelector('#option-cursor-style'), - scrollback: document.querySelector('#option-scrollback'), - tabstopwidth: document.querySelector('#option-tabstopwidth'), - bellStyle: document.querySelector('#option-bell-style') - }, - colsElement = document.getElementById('cols'), - rowsElement = document.getElementById('rows'); - -function setTerminalSize() { - var cols = parseInt(colsElement.value, 10); - var rows = parseInt(rowsElement.value, 10); - var viewportElement = document.querySelector('.xterm-viewport'); - var scrollBarWidth = viewportElement.offsetWidth - viewportElement.clientWidth; - var width = (cols * term.charMeasure.width + 20 /*room for scrollbar*/).toString() + 'px'; - var height = (rows * term.charMeasure.height).toString() + 'px'; - - terminalContainer.style.width = width; - terminalContainer.style.height = height; - term.resize(cols, rows); -} - -colsElement.addEventListener('change', setTerminalSize); -rowsElement.addEventListener('change', setTerminalSize); - -actionElements.findNext.addEventListener('keypress', function (e) { - if (e.key === "Enter") { - e.preventDefault(); - term.findNext(actionElements.findNext.value); - } -}); -actionElements.findPrevious.addEventListener('keypress', function (e) { - if (e.key === "Enter") { - e.preventDefault(); - term.findPrevious(actionElements.findPrevious.value); - } -}); - -optionElements.cursorBlink.addEventListener('change', function () { - term.setOption('cursorBlink', optionElements.cursorBlink.checked); -}); -optionElements.cursorStyle.addEventListener('change', function () { - term.setOption('cursorStyle', optionElements.cursorStyle.value); -}); -optionElements.bellStyle.addEventListener('change', function () { - term.setOption('bellStyle', optionElements.bellStyle.value); -}); -optionElements.scrollback.addEventListener('change', function () { - term.setOption('scrollback', parseInt(optionElements.scrollback.value, 10)); -}); -optionElements.tabstopwidth.addEventListener('change', function () { - term.setOption('tabStopWidth', parseInt(optionElements.tabstopwidth.value, 10)); -}); - -createTerminal(); - -function createTerminal() { - // Clean terminal - while (terminalContainer.children.length) { - terminalContainer.removeChild(terminalContainer.children[0]); - } - term = new Terminal({ - cursorBlink: optionElements.cursorBlink.checked, - scrollback: parseInt(optionElements.scrollback.value, 10), - tabStopWidth: parseInt(optionElements.tabstopwidth.value, 10) - }); - term.on('resize', function (size) { - if (!pid) { - return; - } - var cols = size.cols, - rows = size.rows, - url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows; - - fetch(url, {method: 'POST'}); - }); - protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://'; - socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/'; - - term.open(terminalContainer); - term.fit(); - - // fit is called within a setTimeout, cols and rows need this. - setTimeout(function () { - colsElement.value = term.cols; - rowsElement.value = term.rows; - - // Set terminal size again to set the specific dimensions on the demo - setTerminalSize(); - - fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then(function (res) { - - res.text().then(function (pid) { - window.pid = pid; - socketURL += pid; - socket = new WebSocket(socketURL); - socket.onopen = runRealTerminal; - socket.onclose = runFakeTerminal; - socket.onerror = runFakeTerminal; - - term.zmodemAttach(socket, { - noTerminalWriteOutsideSession: true, - } ); - - term.on("zmodemRetract", () => { - start_form.style.display = "none"; - start_form.onsubmit = null; - }); - - term.on("zmodemDetect", (detection) => { - function do_zmodem() { - term.detach(); - let zsession = detection.confirm(); - - var promise; - - if (zsession.type === "receive") { - promise = _handle_receive_session(zsession); - } - else { - promise = _handle_send_session(zsession); - } - - promise.catch( console.error.bind(console) ).then( () => { - term.attach(socket); - } ); - } - - if (_auto_zmodem()) { - do_zmodem(); - } - else { - start_form.style.display = ""; - start_form.onsubmit = function(e) { - start_form.style.display = "none"; - - if (document.getElementById("zmstart_yes").checked) { - do_zmodem(); - } - else { - detection.deny(); - } - }; - } - }); - }); - }); - }, 0); -} - -//---------------------------------------------------------------------- -// UI STUFF - -function _show_file_info(xfer) { - var file_info = xfer.get_details(); - - document.getElementById("name").textContent = file_info.name; - document.getElementById("size").textContent = file_info.size; - document.getElementById("mtime").textContent = file_info.mtime; - document.getElementById("files_remaining").textContent = file_info.files_remaining; - document.getElementById("bytes_remaining").textContent = file_info.bytes_remaining; - - document.getElementById("mode").textContent = "0" + file_info.mode.toString(8); - - var xfer_opts = xfer.get_options(); - ["conversion", "management", "transport", "sparse"].forEach( (lbl) => { - document.getElementById(`zfile_${lbl}`).textContent = xfer_opts[lbl]; - } ); - - document.getElementById("zm_file").style.display = ""; -} -function _hide_file_info() { - document.getElementById("zm_file").style.display = "none"; -} - -function _save_to_disk(xfer, buffer) { - return Zmodem.Browser.save_to_disk(buffer, xfer.get_details().name); -} - -var skipper_button = document.getElementById("zm_progress_skipper"); -var skipper_button_orig_text = skipper_button.textContent; - -function _show_progress() { - skipper_button.disabled = false; - skipper_button.textContent = skipper_button_orig_text; - - document.getElementById("bytes_received").textContent = 0; - document.getElementById("percent_received").textContent = 0; - - document.getElementById("zm_progress").style.display = ""; -} - -function _update_progress(xfer) { - var total_in = xfer.get_offset(); - - document.getElementById("bytes_received").textContent = total_in; - - var percent_received = 100 * total_in / xfer.get_details().size; - document.getElementById("percent_received").textContent = percent_received.toFixed(2); -} - -function _hide_progress() { - document.getElementById("zm_progress").style.display = "none"; -} - -var start_form = document.getElementById("zm_start"); - -function _auto_zmodem() { - return document.getElementById("zmodem-auto").checked; -} - -// END UI STUFF -//---------------------------------------------------------------------- - -function _handle_receive_session(zsession) { - zsession.on("offer", function(xfer) { - current_receive_xfer = xfer; - - _show_file_info(xfer); - - var offer_form = document.getElementById("zm_offer"); - - function on_form_submit() { - offer_form.style.display = "none"; - - //START - //if (offer_form.zmaccept.value) { - if (_auto_zmodem() || document.getElementById("zmaccept_yes").checked) { - _show_progress(); - - var FILE_BUFFER = []; - xfer.on("input", (payload) => { - _update_progress(xfer); - FILE_BUFFER.push( new Uint8Array(payload) ); - }); - xfer.accept().then( - () => { - _save_to_disk(xfer, FILE_BUFFER); - }, - console.error.bind(console) - ); - } - else { - xfer.skip(); - } - //END - } - - if (_auto_zmodem()) { - on_form_submit(); - } - else { - offer_form.onsubmit = on_form_submit; - offer_form.style.display = ""; - } - } ); - - var promise = new Promise( (res) => { - zsession.on("session_end", () => { - _hide_file_info(); - _hide_progress(); - res(); - } ); - } ); - - zsession.start(); - - return promise; -} - -function _handle_send_session(zsession) { - var choose_form = document.getElementById("zm_choose"); - choose_form.style.display = ""; - - var file_el = document.getElementById("zm_files"); - - var promise = new Promise( (res) => { - file_el.onchange = function(e) { - choose_form.style.display = "none"; - - var files_obj = file_el.files; - - Zmodem.Browser.send_files( - zsession, - files_obj, - { - on_offer_response(obj, xfer) { - if (xfer) _show_progress(); - //console.log("offer", xfer ? "accepted" : "skipped"); - }, - on_progress(obj, xfer) { - _update_progress(xfer); - }, - on_file_complete(obj) { - //console.log("COMPLETE", obj); - _hide_progress(); - }, - } - ).then(_hide_progress).then( - zsession.close.bind(zsession), - console.error.bind(console) - ).then( () => { - _hide_file_info(); - _hide_progress(); - res(); - } ); - }; - } ); - - return promise; -} - -//This is here to allow canceling of an in-progress ZMODEM transfer. -var current_receive_xfer; - -//Called from HTML directly. -function skip_current_file() { - current_receive_xfer.skip(); - - skipper_button.disabled = true; - skipper_button.textContent = "Waiting for server to acknowledge skip …"; -} - -function runRealTerminal() { - term.attach(socket); - - term._initialized = true; -} - -function runFakeTerminal() { - if (term._initialized) { - return; - } - - term._initialized = true; - - var shellprompt = '$ '; - - term.prompt = function () { - term.write('\r\n' + shellprompt); - }; - - term.writeln('Welcome to xterm.js'); - term.writeln('This is a local terminal emulation, without a real terminal in the back-end.'); - term.writeln('Type some keys and commands to play around.'); - term.writeln(''); - term.prompt(); - - term.on('key', function (key, ev) { - var printable = ( - !ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey - ); - - if (ev.keyCode == 13) { - term.prompt(); - } else if (ev.keyCode == 8) { - // Do not delete the prompt - if (term.x > 2) { - term.write('\b \b'); - } - } else if (printable) { - term.write(key); - } - }); - - term.on('paste', function (data, ev) { - term.write(data); - }); -} diff --git a/gulpfile.js b/gulpfile.js index 9af8d6e4..bbb4d6e1 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -16,9 +16,9 @@ const ts = require('gulp-typescript'); const util = require('gulp-util'); const buildDir = process.env.BUILD_DIR || 'build'; -const tsProject = ts.createProject('tsconfig.json'); -let srcDir = tsProject.config.compilerOptions.rootDir; -let outDir = tsProject.config.compilerOptions.outDir; +const tsProject = ts.createProject('src/tsconfig.json'); +let srcDir = './src'; +let outDir = './lib'; const addons = fs.readdirSync(`${__dirname}/src/addons`); @@ -61,7 +61,7 @@ gulp.task('browserify', function() { }; let bundleStream = browserify(browserifyOptions) .bundle() - .pipe(source('xterm.js')) + .pipe(source(`xterm.js`)) .pipe(buffer()) .pipe(sourcemaps.init({loadMaps: true, sourceRoot: '..'})) .pipe(sourcemaps.write('./')) @@ -136,6 +136,6 @@ gulp.task('sorcery-addons', ['browserify-addons'], function () { }) }); -gulp.task('build', ['sorcery', 'sorcery-addons']); +gulp.task('build', ['css', 'sorcery', 'sorcery-addons']); gulp.task('test', ['mocha']); gulp.task('default', ['build']); diff --git a/package.json b/package.json index c5fad515..539b96da 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "@types/webpack": "^4.4.11", "browserify": "^13.3.0", "chai": "3.5.0", - "concurrently": "^3.5.1", "coveralls": "^3.0.1", "express": "4.13.4", "express-ws": "2.0.0-rc.1", @@ -38,7 +37,7 @@ "ts-loader": "^4.5.0", "tslint": "^5.9.1", "tslint-consistent-codestyle": "^1.13.0", - "typescript": "3.1", + "typescript": "3.4", "vinyl-buffer": "^1.0.0", "vinyl-source-stream": "^1.1.0", "webpack": "^4.17.1", @@ -50,20 +49,16 @@ "start-debug": "node --inspect-brk demo/start", "start-zmodem": "node demo/zmodem/app", "lint": "tslint 'src/**/*.ts' './demo/**/*.ts'", - "pretest": "npm run layering", "test": "npm run mocha", "posttest": "npm run lint", "test-debug": "node --inspect-brk node_modules/.bin/gulp test", "test-suite": "gulp mocha-suite --test", "test-coverage": "nyc -x gulpfile.js -x '**/*test*' npm run mocha", "mocha": "gulp test", - "tsc": "tsc", - "prebuild": "concurrently --kill-others-on-fail --names \"lib,attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem,css\" \"tsc\" \"tsc -p ./src/addons/attach\" \"tsc -p ./src/addons/fit\" \"tsc -p ./src/addons/fullscreen\" \"tsc -p ./src/addons/search\" \"tsc -p ./src/addons/terminado\" \"tsc -p ./src/addons/webLinks\" \"tsc -p ./src/addons/winptyCompat\" \"tsc -p ./src/addons/zmodem\" \"gulp css\"", + "prebuild": "tsc -b ./src/tsconfig.all.json", "build": "gulp build", "prepublish": "npm run build", "coveralls": "nyc report --reporter=text-lcov | coveralls", - "watch": "concurrently --kill-others-on-fail --names \"lib,css\" \"tsc -w\" \"gulp watch-css\"", - "watch-addons": "concurrently --kill-others-on-fail --names \"attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem\" \"tsc -w -p ./src/addons/attach\" \"tsc -w -p ./src/addons/fit\" \"tsc -w -p ./src/addons/fullscreen\" \"tsc -w -p ./src/addons/search\" \"tsc -w -p ./src/addons/terminado\" \"tsc -w -p ./src/addons/webLinks\" \"tsc -w -p ./src/addons/winptyCompat\" \"tsc -w -p ./src/addons/zmodem\"", - "layering": "concurrently --kill-others-on-fail --names \"common,core\" \"tsc -p ./src/common\" \"tsc -p ./src/core\"" + "watch": "tsc -b -w ./src/tsconfig.all.json --preserveWatchOutput" } } diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 53de19b7..59475adb 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -5,10 +5,10 @@ import { assert, expect } from 'chai'; import { ITerminal } from './Types'; -import { Buffer, DEFAULT_ATTR, CHAR_DATA_CHAR_INDEX } from './Buffer'; +import { Buffer, DEFAULT_ATTR } from './Buffer'; import { CircularList } from './common/CircularList'; import { MockTerminal, TestTerminal } from './ui/TestUtils.test'; -import { BufferLine } from './BufferLine'; +import { BufferLine, CellData } from './BufferLine'; const INIT_COLS = 80; const INIT_ROWS = 24; @@ -37,13 +37,13 @@ describe('Buffer', () => { describe('fillViewportRows', () => { it('should fill the buffer with blank lines based on the size of the viewport', () => { - const blankLineChar = buffer.getBlankLine(DEFAULT_ATTR).get(0); + const blankLineChar = buffer.getBlankLine(DEFAULT_ATTR).loadCell(0, new CellData()).getAsCharData; buffer.fillViewportRows(); assert.equal(buffer.lines.length, INIT_ROWS); for (let y = 0; y < INIT_ROWS; y++) { assert.equal(buffer.lines.get(y).length, INIT_COLS); for (let x = 0; x < INIT_COLS; x++) { - assert.deepEqual(buffer.lines.get(y).get(x), blankLineChar); + assert.deepEqual(buffer.lines.get(y).loadCell(x, new CellData()).getAsCharData, blankLineChar); } } }); @@ -155,15 +155,15 @@ describe('Buffer', () => { assert.equal(buffer.lines.maxLength, INIT_ROWS); buffer.y = INIT_ROWS - 1; buffer.fillViewportRows(); - let chData = buffer.lines.get(5).get(0); + let chData = buffer.lines.get(5).loadCell(0, new CellData()).getAsCharData(); chData[1] = 'a'; - buffer.lines.get(5).set(0, chData); - chData = buffer.lines.get(INIT_ROWS - 1).get(0); + buffer.lines.get(5).setCell(0, CellData.fromCharData(chData)); + chData = buffer.lines.get(INIT_ROWS - 1).loadCell(0, new CellData()).getAsCharData(); chData[1] = 'b'; - buffer.lines.get(INIT_ROWS - 1).set(0, chData); + buffer.lines.get(INIT_ROWS - 1).setCell(0, CellData.fromCharData(chData)); buffer.resize(INIT_COLS, INIT_ROWS - 5); - assert.equal(buffer.lines.get(0).get(0)[1], 'a'); - assert.equal(buffer.lines.get(INIT_ROWS - 1 - 5).get(0)[1], 'b'); + assert.equal(buffer.lines.get(0).loadCell(0, new CellData()).getAsCharData()[1], 'a'); + assert.equal(buffer.lines.get(INIT_ROWS - 1 - 5).loadCell(0, new CellData()).getAsCharData()[1], 'b'); }); }); }); @@ -1045,10 +1045,10 @@ describe('Buffer', () => { describe ('translateBufferLineToString', () => { it('should handle selecting a section of ascii text', () => { const line = new BufferLine(4); - line.set(0, [ null, 'a', 1, 'a'.charCodeAt(0)]); - line.set(1, [ null, 'b', 1, 'b'.charCodeAt(0)]); - line.set(2, [ null, 'c', 1, 'c'.charCodeAt(0)]); - line.set(3, [ null, 'd', 1, 'd'.charCodeAt(0)]); + line.setCell(0, CellData.fromCharData([ null, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(1, CellData.fromCharData([ null, 'b', 1, 'b'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([ null, 'c', 1, 'c'.charCodeAt(0)])); + line.setCell(3, CellData.fromCharData([ null, 'd', 1, 'd'.charCodeAt(0)])); buffer.lines.set(0, line); const str = buffer.translateBufferLineToString(0, true, 0, 2); @@ -1057,9 +1057,9 @@ describe('Buffer', () => { it('should handle a cut-off double width character by including it', () => { const line = new BufferLine(3); - line.set(0, [ null, '語', 2, 35486 ]); - line.set(1, [ null, '', 0, null]); - line.set(2, [ null, 'a', 1, 'a'.charCodeAt(0)]); + line.setCell(0, CellData.fromCharData([ null, '語', 2, 35486 ])); + line.setCell(1, CellData.fromCharData([ null, '', 0, null])); + line.setCell(2, CellData.fromCharData([ null, 'a', 1, 'a'.charCodeAt(0)])); buffer.lines.set(0, line); const str1 = buffer.translateBufferLineToString(0, true, 0, 1); @@ -1068,9 +1068,9 @@ describe('Buffer', () => { it('should handle a zero width character in the middle of the string by not including it', () => { const line = new BufferLine(3); - line.set(0, [ null, '語', 2, '語'.charCodeAt(0) ]); - line.set(1, [ null, '', 0, null]); - line.set(2, [ null, 'a', 1, 'a'.charCodeAt(0)]); + line.setCell(0, CellData.fromCharData([ null, '語', 2, '語'.charCodeAt(0) ])); + line.setCell(1, CellData.fromCharData([ null, '', 0, null])); + line.setCell(2, CellData.fromCharData([ null, 'a', 1, 'a'.charCodeAt(0)])); buffer.lines.set(0, line); const str0 = buffer.translateBufferLineToString(0, true, 0, 1); @@ -1085,8 +1085,8 @@ describe('Buffer', () => { it('should handle single width emojis', () => { const line = new BufferLine(2); - line.set(0, [ null, '😁', 1, '😁'.charCodeAt(0) ]); - line.set(1, [ null, 'a', 1, 'a'.charCodeAt(0)]); + line.setCell(0, CellData.fromCharData([ null, '😁', 1, '😁'.charCodeAt(0) ])); + line.setCell(1, CellData.fromCharData([ null, 'a', 1, 'a'.charCodeAt(0)])); buffer.lines.set(0, line); const str1 = buffer.translateBufferLineToString(0, true, 0, 1); @@ -1098,8 +1098,8 @@ describe('Buffer', () => { it('should handle double width emojis', () => { const line = new BufferLine(2); - line.set(0, [ null, '😁', 2, '😁'.charCodeAt(0) ]); - line.set(1, [ null, '', 0, null]); + line.setCell(0, CellData.fromCharData([ null, '😁', 2, '😁'.charCodeAt(0) ])); + line.setCell(1, CellData.fromCharData([ null, '', 0, null])); buffer.lines.set(0, line); const str1 = buffer.translateBufferLineToString(0, true, 0, 1); @@ -1109,9 +1109,9 @@ describe('Buffer', () => { assert.equal(str2, '😁'); const line2 = new BufferLine(3); - line2.set(0, [ null, '😁', 2, '😁'.charCodeAt(0) ]); - line2.set(1, [ null, '', 0, null]); - line2.set(2, [ null, 'a', 1, 'a'.charCodeAt(0)]); + line2.setCell(0, CellData.fromCharData([ null, '😁', 2, '😁'.charCodeAt(0) ])); + line2.setCell(1, CellData.fromCharData([ null, '', 0, null])); + line2.setCell(2, CellData.fromCharData([ null, 'a', 1, 'a'.charCodeAt(0)])); buffer.lines.set(0, line2); const str3 = buffer.translateBufferLineToString(0, true, 0, 3); @@ -1264,7 +1264,7 @@ describe('Buffer', () => { assert.equal(input, s); const stringIndex = s.match(/😃/).index; const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, stringIndex); - assert(terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX], '😃'); + assert(terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars(), '😃'); }); it('multiline fullwidth chars with offset 1 (currently tests for broken behavior)', () => { @@ -1291,7 +1291,7 @@ describe('Buffer', () => { assert.equal(input, s); for (let i = 0; i < input.length; ++i) { const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); - assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX]); + assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars()); } }); @@ -1309,7 +1309,7 @@ describe('Buffer', () => { : (i % 3 === 1) ? input.substr(i, 2) : input.substr(i - 1, 2), - terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX]); + terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars()); } }); diff --git a/src/Buffer.ts b/src/Buffer.ts index 5eea5d9a..9cc1adba 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -3,13 +3,14 @@ * @license MIT */ -import { IMarker } from 'xterm'; -import { BufferLine } from './BufferLine'; -import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from './BufferReflow'; -import { CircularList, IDeleteEvent, IInsertEvent } from './common/CircularList'; +import { CircularList, IInsertEvent, IDeleteEvent } from './common/CircularList'; +import { ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult, ICellData } from './Types'; import { EventEmitter } from './common/EventEmitter'; +import { IMarker } from 'xterm'; +import { BufferLine, CellData } from './BufferLine'; +import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from './BufferReflow'; import { DEFAULT_COLOR } from './renderer/atlas/Types'; -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; @@ -18,16 +19,24 @@ export const CHAR_DATA_WIDTH_INDEX = 2; export const CHAR_DATA_CODE_INDEX = 3; export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1 +/** + * Null cell - a real empty cell (containing nothing). + * Note that code should always be 0 for a null cell as + * several test condition of the buffer line rely on this. + */ export const NULL_CELL_CHAR = ''; export const NULL_CELL_WIDTH = 1; export const NULL_CELL_CODE = 0; +/** + * Whitespace cell. + * This is meant as a replacement for empty cells when needed + * during rendering lines to preserve correct aligment. + */ export const WHITESPACE_CELL_CHAR = ' '; export const WHITESPACE_CELL_WIDTH = 1; export const WHITESPACE_CELL_CODE = 32; -export const FILL_CHAR_DATA: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - /** * This class represents a terminal buffer (an internal state of the terminal), where the * following information is stored (in high-level): @@ -48,6 +57,8 @@ export class Buffer implements IBuffer { public savedX: number; public savedCurAttr: number; public markers: Marker[] = []; + private _nullCell: ICellData = CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + private _whitespaceCell: ICellData = CellData.fromCharData([0, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_WIDTH, WHITESPACE_CELL_CODE]); private _cols: number; private _rows: number; @@ -66,9 +77,20 @@ export class Buffer implements IBuffer { this.clear(); } + public getNullCell(fg: number = 0, bg: number = 0): ICellData { + this._nullCell.fg = fg; + this._nullCell.bg = bg; + return this._nullCell; + } + + public getWhitespaceCell(fg: number = 0, bg: number = 0): ICellData { + this._whitespaceCell.fg = fg; + this._whitespaceCell.bg = bg; + return this._whitespaceCell; + } + public getBlankLine(attr: number, isWrapped?: boolean): IBufferLine { - const fillCharData: CharData = [attr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - return new BufferLine(this._cols, fillCharData, isWrapped); + return new BufferLine(this._terminal.cols, this.getNullCell(attr), isWrapped); } public get hasScrollback(): boolean { @@ -131,6 +153,9 @@ export class Buffer implements IBuffer { * @param newRows The new number of rows. */ public resize(newCols: number, newRows: number): void { + // store reference to null cell with default attrs + const nullCell = this.getNullCell(DEFAULT_ATTR); + // Increase max length if needed before adjustments to allow space to fill // as required. const newMaxLength = this._getCorrectBufferLength(newRows); @@ -144,7 +169,7 @@ export class Buffer implements IBuffer { // Deal with columns increasing (reducing needs to happen after reflow) if (this._cols < newCols) { for (let i = 0; i < this.lines.length; i++) { - this.lines.get(i).resize(newCols, FILL_CHAR_DATA); + this.lines.get(i).resize(newCols, nullCell); } } @@ -165,7 +190,7 @@ export class Buffer implements IBuffer { } else { // Add a blank line if there is no buffer left at the top to scroll to, or if there // are blank lines after the cursor - this.lines.push(new BufferLine(newCols, FILL_CHAR_DATA)); + this.lines.push(new BufferLine(newCols, nullCell)); } } } @@ -217,7 +242,7 @@ export class Buffer implements IBuffer { // Trim the end of the line off if cols shrunk if (this._cols > newCols) { for (let i = 0; i < this.lines.length; i++) { - this.lines.get(i).resize(newCols, FILL_CHAR_DATA); + this.lines.get(i).resize(newCols, nullCell); } } } @@ -253,6 +278,7 @@ export class Buffer implements IBuffer { } private _reflowLargerAdjustViewport(newCols: number, newRows: number, countRemoved: number): void { + const nullCell = this.getNullCell(DEFAULT_ATTR); // Adjust viewport based on number of items removed let viewportAdjustments = countRemoved; while (viewportAdjustments-- > 0) { @@ -262,7 +288,7 @@ export class Buffer implements IBuffer { } if (this.lines.length < newRows) { // Add an extra row at the bottom of the viewport - this.lines.push(new BufferLine(newCols, FILL_CHAR_DATA)); + this.lines.push(new BufferLine(newCols, nullCell)); } } else { if (this.ydisp === this.ybase) { @@ -274,6 +300,7 @@ export class Buffer implements IBuffer { } private _reflowSmaller(newCols: number, newRows: number): void { + const nullCell = this.getNullCell(DEFAULT_ATTR); // 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 = []; @@ -356,7 +383,7 @@ export class Buffer implements IBuffer { // Null out the end of the line ends if a wide character wrapped to the following line for (let i = 0; i < wrappedLines.length; i++) { if (destLineLengths[i] < newCols) { - wrappedLines[i].set(destLineLengths[i], FILL_CHAR_DATA); + wrappedLines[i].setCell(destLineLengths[i], nullCell); } } diff --git a/src/BufferLine.test.ts b/src/BufferLine.test.ts index 0ef29505..29a783ae 100644 --- a/src/BufferLine.test.ts +++ b/src/BufferLine.test.ts @@ -3,7 +3,7 @@ * @license MIT */ import * as chai from 'chai'; -import { BufferLine } from './BufferLine'; +import { BufferLine, CellData, ContentMasks } from './BufferLine'; import { CharData, IBufferLine } from './Types'; import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR } from './Buffer'; @@ -16,12 +16,38 @@ class TestBufferLine extends BufferLine { public toArray(): CharData[] { const result = []; for (let i = 0; i < this.length; ++i) { - result.push(this.get(i)); + result.push(this.loadCell(i, new CellData()).getAsCharData()); } return result; } } +describe('CellData', () => { + it('CharData <--> CellData equality', () => { + const cell = new CellData(); + // ASCII + cell.setFromCharData([123, 'a', 1, 'a'.charCodeAt(0)]); + chai.assert.deepEqual(cell.getAsCharData(), [123, 'a', 1, 'a'.charCodeAt(0)]); + chai.assert.equal(cell.isCombined(), 0); + // combining + cell.setFromCharData([123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); + chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); + chai.assert.equal(cell.isCombined(), ContentMasks.IS_COMBINED); + // surrogate + cell.setFromCharData([123, '𝄞', 1, 0x1D11E]); + chai.assert.deepEqual(cell.getAsCharData(), [123, '𝄞', 1, 0x1D11E]); + chai.assert.equal(cell.isCombined(), 0); + // surrogate + combining + cell.setFromCharData([123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]); + chai.assert.deepEqual(cell.getAsCharData(), [123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]); + chai.assert.equal(cell.isCombined(), ContentMasks.IS_COMBINED); + // wide char + cell.setFromCharData([123, '1', 2, '1'.charCodeAt(0)]); + chai.assert.deepEqual(cell.getAsCharData(), [123, '1', 2, '1'.charCodeAt(0)]); + chai.assert.equal(cell.isCombined(), 0); + }); +}); + describe('BufferLine', function(): void { it('ctor', function(): void { let line: IBufferLine = new TestBufferLine(0); @@ -29,23 +55,23 @@ describe('BufferLine', function(): void { chai.expect(line.isWrapped).equals(false); line = new TestBufferLine(10); chai.expect(line.length).equals(10); - chai.expect(line.get(0)).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + chai.expect(line.loadCell(0, new CellData()).getAsCharData()).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); chai.expect(line.isWrapped).equals(false); line = new TestBufferLine(10, null, true); chai.expect(line.length).equals(10); - chai.expect(line.get(0)).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + chai.expect(line.loadCell(0, new CellData()).getAsCharData()).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); chai.expect(line.isWrapped).equals(true); - line = new TestBufferLine(10, [123, 'a', 456, 'a'.charCodeAt(0)], true); + line = new TestBufferLine(10, CellData.fromCharData([123, 'a', 456, 'a'.charCodeAt(0)]), true); chai.expect(line.length).equals(10); - chai.expect(line.get(0)).eql([123, 'a', 456, 'a'.charCodeAt(0)]); + chai.expect(line.loadCell(0, new CellData()).getAsCharData()).eql([123, 'a', 456, 'a'.charCodeAt(0)]); chai.expect(line.isWrapped).equals(true); }); it('insertCells', function(): void { const line = new TestBufferLine(3); - line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]); - line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]); - line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]); - line.insertCells(1, 3, [4, 'd', 0, 'd'.charCodeAt(0)]); + line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); + line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)])); + line.insertCells(1, 3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)])); chai.expect(line.toArray()).eql([ [1, 'a', 0, 'a'.charCodeAt(0)], [4, 'd', 0, 'd'.charCodeAt(0)], @@ -54,12 +80,12 @@ describe('BufferLine', function(): void { }); it('deleteCells', function(): void { const line = new TestBufferLine(5); - line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]); - line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]); - line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]); - line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]); - line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]); - line.deleteCells(1, 2, [6, 'f', 0, 'f'.charCodeAt(0)]); + line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); + line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)])); + line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)])); + line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)])); + line.deleteCells(1, 2, CellData.fromCharData([6, 'f', 0, 'f'.charCodeAt(0)])); chai.expect(line.toArray()).eql([ [1, 'a', 0, 'a'.charCodeAt(0)], [4, 'd', 0, 'd'.charCodeAt(0)], @@ -70,12 +96,12 @@ describe('BufferLine', function(): void { }); it('replaceCells', function(): void { const line = new TestBufferLine(5); - line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]); - line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]); - line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]); - line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]); - line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]); - line.replaceCells(2, 4, [6, 'f', 0, 'f'.charCodeAt(0)]); + line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); + line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)])); + line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)])); + line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)])); + line.replaceCells(2, 4, CellData.fromCharData([6, 'f', 0, 'f'.charCodeAt(0)])); chai.expect(line.toArray()).eql([ [1, 'a', 0, 'a'.charCodeAt(0)], [2, 'b', 0, 'b'.charCodeAt(0)], @@ -86,12 +112,12 @@ describe('BufferLine', function(): void { }); it('fill', function(): void { const line = new TestBufferLine(5); - line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]); - line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]); - line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]); - line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]); - line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]); - line.fill([123, 'z', 0, 'z'.charCodeAt(0)]); + line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); + line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)])); + line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)])); + line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)])); + line.fill(CellData.fromCharData([123, 'z', 0, 'z'.charCodeAt(0)])); chai.expect(line.toArray()).eql([ [123, 'z', 0, 'z'.charCodeAt(0)], [123, 'z', 0, 'z'.charCodeAt(0)], @@ -102,11 +128,11 @@ describe('BufferLine', function(): void { }); it('clone', function(): void { const line = new TestBufferLine(5, null, true); - line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]); - line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]); - line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]); - line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]); - line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]); + line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); + line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)])); + line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)])); + line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)])); const line2 = line.clone(); chai.expect(TestBufferLine.prototype.toArray.apply(line2)).eql(line.toArray()); chai.expect(line2.length).equals(line.length); @@ -114,12 +140,12 @@ describe('BufferLine', function(): void { }); it('copyFrom', function(): void { const line = new TestBufferLine(5); - line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]); - line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]); - line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]); - line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]); - line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]); - const line2 = new TestBufferLine(5, [1, 'a', 0, 'a'.charCodeAt(0)], true); + line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); + line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)])); + line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)])); + line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)])); + const line2 = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), true); line2.copyFrom(line); chai.expect(line2.toArray()).eql(line.toArray()); chai.expect(line2.length).equals(line.length); @@ -129,9 +155,9 @@ describe('BufferLine', function(): void { // CHAR_DATA_CODE_INDEX resembles current behavior in InputHandler.print // --> set code to the last charCodeAt value of the string // Note: needs to be fixed once the string pointer is in place - const line = new TestBufferLine(2, [1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]); + const line = new TestBufferLine(2, CellData.fromCharData([1, 'e\u0301', 0, '\u0301'.charCodeAt(0)])); chai.expect(line.toArray()).eql([[1, 'e\u0301', 0, '\u0301'.charCodeAt(0)], [1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]]); - const line2 = new TestBufferLine(5, [1, 'a', 0, '\u0301'.charCodeAt(0)], true); + const line2 = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, '\u0301'.charCodeAt(0)]), true); line2.copyFrom(line); chai.expect(line2.toArray()).eql(line.toArray()); const line3 = line.clone(); @@ -139,81 +165,81 @@ describe('BufferLine', function(): void { }); describe('resize', function(): void { it('enlarge(false)', function(): void { - const line = new TestBufferLine(5, [1, 'a', 0, 'a'.charCodeAt(0)], false); - line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)]); + const line = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); + line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)])); }); it('enlarge(true)', function(): void { - const line = new TestBufferLine(5, [1, 'a', 0, 'a'.charCodeAt(0)], false); - line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)]); + const line = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); + line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)])); }); it('shrink(true) - should apply new size', function(): void { - const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false); - line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)]); + const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); + line.resize(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); chai.expect(line.toArray()).eql(Array(5).fill([1, 'a', 0, 'a'.charCodeAt(0)])); }); it('shrink to 0 length', function(): void { - const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false); - line.resize(0, [1, 'a', 0, 'a'.charCodeAt(0)]); + const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); + line.resize(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); chai.expect(line.toArray()).eql(Array(0).fill([1, 'a', 0, 'a'.charCodeAt(0)])); }); it('should remove combining data on replaced cells after shrinking then enlarging', () => { - const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false); + const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.set(2, [ null, '😁', 1, '😁'.charCodeAt(0) ]); line.set(9, [ null, '😁', 1, '😁'.charCodeAt(0) ]); chai.expect(line.translateToString()).eql('aa😁aaaaaa😁'); chai.expect(Object.keys(line.combined).length).eql(2); - line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)]); + line.resize(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); chai.expect(line.translateToString()).eql('aa😁aa'); - line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)]); + line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); chai.expect(line.translateToString()).eql('aa😁aaaaaaa'); chai.expect(Object.keys(line.combined).length).eql(1); }); }); describe('getTrimLength', function(): void { it('empty line', function(): void { - const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false); + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); chai.expect(line.getTrimmedLength()).equal(0); }); it('ASCII', function(): void { - const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false); - line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); - line.set(2, [1, 'a', 1, 'a'.charCodeAt(0)]); + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); + line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); chai.expect(line.getTrimmedLength()).equal(3); }); it('surrogate', function(): void { - const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false); - line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); - line.set(2, [1, '𝄞', 1, '𝄞'.charCodeAt(0)]); + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); + line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)])); chai.expect(line.getTrimmedLength()).equal(3); }); it('combining', function(): void { - const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false); - line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); - line.set(2, [1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); + line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)])); chai.expect(line.getTrimmedLength()).equal(3); }); it('fullwidth', function(): void { - const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false); - line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); - line.set(2, [1, '1', 2, '1'.charCodeAt(0)]); - line.set(3, [0, '', 0, undefined]); + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); + line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)])); + line.setCell(3, CellData.fromCharData([0, '', 0, undefined])); chai.expect(line.getTrimmedLength()).equal(4); // also counts null cell after fullwidth }); }); describe('translateToString with and w\'o trimming', function(): void { it('empty line', function(): void { - const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false); + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); chai.expect(line.translateToString(false)).equal(' '); chai.expect(line.translateToString(true)).equal(''); }); it('ASCII', function(): void { - const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false); - line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); - line.set(2, [1, 'a', 1, 'a'.charCodeAt(0)]); - line.set(4, [1, 'a', 1, 'a'.charCodeAt(0)]); - line.set(5, [1, 'a', 1, 'a'.charCodeAt(0)]); + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); + line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(4, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(5, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); chai.expect(line.translateToString(false)).equal('a a aa '); chai.expect(line.translateToString(true)).equal('a a aa'); chai.expect(line.translateToString(false, 0, 5)).equal('a a a'); @@ -225,11 +251,11 @@ describe('BufferLine', function(): void { }); it('surrogate', function(): void { - const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false); - line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); - line.set(2, [1, '𝄞', 1, '𝄞'.charCodeAt(0)]); - line.set(4, [1, '𝄞', 1, '𝄞'.charCodeAt(0)]); - line.set(5, [1, '𝄞', 1, '𝄞'.charCodeAt(0)]); + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); + line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)])); + line.setCell(4, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)])); + line.setCell(5, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)])); chai.expect(line.translateToString(false)).equal('a 𝄞 𝄞𝄞 '); chai.expect(line.translateToString(true)).equal('a 𝄞 𝄞𝄞'); chai.expect(line.translateToString(false, 0, 5)).equal('a 𝄞 𝄞'); @@ -240,11 +266,11 @@ describe('BufferLine', function(): void { chai.expect(line.translateToString(true, 0, 3)).equal('a 𝄞'); }); it('combining', function(): void { - const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false); - line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); - line.set(2, [1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); - line.set(4, [1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); - line.set(5, [1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]); + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); + line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)])); + line.setCell(4, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)])); + line.setCell(5, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)])); chai.expect(line.translateToString(false)).equal('a e\u0301 e\u0301e\u0301 '); chai.expect(line.translateToString(true)).equal('a e\u0301 e\u0301e\u0301'); chai.expect(line.translateToString(false, 0, 5)).equal('a e\u0301 e\u0301'); @@ -255,14 +281,14 @@ describe('BufferLine', function(): void { chai.expect(line.translateToString(true, 0, 3)).equal('a e\u0301'); }); it('fullwidth', function(): void { - const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false); - line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); - line.set(2, [1, '1', 2, '1'.charCodeAt(0)]); - line.set(3, [0, '', 0, undefined]); - line.set(5, [1, '1', 2, '1'.charCodeAt(0)]); - line.set(6, [0, '', 0, undefined]); - line.set(7, [1, '1', 2, '1'.charCodeAt(0)]); - line.set(8, [0, '', 0, undefined]); + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); + line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)])); + line.setCell(3, CellData.fromCharData([0, '', 0, undefined])); + line.setCell(5, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)])); + line.setCell(6, CellData.fromCharData([0, '', 0, undefined])); + line.setCell(7, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)])); + line.setCell(8, CellData.fromCharData([0, '', 0, undefined])); chai.expect(line.translateToString(false)).equal('a 1 11 '); chai.expect(line.translateToString(true)).equal('a 1 11'); chai.expect(line.translateToString(false, 0, 7)).equal('a 1 1'); @@ -279,12 +305,12 @@ describe('BufferLine', function(): void { chai.expect(line.translateToString(true, 0, 2)).equal('a '); }); it('space at end', function(): void { - const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE], false); - line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); - line.set(2, [1, 'a', 1, 'a'.charCodeAt(0)]); - line.set(4, [1, 'a', 1, 'a'.charCodeAt(0)]); - line.set(5, [1, 'a', 1, 'a'.charCodeAt(0)]); - line.set(6, [1, ' ', 1, ' '.charCodeAt(0)]); + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); + line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(4, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(5, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); + line.setCell(6, CellData.fromCharData([1, ' ', 1, ' '.charCodeAt(0)])); chai.expect(line.translateToString(false)).equal('a a aa '); chai.expect(line.translateToString(true)).equal('a a aa '); }); @@ -292,14 +318,52 @@ describe('BufferLine', function(): void { // sanity check - broken line with invalid out of bound null width cells // this can atm happen with deleting/inserting chars in inputhandler by "breaking" // fullwidth pairs --> needs to be fixed after settling BufferLine impl - const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE], false); + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); chai.expect(line.translateToString(false)).equal(' '); chai.expect(line.translateToString(true)).equal(''); }); it('should work with endCol=0', () => { - const line = new TestBufferLine(10, [DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE], false); - line.set(0, [1, 'a', 1, 'a'.charCodeAt(0)]); + const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); chai.expect(line.translateToString(true, 0, 0)).equal(''); }); }); + describe('addCharToCell', () => { + it('should set width to 1 for empty cell', () => { + const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); + line.addCodepointToCell(0, '\u0301'.charCodeAt(0)); + const cell = line.loadCell(0, new CellData()); + // chars contains single combining char + // width is set to 1 + chai.assert.deepEqual(cell.getAsCharData(), [DEFAULT_ATTR, '\u0301', 1, 0x0301]); + // do not account a single combining char as combined + chai.assert.equal(cell.isCombined(), 0); + }); + it('should add char to combining string in cell', () => { + const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); + const cell = line .loadCell(0, new CellData()); + cell.setFromCharData([123, 'e\u0301', 1, 'e\u0301'.charCodeAt(1)]); + line.setCell(0, cell); + line.addCodepointToCell(0, '\u0301'.charCodeAt(0)); + line.loadCell(0, cell); + // chars contains 3 chars + // width is set to 1 + chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301\u0301', 1, 0x0301]); + // do not account a single combining char as combined + chai.assert.equal(cell.isCombined(), ContentMasks.IS_COMBINED); + }); + it('should create combining string on taken cell', () => { + const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); + const cell = line .loadCell(0, new CellData()); + cell.setFromCharData([123, 'e', 1, 'e'.charCodeAt(1)]); + line.setCell(0, cell); + line.addCodepointToCell(0, '\u0301'.charCodeAt(0)); + line.loadCell(0, cell); + // chars contains 2 chars + // width is set to 1 + chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301', 1, 0x0301]); + // do not account a single combining char as combined + chai.assert.equal(cell.isCombined(), ContentMasks.IS_COMBINED); + }); + }); }); diff --git a/src/BufferLine.ts b/src/BufferLine.ts index 6bc30586..c4aa55be 100644 --- a/src/BufferLine.ts +++ b/src/BufferLine.ts @@ -2,112 +2,405 @@ * Copyright (c) 2018 The xterm.js authors. All rights reserved. * @license MIT */ -import { CharData, IBufferLine } from './Types'; -import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, WHITESPACE_CELL_CHAR } from './Buffer'; +import { CharData, IBufferLine, ICellData } from './Types'; +import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, WHITESPACE_CELL_CHAR, CHAR_DATA_ATTR_INDEX } from './Buffer'; +import { stringFromCodePoint } from './core/input/TextDecoder'; + + +/** + * buffer memory layout: + * + * | uint32_t | uint32_t | uint32_t | + * | `content` | `FG` | `BG` | + * | wcwidth(2) comb(1) codepoint(21) | flags(8) R(8) G(8) B(8) | flags(8) R(8) G(8) B(8) | + */ /** typed array slots taken by one cell */ const CELL_SIZE = 3; -/** cell member indices */ +/** + * Cell member indices. + * + * Direct access: + * `content = data[column * CELL_SIZE + Cell.CONTENT];` + * `fg = data[column * CELL_SIZE + Cell.FG];` + * `bg = data[column * CELL_SIZE + Cell.BG];` + */ const enum Cell { - FLAGS = 0, - STRING = 1, - WIDTH = 2 + CONTENT = 0, + FG = 1, // currently simply holds all known attrs + BG = 2 // currently unused +} + +/** + * Bitmasks for accessing data in `content`. + */ +export const enum ContentMasks { + /** + * bit 1..21 codepoint, max allowed in UTF32 is 0x10FFFF (21 bits taken) + * read: `codepoint = content & Content.codepointMask;` + * write: `content |= codepoint & Content.codepointMask;` + * shortcut if precondition `codepoint <= 0x10FFFF` is met: + * `content |= codepoint;` + */ + CODEPOINT = 0x1FFFFF, + + /** + * bit 22 flag indication whether a cell contains combined content + * read: `isCombined = content & Content.isCombined;` + * set: `content |= Content.isCombined;` + * clear: `content &= ~Content.isCombined;` + */ + IS_COMBINED = 0x200000, // 1 << 21 + + /** + * bit 1..22 mask to check whether a cell contains any string data + * we need to check for codepoint and isCombined bits to see + * whether a cell contains anything + * read: `isEmtpy = !(content & Content.hasContent)` + */ + HAS_CONTENT = 0x3FFFFF, + + /** + * bit 23..24 wcwidth value of cell, takes 2 bits (ranges from 0..2) + * read: `width = (content & Content.widthMask) >> Content.widthShift;` + * `hasWidth = content & Content.widthMask;` + * as long as wcwidth is highest value in `content`: + * `width = content >> Content.widthShift;` + * write: `content |= (width << Content.widthShift) & Content.widthMask;` + * shortcut if precondition `0 <= width <= 3` is met: + * `content |= width << Content.widthShift;` + */ + WIDTH = 0xC00000 // 3 << 22 +} + +const WIDTH_MASK_SHIFT = 22; + +/** + * CellData - represents a single Cell in the terminal buffer. + */ +export class CellData implements ICellData { + + /** Helper to create CellData from CharData. */ + public static fromCharData(value: CharData): CellData { + const obj = new CellData(); + obj.setFromCharData(value); + return obj; + } + + /** Primitives from terminal buffer. */ + public content: number = 0; + public fg: number = 0; + public bg: number = 0; + public combinedData: string = ''; + + /** Whether cell contains a combined string. */ + public isCombined(): number { + return this.content & ContentMasks.IS_COMBINED; + } + + /** Width of the cell. */ + public getWidth(): number { + return this.content >> WIDTH_MASK_SHIFT; + } + + /** JS string of the content. */ + public getChars(): string { + if (this.content & ContentMasks.IS_COMBINED) { + return this.combinedData; + } + if (this.content & ContentMasks.CODEPOINT) { + return stringFromCodePoint(this.content & ContentMasks.CODEPOINT); + } + return ''; + } + + /** + * Codepoint of cell + * Note this returns the UTF32 codepoint of single chars, + * if content is a combined string it returns the codepoint + * of the last char in string to be in line with code in CharData. + * */ + public getCode(): number { + return (this.isCombined()) + ? this.combinedData.charCodeAt(this.combinedData.length - 1) + : this.content & ContentMasks.CODEPOINT; + } + + /** Set data from CharData */ + public setFromCharData(value: CharData): void { + this.fg = value[CHAR_DATA_ATTR_INDEX]; + this.bg = 0; + let combined = false; + + // surrogates and combined strings need special treatment + if (value[CHAR_DATA_CHAR_INDEX].length > 2) { + combined = true; + } else if (value[CHAR_DATA_CHAR_INDEX].length === 2) { + const code = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0); + // if the 2-char string is a surrogate create single codepoint + // everything else is combined + if (0xD800 <= code && code <= 0xDBFF) { + const second = value[CHAR_DATA_CHAR_INDEX].charCodeAt(1); + if (0xDC00 <= second && second <= 0xDFFF) { + this.content = ((code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000) | (value[CHAR_DATA_WIDTH_INDEX] << WIDTH_MASK_SHIFT); + } else { + combined = true; + } + } else { + combined = true; + } + } else { + this.content = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << WIDTH_MASK_SHIFT); + } + if (combined) { + this.combinedData = value[CHAR_DATA_CHAR_INDEX]; + this.content = ContentMasks.IS_COMBINED | (value[CHAR_DATA_WIDTH_INDEX] << WIDTH_MASK_SHIFT); + } + } + + /** Get data as CharData. */ + public getAsCharData(): CharData { + return [this.fg, this.getChars(), this.getWidth(), this.getCode()]; + } } -/** single vs. combined char distinction */ -const IS_COMBINED_BIT_MASK = 0x80000000; /** * Typed array based bufferline implementation. + * + * There are 2 ways to insert data into the cell buffer: + * - `setCellFromCodepoint` + `addCodepointToCell` + * Use these for data that is already UTF32. + * Used during normal input in `InputHandler` for faster buffer access. + * - `setCell` + * This method takes a CellData object and stores the data in the buffer. + * Use `CellData.fromCharData` to create the CellData object (e.g. from JS string). + * + * To retrieve data from the buffer use either one of the primitive methods + * (if only one particular value is needed) or `loadCell`. For `loadCell` in a loop + * memory allocs / GC pressure can be greatly reduced by reusing the CellData object. */ export class BufferLine implements IBufferLine { protected _data: Uint32Array | null = null; protected _combined: {[index: number]: string} = {}; public length: number; - constructor(cols: number, fillCharData?: CharData, public isWrapped: boolean = false) { - if (!fillCharData) { - fillCharData = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; - } + constructor(cols: number, fillCellData?: ICellData, public isWrapped: boolean = false) { if (cols) { this._data = new Uint32Array(cols * CELL_SIZE); + const cell = fillCellData || CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); for (let i = 0; i < cols; ++i) { - this.set(i, fillCharData); + this.setCell(i, cell); } } this.length = cols; } + /** + * Get cell data CharData. + * @deprecated + */ public get(index: number): CharData { - const stringData = this._data[index * CELL_SIZE + Cell.STRING]; + const content = this._data[index * CELL_SIZE + Cell.CONTENT]; + const cp = content & ContentMasks.CODEPOINT; return [ - this._data[index * CELL_SIZE + Cell.FLAGS], - (stringData & IS_COMBINED_BIT_MASK) + this._data[index * CELL_SIZE + Cell.FG], + (content & ContentMasks.IS_COMBINED) ? this._combined[index] - : (stringData) ? String.fromCharCode(stringData) : '', - this._data[index * CELL_SIZE + Cell.WIDTH], - (stringData & IS_COMBINED_BIT_MASK) + : (cp) ? stringFromCodePoint(cp) : '', + content >> WIDTH_MASK_SHIFT, + (content & ContentMasks.IS_COMBINED) ? this._combined[index].charCodeAt(this._combined[index].length - 1) - : stringData + : cp ]; } - public getWidth(index: number): number { - return this._data[index * CELL_SIZE + Cell.WIDTH]; - } - + /** + * Set cell data from CharData. + * @deprecated + */ public set(index: number, value: CharData): void { - this._data[index * CELL_SIZE + Cell.FLAGS] = value[0]; - if (value[1].length > 1) { + this._data[index * CELL_SIZE + Cell.FG] = value[CHAR_DATA_ATTR_INDEX]; + if (value[CHAR_DATA_CHAR_INDEX].length > 1) { this._combined[index] = value[1]; - this._data[index * CELL_SIZE + Cell.STRING] = index | IS_COMBINED_BIT_MASK; + this._data[index * CELL_SIZE + Cell.CONTENT] = index | ContentMasks.IS_COMBINED | (value[CHAR_DATA_WIDTH_INDEX] << WIDTH_MASK_SHIFT); } else { - this._data[index * CELL_SIZE + Cell.STRING] = value[1].charCodeAt(0); + this._data[index * CELL_SIZE + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << WIDTH_MASK_SHIFT); } - this._data[index * CELL_SIZE + Cell.WIDTH] = value[2]; } - public insertCells(pos: number, n: number, fillCharData: CharData): void { + /** + * primitive getters + * use these when only one value is needed, otherwise use `loadCell` + */ + public getWidth(index: number): number { + return this._data[index * CELL_SIZE + Cell.CONTENT] >> WIDTH_MASK_SHIFT; + } + + /** Test whether content has width. */ + public hasWidth(index: number): number { + return this._data[index * CELL_SIZE + Cell.CONTENT] & ContentMasks.WIDTH; + } + + /** Get FG cell component. */ + public getFg(index: number): number { + return this._data[index * CELL_SIZE + Cell.FG]; + } + + /** Get BG cell component. */ + public getBg(index: number): number { + return this._data[index * CELL_SIZE + Cell.BG]; + } + + /** + * Test whether contains any chars. + * Basically an empty has no content, but other cells might differ in FG/BG + * from real empty cells. + * */ + public hasContent(index: number): number { + return this._data[index * CELL_SIZE + Cell.CONTENT] & ContentMasks.HAS_CONTENT; + } + + /** + * Get codepoint of the cell. + * To be in line with `code` in CharData this either returns + * a single UTF32 codepoint or the last codepoint of a combined string. + */ + public getCodePoint(index: number): number { + const content = this._data[index * CELL_SIZE + Cell.CONTENT]; + if (content & ContentMasks.IS_COMBINED) { + return this._combined[index].charCodeAt(this._combined[index].length - 1); + } + return content & ContentMasks.CODEPOINT; + } + + /** Test whether the cell contains a combined string. */ + public isCombined(index: number): number { + return this._data[index * CELL_SIZE + Cell.CONTENT] & ContentMasks.IS_COMBINED; + } + + /** Returns the string content of the cell. */ + public getString(index: number): string { + const content = this._data[index * CELL_SIZE + Cell.CONTENT]; + if (content & ContentMasks.IS_COMBINED) { + return this._combined[index]; + } + if (content & ContentMasks.CODEPOINT) { + return stringFromCodePoint(content & ContentMasks.CODEPOINT); + } + // return empty string for empty cells + return ''; + } + + /** + * Load data at `index` into `cell`. This is used to access cells in a way that's more friendly + * to GC as it significantly reduced the amount of new objects/references needed. + */ + public loadCell(index: number, cell: ICellData): ICellData { + const startIndex = index * CELL_SIZE; + cell.content = this._data[startIndex + Cell.CONTENT]; + cell.fg = this._data[startIndex + Cell.FG]; + cell.bg = this._data[startIndex + Cell.BG]; + if (cell.content & ContentMasks.IS_COMBINED) { + cell.combinedData = this._combined[index]; + } + return cell; + } + + /** + * Set data at `index` to `cell`. + */ + public setCell(index: number, cell: ICellData): void { + if (cell.content & ContentMasks.IS_COMBINED) { + this._combined[index] = cell.combinedData; + } + this._data[index * CELL_SIZE + Cell.CONTENT] = cell.content; + this._data[index * CELL_SIZE + Cell.FG] = cell.fg; + this._data[index * CELL_SIZE + Cell.BG] = cell.bg; + } + + /** + * Set cell data from input handler. + * Since the input handler see the incoming chars as UTF32 codepoints, + * it gets an optimized access method. + */ + public setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void { + this._data[index * CELL_SIZE + Cell.CONTENT] = codePoint | (width << WIDTH_MASK_SHIFT); + this._data[index * CELL_SIZE + Cell.FG] = fg; + this._data[index * CELL_SIZE + Cell.BG] = bg; + } + + /** + * Add a codepoint to a cell from input handler. + * During input stage combining chars with a width of 0 follow and stack + * onto a leading char. Since we already set the attrs + * by the previous `setDataFromCodePoint` call, we can omit it here. + */ + public addCodepointToCell(index: number, codePoint: number): void { + let content = this._data[index * CELL_SIZE + Cell.CONTENT]; + if (content & ContentMasks.IS_COMBINED) { + // we already have a combined string, simply add + this._combined[index] += stringFromCodePoint(codePoint); + } else { + if (content & ContentMasks.CODEPOINT) { + // normal case for combining chars: + // - move current leading char + new one into combined string + // - set combined flag + this._combined[index] = stringFromCodePoint(content & ContentMasks.CODEPOINT) + stringFromCodePoint(codePoint); + content &= ~ContentMasks.CODEPOINT; // set codepoint in buffer to 0 + content |= ContentMasks.IS_COMBINED; + } else { + // should not happen - we actually have no data in the cell yet + // simply set the data in the cell buffer with a width of 1 + content = codePoint | (1 << WIDTH_MASK_SHIFT); + } + this._data[index * CELL_SIZE + Cell.CONTENT] = content; + } + } + + public insertCells(pos: number, n: number, fillCellData: ICellData): void { pos %= this.length; if (n < this.length - pos) { + const cell = new CellData(); for (let i = this.length - pos - n - 1; i >= 0; --i) { - this.set(pos + n + i, this.get(pos + i)); + this.setCell(pos + n + i, this.loadCell(pos + i, cell)); } for (let i = 0; i < n; ++i) { - this.set(pos + i, fillCharData); + this.setCell(pos + i, fillCellData); } } else { for (let i = pos; i < this.length; ++i) { - this.set(i, fillCharData); + this.setCell(i, fillCellData); } } } - public deleteCells(pos: number, n: number, fillCharData: CharData): void { + public deleteCells(pos: number, n: number, fillCellData: ICellData): void { pos %= this.length; if (n < this.length - pos) { + const cell = new CellData(); for (let i = 0; i < this.length - pos - n; ++i) { - this.set(pos + i, this.get(pos + n + i)); + this.setCell(pos + i, this.loadCell(pos + n + i, cell)); } for (let i = this.length - n; i < this.length; ++i) { - this.set(i, fillCharData); + this.setCell(i, fillCellData); } } else { for (let i = pos; i < this.length; ++i) { - this.set(i, fillCharData); + this.setCell(i, fillCellData); } } } - public replaceCells(start: number, end: number, fillCharData: CharData): void { + public replaceCells(start: number, end: number, fillCellData: ICellData): void { while (start < end && start < this.length) { - this.set(start++, fillCharData); + this.setCell(start++, fillCellData); } } - public resize(cols: number, fillCharData: CharData): void { + public resize(cols: number, fillCellData: ICellData): void { if (cols === this.length) { return; } @@ -122,7 +415,7 @@ export class BufferLine implements IBufferLine { } this._data = data; for (let i = this.length; i < cols; ++i) { - this.set(i, fillCharData); + this.setCell(i, fillCellData); } } else { if (cols) { @@ -146,10 +439,10 @@ export class BufferLine implements IBufferLine { } /** fill a line with fillCharData */ - public fill(fillCharData: CharData): void { + public fill(fillCellData: ICellData): void { this._combined = {}; for (let i = 0; i < this.length; ++i) { - this.set(i, fillCharData); + this.setCell(i, fillCellData); } } @@ -172,8 +465,6 @@ export class BufferLine implements IBufferLine { /** create a new clone */ public clone(): IBufferLine { const newLine = new BufferLine(0); - // creation of new typed array from another is actually pretty slow :( - // still faster than copying values one by one newLine._data = new Uint32Array(this._data); newLine.length = this.length; for (const el in this._combined) { @@ -185,8 +476,8 @@ export class BufferLine implements IBufferLine { public getTrimmedLength(): number { for (let i = this.length - 1; i >= 0; --i) { - if (this._data[i * CELL_SIZE + Cell.STRING] !== 0) { // 0 ==> ''.charCodeAt(0) ==> NaN ==> 0 - return i + this._data[i * CELL_SIZE + Cell.WIDTH]; + if ((this._data[i * CELL_SIZE + Cell.CONTENT] & ContentMasks.HAS_CONTENT)) { + return i + (this._data[i * CELL_SIZE + Cell.CONTENT] >> WIDTH_MASK_SHIFT); } } return 0; @@ -224,9 +515,10 @@ export class BufferLine implements IBufferLine { } let result = ''; while (startCol < endCol) { - const stringData = this._data[startCol * CELL_SIZE + Cell.STRING]; - result += (stringData & IS_COMBINED_BIT_MASK) ? this._combined[startCol] : (stringData) ? String.fromCharCode(stringData) : WHITESPACE_CELL_CHAR; - startCol += this._data[startCol * CELL_SIZE + Cell.WIDTH] || 1; + const content = this._data[startCol * CELL_SIZE + Cell.CONTENT]; + const cp = content & ContentMasks.CODEPOINT; + result += (content & ContentMasks.IS_COMBINED) ? this._combined[startCol] : (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR; + startCol += (content >> WIDTH_MASK_SHIFT) || 1; // always advance by 1 } return result; } diff --git a/src/BufferReflow.ts b/src/BufferReflow.ts index 24ab69e6..d27d7c48 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 { BufferLine, CellData } from './BufferLine'; import { CircularList, IDeleteEvent } from './common/CircularList'; import { IBufferLine } from './Types'; +import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR } from './Buffer'; export interface INewLayoutResult { layout: number[]; @@ -20,6 +20,7 @@ export interface INewLayoutResult { * @param newCols The columns after resize. */ export function reflowLargerGetLinesToRemove(lines: CircularList, newCols: number, bufferAbsoluteY: number): number[] { + const nullCell = CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); // 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[] = []; @@ -75,13 +76,13 @@ export function reflowLargerGetLinesToRemove(lines: CircularList, n 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 - wrappedLines[destLineIndex - 1].set(newCols - 1, FILL_CHAR_DATA); + wrappedLines[destLineIndex - 1].setCell(newCols - 1, nullCell); } } } // Clear out remaining cells or fragments could remain; - wrappedLines[destLineIndex].replaceCells(destCol, newCols, FILL_CHAR_DATA); + wrappedLines[destLineIndex].replaceCells(destCol, newCols, nullCell); // Work backwards and remove any rows at the end that only contain null cells let countToRemove = 0; diff --git a/src/CharWidth.test.ts b/src/CharWidth.test.ts index 0747fdf1..8608c6fa 100644 --- a/src/CharWidth.test.ts +++ b/src/CharWidth.test.ts @@ -8,6 +8,7 @@ import { assert } from 'chai'; import { getStringCellWidth, wcwidth } from './CharWidth'; import { IBuffer } from './Types'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer'; +import { CellData } from './BufferLine'; describe('getStringCellWidth', function(): void { @@ -22,7 +23,7 @@ describe('getStringCellWidth', function(): void { for (let i = start; i < end; ++i) { const line = buffer.lines.get(i); for (let j = 0; j < line.length; ++j) { // TODO: change to trimBorder with multiline - const ch = line.get(j); + const ch = line.loadCell(j, new CellData()).getAsCharData(); result += ch[CHAR_DATA_WIDTH_INDEX]; // return on sentinel if (ch[CHAR_DATA_CHAR_INDEX] === sentinel) { diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index a7963a2c..24eb0884 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -6,9 +6,10 @@ import { assert, expect } from 'chai'; import { InputHandler } from './InputHandler'; import { MockInputHandlingTerminal } from './ui/TestUtils.test'; -import { CHAR_DATA_ATTR_INDEX, DEFAULT_ATTR } from './Buffer'; +import { DEFAULT_ATTR } from './Buffer'; import { Terminal } from './Terminal'; import { IBufferLine } from './Types'; +import { CellData } from './BufferLine'; describe('InputHandler', () => { describe('save and restore cursor', () => { @@ -356,45 +357,45 @@ describe('InputHandler', () => { expect(term.buffer.translateBufferLineToString(0, true)).to.equal(''); expect(term.buffer.translateBufferLineToString(1, true)).to.equal(' TEST'); // Text color of 'TEST' should be red - expect((term.buffer.lines.get(1).get(4)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1); + expect((term.buffer.lines.get(1).loadCell(4, new CellData()).fg >> 9) & 0x1ff).to.equal(1); }); it('should handle DECSET/DECRST 1047 (alt screen buffer)', () => { handler.parse('\x1b[?1047h\r\n\x1b[31mJUNK\x1b[?1047lTEST'); expect(term.buffer.translateBufferLineToString(0, true)).to.equal(''); expect(term.buffer.translateBufferLineToString(1, true)).to.equal(' TEST'); // Text color of 'TEST' should be red - expect((term.buffer.lines.get(1).get(4)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1); + expect((term.buffer.lines.get(1).loadCell(4, new CellData()).fg >> 9) & 0x1ff).to.equal(1); }); it('should handle DECSET/DECRST 1048 (alt screen cursor)', () => { handler.parse('\x1b[?1048h\r\n\x1b[31mJUNK\x1b[?1048lTEST'); expect(term.buffer.translateBufferLineToString(0, true)).to.equal('TEST'); expect(term.buffer.translateBufferLineToString(1, true)).to.equal('JUNK'); // Text color of 'TEST' should be default - expect(term.buffer.lines.get(0).get(0)[CHAR_DATA_ATTR_INDEX]).to.equal(DEFAULT_ATTR); + expect(term.buffer.lines.get(0).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR); // Text color of 'JUNK' should be red - expect((term.buffer.lines.get(1).get(0)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1); + expect((term.buffer.lines.get(1).loadCell(0, new CellData()).fg >> 9) & 0x1ff).to.equal(1); }); it('should handle DECSET/DECRST 1049 (alt screen buffer+cursor)', () => { handler.parse('\x1b[?1049h\r\n\x1b[31mJUNK\x1b[?1049lTEST'); expect(term.buffer.translateBufferLineToString(0, true)).to.equal('TEST'); expect(term.buffer.translateBufferLineToString(1, true)).to.equal(''); // Text color of 'TEST' should be default - expect(term.buffer.lines.get(0).get(0)[CHAR_DATA_ATTR_INDEX]).to.equal(DEFAULT_ATTR); + expect(term.buffer.lines.get(0).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR); }); it('should handle DECSET/DECRST 1049 - maintains saved cursor for alt buffer', () => { handler.parse('\x1b[?1049h\r\n\x1b[31m\x1b[s\x1b[?1049lTEST'); expect(term.buffer.translateBufferLineToString(0, true)).to.equal('TEST'); // Text color of 'TEST' should be default - expect(term.buffer.lines.get(0).get(0)[CHAR_DATA_ATTR_INDEX]).to.equal(DEFAULT_ATTR); + expect(term.buffer.lines.get(0).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR); handler.parse('\x1b[?1049h\x1b[uTEST'); expect(term.buffer.translateBufferLineToString(1, true)).to.equal('TEST'); // Text color of 'TEST' should be red - expect((term.buffer.lines.get(1).get(0)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1); + expect((term.buffer.lines.get(1).loadCell(0, new CellData()).fg >> 9) & 0x1ff).to.equal(1); }); it('should handle DECSET/DECRST 1049 - clears alt buffer with erase attributes', () => { handler.parse('\x1b[42m\x1b[?1049h'); // Buffer should be filled with green background - expect(term.buffer.lines.get(20).get(10)[CHAR_DATA_ATTR_INDEX] & 0x1ff).to.equal(2); + expect(term.buffer.lines.get(20).loadCell(10, new CellData()).fg & 0x1ff).to.equal(2); }); }); }); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 7405ff9f..37c9bbe5 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -7,7 +7,7 @@ import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types'; import { C0, C1 } from './common/data/EscapeSequences'; import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets'; -import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer'; +import { DEFAULT_ATTR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer'; import { FLAGS } from './renderer/Types'; import { wcwidth } from './CharWidth'; import { EscapeSequenceParser } from './EscapeSequenceParser'; @@ -16,6 +16,7 @@ import { IDisposable } from 'xterm'; import { Disposable } from './common/Lifecycle'; import { concat } from './common/TypedArrayUtils'; import { StringToUtf32, stringFromCodePoint, utf32ToString } from './core/input/TextDecoder'; +import { CellData } from './BufferLine'; /** * Map collect to glevel. Used in `selectCharset`. @@ -105,6 +106,7 @@ class DECRQSS implements IDcsHandler { export class InputHandler extends Disposable implements IInputHandler { private _parseBuffer: Uint32Array = new Uint32Array(4096); private _stringDecoder: StringToUtf32 = new StringToUtf32(); + private _workCell: CellData = new CellData(); constructor( protected _terminal: IInputHandlingTerminal, @@ -301,9 +303,6 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._parseBuffer.length < data.length) { this._parseBuffer = new Uint32Array(data.length); } - 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; @@ -314,7 +313,6 @@ export class InputHandler extends Disposable implements IInputHandler { 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; @@ -328,25 +326,23 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.updateRange(buffer.y); 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 - // charset are only defined for ASCII, therefore we only + // charset is only defined for ASCII, therefore we only // search for an replacement char if code < 127 if (code < 127 && charset) { - const ch = charset[char]; + const ch = charset[String.fromCharCode(code)]; if (ch) { code = ch.charCodeAt(0); - char = ch; } } if (screenReaderMode) { - this._terminal.emit('a11y.char', char); + this._terminal.emit('a11y.char', stringFromCodePoint(code)); } // insert combining char at last cursor position @@ -355,23 +351,13 @@ export class InputHandler extends Disposable implements IInputHandler { // since they always follow a cell consuming char // therefore we can test for buffer.x to avoid overflow left if (!chWidth && buffer.x) { - const chMinusOne = bufferRow.get(buffer.x - 1); - if (chMinusOne) { - if (!chMinusOne[CHAR_DATA_WIDTH_INDEX]) { - // found empty cell after fullwidth, need to go 2 cells back - // it is save to step 2 cells back here - // since an empty cell is only set by fullwidth chars - const chMinusTwo = bufferRow.get(buffer.x - 2); - if (chMinusTwo) { - chMinusTwo[CHAR_DATA_CHAR_INDEX] += char; - chMinusTwo[CHAR_DATA_CODE_INDEX] = code; - bufferRow.set(buffer.x - 2, chMinusTwo); // must be set explicitly now - } - } else { - chMinusOne[CHAR_DATA_CHAR_INDEX] += char; - chMinusOne[CHAR_DATA_CODE_INDEX] = code; - bufferRow.set(buffer.x - 1, chMinusOne); // must be set explicitly now - } + if (!bufferRow.getWidth(buffer.x - 1)) { + // found empty cell after fullwidth, need to go 2 cells back + // it is save to step 2 cells back here + // since an empty cell is only set by fullwidth chars + bufferRow.addCodepointToCell(buffer.x - 2, code); + } else { + bufferRow.addCodepointToCell(buffer.x - 1, code); } continue; } @@ -410,25 +396,25 @@ export class InputHandler extends Disposable implements IInputHandler { // insert mode: move characters to right if (insertMode) { // right shift cells according to the width - bufferRow.insertCells(buffer.x, chWidth, [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + bufferRow.insertCells(buffer.x, chWidth, buffer.getNullCell(curAttr)); // test last cell - since the last cell has only room for // a halfwidth char any fullwidth shifted there is lost - // and will be set to eraseChar - const lastCell = bufferRow.get(cols - 1); - if (lastCell[CHAR_DATA_WIDTH_INDEX] === 2) { - bufferRow.set(cols - 1, [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + // and will be set to empty cell + if (bufferRow.getWidth(cols - 1) === 2) { + bufferRow.setCellFromCodePoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr, 0); } } // write current char to buffer and advance cursor - bufferRow.set(buffer.x++, [curAttr, char, chWidth, code]); + bufferRow.setCellFromCodePoint(buffer.x++, code, chWidth, curAttr, 0); // fullwidth char - also set next cell to placeholder stub and advance cursor // for graphemes bigger than fullwidth we can simply loop to zero // we already made sure above, that buffer.x + chWidth will not overflow right if (chWidth > 0) { while (--chWidth) { - bufferRow.set(buffer.x++, [curAttr, '', 0, undefined]); + // other than a regular empty cell a cell following a wide char has no width + bufferRow.setCellFromCodePoint(buffer.x++, 0, 0, curAttr, 0); } } } @@ -534,7 +520,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).insertCells( this._terminal.buffer.x, params[0] || 1, - [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE] + this._terminal.buffer.getNullCell(this._terminal.eraseAttr()) ); this._terminal.updateRange(this._terminal.buffer.y); } @@ -708,7 +694,7 @@ export class InputHandler extends Disposable implements IInputHandler { line.replaceCells( start, end, - [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE] + this._terminal.buffer.getNullCell(this._terminal.eraseAttr()) ); if (clearWrap) { line.isWrapped = false; @@ -877,7 +863,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).deleteCells( this._terminal.buffer.x, params[0] || 1, - [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE] + this._terminal.buffer.getNullCell(this._terminal.eraseAttr()) ); this._terminal.updateRange(this._terminal.buffer.y); } @@ -928,7 +914,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).replaceCells( this._terminal.buffer.x, this._terminal.buffer.x + (params[0] || 1), - [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE] + this._terminal.buffer.getNullCell(this._terminal.eraseAttr()) ); } @@ -984,9 +970,10 @@ export class InputHandler extends Disposable implements IInputHandler { // make buffer local for faster access const buffer = this._terminal.buffer; const line = buffer.lines.get(buffer.ybase + buffer.y); + line.loadCell(buffer.x - 1, this._workCell); line.replaceCells(buffer.x, buffer.x + (params[0] || 1), - line.get(buffer.x - 1) || [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE] + (this._workCell.content !== undefined) ? this._workCell : buffer.getNullCell(DEFAULT_ATTR) ); // FIXME: no updateRange here? } diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index 07dbf1b3..c7bbbeb8 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -9,7 +9,7 @@ import { ILinkMatcher, ITerminal, IBufferLine } from './Types'; import { Linkifier } from './Linkifier'; import { MockBuffer, MockTerminal, TestTerminal } from './ui/TestUtils.test'; import { CircularList } from './common/CircularList'; -import { BufferLine } from './BufferLine'; +import { BufferLine, CellData } from './BufferLine'; class TestLinkifier extends Linkifier { constructor(terminal: ITerminal) { @@ -53,7 +53,7 @@ describe('Linkifier', () => { function stringToRow(text: string): IBufferLine { const result = new BufferLine(text.length); for (let i = 0; i < text.length; i++) { - result.set(i, [0, text.charAt(i), 1, text.charCodeAt(i)]); + result.setCell(i, CellData.fromCharData([0, text.charAt(i), 1, text.charCodeAt(i)])); } return result; } diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 53247c95..80399904 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -7,7 +7,6 @@ import { IMouseZoneManager } from './ui/Types'; import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal, IBufferStringIteratorResult } from './Types'; import { MouseZone } from './ui/MouseZoneManager'; import { EventEmitter } from './common/EventEmitter'; -import { CHAR_DATA_ATTR_INDEX } from './Buffer'; import { getStringCellWidth } from './CharWidth'; /** @@ -232,10 +231,9 @@ export class Linkifier extends EventEmitter implements ILinkifier { } const line = this._terminal.buffer.lines.get(bufferIndex[0]); - const char = line.get(bufferIndex[1]); + const attr = line.getFg(bufferIndex[1]); let fg: number | undefined; - if (char) { - const attr: number = char[CHAR_DATA_ATTR_INDEX]; + if (attr) { fg = (attr >> 9) & 0x1ff; } diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index 21bc6754..d65f9716 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -10,7 +10,7 @@ import { SelectionModel } from './SelectionModel'; import { BufferSet } from './BufferSet'; import { ITerminal, IBuffer, IBufferLine } from './Types'; import { MockTerminal } from './ui/TestUtils.test'; -import { BufferLine } from './BufferLine'; +import { BufferLine, CellData } from './BufferLine'; class TestMockTerminal extends MockTerminal { emit(event: string, data: any): void {} @@ -57,14 +57,14 @@ describe('SelectionManager', () => { function stringToRow(text: string): IBufferLine { const result = new BufferLine(text.length); for (let i = 0; i < text.length; i++) { - result.set(i, [0, text.charAt(i), 1, text.charCodeAt(i)]); + result.setCell(i, CellData.fromCharData([0, text.charAt(i), 1, text.charCodeAt(i)])); } return result; } function stringArrayToRow(chars: string[]): IBufferLine { const line = new BufferLine(chars.length); - chars.map((c, idx) => line.set(idx, [0, c, 1, c.charCodeAt(0)])); + chars.map((c, idx) => line.setCell(idx, CellData.fromCharData([0, c, 1, c.charCodeAt(0)]))); return line; } @@ -119,7 +119,7 @@ describe('SelectionManager', () => { [null, 'o', 1, 'o'.charCodeAt(0)] ]; const line = new BufferLine(data.length); - for (let i = 0; i < data.length; ++i) line.set(i, data[i]); + for (let i = 0; i < data.length; ++i) line.setCell(i, CellData.fromCharData(data[i])); buffer.lines.set(0, line); // Ensure wide characters take up 2 columns selectionManager.selectWordAt([0, 0]); diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index f93328fe..361b1123 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -3,15 +3,15 @@ * @license MIT */ -import { ITerminal, ISelectionManager, IBuffer, CharData, IBufferLine } from './Types'; +import { ITerminal, ISelectionManager, IBuffer, IBufferLine } from './Types'; import { XtermListener } from './common/Types'; import { MouseHelper } from './ui/MouseHelper'; import * as Browser from './core/Platform'; import { CharMeasure } from './ui/CharMeasure'; import { EventEmitter } from './common/EventEmitter'; import { SelectionModel } from './SelectionModel'; -import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_CODE_INDEX } from './Buffer'; import { AltClickHandler } from './handlers/AltClickHandler'; +import { CellData } from './BufferLine'; /** * The number of pixels the mouse needs to be above or below the viewport in @@ -103,6 +103,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager private _mouseMoveListener: EventListener; private _mouseUpListener: EventListener; private _trimListener: XtermListener; + private _workCell: CellData = new CellData(); private _mouseDownTimeStamp: number; @@ -506,8 +507,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // If the mouse is over the second half of a wide character, adjust the // selection to cover the whole character - const char = line.get(this._model.selectionStart[0]); - if (char[CHAR_DATA_WIDTH_INDEX] === 0) { + if (line.hasWidth(this._model.selectionStart[0]) === 0) { this._model.selectionStart[0]++; } } @@ -596,8 +596,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // selection. Note that selections at the very end of the line will never // have a character. if (this._model.selectionEnd[1] < this._buffer.lines.length) { - const char = this._buffer.lines.get(this._model.selectionEnd[1]).get(this._model.selectionEnd[0]); - if (char && char[CHAR_DATA_WIDTH_INDEX] === 0) { + if (this._buffer.lines.get(this._model.selectionEnd[1]).hasWidth(this._model.selectionEnd[0]) === 0) { this._model.selectionEnd[0]++; } } @@ -670,16 +669,16 @@ export class SelectionManager extends EventEmitter implements ISelectionManager private _convertViewportColToCharacterIndex(bufferLine: IBufferLine, coords: [number, number]): number { let charIndex = coords[0]; for (let i = 0; coords[0] >= i; i++) { - const char = bufferLine.get(i); - if (char[CHAR_DATA_WIDTH_INDEX] === 0) { + const length = bufferLine.loadCell(i, this._workCell).getChars().length; + if (this._workCell.getWidth() === 0) { // Wide characters aren't included in the line string so decrement the // index so the index is back on the wide character. charIndex--; - } else if (char[CHAR_DATA_CHAR_INDEX].length > 1 && coords[0] !== i) { + } else if (length > 1 && coords[0] !== i) { // Emojis take up multiple characters, so adjust accordingly. For these // we don't want ot include the character at the column as we're // returning the start index in the string, not the end index. - charIndex += char[CHAR_DATA_CHAR_INDEX].length - 1; + charIndex += length - 1; } } return charIndex; @@ -739,48 +738,51 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // Consider the initial position, skip it and increment the wide char // variable - if (bufferLine.get(startCol)[CHAR_DATA_WIDTH_INDEX] === 0) { + if (bufferLine.getWidth(startCol) === 0) { leftWideCharCount++; startCol--; } - if (bufferLine.get(endCol)[CHAR_DATA_WIDTH_INDEX] === 2) { + if (bufferLine.getWidth(endCol) === 2) { rightWideCharCount++; endCol++; } // Adjust the end index for characters whose length are > 1 (emojis) - if (bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length > 1) { - rightLongCharOffset += bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length - 1; - endIndex += bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length - 1; + const length = bufferLine.getString(endCol).length; + if (length > 1) { + rightLongCharOffset += length - 1; + endIndex += length - 1; } // Expand the string in both directions until a space is hit - while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.get(startCol - 1))) { - const char = bufferLine.get(startCol - 1); - if (char[CHAR_DATA_WIDTH_INDEX] === 0) { + while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.loadCell(startCol - 1, this._workCell))) { + bufferLine.loadCell(startCol - 1, this._workCell); + const length = this._workCell.getChars().length; + if (this._workCell.getWidth() === 0) { // If the next character is a wide char, record it and skip the column leftWideCharCount++; startCol--; - } else if (char[CHAR_DATA_CHAR_INDEX].length > 1) { + } else if (length > 1) { // If the next character's string is longer than 1 char (eg. emoji), // adjust the index - leftLongCharOffset += char[CHAR_DATA_CHAR_INDEX].length - 1; - startIndex -= char[CHAR_DATA_CHAR_INDEX].length - 1; + leftLongCharOffset += length - 1; + startIndex -= length - 1; } startIndex--; startCol--; } - while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.get(endCol + 1))) { - const char = bufferLine.get(endCol + 1); - if (char[CHAR_DATA_WIDTH_INDEX] === 2) { + while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.loadCell(endCol + 1, this._workCell))) { + bufferLine.loadCell(endCol + 1, this._workCell); + const length = this._workCell.getChars().length; + if (this._workCell.getWidth() === 2) { // If the next character is a wide char, record it and skip the column rightWideCharCount++; endCol++; - } else if (char[CHAR_DATA_CHAR_INDEX].length > 1) { + } else if (length > 1) { // If the next character's string is longer than 1 char (eg. emoji), // adjust the index - rightLongCharOffset += char[CHAR_DATA_CHAR_INDEX].length - 1; - endIndex += char[CHAR_DATA_CHAR_INDEX].length - 1; + rightLongCharOffset += length - 1; + endIndex += length - 1; } endIndex++; endCol++; @@ -814,9 +816,9 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // Recurse upwards if the line is wrapped and the word wraps to the above line if (followWrappedLinesAbove) { - if (start === 0 && bufferLine.get(0)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) { + if (start === 0 && bufferLine.getCodePoint(0) !== 32 /*' '*/) { const previousBufferLine = this._buffer.lines.get(coords[1] - 1); - if (previousBufferLine && bufferLine.isWrapped && previousBufferLine.get(this._terminal.cols - 1)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) { + if (previousBufferLine && bufferLine.isWrapped && previousBufferLine.getCodePoint(this._terminal.cols - 1) !== 32 /*' '*/) { const previousLineWordPosition = this._getWordAt([this._terminal.cols - 1, coords[1] - 1], false, true, false); if (previousLineWordPosition) { const offset = this._terminal.cols - previousLineWordPosition.start; @@ -829,9 +831,9 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // Recurse downwards if the line is wrapped and the word wraps to the next line if (followWrappedLinesBelow) { - if (start + length === this._terminal.cols && bufferLine.get(this._terminal.cols - 1)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) { + if (start + length === this._terminal.cols && bufferLine.getCodePoint(this._terminal.cols - 1) !== 32 /*' '*/) { const nextBufferLine = this._buffer.lines.get(coords[1] + 1); - if (nextBufferLine && nextBufferLine.isWrapped && nextBufferLine.get(0)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) { + if (nextBufferLine && nextBufferLine.isWrapped && nextBufferLine.getCodePoint(0) !== 32 /*' '*/) { const nextLineWordPosition = this._getWordAt([0, coords[1] + 1], false, false, true); if (nextLineWordPosition) { length += nextLineWordPosition.length; @@ -894,13 +896,13 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * word logic. * @param char The character to check. */ - private _isCharWordSeparator(charData: CharData): boolean { + private _isCharWordSeparator(cell: CellData): boolean { // Zero width characters are never separators as they are always to the // right of wide characters - if (charData[CHAR_DATA_WIDTH_INDEX] === 0) { + if (cell.getWidth() === 0) { return false; } - return WORD_SEPARATORS.indexOf(charData[CHAR_DATA_CHAR_INDEX]) >= 0; + return WORD_SEPARATORS.indexOf(cell.getChars()) >= 0; } /** diff --git a/src/Terminal.integration.ts b/src/Terminal.integration.ts index d2a5cd7c..10043006 100644 --- a/src/Terminal.integration.ts +++ b/src/Terminal.integration.ts @@ -13,8 +13,9 @@ import * as path from 'path'; import * as pty from 'node-pty'; import { assert } from 'chai'; import { Terminal } from './Terminal'; -import { CHAR_DATA_CHAR_INDEX, WHITESPACE_CELL_CHAR } from './Buffer'; +import { WHITESPACE_CELL_CHAR } from './Buffer'; import { IViewport } from './Types'; +import { CellData } from './BufferLine'; class TestTerminal extends Terminal { innerWrite(): void { this._innerWrite(); } @@ -67,7 +68,7 @@ function terminalToString(term: Terminal): string { for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) { lineText = ''; for (let cell = 0; cell < term.cols; ++cell) { - lineText += term.buffer.lines.get(line).get(cell)[CHAR_DATA_CHAR_INDEX] || WHITESPACE_CELL_CHAR; + lineText += term.buffer.lines.get(line).loadCell(cell, new CellData()).getChars() || WHITESPACE_CELL_CHAR; } // rtrim empty cells as xterm does lineText = lineText.replace(/\s+$/, ''); diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index fdc9678b..08bceb34 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -6,7 +6,8 @@ import { assert, expect } from 'chai'; import { Terminal } from './Terminal'; import { MockViewport, MockCompositionHelper, MockRenderer } from './ui/TestUtils.test'; -import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, DEFAULT_ATTR } from './Buffer'; +import { DEFAULT_ATTR } from './Buffer'; +import { CellData } from './BufferLine'; const INIT_COLS = 80; const INIT_ROWS = 24; @@ -227,7 +228,7 @@ describe('term.js addons', () => { }); describe('setOption', () => { - it('should set the option correctly', () => { + it('should set option correctly', () => { term.setOption('cursorBlink', true); assert.equal(term.options.cursorBlink, true); term.setOption('cursorBlink', false); @@ -455,62 +456,62 @@ describe('term.js addons', () => { describe('scroll() function', () => { describe('when scrollback > 0', () => { it('should create a new line and scroll', () => { - term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); - term.buffer.lines.get(INIT_ROWS - 1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); + term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(INIT_ROWS - 1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS + 1); - assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a'); - assert.equal(term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX], 'b'); - assert.equal(term.buffer.lines.get(INIT_ROWS).get(0)[CHAR_DATA_CHAR_INDEX], ''); + assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a'); + assert.equal(term.buffer.lines.get(INIT_ROWS - 1).loadCell(0, new CellData()).getChars(), 'b'); + assert.equal(term.buffer.lines.get(INIT_ROWS).loadCell(0, new CellData()).getChars(), ''); }); it('should properly scroll inside a scroll region (scrollTop set)', () => { - term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); - term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); - term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]); + term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); + term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.scrollTop = 1; term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS); - assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a'); - assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c'); + assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a'); + assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c'); }); it('should properly scroll inside a scroll region (scrollBottom set)', () => { - term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); - term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); - term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]); - term.buffer.lines.get(3).set(0, [0, 'd', 0, 'd'.charCodeAt(0)]); - term.buffer.lines.get(4).set(0, [0, 'e', 0, 'e'.charCodeAt(0)]); + term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); + term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); + term.buffer.lines.get(3).setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)])); + term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)])); term.buffer.y = 3; term.buffer.scrollBottom = 3; term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS + 1); - assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a', '\'a\' should be pushed to the scrollback'); - assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'b'); - assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'c'); - assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], 'd'); - assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], '', 'a blank line should be added at scrollBottom\'s index'); - assert.equal(term.buffer.lines.get(5).get(0)[CHAR_DATA_CHAR_INDEX], 'e'); + assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a', '\'a\' should be pushed to the scrollback'); + assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'b'); + assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).getChars(), 'c'); + assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).getChars(), 'd'); + assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(5).loadCell(0, new CellData()).getChars(), 'e'); }); it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => { - term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); - term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); - term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]); - term.buffer.lines.get(3).set(0, [0, 'd', 0, 'd'.charCodeAt(0)]); - term.buffer.lines.get(4).set(0, [0, 'e', 0, 'e'.charCodeAt(0)]); + term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); + term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); + term.buffer.lines.get(3).setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)])); + term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)])); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.scrollTop = 1; term.buffer.scrollBottom = 3; term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS); - assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a'); - assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer'); - assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd'); - assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], '', 'a blank line should be added at scrollBottom\'s index'); - assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e'); + assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a'); + assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c', '\'b\' should be removed from the buffer'); + assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).getChars(), 'd'); + assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).getChars(), 'e'); }); }); @@ -521,65 +522,65 @@ describe('term.js addons', () => { }); it('should create a new line and shift everything up', () => { - term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); - term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); - term.buffer.lines.get(INIT_ROWS - 1).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]); + term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); + term.buffer.lines.get(INIT_ROWS - 1).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line assert.equal(term.buffer.lines.length, INIT_ROWS); term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS); // 'a' gets pushed out of buffer - assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'b'); - assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], ''); - assert.equal(term.buffer.lines.get(INIT_ROWS - 2).get(0)[CHAR_DATA_CHAR_INDEX], 'c'); - assert.equal(term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX], ''); + assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'b'); + assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), ''); + assert.equal(term.buffer.lines.get(INIT_ROWS - 2).loadCell(0, new CellData()).getChars(), 'c'); + assert.equal(term.buffer.lines.get(INIT_ROWS - 1).loadCell(0, new CellData()).getChars(), ''); }); it('should properly scroll inside a scroll region (scrollTop set)', () => { - term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); - term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); - term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]); + term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); + term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.scrollTop = 1; term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS); - assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a'); - assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c'); + assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a'); + assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c'); }); it('should properly scroll inside a scroll region (scrollBottom set)', () => { - term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); - term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); - term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]); - term.buffer.lines.get(3).set(0, [0, 'd', 0, 'd'.charCodeAt(0)]); - term.buffer.lines.get(4).set(0, [0, 'e', 0, 'e'.charCodeAt(0)]); + term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); + term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); + term.buffer.lines.get(3).setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)])); + term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)])); term.buffer.y = 3; term.buffer.scrollBottom = 3; term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS); - assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'b'); - assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c'); - assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd'); - assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], '', 'a blank line should be added at scrollBottom\'s index'); - assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e'); + assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'b'); + assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c'); + assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).getChars(), 'd'); + assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).getChars(), 'e'); }); it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => { - term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]); - term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]); - term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]); - term.buffer.lines.get(3).set(0, [0, 'd', 0, 'd'.charCodeAt(0)]); - term.buffer.lines.get(4).set(0, [0, 'e', 0, 'e'.charCodeAt(0)]); + term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); + term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); + term.buffer.lines.get(3).setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)])); + term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)])); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.scrollTop = 1; term.buffer.scrollBottom = 3; term.scroll(); assert.equal(term.buffer.lines.length, INIT_ROWS); - assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a'); - assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer'); - assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd'); - assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], '', 'a blank line should be added at scrollBottom\'s index'); - assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e'); + assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a'); + assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c', '\'b\' should be removed from the buffer'); + assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).getChars(), 'd'); + assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).getChars(), 'e'); }); }); }); @@ -770,116 +771,126 @@ describe('term.js addons', () => { it('2 characters per cell', function (): void { this.timeout(10000); // This is needed because istanbul patches code and slows it down const high = String.fromCharCode(0xD800); + const cell = new CellData(); for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.write(high + String.fromCharCode(i)); - const tchar = term.buffer.lines.get(0).get(0); - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); - expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql(''); + const tchar = term.buffer.lines.get(0).loadCell(0, cell); + expect(tchar.getChars()).eql(high + String.fromCharCode(i)); + expect(tchar.getChars().length).eql(2); + expect(tchar.getWidth()).eql(1); + expect(term.buffer.lines.get(0).loadCell(1, cell).getChars()).eql(''); term.reset(); } }); it('2 characters at last cell', () => { const high = String.fromCharCode(0xD800); + const cell = new CellData(); for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.buffer.x = term.cols - 1; term.write(high + String.fromCharCode(i)); - expect(term.buffer.lines.get(0).get(term.buffer.x - 1)[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); - expect(term.buffer.lines.get(0).get(term.buffer.x - 1)[CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX]).eql(''); + expect(term.buffer.lines.get(0).loadCell(term.buffer.x - 1, cell).getChars()).eql(high + String.fromCharCode(i)); + expect(term.buffer.lines.get(0).loadCell(term.buffer.x - 1, cell).getChars().length).eql(2); + expect(term.buffer.lines.get(1).loadCell(0, cell).getChars()).eql(''); term.reset(); } }); it('2 characters per cell over line end with autowrap', () => { const high = String.fromCharCode(0xD800); + const cell = new CellData(); for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.buffer.x = term.cols - 1; term.wraparoundMode = true; term.write('a' + high + String.fromCharCode(i)); - expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('a'); - expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); - expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(term.buffer.lines.get(1).get(1)[CHAR_DATA_CHAR_INDEX]).eql(''); + expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars()).eql('a'); + expect(term.buffer.lines.get(1).loadCell(0, cell).getChars()).eql(high + String.fromCharCode(i)); + expect(term.buffer.lines.get(1).loadCell(0, cell).getChars().length).eql(2); + expect(term.buffer.lines.get(1).loadCell(1, cell).getChars()).eql(''); term.reset(); } }); it('2 characters per cell over line end without autowrap', () => { const high = String.fromCharCode(0xD800); + const cell = new CellData(); for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.buffer.x = term.cols - 1; term.wraparoundMode = false; term.write('a' + high + String.fromCharCode(i)); // auto wraparound mode should cut off the rest of the line - expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('a'); - expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX].length).eql(1); - expect(term.buffer.lines.get(1).get(1)[CHAR_DATA_CHAR_INDEX]).eql(''); + expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars()).eql('a'); + expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars().length).eql(1); + expect(term.buffer.lines.get(1).loadCell(1, cell).getChars()).eql(''); term.reset(); } }); it('splitted surrogates', () => { const high = String.fromCharCode(0xD800); + const cell = new CellData(); for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.write(high); term.write(String.fromCharCode(i)); - const tchar = term.buffer.lines.get(0).get(0); - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); - expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql(''); + const tchar = term.buffer.lines.get(0).loadCell(0, cell); + expect(tchar.getChars()).eql(high + String.fromCharCode(i)); + expect(tchar.getChars().length).eql(2); + expect(tchar.getWidth()).eql(1); + expect(term.buffer.lines.get(0).loadCell(1, cell).getChars()).eql(''); term.reset(); } }); }); describe('unicode - combining characters', () => { + const cell = new CellData(); it('café', () => { term.write('cafe\u0301'); - expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_CHAR_INDEX]).eql('e\u0301'); - expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_WIDTH_INDEX]).eql(1); + term.buffer.lines.get(0).loadCell(3, cell); + expect(cell.getChars()).eql('e\u0301'); + expect(cell.getChars().length).eql(2); + expect(cell.getWidth()).eql(1); }); it('café - end of line', () => { term.buffer.x = term.cols - 1 - 3; term.write('cafe\u0301'); - expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('e\u0301'); - expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_WIDTH_INDEX]).eql(1); - expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql(''); - expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX].length).eql(0); - expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_WIDTH_INDEX]).eql(1); + term.buffer.lines.get(0).loadCell(term.cols - 1, cell); + expect(cell.getChars()).eql('e\u0301'); + expect(cell.getChars().length).eql(2); + expect(cell.getWidth()).eql(1); + term.buffer.lines.get(0).loadCell(1, cell); + expect(cell.getChars()).eql(''); + expect(cell.getChars().length).eql(0); + expect(cell.getWidth()).eql(1); }); it('multiple combined é', () => { term.wraparoundMode = true; term.write(Array(100).join('e\u0301')); for (let i = 0; i < term.cols; ++i) { - const tchar = term.buffer.lines.get(0).get(i); - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('e\u0301'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); + term.buffer.lines.get(0).loadCell(i, cell); + expect(cell.getChars()).eql('e\u0301'); + expect(cell.getChars().length).eql(2); + expect(cell.getWidth()).eql(1); } - const tchar = term.buffer.lines.get(1).get(0); - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('e\u0301'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); + term.buffer.lines.get(1).loadCell(0, cell); + expect(cell.getChars()).eql('e\u0301'); + expect(cell.getChars().length).eql(2); + expect(cell.getWidth()).eql(1); }); it('multiple surrogate with combined', () => { term.wraparoundMode = true; term.write(Array(100).join('\uD800\uDC00\u0301')); for (let i = 0; i < term.cols; ++i) { - const tchar = term.buffer.lines.get(0).get(i); - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\uD800\uDC00\u0301'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); + term.buffer.lines.get(0).loadCell(i, cell); + expect(cell.getChars()).eql('\uD800\uDC00\u0301'); + expect(cell.getChars().length).eql(3); + expect(cell.getWidth()).eql(1); } - const tchar = term.buffer.lines.get(1).get(0); - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\uD800\uDC00\u0301'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); + term.buffer.lines.get(1).loadCell(0, cell); + expect(cell.getChars()).eql('\uD800\uDC00\u0301'); + expect(cell.getChars().length).eql(3); + expect(cell.getWidth()).eql(1); }); }); describe('unicode - fullwidth characters', () => { + const cell = new CellData(); it('cursor movement even', () => { expect(term.buffer.x).eql(0); term.write('¥'); @@ -895,140 +906,141 @@ describe('term.js addons', () => { term.wraparoundMode = true; term.write(Array(50).join('¥')); for (let i = 0; i < term.cols; ++i) { - const tchar = term.buffer.lines.get(0).get(i); + term.buffer.lines.get(0).loadCell(i, cell); if (i % 2) { - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0); + expect(cell.getChars()).eql(''); + expect(cell.getChars().length).eql(0); + expect(cell.getWidth()).eql(0); } else { - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + expect(cell.getChars()).eql('¥'); + expect(cell.getChars().length).eql(1); + expect(cell.getWidth()).eql(2); } } - const tchar = term.buffer.lines.get(1).get(0); - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + term.buffer.lines.get(1).loadCell(0, cell); + expect(cell.getChars()).eql('¥'); + expect(cell.getChars().length).eql(1); + expect(cell.getWidth()).eql(2); }); it('line of ¥ odd', () => { term.wraparoundMode = true; term.buffer.x = 1; term.write(Array(50).join('¥')); for (let i = 1; i < term.cols - 1; ++i) { - const tchar = term.buffer.lines.get(0).get(i); + term.buffer.lines.get(0).loadCell(i, cell); if (!(i % 2)) { - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0); + expect(cell.getChars()).eql(''); + expect(cell.getChars().length).eql(0); + expect(cell.getWidth()).eql(0); } else { - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + expect(cell.getChars()).eql('¥'); + expect(cell.getChars().length).eql(1); + expect(cell.getWidth()).eql(2); } } - let tchar = term.buffer.lines.get(0).get(term.cols - 1); - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); - tchar = term.buffer.lines.get(1).get(0); - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + term.buffer.lines.get(0).loadCell(term.cols - 1, cell); + expect(cell.getChars()).eql(''); + expect(cell.getChars().length).eql(0); + expect(cell.getWidth()).eql(1); + term.buffer.lines.get(1).loadCell(0, cell); + expect(cell.getChars()).eql('¥'); + expect(cell.getChars().length).eql(1); + expect(cell.getWidth()).eql(2); }); it('line of ¥ with combining odd', () => { term.wraparoundMode = true; term.buffer.x = 1; term.write(Array(50).join('¥\u0301')); for (let i = 1; i < term.cols - 1; ++i) { - const tchar = term.buffer.lines.get(0).get(i); + term.buffer.lines.get(0).loadCell(i, cell); if (!(i % 2)) { - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0); + expect(cell.getChars()).eql(''); + expect(cell.getChars().length).eql(0); + expect(cell.getWidth()).eql(0); } else { - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + expect(cell.getChars()).eql('¥\u0301'); + expect(cell.getChars().length).eql(2); + expect(cell.getWidth()).eql(2); } } - let tchar = term.buffer.lines.get(0).get(term.cols - 1); - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); - tchar = term.buffer.lines.get(1).get(0); - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + term.buffer.lines.get(0).loadCell(term.cols - 1, cell); + expect(cell.getChars()).eql(''); + expect(cell.getChars().length).eql(0); + expect(cell.getWidth()).eql(1); + term.buffer.lines.get(1).loadCell(0, cell); + expect(cell.getChars()).eql('¥\u0301'); + expect(cell.getChars().length).eql(2); + expect(cell.getWidth()).eql(2); }); it('line of ¥ with combining even', () => { term.wraparoundMode = true; term.write(Array(50).join('¥\u0301')); for (let i = 0; i < term.cols; ++i) { - const tchar = term.buffer.lines.get(0).get(i); + term.buffer.lines.get(0).loadCell(i, cell); if (i % 2) { - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0); + expect(cell.getChars()).eql(''); + expect(cell.getChars().length).eql(0); + expect(cell.getWidth()).eql(0); } else { - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + expect(cell.getChars()).eql('¥\u0301'); + expect(cell.getChars().length).eql(2); + expect(cell.getWidth()).eql(2); } } - const tchar = term.buffer.lines.get(1).get(0); - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + term.buffer.lines.get(1).loadCell(0, cell); + expect(cell.getChars()).eql('¥\u0301'); + expect(cell.getChars().length).eql(2); + expect(cell.getWidth()).eql(2); }); it('line of surrogate fullwidth with combining odd', () => { term.wraparoundMode = true; term.buffer.x = 1; term.write(Array(50).join('\ud843\ude6d\u0301')); for (let i = 1; i < term.cols - 1; ++i) { - const tchar = term.buffer.lines.get(0).get(i); + term.buffer.lines.get(0).loadCell(i, cell); if (!(i % 2)) { - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0); + expect(cell.getChars()).eql(''); + expect(cell.getChars().length).eql(0); + expect(cell.getWidth()).eql(0); } else { - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + expect(cell.getChars()).eql('\ud843\ude6d\u0301'); + expect(cell.getChars().length).eql(3); + expect(cell.getWidth()).eql(2); } } - let tchar = term.buffer.lines.get(0).get(term.cols - 1); - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); - tchar = term.buffer.lines.get(1).get(0); - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + term.buffer.lines.get(0).loadCell(term.cols - 1, cell); + expect(cell.getChars()).eql(''); + expect(cell.getChars().length).eql(0); + expect(cell.getWidth()).eql(1); + term.buffer.lines.get(1).loadCell(0, cell); + expect(cell.getChars()).eql('\ud843\ude6d\u0301'); + expect(cell.getChars().length).eql(3); + expect(cell.getWidth()).eql(2); }); it('line of surrogate fullwidth with combining even', () => { term.wraparoundMode = true; term.write(Array(50).join('\ud843\ude6d\u0301')); for (let i = 0; i < term.cols; ++i) { - const tchar = term.buffer.lines.get(0).get(i); + term.buffer.lines.get(0).loadCell(i, cell); if (i % 2) { - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0); + expect(cell.getChars()).eql(''); + expect(cell.getChars().length).eql(0); + expect(cell.getWidth()).eql(0); } else { - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + expect(cell.getChars()).eql('\ud843\ude6d\u0301'); + expect(cell.getChars().length).eql(3); + expect(cell.getWidth()).eql(2); } } - const tchar = term.buffer.lines.get(1).get(0); - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301'); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + term.buffer.lines.get(1).loadCell(0, cell); + expect(cell.getChars()).eql('\ud843\ude6d\u0301'); + expect(cell.getChars().length).eql(3); + expect(cell.getWidth()).eql(2); }); }); describe('insert mode', () => { + const cell = new CellData(); it('halfwidth - all', () => { term.write(Array(9).join('0123456789').slice(-80)); term.buffer.x = 10; @@ -1036,10 +1048,10 @@ describe('term.js addons', () => { term.insertMode = true; term.write('abcde'); expect(term.buffer.lines.get(0).length).eql(term.cols); - expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('a'); - expect(term.buffer.lines.get(0).get(14)[CHAR_DATA_CHAR_INDEX]).eql('e'); - expect(term.buffer.lines.get(0).get(15)[CHAR_DATA_CHAR_INDEX]).eql('0'); - expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql('4'); + expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('a'); + expect(term.buffer.lines.get(0).loadCell(14, cell).getChars()).eql('e'); + expect(term.buffer.lines.get(0).loadCell(15, cell).getChars()).eql('0'); + expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql('4'); }); it('fullwidth - insert', () => { term.write(Array(9).join('0123456789').slice(-80)); @@ -1048,11 +1060,11 @@ describe('term.js addons', () => { term.insertMode = true; term.write('¥¥¥'); expect(term.buffer.lines.get(0).length).eql(term.cols); - expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('¥'); - expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql(''); - expect(term.buffer.lines.get(0).get(14)[CHAR_DATA_CHAR_INDEX]).eql('¥'); - expect(term.buffer.lines.get(0).get(15)[CHAR_DATA_CHAR_INDEX]).eql(''); - expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql('3'); + expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('¥'); + expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql(''); + expect(term.buffer.lines.get(0).loadCell(14, cell).getChars()).eql('¥'); + expect(term.buffer.lines.get(0).loadCell(15, cell).getChars()).eql(''); + expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql('3'); }); it('fullwidth - right border', () => { term.write(Array(41).join('¥')); @@ -1061,14 +1073,14 @@ describe('term.js addons', () => { term.insertMode = true; term.write('a'); expect(term.buffer.lines.get(0).length).eql(term.cols); - expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('a'); - expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('¥'); - expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql(''); // fullwidth char got replaced + expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('a'); + expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql('¥'); + expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql(''); // fullwidth char got replaced term.write('b'); expect(term.buffer.lines.get(0).length).eql(term.cols); - expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('b'); - expect(term.buffer.lines.get(0).get(12)[CHAR_DATA_CHAR_INDEX]).eql('¥'); - expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql(''); // empty cell after fullwidth + expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql('b'); + expect(term.buffer.lines.get(0).loadCell(12, cell).getChars()).eql('¥'); + expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql(''); // empty cell after fullwidth }); }); }); diff --git a/src/Terminal.ts b/src/Terminal.ts index 0ced61c7..f011d2e4 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -25,7 +25,7 @@ import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions import { IMouseZoneManager } from './ui/Types'; import { IRenderer } from './renderer/Types'; import { BufferSet } from './BufferSet'; -import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, CHAR_DATA_ATTR_INDEX } from './Buffer'; +import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer'; import { CompositionHelper } from './CompositionHelper'; import { EventEmitter } from './common/EventEmitter'; import { Viewport } from './Viewport'; @@ -344,7 +344,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II */ public focus(): void { if (this.textarea) { - this.textarea.focus(); + this.textarea.focus({ preventScroll: true }); } } @@ -1183,7 +1183,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II public scroll(isWrapped: boolean = false): void { let newLine: IBufferLine; newLine = this._blankLine; - if (!newLine || newLine.length !== this.cols || newLine.get(0)[CHAR_DATA_ATTR_INDEX] !== this.eraseAttr()) { + if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== this.eraseAttr()) { newLine = this.buffer.getBlankLine(this.eraseAttr(), isWrapped); this._blankLine = newLine; } diff --git a/src/Types.ts b/src/Types.ts index 888a321f..02ed41e2 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -298,6 +298,8 @@ export interface IBuffer { getBlankLine(attr: number, isWrapped?: boolean): IBufferLine; stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[]; iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator; + getNullCell(fg?: number, bg?: number): ICellData; + getWhitespaceCell(fg?: number, bg?: number): ICellData; } export interface IBufferSet extends IEventEmitter { @@ -520,6 +522,20 @@ export interface IEscapeSequenceParser extends IDisposable { clearErrorHandler(): void; } +/** Cell data */ +export interface ICellData { + content: number; + fg: number; + bg: number; + combinedData: string; + isCombined(): number; + getWidth(): number; + getChars(): string; + getCode(): number; + setFromCharData(value: CharData): void; + getAsCharData(): CharData; +} + /** * Interface for a line in the terminal buffer. */ @@ -528,13 +544,27 @@ export interface IBufferLine { isWrapped: boolean; get(index: number): CharData; set(index: number, value: CharData): void; - insertCells(pos: number, n: number, ch: CharData): void; - deleteCells(pos: number, n: number, fill: CharData): void; - replaceCells(start: number, end: number, fill: CharData): void; - resize(cols: number, fill: CharData): void; - fill(fillCharData: CharData): void; + loadCell(index: number, cell: ICellData): ICellData; + setCell(index: number, cell: ICellData): void; + setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void; + addCodepointToCell(index: number, codePoint: number): void; + insertCells(pos: number, n: number, ch: ICellData): void; + deleteCells(pos: number, n: number, fill: ICellData): void; + replaceCells(start: number, end: number, fill: ICellData): void; + resize(cols: number, fill: ICellData): void; + fill(fillCellData: ICellData): void; copyFrom(line: IBufferLine): void; clone(): IBufferLine; getTrimmedLength(): number; translateToString(trimRight?: boolean, startCol?: number, endCol?: number): string; + + /* direct access to cell attrs */ + getWidth(index: number): number; + hasWidth(index: number): number; + getFg(index: number): number; + getBg(index: number): number; + hasContent(index: number): number; + getCodePoint(index: number): number; + isCombined(index: number): number; + getString(index: number): string; } diff --git a/src/addons/attach/tsconfig.json b/src/addons/attach/tsconfig.json index 359fbd24..2f39102c 100644 --- a/src/addons/attach/tsconfig.json +++ b/src/addons/attach/tsconfig.json @@ -10,8 +10,7 @@ "outDir": "../../../lib/addons/attach/", "sourceMap": true, "removeComments": true, - "declaration": true, - "preserveWatchOutput": true + "declaration": true }, "include": [ "**/*.ts", diff --git a/src/addons/fit/tsconfig.json b/src/addons/fit/tsconfig.json index 489ccdfe..3458d23a 100644 --- a/src/addons/fit/tsconfig.json +++ b/src/addons/fit/tsconfig.json @@ -11,7 +11,6 @@ "sourceMap": true, "removeComments": true, "declaration": true, - "preserveWatchOutput": true, "types": [ "../../node_modules/@types/mocha" ] diff --git a/src/addons/fullscreen/tsconfig.json b/src/addons/fullscreen/tsconfig.json index 05e6df68..0c74c25c 100644 --- a/src/addons/fullscreen/tsconfig.json +++ b/src/addons/fullscreen/tsconfig.json @@ -11,7 +11,6 @@ "sourceMap": true, "removeComments": true, "declaration": true, - "preserveWatchOutput": true, "types": [ "../../node_modules/@types/mocha" ] diff --git a/src/addons/search/tsconfig.json b/src/addons/search/tsconfig.json index 87899cda..6a1611a5 100644 --- a/src/addons/search/tsconfig.json +++ b/src/addons/search/tsconfig.json @@ -11,7 +11,6 @@ "sourceMap": true, "removeComments": true, "declaration": true, - "preserveWatchOutput": true, "types": [ "../../node_modules/@types/mocha" ] diff --git a/src/addons/terminado/tsconfig.json b/src/addons/terminado/tsconfig.json index 91c18314..e2e19445 100644 --- a/src/addons/terminado/tsconfig.json +++ b/src/addons/terminado/tsconfig.json @@ -10,7 +10,6 @@ "sourceMap": true, "removeComments": true, "declaration": true, - "preserveWatchOutput": true, "types": [ "../../node_modules/@types/mocha" ] diff --git a/src/addons/webLinks/tsconfig.json b/src/addons/webLinks/tsconfig.json index 18105aa2..9c4f1176 100644 --- a/src/addons/webLinks/tsconfig.json +++ b/src/addons/webLinks/tsconfig.json @@ -11,7 +11,6 @@ "sourceMap": true, "removeComments": true, "declaration": true, - "preserveWatchOutput": true, "types": [ "../../node_modules/@types/mocha" ] diff --git a/src/addons/winptyCompat/tsconfig.json b/src/addons/winptyCompat/tsconfig.json index 9fc4d25e..fa48c963 100644 --- a/src/addons/winptyCompat/tsconfig.json +++ b/src/addons/winptyCompat/tsconfig.json @@ -10,7 +10,6 @@ "sourceMap": true, "removeComments": true, "declaration": true, - "preserveWatchOutput": true, "types": [ "../../node_modules/@types/mocha" ] diff --git a/src/addons/winptyCompat/winptyCompat.ts b/src/addons/winptyCompat/winptyCompat.ts index d162f4e9..58f59fd9 100644 --- a/src/addons/winptyCompat/winptyCompat.ts +++ b/src/addons/winptyCompat/winptyCompat.ts @@ -13,12 +13,6 @@ const WHITESPACE_CELL_CODE = 32; export function winptyCompatInit(terminal: Terminal): void { const addonTerminal = terminal; - // Don't do anything when the platform is not Windows - const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0; - if (!isWindows) { - return; - } - (addonTerminal._core as any).isWinptyCompatEnabled = true; // Winpty does not support wraparound mode which means that lines will never diff --git a/src/addons/zmodem/tsconfig.json b/src/addons/zmodem/tsconfig.json index 2b49f537..7d821b7c 100644 --- a/src/addons/zmodem/tsconfig.json +++ b/src/addons/zmodem/tsconfig.json @@ -10,7 +10,6 @@ "sourceMap": true, "removeComments": true, "declaration": true, - "preserveWatchOutput": true, "types": [ "../../node_modules/@types/mocha" ] diff --git a/src/common/tsconfig.json b/src/common/tsconfig.json index 19dd0273..b40bb2f5 100644 --- a/src/common/tsconfig.json +++ b/src/common/tsconfig.json @@ -1,17 +1,7 @@ { + "extends": "../tsconfig-library-base", "compilerOptions": { - "target": "es5", - "lib": [ - "es5" - ], - "rootDir": ".", - "noEmit": true, - "strict": true, - "pretty": true, - "types": [ - "../../node_modules/@types/mocha", - "../../" - ] + "outDir": "../../lib" }, "include": [ "./**/*" diff --git a/src/core/tsconfig.json b/src/core/tsconfig.json index 4f024a28..41e41f0c 100644 --- a/src/core/tsconfig.json +++ b/src/core/tsconfig.json @@ -1,20 +1,12 @@ { + "extends": "../tsconfig-library-base", "compilerOptions": { - "target": "es5", - "lib": [ - "es5" - ], - "rootDir": ".", - "noEmit": true, - "strict": true, - "pretty": true, - "types": [ - "../../node_modules/@types/mocha", - "../../" - ] + "outDir": "../../lib" }, "include": [ - "./**/*", - "../common/**/*" + "./**/*" + ], + "references": [ + { "path": "../common" } ] } diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index a609d79c..7bd38bb1 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -4,12 +4,12 @@ */ import { IRenderLayer, IColorSet, IRenderDimensions } from './Types'; -import { CharData, ITerminal } from '../Types'; +import { ITerminal } from '../Types'; import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, IGlyphIdentifier } from './atlas/Types'; import BaseCharAtlas from './atlas/BaseCharAtlas'; import { acquireCharAtlas } from './atlas/CharAtlasCache'; -import { CHAR_DATA_CHAR_INDEX } from '../Buffer'; import { is256Color } from './atlas/CharAtlasUtils'; +import { CellData } from '../BufferLine'; export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; @@ -229,17 +229,17 @@ export abstract class BaseRenderLayer implements IRenderLayer { * ensure that it fits with the cell, including the cell to the right if it's * a wide character. This uses the existing fillStyle on the context. * @param terminal The terminal. - * @param charData The char data for the character to draw. + * @param cell The cell data for the character to draw. * @param x The column to draw at. * @param y The row to draw at. * @param color The color of the character. */ - protected fillCharTrueColor(terminal: ITerminal, charData: CharData, x: number, y: number): void { + protected fillCharTrueColor(terminal: ITerminal, cell: CellData, x: number, y: number): void { this._ctx.font = this._getFont(terminal, false, false); this._ctx.textBaseline = 'middle'; this._clipRow(terminal, y); this._ctx.fillText( - charData[CHAR_DATA_CHAR_INDEX], + cell.getChars(), x * this._scaledCellWidth + this._scaledCharLeft, y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2); } diff --git a/src/renderer/CharacterJoinerRegistry.test.ts b/src/renderer/CharacterJoinerRegistry.test.ts index 0c29566a..effdbfaa 100644 --- a/src/renderer/CharacterJoinerRegistry.test.ts +++ b/src/renderer/CharacterJoinerRegistry.test.ts @@ -5,7 +5,7 @@ import { CircularList } from '../common/CircularList'; import { ICharacterJoinerRegistry } from './Types'; import { CharacterJoinerRegistry } from './CharacterJoinerRegistry'; -import { BufferLine } from '../BufferLine'; +import { BufferLine, CellData } from '../BufferLine'; import { IBufferLine } from '../Types'; describe('CharacterJoinerRegistry', () => { @@ -24,18 +24,18 @@ describe('CharacterJoinerRegistry', () => { lines.set(4, new BufferLine(0)); lines.set(5, lineData([['a', 0x11111111], [' -> b -> c -> '], ['d', 0x22222222]])); const line6 = lineData([['wi']]); - line6.resize(line6.length + 1, [0, '¥', 2, '¥'.charCodeAt(0)]); - line6.resize(line6.length + 1, [0, '', 0, null]); + line6.resize(line6.length + 1, CellData.fromCharData([0, '¥', 2, '¥'.charCodeAt(0)])); + line6.resize(line6.length + 1, CellData.fromCharData([0, '', 0, null])); let sub = lineData([['deemo']]); let oldSize = line6.length; - line6.resize(oldSize + sub.length, [0, '', 0, 0]); - for (let i = 0; i < sub.length; ++i) line6.set(i + oldSize, sub.get(i)); - line6.resize(line6.length + 1, [0, '\xf0\x9f\x98\x81', 1, 128513]); - line6.resize(line6.length + 1, [0, ' ', 1, ' '.charCodeAt(0)]); + line6.resize(oldSize + sub.length, CellData.fromCharData([0, '', 0, 0])); + for (let i = 0; i < sub.length; ++i) line6.setCell(i + oldSize, sub.loadCell(i, new CellData())); + line6.resize(line6.length + 1, CellData.fromCharData([0, '\xf0\x9f\x98\x81', 1, 128513])); + line6.resize(line6.length + 1, CellData.fromCharData([0, ' ', 1, ' '.charCodeAt(0)])); sub = lineData([['jiabc']]); oldSize = line6.length; - line6.resize(oldSize + sub.length, [0, '', 0, 0]); - for (let i = 0; i < sub.length; ++i) line6.set(i + oldSize, sub.get(i)); + line6.resize(oldSize + sub.length, CellData.fromCharData([0, '', 0, 0])); + for (let i = 0; i < sub.length; ++i) line6.setCell(i + oldSize, sub.loadCell(i, new CellData())); lines.set(6, line6); (terminal.buffer).setLines(lines); @@ -273,8 +273,8 @@ function lineData(data: IPartialLineData[]): IBufferLine { const line = data[i][0]; const attr = (data[i][1] || 0); const offset = tline.length; - tline.resize(tline.length + line.split('').length, [0, '', 0, 0]); - line.split('').map((char, idx) => tline.set(idx + offset, [attr, char, 1, char.charCodeAt(0)])); + tline.resize(tline.length + line.split('').length, CellData.fromCharData([0, '', 0, 0])); + line.split('').map((char, idx) => tline.setCell(idx + offset, CellData.fromCharData([attr, char, 1, char.charCodeAt(0)]))); } return tline; } diff --git a/src/renderer/CharacterJoinerRegistry.ts b/src/renderer/CharacterJoinerRegistry.ts index dc9e95dd..4a899d72 100644 --- a/src/renderer/CharacterJoinerRegistry.ts +++ b/src/renderer/CharacterJoinerRegistry.ts @@ -1,11 +1,12 @@ -import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; import { ITerminal, IBufferLine } from '../Types'; import { ICharacterJoinerRegistry, ICharacterJoiner } from './Types'; +import { CellData } from '../BufferLine'; export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { private _characterJoiners: ICharacterJoiner[] = []; private _nextCharacterJoinerId: number = 0; + private _workCell: CellData = new CellData(); constructor(private _terminal: ITerminal) { } @@ -51,13 +52,13 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { let rangeStartColumn = 0; let currentStringIndex = 0; let rangeStartStringIndex = 0; - let rangeAttr = line.get(0)[CHAR_DATA_ATTR_INDEX] >> 9; + let rangeAttr = line.getFg(0) >> 9; for (let x = 0; x < this._terminal.cols; x++) { - const charData = line.get(x); - const chars = charData[CHAR_DATA_CHAR_INDEX]; - const width = charData[CHAR_DATA_WIDTH_INDEX]; - const attr = charData[CHAR_DATA_ATTR_INDEX] >> 9; + line.loadCell(x, this._workCell); + const chars = this._workCell.getChars(); + const width = this._workCell.getWidth(); + const attr = this._workCell.fg >> 9; if (width === 0) { // If this character is of width 0, skip it. @@ -152,9 +153,8 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry { } for (let x = startCol; x < this._terminal.cols; x++) { - const charData = line.get(x); - const width = charData[CHAR_DATA_WIDTH_INDEX]; - const length = charData[CHAR_DATA_CHAR_INDEX].length; + const width = line.getWidth(x); + const length = line.getString(x).length; // We skip zero-width characters when creating the string to join the text // so we do the same here diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 08a14739..c3b751fa 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -3,10 +3,10 @@ * @license MIT */ -import { CHAR_DATA_WIDTH_INDEX } from '../Buffer'; import { IColorSet, IRenderDimensions } from './Types'; import { BaseRenderLayer } from './BaseRenderLayer'; -import { CharData, ITerminal } from '../Types'; +import { ITerminal, ICellData } from '../Types'; +import { CellData } from '../BufferLine'; interface ICursorState { x: number; @@ -23,8 +23,9 @@ const BLINK_INTERVAL = 600; export class CursorRenderLayer extends BaseRenderLayer { private _state: ICursorState; - private _cursorRenderers: {[key: string]: (terminal: ITerminal, x: number, y: number, charData: CharData) => void}; + private _cursorRenderers: {[key: string]: (terminal: ITerminal, x: number, y: number, cell: ICellData) => void}; private _cursorBlinkStateManager: CursorBlinkStateManager; + private _cell: ICellData = new CellData(); constructor(container: HTMLElement, zIndex: number, colors: IColorSet) { super(container, 'cursor', zIndex, true, colors); @@ -127,8 +128,8 @@ export class CursorRenderLayer extends BaseRenderLayer { return; } - const charData = terminal.buffer.lines.get(cursorY).get(terminal.buffer.x); - if (!charData) { + terminal.buffer.lines.get(cursorY).loadCell(terminal.buffer.x, this._cell); + if (this._cell.content === undefined) { return; } @@ -136,13 +137,13 @@ export class CursorRenderLayer extends BaseRenderLayer { this._clearCursor(); this._ctx.save(); this._ctx.fillStyle = this._colors.cursor.css; - this._renderBlurCursor(terminal, terminal.buffer.x, viewportRelativeCursorY, charData); + this._renderBlurCursor(terminal, terminal.buffer.x, viewportRelativeCursorY, this._cell); this._ctx.restore(); this._state.x = terminal.buffer.x; this._state.y = viewportRelativeCursorY; this._state.isFocused = false; this._state.style = terminal.options.cursorStyle; - this._state.width = charData[CHAR_DATA_WIDTH_INDEX]; + this._state.width = this._cell.getWidth(); return; } @@ -158,21 +159,21 @@ export class CursorRenderLayer extends BaseRenderLayer { this._state.y === viewportRelativeCursorY && this._state.isFocused === terminal.isFocused && this._state.style === terminal.options.cursorStyle && - this._state.width === charData[CHAR_DATA_WIDTH_INDEX]) { + this._state.width === this._cell.getWidth()) { return; } this._clearCursor(); } this._ctx.save(); - this._cursorRenderers[terminal.options.cursorStyle || 'block'](terminal, terminal.buffer.x, viewportRelativeCursorY, charData); + this._cursorRenderers[terminal.options.cursorStyle || 'block'](terminal, terminal.buffer.x, viewportRelativeCursorY, this._cell); this._ctx.restore(); this._state.x = terminal.buffer.x; this._state.y = viewportRelativeCursorY; this._state.isFocused = false; this._state.style = terminal.options.cursorStyle; - this._state.width = charData[CHAR_DATA_WIDTH_INDEX]; + this._state.width = this._cell.getWidth(); } private _clearCursor(): void { @@ -188,33 +189,33 @@ export class CursorRenderLayer extends BaseRenderLayer { } } - private _renderBarCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { + private _renderBarCursor(terminal: ITerminal, x: number, y: number, cell: ICellData): void { this._ctx.save(); this._ctx.fillStyle = this._colors.cursor.css; this.fillLeftLineAtCell(x, y); this._ctx.restore(); } - private _renderBlockCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { + private _renderBlockCursor(terminal: ITerminal, x: number, y: number, cell: ICellData): void { this._ctx.save(); this._ctx.fillStyle = this._colors.cursor.css; - this.fillCells(x, y, charData[CHAR_DATA_WIDTH_INDEX], 1); + this.fillCells(x, y, cell.getWidth(), 1); this._ctx.fillStyle = this._colors.cursorAccent.css; - this.fillCharTrueColor(terminal, charData, x, y); + this.fillCharTrueColor(terminal, cell, x, y); this._ctx.restore(); } - private _renderUnderlineCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { + private _renderUnderlineCursor(terminal: ITerminal, x: number, y: number, cell: ICellData): void { this._ctx.save(); this._ctx.fillStyle = this._colors.cursor.css; this.fillBottomLineAtCells(x, y); this._ctx.restore(); } - private _renderBlurCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void { + private _renderBlurCursor(terminal: ITerminal, x: number, y: number, cell: ICellData): void { this._ctx.save(); this._ctx.strokeStyle = this._colors.cursor.css; - this.strokeRectAtCell(x, y, charData[CHAR_DATA_WIDTH_INDEX], 1); + this.strokeRectAtCell(x, y, cell.getWidth(), 1); this._ctx.restore(); } } diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index b8ef87aa..2c1b516a 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -85,6 +85,7 @@ export class Renderer extends EventEmitter implements IRenderer { this._isPaused = entry.intersectionRatio === 0; if (!this._isPaused && this._needsFullRefresh) { this._terminal.refresh(0, this._terminal.rows - 1); + this._needsFullRefresh = false; } } diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index ade2dd4c..f56ccf3a 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -3,13 +3,14 @@ * @license MIT */ -import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from '../Buffer'; +import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from '../Buffer'; import { FLAGS, IColorSet, IRenderDimensions, ICharacterJoinerRegistry } from './Types'; import { CharData, ITerminal } from '../Types'; import { INVERTED_DEFAULT_COLOR, DEFAULT_COLOR } from './atlas/Types'; import { GridCache } from './GridCache'; import { BaseRenderLayer } from './BaseRenderLayer'; import { is256Color } from './atlas/CharAtlasUtils'; +import { CellData } from '../BufferLine'; /** * This CharData looks like a null character, which will forc a clear and render @@ -24,6 +25,7 @@ export class TextRenderLayer extends BaseRenderLayer { private _characterFont: string; private _characterOverlapCache: { [key: string]: boolean } = {}; private _characterJoinerRegistry: ICharacterJoinerRegistry; + private _workCell = new CellData(); constructor(container: HTMLElement, zIndex: number, colors: IColorSet, characterJoinerRegistry: ICharacterJoinerRegistry, alpha: boolean) { super(container, 'text', zIndex, alpha, colors); @@ -72,14 +74,14 @@ export class TextRenderLayer extends BaseRenderLayer { const line = terminal.buffer.lines.get(row); const joinedRanges = joinerRegistry ? joinerRegistry.getJoinedCharacters(row) : []; for (let x = 0; x < terminal.cols; x++) { - const charData = line.get(x); - let code: number = charData[CHAR_DATA_CODE_INDEX] || WHITESPACE_CELL_CODE; + line.loadCell(x, this._workCell); + let code: number = this._workCell.getCode() || WHITESPACE_CELL_CODE; // Can either represent character(s) for a single cell or multiple cells // if indicated by a character joiner. - let chars: string = charData[CHAR_DATA_CHAR_INDEX] || WHITESPACE_CELL_CHAR; - const attr: number = charData[CHAR_DATA_ATTR_INDEX]; - let width: number = charData[CHAR_DATA_WIDTH_INDEX]; + let chars = this._workCell.getChars() || WHITESPACE_CELL_CHAR; + const attr = this._workCell.fg; + let width = this._workCell.getWidth(); // If true, indicates that the current character(s) to draw were joined. let isJoined = false; @@ -117,7 +119,7 @@ export class TextRenderLayer extends BaseRenderLayer { // right is a space, take ownership of the cell to the right. We skip // this check for joined characters because their rendering likely won't // yield the same result as rendering the last character individually. - if (!isJoined && this._isOverlapping(charData)) { + if (!isJoined && this._isOverlapping(chars, width, code)) { // If the character is overlapping, we want to force a re-render on every // frame. This is specifically to work around the case where two // overlaping chars `a` and `b` are adjacent, the cursor is moved to b and a @@ -125,7 +127,7 @@ export class TextRenderLayer extends BaseRenderLayer { // get removed, and `a` would not re-render because it thinks it's // already in the correct state. // this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA; - if (lastCharX < line.length - 1 && line.get(lastCharX + 1)[CHAR_DATA_CODE_INDEX] === NULL_CELL_CODE) { + if (lastCharX < line.length - 1 && line.loadCell(lastCharX + 1, this._workCell).getCode() === NULL_CELL_CODE) { width = 2; // this._clearChar(x + 1, y); // The overlapping char's char data will force a clear and render when the @@ -271,21 +273,19 @@ export class TextRenderLayer extends BaseRenderLayer { /** * Whether a character is overlapping to the next cell. */ - private _isOverlapping(charData: CharData): boolean { + private _isOverlapping(char: string, width: number, code: number): boolean { // Only single cell characters can be overlapping, rendering issues can // occur without this check - if (charData[CHAR_DATA_WIDTH_INDEX] !== 1) { + if (width !== 1) { return false; } // We assume that any ascii character will not overlap - const code = charData[CHAR_DATA_CODE_INDEX]; if (code < 256) { return false; } // Deliver from cache if available - const char = charData[CHAR_DATA_CHAR_INDEX]; if (this._characterOverlapCache.hasOwnProperty(char)) { return this._characterOverlapCache[char]; } diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index c5ef212d..78ccc620 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -9,7 +9,7 @@ import { ITheme } from 'xterm'; import { EventEmitter } from '../../common/EventEmitter'; import { ColorManager } from '../ColorManager'; import { RenderDebouncer } from '../../ui/RenderDebouncer'; -import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from './DomRendererRowFactory'; +import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from './DomRendererRowFactory'; import { INVERTED_DEFAULT_COLOR } from '../atlas/Types'; const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-'; @@ -75,7 +75,7 @@ export class DomRenderer extends EventEmitter implements IRenderer { this._updateDimensions(); this._renderDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this)); - this._rowFactory = new DomRendererRowFactory(document); + this._rowFactory = new DomRendererRowFactory(_terminal.options, document); this._terminal.element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass); this._terminal.screenElement.appendChild(this._rowContainer); @@ -165,12 +165,22 @@ export class DomRenderer extends EventEmitter implements IRenderer { `${this._terminalSelector} span.${ITALIC_CLASS} {` + ` font-style: italic;` + `}`; + // Blink animation + styles += + `@keyframes blink {` + + ` 0 % { opacity: 1.0; }` + + ` 50% { opacity: 0.0; }` + + ` 100 % { opacity: 1.0; }` + + `}`; // Cursor styles += `${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS} {` + ` outline: 1px solid ${this.colorManager.colors.cursor.css};` + ` outline-offset: -1px;` + `}` + + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS} {` + + ` animation: blink 1s step-end infinite;` + + `}` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` + ` background-color: ${this.colorManager.colors.cursor.css};` + ` color: ${this.colorManager.colors.cursorAccent.css};` + @@ -328,6 +338,7 @@ export class DomRenderer extends EventEmitter implements IRenderer { const cursorAbsoluteY = terminal.buffer.ybase + terminal.buffer.y; const cursorX = this._terminal.buffer.x; + const cursorBlink = this._terminal.options.cursorBlink; for (let y = start; y <= end; y++) { const rowElement = this._rowElements[y]; @@ -336,7 +347,7 @@ export class DomRenderer extends EventEmitter implements IRenderer { const row = y + terminal.buffer.ydisp; const lineData = terminal.buffer.lines.get(row); const cursorStyle = terminal.options.cursorStyle; - rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorStyle, cursorX, this.dimensions.actualCellWidth, terminal.cols)); + rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.actualCellWidth, terminal.cols)); } this._terminal.emit('refresh', {start, end}); diff --git a/src/renderer/dom/DomRendererRowFactory.test.ts b/src/renderer/dom/DomRendererRowFactory.test.ts index 67342da0..5ca008bc 100644 --- a/src/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/renderer/dom/DomRendererRowFactory.test.ts @@ -8,34 +8,39 @@ import { assert } from 'chai'; import { DomRendererRowFactory } from './DomRendererRowFactory'; import { DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from '../../Buffer'; import { FLAGS } from '../Types'; -import { BufferLine } from '../../BufferLine'; -import { IBufferLine } from '../../Types'; +import { BufferLine, CellData } from '../../BufferLine'; +import { IBufferLine, ITerminalOptions } from '../../Types'; import { DEFAULT_COLOR } from '../atlas/Types'; describe('DomRendererRowFactory', () => { let dom: jsdom.JSDOM; + const options: ITerminalOptions = {}; let rowFactory: DomRendererRowFactory; let lineData: IBufferLine; beforeEach(() => { dom = new jsdom.JSDOM(''); - rowFactory = new DomRendererRowFactory(dom.window.document); + + options.enableBold = true; + options.drawBoldTextInBrightColors = true; + + rowFactory = new DomRendererRowFactory(options, dom.window.document); lineData = createEmptyLineData(2); }); describe('createRow', () => { it('should not create anything for an empty row', () => { - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), '' ); }); it('should set correct attributes for double width characters', () => { - lineData.set(0, [DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)]); + lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)])); // There should be no element for the following "empty" cell - lineData.set(1, [DEFAULT_ATTR, '', 0, undefined]); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, '', 0, undefined])); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), '' ); @@ -43,17 +48,24 @@ describe('DomRendererRowFactory', () => { it('should add class for cursor and cursor style', () => { for (const style of ['block', 'bar', 'underline']) { - const fragment = rowFactory.createRow(lineData, true, style, 0, 5, 20); + const fragment = rowFactory.createRow(lineData, true, style, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), ` ` ); } }); + it('should add class for cursor blink', () => { + const fragment = rowFactory.createRow(lineData, true, 'block', 0, true, 5, 20); + assert.equal(getFragmentHtml(fragment), + ` ` + ); + }); + it('should not render cells that go beyond the terminal\'s columns', () => { - lineData.set(0, [DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]); - lineData.set(1, [DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 1); + lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)])); + lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)])); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 1); assert.equal(getFragmentHtml(fragment), 'a' ); @@ -61,16 +73,16 @@ describe('DomRendererRowFactory', () => { describe('attributes', () => { it('should add class for bold', () => { - lineData.set(0, [DEFAULT_ATTR | (FLAGS.BOLD << 18), 'a', 1, 'a'.charCodeAt(0)]); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR | (FLAGS.BOLD << 18), 'a', 1, 'a'.charCodeAt(0)])); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' ); }); it('should add class for italic', () => { - lineData.set(0, [DEFAULT_ATTR | (FLAGS.ITALIC << 18), 'a', 1, 'a'.charCodeAt(0)]); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR | (FLAGS.ITALIC << 18), 'a', 1, 'a'.charCodeAt(0)])); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' ); @@ -79,8 +91,8 @@ describe('DomRendererRowFactory', () => { it('should add classes for 256 foreground colors', () => { const defaultAttrNoFgColor = (0 << 9) | (DEFAULT_COLOR << 0); for (let i = 0; i < 256; i++) { - lineData.set(0, [defaultAttrNoFgColor | (i << 9), 'a', 1, 'a'.charCodeAt(0)]); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + lineData.setCell(0, CellData.fromCharData([defaultAttrNoFgColor | (i << 9), 'a', 1, 'a'.charCodeAt(0)])); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), `a` ); @@ -90,8 +102,8 @@ describe('DomRendererRowFactory', () => { it('should add classes for 256 background colors', () => { const defaultAttrNoBgColor = (DEFAULT_ATTR << 9) | (0 << 0); for (let i = 0; i < 256; i++) { - lineData.set(0, [defaultAttrNoBgColor | (i << 0), 'a', 1, 'a'.charCodeAt(0)]); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + lineData.setCell(0, CellData.fromCharData([defaultAttrNoBgColor | (i << 0), 'a', 1, 'a'.charCodeAt(0)])); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), `a` ); @@ -99,24 +111,24 @@ describe('DomRendererRowFactory', () => { }); it('should correctly invert colors', () => { - lineData.set(0, [(FLAGS.INVERSE << 18) | (2 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + lineData.setCell(0, CellData.fromCharData([(FLAGS.INVERSE << 18) | (2 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)])); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' ); }); it('should correctly invert default fg color', () => { - lineData.set(0, [(FLAGS.INVERSE << 18) | (DEFAULT_ATTR << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + lineData.setCell(0, CellData.fromCharData([(FLAGS.INVERSE << 18) | (DEFAULT_ATTR << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)])); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' ); }); it('should correctly invert default bg color', () => { - lineData.set(0, [(FLAGS.INVERSE << 18) | (1 << 9) | (DEFAULT_COLOR << 0), 'a', 1, 'a'.charCodeAt(0)]); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + lineData.setCell(0, CellData.fromCharData([(FLAGS.INVERSE << 18) | (1 << 9) | (DEFAULT_COLOR << 0), 'a', 1, 'a'.charCodeAt(0)])); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), 'a' ); @@ -124,8 +136,8 @@ describe('DomRendererRowFactory', () => { it('should turn bold fg text bright', () => { for (let i = 0; i < 8; i++) { - lineData.set(0, [(FLAGS.BOLD << 18) | (i << 9) | (DEFAULT_COLOR << 0), 'a', 1, 'a'.charCodeAt(0)]); - const fragment = rowFactory.createRow(lineData, false, undefined, 0, 5, 20); + lineData.setCell(0, CellData.fromCharData([(FLAGS.BOLD << 18) | (i << 9) | (DEFAULT_COLOR << 0), 'a', 1, 'a'.charCodeAt(0)])); + const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); assert.equal(getFragmentHtml(fragment), `a` ); @@ -143,7 +155,7 @@ describe('DomRendererRowFactory', () => { function createEmptyLineData(cols: number): IBufferLine { const lineData = new BufferLine(cols); for (let i = 0; i < cols; i++) { - lineData.set(i, [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); + lineData.setCell(i, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE])); } return lineData; } diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts index 8bcde39a..47232981 100644 --- a/src/renderer/dom/DomRendererRowFactory.ts +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -3,25 +3,30 @@ * @license MIT */ -import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CHAR } from '../../Buffer'; +import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR } from '../../Buffer'; import { FLAGS } from '../Types'; -import { IBufferLine } from '../../Types'; +import { IBufferLine, ITerminalOptions } from '../../Types'; import { DEFAULT_COLOR, INVERTED_DEFAULT_COLOR } from '../atlas/Types'; +import { CellData } from '../../BufferLine'; export const BOLD_CLASS = 'xterm-bold'; export const ITALIC_CLASS = 'xterm-italic'; export const CURSOR_CLASS = 'xterm-cursor'; +export const CURSOR_BLINK_CLASS = 'xterm-cursor-blink'; export const CURSOR_STYLE_BLOCK_CLASS = 'xterm-cursor-block'; export const CURSOR_STYLE_BAR_CLASS = 'xterm-cursor-bar'; export const CURSOR_STYLE_UNDERLINE_CLASS = 'xterm-cursor-underline'; export class DomRendererRowFactory { + private _workCell: CellData = new CellData(); + constructor( + private _terminalOptions: ITerminalOptions, private _document: Document ) { } - public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cellWidth: number, cols: number): DocumentFragment { + public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number): DocumentFragment { const fragment = this._document.createDocumentFragment(); // Find the line length first, this prevents the need to output a bunch of @@ -31,19 +36,16 @@ export class DomRendererRowFactory { // the viewport). let lineLength = 0; for (let x = Math.min(lineData.length, cols) - 1; x >= 0; x--) { - const charData = lineData.get(x); - const code = charData[CHAR_DATA_CODE_INDEX]; - if (code !== NULL_CELL_CODE || (isCursorRow && x === cursorX)) { + if (lineData.loadCell(x, this._workCell).getCode() !== NULL_CELL_CODE || (isCursorRow && x === cursorX)) { lineLength = x + 1; break; } } for (let x = 0; x < lineLength; x++) { - const charData = lineData.get(x); - const char = charData[CHAR_DATA_CHAR_INDEX] || WHITESPACE_CELL_CHAR; - const attr = charData[CHAR_DATA_ATTR_INDEX]; - const width = charData[CHAR_DATA_WIDTH_INDEX]; + lineData.loadCell(x, this._workCell); + const attr = this._workCell.fg; + const width = this._workCell.getWidth(); // The character to the left is a wide character, drawing is owned by the char at x-1 if (width === 0) { @@ -62,6 +64,10 @@ export class DomRendererRowFactory { if (isCursorRow && x === cursorX) { charElement.classList.add(CURSOR_CLASS); + if (cursorBlink) { + charElement.classList.add(CURSOR_BLINK_CLASS); + } + switch (cursorStyle) { case 'bar': charElement.classList.add(CURSOR_STYLE_BAR_CLASS); @@ -88,10 +94,10 @@ export class DomRendererRowFactory { } } - if (flags & FLAGS.BOLD) { + if (flags & FLAGS.BOLD && this._terminalOptions.enableBold) { // Convert the FG color to the bold variant. This should not happen when // the fg is the inverse default color as there is no bold variant. - if (fg < 8) { + if (fg < 8 && this._terminalOptions.drawBoldTextInBrightColors) { fg += 8; } charElement.classList.add(BOLD_CLASS); @@ -101,7 +107,7 @@ export class DomRendererRowFactory { charElement.classList.add(ITALIC_CLASS); } - charElement.textContent = char; + charElement.textContent = this._workCell.getChars() || WHITESPACE_CELL_CHAR; if (fg !== DEFAULT_COLOR) { charElement.classList.add(`xterm-fg-${fg}`); } diff --git a/src/tsconfig-base.json b/src/tsconfig-base.json new file mode 100644 index 00000000..5c6afcc5 --- /dev/null +++ b/src/tsconfig-base.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ "es5" ], + "rootDir": ".", + + "sourceMap": true, + "removeComments": true, + "pretty": true, + + "incremental": true, + + "skipLibCheck": true + } +} diff --git a/src/tsconfig-library-base.json b/src/tsconfig-library-base.json new file mode 100644 index 00000000..c82e0873 --- /dev/null +++ b/src/tsconfig-library-base.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig-base.json", + "compilerOptions": { + "types": [ + "../../node_modules/@types/mocha", + "../../" + ], + "composite": true, + "strict": true + } +} diff --git a/src/tsconfig.all.json b/src/tsconfig.all.json new file mode 100644 index 00000000..bee5df32 --- /dev/null +++ b/src/tsconfig.all.json @@ -0,0 +1,16 @@ +{ + "files": [], + "include": [], + "references": [ + { "path": "." }, + { "path": "./addons/attach" }, + { "path": "./addons/fit" }, + { "path": "./addons/fullscreen" }, + { "path": "./addons/search" }, + { "path": "./addons/terminado" }, + { "path": "./addons/webLinks" }, + { "path": "./addons/winptyCompat" }, + { "path": "./addons/zmodem" } + ] +} + \ No newline at end of file diff --git a/tsconfig.json b/src/tsconfig.json similarity index 53% rename from tsconfig.json rename to src/tsconfig.json index 2d1d6e35..0aa3abb8 100644 --- a/tsconfig.json +++ b/src/tsconfig.json @@ -1,7 +1,7 @@ { + "extends": "./tsconfig-base", "compilerOptions": { "module": "commonjs", - "target": "es5", "lib": [ "dom", "es5", @@ -9,19 +9,22 @@ "scripthost", "es2015.promise" ], - "rootDir": "src", - "outDir": "lib", - "sourceMap": true, - "removeComments": true, - "preserveWatchOutput": true, + "rootDir": ".", + "outDir": "../lib", + "noUnusedLocals": true, "noImplicitAny": true }, "include": [ - "src/**/*", - "typings/xterm.d.ts" + "./**/*", + "../typings/xterm.d.ts" ], "exclude": [ - "src/addons/**/*" + "./addons/**/*" + ], + "references": [ + { "path": "./common" }, + { "path": "./core" } ] } + \ No newline at end of file diff --git a/src/ui/MouseZoneManager.ts b/src/ui/MouseZoneManager.ts index 79022723..372dccc5 100644 --- a/src/ui/MouseZoneManager.ts +++ b/src/ui/MouseZoneManager.ts @@ -29,6 +29,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { private _tooltipTimeout: number = null; private _currentZone: IMouseZone = null; private _lastHoverCoords: [number, number] = [null, null]; + private _initialSelectionLength: number; constructor( private _terminal: ITerminal @@ -157,6 +158,10 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { } private _onMouseDown(e: MouseEvent): void { + // Store current terminal selection length, to check if we're performing + // a selection operation + this._initialSelectionLength = this._terminal.getSelection().length; + // Ignore the event if there are no zones active if (!this._areZonesActive) { return; @@ -186,9 +191,12 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { } private _onClick(e: MouseEvent): void { - // Find the active zone and click it if found + // Find the active zone and click it if found and no selection was + // being performed const zone = this._findZoneEventAt(e); - if (zone) { + const currentSelectionLength = this._terminal.getSelection().length; + + if (zone && currentSelectionLength === this._initialSelectionLength) { zone.clickCallback(e); e.preventDefault(); e.stopImmediatePropagation(); diff --git a/src/ui/TestUtils.test.ts b/src/ui/TestUtils.test.ts index e6e4aaa3..9d525fbf 100644 --- a/src/ui/TestUtils.test.ts +++ b/src/ui/TestUtils.test.ts @@ -4,7 +4,7 @@ */ import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Types'; -import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ILinkifier, IMouseHelper, ILinkMatcherOptions, CharacterJoinerHandler, IBufferLine, IBufferStringIterator } from '../Types'; +import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ILinkifier, IMouseHelper, ILinkMatcherOptions, CharacterJoinerHandler, IBufferLine, IBufferStringIterator, ICellData } from '../Types'; import { ICircularList, XtermListener } from '../common/Types'; import { Buffer } from '../Buffer'; import * as Browser from '../core/Platform'; @@ -334,6 +334,12 @@ export class MockBuffer implements IBuffer { iterator(trimRight: boolean, startIndex?: number, endIndex?: number): IBufferStringIterator { return Buffer.prototype.iterator.apply(this, arguments); } + getNullCell(fg: number = 0, bg: number = 0): ICellData { + throw new Error('Method not implemented.'); + } + getWhitespaceCell(fg: number = 0, bg: number = 0): ICellData { + throw new Error('Method not implemented.'); + } } export class MockRenderer implements IRenderer { diff --git a/yarn.lock b/yarn.lock index 5555321d..440aa4f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1275,11 +1275,6 @@ combined-stream@1.0.6, combined-stream@~1.0.5: dependencies: delayed-stream "~1.0.0" -commander@2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.6.0.tgz#9df7e52fb2a0cb0fb89058ee80c3104225f37e1d" - integrity sha1-nfflL7Kgyw+4kFjugMMQQiXzfh0= - commander@2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" @@ -1338,21 +1333,6 @@ concat-with-sourcemaps@^1.0.0: dependencies: source-map "^0.6.1" -concurrently@^3.5.1: - version "3.6.0" - resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-3.6.0.tgz#c25e34b156a9d5bd4f256a0d85f6192438ae481f" - integrity sha512-6XiIYtYzmGEccNZFkih5JOH92jLA4ulZArAYy5j1uDSdrPLB3KzdE8GW7t2fHPcg9ry2+5LP9IEYzXzxw9lFdA== - dependencies: - chalk "^2.4.1" - commander "2.6.0" - date-fns "^1.23.0" - lodash "^4.5.1" - read-pkg "^3.0.0" - rx "2.3.24" - spawn-command "^0.0.2-1" - supports-color "^3.2.3" - tree-kill "^1.1.0" - configstore@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/configstore/-/configstore-1.4.0.tgz#c35781d0501d268c25c54b8b17f6240e8a4fb021" @@ -1595,11 +1575,6 @@ data-urls@^1.0.0: whatwg-mimetype "^2.0.0" whatwg-url "^6.4.0" -date-fns@^1.23.0: - version "1.29.0" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6" - integrity sha512-lbTXWZ6M20cWH8N9S6afb0SBm6tMk+uUg6z3MqHPKE9atmsY3kJkTm8vKe93izJ2B2+q5MV990sM2CHgtAZaOw== - date-now@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" @@ -1933,7 +1908,7 @@ errno@^0.1.3, errno@~0.1.7: dependencies: prr "~1.0.1" -error-ex@^1.2.0, error-ex@^1.3.1: +error-ex@^1.2.0: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== @@ -3625,7 +3600,7 @@ jsesc@^1.3.0: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" integrity sha1-RsP+yMGJKxKwgz25vHYiF226s0s= -json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: +json-parse-better-errors@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== @@ -3815,16 +3790,6 @@ load-json-file@^1.0.0: pinkie-promise "^2.0.0" strip-bom "^2.0.0" -load-json-file@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" - integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= - dependencies: - graceful-fs "^4.1.2" - parse-json "^4.0.0" - pify "^3.0.0" - strip-bom "^3.0.0" - loader-runner@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" @@ -4037,7 +4002,7 @@ lodash.templatesettings@^3.0.0: lodash._reinterpolate "^3.0.0" lodash.escape "^3.0.0" -lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.4, lodash@^4.5.1: +lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.4: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" integrity sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg== @@ -4984,14 +4949,6 @@ parse-json@^2.2.0: dependencies: error-ex "^1.2.0" -parse-json@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" - integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= - dependencies: - error-ex "^1.3.1" - json-parse-better-errors "^1.0.1" - parse-passwd@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" @@ -5092,13 +5049,6 @@ path-type@^1.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" -path-type@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" - integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== - dependencies: - pify "^3.0.0" - pause-stream@0.0.11: version "0.0.11" resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" @@ -5384,15 +5334,6 @@ read-pkg@^1.0.0: normalize-package-data "^2.3.2" path-type "^1.0.0" -read-pkg@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" - integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= - dependencies: - load-json-file "^4.0.0" - normalize-package-data "^2.3.2" - path-type "^3.0.0" - "readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" @@ -5688,11 +5629,6 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" -rx@2.3.24: - version "2.3.24" - resolved "https://registry.yarnpkg.com/rx/-/rx-2.3.24.tgz#14f950a4217d7e35daa71bbcbe58eff68ea4b2b7" - integrity sha1-FPlQpCF9fjXapxu8vljv9o6ksrc= - rxjs@^6.1.0: version "6.3.1" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.3.1.tgz#878a1a8c64b8a5da11dcf74b5033fe944cdafb84" @@ -6025,11 +5961,6 @@ sparkles@^1.0.0: resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.1.tgz#008db65edce6c50eec0c5e228e1945061dd0437c" integrity sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw== -spawn-command@^0.0.2-1: - version "0.0.2-1" - resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0" - integrity sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A= - spawn-wrap@^1.4.2: version "1.4.2" resolved "https://registry.yarnpkg.com/spawn-wrap/-/spawn-wrap-1.4.2.tgz#cff58e73a8224617b6561abdc32586ea0c82248c" @@ -6271,11 +6202,6 @@ strip-bom@^1.0.0: first-chunk-stream "^1.0.0" is-utf8 "^0.2.0" -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= - strip-eof@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" @@ -6305,7 +6231,7 @@ supports-color@^2.0.0: resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= -supports-color@^3.1.2, supports-color@^3.2.3: +supports-color@^3.1.2: version "3.2.3" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" integrity sha1-ZawFBLOVQXHYpklGsq48u4pfVPY= @@ -6521,11 +6447,6 @@ tr46@^1.0.1: dependencies: punycode "^2.1.0" -tree-kill@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.0.tgz#5846786237b4239014f05db156b643212d4c6f36" - integrity sha512-DlX6dR0lOIRDFxI0mjL9IYg6OTncLm/Zt+JiBhE5OlFcAR8yc9S7FFXU9so0oda47frdM/JFsk7UjNt9vscKcg== - trim-right@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" @@ -6633,10 +6554,10 @@ typedarray@^0.0.6, typedarray@~0.0.5: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@3.1: - version "3.1.6" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.1.6.tgz#b6543a83cfc8c2befb3f4c8fba6896f5b0c9be68" - integrity sha512-tDMYfVtvpb96msS1lDX9MEdHrW4yOuZ4Kdc4Him9oU796XldPYF/t2+uKoX0BBa0hXXwDlqYQbXY5Rzjzc5hBA== +typescript@3.4: + version "3.4.1" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.1.tgz#b6691be11a881ffa9a05765a205cb7383f3b63c6" + integrity sha512-3NSMb2VzDQm8oBTLH6Nj55VVtUEpe/rgkIzMir0qVoLyjDZlnMBva0U6vDiV3IH+sl/Yu6oP5QwsAQtHPmDd2Q== uglify-es@^3.3.4: version "3.3.9"