Merge branch 'master' into benchmark_integration
This commit is contained in:
@@ -7,7 +7,7 @@ Xterm.js is a front-end component written in TypeScript that lets applications b
|
||||
## Features
|
||||
|
||||
- **Terminal apps just work**: Xterm.js works with most terminal apps such as `bash`, `vim` and `tmux`, this includes support for curses-based apps and mouse event support.
|
||||
- **Perfomant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer.
|
||||
- **Performant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer.
|
||||
- **Rich unicode support**: Supports CJK, emojis and IMEs.
|
||||
- **Self-contained**: Requires zero dependencies to work.
|
||||
- **Accessible**: Screen reader support can be turned on using the `screenReaderMode` option.
|
||||
|
||||
+25
-26
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, ISelectionManager } from './Types';
|
||||
import { ISelectionManager } from 'browser/selection/Types';
|
||||
|
||||
/**
|
||||
* Prepares text to be pasted into the terminal by normalizing the line endings
|
||||
@@ -28,7 +28,7 @@ export function bracketTextForPaste(text: string, bracketedPasteMode: boolean):
|
||||
* Binds copy functionality to the given terminal.
|
||||
* @param ev The original copy event to be handled
|
||||
*/
|
||||
export function copyHandler(ev: ClipboardEvent, term: ITerminal, selectionManager: ISelectionManager): void {
|
||||
export function copyHandler(ev: ClipboardEvent, selectionManager: ISelectionManager): void {
|
||||
ev.clipboardData.setData('text/plain', selectionManager.selectionText);
|
||||
// Prevent or the original text will be copied.
|
||||
ev.preventDefault();
|
||||
@@ -39,17 +39,16 @@ export function copyHandler(ev: ClipboardEvent, term: ITerminal, selectionManage
|
||||
* @param ev The original paste event to be handled
|
||||
* @param term The terminal on which to apply the handled paste event
|
||||
*/
|
||||
export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void {
|
||||
export function pasteHandler(ev: ClipboardEvent, textarea: HTMLTextAreaElement, bracketedPasteMode: boolean, triggerUserInput: (data: string) => void): void {
|
||||
ev.stopPropagation();
|
||||
|
||||
let text: string;
|
||||
|
||||
const dispatchPaste = function(text: string): void {
|
||||
text = prepareTextForTerminal(text);
|
||||
text = bracketTextForPaste(text, term.bracketedPasteMode);
|
||||
term.handler(text);
|
||||
term.textarea.value = '';
|
||||
term.cancel(ev);
|
||||
text = bracketTextForPaste(text, bracketedPasteMode);
|
||||
triggerUserInput(text);
|
||||
textarea.value = '';
|
||||
};
|
||||
|
||||
if (ev.clipboardData) {
|
||||
@@ -63,32 +62,32 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void {
|
||||
* @param ev The original right click event to be handled.
|
||||
* @param textarea The terminal's textarea.
|
||||
*/
|
||||
export function moveTextAreaUnderMouseCursor(ev: MouseEvent, term: ITerminal): void {
|
||||
export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextAreaElement, screenElement: HTMLElement): void {
|
||||
|
||||
// Calculate textarea position relative to the screen element
|
||||
const pos = term.screenElement.getBoundingClientRect();
|
||||
const pos = screenElement.getBoundingClientRect();
|
||||
const left = ev.clientX - pos.left - 10;
|
||||
const top = ev.clientY - pos.top - 10;
|
||||
|
||||
// Bring textarea at the cursor position
|
||||
term.textarea.style.position = 'absolute';
|
||||
term.textarea.style.width = '20px';
|
||||
term.textarea.style.height = '20px';
|
||||
term.textarea.style.left = `${left}px`;
|
||||
term.textarea.style.top = `${top}px`;
|
||||
term.textarea.style.zIndex = '1000';
|
||||
textarea.style.position = 'absolute';
|
||||
textarea.style.width = '20px';
|
||||
textarea.style.height = '20px';
|
||||
textarea.style.left = `${left}px`;
|
||||
textarea.style.top = `${top}px`;
|
||||
textarea.style.zIndex = '1000';
|
||||
|
||||
term.textarea.focus();
|
||||
textarea.focus();
|
||||
|
||||
// Reset the terminal textarea's styling
|
||||
// Timeout needs to be long enough for click event to be handled.
|
||||
setTimeout(() => {
|
||||
term.textarea.style.position = null;
|
||||
term.textarea.style.width = null;
|
||||
term.textarea.style.height = null;
|
||||
term.textarea.style.left = null;
|
||||
term.textarea.style.top = null;
|
||||
term.textarea.style.zIndex = null;
|
||||
textarea.style.position = null;
|
||||
textarea.style.width = null;
|
||||
textarea.style.height = null;
|
||||
textarea.style.left = null;
|
||||
textarea.style.top = null;
|
||||
textarea.style.zIndex = null;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
@@ -99,14 +98,14 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, term: ITerminal): v
|
||||
* @param selectionManager The terminal's selection manager.
|
||||
* @param shouldSelectWord If true and there is no selection the current word will be selected
|
||||
*/
|
||||
export function rightClickHandler(ev: MouseEvent, term: ITerminal, selectionManager: ISelectionManager, shouldSelectWord: boolean): void {
|
||||
moveTextAreaUnderMouseCursor(ev, term);
|
||||
export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, screenElement: HTMLElement, selectionManager: ISelectionManager, shouldSelectWord: boolean): void {
|
||||
moveTextAreaUnderMouseCursor(ev, textarea, screenElement);
|
||||
|
||||
if (shouldSelectWord && !selectionManager.isClickInSelection(ev)) {
|
||||
selectionManager.selectWordAtCursor(ev);
|
||||
}
|
||||
|
||||
// Get textarea ready to copy from the context menu
|
||||
term.textarea.value = selectionManager.selectionText;
|
||||
term.textarea.select();
|
||||
textarea.value = selectionManager.selectionText;
|
||||
textarea.select();
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ import { ITerminal } from './Types';
|
||||
import { IBuffer } from 'common/buffer/Types';
|
||||
import { IBufferLine } from 'common/Types';
|
||||
import { MockTerminal } from './TestUtils.test';
|
||||
import { MockBufferService } from 'common/TestUtils.test';
|
||||
import { MockBufferService, MockOptionsService } from 'common/TestUtils.test';
|
||||
import { BufferLine } from 'common/buffer/BufferLine';
|
||||
import { IBufferService } from 'common/services/Services';
|
||||
import { IBufferService, IOptionsService } from 'common/services/Services';
|
||||
import { MockCharSizeService, MockMouseService } from 'browser/TestUtils.test';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
|
||||
@@ -23,9 +23,10 @@ class TestMockTerminal extends MockTerminal {
|
||||
class TestSelectionManager extends SelectionManager {
|
||||
constructor(
|
||||
terminal: ITerminal,
|
||||
bufferService: IBufferService
|
||||
bufferService: IBufferService,
|
||||
optionsService: IOptionsService
|
||||
) {
|
||||
super(terminal, new MockCharSizeService(10, 10), bufferService, new MockMouseService());
|
||||
super(terminal, null, new MockCharSizeService(10, 10), bufferService, new MockMouseService(), optionsService);
|
||||
}
|
||||
|
||||
public get model(): SelectionModel { return this._model; }
|
||||
@@ -46,17 +47,19 @@ describe('SelectionManager', () => {
|
||||
let terminal: ITerminal;
|
||||
let buffer: IBuffer;
|
||||
let bufferService: IBufferService;
|
||||
let optionsService: IOptionsService;
|
||||
let selectionManager: TestSelectionManager;
|
||||
|
||||
beforeEach(() => {
|
||||
terminal = new TestMockTerminal();
|
||||
bufferService = new MockBufferService(20, 20);
|
||||
optionsService = new MockOptionsService();
|
||||
bufferService = new MockBufferService(20, 20, optionsService);
|
||||
terminal.buffers = bufferService.buffers;
|
||||
terminal.cols = 20;
|
||||
terminal.rows = 20;
|
||||
terminal.buffer = terminal.buffers.active;
|
||||
buffer = terminal.buffer;
|
||||
selectionManager = new TestSelectionManager(terminal, bufferService);
|
||||
selectionManager = new TestSelectionManager(terminal, bufferService, optionsService);
|
||||
});
|
||||
|
||||
function stringToRow(text: string): IBufferLine {
|
||||
|
||||
+72
-63
@@ -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 } from 'common/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
|
||||
@@ -38,12 +39,6 @@ const DRAG_SCROLL_INTERVAL = 50;
|
||||
*/
|
||||
const ALT_CLICK_MOVE_CURSOR_TIME = 500;
|
||||
|
||||
/**
|
||||
* A string containing all characters that are considered word separated by the
|
||||
* double click to select work logic.
|
||||
*/
|
||||
const WORD_SEPARATORS = ' ()[]{}\'"';
|
||||
|
||||
const NON_BREAKING_SPACE_CHAR = String.fromCharCode(160);
|
||||
const ALL_NON_BREAKING_SPACE_REGEX = new RegExp(NON_BREAKING_SPACE_CHAR, 'g');
|
||||
|
||||
@@ -119,14 +114,16 @@ export class SelectionManager implements ISelectionManager {
|
||||
|
||||
constructor(
|
||||
private readonly _terminal: ITerminal,
|
||||
private readonly _screenElement: HTMLElement,
|
||||
private readonly _charSizeService: ICharSizeService,
|
||||
readonly bufferService: IBufferService,
|
||||
private readonly _mouseService: IMouseService
|
||||
private readonly _bufferService: IBufferService,
|
||||
private readonly _mouseService: IMouseService,
|
||||
private readonly _optionsService: IOptionsService
|
||||
) {
|
||||
this._initListeners();
|
||||
this.enable();
|
||||
|
||||
this._model = new SelectionModel(bufferService);
|
||||
this._model = new SelectionModel(this._bufferService);
|
||||
this._activeSelectionMode = SelectionMode.NORMAL;
|
||||
}
|
||||
|
||||
@@ -134,10 +131,6 @@ export class SelectionManager implements ISelectionManager {
|
||||
this._removeMouseDownListeners();
|
||||
}
|
||||
|
||||
private get _buffer(): IBuffer {
|
||||
return this._terminal.buffers.active;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes listener variables.
|
||||
*/
|
||||
@@ -149,8 +142,8 @@ export class SelectionManager implements ISelectionManager {
|
||||
}
|
||||
|
||||
public initBuffersListeners(): void {
|
||||
this._trimListener = this._terminal.buffer.lines.onTrim(amount => this._onTrim(amount));
|
||||
this._terminal.buffers.onBufferActivate(e => this._onBufferActivate(e));
|
||||
this._trimListener = this._bufferService.buffer.lines.onTrim(amount => this._onTrim(amount));
|
||||
this._bufferService.buffers.onBufferActivate(e => this._onBufferActivate(e));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -194,6 +187,7 @@ export class SelectionManager implements ISelectionManager {
|
||||
return '';
|
||||
}
|
||||
|
||||
const buffer = this._bufferService.buffer;
|
||||
const result: string[] = [];
|
||||
|
||||
if (this._activeSelectionMode === SelectionMode.COLUMN) {
|
||||
@@ -203,18 +197,18 @@ export class SelectionManager implements ISelectionManager {
|
||||
}
|
||||
|
||||
for (let i = start[1]; i <= end[1]; i++) {
|
||||
const lineText = this._buffer.translateBufferLineToString(i, true, start[0], end[0]);
|
||||
const lineText = buffer.translateBufferLineToString(i, true, start[0], end[0]);
|
||||
result.push(lineText);
|
||||
}
|
||||
} else {
|
||||
// Get first row
|
||||
const startRowEndCol = start[1] === end[1] ? end[0] : undefined;
|
||||
result.push(this._buffer.translateBufferLineToString(start[1], true, start[0], startRowEndCol));
|
||||
result.push(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.lines.get(i);
|
||||
const lineText = this._buffer.translateBufferLineToString(i, true);
|
||||
const bufferLine = buffer.lines.get(i);
|
||||
const lineText = buffer.translateBufferLineToString(i, true);
|
||||
if (bufferLine.isWrapped) {
|
||||
result[result.length - 1] += lineText;
|
||||
} else {
|
||||
@@ -224,8 +218,8 @@ export class SelectionManager implements ISelectionManager {
|
||||
|
||||
// Get final row
|
||||
if (start[1] !== end[1]) {
|
||||
const bufferLine = this._buffer.lines.get(end[1]);
|
||||
const lineText = this._buffer.translateBufferLineToString(end[1], true, 0, end[0]);
|
||||
const bufferLine = buffer.lines.get(end[1]);
|
||||
const lineText = buffer.translateBufferLineToString(end[1], true, 0, end[0]);
|
||||
if (bufferLine.isWrapped) {
|
||||
result[result.length - 1] += lineText;
|
||||
} else {
|
||||
@@ -335,9 +329,9 @@ export class SelectionManager implements ISelectionManager {
|
||||
public selectLines(start: number, end: number): void {
|
||||
this._model.clearSelection();
|
||||
start = Math.max(start, 0);
|
||||
end = Math.min(end, this._terminal.buffer.lines.length - 1);
|
||||
end = Math.min(end, this._bufferService.buffer.lines.length - 1);
|
||||
this._model.selectionStart = [0, start];
|
||||
this._model.selectionEnd = [this._terminal.cols, end];
|
||||
this._model.selectionEnd = [this._bufferService.cols, end];
|
||||
this.refresh();
|
||||
this._onSelectionChange.fire();
|
||||
}
|
||||
@@ -358,7 +352,7 @@ export class SelectionManager implements ISelectionManager {
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _getMouseBufferCoords(event: MouseEvent): [number, number] {
|
||||
const coords = this._mouseService.getCoords(event, this._terminal.screenElement, this._terminal.cols, this._terminal.rows, true);
|
||||
const coords = this._mouseService.getCoords(event, this._screenElement, this._bufferService.cols, this._bufferService.rows, true);
|
||||
if (!coords) {
|
||||
return null;
|
||||
}
|
||||
@@ -368,7 +362,7 @@ export class SelectionManager implements ISelectionManager {
|
||||
coords[1]--;
|
||||
|
||||
// Convert viewport coords to buffer coords
|
||||
coords[1] += this._terminal.buffer.ydisp;
|
||||
coords[1] += this._bufferService.buffer.ydisp;
|
||||
return coords;
|
||||
}
|
||||
|
||||
@@ -378,8 +372,8 @@ export class SelectionManager implements ISelectionManager {
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _getMouseEventScrollAmount(event: MouseEvent): number {
|
||||
let offset = getCoordsRelativeToElement(event, this._terminal.screenElement)[1];
|
||||
const terminalHeight = this._terminal.rows * Math.ceil(this._charSizeService.height * this._terminal.options.lineHeight);
|
||||
let offset = getCoordsRelativeToElement(event, this._screenElement)[1];
|
||||
const terminalHeight = this._bufferService.rows * Math.ceil(this._charSizeService.height * this._optionsService.options.lineHeight);
|
||||
if (offset >= 0 && offset <= terminalHeight) {
|
||||
return 0;
|
||||
}
|
||||
@@ -399,7 +393,7 @@ export class SelectionManager implements ISelectionManager {
|
||||
*/
|
||||
public shouldForceSelection(event: MouseEvent): boolean {
|
||||
if (Browser.isMac) {
|
||||
return event.altKey && this._terminal.options.macOptionClickForcesSelection;
|
||||
return event.altKey && this._optionsService.options.macOptionClickForcesSelection;
|
||||
}
|
||||
|
||||
return event.shiftKey;
|
||||
@@ -459,8 +453,8 @@ export class SelectionManager implements ISelectionManager {
|
||||
*/
|
||||
private _addMouseDownListeners(): void {
|
||||
// Listen on the document so that dragging outside of viewport works
|
||||
this._terminal.element.ownerDocument.addEventListener('mousemove', this._mouseMoveListener);
|
||||
this._terminal.element.ownerDocument.addEventListener('mouseup', this._mouseUpListener);
|
||||
this._screenElement.ownerDocument.addEventListener('mousemove', this._mouseMoveListener);
|
||||
this._screenElement.ownerDocument.addEventListener('mouseup', this._mouseUpListener);
|
||||
this._dragScrollIntervalTimer = setInterval(() => this._dragScroll(), DRAG_SCROLL_INTERVAL);
|
||||
}
|
||||
|
||||
@@ -468,9 +462,9 @@ export class SelectionManager implements ISelectionManager {
|
||||
* Removes the listeners that are registered when mousedown is triggered.
|
||||
*/
|
||||
private _removeMouseDownListeners(): void {
|
||||
if (this._terminal.element.ownerDocument) {
|
||||
this._terminal.element.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener);
|
||||
this._terminal.element.ownerDocument.removeEventListener('mouseup', this._mouseUpListener);
|
||||
if (this._screenElement.ownerDocument) {
|
||||
this._screenElement.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener);
|
||||
this._screenElement.ownerDocument.removeEventListener('mouseup', this._mouseUpListener);
|
||||
}
|
||||
clearInterval(this._dragScrollIntervalTimer);
|
||||
this._dragScrollIntervalTimer = null;
|
||||
@@ -505,7 +499,7 @@ export class SelectionManager implements ISelectionManager {
|
||||
this._model.selectionEnd = null;
|
||||
|
||||
// Ensure the line exists
|
||||
const line = this._buffer.lines.get(this._model.selectionStart[1]);
|
||||
const line = this._bufferService.buffer.lines.get(this._model.selectionStart[1]);
|
||||
if (!line) {
|
||||
return;
|
||||
}
|
||||
@@ -552,7 +546,7 @@ export class SelectionManager implements ISelectionManager {
|
||||
* @param event the mouse or keyboard event
|
||||
*/
|
||||
public shouldColumnSelect(event: KeyboardEvent | MouseEvent): boolean {
|
||||
return event.altKey && !(Browser.isMac && this._terminal.options.macOptionClickForcesSelection);
|
||||
return event.altKey && !(Browser.isMac && this._optionsService.options.macOptionClickForcesSelection);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -582,7 +576,7 @@ export class SelectionManager implements ISelectionManager {
|
||||
if (this._model.selectionEnd[1] < this._model.selectionStart[1]) {
|
||||
this._model.selectionEnd[0] = 0;
|
||||
} else {
|
||||
this._model.selectionEnd[0] = this._terminal.cols;
|
||||
this._model.selectionEnd[0] = this._bufferService.cols;
|
||||
}
|
||||
} else if (this._activeSelectionMode === SelectionMode.WORD) {
|
||||
this._selectToWordAt(this._model.selectionEnd);
|
||||
@@ -596,7 +590,7 @@ export class SelectionManager implements ISelectionManager {
|
||||
// NOT in column select mode.
|
||||
if (this._activeSelectionMode !== SelectionMode.COLUMN) {
|
||||
if (this._dragScrollAmount > 0) {
|
||||
this._model.selectionEnd[0] = this._terminal.cols;
|
||||
this._model.selectionEnd[0] = this._bufferService.cols;
|
||||
} else if (this._dragScrollAmount < 0) {
|
||||
this._model.selectionEnd[0] = 0;
|
||||
}
|
||||
@@ -605,8 +599,9 @@ export class SelectionManager 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.lines.length) {
|
||||
if (this._buffer.lines.get(this._model.selectionEnd[1]).hasWidth(this._model.selectionEnd[0]) === 0) {
|
||||
const buffer = this._bufferService.buffer;
|
||||
if (this._model.selectionEnd[1] < buffer.lines.length) {
|
||||
if (buffer.lines.get(this._model.selectionEnd[1]).hasWidth(this._model.selectionEnd[0]) === 0) {
|
||||
this._model.selectionEnd[0]++;
|
||||
}
|
||||
}
|
||||
@@ -630,16 +625,17 @@ export class SelectionManager implements ISelectionManager {
|
||||
// If the cursor was above or below the viewport, make sure it's at the
|
||||
// start or end of the viewport respectively. This should only happen when
|
||||
// NOT in column select mode.
|
||||
const buffer = this._bufferService.buffer;
|
||||
if (this._dragScrollAmount > 0) {
|
||||
if (this._activeSelectionMode !== SelectionMode.COLUMN) {
|
||||
this._model.selectionEnd[0] = this._terminal.cols;
|
||||
this._model.selectionEnd[0] = this._bufferService.cols;
|
||||
}
|
||||
this._model.selectionEnd[1] = Math.min(this._terminal.buffer.ydisp + this._terminal.rows, this._terminal.buffer.lines.length - 1);
|
||||
this._model.selectionEnd[1] = Math.min(buffer.ydisp + this._bufferService.rows, buffer.lines.length - 1);
|
||||
} else {
|
||||
if (this._activeSelectionMode !== SelectionMode.COLUMN) {
|
||||
this._model.selectionEnd[0] = 0;
|
||||
}
|
||||
this._model.selectionEnd[1] = this._terminal.buffer.ydisp;
|
||||
this._model.selectionEnd[1] = buffer.ydisp;
|
||||
}
|
||||
this.refresh();
|
||||
}
|
||||
@@ -655,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();
|
||||
}
|
||||
@@ -710,16 +718,17 @@ export class SelectionManager implements ISelectionManager {
|
||||
*/
|
||||
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) {
|
||||
if (coords[0] >= this._bufferService.cols) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bufferLine = this._buffer.lines.get(coords[1]);
|
||||
const buffer = this._bufferService.buffer;
|
||||
const bufferLine = buffer.lines.get(coords[1]);
|
||||
if (!bufferLine) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const line = this._buffer.translateBufferLineToString(coords[1], false);
|
||||
const line = buffer.translateBufferLineToString(coords[1], false);
|
||||
|
||||
// Get actual index, taking into consideration wide characters
|
||||
let startIndex = this._convertViewportColToCharacterIndex(bufferLine, coords);
|
||||
@@ -814,7 +823,7 @@ export class SelectionManager implements ISelectionManager {
|
||||
|
||||
// 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
|
||||
let length = Math.min(this._bufferService.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
|
||||
@@ -829,11 +838,11 @@ export class SelectionManager implements ISelectionManager {
|
||||
// Recurse upwards if the line is wrapped and the word wraps to the above line
|
||||
if (followWrappedLinesAbove) {
|
||||
if (start === 0 && bufferLine.getCodePoint(0) !== 32 /*' '*/) {
|
||||
const previousBufferLine = this._buffer.lines.get(coords[1] - 1);
|
||||
if (previousBufferLine && bufferLine.isWrapped && previousBufferLine.getCodePoint(this._terminal.cols - 1) !== 32 /*' '*/) {
|
||||
const previousLineWordPosition = this._getWordAt([this._terminal.cols - 1, coords[1] - 1], false, true, false);
|
||||
const previousBufferLine = buffer.lines.get(coords[1] - 1);
|
||||
if (previousBufferLine && bufferLine.isWrapped && previousBufferLine.getCodePoint(this._bufferService.cols - 1) !== 32 /*' '*/) {
|
||||
const previousLineWordPosition = this._getWordAt([this._bufferService.cols - 1, coords[1] - 1], false, true, false);
|
||||
if (previousLineWordPosition) {
|
||||
const offset = this._terminal.cols - previousLineWordPosition.start;
|
||||
const offset = this._bufferService.cols - previousLineWordPosition.start;
|
||||
start -= offset;
|
||||
length += offset;
|
||||
}
|
||||
@@ -843,8 +852,8 @@ export class SelectionManager implements ISelectionManager {
|
||||
|
||||
// Recurse downwards if the line is wrapped and the word wraps to the next line
|
||||
if (followWrappedLinesBelow) {
|
||||
if (start + length === this._terminal.cols && bufferLine.getCodePoint(this._terminal.cols - 1) !== 32 /*' '*/) {
|
||||
const nextBufferLine = this._buffer.lines.get(coords[1] + 1);
|
||||
if (start + length === this._bufferService.cols && bufferLine.getCodePoint(this._bufferService.cols - 1) !== 32 /*' '*/) {
|
||||
const nextBufferLine = buffer.lines.get(coords[1] + 1);
|
||||
if (nextBufferLine && nextBufferLine.isWrapped && nextBufferLine.getCodePoint(0) !== 32 /*' '*/) {
|
||||
const nextLineWordPosition = this._getWordAt([0, coords[1] + 1], false, false, true);
|
||||
if (nextLineWordPosition) {
|
||||
@@ -867,7 +876,7 @@ export class SelectionManager implements ISelectionManager {
|
||||
if (wordPosition) {
|
||||
// Adjust negative start value
|
||||
while (wordPosition.start < 0) {
|
||||
wordPosition.start += this._terminal.cols;
|
||||
wordPosition.start += this._bufferService.cols;
|
||||
coords[1]--;
|
||||
}
|
||||
this._model.selectionStart = [wordPosition.start, coords[1]];
|
||||
@@ -886,15 +895,15 @@ export class SelectionManager implements ISelectionManager {
|
||||
|
||||
// Adjust negative start value
|
||||
while (wordPosition.start < 0) {
|
||||
wordPosition.start += this._terminal.cols;
|
||||
wordPosition.start += this._bufferService.cols;
|
||||
endRow--;
|
||||
}
|
||||
|
||||
// Adjust wrapped length value, this only needs to happen when values are reversed as in that
|
||||
// case we're interested in the start of the word, not the end
|
||||
if (!this._model.areSelectionValuesReversed()) {
|
||||
while (wordPosition.start + wordPosition.length > this._terminal.cols) {
|
||||
wordPosition.length -= this._terminal.cols;
|
||||
while (wordPosition.start + wordPosition.length > this._bufferService.cols) {
|
||||
wordPosition.length -= this._bufferService.cols;
|
||||
endRow++;
|
||||
}
|
||||
}
|
||||
@@ -914,7 +923,7 @@ export class SelectionManager implements ISelectionManager {
|
||||
if (cell.getWidth() === 0) {
|
||||
return false;
|
||||
}
|
||||
return WORD_SEPARATORS.indexOf(cell.getChars()) >= 0;
|
||||
return this._optionsService.options.wordSeparator.indexOf(cell.getChars()) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -922,9 +931,9 @@ export class SelectionManager implements ISelectionManager {
|
||||
* @param line The line index.
|
||||
*/
|
||||
protected _selectLineAt(line: number): void {
|
||||
const wrappedRange = this._buffer.getWrappedRangeForLine(line);
|
||||
const wrappedRange = this._bufferService.buffer.getWrappedRangeForLine(line);
|
||||
this._model.selectionStart = [0, wrappedRange.first];
|
||||
this._model.selectionEnd = [this._terminal.cols, wrappedRange.last];
|
||||
this._model.selectionEnd = [this._bufferService.cols, wrappedRange.last];
|
||||
this._model.selectionStartLength = 0;
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -478,9 +478,9 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
|
||||
if (!this.hasSelection()) {
|
||||
return;
|
||||
}
|
||||
copyHandler(event, this, this.selectionManager);
|
||||
copyHandler(event, this.selectionManager);
|
||||
}));
|
||||
const pasteHandlerWrapper = (event: ClipboardEvent) => pasteHandler(event, this);
|
||||
const pasteHandlerWrapper = (event: ClipboardEvent) => pasteHandler(event, this.textarea, this.bracketedPasteMode, e => this.handler(e));
|
||||
this.register(addDisposableDomListener(this.textarea, 'paste', pasteHandlerWrapper));
|
||||
this.register(addDisposableDomListener(this.element, 'paste', pasteHandlerWrapper));
|
||||
|
||||
@@ -489,12 +489,12 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
|
||||
// Firefox doesn't appear to fire the contextmenu event on right click
|
||||
this.register(addDisposableDomListener(this.element, 'mousedown', (event: MouseEvent) => {
|
||||
if (event.button === 2) {
|
||||
rightClickHandler(event, this, this.selectionManager, this.options.rightClickSelectsWord);
|
||||
rightClickHandler(event, this.textarea, this.screenElement, this.selectionManager, this.options.rightClickSelectsWord);
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
this.register(addDisposableDomListener(this.element, 'contextmenu', (event: MouseEvent) => {
|
||||
rightClickHandler(event, this, this.selectionManager, this.options.rightClickSelectsWord);
|
||||
rightClickHandler(event, this.textarea, this.screenElement, this.selectionManager, this.options.rightClickSelectsWord);
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -506,7 +506,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
|
||||
// that the regular click event doesn't fire for the middle mouse button.
|
||||
this.register(addDisposableDomListener(this.element, 'auxclick', (event: MouseEvent) => {
|
||||
if (event.button === 1) {
|
||||
moveTextAreaUnderMouseCursor(event, this);
|
||||
moveTextAreaUnderMouseCursor(event, this.textarea, this.screenElement);
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -640,7 +640,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
|
||||
this.register(this.onFocus(() => this._renderService.onFocus()));
|
||||
this.register(this._renderService.onDimensionsChange(() => this.viewport.syncScrollArea()));
|
||||
|
||||
this.selectionManager = new SelectionManager(this, this._charSizeService, this._bufferService, this._mouseService);
|
||||
this.selectionManager = new SelectionManager(this, this.screenElement, this._charSizeService, this._bufferService, this._mouseService, this.optionsService);
|
||||
this.register(this.selectionManager.onSelectionChange(() => this._onSelectionChange.fire()));
|
||||
this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e)));
|
||||
this.register(this.selectionManager.onRedrawRequest(e => this._renderService.onSelectionChanged(e.start, e.end, e.columnSelectMode)));
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Vendored
+1
-18
@@ -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>;
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
Vendored
+22
@@ -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;
|
||||
}
|
||||
@@ -46,7 +46,8 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({
|
||||
screenKeys: false,
|
||||
debug: false,
|
||||
cancelEvents: false,
|
||||
useFlowControl: false
|
||||
useFlowControl: false,
|
||||
wordSeparator: ' ()[]{}\'"'
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -99,6 +100,7 @@ export class OptionsService implements IOptionsService {
|
||||
case 'fontWeight':
|
||||
case 'fontWeightBold':
|
||||
case 'rendererType':
|
||||
case 'wordSeparator':
|
||||
if (!value) {
|
||||
value = DEFAULT_OPTIONS[key];
|
||||
}
|
||||
|
||||
Vendored
+2
@@ -56,6 +56,7 @@ export interface IPartialTerminalOptions {
|
||||
tabStopWidth?: number;
|
||||
theme?: ITheme;
|
||||
windowsMode?: boolean;
|
||||
wordSeparator?: string;
|
||||
}
|
||||
|
||||
export interface ITerminalOptions {
|
||||
@@ -91,6 +92,7 @@ export interface ITerminalOptions {
|
||||
screenKeys: boolean;
|
||||
termName: string;
|
||||
useFlowControl: boolean;
|
||||
wordSeparator?: string;
|
||||
}
|
||||
|
||||
export interface ITheme {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -125,7 +125,7 @@ export class Terminal implements ITerminalApi {
|
||||
public writeUtf8(data: Uint8Array): void {
|
||||
this._core.writeUtf8(data);
|
||||
}
|
||||
public getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'fontWeight' | 'fontWeightBold' | 'rendererType' | 'termName'): string;
|
||||
public getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'fontWeight' | 'fontWeightBold' | 'rendererType' | 'termName' | 'wordSeparator'): string;
|
||||
public getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell'): boolean;
|
||||
public getOption(key: 'colors'): string[];
|
||||
public getOption(key: 'cols' | 'fontSize' | 'letterSpacing' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback'): number;
|
||||
@@ -134,7 +134,7 @@ export class Terminal implements ITerminalApi {
|
||||
public getOption(key: any): any {
|
||||
return this._core.optionsService.getOption(key);
|
||||
}
|
||||
public setOption(key: 'bellSound' | 'fontFamily' | 'termName', value: string): void;
|
||||
public setOption(key: 'bellSound' | 'fontFamily' | 'termName' | 'wordSeparator', value: string): void;
|
||||
public setOption(key: 'fontWeight' | 'fontWeightBold', value: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'): void;
|
||||
public setOption(key: 'bellStyle', value: 'none' | 'visual' | 'sound' | 'both'): void;
|
||||
public setOption(key: 'cursorStyle', value: 'block' | 'underline' | 'bar'): void;
|
||||
|
||||
Vendored
+8
-2
@@ -177,6 +177,12 @@ declare module 'xterm' {
|
||||
* not whitespace.
|
||||
*/
|
||||
windowsMode?: boolean;
|
||||
|
||||
/**
|
||||
* A string containing all characters that are considered word separated by the
|
||||
* double click to select work logic.
|
||||
*/
|
||||
wordSeparator?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -673,7 +679,7 @@ declare module 'xterm' {
|
||||
* Retrieves an option's value from the terminal.
|
||||
* @param key The option key.
|
||||
*/
|
||||
getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'fontWeight' | 'fontWeightBold'| 'rendererType' | 'termName'): string;
|
||||
getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'fontWeight' | 'fontWeightBold'| 'rendererType' | 'termName' | 'wordSeparator'): string;
|
||||
/**
|
||||
* Retrieves an option's value from the terminal.
|
||||
* @param key The option key.
|
||||
@@ -705,7 +711,7 @@ declare module 'xterm' {
|
||||
* @param key The option key.
|
||||
* @param value The option value.
|
||||
*/
|
||||
setOption(key: 'fontFamily' | 'termName' | 'bellSound', value: string): void;
|
||||
setOption(key: 'fontFamily' | 'termName' | 'bellSound' | 'wordSeparator', value: string): void;
|
||||
/**
|
||||
* Sets an option on the terminal.
|
||||
* @param key The option key.
|
||||
|
||||
Reference in New Issue
Block a user