diff --git a/addons/xterm-addon-fit/src/FitAddon.ts b/addons/xterm-addon-fit/src/FitAddon.ts index 360397ec..7b9c228f 100644 --- a/addons/xterm-addon-fit/src/FitAddon.ts +++ b/addons/xterm-addon-fit/src/FitAddon.ts @@ -63,6 +63,9 @@ export class FitAddon implements ITerminalAddon { return undefined; } + const scrollbarWidth = this._terminal.options.scrollback === 0 ? + 0 : core.viewport.scrollBarWidth; + const parentElementStyle = window.getComputedStyle(this._terminal.element.parentElement); const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')); const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width'))); @@ -76,7 +79,7 @@ export class FitAddon implements ITerminalAddon { const elementPaddingVer = elementPadding.top + elementPadding.bottom; const elementPaddingHor = elementPadding.right + elementPadding.left; const availableHeight = parentElementHeight - elementPaddingVer; - const availableWidth = parentElementWidth - elementPaddingHor - core.viewport.scrollBarWidth; + const availableWidth = parentElementWidth - elementPaddingHor - scrollbarWidth; const geometry = { cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / core._renderService.dimensions.actualCellWidth)), rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / core._renderService.dimensions.actualCellHeight)) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index e7ece483..0f356e14 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -77,7 +77,7 @@ export class SearchAddon implements ITerminalAddon { public activate(terminal: Terminal): void { this._terminal = terminal; - this._onDataDisposable = this._terminal.onData(() => this._updateMatches()); + this._onDataDisposable = this._terminal.onWriteParsed(() => this._updateMatches()); this._onResizeDisposable = this._terminal.onResize(() => this._updateMatches()); } @@ -88,6 +88,7 @@ export class SearchAddon implements ITerminalAddon { if (this._cachedSearchTerm && this._lastSearchOptions?.decorations) { this._highlightTimeout = setTimeout(() => { this.findPrevious(this._cachedSearchTerm!, { ...this._lastSearchOptions, incremental: true, noScroll: true }); + this._resultIndex = this._searchResults ? this._searchResults.size -1 : -1; this._onDidChangeResults.fire({ resultIndex: this._searchResults ? this._searchResults.size - 1 : -1, resultCount: this._searchResults ? this._searchResults.size : -1 }); }, 200); } @@ -679,7 +680,7 @@ export class SearchAddon implements ITerminalAddon { color: options.activeMatchColorOverviewRuler } }); - this._selectedDecoration?.onRender((e) => this._applyStyles(e, options.activeMatchBorder)); + this._selectedDecoration?.onRender((e) => this._applyStyles(e, options.activeMatchBorder, true)); this._selectedDecoration?.onDispose(() => marker.dispose()); } } @@ -702,7 +703,7 @@ export class SearchAddon implements ITerminalAddon { * @param borderColor the border color to apply * @returns */ - private _applyStyles(element: HTMLElement, borderColor: string | undefined): void { + private _applyStyles(element: HTMLElement, borderColor: string | undefined, isActiveResult: boolean): void { if (element.clientWidth <= 0) { return; } @@ -712,6 +713,9 @@ export class SearchAddon implements ITerminalAddon { element.style.outline = `1px solid ${borderColor}`; } } + if (isActiveResult) { + element.classList.add('xterm-find-active-result-decoration'); + } } /** @@ -736,7 +740,7 @@ export class SearchAddon implements ITerminalAddon { position: 'center' } }); - findResultDecoration?.onRender((e) => this._applyStyles(e, options.matchBorder)); + findResultDecoration?.onRender((e) => this._applyStyles(e, options.matchBorder, false)); findResultDecoration?.onDispose(() => marker.dispose()); return findResultDecoration; } diff --git a/addons/xterm-addon-web-links/test/tsconfig.json b/addons/xterm-addon-web-links/test/tsconfig.json index 1c772984..9f4d23df 100644 --- a/addons/xterm-addon-web-links/test/tsconfig.json +++ b/addons/xterm-addon-web-links/test/tsconfig.json @@ -12,8 +12,7 @@ "strict": true, "types": [ "../../../node_modules/@types/mocha", - "../../../node_modules/@types/node", - "../../../out-test/api/TestUtils" + "../../../node_modules/@types/node" ] }, "include": [ diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index d060c4d1..8edda780 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -167,7 +167,6 @@ export class WebglRenderer extends Disposable implements IRenderer { this._core.screenElement!.style.height = `${this.dimensions.canvasHeight}px`; this._rectangleRenderer.onResize(); - this._glyphRenderer.setDimensions(this.dimensions); this._glyphRenderer.onResize(); @@ -398,6 +397,9 @@ export class WebglRenderer extends Disposable implements IRenderer { // Apply the selection color if needed if (this._isCellSelected(x, y)) { bgOverride = this._colors.selectionOpaque.rgba >> 8 & 0xFFFFFF; + if (this._colors.selectionForeground) { + fgOverride = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF; + } } // Apply decorations on the top layer diff --git a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts index 0ce893df..92cfd4a9 100644 --- a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts +++ b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts @@ -23,6 +23,7 @@ export function generateConfig(scaledCellWidth: number, scaledCellHeight: number cursorAccent: NULL_COLOR, selectionTransparent: NULL_COLOR, selectionOpaque: NULL_COLOR, + selectionForeground: NULL_COLOR, // For the static char atlas, we only use the first 16 colors, but we need all 256 for the // dynamic character atlas. ansi: colors.ansi.slice(), diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts index cec1e3b1..d51990a7 100644 --- a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts @@ -875,6 +875,25 @@ describe('WebGL Renderer Integration Tests', async () => { }); }); + describe('selectionForeground', () => { + if (areTestsEnabled) { + before(async () => setupBrowser({ rendererType: 'dom' })); + after(async () => browser.close()); + beforeEach(async () => page.evaluate(`window.term.reset()`)); + } + + itWebgl('transparent background inverse', async () => { + const theme: ITheme = { + selectionForeground: '#ff0000' + }; + await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + const data = `\\x1b[7m█\x1b[0m`; + await writeSync(page, data); + await page.evaluate(`window.term.selectAll()`); + await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]); + }); + }); + describe('decoration color overrides', async () => { if (areTestsEnabled) { before(async () => setupBrowser({ rendererType: 'dom' })); diff --git a/css/xterm.css b/css/xterm.css index 2f84c859..95fc61ed 100644 --- a/css/xterm.css +++ b/css/xterm.css @@ -183,3 +183,8 @@ right: 0; pointer-events: none; } + +.xterm-decoration-top { + z-index: 2; + position: relative; +} diff --git a/package.json b/package.json index c3e29be5..b6a18f8b 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "mustache": "^4.2.0", "node-pty": "^0.10.1", "nyc": "^15.1.0", - "playwright": "^1.16.2", + "playwright": "^1.22.1", "source-map-loader": "^3.0.0", "source-map-support": "^0.5.20", "ts-loader": "^9.1.2", diff --git a/src/browser/ColorManager.test.ts b/src/browser/ColorManager.test.ts index 926a7df3..96bc82e4 100644 --- a/src/browser/ColorManager.test.ts +++ b/src/browser/ColorManager.test.ts @@ -34,7 +34,7 @@ describe('ColorManager', () => { describe('constructor', () => { it('should fill all colors with values', () => { for (const key of Object.keys(cm.colors)) { - if (key !== 'ansi' && key !== 'contrastCache') { + if (key !== 'ansi' && key !== 'contrastCache' && key !== 'selectionForeground') { // A #rrggbb or rgba(...) assert.ok((cm.colors as any)[key].css.length >= 7); } diff --git a/src/browser/ColorManager.ts b/src/browser/ColorManager.ts index 2d6e4ea5..f11ef8fd 100644 --- a/src/browser/ColorManager.ts +++ b/src/browser/ColorManager.ts @@ -104,6 +104,7 @@ export class ColorManager implements IColorManager { cursorAccent: DEFAULT_CURSOR_ACCENT, selectionTransparent: DEFAULT_SELECTION, selectionOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION), + selectionForeground: undefined, ansi: DEFAULT_ANSI_COLORS.slice(), contrastCache: this._contrastCache }; @@ -128,6 +129,15 @@ export class ColorManager implements IColorManager { this.colors.cursorAccent = this._parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT, true); this.colors.selectionTransparent = this._parseColor(theme.selection, DEFAULT_SELECTION, true); this.colors.selectionOpaque = color.blend(this.colors.background, this.colors.selectionTransparent); + const nullColor: IColor = { + css: '', + rgba: 0 + }; + this.colors.selectionForeground = theme.selectionForeground ? this._parseColor(theme.selectionForeground, nullColor) : undefined; + if (this.colors.selectionForeground === nullColor) { + this.colors.selectionForeground = undefined; + } + /** * If selection color is opaque, blend it with background with 0.3 opacity * Issue #2737 diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index 89542936..dae9acfa 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -18,6 +18,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 { private _linkProviders: ILinkProvider[] = []; public get currentLink(): ILinkWithState | undefined { return this._currentLink; } protected _currentLink: ILinkWithState | undefined; + private _mouseDownLink: ILinkWithState | undefined; private _lastMouseEvent: MouseEvent | undefined; private _linkCacheDisposables: IDisposable[] = []; private _lastBufferCell: IBufferCellPosition | undefined; @@ -61,7 +62,8 @@ export class Linkifier2 extends Disposable implements ILinkifier2 { this._clearCurrentLink(); })); this.register(addDisposableDomListener(this._element, 'mousemove', this._onMouseMove.bind(this))); - this.register(addDisposableDomListener(this._element, 'click', this._onClick.bind(this))); + this.register(addDisposableDomListener(this._element, 'mousedown', this._handleMouseDown.bind(this))); + this.register(addDisposableDomListener(this._element, 'mouseup', this._handleMouseUp.bind(this))); } private _onMouseMove(event: MouseEvent): void { @@ -129,7 +131,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 { let linkProvided = false; // There is no link cached, so ask for one - this._linkProviders.forEach((linkProvider, i) => { + for (const [i, linkProvider] of this._linkProviders.entries()) { if (useLineCache) { const existingReply = this._activeProviderReplies?.get(i); // If there isn't a reply, the provider hasn't responded yet. @@ -156,7 +158,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 { } }); } - }); + } } private _removeIntersectingLinks(y: number, replies: Map): void { @@ -222,18 +224,21 @@ export class Linkifier2 extends Disposable implements ILinkifier2 { return linkProvided; } - private _onClick(event: MouseEvent): void { + private _handleMouseDown(): void { + this._mouseDownLink = this._currentLink; + } + + private _handleMouseUp(event: MouseEvent): void { if (!this._element || !this._mouseService || !this._currentLink) { return; } const position = this._positionFromMouseEvent(event, this._element, this._mouseService); - if (!position) { return; } - if (this._linkAtPosition(this._currentLink.link, position)) { + if (this._mouseDownLink === this._currentLink && this._linkAtPosition(this._currentLink.link, position)) { this._currentLink.link.activate(event, this._currentLink.link.text); } } @@ -303,7 +308,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 { // Add listener for rerendering if (this._renderService) { - this._linkCacheDisposables.push(this._renderService.onRenderedBufferChange(e => { + this._linkCacheDisposables.push(this._renderService.onRenderedViewportChange(e => { // When start is 0 a scroll most likely occurred, make sure links above the fold also get // cleared. const start = e.start === 0 ? 0 : e.start + 1 + this._bufferService.buffer.ydisp; diff --git a/src/browser/RenderDebouncer.ts b/src/browser/RenderDebouncer.ts index 02521070..ad2d79b4 100644 --- a/src/browser/RenderDebouncer.ts +++ b/src/browser/RenderDebouncer.ts @@ -3,16 +3,17 @@ * @license MIT */ -import { IRenderDebouncer } from 'browser/Types'; +import { IRenderDebouncerWithCallback } from 'browser/Types'; /** * Debounces calls to render terminal rows using animation frames. */ -export class RenderDebouncer implements IRenderDebouncer { +export class RenderDebouncer implements IRenderDebouncerWithCallback { private _rowStart: number | undefined; private _rowEnd: number | undefined; private _rowCount: number | undefined; private _animationFrame: number | undefined; + private _refreshCallbacks: FrameRequestCallback[] = []; constructor( private _renderCallback: (start: number, end: number) => void @@ -26,6 +27,14 @@ export class RenderDebouncer implements IRenderDebouncer { } } + public addRefreshCallback(callback: FrameRequestCallback): number { + this._refreshCallbacks.push(callback); + if (!this._animationFrame) { + this._animationFrame = window.requestAnimationFrame(() => this._innerRefresh()); + } + return this._animationFrame; + } + public refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void { this._rowCount = rowCount; // Get the min/max row start/end for the arg values @@ -43,8 +52,11 @@ export class RenderDebouncer implements IRenderDebouncer { } private _innerRefresh(): void { + this._animationFrame = undefined; + // Make sure values are set if (this._rowStart === undefined || this._rowEnd === undefined || this._rowCount === undefined) { + this._runRefreshCallbacks(); return; } @@ -55,9 +67,16 @@ export class RenderDebouncer implements IRenderDebouncer { // Reset debouncer (this happens before render callback as the render could trigger it again) this._rowStart = undefined; this._rowEnd = undefined; - this._animationFrame = undefined; // Run render callback this._renderCallback(start, end); + this._runRefreshCallbacks(); + } + + private _runRefreshCallbacks(): void { + for (const callback of this._refreshCallbacks) { + callback(0); + } + this._refreshCallbacks = []; } } diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 7d3a3956..c24b1330 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -526,7 +526,7 @@ export class Terminal extends CoreTerminal implements ITerminal { const renderer = this._createRenderer(); this._renderService = this.register(this._instantiationService.createInstance(RenderService, renderer, this.rows, this.screenElement)); this._instantiationService.setService(IRenderService, this._renderService); - this.register(this._renderService.onRenderedBufferChange(e => this._onRender.fire(e))); + this.register(this._renderService.onRenderedViewportChange(e => this._onRender.fire(e))); this.onResize(e => this._renderService!.resize(e.cols, e.rows)); this._compositionView = document.createElement('div'); diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index 85e2bb55..1ba175f3 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -7,7 +7,7 @@ import { IDisposable, IMarker, ISelectionPosition, ILinkProvider, IDecorationOpt import { IEvent, EventEmitter } from 'common/EventEmitter'; import { ICharacterJoinerService, ICharSizeService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services'; import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types'; -import { IColorSet, ILinkMatcherOptions, ITerminal, ILinkifier, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler } from 'browser/Types'; +import { IColorSet, ILinkMatcherOptions, ITerminal, ILinkifier, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler, IRenderDebouncer } from 'browser/Types'; import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types'; import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions } from 'common/Types'; import { Buffer } from 'common/buffer/Buffer'; @@ -16,6 +16,7 @@ import { Terminal } from 'browser/Terminal'; import { IUnicodeService, IOptionsService, ICoreService, ICoreMouseService } from 'common/services/Services'; import { IFunctionIdentifier, IParams } from 'common/parser/Types'; import { AttributeData } from 'common/buffer/AttributeData'; +import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types'; export class TestTerminal extends Terminal { public get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; } @@ -30,6 +31,7 @@ export class MockTerminal implements ITerminal { public onBlur!: IEvent; public onFocus!: IEvent; public onA11yChar!: IEvent; + public onWriteParsed!: IEvent; public onA11yTab!: IEvent; public onCursorMove!: IEvent; public onLineFeed!: IEvent; @@ -370,7 +372,7 @@ export class MockMouseService implements IMouseService { export class MockRenderService implements IRenderService { public serviceBrand: undefined; public onDimensionsChange: IEvent = new EventEmitter().event; - public onRenderedBufferChange: IEvent<{ start: number, end: number }, void> = new EventEmitter<{ start: number, end: number }>().event; + public onRenderedViewportChange: IEvent<{ start: number, end: number }, void> = new EventEmitter<{ start: number, end: number }>().event; public onRender: IEvent<{ start: number, end: number }, void> = new EventEmitter<{ start: number, end: number }>().event; public onRefreshRequest: IEvent<{ start: number, end: number}, void> = new EventEmitter<{ start: number, end: number }>().event; public dimensions: IRenderDimensions = { @@ -390,15 +392,15 @@ export class MockRenderService implements IRenderService { public refreshRows(start: number, end: number): void { throw new Error('Method not implemented.'); } + public addRefreshCallback(callback: FrameRequestCallback): number { + throw new Error('Method not implemented.'); + } public clearTextureAtlas(): void { throw new Error('Method not implemented.'); } public resize(cols: number, rows: number): void { throw new Error('Method not implemented.'); } - public changeOptions(): void { - throw new Error('Method not implemented.'); - } public setRenderer(renderer: IRenderer): void { throw new Error('Method not implemented.'); } @@ -449,3 +451,54 @@ export class MockCharacterJoinerService implements ICharacterJoinerService { return []; } } + +export class MockSelectionService implements ISelectionService { + public serviceBrand: undefined; + public selectionText: string = ''; + public hasSelection: boolean = false; + public selectionStart: [number, number] | undefined; + public selectionEnd: [number, number] | undefined; + public onLinuxMouseSelection = new EventEmitter().event; + public onRequestRedraw = new EventEmitter().event; + public onRequestScrollLines = new EventEmitter().event; + public onSelectionChange = new EventEmitter().event; + public disable(): void { + throw new Error('Method not implemented.'); + } + public enable(): void { + throw new Error('Method not implemented.'); + } + public reset(): void { + throw new Error('Method not implemented.'); + } + public setSelection(row: number, col: number, length: number): void { + throw new Error('Method not implemented.'); + } + public selectAll(): void { + throw new Error('Method not implemented.'); + } + public selectLines(start: number, end: number): void { + throw new Error('Method not implemented.'); + } + public clearSelection(): void { + throw new Error('Method not implemented.'); + } + public rightClickSelect(event: MouseEvent): void { + throw new Error('Method not implemented.'); + } + public shouldColumnSelect(event: MouseEvent | KeyboardEvent): boolean { + throw new Error('Method not implemented.'); + } + public shouldForceSelection(event: MouseEvent): boolean { + throw new Error('Method not implemented.'); + } + public refresh(isLinuxMouseSelection?: boolean): void { + throw new Error('Method not implemented.'); + } + public onMouseDown(event: MouseEvent): void { + throw new Error('Method not implemented.'); + } + public isCellInSelection(x: number, y: number): boolean { + return false; + } +} diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index 0e83c213..4e203ab7 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -44,6 +44,7 @@ export interface IPublicTerminal extends IDisposable { onSelectionChange: IEvent; onRender: IEvent<{ start: number, end: number }>; onResize: IEvent<{ cols: number, rows: number }>; + onWriteParsed: IEvent; onTitleChange: IEvent; onBell: IEvent; blur(): void; @@ -120,6 +121,7 @@ export interface IColorSet { selectionTransparent: IColor; /** The selection blended on top of background. */ selectionOpaque: IColor; + selectionForeground: IColor | undefined; ansi: IColor[]; contrastCache: IColorContrastCache; } @@ -309,3 +311,7 @@ export interface ICharacterJoiner { export interface IRenderDebouncer extends IDisposable { refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void; } + +export interface IRenderDebouncerWithCallback extends IRenderDebouncer { + addRefreshCallback(callback: FrameRequestCallback): number; +} diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index 14fab897..1eb9dc4e 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -26,7 +26,6 @@ export class Viewport extends Disposable implements IViewport { private _lastRecordedBufferHeight: number = 0; private _lastTouchY: number = 0; private _lastScrollTop: number = 0; - private _lastHadScrollBar: boolean = false; private _activeBuffer: IBuffer; private _renderDimensions: IRenderDimensions; @@ -54,7 +53,6 @@ export class Viewport extends Disposable implements IViewport { // Unfortunately the overlay scrollbar would be hidden underneath the screen element in that case, // therefore we account for a standard amount to make it visible this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH; - this._lastHadScrollBar = true; this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._onScroll.bind(this))); // Track properties used in performance critical code manually to avoid using slow getters @@ -109,17 +107,6 @@ export class Viewport extends Disposable implements IViewport { this._viewportElement.scrollTop = scrollTop; } - // Update scroll bar width - if (this._optionsService.rawOptions.scrollback === 0) { - this.scrollBarWidth = 0; - } else { - this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH; - } - this._lastHadScrollBar = this.scrollBarWidth > 0; - - const elementStyle = window.getComputedStyle(this._element); - const elementPadding = parseInt(elementStyle.paddingLeft) + parseInt(elementStyle.paddingRight); - this._viewportElement.style.width = (this._renderService.dimensions.actualCellWidth * (this._bufferService.cols) + this.scrollBarWidth + (this._lastHadScrollBar ? elementPadding : 0)).toString() + 'px'; this._refreshAnimationFrame = null; } @@ -151,11 +138,6 @@ export class Viewport extends Disposable implements IViewport { this._refresh(immediate); return; } - - // If the scroll bar visibility changed - if (this._lastHadScrollBar !== (this._optionsService.rawOptions.scrollback > 0)) { - this._refresh(immediate); - } } /** diff --git a/src/browser/decorations/BufferDecorationRenderer.ts b/src/browser/decorations/BufferDecorationRenderer.ts index ac3457f3..7fcc5ea9 100644 --- a/src/browser/decorations/BufferDecorationRenderer.ts +++ b/src/browser/decorations/BufferDecorationRenderer.ts @@ -14,6 +14,7 @@ export class BufferDecorationRenderer extends Disposable { private _animationFrame: number | undefined; private _altBufferIsActive: boolean = false; + private _dimensionsChanged: boolean = false; constructor( private readonly _screenElement: HTMLElement, @@ -27,8 +28,11 @@ export class BufferDecorationRenderer extends Disposable { this._container.classList.add('xterm-decoration-container'); this._screenElement.appendChild(this._container); - this.register(this._renderService.onRenderedBufferChange(() => this._queueRefresh())); - this.register(this._renderService.onDimensionsChange(() => this._queueRefresh())); + this.register(this._renderService.onRenderedViewportChange(() => this._queueRefresh())); + this.register(this._renderService.onDimensionsChange(() => { + this._dimensionsChanged = true; + this._queueRefresh(); + })); this.register(addDisposableDomListener(window, 'resize', () => this._queueRefresh())); this.register(this._bufferService.buffers.onBufferActivate(() => { this._altBufferIsActive = this._bufferService.buffer === this._bufferService.buffers.alt; @@ -47,7 +51,7 @@ export class BufferDecorationRenderer extends Disposable { if (this._animationFrame !== undefined) { return; } - this._animationFrame = window.requestAnimationFrame(() => { + this._animationFrame = this._renderService.addRefreshCallback(() => { this.refreshDecorations(); this._animationFrame = undefined; }); @@ -57,10 +61,14 @@ export class BufferDecorationRenderer extends Disposable { for (const decoration of this._decorationService.decorations) { this._renderDecoration(decoration); } + this._dimensionsChanged = false; } private _renderDecoration(decoration: IInternalDecoration): void { this._refreshStyle(decoration); + if (this._dimensionsChanged) { + this._refreshXPosition(decoration); + } } private _createElement(decoration: IInternalDecoration): HTMLElement { @@ -76,11 +84,7 @@ export class BufferDecorationRenderer extends Disposable { // exceeded the container width, so hide element.style.display = 'none'; } - if ((decoration.options.anchor || 'left') === 'right') { - element.style.right = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : ''; - } else { - element.style.left = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : ''; - } + this._refreshXPosition(decoration, element); return element; } @@ -108,6 +112,18 @@ export class BufferDecorationRenderer extends Disposable { } } + private _refreshXPosition(decoration: IInternalDecoration, element: HTMLElement | undefined = decoration.element): void { + if (!element) { + return; + } + const x = decoration.options.x ?? 0; + if ((decoration.options.anchor || 'left') === 'right') { + element.style.right = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : ''; + } else { + element.style.left = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : ''; + } + } + private _removeDecoration(decoration: IInternalDecoration): void { this._decorationElements.get(decoration)?.remove(); this._decorationElements.delete(decoration); diff --git a/src/browser/decorations/OverviewRulerRenderer.ts b/src/browser/decorations/OverviewRulerRenderer.ts index 39480ca2..f31409ca 100644 --- a/src/browser/decorations/OverviewRulerRenderer.ts +++ b/src/browser/decorations/OverviewRulerRenderer.ts @@ -82,7 +82,7 @@ export class OverviewRulerRenderer extends Disposable { * and hide the canvas if the alt buffer is active */ private _registerBufferChangeListeners(): void { - this.register(this._renderService.onRenderedBufferChange(() => this._queueRefresh())); + this.register(this._renderService.onRenderedViewportChange(() => this._queueRefresh())); this.register(this._bufferService.buffers.onBufferActivate(() => { this._canvas!.style.display = this._bufferService.buffer === this._bufferService.buffers.alt ? 'none' : 'block'; })); diff --git a/src/browser/input/Mouse.test.ts b/src/browser/input/Mouse.test.ts index 6a908499..00260361 100644 --- a/src/browser/input/Mouse.test.ts +++ b/src/browser/input/Mouse.test.ts @@ -11,30 +11,38 @@ const CHAR_WIDTH = 10; const CHAR_HEIGHT = 20; describe('Mouse getCoords', () => { + let windowOverride: Pick; let document: Document; beforeEach(() => { + windowOverride = { + getComputedStyle(): any { + return { + getPropertyValue: () => '0px' + } as Pick; + } + }; document = new jsdom.JSDOM('').window.document; }); it('should return the cell that was clicked', () => { let coords: [number, number] | undefined; - coords = getCoords({ clientX: CHAR_WIDTH / 2, clientY: CHAR_HEIGHT / 2 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT); + coords = getCoords(windowOverride, { clientX: CHAR_WIDTH / 2, clientY: CHAR_HEIGHT / 2 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT); assert.deepEqual(coords, [1, 1]); - coords = getCoords({ clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT); + coords = getCoords(windowOverride, { clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT); assert.deepEqual(coords, [1, 1]); - coords = getCoords({ clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT + 1 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT); + coords = getCoords(windowOverride, { clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT + 1 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT); assert.deepEqual(coords, [1, 2]); - coords = getCoords({ clientX: CHAR_WIDTH + 1, clientY: CHAR_HEIGHT }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT); + coords = getCoords(windowOverride, { clientX: CHAR_WIDTH + 1, clientY: CHAR_HEIGHT }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT); assert.deepEqual(coords, [2, 1]); }); it('should ensure the coordinates are returned within the terminal bounds', () => { let coords: [number, number] | undefined; - coords = getCoords({ clientX: -1, clientY: -1 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT); + coords = getCoords(windowOverride, { clientX: -1, clientY: -1 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT); assert.deepEqual(coords, [1, 1]); // Event are double the cols/rows - coords = getCoords({ clientX: CHAR_WIDTH * 20, clientY: CHAR_HEIGHT * 20 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT); + coords = getCoords(windowOverride, { clientX: CHAR_WIDTH * 20, clientY: CHAR_HEIGHT * 20 }, document.createElement('div'), 10, 10, true, CHAR_WIDTH, CHAR_HEIGHT); assert.deepEqual(coords, [10, 10], 'coordinates should never come back as larger than the terminal'); }); }); diff --git a/src/browser/input/Mouse.ts b/src/browser/input/Mouse.ts index 2986fb3c..6c377edb 100644 --- a/src/browser/input/Mouse.ts +++ b/src/browser/input/Mouse.ts @@ -3,9 +3,15 @@ * @license MIT */ -export function getCoordsRelativeToElement(event: {clientX: number, clientY: number}, element: HTMLElement): [number, number] { +export function getCoordsRelativeToElement(window: Pick, event: {clientX: number, clientY: number}, element: HTMLElement): [number, number] { const rect = element.getBoundingClientRect(); - return [event.clientX - rect.left, event.clientY - rect.top]; + const elementStyle = window.getComputedStyle(element); + const leftPadding = parseInt(elementStyle.getPropertyValue('padding-left')); + const topPadding = parseInt(elementStyle.getPropertyValue('padding-top')); + return [ + event.clientX - rect.left - leftPadding, + event.clientY - rect.top - topPadding + ]; } /** @@ -20,13 +26,13 @@ export function getCoordsRelativeToElement(event: {clientX: number, clientY: num * apply an offset to the x value such that the left half of the cell will * select that cell and the right half will select the next cell. */ -export function getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, hasValidCharSize: boolean, actualCellWidth: number, actualCellHeight: number, isSelection?: boolean): [number, number] | undefined { +export function getCoords(window: Pick, event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, hasValidCharSize: boolean, actualCellWidth: number, actualCellHeight: number, isSelection?: boolean): [number, number] | undefined { // Coordinates cannot be measured if there are no valid if (!hasValidCharSize) { return undefined; } - const coords = getCoordsRelativeToElement(event, element); + const coords = getCoordsRelativeToElement(window, event, element); if (!coords) { return undefined; } diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts index 1acde934..187bd3b5 100644 --- a/src/browser/public/Terminal.ts +++ b/src/browser/public/Terminal.ts @@ -74,6 +74,7 @@ export class Terminal implements ITerminalApi { public get onScroll(): IEvent { return this._core.onScroll; } public get onSelectionChange(): IEvent { return this._core.onSelectionChange; } public get onTitleChange(): IEvent { return this._core.onTitleChange; } + public get onWriteParsed(): IEvent { return this._core.onWriteParsed; } public get element(): HTMLElement | undefined { return this._core.element; } public get parser(): IParser { diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts index 696b793f..ead0844f 100644 --- a/src/browser/renderer/BaseRenderLayer.ts +++ b/src/browser/renderer/BaseRenderLayer.ts @@ -29,6 +29,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { private _scaledCharLeft: number = 0; private _scaledCharTop: number = 0; + private _selectionStart: [number, number] | undefined; + private _selectionEnd: [number, number] | undefined; + private _columnSelectMode: boolean = false; + protected _charAtlas: BaseCharAtlas | undefined; /** @@ -80,7 +84,12 @@ export abstract class BaseRenderLayer implements IRenderLayer { public onFocus(): void {} public onCursorMove(): void {} public onGridChanged(startRow: number, endRow: number): void {} - public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {} + + public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void { + this._selectionStart = start; + this._selectionEnd = end; + this._columnSelectMode = columnSelectMode; + } public setColors(colorSet: IColorSet): void { this._refreshCharAtlas(colorSet); @@ -457,6 +466,13 @@ export abstract class BaseRenderLayer implements IRenderLayer { isTop = d.options.layer === 'top'; } + // Apply selection foreground if applicable + if (!isTop) { + if (this._colors.selectionForeground && this._isCellInSelection(x, y)) { + fgOverride = this._colors.selectionForeground.rgba; + } + } + if (!bgOverride && !fgOverride && (this._optionsService.rawOptions.minimumContrastRatio === 1 || isPowerlineGlyph(cell.getCode()))) { return undefined; } @@ -546,5 +562,21 @@ export abstract class BaseRenderLayer implements IRenderLayer { return this._colors.foreground.rgba; } } + + private _isCellInSelection(x: number, y: number): boolean { + const start = this._selectionStart; + const end = this._selectionEnd; + if (!start || !end) { + return false; + } + if (this._columnSelectMode) { + return x >= start[0] && y >= start[1] && + x < end[0] && y < end[1]; + } + return (y > start[1] && y < end[1]) || + (start[1] === end[1] && y === start[1] && x >= start[0] && x < end[0]) || + (start[1] < end[1] && y === end[1] && x < end[0]) || + (start[1] < end[1] && y === start[1] && x >= start[0]); + } } diff --git a/src/browser/renderer/Renderer.ts b/src/browser/renderer/Renderer.ts index 8dfe09c9..8bc32278 100644 --- a/src/browser/renderer/Renderer.ts +++ b/src/browser/renderer/Renderer.ts @@ -119,6 +119,10 @@ export class Renderer extends Disposable implements IRenderer { public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void { this._runOperation(l => l.onSelectionChanged(start, end, columnSelectMode)); + // Selection foreground requires a full re-render + if (this._colors.selectionForeground) { + this._onRequestRedraw.fire({ start: 0, end: this._bufferService.rows - 1 }); + } } public onCursorMove(): void { diff --git a/src/browser/renderer/SelectionRenderLayer.ts b/src/browser/renderer/SelectionRenderLayer.ts index be911eb9..ce4fe071 100644 --- a/src/browser/renderer/SelectionRenderLayer.ts +++ b/src/browser/renderer/SelectionRenderLayer.ts @@ -54,6 +54,8 @@ export class SelectionRenderLayer extends BaseRenderLayer { } public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void { + super.onSelectionChanged(start, end, columnSelectMode); + // Selection has not changed if (!this._didStateChange(start, end, columnSelectMode, this._bufferService.buffer.ydisp)) { return; diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index d15d7eac..fdeef91c 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -281,6 +281,9 @@ export class DomRenderer extends Disposable implements IRenderer { this._selectionContainer.removeChild(this._selectionContainer.children[0]); } + this._rowFactory.onSelectionChanged(start, end, columnSelectMode); + this.renderRows(0, this._bufferService.rows - 1); + // Selection does not exist if (!start || !end) { return; diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index bb511a47..5a374b14 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts @@ -12,7 +12,7 @@ import { IBufferLine } from 'common/Types'; import { CellData } from 'common/buffer/CellData'; import { MockCoreService, MockDecorationService, MockOptionsService } from 'common/TestUtils.test'; import { css } from 'common/Color'; -import { MockCharacterJoinerService } from 'browser/TestUtils.test'; +import { MockCharacterJoinerService, MockSelectionService } from 'browser/TestUtils.test'; describe('DomRendererRowFactory', () => { let dom: jsdom.JSDOM; diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index bf3939e8..266d7587 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -7,10 +7,10 @@ import { IBufferLine, ICellData, IColor } from 'common/Types'; import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; -import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; +import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; import { color, rgba } from 'common/Color'; import { IColorSet } from 'browser/Types'; -import { ICharacterJoinerService } from 'browser/services/Services'; +import { ICharacterJoinerService, ISelectionService } from 'browser/services/Services'; import { JoinedCellData } from 'browser/services/CharacterJoinerService'; import { isPowerlineGlyph } from 'browser/renderer/RendererUtils'; @@ -28,6 +28,10 @@ export const CURSOR_STYLE_UNDERLINE_CLASS = 'xterm-cursor-underline'; export class DomRendererRowFactory { private _workCell: CellData = new CellData(); + private _selectionStart: [number, number] | undefined; + private _selectionEnd: [number, number] | undefined; + private _columnSelectMode: boolean = false; + constructor( private readonly _document: Document, private _colors: IColorSet, @@ -42,6 +46,12 @@ export class DomRendererRowFactory { this._colors = colors; } + public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void { + this._selectionStart = start; + this._selectionEnd = end; + this._columnSelectMode = columnSelectMode; + } + public createRow(lineData: IBufferLine, row: number, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number): DocumentFragment { const fragment = this._document.createDocumentFragment(); @@ -195,6 +205,20 @@ export class DomRendererRowFactory { isTop = d.options.layer === 'top'; } + // Apply selection foreground if applicable + if (!isTop) { + if (this._colors.selectionForeground && this._isCellInSelection(x, row)) { + fgColorMode = Attributes.CM_RGB; + fg = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF; + fgOverride = this._colors.selectionForeground; + } + } + + // If it's a top decoration, render above the selection + if (isTop) { + charElement.classList.add(`xterm-decoration-top`); + } + // Foreground switch (fgColorMode) { case Attributes.CM_P16: @@ -278,6 +302,22 @@ export class DomRendererRowFactory { private _addStyle(element: HTMLElement, style: string): void { element.setAttribute('style', `${element.getAttribute('style') || ''}${style};`); } + + private _isCellInSelection(x: number, y: number): boolean { + const start = this._selectionStart; + const end = this._selectionEnd; + if (!start || !end) { + return false; + } + if (this._columnSelectMode) { + return x >= start[0] && y >= start[1] && + x < end[0] && y < end[1]; + } + return (y > start[1] && y < end[1]) || + (start[1] === end[1] && y === start[1] && x >= start[0] && x < end[0]) || + (start[1] < end[1] && y === end[1] && x < end[0]) || + (start[1] < end[1] && y === start[1] && x >= start[0]); + } } function padStart(text: string, padChar: string, length: number): string { diff --git a/src/browser/services/MouseService.ts b/src/browser/services/MouseService.ts index 348ba64e..69123ba3 100644 --- a/src/browser/services/MouseService.ts +++ b/src/browser/services/MouseService.ts @@ -17,6 +17,7 @@ export class MouseService implements IMouseService { public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined { return getCoords( + window, event, element, colCount, diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index b2e619fe..78bd7f56 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -9,7 +9,7 @@ import { EventEmitter, IEvent } from 'common/EventEmitter'; import { Disposable } from 'common/Lifecycle'; import { ScreenDprMonitor } from 'browser/ScreenDprMonitor'; import { addDisposableDomListener } from 'browser/Lifecycle'; -import { IColorSet, IRenderDebouncer } from 'browser/Types'; +import { IColorSet, IRenderDebouncer, IRenderDebouncerWithCallback } from 'browser/Types'; import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services'; import { ICharSizeService, IRenderService } from 'browser/services/Services'; @@ -22,7 +22,7 @@ interface ISelectionState { export class RenderService extends Disposable implements IRenderService { public serviceBrand: undefined; - private _renderDebouncer: IRenderDebouncer; + private _renderDebouncer: IRenderDebouncerWithCallback; private _screenDprMonitor: ScreenDprMonitor; private _isPaused: boolean = false; @@ -39,8 +39,8 @@ export class RenderService extends Disposable implements IRenderService { private _onDimensionsChange = new EventEmitter(); public get onDimensionsChange(): IEvent { return this._onDimensionsChange.event; } - private _onRenderedBufferChange = new EventEmitter<{ start: number, end: number }>(); - public get onRenderedBufferChange(): IEvent<{ start: number, end: number }> { return this._onRenderedBufferChange.event; } + private _onRenderedViewportChange = new EventEmitter<{ start: number, end: number }>(); + public get onRenderedViewportChange(): IEvent<{ start: number, end: number }> { return this._onRenderedViewportChange.event; } private _onRender = new EventEmitter<{ start: number, end: number }>(); public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; } private _onRefreshRequest = new EventEmitter<{ start: number, end: number }>(); @@ -70,7 +70,7 @@ export class RenderService extends Disposable implements IRenderService { this.register(bufferService.onResize(() => this._fullRefresh())); this.register(bufferService.buffers.onBufferActivate(() => this._renderer?.clear())); - this.register(optionsService.onOptionChange(() => this._renderer.onOptionsChanged())); + this.register(optionsService.onOptionChange(() => this._handleOptionsChanged())); this.register(this._charSizeService.onCharSizeChange(() => this.onCharSizeChanged())); // Do a full refresh whenever any decoration is added or removed. This may not actually result @@ -131,7 +131,7 @@ export class RenderService extends Disposable implements IRenderService { // Fire render event only if it was not a redraw if (!this._isNextRenderRedrawOnly) { - this._onRenderedBufferChange.fire({ start, end }); + this._onRenderedViewportChange.fire({ start, end }); } this._onRender.fire({ start, end }); this._isNextRenderRedrawOnly = true; @@ -142,7 +142,7 @@ export class RenderService extends Disposable implements IRenderService { this._fireOnCanvasResize(); } - public changeOptions(): void { + private _handleOptionsChanged(): void { this._renderer.onOptionsChanged(); this.refreshRows(0, this._rowCount - 1); this._fireOnCanvasResize(); @@ -171,6 +171,10 @@ export class RenderService extends Disposable implements IRenderService { this._fullRefresh(); } + public addRefreshCallback(callback: FrameRequestCallback): number { + return this._renderDebouncer.addRefreshCallback(callback); + } + private _fullRefresh(): void { if (this._isPaused) { this._needsFullRefresh = true; diff --git a/src/browser/services/SelectionService.ts b/src/browser/services/SelectionService.ts index 53020b53..57ba048f 100644 --- a/src/browser/services/SelectionService.ts +++ b/src/browser/services/SelectionService.ts @@ -308,6 +308,15 @@ export class SelectionService extends Disposable implements ISelectionService { return this._areCoordsInSelection(coords, start, end); } + public isCellInSelection(x: number, y: number): boolean { + const start = this._model.finalSelectionStart; + const end = this._model.finalSelectionEnd; + if (!start || !end) { + return false; + } + return this._areCoordsInSelection([x, y], start, end); + } + protected _areCoordsInSelection(coords: [number, number], start: [number, number], end: [number, number]): boolean { 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]) || @@ -393,7 +402,7 @@ export class SelectionService extends Disposable implements ISelectionService { * @param event The mouse event. */ private _getMouseEventScrollAmount(event: MouseEvent): number { - let offset = getCoordsRelativeToElement(event, this._screenElement)[1]; + let offset = getCoordsRelativeToElement(window, event, this._screenElement)[1]; const terminalHeight = this._renderService.dimensions.canvasHeight; if (offset >= 0 && offset <= terminalHeight) { return 0; @@ -771,6 +780,7 @@ export class SelectionService extends Disposable implements ISelectionService { this._model.selectionStart = [col, row]; this._model.selectionStartLength = length; this.refresh(); + this._fireEventIfSelectionChanged(); } public rightClickSelect(ev: MouseEvent): void { diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts index 7191d0ed..e1fb5dbd 100644 --- a/src/browser/services/Services.ts +++ b/src/browser/services/Services.ts @@ -5,7 +5,7 @@ import { IEvent } from 'common/EventEmitter'; import { IRenderDimensions, IRenderer } from 'browser/renderer/Types'; -import { IColorSet } from 'browser/Types'; +import { IColorSet, IRenderDebouncer } from 'browser/Types'; import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; import { IDisposable } from 'common/Types'; @@ -49,7 +49,7 @@ export interface IRenderService extends IDisposable { * Fires when buffer changes are rendered. This does not fire when only cursor * or selections are rendered. */ - onRenderedBufferChange: IEvent<{ start: number, end: number }>; + onRenderedViewportChange: IEvent<{ start: number, end: number }>; /** * Fires on render */ @@ -58,10 +58,11 @@ export interface IRenderService extends IDisposable { dimensions: IRenderDimensions; + addRefreshCallback(callback: FrameRequestCallback): number; + refreshRows(start: number, end: number): void; clearTextureAtlas(): void; resize(cols: number, rows: number): void; - changeOptions(): void; setRenderer(renderer: IRenderer): void; setColors(colors: IColorSet): void; onDevicePixelRatioChange(): void; @@ -101,6 +102,7 @@ export interface ISelectionService { shouldForceSelection(event: MouseEvent): boolean; refresh(isLinuxMouseSelection?: boolean): void; onMouseDown(event: MouseEvent): void; + isCellInSelection(x: number, y: number): boolean; } export const ISoundService = createDecorator('SoundService'); diff --git a/src/common/Color.test.ts b/src/common/Color.test.ts index f16e6ffb..082c81c3 100644 --- a/src/common/Color.test.ts +++ b/src/common/Color.test.ts @@ -153,6 +153,44 @@ describe('Color', () => { describe('css', () => { describe('toColor', () => { + it('should convert the #rgb format to an IColor', () => { + assert.deepEqual(css.toColor('#000'), { css: '#000000', rgba: 0x000000FF }); + assert.deepEqual(css.toColor('#111'), { css: '#111111', rgba: 0x111111FF }); + assert.deepEqual(css.toColor('#222'), { css: '#222222', rgba: 0x222222FF }); + assert.deepEqual(css.toColor('#333'), { css: '#333333', rgba: 0x333333FF }); + assert.deepEqual(css.toColor('#444'), { css: '#444444', rgba: 0x444444FF }); + assert.deepEqual(css.toColor('#555'), { css: '#555555', rgba: 0x555555FF }); + assert.deepEqual(css.toColor('#666'), { css: '#666666', rgba: 0x666666FF }); + assert.deepEqual(css.toColor('#777'), { css: '#777777', rgba: 0x777777FF }); + assert.deepEqual(css.toColor('#888'), { css: '#888888', rgba: 0x888888FF }); + assert.deepEqual(css.toColor('#999'), { css: '#999999', rgba: 0x999999FF }); + assert.deepEqual(css.toColor('#aaa'), { css: '#aaaaaa', rgba: 0xaaaaaaFF }); + assert.deepEqual(css.toColor('#bbb'), { css: '#bbbbbb', rgba: 0xbbbbbbFF }); + assert.deepEqual(css.toColor('#ccc'), { css: '#cccccc', rgba: 0xccccccFF }); + assert.deepEqual(css.toColor('#ddd'), { css: '#dddddd', rgba: 0xddddddFF }); + assert.deepEqual(css.toColor('#eee'), { css: '#eeeeee', rgba: 0xeeeeeeFF }); + assert.deepEqual(css.toColor('#fff'), { css: '#ffffff', rgba: 0xffffffFF }); + assert.deepEqual(css.toColor('#fff'), { css: '#ffffff', rgba: 0xffffffFF }); + }); + it('should convert the #rgb format to an IColor', () => { + assert.deepEqual(css.toColor('#0000'), { css: '#00000000', rgba: 0x00000000 }); + assert.deepEqual(css.toColor('#1111'), { css: '#11111111', rgba: 0x11111111 }); + assert.deepEqual(css.toColor('#2222'), { css: '#22222222', rgba: 0x22222222 }); + assert.deepEqual(css.toColor('#3333'), { css: '#33333333', rgba: 0x33333333 }); + assert.deepEqual(css.toColor('#4444'), { css: '#44444444', rgba: 0x44444444 }); + assert.deepEqual(css.toColor('#5555'), { css: '#55555555', rgba: 0x55555555 }); + assert.deepEqual(css.toColor('#6666'), { css: '#66666666', rgba: 0x66666666 }); + assert.deepEqual(css.toColor('#7777'), { css: '#77777777', rgba: 0x77777777 }); + assert.deepEqual(css.toColor('#8888'), { css: '#88888888', rgba: 0x88888888 }); + assert.deepEqual(css.toColor('#9999'), { css: '#99999999', rgba: 0x99999999 }); + assert.deepEqual(css.toColor('#aaaa'), { css: '#aaaaaaaa', rgba: 0xaaaaaaaa }); + assert.deepEqual(css.toColor('#bbbb'), { css: '#bbbbbbbb', rgba: 0xbbbbbbbb }); + assert.deepEqual(css.toColor('#cccc'), { css: '#cccccccc', rgba: 0xcccccccc }); + assert.deepEqual(css.toColor('#dddd'), { css: '#dddddddd', rgba: 0xdddddddd }); + assert.deepEqual(css.toColor('#eeee'), { css: '#eeeeeeee', rgba: 0xeeeeeeee }); + assert.deepEqual(css.toColor('#ffff'), { css: '#ffffffff', rgba: 0xffffffff }); + assert.deepEqual(css.toColor('#ffff'), { css: '#ffffffff', rgba: 0xffffffff }); + }); it('should convert the #rrggbb format to an IColor', () => { assert.deepEqual(css.toColor('#000000'), { css: '#000000', rgba: 0x000000FF }); assert.deepEqual(css.toColor('#101010'), { css: '#101010', rgba: 0x101010FF }); @@ -191,6 +229,20 @@ describe('Color', () => { assert.deepEqual(css.toColor('#f0f0f0f0'), { css: '#f0f0f0f0', rgba: 0xf0f0f0f0 }); assert.deepEqual(css.toColor('#ffffffff'), { css: '#ffffffff', rgba: 0xffffffff }); }); + it('should convert the rgb() format to an IColor', () => { + assert.deepEqual(css.toColor('rgb(0, 0, 0)'), { css: '#000000ff', rgba: 0x000000ff }); + assert.deepEqual(css.toColor('rgb(80, 0, 0)'), { css: '#500000ff', rgba: 0x500000ff }); + assert.deepEqual(css.toColor('rgb(0, 80, 0)'), { css: '#005000ff', rgba: 0x005000ff }); + assert.deepEqual(css.toColor('rgb(0, 0, 80)'), { css: '#000050ff', rgba: 0x000050ff }); + assert.deepEqual(css.toColor('rgb(255, 255, 255)'), { css: '#ffffffff', rgba: 0xffffffff }); + }); + it('should convert the rgba() format to an IColor', () => { + assert.deepEqual(css.toColor('rgba(0, 0, 0, 0)'), { css: '#00000000', rgba: 0x00000000 }); + assert.deepEqual(css.toColor('rgba(80, 0, 0, 0.5)'), { css: '#50000080', rgba: 0x50000080 }); + assert.deepEqual(css.toColor('rgba(0, 80, 0, 0.5)'), { css: '#00500080', rgba: 0x00500080 }); + assert.deepEqual(css.toColor('rgba(0, 0, 80, 0.5)'), { css: '#00005080', rgba: 0x00005080 }); + assert.deepEqual(css.toColor('rgba(255, 255, 255, 1)'), { css: '#ffffffff', rgba: 0xffffffff }); + }); }); }); diff --git a/src/common/Color.ts b/src/common/Color.ts index b197cd66..e5c7e3eb 100644 --- a/src/common/Color.ts +++ b/src/common/Color.ts @@ -94,17 +94,40 @@ export namespace color { */ export namespace css { export function toColor(css: string): IColor { - switch (css.length) { - case 7: // #rrggbb - return { - css, - rgba: (parseInt(css.slice(1), 16) << 8 | 0xFF) >>> 0 - }; - case 9: // #rrggbbaa - return { - css, - rgba: parseInt(css.slice(1), 16) >>> 0 - }; + if (css.match(/#[0-9a-f]{3,8}/i)) { + switch (css.length) { + case 4: { // #rgb + const r = parseInt(css.slice(1, 2).repeat(2), 16); + const g = parseInt(css.slice(2, 3).repeat(2), 16); + const b = parseInt(css.slice(3, 4).repeat(2), 16); + return rgba.toColor(r, g, b); + } + case 5: { // #rgba + const r = parseInt(css.slice(1, 2).repeat(2), 16); + const g = parseInt(css.slice(2, 3).repeat(2), 16); + const b = parseInt(css.slice(3, 4).repeat(2), 16); + const a = parseInt(css.slice(4, 5).repeat(2), 16); + return rgba.toColor(r, g, b, a); + } + case 7: // #rrggbb + return { + css, + rgba: (parseInt(css.slice(1), 16) << 8 | 0xFF) >>> 0 + }; + case 9: // #rrggbbaa + return { + css, + rgba: parseInt(css.slice(1), 16) >>> 0 + }; + } + } + const rgbaMatch = css.match(/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(,\s*(0|1|\d?\.(\d+))\s*)?\)/); + if (rgbaMatch) { // rgb() or rgba() + const r = parseInt(rgbaMatch[1]); + const g = parseInt(rgbaMatch[2]); + const b = parseInt(rgbaMatch[3]); + const a = Math.round((rgbaMatch[5] === undefined ? 1 : parseFloat(rgbaMatch[5])) * 0xFF); + return rgba.toColor(r, g, b, a); } throw new Error('css.toColor: Unsupported css format'); } @@ -150,15 +173,42 @@ export namespace rgb { * Helper functions where the source type is "rgba" (number: 0xrrggbbaa). */ export namespace rgba { + /** + * Given a foreground color and a background color, either increase or reduce the luminance of the + * foreground color until the specified contrast ratio is met. If pure white or black is hit + * without the contrast ratio being met, go the other direction using the background color as the + * foreground color and take either the first or second result depending on which has the higher + * contrast ratio. + * + * `undefined` will be returned if the contrast ratio is already met. + * + * @param bgRgba The background color in rgba format. + * @param fgRgba The foreground color in rgba format. + * @param ratio The contrast ratio to achieve. + */ export function ensureContrastRatio(bgRgba: number, fgRgba: number, ratio: number): number | undefined { const bgL = rgb.relativeLuminance(bgRgba >> 8); const fgL = rgb.relativeLuminance(fgRgba >> 8); const cr = contrastRatio(bgL, fgL); if (cr < ratio) { if (fgL < bgL) { - return reduceLuminance(bgRgba, fgRgba, ratio); + const resultA = reduceLuminance(bgRgba, fgRgba, ratio); + const resultARatio = contrastRatio(bgL, rgb.relativeLuminance(resultA >> 8)); + if (resultARatio < ratio) { + const resultB = increaseLuminance(bgRgba, bgRgba, ratio); + const resultBRatio = contrastRatio(bgL, rgb.relativeLuminance(resultB >> 8)); + return resultARatio > resultBRatio ? resultA : resultB; + } + return resultA; } - return increaseLuminance(bgRgba, fgRgba, ratio); + const resultA = increaseLuminance(bgRgba, fgRgba, ratio); + const resultARatio = contrastRatio(bgL, rgb.relativeLuminance(resultA >> 8)); + if (resultARatio < ratio) { + const resultB = reduceLuminance(bgRgba, bgRgba, ratio); + const resultBRatio = contrastRatio(bgL, rgb.relativeLuminance(resultB >> 8)); + return resultARatio > resultBRatio ? resultA : resultB; + } + return resultA; } return undefined; } @@ -208,10 +258,10 @@ export namespace rgba { return [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF]; } - export function toColor(r: number, g: number, b: number): IColor { + export function toColor(r: number, g: number, b: number, a?: number): IColor { return { - css: channels.toCss(r, g, b), - rgba: channels.toRgba(r, g, b) + css: channels.toCss(r, g, b, a), + rgba: channels.toRgba(r, g, b, a) }; } } diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index 12b374c8..af9ec3f9 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -68,6 +68,8 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { private _onResize = new EventEmitter<{ cols: number, rows: number }>(); public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; } protected _onScroll = new EventEmitter(); + public get onWriteParsed(): IEvent { return this._onWriteParsed.event; } + protected _onWriteParsed = new EventEmitter(); /** * Internally we track the source of the scroll but this is meaningless outside the library so * it's filtered out. @@ -138,6 +140,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { // Setup WriteBuffer this._writeBuffer = new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult)); + this.register(forwardEvent(this._writeBuffer.onWriteParsed, this._onWriteParsed)); } public dispose(): void { diff --git a/src/common/input/WriteBuffer.ts b/src/common/input/WriteBuffer.ts index cc84c9ab..67fd751e 100644 --- a/src/common/input/WriteBuffer.ts +++ b/src/common/input/WriteBuffer.ts @@ -4,6 +4,8 @@ * @license MIT */ +import { EventEmitter, IEvent } from 'common/EventEmitter'; + declare const setTimeout: (handler: () => void, timeout?: number) => void; /** @@ -44,6 +46,8 @@ export class WriteBuffer { private _bufferOffset = 0; private _isSyncWriting = false; private _syncCalls = 0; + public get onWriteParsed(): IEvent { return this._onWriteParsed.event; } + private _onWriteParsed = new EventEmitter(); constructor(private _action: (data: string | Uint8Array, promiseResult?: boolean) => void | Promise) { } @@ -220,5 +224,6 @@ export class WriteBuffer { this._pendingData = 0; this._bufferOffset = 0; } + this._onWriteParsed.fire(); } } diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index c3190210..017eb386 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -259,6 +259,7 @@ export interface ITheme { cursor?: string; cursorAccent?: string; selection?: string; + selectionForeground?: string; black?: string; red?: string; green?: string; diff --git a/test/api/Terminal.api.ts b/test/api/Terminal.api.ts index b1582be8..3a023d4a 100644 --- a/test/api/Terminal.api.ts +++ b/test/api/Terminal.api.ts @@ -792,6 +792,9 @@ describe('API Integration Tests', function(): void { describe('registerLinkProvider', () => { it('should fire provideLinks when hovering cells', async () => { await openTerminal(page, { rendererType: 'dom' }); + // Focus the terminal as the cursor will show and trigger a rerender, which can clear the + // active link + await page.evaluate('window.term.focus()'); await page.evaluate(` window.calls = []; window.disposable = window.term.registerLinkProvider({ @@ -811,6 +814,9 @@ describe('API Integration Tests', function(): void { it('should fire hover and leave events on the link', async () => { await openTerminal(page, { rendererType: 'dom' }); + // Focus the terminal as the cursor will show and trigger a rerender, which can clear the + // active link + await page.evaluate('window.term.focus()'); await writeSync(page, 'foo bar baz'); // Wait for renderer to catch up as links are cleared on render await pollFor(page, `document.querySelector('.xterm-rows').textContent`, 'foo bar baz '); @@ -846,6 +852,9 @@ describe('API Integration Tests', function(): void { it('should work fine when hover and leave callbacks are not provided', async () => { await openTerminal(page, { rendererType: 'dom' }); + // Focus the terminal as the cursor will show and trigger a rerender, which can clear the + // active link + await page.evaluate('window.term.focus()'); await writeSync(page, 'foo bar baz'); // Wait for renderer to catch up as links are cleared on render await pollFor(page, `document.querySelector('.xterm-rows').textContent`, 'foo bar baz '); @@ -886,18 +895,12 @@ describe('API Integration Tests', function(): void { it('should fire activate events when clicking the link', async () => { await openTerminal(page, { rendererType: 'dom' }); + // Focus the terminal as the cursor will show and trigger a rerender, which can clear the + // active link + await page.evaluate('window.term.focus()'); await writeSync(page, 'a b c'); - // Wait for renderer to catch up as links are cleared on render await pollFor(page, `document.querySelector('.xterm-rows').textContent`, 'a b c '); - - // Focus terminal to avoid a render event clearing the active link - const dims = await getDimensions(); - await moveMouseCell(page, dims, 5, 5); - await page.mouse.down(); - await page.mouse.up(); - await timeout(200); // Not sure how to avoid this timeout, checking for xterm-focus doesn't help - await page.evaluate(` window.calls = []; window.disposable = window.term.registerLinkProvider({ @@ -913,6 +916,7 @@ describe('API Integration Tests', function(): void { } }); `); + const dims = await getDimensions(); await moveMouseCell(page, dims, 3, 1); await pollFor(page, `window.calls`, ['provide 1', 'hover 1']); await page.mouse.down(); @@ -933,6 +937,9 @@ describe('API Integration Tests', function(): void { it('should work when multiple links are provided on the same line', async () => { await openTerminal(page, { rendererType: 'dom' }); + // Focus the terminal as the cursor will show and trigger a rerender, which can clear the + // active link + await page.evaluate('window.term.focus()'); await writeSync(page, 'foo bar baz'); // Wait for renderer to catch up as links are cleared on render await pollFor(page, `document.querySelector('.xterm-rows').textContent`, 'foo bar baz '); @@ -979,6 +986,9 @@ describe('API Integration Tests', function(): void { it('should dispose links when hovering away', async () => { await openTerminal(page, { rendererType: 'dom' }); + // Focus the terminal as the cursor will show and trigger a rerender, which can clear the + // active link + await page.evaluate('window.term.focus()'); await writeSync(page, 'foo bar baz'); // Wait for renderer to catch up as links are cleared on render await pollFor(page, `document.querySelector('.xterm-rows').textContent`, 'foo bar baz '); diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 15ee4650..39dcaee9 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -288,6 +288,8 @@ declare module 'xterm' { cursorAccent?: string; /** The selection background color (can be transparent) */ selection?: string; + /** The selection foreground color */ + selectionForeground?: string; /** ANSI black (eg. `\x1b[30m`) */ black?: string; /** ANSI red (eg. `\x1b[31m`) */ @@ -839,6 +841,17 @@ declare module 'xterm' { */ onRender: IEvent<{ start: number, end: number }>; + /** + * Adds an event listener for when data has been parsed by the terminal, + * after {@link write} is called. This event is useful to listen for any + * changes in the buffer. + * + * This fires at most once per frame, after data parsing completes. Note + * that this can fire when there are still writes pending if there is a lot + * of data. + */ + onWriteParsed: IEvent; + /** * Adds an event listener for when the terminal is resized. The event value * contains the new size. diff --git a/yarn.lock b/yarn.lock index 2523d316..7cb970bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -429,13 +429,6 @@ dependencies: "@types/node" "*" -"@types/yauzl@^2.9.1": - version "2.9.2" - resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.2.tgz#c48e5d56aff1444409e39fa164b0b4d4552a7b7a" - integrity sha512-8uALY5LTvSuHgloDVUvWP3pIauILm+8/0pDMokuDYIoNsOkSwd5AiHBTSEJjKTDcZr5z8UpgOWZkxBF4iJftoA== - dependencies: - "@types/node" "*" - "@typescript-eslint/eslint-plugin@^5.3.0": version "5.3.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.3.0.tgz#a55ae72d28ffeb6badd817fe4566c9cced1f5e29" @@ -831,7 +824,7 @@ acorn@^8.4.1, acorn@^8.5.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.5.0.tgz#4512ccb99b3698c752591e9bb4472e38ad43cee2" integrity sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q== -agent-base@6, agent-base@^6.0.2: +agent-base@6: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== @@ -1027,11 +1020,6 @@ browserslist@^4.14.5: escalade "^3.1.1" node-releases "^1.1.71" -buffer-crc32@~0.2.3: - version "0.2.13" - resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" - integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= - buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" @@ -1255,11 +1243,6 @@ commander@^7.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== -commander@^8.2.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" - integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== - commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -1367,7 +1350,7 @@ debug@4.3.3: dependencies: ms "2.1.2" -debug@^4.3.1, debug@^4.3.2: +debug@^4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== @@ -1504,13 +1487,6 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= -end-of-stream@^1.1.0: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - enhanced-resolve@^5.0.0, enhanced-resolve@^5.8.3: version "5.8.3" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.8.3.tgz#6d552d465cce0423f5b3d718511ea53826a7b2f0" @@ -1624,11 +1600,6 @@ escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= -escape-string-regexp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" - integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== - escodegen@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" @@ -1834,17 +1805,6 @@ express@^4.17.1: utils-merge "1.0.1" vary "~1.1.2" -extract-zip@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" - integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== - dependencies: - debug "^4.1.1" - get-stream "^5.1.0" - yauzl "^2.10.0" - optionalDependencies: - "@types/yauzl" "^2.9.1" - fast-deep-equal@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" @@ -1888,13 +1848,6 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" -fd-slicer@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" - integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= - dependencies: - pend "~1.2.0" - file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -2056,13 +2009,6 @@ get-package-type@^0.1.0: resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== -get-stream@^5.1.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" - integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== - dependencies: - pump "^3.0.0" - get-stream@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" @@ -2319,11 +2265,6 @@ interpret@^2.2.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== -ip@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" - integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= - ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" @@ -2601,11 +2542,6 @@ jest-worker@^27.0.6: merge-stream "^2.0.0" supports-color "^8.0.0" -jpeg-js@^0.4.2: - version "0.4.3" - resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.3.tgz#6158e09f1983ad773813704be80680550eff977b" - integrity sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q== - js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -2852,11 +2788,6 @@ mime@1.6.0: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@^2.4.6: - version "2.5.2" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" - integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== - mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" @@ -3058,7 +2989,7 @@ on-finished@~2.3.0: dependencies: ee-first "1.1.1" -once@^1.3.0, once@^1.3.1, once@^1.4.0: +once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= @@ -3205,11 +3136,6 @@ pathval@^1.1.1: resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== -pend@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" - integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= - picomatch@^2.0.4, picomatch@^2.2.1: version "2.2.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" @@ -3227,39 +3153,17 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -playwright-core@=1.16.2: - version "1.16.2" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.16.2.tgz#13c4c352a41e431ba167dbadb80e8c628e1e1b79" - integrity sha512-8WkoP5OfZAYrRxtW/PCVACn9bNgqrTxVVPlc+MoxvJ48knNsZ+skrPjfno/XF3SgTUY9DyYX0g5fVOB7lkPtGg== - dependencies: - commander "^8.2.0" - debug "^4.1.1" - extract-zip "^2.0.1" - https-proxy-agent "^5.0.0" - jpeg-js "^0.4.2" - mime "^2.4.6" - pngjs "^5.0.0" - progress "^2.0.3" - proper-lockfile "^4.1.1" - proxy-from-env "^1.1.0" - rimraf "^3.0.2" - socks-proxy-agent "^6.1.0" - stack-utils "^2.0.3" - ws "^7.4.6" - yauzl "^2.10.0" - yazl "^2.5.1" +playwright-core@1.22.1: + version "1.22.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.22.1.tgz#59ddf903546171fdfd9c3dc189630c883619667c" + integrity sha512-H+ZUVYnceWNXrRf3oxTEKAr81QzFsCKu5Fp//fEjQvqgKkfA1iX3E9DBrPJpPNOrgVzcE+IqeI0fDmYJe6Ynnw== -playwright@^1.16.2: - version "1.16.2" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.16.2.tgz#92e1c48a74cab778970facca4094ce79fb57cae4" - integrity sha512-lqndwy5rDIp3tOLex5lnLSrltomqxgVkRkRi547rXoTl2qy7PY2rtHbUouQl7KY2XoVSeSeECYVq/bLJ+bbJNg== +playwright@^1.22.1: + version "1.22.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.22.1.tgz#a41bf55065953a6b31c151433e0f00c9e23c54dd" + integrity sha512-pn4dvphQdL3zTgI+xfqkTL3HK1xbtQz+zjJxzyhwKLDxMNuAH7/NCm75auWbYLyssdnvApznujwrQuJvqXTKUw== dependencies: - playwright-core "=1.16.2" - -pngjs@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" - integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== + playwright-core "1.22.1" prelude-ls@^1.2.1: version "1.2.1" @@ -3278,20 +3182,11 @@ process-on-spawn@^1.0.0: dependencies: fromentries "^1.2.0" -progress@^2.0.0, progress@^2.0.3: +progress@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== -proper-lockfile@^4.1.1: - version "4.1.2" - resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-4.1.2.tgz#c8b9de2af6b2f1601067f98e01ac66baa223141f" - integrity sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA== - dependencies: - graceful-fs "^4.2.4" - retry "^0.12.0" - signal-exit "^3.0.2" - proxy-addr@~2.0.5: version "2.0.6" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" @@ -3300,24 +3195,11 @@ proxy-addr@~2.0.5: forwarded "~0.1.2" ipaddr.js "1.9.1" -proxy-from-env@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== - psl@^1.1.33: version "1.8.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" @@ -3436,11 +3318,6 @@ resolve@^1.9.0: is-core-module "^2.2.0" path-parse "^1.0.6" -retry@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" - integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= - reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" @@ -3618,28 +3495,6 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -smart-buffer@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" - integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== - -socks-proxy-agent@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.1.0.tgz#869cf2d7bd10fea96c7ad3111e81726855e285c3" - integrity sha512-57e7lwCN4Tzt3mXz25VxOErJKXlPfXmkMLnk310v/jwW20jWRVcgsOit+xNkN3eIEdB47GwnfAEBLacZ/wVIKg== - dependencies: - agent-base "^6.0.2" - debug "^4.3.1" - socks "^2.6.1" - -socks@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.1.tgz#989e6534a07cf337deb1b1c94aaa44296520d30e" - integrity sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA== - dependencies: - ip "^1.1.5" - smart-buffer "^4.1.0" - source-list-map@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" @@ -3707,13 +3562,6 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= -stack-utils@^2.0.3: - version "2.0.5" - resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5" - integrity sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA== - dependencies: - escape-string-regexp "^2.0.0" - "statuses@>= 1.5.0 < 2", statuses@~1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" @@ -4396,21 +4244,6 @@ yargs@^15.0.2: y18n "^4.0.0" yargs-parser "^18.1.1" -yauzl@^2.10.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" - integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= - dependencies: - buffer-crc32 "~0.2.3" - fd-slicer "~1.1.0" - -yazl@^2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/yazl/-/yazl-2.5.1.tgz#a3d65d3dd659a5b0937850e8609f22fffa2b5c35" - integrity sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw== - dependencies: - buffer-crc32 "~0.2.3" - yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"