From b5b350fdcff6e31263b52208f3f1985821f39c88 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 7 Jul 2018 11:12:29 -0700 Subject: [PATCH 01/16] Add pointer to .d.ts and mention addon CSS Part of #1554 --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 1122c59c..6e139f8d 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,8 @@ var xterm = new Terminal(); // Instantiate the terminal xterm.fit(); // Use the `fit` method, provided by the `fit` addon ``` +You will also need to include the addon's CSS file if it has one in the folder. + #### Importing Addons in TypeScript There are currently no typings for addons if they are accessed via extending Terminal prototype, so you will need to upcast if using TypeScript, eg. `(xterm).fit()`. @@ -107,6 +109,10 @@ Since xterm.js is typically implemented as a developer tool, only modern browser Xterm.js works seamlessly in Electron apps and may even work on earlier versions of the browsers but these are the browsers we strive to keep working. +## API + +The current full API documentation is available in the [TypeScript declaration file on the repository](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), switch the tag (press `w` when viewing the file) to point at the specific version tag you're using. + ## Real-world uses Xterm.js is used in several world-class applications to provide great terminal experiences. From 3582f8733d341c43abd4f5935c43c97eca05cf30 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 14 Jul 2018 10:46:12 -0700 Subject: [PATCH 02/16] Deprecate emit Part of #1505 --- typings/xterm.d.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index b4df9210..1db05639 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -416,6 +416,13 @@ declare module 'xterm' { */ off(type: 'blur' | 'focus' | 'linefeed' | 'selection' | 'data' | 'key' | 'keypress' | 'keydown' | 'refresh' | 'resize' | 'scroll' | 'title' | string, listener: (...args: any[]) => void): void; + /** + * Emits an event on the terminal. + * @param type The type of event + * @param data data associated with the event. + * @deprecated This is being removed from the API with no replacement, see + * issue #1505. + */ emit(type: string, data?: any): void; addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable; @@ -474,7 +481,7 @@ declare module 'xterm' { * (EXPERIMENTAL) Registers a character joiner, allowing custom sequences of * characters to be rendered as a single unit. This is useful in particular * for rendering ligatures and graphemes, among other things. - * + * * Each registered character joiner is called with a string of text * representing a portion of a line in the terminal that can be rendered as * a single unit. The joiner must return a sorted array, where each entry is @@ -483,16 +490,16 @@ declare module 'xterm' { * a single unit. When multiple joiners are provided, the results of each * are collected. If there are any overlapping substrings between them, they * are combined into one larger unit that is drawn together. - * + * * All character joiners that are registered get called every time a line is * rendered in the terminal, so it is essential for the handler function to * run as quickly as possible to avoid slowdowns when rendering. Similarly, * joiners should strive to return the smallest possible substrings to * render together, since they aren't drawn as optimally as individual * characters. - * + * * NOTE: character joiners are only used by the canvas renderer. - * + * * @param handler The function that determines character joins. It is called * with a string of text that is eligible for joining and returns an array * where each entry is an array containing the start (inclusive) and end From d964c23c43a6a0f8675a8c44291e6e57d19e860a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 24 Jul 2018 06:27:05 -0700 Subject: [PATCH 03/16] Add note in readme about API/experimental API --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index bde713a7..3fb2f407 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,12 @@ There are also the following third party addons available: - [xterm-webfont](https://www.npmjs.com/package/xterm-webfont) +## API + +The full API for xterm.js is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API. + +Note that some APIs are marked *experimental*, these are added so we can experiment with new ideas without committing to support it like a normal semver API. Note that these APIs can change radically between versions so be sure to read release notes if you plan on using experimental APIs. + ## Browser Support Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Here is a list of the versions we aim to support: From 4a443c4a45512d0ad3fa8279557064a9dc98ad34 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 24 Jul 2018 06:30:18 -0700 Subject: [PATCH 04/16] Move API section to getting started, above addons --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3fb2f407..bc1deeb7 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,12 @@ The proposed way to load xterm.js is via the ES6 module syntax. import { Terminal } from 'xterm'; ``` +### API + +The full API for xterm.js is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API. + +Note that some APIs are marked *experimental*, these are added so we can experiment with new ideas without committing to support it like a normal semver API. Note that these APIs can change radically between versions so be sure to read release notes if you plan on using experimental APIs. + ### Addons Addons are JavaScript modules that extend the `Terminal` prototype with new methods and attributes to provide additional functionality. There are a handful available in the main repository in the `src/addons` directory and you can even write your own, by using xterm.js' public API. @@ -95,12 +101,6 @@ There are also the following third party addons available: - [xterm-webfont](https://www.npmjs.com/package/xterm-webfont) -## API - -The full API for xterm.js is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API. - -Note that some APIs are marked *experimental*, these are added so we can experiment with new ideas without committing to support it like a normal semver API. Note that these APIs can change radically between versions so be sure to read release notes if you plan on using experimental APIs. - ## Browser Support Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Here is a list of the versions we aim to support: From bdfe877c8f76875b7ccc7cdad3c7928a905cdcbe Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 24 Jul 2018 08:58:30 -0700 Subject: [PATCH 05/16] Remove jsdoc types They're redundant in TS/tsdoc --- src/BufferSet.ts | 5 +--- src/CompositionHelper.ts | 2 +- src/Terminal.ts | 59 ++++++++++++++++++------------------- src/handlers/Clipboard.ts | 6 ++-- src/renderer/Renderer.ts | 4 +-- src/shared/utils/Browser.ts | 4 +-- 6 files changed, 38 insertions(+), 42 deletions(-) diff --git a/src/BufferSet.ts b/src/BufferSet.ts index 553b2056..4b5ca2d1 100644 --- a/src/BufferSet.ts +++ b/src/BufferSet.ts @@ -18,7 +18,7 @@ export class BufferSet extends EventEmitter implements IBufferSet { /** * Create a new BufferSet for the given terminal. - * @param {Terminal} terminal - The terminal the BufferSet will belong to + * @param _terminal - The terminal the BufferSet will belong to */ constructor(private _terminal: ITerminal) { super(); @@ -35,7 +35,6 @@ export class BufferSet extends EventEmitter implements IBufferSet { /** * Returns the alt Buffer of the BufferSet - * @returns {Buffer} */ public get alt(): Buffer { return this._alt; @@ -43,7 +42,6 @@ export class BufferSet extends EventEmitter implements IBufferSet { /** * Returns the normal Buffer of the BufferSet - * @returns {Buffer} */ public get active(): Buffer { return this._activeBuffer; @@ -51,7 +49,6 @@ export class BufferSet extends EventEmitter implements IBufferSet { /** * Returns the currently active Buffer of the BufferSet - * @returns {Buffer} */ public get normal(): Buffer { return this._normal; diff --git a/src/CompositionHelper.ts b/src/CompositionHelper.ts index b721b7f1..b1745d41 100644 --- a/src/CompositionHelper.ts +++ b/src/CompositionHelper.ts @@ -61,7 +61,7 @@ export class CompositionHelper { /** * Handles the compositionupdate event, updating the composition view. - * @param {CompositionEvent} ev The event. + * @param ev The event. */ public compositionupdate(ev: CompositionEvent): void { this._compositionView.textContent = ev.data; diff --git a/src/Terminal.ts b/src/Terminal.ts index e8366290..a4b9a6c7 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -214,7 +214,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II /** * Creates a new `Terminal` object. * - * @param {object} options An object containing a set of options, the available options are: + * @param options An object containing a set of options, the available options are: * - `cursorBlink` (boolean): Whether the terminal cursor blinks * - `cols` (number): The number of columns of the terminal (horizontal size) * - `rows` (number): The number of rows of the terminal (vertical size) @@ -348,7 +348,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II /** * Retrieves an option's value from the terminal. - * @param {string} key The option key. + * @param key The option key. */ public getOption(key: string): any { if (!(key in DEFAULT_OPTIONS)) { @@ -360,8 +360,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II /** * Sets an option on the terminal. - * @param {string} key The option key. - * @param {any} value The option value. + * @param key The option key. + * @param value The option value. */ public setOption(key: string, value: any): void { if (!(key in DEFAULT_OPTIONS)) { @@ -602,7 +602,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II /** * Opens the terminal within an element. * - * @param {HTMLElement} parent The element to create the terminal within. + * @param parent The element to create the terminal within. */ public open(parent: HTMLElement): void { this._parent = parent || this._parent; @@ -1098,8 +1098,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II /** * Tells the renderer to refresh terminal content between two rows (inclusive) at the next * opportunity. - * @param {number} start The row to start from (between 0 and this.rows - 1). - * @param {number} end The row to end at (between start and this.rows - 1). + * @param start The row to start from (between 0 and this.rows - 1). + * @param end The row to end at (between start and this.rows - 1). */ public refresh(start: number, end: number): void { if (this.renderer) { @@ -1109,8 +1109,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II /** * Queues linkification for the specified rows. - * @param {number} start The row to start from (between 0 and this.rows - 1). - * @param {number} end The row to end at (between start and this.rows - 1). + * @param start The row to start from (between 0 and this.rows - 1). + * @param end The row to end at (between start and this.rows - 1). */ private _queueLinkification(start: number, end: number): void { if (this.linkifier) { @@ -1202,8 +1202,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II /** * Scroll the display of the terminal - * @param {number} disp The number of lines to scroll down (negative scroll up). - * @param {boolean} suppressScrollEvent Don't emit the scroll event as scrollLines. This is used + * @param disp The number of lines to scroll down (negative scroll up). + * @param suppressScrollEvent Don't emit the scroll event as scrollLines. This is used * to avoid unwanted events being handled by the viewport when the event was triggered from the * viewport originally. */ @@ -1234,7 +1234,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II /** * Scroll the display of the terminal by a number of pages. - * @param {number} pageCount The number of pages to scroll (negative scrolls up). + * @param pageCount The number of pages to scroll (negative scrolls up). */ public scrollPages(pageCount: number): void { this.scrollLines(pageCount * (this.rows - 1)); @@ -1263,7 +1263,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II /** * Writes text to the terminal. - * @param {string} data The text to write to the terminal. + * @param data The text to write to the terminal. */ public write(data: string): void { // Ignore falsy data values (including the empty string) @@ -1329,7 +1329,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II /** * Writes text to the terminal, followed by a break line character (\n). - * @param {string} data The text to write to the terminal. + * @param data The text to write to the terminal. */ public writeln(data: string): void { this.write(data + '\r\n'); @@ -1442,7 +1442,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II * Handle a keydown event * Key Resources: * - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent - * @param {KeyboardEvent} ev The keydown event to be handled. + * @param ev The keydown event to be handled. */ protected _keyDown(event: KeyboardEvent): boolean { if (this._customKeyEventHandler && this._customKeyEventHandler(event) === false) { @@ -1539,7 +1539,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II * Handle a keypress event. * Key Resources: * - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent - * @param {KeyboardEvent} ev The keypress event to be handled. + * @param ev The keypress event to be handled. */ protected _keyPress(ev: KeyboardEvent): boolean { let key; @@ -1578,7 +1578,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II /** * Send data for handling to the terminal - * @param {string} data */ public send(data: string): void { if (!this._sendDataQueue) { @@ -1631,8 +1630,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II /** * Resizes the terminal. * - * @param {number} x The number of columns to resize to. - * @param {number} y The number of rows to resize to. + * @param x The number of columns to resize to. + * @param y The number of rows to resize to. */ public resize(x: number, y: number): void { if (isNaN(x) || isNaN(y)) { @@ -1666,7 +1665,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II /** * Updates the range of rows to refresh - * @param {number} y The number of rows to refresh next. + * @param y The number of rows to refresh next. */ public updateRange(y: number): void { if (y < this._refreshStart) this._refreshStart = y; @@ -1689,8 +1688,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II /** * Erase in the identified line everything from "x" to the end of the line (right). - * @param {number} x The column from which to start erasing to the end of the line. - * @param {number} y The line in which to operate. + * @param x The column from which to start erasing to the end of the line. + * @param y The line in which to operate. */ public eraseRight(x: number, y: number): void { const line = this.buffer.lines.get(this.buffer.ybase + y); @@ -1706,8 +1705,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II /** * Erase in the identified line everything from "x" to the start of the line (left). - * @param {number} x The column from which to start erasing to the start of the line. - * @param {number} y The line in which to operate. + * @param x The column from which to start erasing to the start of the line. + * @param y The line in which to operate. */ public eraseLeft(x: number, y: number): void { const line = this.buffer.lines.get(this.buffer.ybase + y); @@ -1744,7 +1743,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II /** * Erase all content in the given line - * @param {number} y The line to erase all of its contents. + * @param y The line to erase all of its contents. */ public eraseLine(y: number): void { this.eraseRight(0, y); @@ -1752,9 +1751,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II /** * Return the data array of a blank line - * @param {boolean} cur First bunch of data for each "blank" character. - * @param {boolean} isWrapped Whether the new line is wrapped from the previous line. - * @param {boolean} cols The number of columns in the terminal, if this is not + * @param cur First bunch of data for each "blank" character. + * @param isWrapped Whether the new line is wrapped from the previous line. + * @param cols The number of columns in the terminal, if this is not * set, the terminal's current column count would be used. */ public blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData { @@ -1798,7 +1797,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II /** * Emit the 'data' event and populate the given data. - * @param {string} data The data to populate in the event. + * @param data The data to populate in the event. */ public handler(data: string): void { // Prevents all events to pty process if stdin is disabled @@ -1820,7 +1819,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II /** * Emit the 'title' event and populate the given title. - * @param {string} title The title to populate in the event. + * @param title The title to populate in the event. */ public handleTitle(title: string): void { /** diff --git a/src/handlers/Clipboard.ts b/src/handlers/Clipboard.ts index bbb8302c..b1acba9d 100644 --- a/src/handlers/Clipboard.ts +++ b/src/handlers/Clipboard.ts @@ -35,7 +35,7 @@ export function bracketTextForPaste(text: string, bracketedPasteMode: boolean): /** * Binds copy functionality to the given terminal. - * @param {ClipboardEvent} ev The original copy event to be handled + * @param ev The original copy event to be handled */ export function copyHandler(ev: ClipboardEvent, term: ITerminal, selectionManager: ISelectionManager): void { if (term.browser.isMSIE) { @@ -50,8 +50,8 @@ export function copyHandler(ev: ClipboardEvent, term: ITerminal, selectionManage /** * Redirect the clipboard's data to the terminal's input handler. - * @param {ClipboardEvent} ev The original paste event to be handled - * @param {Terminal} term The terminal on which to apply the handled paste event + * @param ev The original paste event to be handled + * @param term The terminal on which to apply the handled paste event */ export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void { ev.stopPropagation(); diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 406717b5..70b466d0 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -174,8 +174,8 @@ export class Renderer extends EventEmitter implements IRenderer { /** * Queues a refresh between two rows (inclusive), to be done on next animation * frame. - * @param {number} start The start row. - * @param {number} end The end row. + * @param start The start row. + * @param end The end row. */ public refreshRows(start: number, end: number): void { if (this._isPaused) { diff --git a/src/shared/utils/Browser.ts b/src/shared/utils/Browser.ts index a023031c..42c20d9d 100644 --- a/src/shared/utils/Browser.ts +++ b/src/shared/utils/Browser.ts @@ -22,8 +22,8 @@ export const isLinux = platform.indexOf('Linux') >= 0; /** * Return if the given array contains the given element - * @param {Array} array The array to search for the given element. - * @param {Object} el The element to look for into the array + * @param arr The array to search for the given element. + * @param el The element to look for into the array */ function contains(arr: any[], el: any): boolean { return arr.indexOf(el) >= 0; From 7b68cb9fe74b8c16a113a13f7ea37d9e8543883b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 26 Jul 2018 11:09:29 -0700 Subject: [PATCH 06/16] Support double click select word over wrapped lines Part of #1571 --- src/SelectionManager.ts | 41 +++++++++++++++++++++++++++++++++++++---- src/SelectionModel.ts | 8 ++++++-- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 1de14e70..20009105 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -9,7 +9,7 @@ import * as Browser from './shared/utils/Browser'; import { CharMeasure } from './ui/CharMeasure'; import { EventEmitter } from './EventEmitter'; import { SelectionModel } from './SelectionModel'; -import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer'; +import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_CODE_INDEX } from './Buffer'; import { AltClickHandler } from './handlers/AltClickHandler'; /** @@ -677,7 +677,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * Gets positional information for the word at the coordinated specified. * @param coords The coordinates to get the word at. */ - private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): IWordPosition { + private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean, followWrappedLinesAbove: boolean = true, followWrappedLinesBelow: boolean = true): IWordPosition { // Ensure coords are within viewport (eg. not within scroll bar) if (coords[0] >= this._terminal.cols) { return null; @@ -772,7 +772,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // Calculate the start _column_, converting the the string indexes back to // column coordinates. - const start = + let start = startIndex // The index of the selection's start char in the line string + charOffset // The difference between the initial char's column and index - leftWideCharCount // The number of wide chars left of the initial char @@ -780,7 +780,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // Calculate the length in _columns_, converting the the string indexes back // to column coordinates. - const length = Math.min(this._terminal.cols, // Disallow lengths larger than the terminal cols + let length = Math.min(this._terminal.cols, // Disallow lengths larger than the terminal cols endIndex // The index of the selection's end char in the line string - startIndex // The index of the selection's start char in the line string + leftWideCharCount // The number of wide chars left of the initial char @@ -792,6 +792,34 @@ export class SelectionManager extends EventEmitter implements ISelectionManager return null; } + // Recurse upwards if the line is wrapped and the word wraps to the above line + if (followWrappedLinesAbove) { + if (start === 0 && bufferLine[0][CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) { + const previousBufferLine = this._buffer.lines.get(coords[1] - 1); + if (previousBufferLine && (bufferLine).isWrapped && previousBufferLine[this._terminal.cols - 1][CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) { + const previousLineWordPosition = this._getWordAt([this._terminal.cols - 1, coords[1] - 1], false, true, false); + if (previousLineWordPosition) { + const offset = this._terminal.cols - previousLineWordPosition.start; + start -= offset; + length += offset; + } + } + } + } + + // Recurse downwards if the line is wrapped and the word wraps to the next line + if (followWrappedLinesBelow) { + if (start + length === this._terminal.cols && bufferLine[this._terminal.cols - 1][CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) { + const nextBufferLine = this._buffer.lines.get(coords[1] + 1); + if (nextBufferLine && (nextBufferLine).isWrapped && nextBufferLine[0][CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) { + const nextLineWordPosition = this._getWordAt([0, coords[1] + 1], false, false, true); + if (nextLineWordPosition) { + length += nextLineWordPosition.length; + } + } + } + } + return { start, length }; } @@ -803,6 +831,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager protected _selectWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): void { const wordPosition = this._getWordAt(coords, allowWhitespaceOnlySelection); if (wordPosition) { + // Adjust negative start values + while (wordPosition.start < 0) { + wordPosition.start += this._terminal.cols; + coords[1]--; + } this._model.selectionStart = [wordPosition.start, coords[1]]; this._model.selectionStartLength = wordPosition.length; } diff --git a/src/SelectionModel.ts b/src/SelectionModel.ts index a9a3c89e..a92ba933 100644 --- a/src/SelectionModel.ts +++ b/src/SelectionModel.ts @@ -77,8 +77,12 @@ export class SelectionModel { } // Use the selection start if the end doesn't exist or they're reversed - if (!this.selectionEnd || this.areSelectionValuesReversed()) { - return [this.selectionStart[0] + this.selectionStartLength, this.selectionStart[1]]; + if (!this.selectionEnd) { + const startPlusLength = this.selectionStart[0] + this.selectionStartLength; + if (startPlusLength > this._terminal.cols) { + return [startPlusLength % this._terminal.cols, this.selectionStart[1] + Math.floor(startPlusLength / this._terminal.cols)]; + } + return [startPlusLength, this.selectionStart[1]]; } // Ensure the the word/line is selected after a double/triple click From 6a9da971637d5b4f06db12cfc97b1a9b02df3725 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 26 Jul 2018 11:15:34 -0700 Subject: [PATCH 07/16] Cover select to word downwards --- src/SelectionManager.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 20009105..3086fd41 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -831,7 +831,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager protected _selectWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): void { const wordPosition = this._getWordAt(coords, allowWhitespaceOnlySelection); if (wordPosition) { - // Adjust negative start values + // Adjust negative start value while (wordPosition.start < 0) { wordPosition.start += this._terminal.cols; coords[1]--; @@ -848,7 +848,21 @@ export class SelectionManager extends EventEmitter implements ISelectionManager private _selectToWordAt(coords: [number, number]): void { const wordPosition = this._getWordAt(coords, true); if (wordPosition) { - this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : (wordPosition.start + wordPosition.length), coords[1]]; + let endRow = coords[1]; + + // Adjust negative start value + while (wordPosition.start < 0) { + wordPosition.start += this._terminal.cols; + endRow--; + } + + // Adjust wrapped length value + while (wordPosition.start + wordPosition.length > this._terminal.cols) { + wordPosition.length -= this._terminal.cols; + endRow++; + } + + this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : (wordPosition.start + wordPosition.length), endRow]; } } From 48f1c4667aacb97d71a0db0ecf90901a88a2e4e8 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 26 Jul 2018 11:30:49 -0700 Subject: [PATCH 08/16] Fix select to word upwards --- src/SelectionManager.ts | 13 ++++++++----- src/SelectionModel.ts | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 3086fd41..3fcc600a 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -856,13 +856,16 @@ export class SelectionManager extends EventEmitter implements ISelectionManager endRow--; } - // Adjust wrapped length value - while (wordPosition.start + wordPosition.length > this._terminal.cols) { - wordPosition.length -= this._terminal.cols; - endRow++; + // Adjust wrapped length value, this only needs to happen when values are reversed as in that + // case we're interested in the start of the word, not the end + if (!this._model.areSelectionValuesReversed()) { + while (wordPosition.start + wordPosition.length > this._terminal.cols) { + wordPosition.length -= this._terminal.cols; + endRow++; + } } - this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : (wordPosition.start + wordPosition.length), endRow]; + this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : wordPosition.start + wordPosition.length, endRow]; } } diff --git a/src/SelectionModel.ts b/src/SelectionModel.ts index a92ba933..b8e770b7 100644 --- a/src/SelectionModel.ts +++ b/src/SelectionModel.ts @@ -77,7 +77,7 @@ export class SelectionModel { } // Use the selection start if the end doesn't exist or they're reversed - if (!this.selectionEnd) { + if (!this.selectionEnd || this.areSelectionValuesReversed()) { const startPlusLength = this.selectionStart[0] + this.selectionStartLength; if (startPlusLength > this._terminal.cols) { return [startPlusLength % this._terminal.cols, this.selectionStart[1] + Math.floor(startPlusLength / this._terminal.cols)]; From 67d6ad57c3719301e082d468c28ce4bd44fef4b4 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 26 Jul 2018 12:23:34 -0700 Subject: [PATCH 09/16] Add tests --- src/SelectionManager.test.ts | 36 ++++++++++++++++++++++++++++++++++++ src/SelectionManager.ts | 6 ++++-- src/SelectionModel.test.ts | 5 +++++ src/SelectionModel.ts | 2 +- 4 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index 4ae0c08f..20e1ca60 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -183,6 +183,42 @@ describe('SelectionManager', () => { selectionManager.selectWordAt([15, 0]); assert.equal(selectionManager.selectionText, 'ij"'); }); + it('should expand upwards or downards for wrapped lines', () => { + buffer.lines.set(0, stringToRow(' foo')); + buffer.lines.set(1, stringToRow('bar ')); + (buffer.lines.get(1)).isWrapped = true; + selectionManager.selectWordAt([1, 1]); + assert.equal(selectionManager.selectionText, 'foobar'); + selectionManager.model.clearSelection(); + selectionManager.selectWordAt([78, 0]); + assert.equal(selectionManager.selectionText, 'foobar'); + }); + it('should expand both upwards and downwards for word wrapped over many lines', () => { + const expectedText = 'fooaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccbar'; + buffer.lines.set(0, stringToRow(' foo')); + buffer.lines.set(1, stringToRow('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')); + buffer.lines.set(2, stringToRow('bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb')); + buffer.lines.set(3, stringToRow('cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc')); + buffer.lines.set(4, stringToRow('bar ')); + (buffer.lines.get(1)).isWrapped = true; + (buffer.lines.get(2)).isWrapped = true; + (buffer.lines.get(3)).isWrapped = true; + (buffer.lines.get(4)).isWrapped = true; + selectionManager.selectWordAt([78, 0]); + assert.equal(selectionManager.selectionText, expectedText); + selectionManager.model.clearSelection(); + selectionManager.selectWordAt([40, 1]); + assert.equal(selectionManager.selectionText, expectedText); + selectionManager.model.clearSelection(); + selectionManager.selectWordAt([40, 2]); + assert.equal(selectionManager.selectionText, expectedText); + selectionManager.model.clearSelection(); + selectionManager.selectWordAt([40, 3]); + assert.equal(selectionManager.selectionText, expectedText); + selectionManager.model.clearSelection(); + selectionManager.selectWordAt([1, 4]); + assert.equal(selectionManager.selectionText, expectedText); + }); describe('emoji', () => { it('should treat a single emoji as a word when wrapped in spaces', () => { buffer.lines.set(0, stringToRow(' ⚽ a')); // The a is here to prevent the space being trimmed in selectionText diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 3fcc600a..3731eb9a 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -451,8 +451,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * Removes the listeners that are registered when mousedown is triggered. */ private _removeMouseDownListeners(): void { - this._terminal.element.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener); - this._terminal.element.ownerDocument.removeEventListener('mouseup', this._mouseUpListener); + if (this._terminal.element.ownerDocument) { + this._terminal.element.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener); + this._terminal.element.ownerDocument.removeEventListener('mouseup', this._mouseUpListener); + } clearInterval(this._dragScrollIntervalTimer); this._dragScrollIntervalTimer = null; } diff --git a/src/SelectionModel.test.ts b/src/SelectionModel.test.ts index 85486bab..59b2ce75 100644 --- a/src/SelectionModel.test.ts +++ b/src/SelectionModel.test.ts @@ -128,6 +128,11 @@ describe('SelectionManager', () => { model.selectionEnd = [3, 2]; assert.deepEqual(model.finalSelectionEnd, [4, 2]); }); + it('should return the end on a different row when start + length overflows onto a following row', () => { + model.selectionStart = [78, 2]; + model.selectionStartLength = 4; + assert.deepEqual(model.finalSelectionEnd, [2, 3]); + }); it('should return selection end if selection end is after selection start + length', () => { model.selectionStart = [2, 2]; model.selectionStartLength = 2; diff --git a/src/SelectionModel.ts b/src/SelectionModel.ts index b8e770b7..f87667f2 100644 --- a/src/SelectionModel.ts +++ b/src/SelectionModel.ts @@ -76,7 +76,7 @@ export class SelectionModel { return null; } - // Use the selection start if the end doesn't exist or they're reversed + // Use the selection start + length if the end doesn't exist or they're reversed if (!this.selectionEnd || this.areSelectionValuesReversed()) { const startPlusLength = this.selectionStart[0] + this.selectionStartLength; if (startPlusLength > this._terminal.cols) { From 05f136b3dafec35e02967f2b58f8aabc80e3cb1f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 28 Jul 2018 12:42:11 -0700 Subject: [PATCH 10/16] Add link to GitHub dependents to real world uses --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index bc1deeb7..bfb5270d 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,8 @@ computational environment for Jupyter, supporting interactive data science and s - [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js - [**SSH Web Client**](https://github.com/roke22/PHP-SSH2-Web-Client): SSH Web Client with PHP. +[And much more...](https://github.com/xtermjs/xterm.js/network/dependents) + 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. Note: Please add any new contributions to the end of the list only. ## Releases From c65dd1f476c0a273a85779457f7b93b74d10592c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 29 Jul 2018 13:05:27 -0700 Subject: [PATCH 11/16] Use const for null cell Fixes #1584 --- src/Buffer.ts | 6 +++++- src/InputHandler.ts | 14 +++++++------- src/Terminal.ts | 12 ++++++------ src/addons/winptyCompat/winptyCompat.ts | 5 ++++- src/renderer/TextRenderLayer.ts | 4 ++-- src/renderer/dom/DomRendererRowFactory.test.ts | 4 ++-- src/utils/TestUtils.test.ts | 4 ++-- 7 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 5183009f..5d45645f 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -15,6 +15,10 @@ export const CHAR_DATA_WIDTH_INDEX = 2; export const CHAR_DATA_CODE_INDEX = 3; export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1 +export const NULL_CELL_CHAR = ' '; +export const NULL_CELL_WIDTH = 1; +export const NULL_CELL_CODE = 32; + /** * This class represents a terminal buffer (an internal state of the terminal), where the * following information is stored (in high-level): @@ -117,7 +121,7 @@ export class Buffer implements IBuffer { if (this.lines.length > 0) { // Deal with columns increasing (we don't do anything when columns reduce) if (this._terminal.cols < newCols) { - const ch: CharData = [DEFAULT_ATTR, ' ', 1, 32]; // does xterm use the default attr? + const ch: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // does xterm use the default attr? for (let i = 0; i < this.lines.length; i++) { while (this.lines.get(i).length < newCols) { this.lines.get(i).push(ch); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 3fda521f..b97152be 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -7,7 +7,7 @@ import { CharData, IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer } from './Types'; import { C0, C1 } from './common/data/EscapeSequences'; import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets'; -import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR } from './Buffer'; +import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer'; import { FLAGS } from './renderer/Types'; import { wcwidth } from './CharWidth'; import { EscapeSequenceParser } from './EscapeSequenceParser'; @@ -431,11 +431,11 @@ export class InputHandler extends Disposable implements IInputHandler { if (removed[CHAR_DATA_WIDTH_INDEX] === 0 && bufferRow[this._terminal.cols - 2] && bufferRow[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) { - bufferRow[this._terminal.cols - 2] = [curAttr, ' ', 1, 32 /* ' '.charCodeAt(0) */ ]; + bufferRow[this._terminal.cols - 2] = [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; } // insert empty cell at cursor - bufferRow.splice(buffer.x, 0, [curAttr, ' ', 1, 32 /* ' '.charCodeAt(0) */ ]); + bufferRow.splice(buffer.x, 0, [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); } } @@ -547,7 +547,7 @@ export class InputHandler extends Disposable implements IInputHandler { const row = buffer.y + buffer.ybase; let j = buffer.x; - const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm + const ch: CharData = [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm while (param-- && j < this._terminal.cols) { buffer.lines.get(row).splice(j++, 0, ch); @@ -857,7 +857,7 @@ export class InputHandler extends Disposable implements IInputHandler { const buffer = this._terminal.buffer; const row = buffer.y + buffer.ybase; - const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm + const ch: CharData = [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm while (param--) { buffer.lines.get(row).splice(buffer.x, 1); @@ -919,7 +919,7 @@ export class InputHandler extends Disposable implements IInputHandler { const row = buffer.y + buffer.ybase; let j = buffer.x; - const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm + const ch: CharData = [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm while (param-- && j < this._terminal.cols) { buffer.lines.get(row)[j++] = ch; @@ -981,7 +981,7 @@ export class InputHandler extends Disposable implements IInputHandler { const buffer = this._terminal.buffer; const line = buffer.lines.get(buffer.ybase + buffer.y); - const ch = line[buffer.x - 1] || [DEFAULT_ATTR, ' ', 1, 32]; + const ch = line[buffer.x - 1] || [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; while (param--) { line[buffer.x++] = ch; diff --git a/src/Terminal.ts b/src/Terminal.ts index e8366290..8bd385f0 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -25,7 +25,7 @@ import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions import { IMouseZoneManager } from './ui/Types'; import { IRenderer } from './renderer/Types'; import { BufferSet } from './BufferSet'; -import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR } from './Buffer'; +import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer'; import { CompositionHelper } from './CompositionHelper'; import { EventEmitter } from './EventEmitter'; import { Viewport } from './Viewport'; @@ -1697,7 +1697,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II if (!line) { return; } - const ch: CharData = [this.eraseAttr(), ' ', 1, 32 /* ' '.charCodeAt(0) */]; // xterm + const ch: CharData = [this.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm for (; x < this.cols; x++) { line[x] = ch; } @@ -1714,7 +1714,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II if (!line) { return; } - const ch: CharData = [this.eraseAttr(), ' ', 1, 32 /* ' '.charCodeAt(0) */]; // xterm + const ch: CharData = [this.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm x++; while (x--) { line[x] = ch; @@ -1760,7 +1760,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II public blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData { const attr = cur ? this.eraseAttr() : DEFAULT_ATTR; - const ch: CharData = [attr, ' ', 1, 32 /* ' '.charCodeAt(0) */]; // width defaults to 1 halfwidth character + const ch: CharData = [attr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // width defaults to 1 halfwidth character const line: LineData = []; // TODO: It is not ideal that this is a property on an array, a buffer line @@ -1783,9 +1783,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II */ public ch(cur?: boolean): CharData { if (cur) { - return [this.eraseAttr(), ' ', 1, 32 /* ' '.charCodeAt(0) */]; + return [this.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; } - return [DEFAULT_ATTR, ' ', 1, 32 /* ' '.charCodeAt(0) */]; + return [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; } /** diff --git a/src/addons/winptyCompat/winptyCompat.ts b/src/addons/winptyCompat/winptyCompat.ts index 84b21590..25ad7d91 100644 --- a/src/addons/winptyCompat/winptyCompat.ts +++ b/src/addons/winptyCompat/winptyCompat.ts @@ -6,6 +6,9 @@ import { Terminal } from 'xterm'; import { IWinptyCompatAddonTerminal } from './Interfaces'; +const CHAR_DATA_CODE_INDEX = 3; +const NULL_CELL_CODE = 32; + export function winptyCompatInit(terminal: Terminal): void { const addonTerminal = terminal; @@ -29,7 +32,7 @@ export function winptyCompatInit(terminal: Terminal): void { const line = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y - 1); const lastChar = line[addonTerminal.cols - 1]; - if (lastChar[3] !== 32 /* ' ' */) { + if (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE) { const nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y); (nextLine).isWrapped = true; } diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts index e57276d7..d40aa28d 100644 --- a/src/renderer/TextRenderLayer.ts +++ b/src/renderer/TextRenderLayer.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../Buffer'; +import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, NULL_CELL_CODE } from '../Buffer'; import { FLAGS, IColorSet, IRenderDimensions, ICharacterJoinerRegistry } from './Types'; import { CharData, ITerminal } from '../Types'; import { INVERTED_DEFAULT_COLOR } from './atlas/Types'; @@ -124,7 +124,7 @@ export class TextRenderLayer extends BaseRenderLayer { // get removed, and `a` would not re-render because it thinks it's // already in the correct state. // this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA; - if (lastCharX < line.length - 1 && line[lastCharX + 1][CHAR_DATA_CODE_INDEX] === 32 /*' '*/) { + if (lastCharX < line.length - 1 && line[lastCharX + 1][CHAR_DATA_CODE_INDEX] === NULL_CELL_CODE) { width = 2; // this._clearChar(x + 1, y); // The overlapping char's char data will force a clear and render when the diff --git a/src/renderer/dom/DomRendererRowFactory.test.ts b/src/renderer/dom/DomRendererRowFactory.test.ts index e91c71fe..c90dd6e5 100644 --- a/src/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/renderer/dom/DomRendererRowFactory.test.ts @@ -7,7 +7,7 @@ import jsdom = require('jsdom'); import { assert } from 'chai'; import { DomRendererRowFactory } from './DomRendererRowFactory'; import { LineData } from '../../Types'; -import { DEFAULT_ATTR } from '../../Buffer'; +import { DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from '../../Buffer'; import { FLAGS } from '../Types'; describe('DomRendererRowFactory', () => { @@ -149,7 +149,7 @@ describe('DomRendererRowFactory', () => { function createEmptyLineData(cols: number): LineData { const lineData: LineData = []; for (let i = 0; i < cols; i++) { - lineData.push([DEFAULT_ATTR, ' ', 1, 32 /* ' '.charCodeAt(0) */]); + lineData.push([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); } return lineData; } diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index 78a1942b..e2e8e393 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -5,7 +5,7 @@ import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Types'; import { LineData, IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ICircularList, ILinkifier, IMouseHelper, ILinkMatcherOptions, XtermListener, CharacterJoinerHandler } from '../Types'; -import { Buffer } from '../Buffer'; +import { Buffer, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from '../Buffer'; import * as Browser from '../shared/utils/Browser'; import { ITheme, IDisposable, IMarker } from 'xterm'; @@ -152,7 +152,7 @@ export class MockTerminal implements ITerminal { const line: LineData = []; cols = cols || this.cols; for (let i = 0; i < cols; i++) { - line.push([0, ' ', 1, 32]); + line.push([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); } return line; } From 5aaf9ef11b3afb046d7b77758c0dfeb18571d629 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 29 Jul 2018 13:49:13 -0700 Subject: [PATCH 12/16] Allow renderer to be swapped in and out at runtime Fixes #1589 --- demo/main.js | 7 +++---- src/Terminal.ts | 35 ++++++++++++++++++++++++++------- src/common/Lifecycle.ts | 12 +++++++++++ src/renderer/BaseRenderLayer.ts | 4 ++++ src/renderer/Renderer.ts | 5 +++++ src/renderer/Types.ts | 2 +- src/renderer/dom/DomRenderer.ts | 9 +++++++++ 7 files changed, 62 insertions(+), 12 deletions(-) diff --git a/demo/main.js b/demo/main.js index a9b11655..70544e38 100644 --- a/demo/main.js +++ b/demo/main.js @@ -174,9 +174,7 @@ function initOptions(term) { 'termName', 'useFlowControl', // Complex option - 'theme', - // Only in constructor - 'rendererType' + 'theme' ]; var stringOptions = { bellSound: null, @@ -185,7 +183,8 @@ function initOptions(term) { experimentalCharAtlas: ['none', 'static', 'dynamic'], fontFamily: null, fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], - fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'] + fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], + rendererType: ['dom', 'canvas'] }; var options = Object.keys(term._core.options); var booleanOptions = []; diff --git a/src/Terminal.ts b/src/Terminal.ts index e8366290..43ccaa08 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -72,7 +72,7 @@ const WRITE_BATCH_SIZE = 300; /** * The set of options that only have an effect when set in the Terminal constructor. */ -const CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows', 'rendererType']; +const CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows']; const DEFAULT_OPTIONS: ITerminalOptions = { cols: 80, @@ -370,6 +370,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II if (CONSTRUCTOR_ONLY_OPTIONS.indexOf(key) !== -1) { console.error(`Option "${key}" can only be set in the constructor`); } + if (this.options[key] === value) { + return; + } switch (key) { case 'bellStyle': if (!value) { @@ -396,6 +399,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II console.warn(`${key} cannot be less than 1, value: ${value}`); return; } + case 'rendererType': + if (!value) { + value = 'canvas'; + } + break; case 'tabStopWidth': if (value < 1) { console.warn(`${key} cannot be less than 1, value: ${value}`); @@ -455,6 +463,15 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.renderer.onResize(this.cols, this.rows); this.refresh(0, this.rows - 1); } + case 'rendererType': + if (this.renderer) { + this.unregister(this.renderer); + this.renderer.dispose(); + this.renderer = null; + } + this._setupRenderer(); + this.renderer.onCharSizeChanged(); + break; case 'scrollback': this.buffers.resize(this.cols, this.rows); if (this.viewport) { @@ -674,12 +691,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // Performance: Add viewport and helper elements from the fragment this.element.appendChild(fragment); - switch (this.options.rendererType) { - case 'canvas': this.renderer = new Renderer(this, this.options.theme); break; - case 'dom': this.renderer = new DomRenderer(this, this.options.theme); break; - default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`); - } - this.register(this.renderer); + this._setupRenderer(); this.options.theme = null; this.viewport = new Viewport(this, this._viewportElement, this._viewportScrollArea, this.charMeasure); this.viewport.onThemeChanged(this.renderer.colorManager.colors); @@ -736,6 +748,15 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } + private _setupRenderer(): void { + switch (this.options.rendererType) { + case 'canvas': this.renderer = new Renderer(this, this.options.theme); break; + case 'dom': this.renderer = new DomRenderer(this, this.options.theme); break; + default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`); + } + this.register(this.renderer); + } + /** * Sets the theme on the renderer. The renderer must have been initialized. * @param theme The theme to ste. diff --git a/src/common/Lifecycle.ts b/src/common/Lifecycle.ts index b9cf1418..46828521 100644 --- a/src/common/Lifecycle.ts +++ b/src/common/Lifecycle.ts @@ -30,4 +30,16 @@ export abstract class Disposable implements IDisposable { public register(d: T): void { this._disposables.push(d); } + + /** + * Unregisters a disposable object if it has been registered, if not do + * nothing. + * @param d The disposable to unregister. + */ + public unregister(d: T): void { + const index = this._disposables.indexOf(d); + if (index !== -1) { + this._disposables.splice(index, 1); + } + } } diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 111e63ff..19c23dc0 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -36,6 +36,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._container.appendChild(this._canvas); } + public dispose(): void { + this._container.removeChild(this._canvas); + } + private _initCanvas(): void { this._ctx = this._canvas.getContext('2d', {alpha: this._alpha}); // Draw the background if this is an opaque layer diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 406717b5..b7a3be1d 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -76,6 +76,11 @@ export class Renderer extends EventEmitter implements IRenderer { } } + public dispose(): void { + super.dispose(); + this._renderLayers.forEach(l => l.dispose()); + } + public onIntersectionChange(entry: IntersectionObserverEntry): void { this._isPaused = entry.intersectionRatio === 0; if (!this._isPaused && this._needsFullRefresh) { diff --git a/src/renderer/Types.ts b/src/renderer/Types.ts index 4006f28c..824304ed 100644 --- a/src/renderer/Types.ts +++ b/src/renderer/Types.ts @@ -66,7 +66,7 @@ export interface IRenderDimensions { actualCellHeight: number; } -export interface IRenderLayer { +export interface IRenderLayer extends IDisposable { /** * Called when the terminal loses focus. */ diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index e336c79e..32dea6ed 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -81,6 +81,15 @@ export class DomRenderer extends EventEmitter implements IRenderer { this._terminal.screenElement.appendChild(this._selectionContainer); } + public dispose(): void { + this._terminal.element.classList.remove(TERMINAL_CLASS_PREFIX + this._terminalClass); + this._terminal.screenElement.removeChild(this._rowContainer); + this._terminal.screenElement.removeChild(this._selectionContainer); + this._terminal.screenElement.removeChild(this._themeStyleElement); + this._terminal.screenElement.removeChild(this._dimensionsStyleElement); + super.dispose(); + } + private _updateDimensions(): void { this.dimensions.scaledCharWidth = this._terminal.charMeasure.width * window.devicePixelRatio; this.dimensions.scaledCharHeight = this._terminal.charMeasure.height * window.devicePixelRatio; From 804cb36adcd7e308dc990dff0a748532b52938b6 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 29 Jul 2018 14:08:02 -0700 Subject: [PATCH 13/16] Retain theme when renderers are swapped --- src/Terminal.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 43ccaa08..2650ae33 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -207,6 +207,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II public mouseHelper: MouseHelper; private _accessibilityManager: AccessibilityManager; private _screenDprMonitor: ScreenDprMonitor; + private _theme: ITheme; public cols: number; public rows: number; @@ -471,6 +472,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } this._setupRenderer(); this.renderer.onCharSizeChanged(); + if (this._theme) { + this.renderer.setTheme(this._theme); + } break; case 'scrollback': this.buffers.resize(this.cols, this.rows); @@ -754,14 +758,16 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II case 'dom': this.renderer = new DomRenderer(this, this.options.theme); break; default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`); } + this._theme = this.options.theme; this.register(this.renderer); } /** * Sets the theme on the renderer. The renderer must have been initialized. - * @param theme The theme to ste. + * @param theme The theme to set. */ private _setTheme(theme: ITheme): void { + this._theme = theme; const colors = this.renderer.setTheme(theme); if (this.viewport) { this.viewport.onThemeChanged(colors); From 4a3c1a4cc61e98d635d439b4b667d82412343399 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 29 Jul 2018 14:25:55 -0700 Subject: [PATCH 14/16] Store theme in right place --- src/Terminal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 2650ae33..040581c4 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -696,6 +696,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.element.appendChild(fragment); this._setupRenderer(); + this._theme = this.options.theme; this.options.theme = null; this.viewport = new Viewport(this, this._viewportElement, this._viewportScrollArea, this.charMeasure); this.viewport.onThemeChanged(this.renderer.colorManager.colors); @@ -758,7 +759,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II case 'dom': this.renderer = new DomRenderer(this, this.options.theme); break; default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`); } - this._theme = this.options.theme; this.register(this.renderer); } From 882aea357fae2445474eaceef4cb5fd7bcd8c4e2 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 3 Aug 2018 11:10:51 -0700 Subject: [PATCH 15/16] Fix NPE in AccessibilityManager Fixes #1596 --- src/AccessibilityManager.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/AccessibilityManager.ts b/src/AccessibilityManager.ts index 0975e0b6..1a2de1d7 100644 --- a/src/AccessibilityManager.ts +++ b/src/AccessibilityManager.ts @@ -72,7 +72,7 @@ export class AccessibilityManager extends Disposable { this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot); this.register(this._renderRowsDebouncer); - this.register(this._terminal.addDisposableListener('resize', data => this._onResize(data.cols, data.rows))); + this.register(this._terminal.addDisposableListener('resize', data => this._onResize(data.rows))); this.register(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end))); this.register(this._terminal.addDisposableListener('scroll', data => this._refreshRows())); // Line feed is an issue as the prompt won't be read out after a command is run @@ -157,7 +157,7 @@ export class AccessibilityManager extends Disposable { e.stopImmediatePropagation(); } - private _onResize(cols: number, rows: number): void { + private _onResize(rows: number): void { // Remove bottom boundary listener this._rowElements[this._rowElements.length - 1].removeEventListener('focus', this._bottomBoundaryFocusListener); @@ -259,6 +259,9 @@ export class AccessibilityManager extends Disposable { if (!this._terminal.renderer.dimensions.actualCellHeight) { return; } + if (this._rowElements.length !== this._terminal.rows) { + this._onResize(this._terminal.rows); + } for (let i = 0; i < this._terminal.rows; i++) { this._refreshRowDimensions(this._rowElements[i]); } From 490f0ad329a0bcb46e099e09aa86d818e7753cf6 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 3 Aug 2018 19:20:21 -0700 Subject: [PATCH 16/16] Remove Travis CI badge --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bfb5270d..9314db38 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # [![xterm.js logo](logo-full.png)](https://xtermjs.org) -[![Travis CI build status](https://api.travis-ci.org/xtermjs/xterm.js.svg)](https://travis-ci.org/xtermjs/xterm.js) [![VSTS Build status](https://xtermjs.visualstudio.com/_apis/public/build/definitions/3e323cf7-5760-460d-af64-ee5675baf366/1/badge)](https://xtermjs.visualstudio.com/xterm.js/_build/index?definitionId=1) [![Coverage Status](https://coveralls.io/repos/github/xtermjs/xterm.js/badge.svg?branch=master)](https://coveralls.io/github/xtermjs/xterm.js?branch=master) [![Gitter](https://badges.gitter.im/sourcelair/xterm.js.svg)](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/xterm/badge?style=rounded)](https://www.jsdelivr.com/package/npm/xterm) +[![VSTS Build status](https://xtermjs.visualstudio.com/_apis/public/build/definitions/3e323cf7-5760-460d-af64-ee5675baf366/1/badge)](https://xtermjs.visualstudio.com/xterm.js/_build/index?definitionId=1) +[![Coverage Status](https://coveralls.io/repos/github/xtermjs/xterm.js/badge.svg?branch=master)](https://coveralls.io/github/xtermjs/xterm.js?branch=master) +[![Gitter](https://badges.gitter.im/sourcelair/xterm.js.svg)](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) +[![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/xterm/badge?style=rounded)](https://www.jsdelivr.com/package/npm/xterm) Xterm.js is a terminal front-end component written in JavaScript that works in the browser.