From 43cb4f471fb69ec1bd7d5f0bf99fe867c8056d56 Mon Sep 17 00:00:00 2001 From: yutaka Date: Fri, 3 Mar 2017 07:57:05 +0000 Subject: [PATCH 01/26] Set charMeasure.height values to each row height --- src/xterm.js | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index a08e9dd14..8385b601f 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -562,6 +562,7 @@ Terminal.bindKeys = function(term) { Terminal.prototype.insertRow = function (row) { if (typeof row != 'object') { row = document.createElement('div'); + row.style.height = this.charMeasure.height + 'px'; } this.rowContainer.appendChild(row); @@ -642,17 +643,18 @@ Terminal.prototype.open = function(parent) { this.charSizeStyleElement = document.createElement('style'); this.helperContainer.appendChild(this.charSizeStyleElement); + this.charMeasure = new CharMeasure(document, this.helperContainer); + this.charMeasure.on('charsizechanged', function () { + self.updateCharSizeCSS(); + self.updateRowHeight(); + }); + this.charMeasure.measure(); + for (; i < this.rows; i++) { this.insertRow(); } this.parent.appendChild(this.element); - this.charMeasure = new CharMeasure(document, this.helperContainer); - this.charMeasure.on('charsizechanged', function () { - self.updateCharSizeCSS(); - }); - this.charMeasure.measure(); - this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure); this.renderer = new Renderer(this); @@ -714,6 +716,15 @@ Terminal.prototype.updateCharSizeCSS = function() { this.charSizeStyleElement.textContent = '.xterm-wide-char{width:' + (this.charMeasure.width * 2) + 'px;}'; } +/** + * Updates the height for each rows + */ +Terminal.prototype.updateRowHeight = function() { + for (var i = 0; i < this.children.length; ++i) { + this.children[i].style.height = this.charMeasure.height + 'px'; + } +} + /** * XTerm mouse events * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking From dc3a136704e1a33aac72317e8385258399e94da6 Mon Sep 17 00:00:00 2001 From: Aleksandr Andrienko Date: Tue, 28 Feb 2017 11:30:47 +0200 Subject: [PATCH 02/26] Use tsconfig.json data in gulpfile.js instead of hardcoded values. Use tsconfig.json data in gulpfile.js instead of hardcoded values. Little code clean up Signed-off-by: Aleksandr Andrienko --- gulpfile.js | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 2e58d26cb..feaa3dcf8 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -13,27 +13,28 @@ const ts = require('gulp-typescript'); let buildDir = process.env.BUILD_DIR || 'build'; - +let tsProject = ts.createProject('tsconfig.json'); +let srcDir = tsProject.config.compilerOptions.rootDir; +let outDir = tsProject.config.compilerOptions.outDir; /** * Compile TypeScript sources to JavaScript files and create a source map file for each TypeScript * file compiled. */ gulp.task('tsc', function () { - // Remove the lib/ directory to prevent confusion if files were deleted in src/ - fs.emptyDirSync('lib'); + // Remove the ${outDir}/ directory to prevent confusion if files were deleted in ${srcDir}/ + fs.emptyDirSync(`${outDir}`); - // Build all TypeScript files (including tests) to lib/, based on the configuration defined in + // Build all TypeScript files (including tests) to ${outDir}/, based on the configuration defined in // `tsconfig.json`. - let tsProject = ts.createProject('tsconfig.json'); let tsResult = tsProject.src().pipe(sourcemaps.init()).pipe(tsProject()); - let tsc = tsResult.js.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest('lib')); + let tsc = tsResult.js.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest(outDir)); - // Copy all addons from src/ to lib/ - let copyAddons = gulp.src('src/addons/**/*').pipe(gulp.dest('lib/addons')); + // Copy all addons from ${srcDir}/ to ${outDir}/ + let copyAddons = gulp.src(`${srcDir}/addons/**/*`).pipe(gulp.dest(`${outDir}/addons`)); - // Copy stylesheets from src/ to lib/ - let copyStylesheets = gulp.src('src/**/*.css').pipe(gulp.dest('lib')); + // Copy stylesheets from ${srcDir}/ to ${outDir}/ + let copyStylesheets = gulp.src(`${srcDir}/**/*.css`).pipe(gulp.dest(outDir)); return merge(tsc, copyAddons, copyStylesheets); }); @@ -49,7 +50,7 @@ gulp.task('browserify', ['tsc'], function() { let browserifyOptions = { basedir: buildDir, debug: true, - entries: ['../lib/xterm.js'], + entries: [`../${outDir}/xterm.js`], standalone: 'Terminal', cache: {}, packageCache: {} @@ -62,17 +63,17 @@ gulp.task('browserify', ['tsc'], function() { .pipe(sourcemaps.write('./')) .pipe(gulp.dest(buildDir)); - // Copy all add-ons from lib/ to buildDir - let copyAddons = gulp.src('lib/addons/**/*').pipe(gulp.dest(`${buildDir}/addons`)); + // Copy all add-ons from ${outDir}/ to buildDir + let copyAddons = gulp.src(`${outDir}/addons/**/*`).pipe(gulp.dest(`${buildDir}/addons`)); - // Copy stylesheets from src/ to lib/ - let copyStylesheets = gulp.src('lib/**/*.css').pipe(gulp.dest(buildDir)); + // Copy stylesheets from ${outDir}/ to ${buildDir}/ + let copyStylesheets = gulp.src(`${outDir}/**/*.css`).pipe(gulp.dest(buildDir)); return merge(bundleStream, copyAddons, copyStylesheets); }); gulp.task('instrument-test', function () { - return gulp.src(['lib/**/*.js']) + return gulp.src([`${outDir}/**/*.js`]) // Covering files .pipe(istanbul()) // Force `require` to return covered files @@ -80,7 +81,7 @@ gulp.task('instrument-test', function () { }); gulp.task('mocha', ['instrument-test'], function () { - return gulp.src(['lib/*test.js', 'lib/**/*test.js'], {read: false}) + return gulp.src([`${outDir}/*test.js`, `${outDir}/**/*test.js`], {read: false}) .pipe(mocha()) .pipe(istanbul.writeReports()); }); @@ -88,11 +89,11 @@ gulp.task('mocha', ['instrument-test'], function () { /** * Use `sorcery` to resolve the source map chain and point back to the TypeScript files. * (Without this task the source maps produced for the JavaScript bundle points into the - * compiled JavaScript files in lib/). + * compiled JavaScript files in ${outDir}/). */ gulp.task('sorcery', ['browserify'], function () { var chain = sorcery.loadSync(`${buildDir}/xterm.js`); - var map = chain.apply(); + chain.apply(); chain.writeSync(); }); From 31a0996b2f6084621bb18dbd836dbadad91433ea Mon Sep 17 00:00:00 2001 From: yutaka Date: Wed, 8 Mar 2017 00:27:22 +0000 Subject: [PATCH 03/26] Revert "Set charMeasure.height values to each row height" This reverts commit 43cb4f471fb69ec1bd7d5f0bf99fe867c8056d56. --- src/xterm.js | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index 8385b601f..a08e9dd14 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -562,7 +562,6 @@ Terminal.bindKeys = function(term) { Terminal.prototype.insertRow = function (row) { if (typeof row != 'object') { row = document.createElement('div'); - row.style.height = this.charMeasure.height + 'px'; } this.rowContainer.appendChild(row); @@ -643,18 +642,17 @@ Terminal.prototype.open = function(parent) { this.charSizeStyleElement = document.createElement('style'); this.helperContainer.appendChild(this.charSizeStyleElement); - this.charMeasure = new CharMeasure(document, this.helperContainer); - this.charMeasure.on('charsizechanged', function () { - self.updateCharSizeCSS(); - self.updateRowHeight(); - }); - this.charMeasure.measure(); - for (; i < this.rows; i++) { this.insertRow(); } this.parent.appendChild(this.element); + this.charMeasure = new CharMeasure(document, this.helperContainer); + this.charMeasure.on('charsizechanged', function () { + self.updateCharSizeCSS(); + }); + this.charMeasure.measure(); + this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure); this.renderer = new Renderer(this); @@ -716,15 +714,6 @@ Terminal.prototype.updateCharSizeCSS = function() { this.charSizeStyleElement.textContent = '.xterm-wide-char{width:' + (this.charMeasure.width * 2) + 'px;}'; } -/** - * Updates the height for each rows - */ -Terminal.prototype.updateRowHeight = function() { - for (var i = 0; i < this.children.length; ++i) { - this.children[i].style.height = this.charMeasure.height + 'px'; - } -} - /** * XTerm mouse events * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking From f0d5b4012134a617408c38eb98382c1ef36ebe17 Mon Sep 17 00:00:00 2001 From: yutaka Date: Wed, 8 Mar 2017 01:03:18 +0000 Subject: [PATCH 04/26] Detect bold font was broken correctly. --- src/Renderer.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Renderer.ts b/src/Renderer.ts index 9b6234caf..e1ebbe8f0 100644 --- a/src/Renderer.ts +++ b/src/Renderer.ts @@ -34,7 +34,7 @@ export class Renderer { // Figure out whether boldness affects // the character width of monospace fonts. if (brokenBold === null) { - brokenBold = checkBoldBroken((this._terminal).document); + brokenBold = checkBoldBroken((this._terminal).element); } // TODO: Pull more DOM interactions into Renderer.constructor, element for @@ -291,14 +291,14 @@ export class Renderer { // if bold is broken, we can't // use it in the terminal. -function checkBoldBroken(document) { - const body = document.getElementsByTagName('body')[0]; +function checkBoldBroken(terminal) { + const document = terminal.ownerDocument; const el = document.createElement('span'); el.innerHTML = 'hello world'; - body.appendChild(el); + terminal.appendChild(el); const w1 = el.scrollWidth; el.style.fontWeight = 'bold'; const w2 = el.scrollWidth; - body.removeChild(el); + terminal.removeChild(el); return w1 !== w2; } From 2ec756fd6bfadf2689d1c243960efaa01a7f93a4 Mon Sep 17 00:00:00 2001 From: yutaka Date: Wed, 8 Mar 2017 00:44:57 +0000 Subject: [PATCH 05/26] Use offsetWidth instead of scrollWidth scrollWidth does not work on Chrome with `display: inline` element. --- src/Renderer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Renderer.ts b/src/Renderer.ts index e1ebbe8f0..a839e7d91 100644 --- a/src/Renderer.ts +++ b/src/Renderer.ts @@ -296,9 +296,9 @@ function checkBoldBroken(terminal) { const el = document.createElement('span'); el.innerHTML = 'hello world'; terminal.appendChild(el); - const w1 = el.scrollWidth; + const w1 = el.offsetWidth; el.style.fontWeight = 'bold'; - const w2 = el.scrollWidth; + const w2 = el.offsetWidth; terminal.removeChild(el); return w1 !== w2; } From 4b2ae6a79fb0f899e895093ef440ac9977210d8f Mon Sep 17 00:00:00 2001 From: yutaka Date: Wed, 8 Mar 2017 05:12:25 +0000 Subject: [PATCH 06/26] Use rendered height for detect broken bold font. --- src/Renderer.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Renderer.ts b/src/Renderer.ts index a839e7d91..4131abc4b 100644 --- a/src/Renderer.ts +++ b/src/Renderer.ts @@ -297,8 +297,10 @@ function checkBoldBroken(terminal) { el.innerHTML = 'hello world'; terminal.appendChild(el); const w1 = el.offsetWidth; + const h1 = el.offsetHeight; el.style.fontWeight = 'bold'; const w2 = el.offsetWidth; + const h2 = el.offsetHeight; terminal.removeChild(el); - return w1 !== w2; + return w1 !== w2 || h1 !== h2; } From 3c3a646d423caad334b2086e76d2ccdf34f6d4f7 Mon Sep 17 00:00:00 2001 From: Michael Irwin Date: Tue, 14 Mar 2017 17:02:53 -0400 Subject: [PATCH 07/26] Fixed characterHeight calculation, which could cause overflowing The previous calculation was simply using the height of the letter W, but wasn't taking into account that each row has a lineHeight, which might be greater. If the lineHeight is .4px off, after many rows, it will cause the proposedGeometry to have an extra row, potentially causing hidden rows (based on layout). --- src/addons/fit/fit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/addons/fit/fit.js b/src/addons/fit/fit.js index 11fc6eb79..390cc0e93 100644 --- a/src/addons/fit/fit.js +++ b/src/addons/fit/fit.js @@ -56,7 +56,7 @@ subjectRow.innerHTML = 'W'; // Common character for measuring width, although on monospace characterWidth = subjectRow.getBoundingClientRect().width; subjectRow.style.display = ''; // Revert style before calculating height, since they differ. - characterHeight = parseInt(subjectRow.offsetHeight); + characterHeight = parseFloat(term.rowContainer.style.lineHeight); subjectRow.innerHTML = contentBuffer; rows = parseInt(availableHeight / characterHeight); From 8cf76cc6e93d8a109a59d7315405dca3150e134e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 14 Mar 2017 15:15:36 -0700 Subject: [PATCH 08/26] Polish development and contribution section --- README.md | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/README.md b/README.md index 84b116dfa..9c22a777f 100644 --- a/README.md +++ b/README.md @@ -121,27 +121,7 @@ The existing releases are available at this GitHub repo's [Releases](https://git Xterm.js is maintained by [SourceLair](https://www.sourcelair.com/) and a few external contributors, but we would love to receive contributions from everyone! -To contribute either code, documentation or issues to xterm.js please read the [Contributing document](CONTRIBUTING.md) before. - -The development of xterm.js does not require any special tool. All you need is an editor that supports JavaScript and a browser (if you would like to run the demo you will need Node.js to get all features). - -It is recommended though to use a development tool that uses xterm.js internally, to develop for xterm.js. [Eating our own dogfood](https://en.wikipedia.org/wiki/Eating_your_own_dog_food) has been proved extremely beneficial for this project. Known tools that use xterm.js internally are: - -#### [SourceLair](https://www.sourcelair.com) - -Visit https://lair.io/sourcelair/xterm and follow the instructions. All development will happen in your browser. - -#### [Visual Studio Code](http://code.visualstudio.com/) - -[Download Visual Studio Code](http://code.visualstudio.com/Download), clone xterm.js and you are all set. - -#### [Eclipse Che](http://www.eclipse.org/che) - -You can start Eclipse Che with `docker run eclipse/che start`. - -#### [Codenvy](http://www.codenvy.io) - -You can create a trial account or install an enterprise version with `docker run codenvy/cli start`. +To contribute either code, documentation or issues to xterm.js please read the [Contributing document](CONTRIBUTING.md) beforehand. The development of xterm.js does not require any special tool. All you need is an editor that supports JavaScript/TypeScript and a browser. You will need Node.js installed locally to get all the features working in the demo. ## License Agreement From 0cb8ecc6f72638aa65f6c04fc5fd5a241f2f482c Mon Sep 17 00:00:00 2001 From: Michael Irwin Date: Wed, 15 Mar 2017 08:28:44 -0400 Subject: [PATCH 09/26] Use boundingClientRect instead of style to get actual height PR #598 --- src/addons/fit/fit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/addons/fit/fit.js b/src/addons/fit/fit.js index 390cc0e93..46b79e9b3 100644 --- a/src/addons/fit/fit.js +++ b/src/addons/fit/fit.js @@ -56,7 +56,7 @@ subjectRow.innerHTML = 'W'; // Common character for measuring width, although on monospace characterWidth = subjectRow.getBoundingClientRect().width; subjectRow.style.display = ''; // Revert style before calculating height, since they differ. - characterHeight = parseFloat(term.rowContainer.style.lineHeight); + characterHeight = subjectRow.getBoundingClientRect().height; subjectRow.innerHTML = contentBuffer; rows = parseInt(availableHeight / characterHeight); From 9055bace966d7ef6862d1596a369ee306f00dbaf Mon Sep 17 00:00:00 2001 From: Paris Kasidiaris Date: Wed, 15 Mar 2017 18:28:17 +0200 Subject: [PATCH 10/26] Fix cursor style on blurred terminals --- src/xterm.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xterm.css b/src/xterm.css index b7f120f9c..aac95444e 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -86,7 +86,7 @@ text-decoration: none; } -.terminal:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar) .terminal-cursor { +.terminal.focus:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar) .terminal-cursor { background-color: #fff; color: #000; } From 5225a6e27a54314d28564296bf60a8e59d969037 Mon Sep 17 00:00:00 2001 From: Paris Kasidiaris Date: Wed, 15 Mar 2017 18:36:06 +0200 Subject: [PATCH 11/26] Rebuild xterm.js even when CSS changes Otherwise you had to run `npm build` manually every time you changed a CSS file to see the updated result in the demo. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 27579a0d3..c1d07e3c4 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "scripts": { "prestart": "npm run build", "start": "node demo/app", - "dev": "nodemon -e js,ts --watch src --watch demo --exec npm start", + "dev": "nodemon -e js,ts,css --watch src --watch demo --exec npm start", "lint": "tslint src/*.ts src/**/*.ts", "test": "gulp test", "build:docs": "jsdoc -c jsdoc.json", From b68180b97bde51b2cb85923921eee2e35e0c79ed Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 15 Mar 2017 13:54:36 -0700 Subject: [PATCH 12/26] Allow ~ char in linkify path and query fragments --- src/Linkifier.test.ts | 14 ++++++++++++++ src/Linkifier.ts | 4 ++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index 012b1b686..ca393c217 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -49,6 +49,20 @@ describe('Linkifier', () => { element.dispatchEvent(event); } + function assertLinkifiesEntireRow(uri: string, done: MochaDone) { + addRow(uri); + linkifier.linkifyRow(0); + setTimeout(() => { + assert.equal((rows[0].firstChild).tagName, 'A'); + assert.equal((rows[0].firstChild).textContent, uri); + done(); + }, 0); + } + + describe('http links', () => { + it('should allow ~ character in URI path', done => assertLinkifiesEntireRow('http://foo.com/a~b#c~d?e~f', done)); + }); + describe('validationCallback', () => { it('should enable link if true', done => { addRow('test'); diff --git a/src/Linkifier.ts b/src/Linkifier.ts index a34edbc2f..df7b08a36 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -16,8 +16,8 @@ const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})'; const localHostClause = '(localhost)'; const portClause = '(:\\d{1,5})'; const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?'; -const pathClause = '(\\/[\\/\\w\\.\\-%]*)*'; -const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;\\=\\.\\-]*'; +const pathClause = '(\\/[\\/\\w\\.\\-%~]*)*'; +const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*'; const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?'; const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?'; const negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+'; From 22cc8079dc17437490189c08fc3bd895e3815975 Mon Sep 17 00:00:00 2001 From: Paris Kasidiaris Date: Thu, 16 Mar 2017 10:53:56 +0200 Subject: [PATCH 13/26] Add xterm.js logo --- README.md | 2 +- logo.png | Bin 0 -> 8446 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 logo.png diff --git a/README.md b/README.md index 9c22a777f..af98ed585 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# xterm.js +# [![xterm.js logo](logo.png)](https://xtermjs.org) [![xterm.js build status](https://api.travis-ci.org/sourcelair/xterm.js.svg)](https://travis-ci.org/sourcelair/xterm.js) [![Coverage Status](https://coveralls.io/repos/github/sourcelair/xterm.js/badge.svg)](https://coveralls.io/github/sourcelair/xterm.js) [![Gitter](https://badges.gitter.im/sourcelair/xterm.js.svg)](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c16ac7260d4fca251eb2a09b1d1b0af77a39a3f4 GIT binary patch literal 8446 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 Date: Thu, 16 Mar 2017 12:07:25 -0700 Subject: [PATCH 14/26] Ensure terminal links are not draggable --- src/Linkifier.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index a34edbc2f..e0e2e7c44 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -232,6 +232,7 @@ export class Linkifier { private _createAnchorElement(uri: string, handler: LinkMatcherHandler, isHypertextLinkHandler: boolean): HTMLAnchorElement { const element = this._document.createElement('a'); element.textContent = uri; + element.draggable = false; if (isHypertextLinkHandler) { element.href = uri; // Force link on another tab so work is not lost From 365f9b4c1bb0a6175d005b1adc8853d4cf513d57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgar=20Andr=C3=A9s=20Margffoy=20Tuay?= Date: Sat, 18 Mar 2017 18:05:05 -0500 Subject: [PATCH 15/26] Added Spyder IDE as a xterm case-use application --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index af98ed585..28d1ac477 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**Codenvy**](http://www.codenvy.com): Cloud workspaces for development teams. - [**CoderPad**](https://coderpad.io): Online interviewing platform for programmers. Run code in many programming languages, with results displayed by `xterm.js`. - [**WebSSH2**](https://github.com/billchurch/WebSSH2): A web based SSH2 client using `xterm.js`, socket.io, and ssh2. +- [**Spyder Terminal**](https://github.com/spyder-ide/spyder-terminal): A full fledged system terminal embedded on Spyder IDE. Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list. From cb77b9dc1a07ae4549d09da4c81307c1fd1201cb Mon Sep 17 00:00:00 2001 From: Lucian Buzzo Date: Sun, 19 Mar 2017 08:26:37 +0000 Subject: [PATCH 16/26] Prevent line data from being discarded on resize. --- src/xterm.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index 37c00ed85..830b8f846 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -1805,14 +1805,8 @@ Terminal.prototype.resize = function(x, y) { this.lines.get(i).push(ch); } } - } else { // (j > x) - i = this.lines.length; - while (i--) { - while (this.lines.get(i).length > x) { - this.lines.get(i).pop(); - } - } } + this.cols = x; this.setupStops(this.cols); From b0624cadb7ca5756c260a2857833ffd831cbfb2f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 24 Mar 2017 11:04:04 -0700 Subject: [PATCH 17/26] Allow links to be registered before terminal is attached to DOM Fixes #618 --- src/Linkifier.test.ts | 154 +++++++++++++++++++++++------------------- src/Linkifier.ts | 19 +++++- src/xterm.js | 4 +- 3 files changed, 103 insertions(+), 74 deletions(-) diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index ca393c217..36f825baa 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -8,9 +8,9 @@ import { Linkifier } from './Linkifier'; import { LinkMatcher } from './Types'; class TestLinkifier extends Linkifier { - constructor(document: Document, rows: HTMLElement[]) { + constructor() { Linkifier.TIME_BEFORE_LINKIFY = 0; - super(document, rows); + super(); } public get linkMatchers(): LinkMatcher[] { return this._linkMatchers; } @@ -25,89 +25,105 @@ describe('Linkifier', () => { let linkifier: TestLinkifier; beforeEach(done => { - rows = []; jsdom.env('', (err, w) => { window = w; document = window.document; - linkifier = new TestLinkifier(document, rows); - container = document.createElement('div'); - document.body.appendChild(container); + linkifier = new TestLinkifier(); done(); }); }); - function addRow(text: string) { - const element = document.createElement('div'); - element.textContent = text; - container.appendChild(element); - rows.push(element); - } - - function clickElement(element: Node) { - const event = document.createEvent('MouseEvent'); - event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); - element.dispatchEvent(event); - } - - function assertLinkifiesEntireRow(uri: string, done: MochaDone) { - addRow(uri); - linkifier.linkifyRow(0); - setTimeout(() => { - assert.equal((rows[0].firstChild).tagName, 'A'); - assert.equal((rows[0].firstChild).textContent, uri); + describe('before attachToDom', () => { + it('should allow link matcher registration', done => { + assert.doesNotThrow(() => { + const linkMatcherId = linkifier.registerLinkMatcher(/foo/, () => {}); + assert.isTrue(linkifier.deregisterLinkMatcher(linkMatcherId)); done(); - }, 0); - } - - describe('http links', () => { - it('should allow ~ character in URI path', done => assertLinkifiesEntireRow('http://foo.com/a~b#c~d?e~f', done)); - }); - - describe('validationCallback', () => { - it('should enable link if true', done => { - addRow('test'); - linkifier.registerLinkMatcher(/test/, () => done(), { - validationCallback: (url, cb) => { - cb(true); - assert.equal((rows[0].firstChild).tagName, 'A'); - setTimeout(() => clickElement(rows[0].firstChild), 0); - } }); - linkifier.linkifyRow(0); - }); - - it('should disable link if false', done => { - addRow('test'); - linkifier.registerLinkMatcher(/test/, () => assert.fail(), { - validationCallback: (url, cb) => { - cb(false); - assert.equal((rows[0].firstChild).tagName, 'A'); - setTimeout(() => clickElement(rows[0].firstChild), 0); - } - }); - linkifier.linkifyRow(0); - // Allow time for the click to be performed - setTimeout(() => done(), 10); }); }); - describe('priority', () => { - it('should order the list from highest priority to lowest #1', () => { - const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 1 }); - const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: -1 }); - assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, 0, bId]); + describe('after attachToDom', () => { + beforeEach(() => { + rows = []; + linkifier.attachToDom(document, rows); + container = document.createElement('div'); + document.body.appendChild(container); }); - it('should order the list from highest priority to lowest #2', () => { - const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: -1 }); - const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 1 }); - assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [bId, 0, aId]); + function addRow(text: string) { + const element = document.createElement('div'); + element.textContent = text; + container.appendChild(element); + rows.push(element); + } + + function clickElement(element: Node) { + const event = document.createEvent('MouseEvent'); + event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); + element.dispatchEvent(event); + } + + function assertLinkifiesEntireRow(uri: string, done: MochaDone) { + addRow(uri); + linkifier.linkifyRow(0); + setTimeout(() => { + assert.equal((rows[0].firstChild).tagName, 'A'); + assert.equal((rows[0].firstChild).textContent, uri); + done(); + }, 0); + } + + describe('http links', () => { + it('should allow ~ character in URI path', done => assertLinkifiesEntireRow('http://foo.com/a~b#c~d?e~f', done)); }); - it('should order items of equal priority in the order they are added', () => { - const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 0 }); - const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 0 }); - assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [0, aId, bId]); + describe('validationCallback', () => { + it('should enable link if true', done => { + addRow('test'); + linkifier.registerLinkMatcher(/test/, () => done(), { + validationCallback: (url, cb) => { + cb(true); + assert.equal((rows[0].firstChild).tagName, 'A'); + setTimeout(() => clickElement(rows[0].firstChild), 0); + } + }); + linkifier.linkifyRow(0); + }); + + it('should disable link if false', done => { + addRow('test'); + linkifier.registerLinkMatcher(/test/, () => assert.fail(), { + validationCallback: (url, cb) => { + cb(false); + assert.equal((rows[0].firstChild).tagName, 'A'); + setTimeout(() => clickElement(rows[0].firstChild), 0); + } + }); + linkifier.linkifyRow(0); + // Allow time for the click to be performed + setTimeout(() => done(), 10); + }); + }); + + describe('priority', () => { + it('should order the list from highest priority to lowest #1', () => { + const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 1 }); + const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: -1 }); + assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, 0, bId]); + }); + + it('should order the list from highest priority to lowest #2', () => { + const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: -1 }); + const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 1 }); + assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [bId, 0, aId]); + }); + + it('should order items of equal priority in the order they are added', () => { + const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 0 }); + const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 0 }); + assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [0, aId, bId]); + }); }); }); }); diff --git a/src/Linkifier.ts b/src/Linkifier.ts index d9a169407..c263496a4 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -49,19 +49,32 @@ export class Linkifier { private _rowTimeoutIds: number[]; private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID; - constructor(document: Document, rows: HTMLElement[]) { - this._document = document; - this._rows = rows; + constructor() { this._rowTimeoutIds = []; this._linkMatchers = []; this.registerLinkMatcher(strictUrlRegex, null, { matchIndex: 1 }); } + /** + * Attaches the linkifier to the DOM, enabling linkification. + * @param document The document object. + * @param rows The array of rows to apply links to. + */ + public attachToDom(document: Document, rows: HTMLElement[]) { + this._document = document; + this._rows = rows; + } + /** * Queues a row for linkification. * @param {number} rowIndex The index of the row to linkify. */ public linkifyRow(rowIndex: number): void { + // Don't attempt linkify if not yet attached to DOM + if (!this._document) { + return; + } + const timeoutId = this._rowTimeoutIds[rowIndex]; if (timeoutId) { clearTimeout(timeoutId); diff --git a/src/xterm.js b/src/xterm.js index 830b8f846..13fca0bba 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -211,7 +211,7 @@ function Terminal(options) { this.parser = new Parser(this.inputHandler, this); // Reuse renderer if the Terminal is being recreated via a Terminal.reset call. this.renderer = this.renderer || null; - this.linkifier = this.linkifier || null;; + this.linkifier = this.linkifier || new Linkifier(); // user input states this.writeBuffer = []; @@ -612,7 +612,7 @@ Terminal.prototype.open = function(parent) { this.rowContainer.classList.add('xterm-rows'); this.element.appendChild(this.rowContainer); this.children = []; - this.linkifier = new Linkifier(document, this.children); + this.linkifier.attachToDom(document, this.children); // Create the container that will hold helpers like the textarea for // capturing DOM Events. Then produce the helpers. From 44cfb2dc87b02948e7e3d565796fd9ccc91b8739 Mon Sep 17 00:00:00 2001 From: coderaiser Date: Thu, 30 Mar 2017 14:26:56 +0300 Subject: [PATCH 18/26] Added Cloud Commander to real world uses --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 28d1ac477..d0de15693 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**CoderPad**](https://coderpad.io): Online interviewing platform for programmers. Run code in many programming languages, with results displayed by `xterm.js`. - [**WebSSH2**](https://github.com/billchurch/WebSSH2): A web based SSH2 client using `xterm.js`, socket.io, and ssh2. - [**Spyder Terminal**](https://github.com/spyder-ide/spyder-terminal): A full fledged system terminal embedded on Spyder IDE. +- [**Cloud Commander**](https://cloudcmd.io "Cloud Commander"): Orthodox web file manager with console and editor. Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list. From 08fd050c3f9bfd3eafe14ebfb41e1426bfaff973 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 31 Mar 2017 01:48:35 -0700 Subject: [PATCH 19/26] Support multiple link matches in a single row Fixes #612 --- src/Linkifier.ts | 98 +++++++++++++++++++++++++++--------------------- 1 file changed, 56 insertions(+), 42 deletions(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index d9a169407..85d1bd5e3 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -155,16 +155,17 @@ export class Linkifier { const text = row.textContent; for (let i = 0; i < this._linkMatchers.length; i++) { const matcher = this._linkMatchers[i]; - const uri = this._findLinkMatch(text, matcher.regex, matcher.matchIndex); - if (uri) { - const linkElement = this._doLinkifyRow(rowIndex, uri, matcher.handler, matcher.id === HYPERTEXT_LINK_MATCHER_ID); + const linkElements = this._doLinkifyRow(row, matcher); + if (linkElements.length > 0) { // Fire validation callback - if (linkElement && matcher.validationCallback) { - matcher.validationCallback(uri, isValid => { - if (!isValid) { - linkElement.classList.add(INVALID_LINK_CLASS); - } - }); + if (matcher.validationCallback) { + for (let j = 0; j < linkElements.length; j++) { + matcher.validationCallback(linkElements[j].textContent, isValid => { + if (!isValid) { + linkElements[j].classList.add(INVALID_LINK_CLASS); + } + }); + } } // Only allow a single LinkMatcher to trigger on any given row. return; @@ -174,22 +175,32 @@ export class Linkifier { /** * Linkifies a row given a specific handler. - * @param {number} rowIndex The index of the row to linkify. - * @param {string} uri The uri that has been found. - * @param {handler} handler The handler to trigger when the link is triggered. + * @param {HTMLElement} row The row to linkify. + * @param {LinkMatcher} matcher The link matcher for this line. * @return The link element if it was added, otherwise undefined. */ - private _doLinkifyRow(rowIndex: number, uri: string, handler: LinkMatcherHandler, isHttpLinkMatcher: boolean): HTMLElement { + private _doLinkifyRow(row: HTMLElement, matcher: LinkMatcher): HTMLElement[] { // Iterate over nodes as we want to consider text nodes - const nodes = this._rows[rowIndex].childNodes; + let result = []; + const isHttpLinkMatcher = matcher.id === HYPERTEXT_LINK_MATCHER_ID; + const nodes = row.childNodes; + + // Find the first match + let match = row.textContent.match(matcher.regex); + if (!match || match.length === 0) { + return result; + } + let uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex]; + // Set the next searches start index + let rowStartIndex = match.index + uri.length; + for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; const searchIndex = node.textContent.indexOf(uri); if (searchIndex >= 0) { - const linkElement = this._createAnchorElement(uri, handler, isHttpLinkMatcher); + const linkElement = this._createAnchorElement(uri, matcher.handler, isHttpLinkMatcher); if (node.textContent.length === uri.length) { // Matches entire string - if (node.nodeType === 3 /*Node.TEXT_NODE*/) { this._replaceNode(node, linkElement); } else { @@ -203,25 +214,22 @@ export class Linkifier { } } else { // Matches part of string - this._replaceNodeSubstringWithNode(node, linkElement, uri, searchIndex); + const nodesAdded = this._replaceNodeSubstringWithNode(node, linkElement, uri, searchIndex); + // No need to consider the new nodes + i += nodesAdded - 1; } - return linkElement; + result.push(linkElement); + + // Find the next match + match = row.textContent.substring(rowStartIndex).match(matcher.regex); + if (!match || match.length === 0) { + return result; + } + uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex]; + rowStartIndex += match.index + uri.length; } } - } - - /** - * Finds a link match in a piece of text. - * @param {string} text The text to search. - * @param {number} matchIndex The regex match index of the link. - * @return {string} The matching URI or null if not found. - */ - private _findLinkMatch(text: string, regex: RegExp, matchIndex?: number): string { - const match = text.match(regex); - if (!match || match.length === 0) { - return null; - } - return match[typeof matchIndex !== 'number' ? 0 : matchIndex]; + return result; } /** @@ -274,8 +282,9 @@ export class Linkifier { * @param {Node} newNode The new node to insert. * @param {string} substring The substring to replace. * @param {number} substringIndex The index of the substring within the string. + * @return The number of nodes to skip when searching for the next uri. */ - private _replaceNodeSubstringWithNode(targetNode: Node, newNode: Node, substring: string, substringIndex: number): void { + private _replaceNodeSubstringWithNode(targetNode: Node, newNode: Node, substring: string, substringIndex: number): number { let node = targetNode; if (node.nodeType !== 3/*Node.TEXT_NODE*/) { node = node.childNodes[0]; @@ -284,7 +293,7 @@ export class Linkifier { // The targetNode will be either a text node or a . The text node // (targetNode or its only-child) needs to be replaced with newNode plus new // text nodes potentially on either side. - if (node.childNodes.length === 0 && node.nodeType !== Node.TEXT_NODE) { + if (node.childNodes.length === 0 && node.nodeType !== 3/*Node.TEXT_NODE*/) { throw new Error('targetNode must be a text node or only contain a single text node'); } @@ -295,18 +304,23 @@ export class Linkifier { const rightText = fullText.substring(substring.length); const rightTextNode = this._document.createTextNode(rightText); this._replaceNode(node, newNode, rightTextNode); - } else if (substringIndex === targetNode.textContent.length - substring.length) { + return 0; + } + + if (substringIndex === targetNode.textContent.length - substring.length) { // Replace with const leftText = fullText.substring(0, substringIndex); const leftTextNode = this._document.createTextNode(leftText); this._replaceNode(node, leftTextNode, newNode); - } else { - // Replace with - const leftText = fullText.substring(0, substringIndex); - const leftTextNode = this._document.createTextNode(leftText); - const rightText = fullText.substring(substringIndex + substring.length); - const rightTextNode = this._document.createTextNode(rightText); - this._replaceNode(node, leftTextNode, newNode, rightTextNode); + return 1; } + + // Replace with + const leftText = fullText.substring(0, substringIndex); + const leftTextNode = this._document.createTextNode(leftText); + const rightText = fullText.substring(substringIndex + substring.length); + const rightTextNode = this._document.createTextNode(rightText); + this._replaceNode(node, leftTextNode, newNode, rightTextNode); + return 1; } } From d8140097214cc17d093cf9a14b3be5125ed3a571 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 31 Mar 2017 01:48:51 -0700 Subject: [PATCH 20/26] Add tests --- src/Linkifier.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index ca393c217..f70718ea6 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -89,6 +89,21 @@ describe('Linkifier', () => { // Allow time for the click to be performed setTimeout(() => done(), 10); }); + + it('should trigger for multiple link matches on one row', done => { + addRow('test test'); + let count = 0; + linkifier.registerLinkMatcher(/test/, () => assert.fail(), { + validationCallback: (url, cb) => { + count += 1; + if (count === 2) { + done(); + } + cb(false); + } + }); + linkifier.linkifyRow(0); + }); }); describe('priority', () => { From 5546baa927e9dd97e527ac6da763f7a9dfcc9d2e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 31 Mar 2017 01:54:21 -0700 Subject: [PATCH 21/26] Fix edge case --- src/Linkifier.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 85d1bd5e3..f305945c0 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -207,7 +207,7 @@ export class Linkifier { const element = (node); if (element.nodeName === 'A') { // This row has already been linkified - return; + return result; } element.innerHTML = ''; element.appendChild(linkElement); @@ -216,7 +216,7 @@ export class Linkifier { // Matches part of string const nodesAdded = this._replaceNodeSubstringWithNode(node, linkElement, uri, searchIndex); // No need to consider the new nodes - i += nodesAdded - 1; + i += nodesAdded; } result.push(linkElement); From 8c2db8ddb8799771b45500af9dbcc6f4f5fbf08e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 31 Mar 2017 02:10:07 -0700 Subject: [PATCH 22/26] More tests --- src/Linkifier.test.ts | 45 +++++++++++++++++++++++++++++++++++++------ src/Linkifier.ts | 2 +- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index f70718ea6..16e388faf 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -36,9 +36,9 @@ describe('Linkifier', () => { }); }); - function addRow(text: string) { + function addRow(html: string) { const element = document.createElement('div'); - element.textContent = text; + element.innerHTML = html; container.appendChild(element); rows.push(element); } @@ -49,7 +49,8 @@ describe('Linkifier', () => { element.dispatchEvent(event); } - function assertLinkifiesEntireRow(uri: string, done: MochaDone) { + describe('http links', () => { + function assertLinkifiesEntireRow(uri: string, done: MochaDone) { addRow(uri); linkifier.linkifyRow(0); setTimeout(() => { @@ -57,12 +58,44 @@ describe('Linkifier', () => { assert.equal((rows[0].firstChild).textContent, uri); done(); }, 0); - } - - describe('http links', () => { + } it('should allow ~ character in URI path', done => assertLinkifiesEntireRow('http://foo.com/a~b#c~d?e~f', done)); }); + describe('link matcher', () => { + function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, expectedHtml: string, done: MochaDone) { + addRow(rowText); + linkifier.registerLinkMatcher(linkMatcherRegex, () => {}); + linkifier.linkifyRow(0); + // Allow linkify to happen + setTimeout(() => { + assert.equal(rows[0].innerHTML, expectedHtml); + done(); + }, 0); + } + it('should match a single link', done => { + assertLinkifiesRow('foo', /foo/, 'foo', done); + }); + it('should match a single link at the start of a text node', done => { + assertLinkifiesRow('foo bar', /foo/, 'foo bar', done); + }); + it('should match a single link in the middle of a text node', done => { + assertLinkifiesRow('foo bar baz', /bar/, 'foo bar baz', done); + }); + it('should match a single link at the end of a text node', done => { + assertLinkifiesRow('foo bar', /bar/, 'foo bar', done); + }); + it('should match a link after a link at the start of a text node', done => { + assertLinkifiesRow('foo bar', /foo|bar/, 'foo bar', done); + }); + it('should match a link after a link in the middle of a text node', done => { + assertLinkifiesRow('foo bar baz', /bar|baz/, 'foo bar baz', done); + }); + it('should match a link immediately after a link at the end of a text node', done => { + assertLinkifiesRow('foo barbaz', /bar|baz/, 'foo barbaz', done); + }); + }); + describe('validationCallback', () => { it('should enable link if true', done => { addRow('test'); diff --git a/src/Linkifier.ts b/src/Linkifier.ts index f305945c0..b229c775a 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -312,7 +312,7 @@ export class Linkifier { const leftText = fullText.substring(0, substringIndex); const leftTextNode = this._document.createTextNode(leftText); this._replaceNode(node, leftTextNode, newNode); - return 1; + return 0; } // Replace with From 26ccf2a3169681c91d0d42e36c71bd49a80987a5 Mon Sep 17 00:00:00 2001 From: InDieTasten Date: Fri, 31 Mar 2017 22:16:11 +0200 Subject: [PATCH 23/26] Fix typos in comment --- src/Linkifier.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index d9a169407..0f9d9a21c 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -37,8 +37,8 @@ const HYPERTEXT_LINK_MATCHER_ID = 0; export class Linkifier { /** * The time to wait after a row is changed before it is linkified. This prevents - * the costly operation of searching every row multiple times, pntentially a - * huge aount of times. + * the costly operation of searching every row multiple times, potentially a + * huge amount of times. */ protected static TIME_BEFORE_LINKIFY = 200; From aca81c764a188738ae89886bab407d93cd37d136 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 3 Apr 2017 14:49:21 -0700 Subject: [PATCH 24/26] Use setInterval over animation for cursor blink Fixes #625 --- src/xterm.css | 23 ++++++++--------------- src/xterm.js | 34 ++++++++++++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/src/xterm.css b/src/xterm.css index aac95444e..a67485e5b 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -97,16 +97,9 @@ background-color: transparent; } -.terminal:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar).focus.xterm-cursor-blink .terminal-cursor { - animation: xterm-cursor-blink 1.2s infinite step-end; -} - -@keyframes xterm-cursor-blink { - 0% { } - 50% { - background-color: transparent; - color: inherit; - } +.terminal:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar).focus.xterm-cursor-blink-on .terminal-cursor { + background-color: transparent; + color: inherit; } .terminal.xterm-cursor-style-bar .terminal-cursor, @@ -132,13 +125,13 @@ right: 0; height: 1px; } +.terminal.xterm-cursor-style-bar.focus.xterm-cursor-blink.xterm-cursor-blink-on .terminal-cursor::before, +.terminal.xterm-cursor-style-underline.focus.xterm-cursor-blink.xterm-cursor-blink-on .terminal-cursor::before { + background-color: transparent; +} .terminal.xterm-cursor-style-bar.focus.xterm-cursor-blink .terminal-cursor::before, .terminal.xterm-cursor-style-underline.focus.xterm-cursor-blink .terminal-cursor::before { - animation: xterm-cursor-non-bar-blink 1.2s infinite step-end; -} -@keyframes xterm-cursor-non-bar-blink { - 0% { background-color: #fff; } - 50% { background-color: transparent; } + background-color: #fff; } .terminal .composition-view { diff --git a/src/xterm.js b/src/xterm.js index 830b8f846..fb690bd46 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -52,6 +52,13 @@ var WRITE_BUFFER_PAUSE_THRESHOLD = 5; */ var WRITE_BATCH_SIZE = 300; +/** + * The time between cursor blinks. This is driven by JS rather than a CSS + * animation due to a bug in Chromium that causes it to use excessive CPU time. + * See https://github.com/Microsoft/vscode/issues/22900 + */ +var CURSOR_BLINK_INTERVAL = 600; + /** * Terminal */ @@ -159,6 +166,7 @@ function Terminal(options) { this.scrollTop = 0; this.scrollBottom = this.rows - 1; this.customKeydownHandler = null; + this.cursorBlinkInterval = null; // modes this.applicationKeypad = false; @@ -423,7 +431,7 @@ Terminal.prototype.setOption = function(key, value) { this[key] = value; this.options[key] = value; switch (key) { - case 'cursorBlink': this.element.classList.toggle('xterm-cursor-blink', value); break; + case 'cursorBlink': this.setCursorBlinking(value); break; case 'cursorStyle': // Style 'block' applies with no class this.element.classList.toggle(`xterm-cursor-style-underline`, value === 'underline'); @@ -433,6 +441,25 @@ Terminal.prototype.setOption = function(key, value) { } }; +Terminal.prototype.restartCursorBlinking = function () { + this.setCursorBlinking(this.options.cursorBlink); +} + +Terminal.prototype.setCursorBlinking = function (enabled) { + this.element.classList.toggle('xterm-cursor-blink', enabled); + this.element.classList.remove('xterm-cursor-blink-on'); + if (this.cursorBlinkInterval) { + clearInterval(this.cursorBlinkInterval); + this.cursorBlinkInterval = null; + } + if (enabled) { + var self = this; + this.cursorBlinkInterval = setInterval(function () { + self.element.classList.toggle('xterm-cursor-blink-on'); + }, CURSOR_BLINK_INTERVAL); + } +} + /** * Binds the desired focus behavior on a given terminal object. * @@ -445,6 +472,7 @@ Terminal.bindFocus = function (term) { } term.element.classList.add('focus'); term.showCursor(); + term.restartCursorBlinking.apply(term); Terminal.focus = term; term.emit('focus', {terminal: term}); }); @@ -594,7 +622,7 @@ Terminal.prototype.open = function(parent) { this.element.classList.add('terminal'); this.element.classList.add('xterm'); this.element.classList.add('xterm-theme-' + this.theme); - this.element.classList.toggle('xterm-cursor-blink', this.options.cursorBlink); + this.setCursorBlinking(this.options.cursorBlink); this.element.style.height this.element.setAttribute('tabindex', 0); @@ -1336,6 +1364,8 @@ Terminal.prototype.keyDown = function(ev) { return false; } + this.restartCursorBlinking(); + if (!this.compositionHelper.keydown.bind(this.compositionHelper)(ev)) { if (this.ybase !== this.ydisp) { this.scrollToBottom(); From 0c4ba9fd676f08b29bdb90aa8f6b4f7197bcb307 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 3 Apr 2017 15:09:38 -0700 Subject: [PATCH 25/26] Clear cursor blink interval on blur --- src/xterm.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index fb690bd46..fb7d0cf74 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -443,22 +443,26 @@ Terminal.prototype.setOption = function(key, value) { Terminal.prototype.restartCursorBlinking = function () { this.setCursorBlinking(this.options.cursorBlink); -} +}; Terminal.prototype.setCursorBlinking = function (enabled) { this.element.classList.toggle('xterm-cursor-blink', enabled); - this.element.classList.remove('xterm-cursor-blink-on'); - if (this.cursorBlinkInterval) { - clearInterval(this.cursorBlinkInterval); - this.cursorBlinkInterval = null; - } + this.clearCursorBlinkingInterval(); if (enabled) { var self = this; this.cursorBlinkInterval = setInterval(function () { self.element.classList.toggle('xterm-cursor-blink-on'); }, CURSOR_BLINK_INTERVAL); } -} +}; + +Terminal.prototype.clearCursorBlinkingInterval = function () { + this.element.classList.remove('xterm-cursor-blink-on'); + if (this.cursorBlinkInterval) { + clearInterval(this.cursorBlinkInterval); + this.cursorBlinkInterval = null; + } +}; /** * Binds the desired focus behavior on a given terminal object. @@ -497,6 +501,7 @@ Terminal.bindBlur = function (term) { term.send(C0.ESC + '[O'); } term.element.classList.remove('focus'); + term.clearCursorBlinkingInterval.apply(term); Terminal.focus = null; term.emit('blur', {terminal: term}); }); From 64663bf4a8145b8aa0dc31cb1246fe6d110a6390 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 4 Apr 2017 09:35:52 -0700 Subject: [PATCH 26/26] Fix tests --- src/test/test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/test.js b/src/test/test.js index 716b48c42..1644e5800 100644 --- a/src/test/test.js +++ b/src/test/test.js @@ -21,7 +21,8 @@ describe('xterm.js', function() { }; xterm.element = { classList: { - toggle: function(){} + toggle: function(){}, + remove: function(){} } }; });