From 7b68cb9fe74b8c16a113a13f7ea37d9e8543883b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 26 Jul 2018 11:09:29 -0700 Subject: [PATCH] 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