Merge pull request #2246 from Tyriar/alt_click_functional

Change alt click to functional and move into browser
This commit is contained in:
Daniel Imms
2019-06-22 19:37:00 -07:00
committed by GitHub
8 changed files with 321 additions and 292 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { ISelectionManager } from './Types';
import { ISelectionManager } from 'browser/selection/Types';
/**
* Prepares text to be pasted into the terminal by normalizing the line endings
+16 -3
View File
@@ -3,18 +3,19 @@
* @license MIT
*/
import { ITerminal, ISelectionManager, ISelectionRedrawRequestEvent } from './Types';
import { ITerminal } from './Types';
import { ISelectionManager, ISelectionRedrawRequestEvent } from 'browser/selection/Types';
import { IBuffer } from 'common/buffer/Types';
import { IBufferLine } from 'common/Types';
import * as Browser from 'common/Platform';
import { SelectionModel } from 'browser/selection/SelectionModel';
import { AltClickHandler } from './handlers/AltClickHandler';
import { CellData } from 'common/buffer/CellData';
import { IDisposable } from 'xterm';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { ICharSizeService, IMouseService } from 'browser/services/Services';
import { IBufferService, IOptionsService } from 'common/services/Services';
import { getCoordsRelativeToElement } from 'browser/input/Mouse';
import { moveToCellSequence } from 'browser/input/MoveToCell';
/**
* The number of pixels the mouse needs to be above or below the viewport in
@@ -650,7 +651,19 @@ export class SelectionManager implements ISelectionManager {
this._removeMouseDownListeners();
if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME) {
(new AltClickHandler(event, this._terminal, this._mouseService)).move();
if (event.altKey) {
const coordinates = this._mouseService.getCoords(
event,
this._terminal.element,
this._bufferService.cols,
this._bufferService.rows,
false
);
if (coordinates && coordinates[0] !== undefined && coordinates[1] !== undefined) {
const sequence = moveToCellSequence(coordinates[0] - 1, coordinates[1] - 1, this._bufferService, this._terminal.applicationCursor);
this._terminal.handler(sequence);
}
}
} else if (this.hasSelection) {
this._onSelectionChange.fire();
}
+2 -1
View File
@@ -4,7 +4,7 @@
*/
import { IRenderer, IRenderDimensions, CharacterJoinerHandler } from 'browser/renderer/Types';
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBrowser, ISelectionManager, ITerminalOptions, ILinkifier, ILinkMatcherOptions } from './Types';
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBrowser, ITerminalOptions, ILinkifier, ILinkMatcherOptions } from './Types';
import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types';
import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener } from 'common/Types';
import { Buffer } from 'common/buffer/Buffer';
@@ -15,6 +15,7 @@ import { AttributeData } from 'common/buffer/AttributeData';
import { IColorManager, IColorSet } from 'browser/Types';
import { IOptionsService } from 'common/services/Services';
import { EventEmitter } from 'common/EventEmitter';
import { ISelectionManager } from 'browser/selection/Types';
export class TestTerminal extends Terminal {
writeSync(data: string): void {
+1 -18
View File
@@ -9,6 +9,7 @@ import { IEvent, IEventEmitter } from 'common/EventEmitter';
import { IColorSet } from 'browser/Types';
import { IOptionsService } from 'common/services/Services';
import { IBuffer, IBufferSet } from 'common/buffer/Types';
import { ISelectionManager } from 'browser/selection/Types';
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
@@ -296,24 +297,6 @@ export interface ITerminalOptions extends IPublicTerminalOptions {
useFlowControl?: boolean;
}
export interface ISelectionManager {
selectionText: string;
selectionStart: [number, number];
selectionEnd: [number, number];
disable(): void;
enable(): void;
setSelection(row: number, col: number, length: number): void;
isClickInSelection(event: MouseEvent): boolean;
selectWordAtCursor(event: MouseEvent): void;
}
export interface ISelectionRedrawRequestEvent {
start: [number, number];
end: [number, number];
columnSelectMode: boolean;
}
export interface ILinkifier {
onLinkHover: IEvent<ILinkifierEvent>;
onLinkLeave: IEvent<ILinkifierEvent>;
+49
View File
@@ -0,0 +1,49 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { IBufferService } from 'common/services/Services';
import { MockBufferService } from 'common/TestUtils.test';
import { moveToCellSequence } from './MoveToCell';
describe('MoveToCell', () => {
let bufferService: IBufferService;
beforeEach(() => {
bufferService = new MockBufferService(5, 5);
bufferService.buffer.x = 3;
bufferService.buffer.y = 3;
});
describe('normal buffer', () => {
it('should use the right directional escape sequences', () => {
assert.equal(moveToCellSequence(2, 3, bufferService, false), '\x1b[D');
assert.equal(moveToCellSequence(4, 3, bufferService, false), '\x1b[C');
});
it('should ignore the Y value', () => {
assert.equal(moveToCellSequence(1, 1, bufferService, false), '\x1b[D\x1b[D');
assert.equal(moveToCellSequence(1, 2, bufferService, false), '\x1b[D\x1b[D');
assert.equal(moveToCellSequence(1, 3, bufferService, false), '\x1b[D\x1b[D');
assert.equal(moveToCellSequence(1, 4, bufferService, false), '\x1b[D\x1b[D');
assert.equal(moveToCellSequence(1, 5, bufferService, false), '\x1b[D\x1b[D');
});
it('should use the correct character for application cursor', () => {
assert.equal(moveToCellSequence(2, 1, bufferService, false), '\x1b[D');
assert.equal(moveToCellSequence(2, 1, bufferService, true), '\x1bOD');
});
});
describe('alt buffer', () => {
beforeEach(() => {
bufferService.buffers.activateAltBuffer();
bufferService.buffer.x = 3;
bufferService.buffer.y = 3;
});
it('should move the cursor across rows', () => {
assert.equal(moveToCellSequence(4, 4, bufferService, false), '\x1b[B\x1b[C');
});
});
});
+230
View File
@@ -0,0 +1,230 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { C0 } from 'common/data/EscapeSequences';
import { IBufferService } from 'common/services/Services';
const enum Direction {
UP = 'A',
DOWN = 'B',
RIGHT = 'C',
LEFT = 'D'
}
/**
* Concatenates all the arrow sequences together.
* Resets the starting row to an unwrapped row, moves to the requested row,
* then moves to requested col.
*/
export function moveToCellSequence(targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string {
const startX = bufferService.buffer.x;
const startY = bufferService.buffer.y;
// The alt buffer should try to navigate between rows
if (!bufferService.buffer.hasScrollback) {
return resetStartingRow(startX, startY, targetX, targetY, bufferService, applicationCursor) +
moveToRequestedRow(startY, targetY, bufferService, applicationCursor) +
moveToRequestedCol(startX, startY, targetX, targetY, bufferService, applicationCursor);
}
// Only move horizontally for the normal buffer
return moveHorizontallyOnly(startX, startY, targetX, targetY, bufferService, applicationCursor);
}
/**
* 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.
*/
function resetStartingRow(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string {
if (moveToRequestedRow(startY, targetY, bufferService, applicationCursor).length === 0) {
return '';
}
return repeat(bufferLine(
startX, startY, startX,
startY - wrappedRowsForRow(bufferService, startY), false, bufferService
).length, sequence(Direction.LEFT, applicationCursor));
}
/**
* Using the reset starting and ending row, move to the requested row,
* ignoring wrapped rows
*/
function moveToRequestedRow(startY: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string {
const startRow = startY - wrappedRowsForRow(bufferService, startY);
const endRow = targetY - wrappedRowsForRow(bufferService, targetY);
const rowsToMove = Math.abs(startRow - endRow) - wrappedRowsCount(startY, targetY, bufferService);
return repeat(rowsToMove, sequence(verticalDirection(startY, targetY), applicationCursor));
}
/**
* Move to the requested col on the ending row
*/
function moveToRequestedCol(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string {
let startRow;
if (moveToRequestedRow(startY, targetY, bufferService, applicationCursor).length > 0) {
startRow = targetY - wrappedRowsForRow(bufferService, targetY);
} else {
startRow = startY;
}
const endRow = targetY;
const direction = horizontalDirection(startX, startY, targetX, targetY, bufferService, applicationCursor);
return repeat(bufferLine(
startX, startRow, targetX, endRow,
direction === Direction.RIGHT, bufferService
).length, sequence(direction, applicationCursor));
}
function moveHorizontallyOnly(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string {
const direction = horizontalDirection(startX, startY, targetX, targetY, bufferService, applicationCursor);
return repeat(Math.abs(startX - targetX), sequence(direction, applicationCursor));
}
/**
* 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.
*/
function wrappedRowsCount(startY: number, targetY: number, bufferService: IBufferService): number {
let wrappedRows = 0;
const startRow = startY - wrappedRowsForRow(bufferService, startY);
const endRow = targetY - wrappedRowsForRow(bufferService, targetY);
for (let i = 0; i < Math.abs(startRow - endRow); i++) {
const direction = verticalDirection(startY, targetY) === Direction.UP ? -1 : 1;
const line = bufferService.buffer.lines.get(startRow + (direction * i));
if (line && line.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
*/
function wrappedRowsForRow(bufferService: IBufferService, currentRow: number): number {
let rowCount = 0;
let line = bufferService.buffer.lines.get(currentRow);
let lineWraps = line && line.isWrapped;
while (lineWraps && currentRow >= 0 && currentRow < bufferService.rows) {
rowCount++;
line = bufferService.buffer.lines.get(--currentRow);
lineWraps = line && line.isWrapped;
}
return rowCount;
}
/**
* Direction determiners
*/
/**
* Determines if the right or left arrow is needed
*/
function horizontalDirection(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): Direction {
let startRow;
if (moveToRequestedRow(targetX, targetY, bufferService, applicationCursor).length > 0) {
startRow = targetY - wrappedRowsForRow(bufferService, targetY);
} else {
startRow = startY;
}
if ((startX < targetX &&
startRow <= targetY) || // down/right or same y/right
(startX >= targetX &&
startRow < targetY)) { // down/left or same y/left
return Direction.RIGHT;
}
return Direction.LEFT;
}
/**
* Determines if the up or down arrow is needed
*/
function verticalDirection(startY: number, targetY: number): Direction {
return startY > targetY ? Direction.UP : Direction.DOWN;
}
/**
* 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
*/
function bufferLine(
startCol: number,
startRow: number,
endCol: number,
endRow: number,
forward: boolean,
bufferService: IBufferService
): string {
let currentCol = startCol;
let currentRow = startRow;
let bufferStr = '';
while (currentCol !== endCol || currentRow !== endRow) {
currentCol += forward ? 1 : -1;
if (forward && currentCol > bufferService.cols - 1) {
bufferStr += bufferService.buffer.translateBufferLineToString(
currentRow, false, startCol, currentCol
);
currentCol = 0;
startCol = 0;
currentRow++;
} else if (!forward && currentCol < 0) {
bufferStr += bufferService.buffer.translateBufferLineToString(
currentRow, false, 0, startCol + 1
);
currentCol = bufferService.cols - 1;
startCol = currentCol;
currentRow--;
}
}
return bufferStr + bufferService.buffer.translateBufferLineToString(
currentRow, false, startCol, currentCol
);
}
/**
* Constructs the escape sequence for clicking an arrow
* @param direction The direction to move
*/
function sequence(direction: Direction, applicationCursor: boolean): string {
const mod = applicationCursor ? 'O' : '[';
return C0.ESC + mod + direction;
}
/**
* 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 count The number of times to repeat the string
* @param string The string that is to be repeated
*/
function repeat(count: number, str: string): string {
count = Math.floor(count);
let rpt = '';
for (let i = 0; i < count; i++) {
rpt += str;
}
return rpt;
}
+22
View File
@@ -0,0 +1,22 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
export interface ISelectionManager {
selectionText: string;
selectionStart: [number, number];
selectionEnd: [number, number];
disable(): void;
enable(): void;
setSelection(row: number, col: number, length: number): void;
isClickInSelection(event: MouseEvent): boolean;
selectWordAtCursor(event: MouseEvent): void;
}
export interface ISelectionRedrawRequestEvent {
start: [number, number];
end: [number, number];
columnSelectMode: boolean;
}
-269
View File
@@ -1,269 +0,0 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ITerminal } from '../Types';
import { IBufferLine, ICircularList } from 'common/Types';
import { C0 } from 'common/data/EscapeSequences';
import { IMouseService } from 'browser/services/Services';
const enum Direction {
UP = 'A',
DOWN = 'B',
RIGHT = 'C',
LEFT = 'D'
}
export class AltClickHandler {
private _startRow: number;
private _startCol: number;
private _endRow: number;
private _endCol: number;
private _lines: ICircularList<IBufferLine>;
constructor(
private _mouseEvent: MouseEvent,
private _terminal: ITerminal,
private readonly _mouseService: IMouseService
) {
this._lines = this._terminal.buffer.lines;
this._startCol = this._terminal.buffer.x;
this._startRow = this._terminal.buffer.y;
const coordinates = this._mouseService.getCoords(
this._mouseEvent,
this._terminal.element,
this._terminal.cols,
this._terminal.rows,
false
);
if (coordinates) {
[this._endCol, this._endRow] = coordinates.map((coordinate: number) => {
return coordinate - 1;
});
}
}
/**
* Writes the escape sequences of arrows to the terminal
*/
public move(): void {
if (this._mouseEvent.altKey && this._endCol !== undefined && this._endRow !== undefined) {
this._terminal.handler(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 {
// The alt buffer should try to navigate between rows
if (!this._terminal.buffer.hasScrollback) {
return this._resetStartingRow() + this._moveToRequestedRow() + this._moveToRequestedCol();
}
// Only move horizontally for the normal buffer
return this._moveHorizontallyOnly();
}
/**
* 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 {
if (this._moveToRequestedRow().length === 0) {
return '';
}
return repeat(this._bufferLine(
this._startCol, this._startRow, this._startCol,
this._startRow - this._wrappedRowsForRow(this._startRow), false
).length, this._sequence(Direction.LEFT));
}
/**
* Using the reset starting and ending row, move to the requested row,
* ignoring wrapped rows
*/
private _moveToRequestedRow(): string {
const startRow = this._startRow - this._wrappedRowsForRow(this._startRow);
const endRow = this._endRow - this._wrappedRowsForRow(this._endRow);
const rowsToMove = Math.abs(startRow - endRow) - this._wrappedRowsCount();
return repeat(rowsToMove, this._sequence(this._verticalDirection()));
}
/**
* 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;
}
const endRow = this._endRow;
const direction = this._horizontalDirection();
return repeat(this._bufferLine(
this._startCol, startRow, this._endCol, endRow,
direction === Direction.RIGHT
).length, this._sequence(direction));
}
private _moveHorizontallyOnly(): string {
const direction = this._horizontalDirection();
return repeat(Math.abs(this._startCol - this._endCol), this._sequence(direction));
}
/**
* 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;
const startRow = this._startRow - this._wrappedRowsForRow(this._startRow);
const endRow = this._endRow - this._wrappedRowsForRow(this._endRow);
for (let i = 0; i < Math.abs(startRow - endRow); i++) {
const direction = this._verticalDirection() === Direction.UP ? -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 _horizontalDirection(): Direction {
let startRow;
if (this._moveToRequestedRow().length > 0) {
startRow = this._endRow - this._wrappedRowsForRow(this._endRow);
} else {
startRow = this._startRow;
}
if ((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
return Direction.RIGHT;
}
return Direction.LEFT;
}
/**
* Determines if the up or down arrow is needed
*/
private _verticalDirection(): Direction {
if (this._startRow > this._endRow) {
return Direction.UP;
}
return Direction.DOWN;
}
/**
* 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
);
}
/**
* Constructs the escape sequence for clicking an arrow
* @param direction The direction to move
*/
private _sequence(direction: Direction): string {
const mod = this._terminal.applicationCursor ? 'O' : '[';
return C0.ESC + mod + direction;
}
}
/**
* 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 count The number of times to repeat the string
* @param string The string that is to be repeated
*/
function repeat(count: number, str: string): string {
count = Math.floor(count);
let rpt = '';
for (let i = 0; i < count; i++) {
rpt += str;
}
return rpt;
}