Adds support for column selections if holding alt on mac and shift on windows/linux
This commit is contained in:
@@ -269,4 +269,81 @@ describe('Buffer', () => {
|
|||||||
assert.equal(buffer.markers.length, 0);
|
assert.equal(buffer.markers.length, 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe ('translateBufferLineToString', () => {
|
||||||
|
it('should handle selecting a section of ascii text', () => {
|
||||||
|
buffer.lines.set(0, [
|
||||||
|
[ null, 'a', 1, 'a'.charCodeAt(0)],
|
||||||
|
[ null, 'b', 1, 'b'.charCodeAt(0)],
|
||||||
|
[ null, 'c', 1, 'c'.charCodeAt(0)],
|
||||||
|
[ null, 'd', 1, 'd'.charCodeAt(0)]
|
||||||
|
]);
|
||||||
|
|
||||||
|
const str = buffer.translateBufferLineToString(0, true, 0, 2);
|
||||||
|
assert.equal(str, 'ab');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle a cut-off double width character by including it', () => {
|
||||||
|
buffer.lines.set(0, [
|
||||||
|
[ null, '語', 2, 35486 ],
|
||||||
|
[ null, '', 0, null],
|
||||||
|
[ null, 'a', 1, 'a'.charCodeAt(0)]
|
||||||
|
]);
|
||||||
|
|
||||||
|
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
|
||||||
|
assert.equal(str1, '語');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle a zero width character in the middle of the string by not including it', () => {
|
||||||
|
buffer.lines.set(0, [
|
||||||
|
[ null, '語', 2, '語'.charCodeAt(0) ],
|
||||||
|
[ null, '', 0, null],
|
||||||
|
[ null, 'a', 1, 'a'.charCodeAt(0)]
|
||||||
|
]);
|
||||||
|
|
||||||
|
const str0 = buffer.translateBufferLineToString(0, true, 0, 1);
|
||||||
|
assert.equal(str0, '語');
|
||||||
|
|
||||||
|
const str1 = buffer.translateBufferLineToString(0, true, 0, 2);
|
||||||
|
assert.equal(str1, '語');
|
||||||
|
|
||||||
|
const str2 = buffer.translateBufferLineToString(0, true, 0, 3);
|
||||||
|
assert.equal(str2, '語a');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle single width emojis', () => {
|
||||||
|
buffer.lines.set(0, [
|
||||||
|
[ null, '😁', 1, '😁'.charCodeAt(0) ],
|
||||||
|
[ null, 'a', 1, 'a'.charCodeAt(0)]
|
||||||
|
]);
|
||||||
|
|
||||||
|
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
|
||||||
|
assert.equal(str1, '😁');
|
||||||
|
|
||||||
|
const str2 = buffer.translateBufferLineToString(0, true, 0, 2);
|
||||||
|
assert.equal(str2, '😁a');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle double width emojis', () => {
|
||||||
|
buffer.lines.set(0, [
|
||||||
|
[ null, '😁', 2, '😁'.charCodeAt(0) ],
|
||||||
|
[ null, '', 0, null]
|
||||||
|
]);
|
||||||
|
|
||||||
|
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
|
||||||
|
assert.equal(str1, '😁');
|
||||||
|
|
||||||
|
const str2 = buffer.translateBufferLineToString(0, true, 0, 2);
|
||||||
|
assert.equal(str2, '😁');
|
||||||
|
|
||||||
|
buffer.lines.set(0, [
|
||||||
|
[ null, '😁', 2, '😁'.charCodeAt(0) ],
|
||||||
|
[ null, '', 0, null],
|
||||||
|
[ null, 'a', 1, 'a'.charCodeAt(0)]
|
||||||
|
]);
|
||||||
|
|
||||||
|
const str3 = buffer.translateBufferLineToString(0, true, 0, 3);
|
||||||
|
assert.equal(str3, '😁a');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -227,7 +227,7 @@ export class Buffer implements IBuffer {
|
|||||||
if (startCol >= i) {
|
if (startCol >= i) {
|
||||||
startIndex--;
|
startIndex--;
|
||||||
}
|
}
|
||||||
if (endCol >= i) {
|
if (endCol > i) {
|
||||||
endIndex--;
|
endIndex--;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import { CharMeasure } from './utils/CharMeasure';
|
import { CharMeasure } from './utils/CharMeasure';
|
||||||
import { SelectionManager } from './SelectionManager';
|
import { SelectionManager, SelectionMode } from './SelectionManager';
|
||||||
import { SelectionModel } from './SelectionModel';
|
import { SelectionModel } from './SelectionModel';
|
||||||
import { BufferSet } from './BufferSet';
|
import { BufferSet } from './BufferSet';
|
||||||
import { LineData, CharData, ITerminal, IBuffer } from './Types';
|
import { LineData, CharData, ITerminal, IBuffer } from './Types';
|
||||||
@@ -25,6 +25,8 @@ class TestSelectionManager extends SelectionManager {
|
|||||||
|
|
||||||
public get model(): SelectionModel { return this._model; }
|
public get model(): SelectionModel { return this._model; }
|
||||||
|
|
||||||
|
public set selectionMode(mode: SelectionMode) { this._activeSelectionMode = mode; }
|
||||||
|
|
||||||
public selectLineAt(line: number): void { this._selectLineAt(line); }
|
public selectLineAt(line: number): void { this._selectLineAt(line); }
|
||||||
public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords, true); }
|
public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords, true); }
|
||||||
|
|
||||||
@@ -378,4 +380,61 @@ describe('SelectionManager', () => {
|
|||||||
assert.equal(selectionManager.hasSelection, true);
|
assert.equal(selectionManager.hasSelection, true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('column selection', () => {
|
||||||
|
it('should select a column of text', () => {
|
||||||
|
buffer.lines.length = 3;
|
||||||
|
buffer.lines.set(0, stringToRow('abcdefghij'));
|
||||||
|
buffer.lines.set(1, stringToRow('klmnopqrst'));
|
||||||
|
buffer.lines.set(2, stringToRow('uvwxyz'));
|
||||||
|
|
||||||
|
selectionManager.selectionMode = SelectionMode.COLUMN;
|
||||||
|
selectionManager.model.selectionStart = [2, 0];
|
||||||
|
selectionManager.model.selectionEnd = [4, 2];
|
||||||
|
|
||||||
|
assert.equal(selectionManager.selectionText, 'cd\nmn\nwx');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select a column of text without chopping up double width characters', () => {
|
||||||
|
buffer.lines.length = 3;
|
||||||
|
buffer.lines.set(0, stringToRow('a'));
|
||||||
|
buffer.lines.set(1, stringToRow('語'));
|
||||||
|
buffer.lines.set(2, stringToRow('b'));
|
||||||
|
|
||||||
|
selectionManager.selectionMode = SelectionMode.COLUMN;
|
||||||
|
selectionManager.model.selectionStart = [0, 0];
|
||||||
|
selectionManager.model.selectionEnd = [1, 2];
|
||||||
|
|
||||||
|
assert.equal(selectionManager.selectionText, 'a\n語\nb');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select a column of text with single character emojis', () => {
|
||||||
|
buffer.lines.length = 3;
|
||||||
|
buffer.lines.set(0, stringToRow('a'));
|
||||||
|
buffer.lines.set(1, stringToRow('☃'));
|
||||||
|
buffer.lines.set(2, stringToRow('c'));
|
||||||
|
|
||||||
|
selectionManager.selectionMode = SelectionMode.COLUMN;
|
||||||
|
selectionManager.model.selectionStart = [0, 0];
|
||||||
|
selectionManager.model.selectionEnd = [1, 2];
|
||||||
|
|
||||||
|
assert.equal(selectionManager.selectionText, 'a\n☃\nc');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select a column of text with double character emojis', () => {
|
||||||
|
// TODO the case this is testing works for me in the demo webapp,
|
||||||
|
// but doing it programmatically fails.
|
||||||
|
buffer.lines.length = 3;
|
||||||
|
buffer.lines.set(0, stringToRow('a'));
|
||||||
|
buffer.lines.set(1, stringToRow('😁'));
|
||||||
|
buffer.lines.set(2, stringToRow('c'));
|
||||||
|
|
||||||
|
selectionManager.selectionMode = SelectionMode.COLUMN;
|
||||||
|
selectionManager.model.selectionStart = [0, 0];
|
||||||
|
selectionManager.model.selectionEnd = [1, 2];
|
||||||
|
|
||||||
|
assert.equal(selectionManager.selectionText, 'a\n😁\nc');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+81
-25
@@ -54,10 +54,11 @@ interface IWordPosition {
|
|||||||
/**
|
/**
|
||||||
* A selection mode, this drives how the selection behaves on mouse move.
|
* A selection mode, this drives how the selection behaves on mouse move.
|
||||||
*/
|
*/
|
||||||
const enum SelectionMode {
|
export const enum SelectionMode {
|
||||||
NORMAL,
|
NORMAL,
|
||||||
WORD,
|
WORD,
|
||||||
LINE
|
LINE,
|
||||||
|
COLUMN
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -80,7 +81,12 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
/**
|
/**
|
||||||
* The current selection mode.
|
* The current selection mode.
|
||||||
*/
|
*/
|
||||||
private _activeSelectionMode: SelectionMode;
|
protected _activeSelectionMode: SelectionMode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The modifier keys required to trigger block select mode with left click + drag
|
||||||
|
*/
|
||||||
|
private _columnSelectRequiredModifiers: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A setInterval timer that is active while the mouse is down whose callback
|
* A setInterval timer that is active while the mouse is down whose callback
|
||||||
@@ -114,6 +120,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
|
|
||||||
this._model = new SelectionModel(_terminal);
|
this._model = new SelectionModel(_terminal);
|
||||||
this._activeSelectionMode = SelectionMode.NORMAL;
|
this._activeSelectionMode = SelectionMode.NORMAL;
|
||||||
|
this._columnSelectRequiredModifiers = this._initColumnSelectModifierKeys();
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _buffer(): IBuffer {
|
private get _buffer(): IBuffer {
|
||||||
@@ -177,30 +184,41 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get first row
|
|
||||||
const startRowEndCol = start[1] === end[1] ? end[0] : null;
|
|
||||||
const result: string[] = [];
|
const result: string[] = [];
|
||||||
result.push(this._buffer.translateBufferLineToString(start[1], true, start[0], startRowEndCol));
|
|
||||||
|
|
||||||
// Get middle rows
|
if (this._activeSelectionMode === SelectionMode.COLUMN) {
|
||||||
for (let i = start[1] + 1; i <= end[1] - 1; i++) {
|
// Ignore zero width selections
|
||||||
const bufferLine = this._buffer.lines.get(i);
|
if (start[0] !== end[0]) {
|
||||||
const lineText = this._buffer.translateBufferLineToString(i, true);
|
for (let i = start[1]; i <= end[1]; i++) {
|
||||||
if ((<any>bufferLine).isWrapped) {
|
const lineText = this._buffer.translateBufferLineToString(i, true, start[0], end[0]);
|
||||||
result[result.length - 1] += lineText;
|
result.push(lineText);
|
||||||
} else {
|
}
|
||||||
result.push(lineText);
|
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
// Get first row
|
||||||
|
const startRowEndCol = start[1] === end[1] ? end[0] : null;
|
||||||
|
result.push(this._buffer.translateBufferLineToString(start[1], true, start[0], startRowEndCol));
|
||||||
|
|
||||||
// Get final row
|
// Get middle rows
|
||||||
if (start[1] !== end[1]) {
|
for (let i = start[1] + 1; i <= end[1] - 1; i++) {
|
||||||
const bufferLine = this._buffer.lines.get(end[1]);
|
const bufferLine = this._buffer.lines.get(i);
|
||||||
const lineText = this._buffer.translateBufferLineToString(end[1], true, 0, end[0]);
|
const lineText = this._buffer.translateBufferLineToString(i, true);
|
||||||
if ((<any>bufferLine).isWrapped) {
|
if ((<any>bufferLine).isWrapped) {
|
||||||
result[result.length - 1] += lineText;
|
result[result.length - 1] += lineText;
|
||||||
} else {
|
} else {
|
||||||
result.push(lineText);
|
result.push(lineText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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]);
|
||||||
|
if ((<any>bufferLine).isWrapped) {
|
||||||
|
result[result.length - 1] += lineText;
|
||||||
|
} else {
|
||||||
|
result.push(lineText);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,7 +267,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
*/
|
*/
|
||||||
private _refresh(): void {
|
private _refresh(): void {
|
||||||
this._refreshAnimationFrame = null;
|
this._refreshAnimationFrame = null;
|
||||||
this.emit('refresh', { start: this._model.finalSelectionStart, end: this._model.finalSelectionEnd });
|
this.emit('refresh', { start: this._model.finalSelectionStart, end: this._model.finalSelectionEnd, columnSelectMode: this._activeSelectionMode === SelectionMode.COLUMN });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -398,7 +416,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
this._onIncrementalClick(event);
|
this._onIncrementalClick(event);
|
||||||
} else {
|
} else {
|
||||||
if (event.detail === 1) {
|
if (event.detail === 1) {
|
||||||
this._onSingleClick(event);
|
if (this.isColumnSelectMode(event)) {
|
||||||
|
this._onColumnSelectSingleClick(event);
|
||||||
|
} else {
|
||||||
|
this._onSingleClick(event);
|
||||||
|
}
|
||||||
} else if (event.detail === 2) {
|
} else if (event.detail === 2) {
|
||||||
this._onDoubleClick(event);
|
this._onDoubleClick(event);
|
||||||
} else if (event.detail === 3) {
|
} else if (event.detail === 3) {
|
||||||
@@ -502,6 +524,40 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures the modifier key for enabling column selection mode
|
||||||
|
*/
|
||||||
|
private _initColumnSelectModifierKeys(): string[] {
|
||||||
|
if (this._terminal.browser.isMac) {
|
||||||
|
return ['altKey'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linux and Windows
|
||||||
|
return ['shiftKey'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Begin a block selection
|
||||||
|
*/
|
||||||
|
private _onColumnSelectSingleClick(event: MouseEvent): void {
|
||||||
|
this._onSingleClick(event); // Perform all the normal setup actions
|
||||||
|
this._activeSelectionMode = SelectionMode.COLUMN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if all required key modifiers are pressed in order to enable block
|
||||||
|
* select mode
|
||||||
|
* @param event the mouse click event
|
||||||
|
*/
|
||||||
|
public isColumnSelectMode(event: KeyboardEvent | MouseEvent): boolean {
|
||||||
|
for (let i = 0; i < this._columnSelectRequiredModifiers.length; i++) {
|
||||||
|
if (!(<any>event)[this._columnSelectRequiredModifiers[i]]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the mousemove event when the mouse button is down, recording the
|
* Handles the mousemove event when the mouse button is down, recording the
|
||||||
* end of the selection and refreshing the selection.
|
* end of the selection and refreshing the selection.
|
||||||
|
|||||||
+21
-1
@@ -590,6 +590,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
if (!wasMondifierKeyOnlyEvent(ev)) {
|
if (!wasMondifierKeyOnlyEvent(ev)) {
|
||||||
this.focus();
|
this.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self._keyUp(ev);
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
on(this.textarea, 'keydown', (ev: KeyboardEvent) => this._keyDown(ev), true);
|
on(this.textarea, 'keydown', (ev: KeyboardEvent) => this._keyDown(ev), true);
|
||||||
@@ -696,7 +698,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
|
|
||||||
this.selectionManager = new SelectionManager(this, this.charMeasure);
|
this.selectionManager = new SelectionManager(this, this.charMeasure);
|
||||||
this.element.addEventListener('mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e));
|
this.element.addEventListener('mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e));
|
||||||
this.selectionManager.on('refresh', data => this.renderer.onSelectionChanged(data.start, data.end));
|
this.selectionManager.on('refresh', data => this.renderer.onSelectionChanged(data.start, data.end, data.columnSelectMode));
|
||||||
this.selectionManager.on('newselection', text => {
|
this.selectionManager.on('newselection', text => {
|
||||||
// If there's a new selection, put it into the textarea, focus and select it
|
// If there's a new selection, put it into the textarea, focus and select it
|
||||||
// in order to register it as a selection on the OS. This event is fired
|
// in order to register it as a selection on the OS. This event is fired
|
||||||
@@ -1098,6 +1100,17 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the cursor style for different selection modes
|
||||||
|
*/
|
||||||
|
public updateCursorStyle(ev: KeyboardEvent): void {
|
||||||
|
if (this.selectionManager.isColumnSelectMode(ev)) {
|
||||||
|
this.element.classList.add('xterm-cursor-crosshair');
|
||||||
|
} else {
|
||||||
|
this.element.classList.remove('xterm-cursor-crosshair');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the cursor element
|
* Display the cursor element
|
||||||
*/
|
*/
|
||||||
@@ -1415,6 +1428,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
|
|
||||||
const result = evaluateKeyboardEvent(event, this.applicationCursor, this.browser.isMac, this.options.macOptionIsMeta);
|
const result = evaluateKeyboardEvent(event, this.applicationCursor, this.browser.isMac, this.options.macOptionIsMeta);
|
||||||
|
|
||||||
|
this.updateCursorStyle(event);
|
||||||
|
|
||||||
// if (result.key === C0.DC3) { // XOFF
|
// if (result.key === C0.DC3) { // XOFF
|
||||||
// this._writeStopped = true;
|
// this._writeStopped = true;
|
||||||
// } else if (result.key === C0.DC1) { // XON
|
// } else if (result.key === C0.DC1) { // XON
|
||||||
@@ -1486,6 +1501,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected _keyUp(ev: KeyboardEvent): boolean {
|
||||||
|
this.updateCursorStyle(ev);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle a keypress event.
|
* Handle a keypress event.
|
||||||
* Key Resources:
|
* Key Resources:
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
public onFocus(terminal: ITerminal): void {}
|
public onFocus(terminal: ITerminal): void {}
|
||||||
public onCursorMove(terminal: ITerminal): void {}
|
public onCursorMove(terminal: ITerminal): void {}
|
||||||
public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void {}
|
public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void {}
|
||||||
public onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void {}
|
public onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number], columnSelectMode: boolean = false): void {}
|
||||||
|
|
||||||
public onThemeChanged(terminal: ITerminal, colorSet: IColorSet): void {
|
public onThemeChanged(terminal: ITerminal, colorSet: IColorSet): void {
|
||||||
this._refreshCharAtlas(terminal, colorSet);
|
this._refreshCharAtlas(terminal, colorSet);
|
||||||
|
|||||||
@@ -141,8 +141,8 @@ export class Renderer extends EventEmitter implements IRenderer {
|
|||||||
this._runOperation(l => l.onFocus(this._terminal));
|
this._runOperation(l => l.onFocus(this._terminal));
|
||||||
}
|
}
|
||||||
|
|
||||||
public onSelectionChanged(start: [number, number], end: [number, number]): void {
|
public onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean = false): void {
|
||||||
this._runOperation(l => l.onSelectionChanged(this._terminal, start, end));
|
this._runOperation(l => l.onSelectionChanged(this._terminal, start, end, columnSelectMode));
|
||||||
}
|
}
|
||||||
|
|
||||||
public onCursorMove(): void {
|
public onCursorMove(): void {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void {
|
public onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number], columnSelectMode: boolean): void {
|
||||||
// Selection has not changed
|
// Selection has not changed
|
||||||
if (this._state.start === start || this._state.end === end) {
|
if (this._state.start === start || this._state.end === end) {
|
||||||
return;
|
return;
|
||||||
@@ -62,21 +62,30 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw first row
|
|
||||||
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
|
|
||||||
const startRowEndCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : terminal.cols;
|
|
||||||
this._ctx.fillStyle = this._colors.selection.css;
|
this._ctx.fillStyle = this._colors.selection.css;
|
||||||
this.fillCells(startCol, viewportCappedStartRow, startRowEndCol - startCol, 1);
|
|
||||||
|
|
||||||
// Draw middle rows
|
if (columnSelectMode) {
|
||||||
const middleRowsCount = Math.max(viewportCappedEndRow - viewportCappedStartRow - 1, 0);
|
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
|
||||||
this.fillCells(0, viewportCappedStartRow + 1, terminal.cols, middleRowsCount);
|
const width = end[0] - startCol;
|
||||||
|
const height = viewportCappedEndRow - viewportCappedStartRow + 1;
|
||||||
|
this.fillCells(startCol, viewportCappedStartRow, width, height);
|
||||||
|
|
||||||
// Draw final row
|
} else {
|
||||||
if (viewportCappedStartRow !== viewportCappedEndRow) {
|
// Draw first row
|
||||||
// Only draw viewportEndRow if it's not the same as viewportStartRow
|
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
|
||||||
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : terminal.cols;
|
const startRowEndCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : terminal.cols;
|
||||||
this.fillCells(0, viewportCappedEndRow, endCol, 1);
|
this.fillCells(startCol, viewportCappedStartRow, startRowEndCol - startCol, 1);
|
||||||
|
|
||||||
|
// Draw middle rows
|
||||||
|
const middleRowsCount = Math.max(viewportCappedEndRow - viewportCappedStartRow - 1, 0);
|
||||||
|
this.fillCells(0, viewportCappedStartRow + 1, terminal.cols, middleRowsCount);
|
||||||
|
|
||||||
|
// Draw final row
|
||||||
|
if (viewportCappedStartRow !== viewportCappedEndRow) {
|
||||||
|
// Only draw viewportEndRow if it's not the same as viewportStartRow
|
||||||
|
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : terminal.cols;
|
||||||
|
this.fillCells(0, viewportCappedEndRow, endCol, 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save state for next render
|
// Save state for next render
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export interface IRenderer extends IEventEmitter {
|
|||||||
onCharSizeChanged(): void;
|
onCharSizeChanged(): void;
|
||||||
onBlur(): void;
|
onBlur(): void;
|
||||||
onFocus(): void;
|
onFocus(): void;
|
||||||
onSelectionChanged(start: [number, number], end: [number, number]): void;
|
onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void;
|
||||||
onCursorMove(): void;
|
onCursorMove(): void;
|
||||||
onOptionsChanged(): void;
|
onOptionsChanged(): void;
|
||||||
clear(): void;
|
clear(): void;
|
||||||
@@ -98,7 +98,7 @@ export interface IRenderLayer {
|
|||||||
/**
|
/**
|
||||||
* Calls when the selection changes.
|
* Calls when the selection changes.
|
||||||
*/
|
*/
|
||||||
onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void;
|
onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number], columnSelectMode: boolean): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resize the render layer.
|
* Resize the render layer.
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ export class DomRenderer extends EventEmitter implements IRenderer {
|
|||||||
this._rowContainer.classList.add(FOCUS_CLASS);
|
this._rowContainer.classList.add(FOCUS_CLASS);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onSelectionChanged(start: [number, number], end: [number, number]): void {
|
public onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void {
|
||||||
// Remove all selections
|
// Remove all selections
|
||||||
while (this._selectionContainer.children.length) {
|
while (this._selectionContainer.children.length) {
|
||||||
this._selectionContainer.removeChild(this._selectionContainer.children[0]);
|
this._selectionContainer.removeChild(this._selectionContainer.children[0]);
|
||||||
@@ -241,18 +241,25 @@ export class DomRenderer extends EventEmitter implements IRenderer {
|
|||||||
|
|
||||||
// Create the selections
|
// Create the selections
|
||||||
const documentFragment = document.createDocumentFragment();
|
const documentFragment = document.createDocumentFragment();
|
||||||
// Draw first row
|
|
||||||
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
|
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
|
||||||
const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
|
|
||||||
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol));
|
if (columnSelectMode) {
|
||||||
// Draw middle rows
|
documentFragment.appendChild(
|
||||||
const middleRowsCount = viewportCappedEndRow - viewportCappedStartRow - 1;
|
this._createSelectionElement(viewportCappedStartRow, startCol, end[0], viewportCappedEndRow - viewportStartRow + 1)
|
||||||
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._terminal.cols, middleRowsCount));
|
);
|
||||||
// Draw final row
|
} else {
|
||||||
if (viewportCappedStartRow !== viewportCappedEndRow) {
|
// Draw first row
|
||||||
// Only draw viewportEndRow if it's not the same as viewporttartRow
|
const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
|
||||||
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
|
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol));
|
||||||
documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, endCol));
|
// Draw middle rows
|
||||||
|
const middleRowsCount = viewportCappedEndRow - viewportCappedStartRow - 1;
|
||||||
|
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._terminal.cols, middleRowsCount));
|
||||||
|
// Draw final row
|
||||||
|
if (viewportCappedStartRow !== viewportCappedEndRow) {
|
||||||
|
// Only draw viewportEndRow if it's not the same as viewporttartRow
|
||||||
|
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
|
||||||
|
documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, endCol));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this._selectionContainer.appendChild(documentFragment);
|
this._selectionContainer.appendChild(documentFragment);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,6 +139,11 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.xterm.xterm-cursor-crosshair {
|
||||||
|
/* Block selection mode */
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
|
||||||
.xterm .xterm-accessibility,
|
.xterm .xterm-accessibility,
|
||||||
.xterm .xterm-message {
|
.xterm .xterm-message {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
Reference in New Issue
Block a user