in src/SelectionManager.ts [695:839]
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;
}
const bufferLine = this._buffer.lines.get(coords[1]);
if (!bufferLine) {
return null;
}
const line = this._buffer.translateBufferLineToString(coords[1], false);
// Get actual index, taking into consideration wide characters
let startIndex = this._convertViewportColToCharacterIndex(bufferLine, coords);
let endIndex = startIndex;
// Record offset to be used later
const charOffset = coords[0] - startIndex;
let leftWideCharCount = 0;
let rightWideCharCount = 0;
let leftLongCharOffset = 0;
let rightLongCharOffset = 0;
if (line.charAt(startIndex) === ' ') {
// Expand until non-whitespace is hit
while (startIndex > 0 && line.charAt(startIndex - 1) === ' ') {
startIndex--;
}
while (endIndex < line.length && line.charAt(endIndex + 1) === ' ') {
endIndex++;
}
} else {
// Expand until whitespace is hit. This algorithm works by scanning left
// and right from the starting position, keeping both the index format
// (line) and the column format (bufferLine) in sync. When a wide
// character is hit, it is recorded and the column index is adjusted.
let startCol = coords[0];
let endCol = coords[0];
// Consider the initial position, skip it and increment the wide char
// variable
if (bufferLine.get(startCol)[CHAR_DATA_WIDTH_INDEX] === 0) {
leftWideCharCount++;
startCol--;
}
if (bufferLine.get(endCol)[CHAR_DATA_WIDTH_INDEX] === 2) {
rightWideCharCount++;
endCol++;
}
// Adjust the end index for characters whose length are > 1 (emojis)
if (bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length > 1) {
rightLongCharOffset += bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length - 1;
endIndex += bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length - 1;
}
// Expand the string in both directions until a space is hit
while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.get(startCol - 1))) {
const char = bufferLine.get(startCol - 1);
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
// If the next character is a wide char, record it and skip the column
leftWideCharCount++;
startCol--;
} else if (char[CHAR_DATA_CHAR_INDEX].length > 1) {
// If the next character's string is longer than 1 char (eg. emoji),
// adjust the index
leftLongCharOffset += char[CHAR_DATA_CHAR_INDEX].length - 1;
startIndex -= char[CHAR_DATA_CHAR_INDEX].length - 1;
}
startIndex--;
startCol--;
}
while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.get(endCol + 1))) {
const char = bufferLine.get(endCol + 1);
if (char[CHAR_DATA_WIDTH_INDEX] === 2) {
// If the next character is a wide char, record it and skip the column
rightWideCharCount++;
endCol++;
} else if (char[CHAR_DATA_CHAR_INDEX].length > 1) {
// If the next character's string is longer than 1 char (eg. emoji),
// adjust the index
rightLongCharOffset += char[CHAR_DATA_CHAR_INDEX].length - 1;
endIndex += char[CHAR_DATA_CHAR_INDEX].length - 1;
}
endIndex++;
endCol++;
}
}
// Incremenet the end index so it is at the start of the next character
endIndex++;
// Calculate the start _column_, converting the the string indexes back to
// column coordinates.
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
+ leftLongCharOffset; // The number of additional chars left of the initial char added by columns with strings longer than 1 (emojis)
// Calculate the length in _columns_, converting the the string indexes back
// to column coordinates.
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
+ rightWideCharCount // The number of wide chars right of the initial char (inclusive)
- leftLongCharOffset // The number of additional chars left of the initial char added by columns with strings longer than 1 (emojis)
- rightLongCharOffset); // The number of additional chars right of the initial char (inclusive) added by columns with strings longer than 1 (emojis)
if (!allowWhitespaceOnlySelection && line.slice(startIndex, endIndex).trim() === '') {
return null;
}
// Recurse upwards if the line is wrapped and the word wraps to the above line
if (followWrappedLinesAbove) {
if (start === 0 && bufferLine.get(0)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
const previousBufferLine = this._buffer.lines.get(coords[1] - 1);
if (previousBufferLine && bufferLine.isWrapped && previousBufferLine.get(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.get(this._terminal.cols - 1)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
const nextBufferLine = this._buffer.lines.get(coords[1] + 1);
if (nextBufferLine && nextBufferLine.isWrapped && nextBufferLine.get(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 };
}