diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts index bd0b78f9a6..5e80ae1700 100644 --- a/src/browser/public/Terminal.ts +++ b/src/browser/public/Terminal.ts @@ -174,7 +174,7 @@ export class Terminal implements ITerminalApi { public paste(data: string): void { this._core.paste(data); } - public getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'logLevel' | 'rendererType' | 'termName' | 'wordSeparator'): string; + public getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'logLevel' | 'selectionStyle' | 'rendererType' | 'termName' | 'wordSeparator'): string; public getOption(key: 'allowTransparency' | 'altClickMovesCursor' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'visualBell'): boolean; public getOption(key: 'cols' | 'fontSize' | 'letterSpacing' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback'): number; public getOption(key: 'fontWeight' | 'fontWeightBold'): FontWeight; @@ -191,6 +191,7 @@ export class Terminal implements ITerminalApi { public setOption(key: 'fontSize' | 'letterSpacing' | 'lineHeight' | 'tabStopWidth' | 'scrollback', value: number): void; public setOption(key: 'theme', value: ITheme): void; public setOption(key: 'cols' | 'rows', value: number): void; + public setOption(key: 'selectionStyle', value: 'plain' | 'mark-start' | 'mark-end'): void; public setOption(key: string, value: any): void; public setOption(key: any, value: any): void { this._core.optionsService.setOption(key, value); diff --git a/src/browser/renderer/SelectionRenderLayer.ts b/src/browser/renderer/SelectionRenderLayer.ts index 80022f0112..8b79b17efb 100644 --- a/src/browser/renderer/SelectionRenderLayer.ts +++ b/src/browser/renderer/SelectionRenderLayer.ts @@ -13,6 +13,7 @@ interface ISelectionState { end?: [number, number]; columnSelectMode?: boolean; ydisp?: number; + selectionStyle?: string; } export class SelectionRenderLayer extends BaseRenderLayer { @@ -35,7 +36,8 @@ export class SelectionRenderLayer extends BaseRenderLayer { start: undefined, end: undefined, columnSelectMode: undefined, - ydisp: undefined + ydisp: undefined, + selectionStyle: undefined }; } @@ -53,8 +55,10 @@ export class SelectionRenderLayer extends BaseRenderLayer { } public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void { + const selectionStyle: string = this._optionsService.getOption('selectionStyle') || 'plain'; + // Selection has not changed - if (!this._didStateChange(start, end, columnSelectMode, this._bufferService.buffer.ydisp)) { + if (!this._didStateChange(start, end, columnSelectMode, this._bufferService.buffer.ydisp, selectionStyle)) { return; } @@ -80,6 +84,7 @@ export class SelectionRenderLayer extends BaseRenderLayer { } this._ctx.fillStyle = this._colors.selectionTransparent.css; + this._ctx.strokeStyle = this._colors.cursor.css; if (columnSelectMode) { const startCol = start[0]; @@ -88,9 +93,23 @@ export class SelectionRenderLayer extends BaseRenderLayer { this._fillCells(startCol, viewportCappedStartRow, width, height); } else { // Draw first row - const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0; - const startRowEndCol = viewportCappedStartRow === viewportEndRow ? end[0] : this._bufferService.cols; - this._fillCells(startCol, viewportCappedStartRow, startRowEndCol - startCol, 1); + let startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0; + let startRowEndCol = viewportCappedStartRow === viewportEndRow ? end[0] : this._bufferService.cols; + switch (selectionStyle) { + case 'mark-start': + this._strokeRectAtCell(startCol, viewportCappedStartRow, 1, 1); + startCol += 1; + break; + case 'mark-end': + if (viewportCappedStartRow === viewportCappedEndRow) { + this._strokeRectAtCell(startRowEndCol - 1, viewportCappedStartRow, 1, 1); + startRowEndCol -= 1; + } + break; + } + if (startRowEndCol - startCol > 0) { + this._fillCells(startCol, viewportCappedStartRow, startRowEndCol - startCol, 1); + } // Draw middle rows const middleRowsCount = Math.max(viewportCappedEndRow - viewportCappedStartRow - 1, 0); @@ -99,8 +118,14 @@ export class SelectionRenderLayer extends BaseRenderLayer { // Draw final row if (viewportCappedStartRow !== viewportCappedEndRow) { // Only draw viewportEndRow if it's not the same as viewportStartRow - const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._bufferService.cols; - this._fillCells(0, viewportCappedEndRow, endCol, 1); + let endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._bufferService.cols; + if (selectionStyle === 'mark-end') { + this._strokeRectAtCell(endCol - 1, viewportCappedEndRow, 1, 1); + endCol -= 1; + } + if (endCol > 0) { + this._fillCells(0, viewportCappedEndRow, endCol, 1); + } } } @@ -109,13 +134,15 @@ export class SelectionRenderLayer extends BaseRenderLayer { this._state.end = [end[0], end[1]]; this._state.columnSelectMode = columnSelectMode; this._state.ydisp = this._bufferService.buffer.ydisp; + this._state.selectionStyle = selectionStyle; } - private _didStateChange(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean, ydisp: number): boolean { + private _didStateChange(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean, ydisp: number, selectionStyle: string): boolean { return !this._areCoordinatesEqual(start, this._state.start) || !this._areCoordinatesEqual(end, this._state.end) || columnSelectMode !== this._state.columnSelectMode || - ydisp !== this._state.ydisp; + ydisp !== this._state.ydisp || + selectionStyle !== this._state.selectionStyle; } private _areCoordinatesEqual(coord1: [number, number] | undefined, coord2: [number, number] | undefined): boolean { diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index b7a1c58e0a..75d1659844 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -53,7 +53,8 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({ altClickMovesCursor: true, convertEol: false, termName: 'xterm', - cancelEvents: false + cancelEvents: false, + selectionStyle: 'plain' }); const FONT_WEIGHT_OPTIONS: Extract[] = ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900']; diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 1fbf57fbe6..3e3f458a3f 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -258,6 +258,7 @@ export interface ITerminalOptions { screenReaderMode: boolean; scrollback: number; scrollSensitivity: number; + selectionStyle: 'plain' | 'mark-start' | 'mark-end'; tabStopWidth: number; theme: ITheme; windowsMode: boolean; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index cab9bdcc8a..f580070391 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -225,6 +225,12 @@ declare module 'xterm' { */ scrollSensitivity?: number; + /** + * Styling for the text selection shade - plain shading, + * or add a marker at the start/end of the shade + */ + selectionStyle?: 'plain' | 'mark-start' | 'mark-end'; + /** * The size of tab stops in the terminal. */ @@ -946,7 +952,7 @@ declare module 'xterm' { * Retrieves an option's value from the terminal. * @param key The option key. */ - getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'logLevel' | 'rendererType' | 'termName' | 'wordSeparator'): string; + getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'logLevel' | 'selectionStyle' | 'rendererType' | 'termName' | 'wordSeparator'): string; /** * Retrieves an option's value from the terminal. * @param key The option key. @@ -1022,6 +1028,12 @@ declare module 'xterm' { * @param value The option value. */ setOption(key: 'cols' | 'rows', value: number): void; + /** + * Sets an option on the terminal. + * @param key The option key. + * @param value The option value. + */ + setOption(key: 'selectionStyle', value: 'plain' | 'mark-start' | 'mark-end'): void; /** * Sets an option on the terminal. * @param key The option key.