diff --git a/src/Buffer.ts b/src/Buffer.ts index eb346d5bc1..46fe6ba8e5 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -6,6 +6,9 @@ import { ITerminal, IBuffer } from './Interfaces'; import { CircularList } from './utils/CircularList'; import { LineData, CharData } from './Types'; +export const CHAR_DATA_CHAR_INDEX = 1; +export const CHAR_DATA_WIDTH_INDEX = 2; + /** * This class represents a terminal buffer (an internal state of the terminal), where the * following information is stored (in high-level): @@ -146,4 +149,52 @@ export class Buffer implements IBuffer { this.scrollTop = 0; this.scrollBottom = newRows - 1; } + + /** + * Translates a buffer line to a string, with optional start and end columns. + * Wide characters will count as two columns in the resulting string. This + * function is useful for getting the actual text underneath the raw selection + * position. + * @param line The line being translated. + * @param trimRight Whether to trim whitespace to the right. + * @param startCol The column to start at. + * @param endCol The column to end at. + */ + public translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol: number = 0, endCol: number = null): string { + // Get full line + let lineString = ''; + let widthAdjustedStartCol = startCol; + let widthAdjustedEndCol = endCol; + const line = this.lines.get(lineIndex); + for (let i = 0; i < line.length; i++) { + const char = line[i]; + lineString += char[CHAR_DATA_CHAR_INDEX]; + // Adjust start and end cols for wide characters if they affect their + // column indexes + if (char[CHAR_DATA_WIDTH_INDEX] === 0) { + if (startCol >= i) { + widthAdjustedStartCol--; + } + if (endCol >= i) { + widthAdjustedEndCol--; + } + } + } + + // Calculate the final end col by trimming whitespace on the right of the + // line if needed. + let finalEndCol = widthAdjustedEndCol || line.length; + if (trimRight) { + const rightWhitespaceIndex = lineString.search(/\s+$/); + if (rightWhitespaceIndex !== -1) { + finalEndCol = Math.min(finalEndCol, rightWhitespaceIndex); + } + // Return the empty string if only trimmed whitespace is selected + if (finalEndCol <= widthAdjustedStartCol) { + return ''; + } + } + + return lineString.substring(widthAdjustedStartCol, finalEndCol); + } } diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 5362cae9af..1c54abef15 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -6,6 +6,7 @@ import { IInputHandler, ITerminal, IInputHandlingTerminal } from './Interfaces'; import { C0 } from './EscapeSequences'; import { DEFAULT_CHARSET } from './Charsets'; import { CharData } from './Types'; +import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer'; /** * The terminal's standard implementation of IInputHandler, this handles all @@ -34,14 +35,14 @@ export class InputHandler implements IInputHandler { if (!ch_width && this._terminal.buffer.x) { // dont overflow left if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1]) { - if (!this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][2]) { + if (!this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) { // found empty cell after fullwidth, need to go 2 cells back if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2]) - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2][1] += char; + this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char; } else { - this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][1] += char; + this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char; } this._terminal.updateRange(this._terminal.buffer.y); } @@ -77,9 +78,9 @@ export class InputHandler implements IInputHandler { // remove last cell, if it's width is 0 // we have to adjust the second last cell as well const removed = this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).pop(); - if (removed[2] === 0 + if (removed[CHAR_DATA_WIDTH_INDEX] === 0 && this._terminal.buffer.lines.get(row)[this._terminal.cols - 2] - && this._terminal.buffer.lines.get(row)[this._terminal.cols - 2][2] === 2) { + && this._terminal.buffer.lines.get(row)[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) { this._terminal.buffer.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1]; } @@ -945,6 +946,7 @@ export class InputHandler implements IInputHandler { case 47: // alt screen buffer case 1047: // alt screen buffer this._terminal.buffers.activateAltBuffer(); + this._terminal.selectionManager.setBuffer(this._terminal.buffer); this._terminal.viewport.syncScrollArea(); this._terminal.showCursor(); break; @@ -1113,7 +1115,7 @@ export class InputHandler implements IInputHandler { // if (params[0] === 1049) { // this.restoreCursor(params); // } - this._terminal.selectionManager.setBuffer(this._terminal.buffer.lines); + this._terminal.selectionManager.setBuffer(this._terminal.buffer); this._terminal.refresh(0, this._terminal.rows - 1); this._terminal.viewport.syncScrollArea(); this._terminal.showCursor(); diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 55317fbc83..f97d929417 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -144,6 +144,7 @@ export interface IBuffer { scrollTop: number; savedY: number; savedX: number; + translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string; } export interface IBufferSet { @@ -166,7 +167,7 @@ export interface ISelectionManager { disable(): void; enable(): void; - setBuffer(buffer: ICircularList): void; + setBuffer(buffer: IBuffer): void; setSelection(row: number, col: number, length: number): void; } diff --git a/src/Renderer.ts b/src/Renderer.ts index f706bffcb3..f2999afaf5 100644 --- a/src/Renderer.ts +++ b/src/Renderer.ts @@ -4,6 +4,7 @@ import { ITerminal } from './Interfaces'; import { DomElementObjectPool } from './utils/DomElementObjectPool'; +import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer'; /** * The maximum number of refresh frames to skip when the write buffer is non- @@ -168,8 +169,8 @@ export class Renderer { for (let i = 0; i < width; i++) { // TODO: Could data be a more specific type? let data: any = line[i][0]; - const ch = line[i][1]; - const ch_width: any = line[i][2]; + const ch = line[i][CHAR_DATA_CHAR_INDEX]; + const ch_width: any = line[i][CHAR_DATA_WIDTH_INDEX]; const isCursor: boolean = i === x; if (!ch_width) { continue; diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index 698cda7eca..c7fd9aef63 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -4,7 +4,7 @@ import jsdom = require('jsdom'); import { assert } from 'chai'; -import { ITerminal, ICircularList } from './Interfaces'; +import { ITerminal, ICircularList, IBuffer } from './Interfaces'; import { CharMeasure } from './utils/CharMeasure'; import { CircularList } from './utils/CircularList'; import { SelectionManager } from './SelectionManager'; @@ -16,7 +16,7 @@ import { LineData } from './Types'; class TestSelectionManager extends SelectionManager { constructor( terminal: ITerminal, - buffer: ICircularList, + buffer: IBuffer, rowContainer: HTMLElement, charMeasure: CharMeasure ) { @@ -40,7 +40,7 @@ describe('SelectionManager', () => { let document: Document; let terminal: ITerminal; - let bufferLines: ICircularList; + let buffer: IBuffer; let rowContainer: HTMLElement; let selectionManager: TestSelectionManager; @@ -55,8 +55,8 @@ describe('SelectionManager', () => { terminal.options.scrollback = 100; terminal.buffers = new BufferSet(terminal); terminal.buffer = terminal.buffers.active; - bufferLines = terminal.buffer.lines; - selectionManager = new TestSelectionManager(terminal, bufferLines, rowContainer, null); + buffer = terminal.buffer; + selectionManager = new TestSelectionManager(terminal, buffer, rowContainer, null); }); function stringToRow(text: string): LineData { @@ -69,7 +69,7 @@ describe('SelectionManager', () => { describe('_selectWordAt', () => { it('should expand selection for normal width chars', () => { - bufferLines.set(0, stringToRow('foo bar')); + buffer.lines.set(0, stringToRow('foo bar')); selectionManager.selectWordAt([0, 0]); assert.equal(selectionManager.selectionText, 'foo'); selectionManager.selectWordAt([1, 0]); @@ -86,7 +86,7 @@ describe('SelectionManager', () => { assert.equal(selectionManager.selectionText, 'bar'); }); it('should expand selection for whitespace', () => { - bufferLines.set(0, stringToRow('a b')); + buffer.lines.set(0, stringToRow('a b')); selectionManager.selectWordAt([0, 0]); assert.equal(selectionManager.selectionText, 'a'); selectionManager.selectWordAt([1, 0]); @@ -100,7 +100,7 @@ describe('SelectionManager', () => { }); it('should expand selection for wide characters', () => { // Wide characters use a special format - bufferLines.set(0, [ + buffer.lines.set(0, [ [null, '中', 2], [null, '', 0], [null, '文', 2], @@ -152,7 +152,7 @@ describe('SelectionManager', () => { assert.equal(selectionManager.selectionText, 'foo'); }); it('should select up to non-path characters that are commonly adjacent to paths', () => { - bufferLines.set(0, stringToRow('(cd)[ef]{gh}\'ij"')); + buffer.lines.set(0, stringToRow('(cd)[ef]{gh}\'ij"')); selectionManager.selectWordAt([0, 0]); assert.equal(selectionManager.selectionText, '(cd'); selectionManager.selectWordAt([1, 0]); @@ -190,7 +190,7 @@ describe('SelectionManager', () => { describe('_selectLineAt', () => { it('should select the entire line', () => { - bufferLines.set(0, stringToRow('foo bar')); + buffer.lines.set(0, stringToRow('foo bar')); selectionManager.selectLineAt(0); assert.equal(selectionManager.selectionText, 'foo bar', 'The selected text is correct'); assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]); @@ -200,14 +200,14 @@ describe('SelectionManager', () => { describe('selectAll', () => { it('should select the entire buffer, beyond the viewport', () => { - bufferLines.length = 5; - bufferLines.set(0, stringToRow('1')); - bufferLines.set(1, stringToRow('2')); - bufferLines.set(2, stringToRow('3')); - bufferLines.set(3, stringToRow('4')); - bufferLines.set(4, stringToRow('5')); + buffer.lines.length = 5; + buffer.lines.set(0, stringToRow('1')); + buffer.lines.set(1, stringToRow('2')); + buffer.lines.set(2, stringToRow('3')); + buffer.lines.set(3, stringToRow('4')); + buffer.lines.set(4, stringToRow('5')); selectionManager.selectAll(); - terminal.buffer.ybase = bufferLines.length - terminal.rows; + terminal.buffer.ybase = buffer.lines.length - terminal.rows; assert.equal(selectionManager.selectionText, '1\n2\n3\n4\n5'); }); }); diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 781a8eda3f..ee03f79a7d 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -7,10 +7,10 @@ import * as Browser from './utils/Browser'; import { CharMeasure } from './utils/CharMeasure'; import { CircularList } from './utils/CircularList'; import { EventEmitter } from './EventEmitter'; -import { ITerminal, ICircularList, ISelectionManager } from './Interfaces'; +import { ITerminal, ICircularList, ISelectionManager, IBuffer } from './Interfaces'; import { SelectionModel } from './SelectionModel'; -import { translateBufferLineToString } from './utils/BufferLine'; import { LineData } from './Types'; +import { CHAR_DATA_WIDTH_INDEX } from './Buffer'; /** * The number of pixels the mouse needs to be above or below the viewport in @@ -34,11 +34,6 @@ const DRAG_SCROLL_INTERVAL = 50; */ const WORD_SEPARATORS = ' ()[]{}\'"'; -// TODO: Move these constants elsewhere, they belong in a buffer or buffer -// data/line class. -const LINE_DATA_CHAR_INDEX = 1; -const LINE_DATA_WIDTH_INDEX = 2; - const NON_BREAKING_SPACE_CHAR = String.fromCharCode(160); const ALL_NON_BREAKING_SPACE_REGEX = new RegExp(NON_BREAKING_SPACE_CHAR, 'g'); @@ -102,7 +97,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager constructor( private _terminal: ITerminal, - private _buffer: ICircularList, + private _buffer: IBuffer, private _rowContainer: HTMLElement, private _charMeasure: CharMeasure ) { @@ -127,7 +122,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // reverseIndex) and delete in a splice is only ever used when the same // number of elements was just added. Given this is could actually be // beneficial to leave the selection as is for these cases. - this._buffer.on('trim', (amount: number) => this._onTrim(amount)); + this._buffer.lines.on('trim', (amount: number) => this._onTrim(amount)); } /** @@ -151,7 +146,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * switched in or out. * @param buffer The active buffer. */ - public setBuffer(buffer: ICircularList): void { + public setBuffer(buffer: IBuffer): void { this._buffer = buffer; this.clearSelection(); } @@ -184,12 +179,12 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // Get first row const startRowEndCol = start[1] === end[1] ? end[0] : null; let result: string[] = []; - result.push(translateBufferLineToString(this._buffer.get(start[1]), true, start[0], startRowEndCol)); + result.push(this._buffer.translateBufferLineToString(start[1], true, start[0], startRowEndCol)); // Get middle rows for (let i = start[1] + 1; i <= end[1] - 1; i++) { - const bufferLine = this._buffer.get(i); - const lineText = translateBufferLineToString(bufferLine, true); + const bufferLine = this._buffer.lines.get(i); + const lineText = this._buffer.translateBufferLineToString(i, true); if ((bufferLine).isWrapped) { result[result.length - 1] += lineText; } else { @@ -199,8 +194,8 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // Get final row if (start[1] !== end[1]) { - const bufferLine = this._buffer.get(end[1]); - const lineText = translateBufferLineToString(bufferLine, true, 0, end[0]); + const bufferLine = this._buffer.lines.get(end[1]); + const lineText = this._buffer.translateBufferLineToString(end[1], true, 0, end[0]); if ((bufferLine).isWrapped) { result[result.length - 1] += lineText; } else { @@ -413,7 +408,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager this._model.selectionEnd = null; // Ensure the line exists - const line = this._buffer.get(this._model.selectionStart[1]); + const line = this._buffer.lines.get(this._model.selectionStart[1]); if (!line) { return; } @@ -421,7 +416,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // If the mouse is over the second half of a wide character, adjust the // selection to cover the whole character const char = line[this._model.selectionStart[0]]; - if (char[LINE_DATA_WIDTH_INDEX] === 0) { + if (char[CHAR_DATA_WIDTH_INDEX] === 0) { this._model.selectionStart[0]++; } } @@ -493,9 +488,9 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // If the character is a wide character include the cell to the right in the // selection. Note that selections at the very end of the line will never // have a character. - if (this._model.selectionEnd[1] < this._buffer.length) { - const char = this._buffer.get(this._model.selectionEnd[1])[this._model.selectionEnd[0]]; - if (char && char[2] === 0) { + if (this._model.selectionEnd[1] < this._buffer.lines.length) { + const char = this._buffer.lines.get(this._model.selectionEnd[1])[this._model.selectionEnd[0]]; + if (char && char[CHAR_DATA_WIDTH_INDEX] === 0) { this._model.selectionEnd[0]++; } } @@ -542,7 +537,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager let charIndex = coords[0]; for (let i = 0; coords[0] >= i; i++) { const char = bufferLine[i]; - if (char[LINE_DATA_WIDTH_INDEX] === 0) { + if (char[CHAR_DATA_WIDTH_INDEX] === 0) { charIndex--; } } @@ -562,12 +557,12 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * @param coords The coordinates to get the word at. */ private _getWordAt(coords: [number, number]): IWordPosition { - const bufferLine = this._buffer.get(coords[1]); + const bufferLine = this._buffer.lines.get(coords[1]); if (!bufferLine) { return null; } - const line = translateBufferLineToString(bufferLine, false); + const line = this._buffer.translateBufferLineToString(coords[1], false); // Get actual index, taking into consideration wide characters let endIndex = this._convertViewportColToCharacterIndex(bufferLine, coords); @@ -595,17 +590,17 @@ export class SelectionManager extends EventEmitter implements ISelectionManager let endCol = coords[0]; // Consider the initial position, skip it and increment the wide char // variable - if (bufferLine[startCol][LINE_DATA_WIDTH_INDEX] === 0) { + if (bufferLine[startCol][CHAR_DATA_WIDTH_INDEX] === 0) { leftWideCharCount++; startCol--; } - if (bufferLine[endCol][LINE_DATA_WIDTH_INDEX] === 2) { + if (bufferLine[endCol][CHAR_DATA_WIDTH_INDEX] === 2) { rightWideCharCount++; endCol++; } // Expand the string in both directions until a space is hit while (startIndex > 0 && !this._isCharWordSeparator(line.charAt(startIndex - 1))) { - if (bufferLine[startCol - 1][LINE_DATA_WIDTH_INDEX] === 0) { + if (bufferLine[startCol - 1][CHAR_DATA_WIDTH_INDEX] === 0) { // If the next character is a wide char, record it and skip the column leftWideCharCount++; startCol--; @@ -614,7 +609,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager startCol--; } while (endIndex + 1 < line.length && !this._isCharWordSeparator(line.charAt(endIndex + 1))) { - if (bufferLine[endCol + 1][LINE_DATA_WIDTH_INDEX] === 2) { + if (bufferLine[endCol + 1][CHAR_DATA_WIDTH_INDEX] === 2) { // If the next character is a wide char, record it and skip the column rightWideCharCount++; endCol++; diff --git a/src/Terminal.ts b/src/Terminal.ts index 4536733e11..31eaef8a1f 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -28,7 +28,6 @@ import * as Browser from './utils/Browser'; import * as Mouse from './utils/Mouse'; import { CHARSETS } from './Charsets'; import { getRawByteCoords } from './utils/Mouse'; -import { translateBufferLineToString } from './utils/BufferLine'; import { CustomKeyEventHandler, Charset, LinkMatcherHandler, LinkMatcherValidationCallback, CharData, LineData, Option, StringOption, BooleanOption, StringArrayOption, NumberOption, GeometryOption, HandlerOption } from './Types'; import { ITerminal, IBrowser, ITerminalOptions, IInputHandlingTerminal, ILinkMatcherOptions } from './Interfaces'; @@ -379,7 +378,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // Ensure the selection manager has the correct buffer if (this.selectionManager) { - this.selectionManager.setBuffer(this.buffer.lines); + this.selectionManager.setBuffer(this.buffer); } this.setupStops(); @@ -744,7 +743,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure); this.renderer = new Renderer(this); - this.selectionManager = new SelectionManager(this, this.buffer.lines, this.rowContainer, this.charMeasure); + this.selectionManager = new SelectionManager(this, this.buffer, this.rowContainer, this.charMeasure); this.selectionManager.on('refresh', data => { this.renderer.refreshSelection(data.start, data.end); }); diff --git a/src/test/escape-sequences-test.js b/src/test/escape-sequences-test.js index d0447451b4..51e5d017bb 100644 --- a/src/test/escape-sequences-test.js +++ b/src/test/escape-sequences-test.js @@ -3,6 +3,7 @@ var fs = require('fs'); var os = require('os'); var pty = require('node-pty'); var Terminal = require('../xterm'); +var CHAR_DATA_CHAR_INDEX = require('../Buffer').CHAR_DATA_CHAR_INDEX; if (os.platform() === 'win32') { // Skip tests on Windows since pty.open isn't supported @@ -65,7 +66,7 @@ function terminalToString(term) { for (var line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) { line_s = ''; for (var cell=0; cell= i) { - widthAdjustedStartCol--; - } - if (endCol >= i) { - widthAdjustedEndCol--; - } - } - } - - // Calculate the final end col by trimming whitespace on the right of the - // line if needed. - let finalEndCol = widthAdjustedEndCol || line.length; - if (trimRight) { - const rightWhitespaceIndex = lineString.search(/\s+$/); - if (rightWhitespaceIndex !== -1) { - finalEndCol = Math.min(finalEndCol, rightWhitespaceIndex); - } - // Return the empty string if only trimmed whitespace is selected - if (finalEndCol <= widthAdjustedStartCol) { - return ''; - } - } - - return lineString.substring(widthAdjustedStartCol, finalEndCol); -} diff --git a/src/utils/TestUtils.ts b/src/utils/TestUtils.ts index 159da055ed..fc78558807 100644 --- a/src/utils/TestUtils.ts +++ b/src/utils/TestUtils.ts @@ -180,4 +180,7 @@ export class MockBuffer implements IBuffer { scrollTop: number; savedY: number; savedX: number; + translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string { + throw new Error('Method not implemented.'); + } }