Files
xterm.js/src/handlers/AltClickHandler.ts
T
2018-01-27 22:36:53 -05:00

282 lines
8.1 KiB
TypeScript

/**
* Alt+Click handler module: exports methods for handling all alt+click-related
* events in the terminal.
* @module xterm/handlers/AltClickHandler
* @license MIT
*/
import { Terminal } from '../Terminal';
import { C0 } from '../EscapeSequences';
import { repeat } from '../utils/Generic';
import { CircularList } from '../utils/CircularList';
import { LineData } from '../Types';
export class AltClickHandler {
private _terminal: Terminal;
private _startRow: number;
private _startCol: number;
private _endRow: number;
private _endCol: number;
private _lines: CircularList<LineData>;
private _mouseEvent: MouseEvent;
constructor(mouseEvent: MouseEvent, terminal: Terminal) {
this._terminal = terminal;
this._lines = terminal.buffer.lines;
this._startCol = this._terminal.buffer.x;
this._startRow = this._terminal.buffer.y;
[this._endCol, this._endRow] = this._terminal.mouseHelper.getCoords(
(this._mouseEvent = mouseEvent),
this._terminal.element,
this._terminal.charMeasure,
this._terminal.options.lineHeight,
this._terminal.cols,
this._terminal.rows,
false
).map((coordinate: number) => {
return coordinate - 1;
});
}
/**
* Writes the escape sequences of arrows to the terminal
*/
public move(): void {
if (this._mouseEvent.altKey) this._terminal.send(this._arrowSequences());
}
/**
* Concatenates all the arrow sequences together.
* Resets the starting row to an unwrapped row, moves to the requested row,
* then moves to requested col.
*/
private _arrowSequences(): string {
return this._resetStartingRow() +
this._moveToRequestedRow() +
this._moveToRequestedCol();
}
/**
* If the initial position of the cursor is on a row that is wrapped, move the
* cursor up to the first row that is not wrapped to have accurate vertical
* positioning.
*/
private _resetStartingRow(): string {
let startRow = this._endRow - this._wrappedRowsForRow(this._endRow);
let endRow = this._endRow;
if (this._moveToRequestedRow().length === 0) {
return ""
} else {
return repeat(this._bufferLine(
this._startCol, this._startRow, this._startCol,
this._startRow - this._wrappedRowsForRow(this._startRow), false
).length, this._colSequence(false));
}
}
/**
* Using the reset starting and ending row, move to the requested row,
* ignoring wrapped rows
*/
private _moveToRequestedRow(): string {
let startRow = this._startRow - this._wrappedRowsForRow(this._startRow);
let endRow = this._endRow - this._wrappedRowsForRow(this._endRow);
let rowsToMove = Math.abs(startRow - endRow) - this._wrappedRowsCount();
return repeat(rowsToMove, this._rowSequence(this._shouldMoveUp()));
}
/**
* Move to the requested col on the ending row
*/
private _moveToRequestedCol(): string {
let startRow;
if (this._moveToRequestedRow().length > 0) {
startRow = this._endRow - this._wrappedRowsForRow(this._endRow);
} else {
startRow = this._startRow;
}
let endRow = this._endRow;
let forward = this._shouldMoveForward();
return repeat(this._bufferLine(
this._startCol, startRow, this._endCol, endRow, forward
).length, this._colSequence(forward));
}
/**
* Utility functions
*/
/**
* Calculates the number of wrapped rows between the unwrapped starting and
* ending rows. These rows need to ignored since the cursor skips over them.
*/
private _wrappedRowsCount(): number {
let wrappedRows = 0;
let startRow = this._startRow - this._wrappedRowsForRow(this._startRow);
let endRow = this._endRow - this._wrappedRowsForRow(this._endRow);
for (let i = 0; i < Math.abs(startRow - endRow); i++) {
let direction = this._shouldMoveUp() ? -1 : 1;
if (this._lines.get(startRow + (direction * i)).isWrapped) wrappedRows++;
}
return wrappedRows;
}
/**
* Calculates the number of wrapped rows that make up a given row.
* @param currentRow The row to determine how many wrapped rows make it up
*/
private _wrappedRowsForRow(currentRow: number): number {
let rowCount = 0;
let lineWraps = this._lines.get(currentRow).isWrapped;
while (lineWraps && currentRow >= 0 && currentRow < this._terminal.rows) {
rowCount++;
currentRow--;
lineWraps = this._lines.get(currentRow).isWrapped;
}
return rowCount;
}
/**
* Direction determiners
*/
/**
* Determines if the right or left arrow is needed
*/
private _shouldMoveForward(): boolean {
let startRow;
if (this._moveToRequestedRow().length > 0) {
startRow = this._endRow - this._wrappedRowsForRow(this._endRow);
} else {
startRow = this._startRow;
}
return (this._startCol < this._endCol &&
startRow <= this._endRow) || // down/right or same y/right
(this._startCol >= this._endCol &&
startRow < this._endRow); // down/left or same y/left
}
/**
* Determines if the up or down arrow is needed
*/
private _shouldMoveUp(): boolean {
return this._startRow > this._endRow;
}
/**
* Constructs the string of chars in the buffer from a starting row and col
* to an ending row and col
* @param startCol The starting column position
* @param startRow The starting row position
* @param endCol The ending column position
* @param endRow The ending row position
* @param forward Direction to move
*/
private _bufferLine(
startCol: number,
startRow: number,
endCol: number,
endRow: number,
forward: boolean): string {
let currentCol = startCol;
let currentRow = startRow;
let bufferStr = '';
while (currentCol !== endCol || currentRow !== endRow) {
currentCol += forward ? 1 : -1;
if (forward && currentCol > this._terminal.cols - 1) {
bufferStr += this._terminal.buffer.translateBufferLineToString(
currentRow, false, startCol, currentCol
);
currentCol = 0;
startCol = 0;
currentRow++;
} else if (!forward && currentCol < 0) {
bufferStr += this._terminal.buffer.translateBufferLineToString(
currentRow, false, 0, startCol + 1
);
currentCol = this._terminal.cols - 1;
startCol = currentCol;
currentRow--;
}
}
return bufferStr + this._terminal.buffer.translateBufferLineToString(
currentRow, false, startCol, currentCol
);
}
/**
* Arrow escape sequences
*/
/**
* Constructs the escape sequence for the left or right arrow
* @param forward Right arrow or left arrow
*/
private _colSequence(forward: boolean): string {
let mod = this._terminal.applicationCursor ? 'O' : '[';
if (forward) {
return C0.ESC + mod + 'C';
} else {
return C0.ESC + mod + 'D';
}
}
/**
* Constructs the escape sequence for clicking the up or down arrow
* @param up Up arrow or down arrow
*/
private _rowSequence(up: boolean): string {
let mod = this._terminal.applicationCursor ? 'O' : '[';
if (up) {
return C0.ESC + mod + 'A';
} else {
return C0.ESC + mod + 'B';
}
}
}
/**
* Returns a string repeated a given number of times
* Polyfill from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat
* @param {Number} count The number of times to repeat the string
* @param {String} string The string that is to be repeated
*/
function repeat(count: number, str: string): string {
if (count < 0) throw new RangeError('repeat count must be non-negative');
if (count === Infinity) throw new RangeError('repeat count must be less than infinity');
count = Math.floor(count);
if (str.length === 0 || count === 0) return '';
// Ensuring count is a 31-bit integer allows us to heavily optimize the
// main part. But anyway, most current (August 2014) browsers can't handle
// strings 1 << 28 chars or longer, so:
if (str.length * count >= 1 << 28) {
throw new RangeError('repeat count must not overflow maximum string size');
}
let rpt = '';
for (let i = 0; i < count; i++) {
rpt += str;
}
return rpt;
}