From ae2881404116d8ee655da7ffa2074f22cfe68fed Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 16 Feb 2018 21:34:57 -0800 Subject: [PATCH 01/20] Check for valid coords before getting word at coords Fixes #1287 --- src/SelectionManager.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index dc1d0682..5aa145e6 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -634,6 +634,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * @param coords The coordinates to get the word at. */ private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): IWordPosition { + // Ensure coords are within viewport (eg. not within scroll bar) + if (coords[0] >= this._terminal.cols) { + return null; + } + const bufferLine = this._buffer.lines.get(coords[1]); if (!bufferLine) { return null; From f8f135515517f5e68276e7a59a4c4449f9c934ac Mon Sep 17 00:00:00 2001 From: Bruno Ribeito Date: Mon, 19 Feb 2018 22:54:09 +0000 Subject: [PATCH 02/20] Validate colors --- src/renderer/ColorManager.ts | 72 +++++++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 21 deletions(-) diff --git a/src/renderer/ColorManager.ts b/src/renderer/ColorManager.ts index 90ef0431..43a41f56 100644 --- a/src/renderer/ColorManager.ts +++ b/src/renderer/ColorManager.ts @@ -66,6 +66,8 @@ function toPaddedHex(c: number): string { * Manages the source of truth for a terminal's colors. */ export class ColorManager implements IColorManager { + private VALID_NON_NAMED_COLORS = new RegExp('^(#[0-9a-f]{3}|#(?:[0-9a-f]{2}){2,4}|(rgb|hsl)a?\((-?\d+%?[,\s]+){2,3}\s*[\d\.]+%?\))$', 'g'); + public colors: IColorSet; constructor() { @@ -85,26 +87,54 @@ export class ColorManager implements IColorManager { * colors will be used where colors are not defined. */ public setTheme(theme: ITheme): void { - this.colors.foreground = theme.foreground || DEFAULT_FOREGROUND; - this.colors.background = theme.background || DEFAULT_BACKGROUND; - this.colors.cursor = theme.cursor || DEFAULT_CURSOR; - this.colors.cursorAccent = theme.cursorAccent || DEFAULT_CURSOR_ACCENT; - this.colors.selection = theme.selection || DEFAULT_SELECTION; - this.colors.ansi[0] = theme.black || DEFAULT_ANSI_COLORS[0]; - this.colors.ansi[1] = theme.red || DEFAULT_ANSI_COLORS[1]; - this.colors.ansi[2] = theme.green || DEFAULT_ANSI_COLORS[2]; - this.colors.ansi[3] = theme.yellow || DEFAULT_ANSI_COLORS[3]; - this.colors.ansi[4] = theme.blue || DEFAULT_ANSI_COLORS[4]; - this.colors.ansi[5] = theme.magenta || DEFAULT_ANSI_COLORS[5]; - this.colors.ansi[6] = theme.cyan || DEFAULT_ANSI_COLORS[6]; - this.colors.ansi[7] = theme.white || DEFAULT_ANSI_COLORS[7]; - this.colors.ansi[8] = theme.brightBlack || DEFAULT_ANSI_COLORS[8]; - this.colors.ansi[9] = theme.brightRed || DEFAULT_ANSI_COLORS[9]; - this.colors.ansi[10] = theme.brightGreen || DEFAULT_ANSI_COLORS[10]; - this.colors.ansi[11] = theme.brightYellow || DEFAULT_ANSI_COLORS[11]; - this.colors.ansi[12] = theme.brightBlue || DEFAULT_ANSI_COLORS[12]; - this.colors.ansi[13] = theme.brightMagenta || DEFAULT_ANSI_COLORS[13]; - this.colors.ansi[14] = theme.brightCyan || DEFAULT_ANSI_COLORS[14]; - this.colors.ansi[15] = theme.brightWhite || DEFAULT_ANSI_COLORS[15]; + this.colors.foreground = this._validateColor(theme.foreground, DEFAULT_FOREGROUND); + this.colors.background = this._validateColor(theme.background, DEFAULT_BACKGROUND); + this.colors.cursor = this._validateColor(theme.cursor, DEFAULT_CURSOR); + this.colors.cursorAccent = this._validateColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT); + this.colors.selection = this._validateColor(theme.selection, DEFAULT_SELECTION); + this.colors.ansi[0] = this._validateColor(theme.black, DEFAULT_ANSI_COLORS[0]); + this.colors.ansi[1] = this._validateColor(theme.red, DEFAULT_ANSI_COLORS[1]); + this.colors.ansi[2] = this._validateColor(theme.green, DEFAULT_ANSI_COLORS[2]); + this.colors.ansi[3] = this._validateColor(theme.yellow, DEFAULT_ANSI_COLORS[3]); + this.colors.ansi[4] = this._validateColor(theme.blue, DEFAULT_ANSI_COLORS[4]); + this.colors.ansi[5] = this._validateColor(theme.magenta, DEFAULT_ANSI_COLORS[5]); + this.colors.ansi[6] = this._validateColor(theme.cyan, DEFAULT_ANSI_COLORS[6]); + this.colors.ansi[7] = this._validateColor(theme.white, DEFAULT_ANSI_COLORS[7]); + this.colors.ansi[8] = this._validateColor(theme.brightBlack, DEFAULT_ANSI_COLORS[8]); + this.colors.ansi[9] = this._validateColor(theme.brightRed, DEFAULT_ANSI_COLORS[9]); + this.colors.ansi[10] = this._validateColor(theme.brightGreen, DEFAULT_ANSI_COLORS[10]); + this.colors.ansi[11] = this._validateColor(theme.brightYellow, DEFAULT_ANSI_COLORS[11]); + this.colors.ansi[12] = this._validateColor(theme.brightBlue, DEFAULT_ANSI_COLORS[12]); + this.colors.ansi[13] = this._validateColor(theme.brightMagenta, DEFAULT_ANSI_COLORS[13]); + this.colors.ansi[14] = this._validateColor(theme.brightCyan, DEFAULT_ANSI_COLORS[14]); + this.colors.ansi[15] = this._validateColor(theme.brightWhite, DEFAULT_ANSI_COLORS[15]); + } + + private _validateColor(color: string, fallback: string): string { + if (!color) { + return fallback; + } + + const isColorValid = this.VALID_NON_NAMED_COLORS.exec(color) || this._validateNamedColor(color); + + if (!isColorValid) { + console.warn(`Color: ${color} is invalid using fallback ${fallback}`); + } + + return isColorValid ? color : fallback; + } + + private _validateNamedColor(color: string): boolean { + const litmus = 'red'; + const d = document.createElement('div'); + d.style.color = litmus; + d.style.color = color; + + // Element's style.color will be reverted to litmus or set to '' if an invalid color is given + if (color !== litmus && (d.style.color === litmus || d.style.color === '')) { + return false; + } + + return true; } } From 99aa142cefe8f1da1c04cc07425d26ec50ed3c08 Mon Sep 17 00:00:00 2001 From: Bruno Ribeito Date: Mon, 19 Feb 2018 23:55:25 +0000 Subject: [PATCH 03/20] Fix test -> wrong Regex flag --- src/renderer/ColorManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/ColorManager.ts b/src/renderer/ColorManager.ts index 43a41f56..ee9c9eac 100644 --- a/src/renderer/ColorManager.ts +++ b/src/renderer/ColorManager.ts @@ -66,7 +66,7 @@ function toPaddedHex(c: number): string { * Manages the source of truth for a terminal's colors. */ export class ColorManager implements IColorManager { - private VALID_NON_NAMED_COLORS = new RegExp('^(#[0-9a-f]{3}|#(?:[0-9a-f]{2}){2,4}|(rgb|hsl)a?\((-?\d+%?[,\s]+){2,3}\s*[\d\.]+%?\))$', 'g'); + private VALID_NON_NAMED_COLORS = new RegExp('^(#[0-9a-f]{3}|#(?:[0-9a-f]{2}){2,4}|(rgb|hsl)a?\((-?\d+%?[,\s]+){2,3}\s*[\d\.]+%?\))$', 'i'); public colors: IColorSet; From a9d6686e14b4f54350561ec5be37861161953525 Mon Sep 17 00:00:00 2001 From: Bruno Ribeito Date: Tue, 20 Feb 2018 20:38:12 +0000 Subject: [PATCH 04/20] Regex is not needed for color validation --- src/renderer/ColorManager.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/renderer/ColorManager.ts b/src/renderer/ColorManager.ts index ee9c9eac..33d69e26 100644 --- a/src/renderer/ColorManager.ts +++ b/src/renderer/ColorManager.ts @@ -66,8 +66,6 @@ function toPaddedHex(c: number): string { * Manages the source of truth for a terminal's colors. */ export class ColorManager implements IColorManager { - private VALID_NON_NAMED_COLORS = new RegExp('^(#[0-9a-f]{3}|#(?:[0-9a-f]{2}){2,4}|(rgb|hsl)a?\((-?\d+%?[,\s]+){2,3}\s*[\d\.]+%?\))$', 'i'); - public colors: IColorSet; constructor() { @@ -115,7 +113,7 @@ export class ColorManager implements IColorManager { return fallback; } - const isColorValid = this.VALID_NON_NAMED_COLORS.exec(color) || this._validateNamedColor(color); + const isColorValid = this._isColorValid(color); if (!isColorValid) { console.warn(`Color: ${color} is invalid using fallback ${fallback}`); @@ -124,7 +122,7 @@ export class ColorManager implements IColorManager { return isColorValid ? color : fallback; } - private _validateNamedColor(color: string): boolean { + private _isColorValid(color: string): boolean { const litmus = 'red'; const d = document.createElement('div'); d.style.color = litmus; From 1114977a2194db5457b4f9f304defa3d4d7d480e Mon Sep 17 00:00:00 2001 From: Bruno Ribeito Date: Wed, 21 Feb 2018 23:35:37 +0000 Subject: [PATCH 05/20] Fix tests --- src/renderer/ColorManager.test.ts | 9 ++++++++- src/renderer/ColorManager.ts | 6 ++++-- src/renderer/Renderer.ts | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/renderer/ColorManager.test.ts b/src/renderer/ColorManager.test.ts index 22407604..2dc60408 100644 --- a/src/renderer/ColorManager.test.ts +++ b/src/renderer/ColorManager.test.ts @@ -3,14 +3,21 @@ * @license MIT */ +import jsdom = require('jsdom'); import { assert } from 'chai'; import { ColorManager } from './ColorManager'; describe('ColorManager', () => { let cm: ColorManager; + let dom: jsdom.JSDOM; + let document: Document; + let window: Window; beforeEach(() => { - cm = new ColorManager(); + dom = new jsdom.JSDOM(''); + window = dom.window; + document = window.document; + cm = new ColorManager(document); }); describe('constructor', () => { diff --git a/src/renderer/ColorManager.ts b/src/renderer/ColorManager.ts index 33d69e26..ddb928a5 100644 --- a/src/renderer/ColorManager.ts +++ b/src/renderer/ColorManager.ts @@ -67,8 +67,10 @@ function toPaddedHex(c: number): string { */ export class ColorManager implements IColorManager { public colors: IColorSet; + private _document: Document; - constructor() { + constructor(document: Document) { + this._document = document; this.colors = { foreground: DEFAULT_FOREGROUND, background: DEFAULT_BACKGROUND, @@ -124,7 +126,7 @@ export class ColorManager implements IColorManager { private _isColorValid(color: string): boolean { const litmus = 'red'; - const d = document.createElement('div'); + const d = this._document.createElement('div'); d.style.color = litmus; d.style.color = color; diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 2408f808..ca5fca6a 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -31,7 +31,7 @@ export class Renderer extends EventEmitter implements IRenderer { constructor(private _terminal: ITerminal, theme: ITheme) { super(); - this.colorManager = new ColorManager(); + this.colorManager = new ColorManager(document); if (theme) { this.colorManager.setTheme(theme); } From bc35f28f512ec5eadb2cf1bff107686b4bbeed36 Mon Sep 17 00:00:00 2001 From: Philip Olson Date: Thu, 22 Feb 2018 17:07:35 -0600 Subject: [PATCH 06/20] Paste newlines as \r Fix issue where non-windows OS terminals don't paste newlines correctly into misbehaving programs. As noted in JetBrains/jediterm#136, the most practical solution is for terminals to handle this in their paste logic. In a perfect world, all terminal programs would implement bracketed paste mode properly, but that's a lot to ask. - translate newlines to \r which is what terminal programs expect --- src/handlers/Clipboard.test.ts | 18 ++++++++++++------ src/handlers/Clipboard.ts | 9 +++------ 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/handlers/Clipboard.test.ts b/src/handlers/Clipboard.test.ts index d35e5fa4..174f6efb 100644 --- a/src/handlers/Clipboard.test.ts +++ b/src/handlers/Clipboard.test.ts @@ -8,13 +8,19 @@ import * as Terminal from '../Terminal'; import * as Clipboard from './Clipboard'; describe('evaluatePastedTextProcessing', () => { - it('should replace carriage return + line feed with line feed on windows', () => { - const pastedText = 'foo\r\nbar\r\n'; - const processedText = Clipboard.prepareTextForTerminal(pastedText, false); - const windowsProcessedText = Clipboard.prepareTextForTerminal(pastedText, true); + it('should replace carriage return and/or line feed with carriage return', () => { + const pastedText = { + unix: 'foo\nbar\n', + windows: 'foo\r\nbar\r\n' + }; - assert.equal(processedText, 'foo\r\nbar\r\n'); - assert.equal(windowsProcessedText, 'foo\rbar\r'); + const processedText = { + unix: Clipboard.prepareTextForTerminal(pastedText.unix), + windows: Clipboard.prepareTextForTerminal(pastedText.windows) + }; + + assert.equal(processedText.unix, 'foo\rbar\r'); + assert.equal(processedText.windows, 'foo\rbar\r'); }); it('should bracket pasted text in bracketedPasteMode', () => { const pastedText = 'foo bar'; diff --git a/src/handlers/Clipboard.ts b/src/handlers/Clipboard.ts index 7ac97714..23925007 100644 --- a/src/handlers/Clipboard.ts +++ b/src/handlers/Clipboard.ts @@ -18,11 +18,8 @@ declare var window: IWindow; * Prepares text to be pasted into the terminal by normalizing the line endings * @param text The pasted text that needs processing before inserting into the terminal */ -export function prepareTextForTerminal(text: string, isMSWindows: boolean): string { - if (isMSWindows) { - return text.replace(/\r?\n/g, '\r'); - } - return text; +export function prepareTextForTerminal(text: string): string { + return text.replace(/\r?\n/g, '\r'); } /** @@ -62,7 +59,7 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void { let text: string; let dispatchPaste = function(text: string): void { - text = prepareTextForTerminal(text, term.browser.isMSWindows); + text = prepareTextForTerminal(text); text = bracketTextForPaste(text, term.bracketedPasteMode); term.handler(text); term.textarea.value = ''; From 5011e629b1f0d5c6ad0009ddc3439a459a4726a4 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 23 Feb 2018 11:08:11 -0800 Subject: [PATCH 07/20] Build addons by scanning addon dir --- gulpfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index 4353e46d..af4fdce0 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -23,7 +23,7 @@ const tsProject = ts.createProject('tsconfig.json'); const srcDir = tsProject.config.compilerOptions.rootDir; let outDir = tsProject.config.compilerOptions.outDir; -const addons = ['attach', 'fit', 'fullscreen', 'search', 'terminado', 'winptyCompat', 'zmodem']; +const addons = fs.readdirSync(`${__dirname}/src/addons`); // Under some environments like TravisCI, this comes out at absolute which can // break the build. This ensures that the outDir is absolute. From dc822787d0869d16ce0173562155e130b3b7db44 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 23 Feb 2018 11:11:23 -0800 Subject: [PATCH 08/20] Move http links to the new webLinks addon Fixes #1297 --- demo/main.js | 3 ++ src/Linkifier.ts | 47 ++-------------------------- src/Terminal.ts | 45 +++----------------------- src/Types.ts | 4 +-- src/addons/webLinks/package.json | 5 +++ src/addons/webLinks/tsconfig.json | 11 +++++++ src/addons/webLinks/webLinks.ts | 41 ++++++++++++++++++++++++ src/addons/winptyCompat/package.json | 2 +- typings/xterm.d.ts | 2 +- 9 files changed, 70 insertions(+), 90 deletions(-) create mode 100644 src/addons/webLinks/package.json create mode 100644 src/addons/webLinks/tsconfig.json create mode 100644 src/addons/webLinks/webLinks.ts diff --git a/demo/main.js b/demo/main.js index 7feb7939..d9c0151a 100644 --- a/demo/main.js +++ b/demo/main.js @@ -3,6 +3,7 @@ import * as attach from '../build/addons/attach/attach'; import * as fit from '../build/addons/fit/fit'; import * as fullscreen from '../build/addons/fullscreen/fullscreen'; import * as search from '../build/addons/search/search'; +import * as webLinks from '../build/addons/webLinks/webLinks'; import * as winptyCompat from '../build/addons/winptyCompat/winptyCompat'; @@ -10,6 +11,7 @@ Terminal.applyAddon(attach); Terminal.applyAddon(fit); Terminal.applyAddon(fullscreen); Terminal.applyAddon(search); +Terminal.applyAddon(webLinks); Terminal.applyAddon(winptyCompat); @@ -121,6 +123,7 @@ function createTerminal() { term.open(terminalContainer); term.winptyCompatInit(); + term.webLinksInit(); term.fit(); term.focus(); diff --git a/src/Linkifier.ts b/src/Linkifier.ts index a9617952..f5e59f25 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -8,30 +8,6 @@ import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkMatcherValidatio import { MouseZone } from './input/MouseZoneManager'; import { EventEmitter } from './EventEmitter'; -const protocolClause = '(https?:\\/\\/)'; -const domainCharacterSet = '[\\da-z\\.-]+'; -const negatedDomainCharacterSet = '[^\\da-z\\.-]+'; -const domainBodyClause = '(' + domainCharacterSet + ')'; -const tldClause = '([a-z\\.]{2,6})'; -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 queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?'; -const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?'; -const negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+'; -const bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause; -const start = '(?:^|' + negatedDomainCharacterSet + ')('; -const end = ')($|' + negatedPathCharacterSet + ')'; -const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end); - -/** - * The ID of the built in http(s) link matcher. - */ -const HYPERTEXT_LINK_MATCHER_ID = 0; - /** * The Linkifier applies links to rows shortly after they have been refreshed. */ @@ -47,7 +23,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { private _mouseZoneManager: IMouseZoneManager; private _rowsTimeoutId: number; - private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID; + private _nextLinkMatcherId = 0; private _rowsToLinkify: {start: number, end: number}; constructor( @@ -58,7 +34,6 @@ export class Linkifier extends EventEmitter implements ILinkifier { start: null, end: null }; - this.registerLinkMatcher(strictUrlRegex, null, { matchIndex: 1 }); } /** @@ -111,23 +86,6 @@ export class Linkifier extends EventEmitter implements ILinkifier { this._rowsToLinkify.end = null; } - /** - * Attaches a handler for hypertext links, overriding default behavior for - * tandard http(s) links. - * @param handler The handler to use, this can be cleared with null. - */ - public setHypertextLinkHandler(handler: LinkMatcherHandler): void { - this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].handler = handler; - } - - /** - * Attaches a validation callback for hypertext links. - * @param callback The callback to use, this can be cleared with null. - */ - public setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void { - this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].validationCallback = callback; - } - /** * Registers a link matcher, allowing custom link patterns to be matched and * handled. @@ -139,7 +97,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { * @return The ID of the new matcher, this can be used to deregister. */ public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options: ILinkMatcherOptions = {}): number { - if (this._nextLinkMatcherId !== HYPERTEXT_LINK_MATCHER_ID && !handler) { + if (!handler) { throw new Error('handler must be defined'); } const matcher: ILinkMatcher = { @@ -222,7 +180,6 @@ export class Linkifier extends EventEmitter implements ILinkifier { private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher, offset: number = 0): void { // Iterate over nodes as we want to consider text nodes let result = []; - const isHttpLinkMatcher = matcher.id === HYPERTEXT_LINK_MATCHER_ID; // Find the first match let match = text.match(matcher.regex); diff --git a/src/Terminal.ts b/src/Terminal.ts index 721996c9..fe9a1b9c 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1326,36 +1326,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.customKeyEventHandler = customKeyEventHandler; } - /** - * Attaches a http(s) link handler, forcing web links to behave differently to - * regular tags. This will trigger a refresh as links potentially need to be - * reconstructed. Calling this with null will remove the handler. - * @param handler The handler callback function. - */ - public setHypertextLinkHandler(handler: LinkMatcherHandler): void { - if (!this.linkifier) { - throw new Error('Cannot attach a hypertext link handler before Terminal.open is called'); - } - this.linkifier.setHypertextLinkHandler(handler); - // Refresh to force links to refresh - this.refresh(0, this.rows - 1); - } - - /** - * Attaches a validation callback for hypertext links. This is useful to use - * validation logic or to do something with the link's element and url. - * @param callback The callback to use, this can - * be cleared with null. - */ - public setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void { - if (!this.linkifier) { - throw new Error('Cannot attach a hypertext validation callback before Terminal.open is called'); - } - this.linkifier.setHypertextValidationCallback(callback); - // // Refresh to force links to refresh - this.refresh(0, this.rows - 1); - } - /** * Registers a link matcher, allowing custom link patterns to be matched and * handled. @@ -1367,12 +1337,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * @return The ID of the new matcher, this can be used to deregister. */ public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number { - if (this.linkifier) { - const matcherId = this.linkifier.registerLinkMatcher(regex, handler, options); - this.refresh(0, this.rows - 1); - return matcherId; - } - return 0; + const matcherId = this.linkifier.registerLinkMatcher(regex, handler, options); + this.refresh(0, this.rows - 1); + return matcherId; } /** @@ -1380,10 +1347,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * @param matcherId The link matcher's ID (returned after register) */ public deregisterLinkMatcher(matcherId: number): void { - if (this.linkifier) { - if (this.linkifier.deregisterLinkMatcher(matcherId)) { - this.refresh(0, this.rows - 1); - } + if (this.linkifier.deregisterLinkMatcher(matcherId)) { + this.refresh(0, this.rows - 1); } } diff --git a/src/Types.ts b/src/Types.ts index be292dbf..b09397d7 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -14,7 +14,7 @@ export type XtermListener = (...args: any[]) => void; export type CharData = [number, string, number, number]; export type LineData = CharData[]; -export type LinkMatcherHandler = (event: MouseEvent, uri: string) => boolean | void; +export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void; export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void; export enum LinkHoverEventTypes { @@ -298,8 +298,6 @@ export interface ISelectionManager { export interface ILinkifier extends IEventEmitter { attachToDom(mouseZoneManager: IMouseZoneManager): void; linkifyRows(start: number, end: number): void; - setHypertextLinkHandler(handler: LinkMatcherHandler): void; - setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void; registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number; deregisterLinkMatcher(matcherId: number): boolean; } diff --git a/src/addons/webLinks/package.json b/src/addons/webLinks/package.json new file mode 100644 index 00000000..f200cab4 --- /dev/null +++ b/src/addons/webLinks/package.json @@ -0,0 +1,5 @@ +{ + "name": "xterm.weblinks", + "main": "weblinks.js", + "private": true +} diff --git a/src/addons/webLinks/tsconfig.json b/src/addons/webLinks/tsconfig.json new file mode 100644 index 00000000..7549370b --- /dev/null +++ b/src/addons/webLinks/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "rootDir": ".", + "outDir": "../../../lib/addons/webLinks/", + "sourceMap": true, + "removeComments": true, + "declaration": true + } +} diff --git a/src/addons/webLinks/webLinks.ts b/src/addons/webLinks/webLinks.ts new file mode 100644 index 00000000..5e4ac029 --- /dev/null +++ b/src/addons/webLinks/webLinks.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +/// + +import { Terminal } from 'xterm'; + +const protocolClause = '(https?:\\/\\/)'; +const domainCharacterSet = '[\\da-z\\.-]+'; +const negatedDomainCharacterSet = '[^\\da-z\\.-]+'; +const domainBodyClause = '(' + domainCharacterSet + ')'; +const tldClause = '([a-z\\.]{2,6})'; +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 queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?'; +const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?'; +const negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+'; +const bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause; +const start = '(?:^|' + negatedDomainCharacterSet + ')('; +const end = ')($|' + negatedPathCharacterSet + ')'; +const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end); + +function handleLink(event: MouseEvent, uri: string): void { + window.open(uri, '_blank'); +} + +export function webLinksInit(term: Terminal): void { + term.registerLinkMatcher(strictUrlRegex, handleLink, { matchIndex: 1 }); +} + +export function apply(terminalConstructor: typeof Terminal): void { + (terminalConstructor.prototype).webLinksInit = function (): void { + webLinksInit(this); + }; +} diff --git a/src/addons/winptyCompat/package.json b/src/addons/winptyCompat/package.json index 2e9fc164..fc929497 100644 --- a/src/addons/winptyCompat/package.json +++ b/src/addons/winptyCompat/package.json @@ -1,5 +1,5 @@ { - "name": "xterm.winptyCompat", + "name": "xterm.winptycompat", "main": "winptyCompat.js", "private": true } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index dd08b55e..4de9e4ed 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -395,7 +395,7 @@ declare module 'xterm' { * @param options Options for the link matcher. * @return The ID of the new matcher, this can be used to deregister. */ - registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number; + registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number; /** * (EXPERIMENTAL) Deregisters a link matcher if it has been registered. From 6a717177513ad81662247ed580ae2023a4e1da13 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 23 Feb 2018 11:28:14 -0800 Subject: [PATCH 09/20] Allow customization of the web links handler and options --- src/addons/webLinks/webLinks.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/addons/webLinks/webLinks.ts b/src/addons/webLinks/webLinks.ts index 5e4ac029..7ff87c60 100644 --- a/src/addons/webLinks/webLinks.ts +++ b/src/addons/webLinks/webLinks.ts @@ -5,7 +5,7 @@ /// -import { Terminal } from 'xterm'; +import { Terminal, ILinkMatcherOptions } from 'xterm'; const protocolClause = '(https?:\\/\\/)'; const domainCharacterSet = '[\\da-z\\.-]+'; @@ -30,12 +30,19 @@ function handleLink(event: MouseEvent, uri: string): void { window.open(uri, '_blank'); } -export function webLinksInit(term: Terminal): void { - term.registerLinkMatcher(strictUrlRegex, handleLink, { matchIndex: 1 }); +/** + * Initialize the web links addon, registering the link matcher. + * @param term The terminal to use web links within. + * @param handler A custom handler to use. + * @param options Custom options to use, matchIndex will always be ignored. + */ +export function webLinksInit(term: Terminal, handler: (event: MouseEvent, uri: string) => void = handleLink, options: ILinkMatcherOptions = {}): void { + options.matchIndex = 1; + term.registerLinkMatcher(strictUrlRegex, handler, options); } export function apply(terminalConstructor: typeof Terminal): void { - (terminalConstructor.prototype).webLinksInit = function (): void { - webLinksInit(this); + (terminalConstructor.prototype).webLinksInit = function (handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): void { + webLinksInit(this, handler, options); }; } From 0d2cc7489ea0888e81f793c7fc1fff1d12ce25c6 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 24 Feb 2018 10:36:34 -0800 Subject: [PATCH 10/20] Fix tests, add webLinks addon test --- src/Linkifier.test.ts | 12 ++------ src/Linkifier.ts | 3 +- src/addons/webLinks/webLinks.test.ts | 42 ++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 11 deletions(-) create mode 100644 src/addons/webLinks/webLinks.test.ts diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index 14b1ac0f..ce1635a3 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -101,12 +101,6 @@ describe('Linkifier', () => { linkifier.attachToDom(mouseZoneManager); }); - 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', () => { it('should match a single link', done => { assertLinkifiesRow('foo', /foo/, [{x: 0, length: 3}], done); @@ -200,19 +194,19 @@ describe('Linkifier', () => { 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]); + assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, 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]); + assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [bId, 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]); + assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, bId]); }); }); }); diff --git a/src/Linkifier.ts b/src/Linkifier.ts index f5e59f25..92a6ca9f 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -143,8 +143,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { * @return Whether a link matcher was found and deregistered. */ public deregisterLinkMatcher(matcherId: number): boolean { - // ID 0 is the hypertext link matcher which cannot be deregistered - for (let i = 1; i < this._linkMatchers.length; i++) { + for (let i = 0; i < this._linkMatchers.length; i++) { if (this._linkMatchers[i].id === matcherId) { this._linkMatchers.splice(i, 1); return true; diff --git a/src/addons/webLinks/webLinks.test.ts b/src/addons/webLinks/webLinks.test.ts new file mode 100644 index 00000000..b3c76703 --- /dev/null +++ b/src/addons/webLinks/webLinks.test.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert, expect } from 'chai'; + +import * as webLinks from './webLinks'; + +class MockTerminal { + public regex: RegExp; + public handler: (event: MouseEvent, uri: string) => void; + public options?: any; + + public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: any): number { + this.regex = regex; + this.handler = handler; + this.options = options; + return 0; + } +} + +describe('webLinks addon', () => { + describe('apply', () => { + it('should do register the `webLinksInit` method', () => { + webLinks.apply(MockTerminal); + assert.equal(typeof (MockTerminal).prototype.webLinksInit, 'function'); + }); + }); + + it('should allow ~ character in URI path', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = ' http://foo.com/a~b#c~d?e~f '; + + let match = row.match(term.regex); + let uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://foo.com/a~b#c~d?e~f'); + }); +}); From 454fd4bfb1ab5c9ecea60a8db102d786dcbb8452 Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Sun, 4 Mar 2018 18:50:46 -0800 Subject: [PATCH 11/20] Split CharAtlas types and utility functions off I'm starting to pull some changes off of my large WIP branch/commit that adds an alternative dynamic character atlas. We're going to need to support multiple CharAtlas implementations, and this should make that easier. --- src/renderer/CharAtlas.ts | 51 ++-------------------------- src/renderer/atlas/CharAtlasUtils.ts | 47 +++++++++++++++++++++++++ src/renderer/atlas/Types.ts | 22 ++++++++++++ 3 files changed, 71 insertions(+), 49 deletions(-) create mode 100644 src/renderer/atlas/CharAtlasUtils.ts create mode 100644 src/renderer/atlas/Types.ts diff --git a/src/renderer/CharAtlas.ts b/src/renderer/CharAtlas.ts index ff8990b3..0c8e90ca 100644 --- a/src/renderer/CharAtlas.ts +++ b/src/renderer/CharAtlas.ts @@ -5,22 +5,13 @@ import { ITerminal } from '../Types'; import { IColorSet } from './Types'; +import { ICharAtlasConfig } from './atlas/Types'; import { isFirefox } from '../shared/utils/Browser'; import { generateCharAtlas, ICharAtlasRequest } from '../shared/CharAtlasGenerator'; +import { generateConfig, configEquals } from './atlas/CharAtlasUtils'; export const CHAR_ATLAS_CELL_SPACING = 1; -interface ICharAtlasConfig { - fontSize: number; - fontFamily: string; - fontWeight: string; - fontWeightBold: string; - scaledCharWidth: number; - scaledCharHeight: number; - allowTransparency: boolean; - colors: IColorSet; -} - interface ICharAtlasCacheEntry { bitmap: HTMLCanvasElement | Promise; config: ICharAtlasConfig; @@ -96,41 +87,3 @@ export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledC charAtlasCache.push(newEntry); return newEntry.bitmap; } - -function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig { - const clonedColors = { - foreground: colors.foreground, - background: colors.background, - cursor: null, - cursorAccent: null, - selection: null, - ansi: colors.ansi.slice(0, 16) - }; - return { - scaledCharWidth, - scaledCharHeight, - fontFamily: terminal.options.fontFamily, - fontSize: terminal.options.fontSize, - fontWeight: terminal.options.fontWeight, - fontWeightBold: terminal.options.fontWeightBold, - allowTransparency: terminal.options.allowTransparency, - colors: clonedColors - }; -} - -function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean { - for (let i = 0; i < a.colors.ansi.length; i++) { - if (a.colors.ansi[i] !== b.colors.ansi[i]) { - return false; - } - } - return a.fontFamily === b.fontFamily && - a.fontSize === b.fontSize && - a.fontWeight === b.fontWeight && - a.fontWeightBold === b.fontWeightBold && - a.allowTransparency === b.allowTransparency && - a.scaledCharWidth === b.scaledCharWidth && - a.scaledCharHeight === b.scaledCharHeight && - a.colors.foreground === b.colors.foreground && - a.colors.background === b.colors.background; -} diff --git a/src/renderer/atlas/CharAtlasUtils.ts b/src/renderer/atlas/CharAtlasUtils.ts new file mode 100644 index 00000000..57e362af --- /dev/null +++ b/src/renderer/atlas/CharAtlasUtils.ts @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ITerminal } from '../../Types'; +import { ITheme } from 'xterm'; +import { IColorSet } from '../Types'; +import { ICharAtlasConfig } from './Types'; + +export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig { + const clonedColors = { + foreground: colors.foreground, + background: colors.background, + cursor: null, + cursorAccent: null, + selection: null, + ansi: colors.ansi.slice(0, 16) + }; + return { + scaledCharWidth, + scaledCharHeight, + fontFamily: terminal.options.fontFamily, + fontSize: terminal.options.fontSize, + fontWeight: terminal.options.fontWeight, + fontWeightBold: terminal.options.fontWeightBold, + allowTransparency: terminal.options.allowTransparency, + colors: clonedColors + }; +} + +export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean { + for (let i = 0; i < a.colors.ansi.length; i++) { + if (a.colors.ansi[i] !== b.colors.ansi[i]) { + return false; + } + } + return a.fontFamily === b.fontFamily && + a.fontSize === b.fontSize && + a.fontWeight === b.fontWeight && + a.fontWeightBold === b.fontWeightBold && + a.allowTransparency === b.allowTransparency && + a.scaledCharWidth === b.scaledCharWidth && + a.scaledCharHeight === b.scaledCharHeight && + a.colors.foreground === b.colors.foreground && + a.colors.background === b.colors.background; +} diff --git a/src/renderer/atlas/Types.ts b/src/renderer/atlas/Types.ts new file mode 100644 index 00000000..3142ce3a --- /dev/null +++ b/src/renderer/atlas/Types.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { FontWeight } from 'xterm'; +import { IColorSet } from '../Types'; + +export const CHAR_ATLAS_CELL_SPACING = 1; +export const INVERTED_DEFAULT_COLOR = -1; +export const DIM_OPACITY = 0.5; + +export interface ICharAtlasConfig { + fontSize: number; + fontFamily: string; + fontWeight: FontWeight; + fontWeightBold: FontWeight; + scaledCharWidth: number; + scaledCharHeight: number; + allowTransparency: boolean; + colors: IColorSet; +} From 18fb1349105a3de4e5520d748c4a0258913b1204 Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Sun, 4 Mar 2018 20:55:42 -0800 Subject: [PATCH 12/20] Remove didCharSizeChange logic from renderer The only thing `didCharSizeChange` was used for was to figure out if we need to refresh the character atlas or not. However, `acquireCharAtlas` already inspects the atlas owned by this terminal, and if it matches, it avoids generating a new atlas. So if `didCharSizeChange` is true and it's not needed, it quickly bails out as a no-op. But if `didCharSizeChange` is false, and it was needed (possible given the complexity of testing all of these edge cases), it could introduce a bug. I think it makes sense to just get rid of `didCharSizeChange`, and just always call `acquireCharAtlas`. --- src/Terminal.ts | 8 +++----- src/renderer/BaseRenderLayer.ts | 6 ++---- src/renderer/CursorRenderLayer.ts | 4 ++-- src/renderer/LinkRenderLayer.ts | 4 ++-- src/renderer/Renderer.ts | 8 ++++---- src/renderer/SelectionRenderLayer.ts | 4 ++-- src/renderer/TextRenderLayer.ts | 4 ++-- src/renderer/Types.ts | 4 ++-- src/utils/TestUtils.test.ts | 2 +- 9 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 721996c9..fdf16830 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -472,11 +472,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT case 'lineHeight': case 'fontWeight': case 'fontWeightBold': - const didCharSizeChange = (key === 'fontWeight' || key === 'fontWeightBold' || key === 'enableBold'); - // When the font changes the size of the cells may change which requires a renderer clear this.renderer.clear(); - this.renderer.onResize(this.cols, this.rows, didCharSizeChange); + this.renderer.onResize(this.cols, this.rows); this.refresh(0, this.rows - 1); case 'scrollback': this.buffers.resize(this.cols, this.rows); @@ -702,14 +700,14 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.viewport.onThemeChanged(this.renderer.colorManager.colors); this.on('cursormove', () => this.renderer.onCursorMove()); - this.on('resize', () => this.renderer.onResize(this.cols, this.rows, false)); + this.on('resize', () => this.renderer.onResize(this.cols, this.rows)); this.on('blur', () => this.renderer.onBlur()); this.on('focus', () => this.renderer.onFocus()); this.on('dprchange', () => this.renderer.onWindowResize(window.devicePixelRatio)); // dprchange should handle this case, we need this as well for browsers that don't support the // matchMedia query. window.addEventListener('resize', () => this.renderer.onWindowResize(window.devicePixelRatio)); - this.charMeasure.on('charsizechanged', () => this.renderer.onResize(this.cols, this.rows, true)); + this.charMeasure.on('charsizechanged', () => this.renderer.onResize(this.cols, this.rows)); this.renderer.on('resize', (dimensions) => this.viewport.syncScrollArea()); this.selectionManager = new SelectionManager(this, this.charMeasure); diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 9423f65d..f977d4c5 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -93,7 +93,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { } } - public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void { + public resize(terminal: ITerminal, dim: IRenderDimensions): void { this._scaledCellWidth = dim.scaledCellWidth; this._scaledCellHeight = dim.scaledCellHeight; this._scaledCharWidth = dim.scaledCharWidth; @@ -110,9 +110,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this.clearAll(); } - if (charSizeChanged) { - this._refreshCharAtlas(terminal, this._colors); - } + this._refreshCharAtlas(terminal, this._colors); } public abstract reset(terminal: ITerminal): void; diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index d430b81d..8db861d5 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -45,8 +45,8 @@ export class CursorRenderLayer extends BaseRenderLayer { // TODO: Consider initial options? Maybe onOptionsChanged should be called at the end of open? } - public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void { - super.resize(terminal, dim, charSizeChanged); + public resize(terminal: ITerminal, dim: IRenderDimensions): void { + super.resize(terminal, dim); // Resizing the canvas discards the contents of the canvas so clear state this._state = { x: null, diff --git a/src/renderer/LinkRenderLayer.ts b/src/renderer/LinkRenderLayer.ts index 61352f15..4071523a 100644 --- a/src/renderer/LinkRenderLayer.ts +++ b/src/renderer/LinkRenderLayer.ts @@ -18,8 +18,8 @@ export class LinkRenderLayer extends BaseRenderLayer { terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: ILinkHoverEvent) => this._onLinkLeave(e)); } - public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void { - super.resize(terminal, dim, charSizeChanged); + public resize(terminal: ITerminal, dim: IRenderDimensions): void { + super.resize(terminal, dim); // Resizing the canvas discards the contents of the canvas so clear state this._state = null; } diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index fa1e34e6..bd0b688a 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -84,7 +84,7 @@ export class Renderer extends EventEmitter implements IRenderer { // and the terminal needs to refreshed if (this._devicePixelRatio !== devicePixelRatio) { this._devicePixelRatio = devicePixelRatio; - this.onResize(this._terminal.cols, this._terminal.rows, true); + this.onResize(this._terminal.cols, this._terminal.rows); } } @@ -106,12 +106,12 @@ export class Renderer extends EventEmitter implements IRenderer { return this.colorManager.colors; } - public onResize(cols: number, rows: number, didCharSizeChange: boolean): void { + public onResize(cols: number, rows: number): void { // Update character and canvas dimensions this._updateDimensions(); // Resize all render layers - this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions, didCharSizeChange)); + this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions)); // Force a refresh if (this._isPaused) { @@ -131,7 +131,7 @@ export class Renderer extends EventEmitter implements IRenderer { } public onCharSizeChanged(): void { - this.onResize(this._terminal.cols, this._terminal.rows, true); + this.onResize(this._terminal.cols, this._terminal.rows); } public onBlur(): void { diff --git a/src/renderer/SelectionRenderLayer.ts b/src/renderer/SelectionRenderLayer.ts index 53fc9b39..7a5557fa 100644 --- a/src/renderer/SelectionRenderLayer.ts +++ b/src/renderer/SelectionRenderLayer.ts @@ -20,8 +20,8 @@ export class SelectionRenderLayer extends BaseRenderLayer { }; } - public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void { - super.resize(terminal, dim, charSizeChanged); + public resize(terminal: ITerminal, dim: IRenderDimensions): void { + super.resize(terminal, dim); // Resizing the canvas discards the contents of the canvas so clear state this._state = { start: null, diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index a2ab9f19..3ab1fdf0 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -27,8 +27,8 @@ export class TextRenderLayer extends BaseRenderLayer { this._state = new GridCache(); } - public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void { - super.resize(terminal, dim, charSizeChanged); + public resize(terminal: ITerminal, dim: IRenderDimensions): void { + super.resize(terminal, dim); // Clear the character width cache if the font or width has changed const terminalFont = this._getFont(terminal, false); diff --git a/src/renderer/Types.ts b/src/renderer/Types.ts index 6f6e5f0e..1885d059 100644 --- a/src/renderer/Types.ts +++ b/src/renderer/Types.ts @@ -24,7 +24,7 @@ export interface IRenderer extends IEventEmitter { setTheme(theme: ITheme): IColorSet; onWindowResize(devicePixelRatio: number): void; - onResize(cols: number, rows: number, didCharSizeChange: boolean): void; + onResize(cols: number, rows: number): void; onCharSizeChanged(): void; onBlur(): void; onFocus(): void; @@ -103,7 +103,7 @@ export interface IRenderLayer { /** * Resize the render layer. */ - resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void; + resize(terminal: ITerminal, dim: IRenderDimensions): void; /** * Clear the state of the render layer. diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index 8ba16179..0c53c9a0 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -310,7 +310,7 @@ export class MockRenderer implements IRenderer { } dimensions: IRenderDimensions; setTheme(theme: ITheme): IColorSet { return {}; } - onResize(cols: number, rows: number, didCharSizeChange: boolean): void {} + onResize(cols: number, rows: number): void {} onCharSizeChanged(): void {} onBlur(): void {} onFocus(): void {} From 9e62c8692f5a85c42c4c3aee316cc495c7ab84ba Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 5 Mar 2018 11:11:54 -0800 Subject: [PATCH 13/20] Only move horizontally on alt click in normal buffer Fixes #1305 --- src/Types.ts | 1 + src/handlers/AltClickHandler.ts | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Types.ts b/src/Types.ts index be292dbf..f0185123 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -252,6 +252,7 @@ export interface IBuffer { tabs: any; scrollBottom: number; scrollTop: number; + hasScrollback: boolean; savedY: number; savedX: number; isCursorInViewport: boolean; diff --git a/src/handlers/AltClickHandler.ts b/src/handlers/AltClickHandler.ts index c9c51cbe..7af32ee6 100644 --- a/src/handlers/AltClickHandler.ts +++ b/src/handlers/AltClickHandler.ts @@ -56,9 +56,13 @@ export class AltClickHandler { * then moves to requested col. */ private _arrowSequences(): string { - return this._resetStartingRow() + - this._moveToRequestedRow() + - this._moveToRequestedCol(); + // The alt buffer should try to navigate between rows + if (!this._terminal.buffer.hasScrollback) { + return this._resetStartingRow() + this._moveToRequestedRow() + this._moveToRequestedCol(); + } + + // Only move horizontally for the normal buffer + return this._moveHorizontallyOnly(); } /** @@ -113,6 +117,11 @@ export class AltClickHandler { ).length, this._sequence(direction)); } + private _moveHorizontallyOnly(): string { + let direction = this._horizontalDirection(); + return repeat(Math.abs(this._startCol - this._endCol), this._sequence(direction)); + } + /** * Utility functions */ From 7c471f5389cd43365aad3f6972df06b2cf7b56ba Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 5 Mar 2018 11:32:05 -0800 Subject: [PATCH 14/20] Fix test compile --- src/utils/TestUtils.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index 8ba16179..8d12120b 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -276,6 +276,7 @@ export class MockBuffer implements IBuffer { lines: ICircularList<[number, string, number, number][]>; ydisp: number; ybase: number; + hasScrollback: boolean; y: number; x: number; tabs: any; From 1f113b1d29c507bfc40ee9f747f5037d6dc3cfff Mon Sep 17 00:00:00 2001 From: Paris Kasidiaris Date: Wed, 7 Mar 2018 12:28:44 +0200 Subject: [PATCH 15/20] Add xterm.js Docker image to documentation Closes #1295 --- README.md | 14 +++++++++++++- docker-compose.yml | 1 + 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cdaf0d71..a2a50372 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,19 @@ Then open your project's [Public URL](https://help.sourcelair.com/projects/the-p ### Docker -First, make sure you have Docker Engine 1.13.0 (or newer) and Docker Compose 1.10.0 (or newer). To run the demo and builder in parallel, run the following command in your terminal: +First, make sure you have Docker Engine 1.13.0 (or newer) and Docker Compose 1.10.0 (or newer). + +Xterm.js [provides a pre-built Docker image](https://hub.docker.com/r/xtermjs/xterm.js/) to help run the demo easily (Git tags are built as [tagged Docker images](https://hub.docker.com/r/xtermjs/xterm.js/tags/) too). + +To run the just demo (with no editing access). run the following command in your terminal: + +``` +docker run -p 3000:3000 xtermjs/xterm.js +``` + +Then open http://0.0.0.0:3000 in a web browser to access the demo. + +To run the demo and builder in parallel, run the following command in your terminal: ``` docker-compose up diff --git a/docker-compose.yml b/docker-compose.yml index effd975c..8e6a2f46 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,6 +2,7 @@ version: "3" services: web: + image: xtermjs/xterm.js:latest build: . volumes: - ./:/usr/src/app From 31ce5c182697d7988e2ac1abe68f3dfde6667df8 Mon Sep 17 00:00:00 2001 From: Paris Kasidiaris Date: Wed, 7 Mar 2018 12:38:29 +0200 Subject: [PATCH 16/20] Run the webpack build in the docker image too --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 1c72e679..1e3a262c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,4 +16,4 @@ RUN npm install COPY . /usr/src/app # Run the tests and build, to make sure everything is working nicely -RUN npm run build && npm run test +RUN npm run build && npm run webpack && npm run test From 70771430b55c085bc5c632b8cfaab7285dc63de3 Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Wed, 7 Mar 2018 22:13:41 -0800 Subject: [PATCH 17/20] Clean up char atlas constant imports/exports - Ensures that everything is only defined in one place, and everything imports from that place. - Moves CHAR_ATLAS_CELL_SPACING into a subdirectory of shared/ so that CharAtlasGenerator can pull from it. This addresses the comments on https://github.com/xtermjs/xterm.js/pull/1307/files/454fd4bfb1ab5c9ece --- src/renderer/BaseRenderLayer.ts | 7 +++---- src/renderer/CharAtlas.ts | 2 -- src/renderer/LinkRenderLayer.ts | 3 ++- src/renderer/TextRenderLayer.ts | 3 ++- src/renderer/atlas/Types.ts | 1 - src/shared/CharAtlasGenerator.ts | 3 +-- src/shared/atlas/Types.ts | 1 + 7 files changed, 9 insertions(+), 11 deletions(-) create mode 100644 src/shared/atlas/Types.ts diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 9423f65d..8a236ada 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -5,12 +5,11 @@ import { IRenderLayer, IColorSet, IRenderDimensions } from './Types'; import { CharData, ITerminal, ITerminalOptions } from '../Types'; -import { acquireCharAtlas, CHAR_ATLAS_CELL_SPACING } from './CharAtlas'; +import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from './atlas/Types'; +import { CHAR_ATLAS_CELL_SPACING } from '../shared/atlas/Types'; +import { acquireCharAtlas } from './CharAtlas'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; -export const INVERTED_DEFAULT_COLOR = -1; -const DIM_OPACITY = 0.5; - export abstract class BaseRenderLayer implements IRenderLayer { private _canvas: HTMLCanvasElement; protected _ctx: CanvasRenderingContext2D; diff --git a/src/renderer/CharAtlas.ts b/src/renderer/CharAtlas.ts index 0c8e90ca..c157ff3d 100644 --- a/src/renderer/CharAtlas.ts +++ b/src/renderer/CharAtlas.ts @@ -10,8 +10,6 @@ import { isFirefox } from '../shared/utils/Browser'; import { generateCharAtlas, ICharAtlasRequest } from '../shared/CharAtlasGenerator'; import { generateConfig, configEquals } from './atlas/CharAtlasUtils'; -export const CHAR_ATLAS_CELL_SPACING = 1; - interface ICharAtlasCacheEntry { bitmap: HTMLCanvasElement | Promise; config: ICharAtlasConfig; diff --git a/src/renderer/LinkRenderLayer.ts b/src/renderer/LinkRenderLayer.ts index 61352f15..16d788b5 100644 --- a/src/renderer/LinkRenderLayer.ts +++ b/src/renderer/LinkRenderLayer.ts @@ -7,7 +7,8 @@ import { ILinkHoverEvent, ITerminal, ILinkifierAccessor, IBuffer, ICharMeasure, import { CHAR_DATA_ATTR_INDEX } from '../Buffer'; import { GridCache } from './GridCache'; import { FLAGS, IColorSet, IRenderDimensions } from './Types'; -import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer'; +import { INVERTED_DEFAULT_COLOR } from './atlas/Types'; +import { BaseRenderLayer } from './BaseRenderLayer'; export class LinkRenderLayer extends BaseRenderLayer { private _state: ILinkHoverEvent = null; diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index a2ab9f19..6e82a05a 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -6,8 +6,9 @@ import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../Buffer'; import { FLAGS, IColorSet, IRenderDimensions } from './Types'; import { CharData, IBuffer, ICharMeasure, ITerminal } from '../Types'; +import { INVERTED_DEFAULT_COLOR } from './atlas/Types'; import { GridCache } from './GridCache'; -import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer'; +import { BaseRenderLayer } from './BaseRenderLayer'; /** * This CharData looks like a null character, which will forc a clear and render diff --git a/src/renderer/atlas/Types.ts b/src/renderer/atlas/Types.ts index 3142ce3a..a79cb327 100644 --- a/src/renderer/atlas/Types.ts +++ b/src/renderer/atlas/Types.ts @@ -6,7 +6,6 @@ import { FontWeight } from 'xterm'; import { IColorSet } from '../Types'; -export const CHAR_ATLAS_CELL_SPACING = 1; export const INVERTED_DEFAULT_COLOR = -1; export const DIM_OPACITY = 0.5; diff --git a/src/shared/CharAtlasGenerator.ts b/src/shared/CharAtlasGenerator.ts index 96cf6cc6..fbf66ac3 100644 --- a/src/shared/CharAtlasGenerator.ts +++ b/src/shared/CharAtlasGenerator.ts @@ -4,6 +4,7 @@ */ import { FontWeight } from 'xterm'; +import { CHAR_ATLAS_CELL_SPACING } from './atlas/Types'; import { isFirefox } from './utils/Browser'; declare const Promise: any; @@ -29,8 +30,6 @@ export interface ICharAtlasRequest { allowTransparency: boolean; } -export const CHAR_ATLAS_CELL_SPACING = 1; - /** * Generates a char atlas. * @param context The window or worker context. diff --git a/src/shared/atlas/Types.ts b/src/shared/atlas/Types.ts new file mode 100644 index 00000000..b69dfe0d --- /dev/null +++ b/src/shared/atlas/Types.ts @@ -0,0 +1 @@ +export const CHAR_ATLAS_CELL_SPACING = 1; From ab69bfbb3260a791e65ea59e00bd935bcab03a2c Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Wed, 7 Mar 2018 22:23:19 -0800 Subject: [PATCH 18/20] Move rest of atlas implementations to atlas dirs Moves CharAtlas into src/renderer/atlas/ and CharAtlasGenerator into src/shared/atlas/. --- src/renderer/BaseRenderLayer.ts | 2 +- src/renderer/{ => atlas}/CharAtlas.ts | 12 ++++++------ src/shared/{ => atlas}/CharAtlasGenerator.ts | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) rename src/renderer/{ => atlas}/CharAtlas.ts (88%) rename src/shared/{ => atlas}/CharAtlasGenerator.ts (97%) diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 8a236ada..7957eed0 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -7,7 +7,7 @@ import { IRenderLayer, IColorSet, IRenderDimensions } from './Types'; import { CharData, ITerminal, ITerminalOptions } from '../Types'; import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from './atlas/Types'; import { CHAR_ATLAS_CELL_SPACING } from '../shared/atlas/Types'; -import { acquireCharAtlas } from './CharAtlas'; +import { acquireCharAtlas } from './atlas/CharAtlas'; import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer'; export abstract class BaseRenderLayer implements IRenderLayer { diff --git a/src/renderer/CharAtlas.ts b/src/renderer/atlas/CharAtlas.ts similarity index 88% rename from src/renderer/CharAtlas.ts rename to src/renderer/atlas/CharAtlas.ts index c157ff3d..d688d328 100644 --- a/src/renderer/CharAtlas.ts +++ b/src/renderer/atlas/CharAtlas.ts @@ -3,12 +3,12 @@ * @license MIT */ -import { ITerminal } from '../Types'; -import { IColorSet } from './Types'; -import { ICharAtlasConfig } from './atlas/Types'; -import { isFirefox } from '../shared/utils/Browser'; -import { generateCharAtlas, ICharAtlasRequest } from '../shared/CharAtlasGenerator'; -import { generateConfig, configEquals } from './atlas/CharAtlasUtils'; +import { ITerminal } from '../../Types'; +import { IColorSet } from '../Types'; +import { ICharAtlasConfig } from './Types'; +import { isFirefox } from '../../shared/utils/Browser'; +import { generateCharAtlas, ICharAtlasRequest } from '../../shared/atlas/CharAtlasGenerator'; +import { generateConfig, configEquals } from './CharAtlasUtils'; interface ICharAtlasCacheEntry { bitmap: HTMLCanvasElement | Promise; diff --git a/src/shared/CharAtlasGenerator.ts b/src/shared/atlas/CharAtlasGenerator.ts similarity index 97% rename from src/shared/CharAtlasGenerator.ts rename to src/shared/atlas/CharAtlasGenerator.ts index fbf66ac3..cf17bbb8 100644 --- a/src/shared/CharAtlasGenerator.ts +++ b/src/shared/atlas/CharAtlasGenerator.ts @@ -4,8 +4,8 @@ */ import { FontWeight } from 'xterm'; -import { CHAR_ATLAS_CELL_SPACING } from './atlas/Types'; -import { isFirefox } from './utils/Browser'; +import { CHAR_ATLAS_CELL_SPACING } from './Types'; +import { isFirefox } from '../utils/Browser'; declare const Promise: any; From 735dfb4710bac33fae32ae8faae7c02269fb8d38 Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Wed, 7 Mar 2018 23:19:38 -0800 Subject: [PATCH 19/20] Add copyright header to shared/atlas/Types.ts --- src/shared/atlas/Types.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/shared/atlas/Types.ts b/src/shared/atlas/Types.ts index b69dfe0d..e8bb6b0a 100644 --- a/src/shared/atlas/Types.ts +++ b/src/shared/atlas/Types.ts @@ -1 +1,6 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + export const CHAR_ATLAS_CELL_SPACING = 1; From 606c68dd0c24127fc29a3b0511fc847ee9ff3b4b Mon Sep 17 00:00:00 2001 From: Paris Kasidiaris Date: Thu, 8 Mar 2018 08:59:46 +0000 Subject: [PATCH 20/20] Bump version to 3.2.0 Signed-off-by: Paris Kasidiaris --- AUTHORS | 2 ++ package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 47328bc9..bcf1c5b8 100644 --- a/AUTHORS +++ b/AUTHORS @@ -32,6 +32,7 @@ Christopher Jeffrey coderaiser Damien Tournoud Dan Brown +Daniel Griffen Daniel Griffen Daniel Imms Daniel Risacher @@ -74,6 +75,7 @@ Martin Chloride Martin Koppehel Martin Wang Matt Bierner +Matthew James Michael Irwin Mikko Karvonen mofux diff --git a/package.json b/package.json index 079333de..594c31a7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xterm", "description": "Full xterm terminal, in your browser", - "version": "3.1.0", + "version": "3.2.0", "ignore": [ "demo", "test",