diff --git a/fixtures/typings-test/typings-test.ts b/fixtures/typings-test/typings-test.ts index 7bbaf214..13da6961 100644 --- a/fixtures/typings-test/typings-test.ts +++ b/fixtures/typings-test/typings-test.ts @@ -148,6 +148,7 @@ namespace methods_core { const r24: string = t.getOption('fontWeight'); const r25: string = t.getOption('fontWeightBold'); const r26: boolean = t.getOption('allowTransparency'); + const r27: boolean = t.getOption('rightClickSelectsWord'); } { const t: Terminal = new Terminal(); @@ -187,6 +188,7 @@ namespace methods_core { t.setOption('fontFamily', 'foo'); t.setOption('theme', {background: '#ff0000'}); t.setOption('macOptionIsMeta', true); + t.setOption('rightClickSelectsWord', false); } } namespace scrolling { diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index 5c53565f..a76947e5 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -28,7 +28,7 @@ class TestSelectionManager extends SelectionManager { public get model(): SelectionModel { return this._model; } public selectLineAt(line: number): void { this._selectLineAt(line); } - public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords); } + public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords, true); } // Disable DOM interaction public enable(): void {} diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 3c927839..11856a92 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -253,6 +253,37 @@ export class SelectionManager extends EventEmitter implements ISelectionManager this.emit('refresh', { start: this._model.finalSelectionStart, end: this._model.finalSelectionEnd }); } + /** + * Checks if the current click was inside the current selection + * @param event The mouse event + */ + public isClickInSelection(event: MouseEvent): boolean { + const coords = this._getMouseBufferCoords(event); + const start = this._model.finalSelectionStart; + const end = this._model.finalSelectionEnd; + + if (!start || !end) { + return false; + } + + return (coords[1] > start[1] && coords[1] < end[1]) || + (start[1] === end[1] && coords[1] === start[1] && coords[0] > start[0] && coords[0] < end[0]) || + (start[1] < end[1] && coords[1] === end[1] && coords[0] < end[0]); + } + + /** + * Selects word at the current mouse event coordinates. + * @param event The mouse event. + */ + public selectWordAtCursor(event: MouseEvent): void { + const coords = this._getMouseBufferCoords(event); + if (coords) { + this._selectWordAt(coords, false); + this._model.selectionEnd = null; + this.refresh(true); + } + } + /** * Selects all text within the terminal. */ @@ -445,7 +476,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager const coords = this._getMouseBufferCoords(event); if (coords) { this._activeSelectionMode = SelectionMode.WORD; - this._selectWordAt(coords); + this._selectWordAt(coords, true); } } @@ -602,7 +633,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * Gets positional information for the word at the coordinated specified. * @param coords The coordinates to get the word at. */ - private _getWordAt(coords: [number, number]): IWordPosition { + private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): IWordPosition { const bufferLine = this._buffer.lines.get(coords[1]); if (!bufferLine) { return null; @@ -708,15 +739,20 @@ export class SelectionManager extends EventEmitter implements ISelectionManager - leftLongCharOffset // The number of additional chars left of the initial char added by columns with strings longer than 1 (emojis) - rightLongCharOffset); // The number of additional chars right of the initial char (inclusive) added by columns with strings longer than 1 (emojis) + if (!allowWhitespaceOnlySelection && line.slice(startIndex, endIndex).trim() === '') { + return null; + } + return { start, length }; } /** * Selects the word at the coordinates specified. * @param coords The coordinates to get the word at. + * @param allowWhitespaceOnlySelection If whitespace should be selected */ - protected _selectWordAt(coords: [number, number]): void { - const wordPosition = this._getWordAt(coords); + protected _selectWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): void { + const wordPosition = this._getWordAt(coords, allowWhitespaceOnlySelection); if (wordPosition) { this._model.selectionStart = [wordPosition.start, coords[1]]; this._model.selectionStartLength = wordPosition.length; @@ -728,7 +764,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * @param coords The coordinates to get the word at. */ private _selectToWordAt(coords: [number, number]): void { - const wordPosition = this._getWordAt(coords); + const wordPosition = this._getWordAt(coords, true); if (wordPosition) { this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : (wordPosition.start + wordPosition.length), coords[1]]; } diff --git a/src/Terminal.ts b/src/Terminal.ts index 6e501d03..dff64045 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -87,7 +87,8 @@ const DEFAULT_OPTIONS: ITerminalOptions = { useFlowControl: false, allowTransparency: false, tabStopWidth: 8, - theme: null + theme: null, + rightClickSelectsWord: Browser.isMac // programFeatures: false, // focusKeys: false, }; @@ -506,12 +507,12 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // Firefox doesn't appear to fire the contextmenu event on right click on(this.element, 'mousedown', (event: MouseEvent) => { if (event.button === 2) { - rightClickHandler(event, this.textarea, this.selectionManager); + rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord); } }); } else { on(this.element, 'contextmenu', (event: MouseEvent) => { - rightClickHandler(event, this.textarea, this.selectionManager); + rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord); }); } diff --git a/src/Types.ts b/src/Types.ts index da3cc9d2..659a8084 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -285,6 +285,8 @@ export interface ISelectionManager { disable(): void; enable(): void; setSelection(row: number, col: number, length: number): void; + isClickInSelection(event: MouseEvent): boolean; + selectWordAtCursor(event: MouseEvent): void; } export interface ILinkifier extends IEventEmitter { diff --git a/src/handlers/Clipboard.ts b/src/handlers/Clipboard.ts index 035b5fff..7ac97714 100644 --- a/src/handlers/Clipboard.ts +++ b/src/handlers/Clipboard.ts @@ -116,10 +116,15 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA * @param ev The original right click event to be handled. * @param textarea The terminal's textarea. * @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, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager): void { +export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager, shouldSelectWord: boolean): void { moveTextAreaUnderMouseCursor(ev, textarea); + if (shouldSelectWord && !selectionManager.isClickInSelection(ev)) { + selectionManager.selectWordAtCursor(ev); + } + // Get textarea ready to copy from the context menu textarea.value = selectionManager.selectionText; textarea.select(); diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 0b35edf1..1d14243e 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -93,6 +93,12 @@ declare module 'xterm' { */ macOptionIsMeta?: boolean; + /** + * Whether to select the word under the cursor on right click, this is + * standard behavior in a lot of macOS applications. + */ + rightClickSelectsWord?: boolean; + /** * The number of rows in the terminal. */ @@ -427,7 +433,7 @@ declare module 'xterm' { * Retrieves an option's value from the terminal. * @param key The option key. */ - getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell'): boolean; + getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell'): boolean; /** * Retrieves an option's value from the terminal. * @param key The option key. @@ -478,7 +484,7 @@ declare module 'xterm' { * @param key The option key. * @param value The option value. */ - setOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell', value: boolean): void; + setOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'popOnBell' | 'rightClickSelectsWord' | 'screenKeys' | 'useFlowControl' | 'visualBell', value: boolean): void; /** * Sets an option on the terminal. * @param key The option key.