From 7ee22193142c04b47c85def4a0c2ea1700cd1813 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 29 Jun 2019 14:27:50 -0700 Subject: [PATCH] Rename SelectionManager to SelectionService --- src/Clipboard.ts | 16 +- src/InputHandler.ts | 8 +- src/SelectionManager.test.ts | 489 ------------------ src/SelectionService.test.ts | 489 ++++++++++++++++++ ...electionManager.ts => SelectionService.ts} | 10 +- src/Terminal.ts | 72 +-- src/TestUtils.test.ts | 6 +- src/Types.d.ts | 6 +- src/browser/selection/Types.d.ts | 12 - src/browser/services/Services.d.ts | 12 + 10 files changed, 560 insertions(+), 560 deletions(-) delete mode 100644 src/SelectionManager.test.ts create mode 100644 src/SelectionService.test.ts rename src/{SelectionManager.ts => SelectionService.ts} (98%) diff --git a/src/Clipboard.ts b/src/Clipboard.ts index 1ee232eee..f4b415deb 100644 --- a/src/Clipboard.ts +++ b/src/Clipboard.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ISelectionManager } from 'browser/selection/Types'; +import { ISelectionService } from 'browser/services/Services'; /** * Prepares text to be pasted into the terminal by normalizing the line endings @@ -28,8 +28,8 @@ 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, selectionManager: ISelectionManager): void { - ev.clipboardData.setData('text/plain', selectionManager.selectionText); +export function copyHandler(ev: ClipboardEvent, selectionService: ISelectionService): void { + ev.clipboardData.setData('text/plain', selectionService.selectionText); // Prevent or the original text will be copied. ev.preventDefault(); } @@ -95,17 +95,17 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA * Bind to right-click event and allow right-click copy and paste. * @param ev The original right click event to be handled. * @param textarea The terminal's textarea. - * @param selectionManager The terminal's selection manager. + * @param selectionService 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, textarea: HTMLTextAreaElement, screenElement: HTMLElement, selectionManager: ISelectionManager, shouldSelectWord: boolean): void { +export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, screenElement: HTMLElement, selectionService: ISelectionService, shouldSelectWord: boolean): void { moveTextAreaUnderMouseCursor(ev, textarea, screenElement); - if (shouldSelectWord && !selectionManager.isClickInSelection(ev)) { - selectionManager.selectWordAtCursor(ev); + if (shouldSelectWord && !selectionService.isClickInSelection(ev)) { + selectionService.selectWordAtCursor(ev); } // Get textarea ready to copy from the context menu - textarea.value = selectionManager.selectionText; + textarea.value = selectionService.selectionText; textarea.select(); } diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 7d1b767d7..c58a32234 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -1347,8 +1347,8 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._terminal.element) { this._terminal.element.classList.add('enable-mouse-events'); } - if (this._terminal.selectionManager) { - this._terminal.selectionManager.disable(); + if (this._terminal.selectionService) { + this._terminal.selectionService.disable(); } this._terminal.log('Binding to mouse events.'); break; @@ -1539,8 +1539,8 @@ export class InputHandler extends Disposable implements IInputHandler { if (this._terminal.element) { this._terminal.element.classList.remove('enable-mouse-events'); } - if (this._terminal.selectionManager) { - this._terminal.selectionManager.enable(); + if (this._terminal.selectionService) { + this._terminal.selectionService.enable(); } break; case 1004: // send focusin/focusout events diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts deleted file mode 100644 index 61d3c669c..000000000 --- a/src/SelectionManager.test.ts +++ /dev/null @@ -1,489 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { assert } from 'chai'; -import { SelectionManager, SelectionMode } from './SelectionManager'; -import { SelectionModel } from 'browser/selection/SelectionModel'; -import { IBufferLine } from 'common/Types'; -import { MockBufferService, MockOptionsService, MockCoreService } from 'common/TestUtils.test'; -import { BufferLine } from 'common/buffer/BufferLine'; -import { IBufferService, IOptionsService } from 'common/services/Services'; -import { MockCharSizeService, MockMouseService } from 'browser/TestUtils.test'; -import { CellData } from 'common/buffer/CellData'; -import { IBuffer } from 'common/buffer/Types'; - -class TestSelectionManager extends SelectionManager { - constructor( - bufferService: IBufferService, - optionsService: IOptionsService - ) { - super(() => {}, null, null, new MockCharSizeService(10, 10), bufferService, new MockCoreService(), new MockMouseService(), optionsService); - } - - public get model(): SelectionModel { return this._model; } - - public set selectionMode(mode: SelectionMode) { this._activeSelectionMode = mode; } - - public selectLineAt(line: number): void { this._selectLineAt(line); } - public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords, true); } - public areCoordsInSelection(coords: [number, number], start: [number, number], end: [number, number]): boolean { return this._areCoordsInSelection(coords, start, end); } - - // Disable DOM interaction - public enable(): void {} - public disable(): void {} - public refresh(): void {} -} - -describe('SelectionManager', () => { - let buffer: IBuffer; - let bufferService: IBufferService; - let optionsService: IOptionsService; - let selectionManager: TestSelectionManager; - - beforeEach(() => { - optionsService = new MockOptionsService(); - bufferService = new MockBufferService(20, 20, optionsService); - buffer = bufferService.buffer; - selectionManager = new TestSelectionManager(bufferService, optionsService); - }); - - function stringToRow(text: string): IBufferLine { - const result = new BufferLine(text.length); - for (let i = 0; i < text.length; i++) { - result.setCell(i, CellData.fromCharData([0, text.charAt(i), 1, text.charCodeAt(i)])); - } - return result; - } - - function stringArrayToRow(chars: string[]): IBufferLine { - const line = new BufferLine(chars.length); - chars.map((c, idx) => line.setCell(idx, CellData.fromCharData([0, c, 1, c.charCodeAt(0)]))); - return line; - } - - describe('_selectWordAt', () => { - it('should expand selection for normal width chars', () => { - buffer.lines.set(0, stringToRow('foo bar')); - selectionManager.selectWordAt([0, 0]); - assert.equal(selectionManager.selectionText, 'foo'); - selectionManager.selectWordAt([1, 0]); - assert.equal(selectionManager.selectionText, 'foo'); - selectionManager.selectWordAt([2, 0]); - assert.equal(selectionManager.selectionText, 'foo'); - selectionManager.selectWordAt([3, 0]); - assert.equal(selectionManager.selectionText, ' '); - selectionManager.selectWordAt([4, 0]); - assert.equal(selectionManager.selectionText, 'bar'); - selectionManager.selectWordAt([5, 0]); - assert.equal(selectionManager.selectionText, 'bar'); - selectionManager.selectWordAt([6, 0]); - assert.equal(selectionManager.selectionText, 'bar'); - }); - it('should expand selection for whitespace', () => { - buffer.lines.set(0, stringToRow('a b')); - selectionManager.selectWordAt([0, 0]); - assert.equal(selectionManager.selectionText, 'a'); - selectionManager.selectWordAt([1, 0]); - assert.equal(selectionManager.selectionText, ' '); - selectionManager.selectWordAt([2, 0]); - assert.equal(selectionManager.selectionText, ' '); - selectionManager.selectWordAt([3, 0]); - assert.equal(selectionManager.selectionText, ' '); - selectionManager.selectWordAt([4, 0]); - assert.equal(selectionManager.selectionText, 'b'); - }); - it('should expand selection for wide characters', () => { - // Wide characters use a special format - const data: [number, string, number, number][] = [ - [null, '中', 2, '中'.charCodeAt(0)], - [null, '', 0, null], - [null, '文', 2, '文'.charCodeAt(0)], - [null, '', 0, null], - [null, ' ', 1, ' '.charCodeAt(0)], - [null, 'a', 1, 'a'.charCodeAt(0)], - [null, '中', 2, '中'.charCodeAt(0)], - [null, '', 0, null], - [null, '文', 2, '文'.charCodeAt(0)], - [null, '', 0, ''.charCodeAt(0)], - [null, 'b', 1, 'b'.charCodeAt(0)], - [null, ' ', 1, ' '.charCodeAt(0)], - [null, 'f', 1, 'f'.charCodeAt(0)], - [null, 'o', 1, 'o'.charCodeAt(0)], - [null, 'o', 1, 'o'.charCodeAt(0)] - ]; - const line = new BufferLine(data.length); - for (let i = 0; i < data.length; ++i) line.setCell(i, CellData.fromCharData(data[i])); - buffer.lines.set(0, line); - // Ensure wide characters take up 2 columns - selectionManager.selectWordAt([0, 0]); - assert.equal(selectionManager.selectionText, '中文'); - selectionManager.selectWordAt([1, 0]); - assert.equal(selectionManager.selectionText, '中文'); - selectionManager.selectWordAt([2, 0]); - assert.equal(selectionManager.selectionText, '中文'); - selectionManager.selectWordAt([3, 0]); - assert.equal(selectionManager.selectionText, '中文'); - selectionManager.selectWordAt([4, 0]); - assert.equal(selectionManager.selectionText, ' '); - // Ensure wide characters work when wrapped in normal width characters - selectionManager.selectWordAt([5, 0]); - assert.equal(selectionManager.selectionText, 'a中文b'); - selectionManager.selectWordAt([6, 0]); - assert.equal(selectionManager.selectionText, 'a中文b'); - selectionManager.selectWordAt([7, 0]); - assert.equal(selectionManager.selectionText, 'a中文b'); - selectionManager.selectWordAt([8, 0]); - assert.equal(selectionManager.selectionText, 'a中文b'); - selectionManager.selectWordAt([9, 0]); - assert.equal(selectionManager.selectionText, 'a中文b'); - selectionManager.selectWordAt([10, 0]); - assert.equal(selectionManager.selectionText, 'a中文b'); - selectionManager.selectWordAt([11, 0]); - assert.equal(selectionManager.selectionText, ' '); - // Ensure normal width characters work fine in a line containing wide characters - selectionManager.selectWordAt([12, 0]); - assert.equal(selectionManager.selectionText, 'foo'); - selectionManager.selectWordAt([13, 0]); - assert.equal(selectionManager.selectionText, 'foo'); - selectionManager.selectWordAt([14, 0]); - assert.equal(selectionManager.selectionText, 'foo'); - }); - it('should select up to non-path characters that are commonly adjacent to paths', () => { - buffer.lines.set(0, stringToRow('(cd)[ef]{gh}\'ij"')); - selectionManager.selectWordAt([0, 0]); - assert.equal(selectionManager.selectionText, '(cd'); - selectionManager.selectWordAt([1, 0]); - assert.equal(selectionManager.selectionText, 'cd'); - selectionManager.selectWordAt([2, 0]); - assert.equal(selectionManager.selectionText, 'cd'); - selectionManager.selectWordAt([3, 0]); - assert.equal(selectionManager.selectionText, 'cd)'); - selectionManager.selectWordAt([4, 0]); - assert.equal(selectionManager.selectionText, '[ef'); - selectionManager.selectWordAt([5, 0]); - assert.equal(selectionManager.selectionText, 'ef'); - selectionManager.selectWordAt([6, 0]); - assert.equal(selectionManager.selectionText, 'ef'); - selectionManager.selectWordAt([7, 0]); - assert.equal(selectionManager.selectionText, 'ef]'); - selectionManager.selectWordAt([8, 0]); - assert.equal(selectionManager.selectionText, '{gh'); - selectionManager.selectWordAt([9, 0]); - assert.equal(selectionManager.selectionText, 'gh'); - selectionManager.selectWordAt([10, 0]); - assert.equal(selectionManager.selectionText, 'gh'); - selectionManager.selectWordAt([11, 0]); - assert.equal(selectionManager.selectionText, 'gh}'); - selectionManager.selectWordAt([12, 0]); - assert.equal(selectionManager.selectionText, '\'ij'); - selectionManager.selectWordAt([13, 0]); - assert.equal(selectionManager.selectionText, 'ij'); - selectionManager.selectWordAt([14, 0]); - assert.equal(selectionManager.selectionText, 'ij'); - selectionManager.selectWordAt([15, 0]); - assert.equal(selectionManager.selectionText, 'ij"'); - }); - it('should expand upwards or downards for wrapped lines', () => { - buffer.lines.set(0, stringToRow(' foo')); - buffer.lines.set(1, stringToRow('bar ')); - buffer.lines.get(1).isWrapped = true; - selectionManager.selectWordAt([1, 1]); - assert.equal(selectionManager.selectionText, 'foobar'); - selectionManager.model.clearSelection(); - selectionManager.selectWordAt([18, 0]); - assert.equal(selectionManager.selectionText, 'foobar'); - }); - it('should expand both upwards and downwards for word wrapped over many lines', () => { - const expectedText = 'fooaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbccccccccccccccccccccbar'; - buffer.lines.set(0, stringToRow(' foo')); - buffer.lines.set(1, stringToRow('aaaaaaaaaaaaaaaaaaaa')); - buffer.lines.set(2, stringToRow('bbbbbbbbbbbbbbbbbbbb')); - buffer.lines.set(3, stringToRow('cccccccccccccccccccc')); - buffer.lines.set(4, stringToRow('bar ')); - buffer.lines.get(1).isWrapped = true; - buffer.lines.get(2).isWrapped = true; - buffer.lines.get(3).isWrapped = true; - buffer.lines.get(4).isWrapped = true; - selectionManager.selectWordAt([18, 0]); - assert.equal(selectionManager.selectionText, expectedText); - selectionManager.model.clearSelection(); - selectionManager.selectWordAt([10, 1]); - assert.equal(selectionManager.selectionText, expectedText); - selectionManager.model.clearSelection(); - selectionManager.selectWordAt([10, 2]); - assert.equal(selectionManager.selectionText, expectedText); - selectionManager.model.clearSelection(); - selectionManager.selectWordAt([10, 3]); - assert.equal(selectionManager.selectionText, expectedText); - selectionManager.model.clearSelection(); - selectionManager.selectWordAt([1, 4]); - assert.equal(selectionManager.selectionText, expectedText); - }); - describe('emoji', () => { - it('should treat a single emoji as a word when wrapped in spaces', () => { - buffer.lines.set(0, stringToRow(' ⚽ a')); // The a is here to prevent the space being trimmed in selectionText - selectionManager.selectWordAt([0, 0]); - assert.equal(selectionManager.selectionText, ' '); - selectionManager.selectWordAt([1, 0]); - assert.equal(selectionManager.selectionText, '⚽'); - selectionManager.selectWordAt([2, 0]); - assert.equal(selectionManager.selectionText, ' '); - }); - it('should treat multiple emojis as a word when wrapped in spaces', () => { - buffer.lines.set(0, stringToRow(' ⚽⚽ a')); // The a is here to prevent the space being trimmed in selectionText - selectionManager.selectWordAt([0, 0]); - assert.equal(selectionManager.selectionText, ' '); - selectionManager.selectWordAt([1, 0]); - assert.equal(selectionManager.selectionText, '⚽⚽'); - selectionManager.selectWordAt([2, 0]); - assert.equal(selectionManager.selectionText, '⚽⚽'); - selectionManager.selectWordAt([3, 0]); - assert.equal(selectionManager.selectionText, ' '); - }); - it('should treat emojis using the zero-width-joiner as a single word', () => { - // Note that the first 3 emojis include the invisible ZWJ char - buffer.lines.set(0, stringArrayToRow([ - ' ', '👨‍', '👩‍', '👧‍', '👦', ' ', 'a' - ])); // The a is here to prevent the space being trimmed in selectionText - selectionManager.selectWordAt([0, 0]); - assert.equal(selectionManager.selectionText, ' '); - // ZWJ emojis do not combine in the terminal so the family emoji used here consumed 4 cells - // The selection text should retain ZWJ chars despite not combining on the terminal - selectionManager.selectWordAt([1, 0]); - assert.equal(selectionManager.selectionText, '👨‍👩‍👧‍👦'); - selectionManager.selectWordAt([2, 0]); - assert.equal(selectionManager.selectionText, '👨‍👩‍👧‍👦'); - selectionManager.selectWordAt([3, 0]); - assert.equal(selectionManager.selectionText, '👨‍👩‍👧‍👦'); - selectionManager.selectWordAt([4, 0]); - assert.equal(selectionManager.selectionText, '👨‍👩‍👧‍👦'); - selectionManager.selectWordAt([5, 0]); - assert.equal(selectionManager.selectionText, ' '); - }); - it('should treat emojis and characters joined together as a word', () => { - buffer.lines.set(0, stringToRow(' ⚽ab cd⚽ ef⚽gh')); // The a is here to prevent the space being trimmed in selectionText - selectionManager.selectWordAt([0, 0]); - assert.equal(selectionManager.selectionText, ' '); - selectionManager.selectWordAt([1, 0]); - assert.equal(selectionManager.selectionText, '⚽ab'); - selectionManager.selectWordAt([2, 0]); - assert.equal(selectionManager.selectionText, '⚽ab'); - selectionManager.selectWordAt([3, 0]); - assert.equal(selectionManager.selectionText, '⚽ab'); - selectionManager.selectWordAt([4, 0]); - assert.equal(selectionManager.selectionText, ' '); - selectionManager.selectWordAt([5, 0]); - assert.equal(selectionManager.selectionText, 'cd⚽'); - selectionManager.selectWordAt([6, 0]); - assert.equal(selectionManager.selectionText, 'cd⚽'); - selectionManager.selectWordAt([7, 0]); - assert.equal(selectionManager.selectionText, 'cd⚽'); - selectionManager.selectWordAt([8, 0]); - assert.equal(selectionManager.selectionText, ' '); - selectionManager.selectWordAt([9, 0]); - assert.equal(selectionManager.selectionText, 'ef⚽gh'); - selectionManager.selectWordAt([10, 0]); - assert.equal(selectionManager.selectionText, 'ef⚽gh'); - selectionManager.selectWordAt([11, 0]); - assert.equal(selectionManager.selectionText, 'ef⚽gh'); - selectionManager.selectWordAt([12, 0]); - assert.equal(selectionManager.selectionText, 'ef⚽gh'); - selectionManager.selectWordAt([13, 0]); - assert.equal(selectionManager.selectionText, 'ef⚽gh'); - }); - it('should treat complex emojis and characters joined together as a word', () => { - // This emoji is the flag for England and is made up of: 1F3F4 E0067 E0062 E0065 E006E E0067 E007F - buffer.lines.set(0, stringArrayToRow([ - ' ', '🏴󠁧󠁢󠁥󠁮󠁧󠁿', 'a', 'b', ' ', 'c', 'd', '🏴󠁧󠁢󠁥󠁮󠁧󠁿', ' ', 'e', 'f', '🏴󠁧󠁢󠁥󠁮󠁧󠁿', 'g', 'h', ' ', 'a' - ])); // The a is here to prevent the space being trimmed in selectionText - selectionManager.selectWordAt([0, 0]); - assert.equal(selectionManager.selectionText, ' '); - selectionManager.selectWordAt([1, 0]); - assert.equal(selectionManager.selectionText, '🏴󠁧󠁢󠁥󠁮󠁧󠁿ab'); - selectionManager.selectWordAt([2, 0]); - assert.equal(selectionManager.selectionText, '🏴󠁧󠁢󠁥󠁮󠁧󠁿ab'); - selectionManager.selectWordAt([3, 0]); - assert.equal(selectionManager.selectionText, '🏴󠁧󠁢󠁥󠁮󠁧󠁿ab'); - selectionManager.selectWordAt([4, 0]); - assert.equal(selectionManager.selectionText, ' '); - selectionManager.selectWordAt([5, 0]); - assert.equal(selectionManager.selectionText, 'cd🏴󠁧󠁢󠁥󠁮󠁧󠁿'); - selectionManager.selectWordAt([6, 0]); - assert.equal(selectionManager.selectionText, 'cd🏴󠁧󠁢󠁥󠁮󠁧󠁿'); - selectionManager.selectWordAt([7, 0]); - assert.equal(selectionManager.selectionText, 'cd🏴󠁧󠁢󠁥󠁮󠁧󠁿'); - selectionManager.selectWordAt([8, 0]); - assert.equal(selectionManager.selectionText, ' '); - selectionManager.selectWordAt([9, 0]); - assert.equal(selectionManager.selectionText, 'ef🏴󠁧󠁢󠁥󠁮󠁧󠁿gh'); - selectionManager.selectWordAt([10, 0]); - assert.equal(selectionManager.selectionText, 'ef🏴󠁧󠁢󠁥󠁮󠁧󠁿gh'); - selectionManager.selectWordAt([11, 0]); - assert.equal(selectionManager.selectionText, 'ef🏴󠁧󠁢󠁥󠁮󠁧󠁿gh'); - selectionManager.selectWordAt([12, 0]); - assert.equal(selectionManager.selectionText, 'ef🏴󠁧󠁢󠁥󠁮󠁧󠁿gh'); - selectionManager.selectWordAt([13, 0]); - assert.equal(selectionManager.selectionText, 'ef🏴󠁧󠁢󠁥󠁮󠁧󠁿gh'); - }); - }); - }); - - describe('_selectLineAt', () => { - it('should select the entire line', () => { - buffer.lines.set(0, stringToRow('foo bar')); - selectionManager.selectLineAt(0); - assert.equal(selectionManager.selectionText, 'foo bar', 'The selected text is correct'); - assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]); - assert.deepEqual(selectionManager.model.finalSelectionEnd, [bufferService.cols, 0], 'The actual selection spans the entire column'); - }); - it('should select the entire wrapped line', () => { - buffer.lines.set(0, stringToRow('foo')); - const line2 = stringToRow('bar'); - line2.isWrapped = true; - buffer.lines.set(1, line2); - selectionManager.selectLineAt(0); - assert.equal(selectionManager.selectionText, 'foobar', 'The selected text is correct'); - assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]); - assert.deepEqual(selectionManager.model.finalSelectionEnd, [bufferService.cols, 1], 'The actual selection spans the entire column'); - }); - }); - - describe('selectAll', () => { - it('should select the entire buffer, beyond the viewport', () => { - bufferService.resize(20, 5); - buffer.lines.set(0, stringToRow('1')); - buffer.lines.set(1, stringToRow('2')); - buffer.lines.set(2, stringToRow('3')); - buffer.lines.set(3, stringToRow('4')); - buffer.lines.set(4, stringToRow('5')); - selectionManager.selectAll(); - assert.equal(selectionManager.selectionText, '1\n2\n3\n4\n5'); - }); - }); - - describe('selectLines', () => { - it('should select a single line', () => { - buffer.lines.length = 3; - buffer.lines.set(0, stringToRow('1')); - buffer.lines.set(1, stringToRow('2')); - buffer.lines.set(2, stringToRow('3')); - selectionManager.selectLines(1, 1); - assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 1]); - assert.deepEqual(selectionManager.model.finalSelectionEnd, [bufferService.cols, 1]); - }); - it('should select multiple lines', () => { - buffer.lines.length = 5; - buffer.lines.set(0, stringToRow('1')); - buffer.lines.set(1, stringToRow('2')); - buffer.lines.set(2, stringToRow('3')); - buffer.lines.set(3, stringToRow('4')); - buffer.lines.set(4, stringToRow('5')); - selectionManager.selectLines(1, 3); - assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 1]); - assert.deepEqual(selectionManager.model.finalSelectionEnd, [bufferService.cols, 3]); - }); - it('should select the to the start when requesting a negative row', () => { - buffer.lines.length = 2; - buffer.lines.set(0, stringToRow('1')); - buffer.lines.set(1, stringToRow('2')); - selectionManager.selectLines(-1, 0); - assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]); - assert.deepEqual(selectionManager.model.finalSelectionEnd, [bufferService.cols, 0]); - }); - it('should select the to the end when requesting beyond the final row', () => { - buffer.lines.length = 2; - buffer.lines.set(0, stringToRow('1')); - buffer.lines.set(1, stringToRow('2')); - selectionManager.selectLines(1, 2); - assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 1]); - assert.deepEqual(selectionManager.model.finalSelectionEnd, [bufferService.cols, 1]); - }); - }); - - describe('hasSelection', () => { - it('should return whether there is a selection', () => { - selectionManager.model.selectionStart = [0, 0]; - selectionManager.model.selectionStartLength = 0; - assert.equal(selectionManager.hasSelection, false); - selectionManager.model.selectionEnd = [0, 0]; - assert.equal(selectionManager.hasSelection, false); - selectionManager.model.selectionEnd = [1, 0]; - assert.equal(selectionManager.hasSelection, true); - selectionManager.model.selectionEnd = [0, 1]; - assert.equal(selectionManager.hasSelection, true); - selectionManager.model.selectionEnd = [1, 1]; - 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, stringArrayToRow(['😁', ' '])); - 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'); - }); - }); - - describe('_areCoordsInSelection', () => { - it('should return whether coords are in the selection', () => { - assert.isFalse(selectionManager.areCoordsInSelection([0, 0], [2, 0], [2, 1])); - assert.isFalse(selectionManager.areCoordsInSelection([1, 0], [2, 0], [2, 1])); - assert.isTrue(selectionManager.areCoordsInSelection([2, 0], [2, 0], [2, 1])); - assert.isTrue(selectionManager.areCoordsInSelection([10, 0], [2, 0], [2, 1])); - assert.isTrue(selectionManager.areCoordsInSelection([0, 1], [2, 0], [2, 1])); - assert.isTrue(selectionManager.areCoordsInSelection([1, 1], [2, 0], [2, 1])); - assert.isFalse(selectionManager.areCoordsInSelection([2, 1], [2, 0], [2, 1])); - }); - }); -}); - diff --git a/src/SelectionService.test.ts b/src/SelectionService.test.ts new file mode 100644 index 000000000..f59c54bfe --- /dev/null +++ b/src/SelectionService.test.ts @@ -0,0 +1,489 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; +import { SelectionService, SelectionMode } from './SelectionService'; +import { SelectionModel } from 'browser/selection/SelectionModel'; +import { IBufferLine } from 'common/Types'; +import { MockBufferService, MockOptionsService, MockCoreService } from 'common/TestUtils.test'; +import { BufferLine } from 'common/buffer/BufferLine'; +import { IBufferService, IOptionsService } from 'common/services/Services'; +import { MockCharSizeService, MockMouseService } from 'browser/TestUtils.test'; +import { CellData } from 'common/buffer/CellData'; +import { IBuffer } from 'common/buffer/Types'; + +class TestSelectionService extends SelectionService { + constructor( + bufferService: IBufferService, + optionsService: IOptionsService + ) { + super(() => {}, null, null, new MockCharSizeService(10, 10), bufferService, new MockCoreService(), new MockMouseService(), optionsService); + } + + public get model(): SelectionModel { return this._model; } + + public set selectionMode(mode: SelectionMode) { this._activeSelectionMode = mode; } + + public selectLineAt(line: number): void { this._selectLineAt(line); } + public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords, true); } + public areCoordsInSelection(coords: [number, number], start: [number, number], end: [number, number]): boolean { return this._areCoordsInSelection(coords, start, end); } + + // Disable DOM interaction + public enable(): void {} + public disable(): void {} + public refresh(): void {} +} + +describe('SelectionService', () => { + let buffer: IBuffer; + let bufferService: IBufferService; + let optionsService: IOptionsService; + let selectionService: TestSelectionService; + + beforeEach(() => { + optionsService = new MockOptionsService(); + bufferService = new MockBufferService(20, 20, optionsService); + buffer = bufferService.buffer; + selectionService = new TestSelectionService(bufferService, optionsService); + }); + + function stringToRow(text: string): IBufferLine { + const result = new BufferLine(text.length); + for (let i = 0; i < text.length; i++) { + result.setCell(i, CellData.fromCharData([0, text.charAt(i), 1, text.charCodeAt(i)])); + } + return result; + } + + function stringArrayToRow(chars: string[]): IBufferLine { + const line = new BufferLine(chars.length); + chars.map((c, idx) => line.setCell(idx, CellData.fromCharData([0, c, 1, c.charCodeAt(0)]))); + return line; + } + + describe('_selectWordAt', () => { + it('should expand selection for normal width chars', () => { + buffer.lines.set(0, stringToRow('foo bar')); + selectionService.selectWordAt([0, 0]); + assert.equal(selectionService.selectionText, 'foo'); + selectionService.selectWordAt([1, 0]); + assert.equal(selectionService.selectionText, 'foo'); + selectionService.selectWordAt([2, 0]); + assert.equal(selectionService.selectionText, 'foo'); + selectionService.selectWordAt([3, 0]); + assert.equal(selectionService.selectionText, ' '); + selectionService.selectWordAt([4, 0]); + assert.equal(selectionService.selectionText, 'bar'); + selectionService.selectWordAt([5, 0]); + assert.equal(selectionService.selectionText, 'bar'); + selectionService.selectWordAt([6, 0]); + assert.equal(selectionService.selectionText, 'bar'); + }); + it('should expand selection for whitespace', () => { + buffer.lines.set(0, stringToRow('a b')); + selectionService.selectWordAt([0, 0]); + assert.equal(selectionService.selectionText, 'a'); + selectionService.selectWordAt([1, 0]); + assert.equal(selectionService.selectionText, ' '); + selectionService.selectWordAt([2, 0]); + assert.equal(selectionService.selectionText, ' '); + selectionService.selectWordAt([3, 0]); + assert.equal(selectionService.selectionText, ' '); + selectionService.selectWordAt([4, 0]); + assert.equal(selectionService.selectionText, 'b'); + }); + it('should expand selection for wide characters', () => { + // Wide characters use a special format + const data: [number, string, number, number][] = [ + [null, '中', 2, '中'.charCodeAt(0)], + [null, '', 0, null], + [null, '文', 2, '文'.charCodeAt(0)], + [null, '', 0, null], + [null, ' ', 1, ' '.charCodeAt(0)], + [null, 'a', 1, 'a'.charCodeAt(0)], + [null, '中', 2, '中'.charCodeAt(0)], + [null, '', 0, null], + [null, '文', 2, '文'.charCodeAt(0)], + [null, '', 0, ''.charCodeAt(0)], + [null, 'b', 1, 'b'.charCodeAt(0)], + [null, ' ', 1, ' '.charCodeAt(0)], + [null, 'f', 1, 'f'.charCodeAt(0)], + [null, 'o', 1, 'o'.charCodeAt(0)], + [null, 'o', 1, 'o'.charCodeAt(0)] + ]; + const line = new BufferLine(data.length); + for (let i = 0; i < data.length; ++i) line.setCell(i, CellData.fromCharData(data[i])); + buffer.lines.set(0, line); + // Ensure wide characters take up 2 columns + selectionService.selectWordAt([0, 0]); + assert.equal(selectionService.selectionText, '中文'); + selectionService.selectWordAt([1, 0]); + assert.equal(selectionService.selectionText, '中文'); + selectionService.selectWordAt([2, 0]); + assert.equal(selectionService.selectionText, '中文'); + selectionService.selectWordAt([3, 0]); + assert.equal(selectionService.selectionText, '中文'); + selectionService.selectWordAt([4, 0]); + assert.equal(selectionService.selectionText, ' '); + // Ensure wide characters work when wrapped in normal width characters + selectionService.selectWordAt([5, 0]); + assert.equal(selectionService.selectionText, 'a中文b'); + selectionService.selectWordAt([6, 0]); + assert.equal(selectionService.selectionText, 'a中文b'); + selectionService.selectWordAt([7, 0]); + assert.equal(selectionService.selectionText, 'a中文b'); + selectionService.selectWordAt([8, 0]); + assert.equal(selectionService.selectionText, 'a中文b'); + selectionService.selectWordAt([9, 0]); + assert.equal(selectionService.selectionText, 'a中文b'); + selectionService.selectWordAt([10, 0]); + assert.equal(selectionService.selectionText, 'a中文b'); + selectionService.selectWordAt([11, 0]); + assert.equal(selectionService.selectionText, ' '); + // Ensure normal width characters work fine in a line containing wide characters + selectionService.selectWordAt([12, 0]); + assert.equal(selectionService.selectionText, 'foo'); + selectionService.selectWordAt([13, 0]); + assert.equal(selectionService.selectionText, 'foo'); + selectionService.selectWordAt([14, 0]); + assert.equal(selectionService.selectionText, 'foo'); + }); + it('should select up to non-path characters that are commonly adjacent to paths', () => { + buffer.lines.set(0, stringToRow('(cd)[ef]{gh}\'ij"')); + selectionService.selectWordAt([0, 0]); + assert.equal(selectionService.selectionText, '(cd'); + selectionService.selectWordAt([1, 0]); + assert.equal(selectionService.selectionText, 'cd'); + selectionService.selectWordAt([2, 0]); + assert.equal(selectionService.selectionText, 'cd'); + selectionService.selectWordAt([3, 0]); + assert.equal(selectionService.selectionText, 'cd)'); + selectionService.selectWordAt([4, 0]); + assert.equal(selectionService.selectionText, '[ef'); + selectionService.selectWordAt([5, 0]); + assert.equal(selectionService.selectionText, 'ef'); + selectionService.selectWordAt([6, 0]); + assert.equal(selectionService.selectionText, 'ef'); + selectionService.selectWordAt([7, 0]); + assert.equal(selectionService.selectionText, 'ef]'); + selectionService.selectWordAt([8, 0]); + assert.equal(selectionService.selectionText, '{gh'); + selectionService.selectWordAt([9, 0]); + assert.equal(selectionService.selectionText, 'gh'); + selectionService.selectWordAt([10, 0]); + assert.equal(selectionService.selectionText, 'gh'); + selectionService.selectWordAt([11, 0]); + assert.equal(selectionService.selectionText, 'gh}'); + selectionService.selectWordAt([12, 0]); + assert.equal(selectionService.selectionText, '\'ij'); + selectionService.selectWordAt([13, 0]); + assert.equal(selectionService.selectionText, 'ij'); + selectionService.selectWordAt([14, 0]); + assert.equal(selectionService.selectionText, 'ij'); + selectionService.selectWordAt([15, 0]); + assert.equal(selectionService.selectionText, 'ij"'); + }); + it('should expand upwards or downards for wrapped lines', () => { + buffer.lines.set(0, stringToRow(' foo')); + buffer.lines.set(1, stringToRow('bar ')); + buffer.lines.get(1).isWrapped = true; + selectionService.selectWordAt([1, 1]); + assert.equal(selectionService.selectionText, 'foobar'); + selectionService.model.clearSelection(); + selectionService.selectWordAt([18, 0]); + assert.equal(selectionService.selectionText, 'foobar'); + }); + it('should expand both upwards and downwards for word wrapped over many lines', () => { + const expectedText = 'fooaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbccccccccccccccccccccbar'; + buffer.lines.set(0, stringToRow(' foo')); + buffer.lines.set(1, stringToRow('aaaaaaaaaaaaaaaaaaaa')); + buffer.lines.set(2, stringToRow('bbbbbbbbbbbbbbbbbbbb')); + buffer.lines.set(3, stringToRow('cccccccccccccccccccc')); + buffer.lines.set(4, stringToRow('bar ')); + buffer.lines.get(1).isWrapped = true; + buffer.lines.get(2).isWrapped = true; + buffer.lines.get(3).isWrapped = true; + buffer.lines.get(4).isWrapped = true; + selectionService.selectWordAt([18, 0]); + assert.equal(selectionService.selectionText, expectedText); + selectionService.model.clearSelection(); + selectionService.selectWordAt([10, 1]); + assert.equal(selectionService.selectionText, expectedText); + selectionService.model.clearSelection(); + selectionService.selectWordAt([10, 2]); + assert.equal(selectionService.selectionText, expectedText); + selectionService.model.clearSelection(); + selectionService.selectWordAt([10, 3]); + assert.equal(selectionService.selectionText, expectedText); + selectionService.model.clearSelection(); + selectionService.selectWordAt([1, 4]); + assert.equal(selectionService.selectionText, expectedText); + }); + describe('emoji', () => { + it('should treat a single emoji as a word when wrapped in spaces', () => { + buffer.lines.set(0, stringToRow(' ⚽ a')); // The a is here to prevent the space being trimmed in selectionText + selectionService.selectWordAt([0, 0]); + assert.equal(selectionService.selectionText, ' '); + selectionService.selectWordAt([1, 0]); + assert.equal(selectionService.selectionText, '⚽'); + selectionService.selectWordAt([2, 0]); + assert.equal(selectionService.selectionText, ' '); + }); + it('should treat multiple emojis as a word when wrapped in spaces', () => { + buffer.lines.set(0, stringToRow(' ⚽⚽ a')); // The a is here to prevent the space being trimmed in selectionText + selectionService.selectWordAt([0, 0]); + assert.equal(selectionService.selectionText, ' '); + selectionService.selectWordAt([1, 0]); + assert.equal(selectionService.selectionText, '⚽⚽'); + selectionService.selectWordAt([2, 0]); + assert.equal(selectionService.selectionText, '⚽⚽'); + selectionService.selectWordAt([3, 0]); + assert.equal(selectionService.selectionText, ' '); + }); + it('should treat emojis using the zero-width-joiner as a single word', () => { + // Note that the first 3 emojis include the invisible ZWJ char + buffer.lines.set(0, stringArrayToRow([ + ' ', '👨‍', '👩‍', '👧‍', '👦', ' ', 'a' + ])); // The a is here to prevent the space being trimmed in selectionText + selectionService.selectWordAt([0, 0]); + assert.equal(selectionService.selectionText, ' '); + // ZWJ emojis do not combine in the terminal so the family emoji used here consumed 4 cells + // The selection text should retain ZWJ chars despite not combining on the terminal + selectionService.selectWordAt([1, 0]); + assert.equal(selectionService.selectionText, '👨‍👩‍👧‍👦'); + selectionService.selectWordAt([2, 0]); + assert.equal(selectionService.selectionText, '👨‍👩‍👧‍👦'); + selectionService.selectWordAt([3, 0]); + assert.equal(selectionService.selectionText, '👨‍👩‍👧‍👦'); + selectionService.selectWordAt([4, 0]); + assert.equal(selectionService.selectionText, '👨‍👩‍👧‍👦'); + selectionService.selectWordAt([5, 0]); + assert.equal(selectionService.selectionText, ' '); + }); + it('should treat emojis and characters joined together as a word', () => { + buffer.lines.set(0, stringToRow(' ⚽ab cd⚽ ef⚽gh')); // The a is here to prevent the space being trimmed in selectionText + selectionService.selectWordAt([0, 0]); + assert.equal(selectionService.selectionText, ' '); + selectionService.selectWordAt([1, 0]); + assert.equal(selectionService.selectionText, '⚽ab'); + selectionService.selectWordAt([2, 0]); + assert.equal(selectionService.selectionText, '⚽ab'); + selectionService.selectWordAt([3, 0]); + assert.equal(selectionService.selectionText, '⚽ab'); + selectionService.selectWordAt([4, 0]); + assert.equal(selectionService.selectionText, ' '); + selectionService.selectWordAt([5, 0]); + assert.equal(selectionService.selectionText, 'cd⚽'); + selectionService.selectWordAt([6, 0]); + assert.equal(selectionService.selectionText, 'cd⚽'); + selectionService.selectWordAt([7, 0]); + assert.equal(selectionService.selectionText, 'cd⚽'); + selectionService.selectWordAt([8, 0]); + assert.equal(selectionService.selectionText, ' '); + selectionService.selectWordAt([9, 0]); + assert.equal(selectionService.selectionText, 'ef⚽gh'); + selectionService.selectWordAt([10, 0]); + assert.equal(selectionService.selectionText, 'ef⚽gh'); + selectionService.selectWordAt([11, 0]); + assert.equal(selectionService.selectionText, 'ef⚽gh'); + selectionService.selectWordAt([12, 0]); + assert.equal(selectionService.selectionText, 'ef⚽gh'); + selectionService.selectWordAt([13, 0]); + assert.equal(selectionService.selectionText, 'ef⚽gh'); + }); + it('should treat complex emojis and characters joined together as a word', () => { + // This emoji is the flag for England and is made up of: 1F3F4 E0067 E0062 E0065 E006E E0067 E007F + buffer.lines.set(0, stringArrayToRow([ + ' ', '🏴󠁧󠁢󠁥󠁮󠁧󠁿', 'a', 'b', ' ', 'c', 'd', '🏴󠁧󠁢󠁥󠁮󠁧󠁿', ' ', 'e', 'f', '🏴󠁧󠁢󠁥󠁮󠁧󠁿', 'g', 'h', ' ', 'a' + ])); // The a is here to prevent the space being trimmed in selectionText + selectionService.selectWordAt([0, 0]); + assert.equal(selectionService.selectionText, ' '); + selectionService.selectWordAt([1, 0]); + assert.equal(selectionService.selectionText, '🏴󠁧󠁢󠁥󠁮󠁧󠁿ab'); + selectionService.selectWordAt([2, 0]); + assert.equal(selectionService.selectionText, '🏴󠁧󠁢󠁥󠁮󠁧󠁿ab'); + selectionService.selectWordAt([3, 0]); + assert.equal(selectionService.selectionText, '🏴󠁧󠁢󠁥󠁮󠁧󠁿ab'); + selectionService.selectWordAt([4, 0]); + assert.equal(selectionService.selectionText, ' '); + selectionService.selectWordAt([5, 0]); + assert.equal(selectionService.selectionText, 'cd🏴󠁧󠁢󠁥󠁮󠁧󠁿'); + selectionService.selectWordAt([6, 0]); + assert.equal(selectionService.selectionText, 'cd🏴󠁧󠁢󠁥󠁮󠁧󠁿'); + selectionService.selectWordAt([7, 0]); + assert.equal(selectionService.selectionText, 'cd🏴󠁧󠁢󠁥󠁮󠁧󠁿'); + selectionService.selectWordAt([8, 0]); + assert.equal(selectionService.selectionText, ' '); + selectionService.selectWordAt([9, 0]); + assert.equal(selectionService.selectionText, 'ef🏴󠁧󠁢󠁥󠁮󠁧󠁿gh'); + selectionService.selectWordAt([10, 0]); + assert.equal(selectionService.selectionText, 'ef🏴󠁧󠁢󠁥󠁮󠁧󠁿gh'); + selectionService.selectWordAt([11, 0]); + assert.equal(selectionService.selectionText, 'ef🏴󠁧󠁢󠁥󠁮󠁧󠁿gh'); + selectionService.selectWordAt([12, 0]); + assert.equal(selectionService.selectionText, 'ef🏴󠁧󠁢󠁥󠁮󠁧󠁿gh'); + selectionService.selectWordAt([13, 0]); + assert.equal(selectionService.selectionText, 'ef🏴󠁧󠁢󠁥󠁮󠁧󠁿gh'); + }); + }); + }); + + describe('_selectLineAt', () => { + it('should select the entire line', () => { + buffer.lines.set(0, stringToRow('foo bar')); + selectionService.selectLineAt(0); + assert.equal(selectionService.selectionText, 'foo bar', 'The selected text is correct'); + assert.deepEqual(selectionService.model.finalSelectionStart, [0, 0]); + assert.deepEqual(selectionService.model.finalSelectionEnd, [bufferService.cols, 0], 'The actual selection spans the entire column'); + }); + it('should select the entire wrapped line', () => { + buffer.lines.set(0, stringToRow('foo')); + const line2 = stringToRow('bar'); + line2.isWrapped = true; + buffer.lines.set(1, line2); + selectionService.selectLineAt(0); + assert.equal(selectionService.selectionText, 'foobar', 'The selected text is correct'); + assert.deepEqual(selectionService.model.finalSelectionStart, [0, 0]); + assert.deepEqual(selectionService.model.finalSelectionEnd, [bufferService.cols, 1], 'The actual selection spans the entire column'); + }); + }); + + describe('selectAll', () => { + it('should select the entire buffer, beyond the viewport', () => { + bufferService.resize(20, 5); + buffer.lines.set(0, stringToRow('1')); + buffer.lines.set(1, stringToRow('2')); + buffer.lines.set(2, stringToRow('3')); + buffer.lines.set(3, stringToRow('4')); + buffer.lines.set(4, stringToRow('5')); + selectionService.selectAll(); + assert.equal(selectionService.selectionText, '1\n2\n3\n4\n5'); + }); + }); + + describe('selectLines', () => { + it('should select a single line', () => { + buffer.lines.length = 3; + buffer.lines.set(0, stringToRow('1')); + buffer.lines.set(1, stringToRow('2')); + buffer.lines.set(2, stringToRow('3')); + selectionService.selectLines(1, 1); + assert.deepEqual(selectionService.model.finalSelectionStart, [0, 1]); + assert.deepEqual(selectionService.model.finalSelectionEnd, [bufferService.cols, 1]); + }); + it('should select multiple lines', () => { + buffer.lines.length = 5; + buffer.lines.set(0, stringToRow('1')); + buffer.lines.set(1, stringToRow('2')); + buffer.lines.set(2, stringToRow('3')); + buffer.lines.set(3, stringToRow('4')); + buffer.lines.set(4, stringToRow('5')); + selectionService.selectLines(1, 3); + assert.deepEqual(selectionService.model.finalSelectionStart, [0, 1]); + assert.deepEqual(selectionService.model.finalSelectionEnd, [bufferService.cols, 3]); + }); + it('should select the to the start when requesting a negative row', () => { + buffer.lines.length = 2; + buffer.lines.set(0, stringToRow('1')); + buffer.lines.set(1, stringToRow('2')); + selectionService.selectLines(-1, 0); + assert.deepEqual(selectionService.model.finalSelectionStart, [0, 0]); + assert.deepEqual(selectionService.model.finalSelectionEnd, [bufferService.cols, 0]); + }); + it('should select the to the end when requesting beyond the final row', () => { + buffer.lines.length = 2; + buffer.lines.set(0, stringToRow('1')); + buffer.lines.set(1, stringToRow('2')); + selectionService.selectLines(1, 2); + assert.deepEqual(selectionService.model.finalSelectionStart, [0, 1]); + assert.deepEqual(selectionService.model.finalSelectionEnd, [bufferService.cols, 1]); + }); + }); + + describe('hasSelection', () => { + it('should return whether there is a selection', () => { + selectionService.model.selectionStart = [0, 0]; + selectionService.model.selectionStartLength = 0; + assert.equal(selectionService.hasSelection, false); + selectionService.model.selectionEnd = [0, 0]; + assert.equal(selectionService.hasSelection, false); + selectionService.model.selectionEnd = [1, 0]; + assert.equal(selectionService.hasSelection, true); + selectionService.model.selectionEnd = [0, 1]; + assert.equal(selectionService.hasSelection, true); + selectionService.model.selectionEnd = [1, 1]; + assert.equal(selectionService.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')); + + selectionService.selectionMode = SelectionMode.COLUMN; + selectionService.model.selectionStart = [2, 0]; + selectionService.model.selectionEnd = [4, 2]; + + assert.equal(selectionService.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')); + + selectionService.selectionMode = SelectionMode.COLUMN; + selectionService.model.selectionStart = [0, 0]; + selectionService.model.selectionEnd = [1, 2]; + + assert.equal(selectionService.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')); + + selectionService.selectionMode = SelectionMode.COLUMN; + selectionService.model.selectionStart = [0, 0]; + selectionService.model.selectionEnd = [1, 2]; + + assert.equal(selectionService.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, stringArrayToRow(['😁', ' '])); + buffer.lines.set(2, stringToRow('c ')); + + selectionService.selectionMode = SelectionMode.COLUMN; + selectionService.model.selectionStart = [0, 0]; + selectionService.model.selectionEnd = [1, 2]; + + assert.equal(selectionService.selectionText, 'a\n😁\nc'); + }); + }); + + describe('_areCoordsInSelection', () => { + it('should return whether coords are in the selection', () => { + assert.isFalse(selectionService.areCoordsInSelection([0, 0], [2, 0], [2, 1])); + assert.isFalse(selectionService.areCoordsInSelection([1, 0], [2, 0], [2, 1])); + assert.isTrue(selectionService.areCoordsInSelection([2, 0], [2, 0], [2, 1])); + assert.isTrue(selectionService.areCoordsInSelection([10, 0], [2, 0], [2, 1])); + assert.isTrue(selectionService.areCoordsInSelection([0, 1], [2, 0], [2, 1])); + assert.isTrue(selectionService.areCoordsInSelection([1, 1], [2, 0], [2, 1])); + assert.isFalse(selectionService.areCoordsInSelection([2, 1], [2, 0], [2, 1])); + }); + }); +}); + diff --git a/src/SelectionManager.ts b/src/SelectionService.ts similarity index 98% rename from src/SelectionManager.ts rename to src/SelectionService.ts index a7f172a6e..46b1556c1 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionService.ts @@ -3,14 +3,14 @@ * @license MIT */ -import { ISelectionManager, ISelectionRedrawRequestEvent } from 'browser/selection/Types'; +import { ISelectionRedrawRequestEvent } from 'browser/selection/Types'; import { IBuffer } from 'common/buffer/Types'; import { IBufferLine, IDisposable } from 'common/Types'; import * as Browser from 'common/Platform'; import { SelectionModel } from 'browser/selection/SelectionModel'; import { CellData } from 'common/buffer/CellData'; import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { ICharSizeService, IMouseService } from 'browser/services/Services'; +import { ICharSizeService, IMouseService, ISelectionService } from 'browser/services/Services'; import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services'; import { getCoordsRelativeToElement } from 'browser/input/Mouse'; import { moveToCellSequence } from 'browser/input/MoveToCell'; @@ -60,13 +60,13 @@ export const enum SelectionMode { /** * A class that manages the selection of the terminal. With help from - * SelectionModel, SelectionManager handles with all logic associated with + * SelectionModel, SelectionService handles with all logic associated with * dealing with the selection, including handling mouse interaction, wide * characters and fetching the actual text within the selection. Rendering is - * not handled by the SelectionManager but the onRedrawRequest event is fired + * not handled by the SelectionService but the onRedrawRequest event is fired * when the selection is ready to be redrawn (on an animation frame). */ -export class SelectionManager implements ISelectionManager { +export class SelectionService implements ISelectionService { protected _model: SelectionModel; /** diff --git a/src/Terminal.ts b/src/Terminal.ts index ac1446348..8e238fe85 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -30,7 +30,7 @@ import { C0 } from 'common/data/EscapeSequences'; import { InputHandler } from './InputHandler'; import { Renderer } from './renderer/Renderer'; import { Linkifier } from './Linkifier'; -import { SelectionManager } from './SelectionManager'; +import { SelectionService } from './SelectionService'; import * as Browser from 'common/Platform'; import { addDisposableDomListener } from 'browser/Lifecycle'; import * as Strings from './browser/LocalizableStrings'; @@ -174,7 +174,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp private _inputHandler: InputHandler; public soundManager: SoundManager; - public selectionManager: SelectionManager; + public selectionService: SelectionService; public linkifier: ILinkifier; public viewport: IViewport; private _compositionHelper: ICompositionHelper; @@ -301,14 +301,14 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._inputHandler.onLineFeed(() => this._onLineFeed.fire()); this.register(this._inputHandler); - this.selectionManager = this.selectionManager || null; + this.selectionService = this.selectionService || null; this.linkifier = this.linkifier || new Linkifier(this); this._mouseZoneManager = this._mouseZoneManager || null; this.soundManager = this.soundManager || new SoundManager(this); - if (this.selectionManager) { - this.selectionManager.clearSelection(); - this.selectionManager.initBuffersListeners(); + if (this.selectionService) { + this.selectionService.clearSelection(); + this.selectionService.initBuffersListeners(); } if (this.options.windowsMode) { @@ -479,7 +479,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp if (!this.hasSelection()) { return; } - copyHandler(event, this.selectionManager); + copyHandler(event, this.selectionService); })); const pasteHandlerWrapper = (event: ClipboardEvent) => pasteHandler(event, this.textarea, this.bracketedPasteMode, e => this._coreService.triggerDataEvent(e, true)); this.register(addDisposableDomListener(this.textarea, 'paste', pasteHandlerWrapper)); @@ -490,12 +490,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.textarea, this.screenElement, this.selectionManager, this.options.rightClickSelectsWord); + rightClickHandler(event, this.textarea, this.screenElement, this.selectionService, this.options.rightClickSelectsWord); } })); } else { this.register(addDisposableDomListener(this.element, 'contextmenu', (event: MouseEvent) => { - rightClickHandler(event, this.textarea, this.screenElement, this.selectionManager, this.options.rightClickSelectsWord); + rightClickHandler(event, this.textarea, this.screenElement, this.selectionService, this.options.rightClickSelectsWord); })); } @@ -641,15 +641,15 @@ 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.selectionService = new SelectionService( (amount: number, suppressEvent: boolean) => this.scrollLines(amount, suppressEvent), this.element, this.screenElement, this._charSizeService, this._bufferService, this._coreService, 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))); - this.register(this.selectionManager.onLinuxMouseSelection(text => { + this.register(this.selectionService.onSelectionChange(() => this._onSelectionChange.fire())); + this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this.selectionService.onMouseDown(e))); + this.register(this.selectionService.onRedrawRequest(e => this._renderService.onSelectionChanged(e.start, e.end, e.columnSelectMode))); + this.register(this.selectionService.onLinuxMouseSelection(text => { // 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 // only on Linux to enable middle click to paste selection. @@ -659,16 +659,16 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp })); this.register(this.onScroll(() => { this.viewport.syncScrollArea(); - this.selectionManager.refresh(); + this.selectionService.refresh(); })); - this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this.selectionManager.refresh())); + this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this.selectionService.refresh())); // apply mouse event classes set by escape codes before terminal was attached this.element.classList.toggle('enable-mouse-events', this.mouseEvents); if (this.mouseEvents) { - this.selectionManager.disable(); + this.selectionService.disable(); } else { - this.selectionManager.enable(); + this.selectionService.enable(); } if (this.options.screenReaderMode) { @@ -946,7 +946,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // Don't send the mouse button to the pty if mouse events are disabled or // if the selection manager is having selection forced (ie. a modifier is // held). - if (!this.mouseEvents || this.selectionManager.shouldForceSelection(ev)) { + if (!this.mouseEvents || this.selectionService.shouldForceSelection(ev)) { return; } @@ -1077,7 +1077,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp * Change the cursor style for different selection modes */ public updateCursorStyle(ev: KeyboardEvent): void { - if (this.selectionManager && this.selectionManager.shouldColumnSelect(ev)) { + if (this.selectionService && this.selectionService.shouldColumnSelect(ev)) { this.element.classList.add('column-select'); } else { this.element.classList.remove('column-select'); @@ -1481,7 +1481,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp * Gets whether the terminal has an active selection. */ public hasSelection(): boolean { - return this.selectionManager ? this.selectionManager.hasSelection : false; + return this.selectionService ? this.selectionService.hasSelection : false; } /** @@ -1491,7 +1491,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp * @param length The length of the selection. */ public select(column: number, row: number, length: number): void { - this.selectionManager.setSelection(column, row, length); + this.selectionService.setSelection(column, row, length); } /** @@ -1499,19 +1499,19 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp * behavior outside of xterm.js. */ public getSelection(): string { - return this.selectionManager ? this.selectionManager.selectionText : ''; + return this.selectionService ? this.selectionService.selectionText : ''; } public getSelectionPosition(): ISelectionPosition | undefined { - if (!this.selectionManager.hasSelection) { + if (!this.selectionService.hasSelection) { return undefined; } return { - startColumn: this.selectionManager.selectionStart[0], - startRow: this.selectionManager.selectionStart[1], - endColumn: this.selectionManager.selectionEnd[0], - endRow: this.selectionManager.selectionEnd[1] + startColumn: this.selectionService.selectionStart[0], + startRow: this.selectionService.selectionStart[1], + endColumn: this.selectionService.selectionEnd[0], + endRow: this.selectionService.selectionEnd[1] }; } @@ -1519,8 +1519,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp * Clears the current terminal selection. */ public clearSelection(): void { - if (this.selectionManager) { - this.selectionManager.clearSelection(); + if (this.selectionService) { + this.selectionService.clearSelection(); } } @@ -1528,14 +1528,14 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp * Selects all text within the terminal. */ public selectAll(): void { - if (this.selectionManager) { - this.selectionManager.selectAll(); + if (this.selectionService) { + this.selectionService.selectAll(); } } public selectLines(start: number, end: number): void { - if (this.selectionManager) { - this.selectionManager.selectLines(start, end); + if (this.selectionService) { + this.selectionService.selectLines(start, end); } } @@ -1808,8 +1808,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // } // // Clear the selection if the selection manager is available and has an active selection - // if (this.selectionManager && this.selectionManager.hasSelection) { - // this.selectionManager.clearSelection(); + // if (this.selectionService && this.selectionService.hasSelection) { + // this.selectionService.clearSelection(); // } // // Input is being sent to the terminal, the terminal should focus the prompt. diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index 8394f5a78..b203d1a14 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -15,7 +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'; +import { ISelectionService } from 'browser/services/Services'; export class TestTerminal extends Terminal { writeSync(data: string): void { @@ -131,7 +131,7 @@ export class MockTerminal implements ITerminal { screenElement: HTMLElement; rowContainer: HTMLElement; selectionContainer: HTMLElement; - selectionManager: ISelectionManager; + selectionService: ISelectionService; textarea: HTMLTextAreaElement; rows: number; cols: number; @@ -216,7 +216,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { buffers: IBufferSet; buffer: IBuffer = new MockBuffer(); viewport: IViewport; - selectionManager: ISelectionManager; + selectionService: ISelectionService; focus(): void { throw new Error('Method not implemented.'); } diff --git a/src/Types.d.ts b/src/Types.d.ts index cd4ad2d0a..facfa98ae 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -9,7 +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'; +import { ISelectionService } from 'browser/services/Services'; export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; @@ -52,7 +52,7 @@ export interface IInputHandlingTerminal { buffers: IBufferSet; buffer: IBuffer; viewport: IViewport; - selectionManager: ISelectionManager; + selectionService: ISelectionService; onA11yCharEmitter: IEventEmitter; onA11yTabEmitter: IEventEmitter; @@ -195,7 +195,7 @@ export interface ILinkifierEvent { export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor { screenElement: HTMLElement; - selectionManager: ISelectionManager; + selectionService: ISelectionService; browser: IBrowser; writeBuffer: string[]; cursorHidden: boolean; diff --git a/src/browser/selection/Types.d.ts b/src/browser/selection/Types.d.ts index 241731f1f..2551be433 100644 --- a/src/browser/selection/Types.d.ts +++ b/src/browser/selection/Types.d.ts @@ -3,18 +3,6 @@ * @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]; diff --git a/src/browser/services/Services.d.ts b/src/browser/services/Services.d.ts index 64539b95b..573f0b638 100644 --- a/src/browser/services/Services.d.ts +++ b/src/browser/services/Services.d.ts @@ -46,3 +46,15 @@ export interface IRenderService { registerCharacterJoiner(handler: CharacterJoinerHandler): number; deregisterCharacterJoiner(joinerId: number): boolean; } + +export interface ISelectionService { + 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; +}