diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index ff113c9baa9..76abbb844ec 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -188,7 +188,7 @@ function _wrapAsStandardKeyboardEvent(handler: (e: IKeyboardEvent) => void): (e: export const addStandardDisposableListener: IAddStandardDisposableListenerSignature = function addStandardDisposableListener(node: HTMLElement, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable { let wrapHandler = handler; - if (type === 'click' || type === 'mousedown') { + if (type === 'click' || type === 'mousedown' || type === 'contextmenu') { wrapHandler = _wrapAsStandardMouseEvent(getWindow(node), handler); } else if (type === 'keydown' || type === 'keypress' || type === 'keyup') { wrapHandler = _wrapAsStandardKeyboardEvent(handler); diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index cbaa5f01d3b..2985f959335 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -250,11 +250,21 @@ export interface IOverlayWidgetPosition { * The position preference for the overlay widget. */ preference: OverlayWidgetPositionPreference | IOverlayWidgetPositionCoordinates | null; + + /** + * When set, stacks with other overlay widgets with the same preference, + * in an order determined by the ordinal value. + */ + stackOridinal?: number; } /** * An overlay widgets renders on top of the text. */ export interface IOverlayWidget { + /** + * Event fired when the widget layout changes. + */ + onDidLayout?: Event; /** * Render this overlay widget in a location where it could overflow the editor's view dom node. */ diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index a803234c4b4..5558cf28cd4 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -634,8 +634,7 @@ export class View extends ViewEventHandler { } public layoutOverlayWidget(widgetData: IOverlayWidgetData): void { - const newPreference = widgetData.position ? widgetData.position.preference : null; - const shouldRender = this._overlayWidgets.setWidgetPosition(widgetData.widget, newPreference); + const shouldRender = this._overlayWidgets.setWidgetPosition(widgetData.widget, widgetData.position); if (shouldRender) { this._scheduleRender(); } diff --git a/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts b/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts index 0953248e2ab..5b3e86a042d 100644 --- a/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts +++ b/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts @@ -5,7 +5,7 @@ import 'vs/css!./overlayWidgets'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; -import { IOverlayWidget, IOverlayWidgetPositionCoordinates, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; +import { IOverlayWidget, IOverlayWidgetPosition, IOverlayWidgetPositionCoordinates, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; import { PartFingerprint, PartFingerprints, ViewPart } from 'vs/editor/browser/view/viewPart'; import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/browser/view/renderingContext'; import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; @@ -17,6 +17,7 @@ import * as dom from 'vs/base/browser/dom'; interface IWidgetData { widget: IOverlayWidget; preference: OverlayWidgetPositionPreference | IOverlayWidgetPositionCoordinates | null; + stack?: number; domNode: FastDomNode; } @@ -109,14 +110,17 @@ export class ViewOverlayWidgets extends ViewPart { this._updateMaxMinWidth(); } - public setWidgetPosition(widget: IOverlayWidget, preference: OverlayWidgetPositionPreference | IOverlayWidgetPositionCoordinates | null): boolean { + public setWidgetPosition(widget: IOverlayWidget, position: IOverlayWidgetPosition | null): boolean { const widgetData = this._widgets[widget.getId()]; - if (widgetData.preference === preference) { + const preference = position ? position.preference : null; + const stack = position?.stackOridinal; + if (widgetData.preference === preference && widgetData.stack === stack) { this._updateMaxMinWidth(); return false; } widgetData.preference = preference; + widgetData.stack = stack; this.setShouldRender(); this._updateMaxMinWidth(); @@ -150,7 +154,7 @@ export class ViewOverlayWidgets extends ViewPart { this._context.viewLayout.setOverlayWidgetsMinWidth(maxMinWidth); } - private _renderWidget(widgetData: IWidgetData): void { + private _renderWidget(widgetData: IWidgetData, stackCoordinates: number[]): void { const domNode = widgetData.domNode; if (widgetData.preference === null) { @@ -158,16 +162,29 @@ export class ViewOverlayWidgets extends ViewPart { return; } - if (widgetData.preference === OverlayWidgetPositionPreference.TOP_RIGHT_CORNER) { - domNode.setTop(0); - domNode.setRight((2 * this._verticalScrollbarWidth) + this._minimapWidth); - } else if (widgetData.preference === OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER) { - const widgetHeight = domNode.domNode.clientHeight; - domNode.setTop((this._editorHeight - widgetHeight - 2 * this._horizontalScrollbarHeight)); - domNode.setRight((2 * this._verticalScrollbarWidth) + this._minimapWidth); + const maxRight = (2 * this._verticalScrollbarWidth) + this._minimapWidth; + if (widgetData.preference === OverlayWidgetPositionPreference.TOP_RIGHT_CORNER || widgetData.preference === OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER) { + if (widgetData.preference === OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER) { + const widgetHeight = domNode.domNode.clientHeight; + domNode.setTop((this._editorHeight - widgetHeight - 2 * this._horizontalScrollbarHeight)); + } else { + domNode.setTop(0); + } + + if (widgetData.stack !== undefined) { + domNode.setTop(stackCoordinates[widgetData.preference]); + stackCoordinates[widgetData.preference] += domNode.domNode.clientWidth; + } else { + domNode.setRight(maxRight); + } } else if (widgetData.preference === OverlayWidgetPositionPreference.TOP_CENTER) { - domNode.setTop(0); domNode.domNode.style.right = '50%'; + if (widgetData.stack !== undefined) { + domNode.setTop(stackCoordinates[OverlayWidgetPositionPreference.TOP_CENTER]); + stackCoordinates[OverlayWidgetPositionPreference.TOP_CENTER] += domNode.domNode.clientHeight; + } else { + domNode.setTop(0); + } } else { const { top, left } = widgetData.preference; const fixedOverflowWidgets = this._context.configuration.options.get(EditorOption.fixedOverflowWidgets); @@ -194,9 +211,12 @@ export class ViewOverlayWidgets extends ViewPart { this._domNode.setWidth(this._editorWidth); const keys = Object.keys(this._widgets); + const stackCoordinates = Array.from({ length: OverlayWidgetPositionPreference.TOP_CENTER + 1 }, () => 0); + keys.sort((a, b) => (this._widgets[a].stack || 0) - (this._widgets[b].stack || 0)); + for (let i = 0, len = keys.length; i < len; i++) { const widgetId = keys[i]; - this._renderWidget(this._widgets[widgetId]); + this._renderWidget(this._widgets[widgetId], stackCoordinates); } } } diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts index fbaa5ffcabb..5fdc32e47ee 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts @@ -26,7 +26,8 @@ import { HideUnchangedRegionsFeature } from 'vs/editor/browser/widget/diffEditor import { MovedBlocksLinesFeature } from 'vs/editor/browser/widget/diffEditor/features/movedBlocksLinesFeature'; import { OverviewRulerFeature } from 'vs/editor/browser/widget/diffEditor/features/overviewRulerFeature'; import { RevertButtonsFeature } from 'vs/editor/browser/widget/diffEditor/features/revertButtonsFeature'; -import { CSSStyle, ObservableElementSizeObserver, applyStyle, applyViewZones, bindContextKey, readHotReloadableExport, translatePosition } from 'vs/editor/browser/widget/diffEditor/utils'; +import { CSSStyle, ObservableElementSizeObserver, applyStyle, applyViewZones, readHotReloadableExport, translatePosition } from 'vs/editor/browser/widget/diffEditor/utils'; +import { bindContextKey } from 'vs/platform/observable/common/platformObservableUtils'; import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IDimension } from 'vs/editor/common/core/dimension'; import { Position } from 'vs/editor/common/core/position'; diff --git a/src/vs/editor/browser/widget/diffEditor/utils.ts b/src/vs/editor/browser/widget/diffEditor/utils.ts index 65705cb8097..3b968353291 100644 --- a/src/vs/editor/browser/widget/diffEditor/utils.ts +++ b/src/vs/editor/browser/widget/diffEditor/utils.ts @@ -8,7 +8,7 @@ import { findLast } from 'vs/base/common/arraysFind'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { isHotReloadEnabled, registerHotReloadHandler } from 'vs/base/common/hotReload'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IObservable, IReader, ISettableObservable, autorun, autorunHandleChanges, autorunOpts, autorunWithStore, observableFromEvent, observableSignalFromEvent, observableValue, transaction } from 'vs/base/common/observable'; +import { IObservable, IReader, ISettableObservable, autorun, autorunHandleChanges, autorunOpts, autorunWithStore, observableSignalFromEvent, observableValue, transaction } from 'vs/base/common/observable'; import { ElementSizeObserver } from 'vs/editor/browser/config/elementSizeObserver'; import { ICodeEditor, IOverlayWidget, IViewZone } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; @@ -16,8 +16,6 @@ import { Range } from 'vs/editor/common/core/range'; import { DetailedLineRangeMapping } from 'vs/editor/common/diff/rangeMapping'; import { IModelDeltaDecoration } from 'vs/editor/common/model'; import { TextLength } from 'vs/editor/common/core/textLength'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyValue, RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; export function joinCombine(arr1: readonly T[], arr2: readonly T[], keySelector: (val: T) => number, combine: (v1: T, v2: T) => T): readonly T[] { if (arr1.length === 0) { @@ -89,17 +87,6 @@ export function prependRemoveOnDispose(parent: HTMLElement, child: HTMLElement) }); } -export function observableConfigValue(key: string, defaultValue: T, configurationService: IConfigurationService): IObservable { - return observableFromEvent( - (handleChange) => configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(key)) { - handleChange(e); - } - }), - () => configurationService.getValue(key) ?? defaultValue, - ); -} - export class ObservableElementSizeObserver extends Disposable { private readonly elementSizeObserver: ElementSizeObserver; @@ -440,13 +427,6 @@ function lengthBetweenPositions(position1: Position, position2: Position): TextL } } -export function bindContextKey(key: RawContextKey, service: IContextKeyService, computeValue: (reader: IReader) => T): IDisposable { - const boundKey = key.bindTo(service); - return autorunOpts({ debugName: () => `Set Context Key "${key.key}"` }, reader => { - boundKey.set(computeValue(reader)); - }); -} - export function filterWithPrevious(arr: T[], filter: (cur: T, prev: T | undefined) => boolean): T[] { let prev: T | undefined; return arr.filter(cur => { diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css b/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css index 8afc9c241cf..3bc52c6c915 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScroll.css @@ -64,6 +64,7 @@ box-shadow: var(--vscode-editorStickyScroll-shadow) 0 3px 2px -2px; z-index: 4; background-color: var(--vscode-editorStickyScroll-background); + right: initial !important; } .monaco-editor .sticky-widget.peek { diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts index bdcaafb4891..d0e8da4b17a 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts @@ -9,7 +9,7 @@ import { equals } from 'vs/base/common/arrays'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/base/common/themables'; import 'vs/css!./stickyScroll'; -import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; import { getColumnOfNodeOffset } from 'vs/editor/browser/viewParts/lines/viewLine'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget'; import { EditorLayoutInfo, EditorOption, RenderLineNumbersType } from 'vs/editor/common/config/editorOptions'; @@ -387,7 +387,8 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { getPosition(): IOverlayWidgetPosition | null { return { - preference: null + preference: OverlayWidgetPositionPreference.TOP_CENTER, + stackOridinal: 10, }; } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index e2c5bd2ea0b..b9c7cbd73ab 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -5387,12 +5387,21 @@ declare namespace monaco.editor { * The position preference for the overlay widget. */ preference: OverlayWidgetPositionPreference | IOverlayWidgetPositionCoordinates | null; + /** + * When set, stacks with other overlay widgets with the same preference, + * in an order determined by the ordinal value. + */ + stackOridinal?: number; } /** * An overlay widgets renders on top of the text. */ export interface IOverlayWidget { + /** + * Event fired when the widget layout changes. + */ + onDidLayout?: IEvent; /** * Render this overlay widget in a location where it could overflow the editor's view dom node. */ diff --git a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts index ba277dbc24e..3ef15eb1b3a 100644 --- a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts +++ b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts @@ -8,12 +8,13 @@ import { getStructuralKey } from 'vs/base/common/equals'; import { Event, IValueWithChangeEvent } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { FileAccess } from 'vs/base/common/network'; -import { derived, IObservable, observableFromEvent } from 'vs/base/common/observable'; +import { derived, observableFromEvent } from 'vs/base/common/observable'; import { ValueWithChangeEventFromObservable } from 'vs/base/common/observableInternal/utils'; import { localize } from 'vs/nls'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; export const IAccessibilitySignalService = createDecorator('accessibilitySignalService'); @@ -201,7 +202,7 @@ export class AccessibilitySignalService extends Disposable implements IAccessibi private readonly _signalConfigValue = new CachedFunction((signal: AccessibilitySignal) => observableConfigValue<{ sound: EnabledState; announcement: EnabledState; - }>(signal.settingsKey, this.configurationService)); + }>(signal.settingsKey, { sound: 'off', announcement: 'off' }, this.configurationService)); private readonly _signalEnabledState = new CachedFunction( { getCacheKey: getStructuralKey }, @@ -589,13 +590,3 @@ export class AccessibilitySignal { }); } -export function observableConfigValue(key: string, configurationService: IConfigurationService): IObservable { - return observableFromEvent( - (handleChange) => configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(key)) { - handleChange(e); - } - }), - () => configurationService.getValue(key), - ); -} diff --git a/src/vs/platform/observable/common/platformObservableUtils.ts b/src/vs/platform/observable/common/platformObservableUtils.ts new file mode 100644 index 00000000000..096993beb80 --- /dev/null +++ b/src/vs/platform/observable/common/platformObservableUtils.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable } from 'vs/base/common/lifecycle'; +import { autorunOpts, IObservable, IReader, observableFromEvent } from 'vs/base/common/observable'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ContextKeyValue, RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; + +/** Creates an observable update when a configuration key updates. */ +export function observableConfigValue(key: string, defaultValue: T, configurationService: IConfigurationService): IObservable { + return observableFromEvent( + (handleChange) => configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(key)) { + handleChange(e); + } + }), + () => configurationService.getValue(key) ?? defaultValue + ); +} + +/** Update the configuration key with a value derived from observables. */ +export function bindContextKey(key: RawContextKey, service: IContextKeyService, computeValue: (reader: IReader) => T): IDisposable { + const boundKey = key.bindTo(service); + return autorunOpts({ debugName: () => `Set Context Key "${key.key}"` }, reader => { + boundKey.set(computeValue(reader)); + }); +} + diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts index 278615a60b2..56760afd5eb 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/diffComputer.ts @@ -11,7 +11,7 @@ import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange'; import { DetailedLineRangeMapping, RangeMapping } from 'vs/workbench/contrib/mergeEditor/browser/model/mapping'; -import { observableConfigValue } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { LineRange as DiffLineRange } from 'vs/editor/common/core/lineRange'; export interface IMergeDiffComputer { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/utils.ts b/src/vs/workbench/contrib/mergeEditor/browser/utils.ts index c085272472c..5ac6522a14b 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/utils.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/utils.ts @@ -6,10 +6,9 @@ import { ArrayQueue, CompareResult } from 'vs/base/common/arrays'; import { onUnexpectedError } from 'vs/base/common/errors'; import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IObservable, autorunOpts, observableFromEvent } from 'vs/base/common/observable'; +import { IObservable, autorunOpts } from 'vs/base/common/observable'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget'; import { IModelDeltaDecoration } from 'vs/editor/common/model'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; export function setStyle( @@ -156,13 +155,3 @@ export class PersistentStore { } } -export function observableConfigValue(key: string, defaultValue: T, configurationService: IConfigurationService): IObservable { - return observableFromEvent( - (handleChange) => configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(key)) { - handleChange(e); - } - }), - () => configurationService.getValue(key) ?? defaultValue, - ); -} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts index 8cd243e3777..29af08fbafe 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView.ts @@ -20,7 +20,8 @@ import { MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { DEFAULT_EDITOR_MAX_DIMENSIONS, DEFAULT_EDITOR_MIN_DIMENSIONS } from 'vs/workbench/browser/parts/editor/editor'; -import { observableConfigValue, setStyle } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { setStyle } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { MergeEditorViewModel } from 'vs/workbench/contrib/mergeEditor/browser/view/viewModel'; export abstract class CodeEditorView extends Disposable { diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts index 3609b0046fa..bec1dec9481 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/mergeEditor.ts @@ -39,7 +39,8 @@ import { readTransientState, writeTransientState } from 'vs/workbench/contrib/co import { MergeEditorInput } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInput'; import { IMergeEditorInputModel } from 'vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel'; import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel'; -import { deepMerge, observableConfigValue, PersistentStore, thenIfNotDisposed } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { deepMerge, PersistentStore, thenIfNotDisposed } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { BaseCodeEditorView } from 'vs/workbench/contrib/mergeEditor/browser/view/editors/baseCodeEditorView'; import { ScrollSynchronizer } from 'vs/workbench/contrib/mergeEditor/browser/view/scrollSynchronizer'; import { MergeEditorViewModel } from 'vs/workbench/contrib/mergeEditor/browser/view/viewModel'; diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts index ca8a00e6b56..1c0f093dc27 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts @@ -15,7 +15,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange'; import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel'; import { InputNumber, ModifiedBaseRange, ModifiedBaseRangeState } from 'vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange'; -import { observableConfigValue } from 'vs/workbench/contrib/mergeEditor/browser/utils'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { BaseCodeEditorView } from 'vs/workbench/contrib/mergeEditor/browser/view/editors/baseCodeEditorView'; import { CodeEditorView } from 'vs/workbench/contrib/mergeEditor/browser/view/editors/codeEditorView'; import { InputCodeEditorView } from 'vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView'; diff --git a/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts b/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts index a6ce0ce1d1c..9040875421a 100644 --- a/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/codeCoverageDecorations.ts @@ -4,16 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; +import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; +import { Action } from 'vs/base/common/actions'; import { mapFindFirst } from 'vs/base/common/arraysFind'; import { assert, assertNever } from 'vs/base/common/assert'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Lazy } from 'vs/base/common/lazy'; -import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { autorun, derived, observableFromEvent, observableValue } from 'vs/base/common/observable'; import { ThemeIcon } from 'vs/base/common/themables'; -import { ICodeEditor, MouseTargetType } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, MouseTargetType, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; @@ -21,20 +25,24 @@ import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { IModelDecorationOptions, InjectedTextCursorStops, InjectedTextOptions, ITextModel } from 'vs/editor/common/model'; import { localize, localize2 } from 'vs/nls'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; -import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ILogService } from 'vs/platform/log/common/log'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { IQuickInputService, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; import * as coverUtils from 'vs/workbench/contrib/testing/browser/codeCoverageDisplayUtils'; -import { testingCoverageMissingBranch } from 'vs/workbench/contrib/testing/browser/icons'; +import { testingCoverageIcon, testingCoverageMissingBranch, testingFilterIcon, testingRerunIcon } from 'vs/workbench/contrib/testing/browser/icons'; import { ManagedTestCoverageBars } from 'vs/workbench/contrib/testing/browser/testCoverageBars'; import { getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; +import { TestCommandId } from 'vs/workbench/contrib/testing/common/constants'; import { FileCoverage } from 'vs/workbench/contrib/testing/common/testCoverage'; import { ITestCoverageService } from 'vs/workbench/contrib/testing/common/testCoverageService'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; +import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { CoverageDetails, DetailType, IDeclarationCoverage, IStatementCoverage } from 'vs/workbench/contrib/testing/common/testTypes'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; @@ -52,7 +60,7 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri private loadingCancellation?: CancellationTokenSource; private readonly displayedStore = this._register(new DisposableStore()); private readonly hoveredStore = this._register(new DisposableStore()); - private readonly summaryWidget: Lazy; + private readonly summaryWidget: Lazy; private decorationIds = new Map this._register(instantiationService.createInstance(CoverageSummaryWidget, this.editor))); + this.summaryWidget = new Lazy(() => this._register(instantiationService.createInstance(CoverageToolbarWidget, this.editor))); const modelObs = observableFromEvent(editor.onDidChangeModel, () => editor.getModel()); const configObs = observableFromEvent(editor.onDidChangeConfiguration, i => i); @@ -108,6 +117,16 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri } })); + const toolbarEnabled = observableConfigValue(TestingConfigKeys.CoverageToolbarEnabled, true, configurationService); + this._register(autorun(reader => { + const c = fileCoverage.read(reader); + if (c && toolbarEnabled.read(reader)) { + this.summaryWidget.value.setCoverage(c); + } else { + this.summaryWidget.rawValue?.setCoverage(undefined); + } + })); + this._register(autorun(reader => { const c = fileCoverage.read(reader); if (c) { @@ -245,7 +264,6 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri } this.displayedStore.clear(); - this.summaryWidget.value.setCoverage(coverage); model.changeDecorations(e => { for (const detailRange of details.ranges) { @@ -309,8 +327,6 @@ export class CodeCoverageDecorations extends Disposable implements IEditorContri }); this.displayedStore.add(toDisposable(() => { - this.summaryWidget.value.setCoverage(undefined); - model.changeDecorations(e => { for (const decoration of this.decorationIds.keys()) { e.removeDecoration(decoration); @@ -513,42 +529,81 @@ function wrapName(functionNameOrCode: string) { return wrapInBackticks(functionNameOrCode); } -class CoverageSummaryWidget implements IDisposable { +class CoverageToolbarWidget extends Disposable implements IOverlayWidget { private current: FileCoverage | undefined; private registered = false; - private readonly registration = new DisposableStore(); - + private isRunning = false; + private readonly showStore = this._register(new DisposableStore()); + private readonly actionBar: ActionBar; private readonly _domNode = dom.h('div.coverage-summary-widget', [ dom.h('div', [ dom.h('span.bars@bars'), dom.h('span.stat@stat'), - dom.h('a.toggleInline@toggleInline'), - dom.h('a.perTestFilter@perTestFilter'), + dom.h('span.toolbar@toolbar'), ]), ]); private readonly bars: ManagedTestCoverageBars; - constructor( private readonly editor: ICodeEditor, @IConfigurationService private readonly configurationService: IConfigurationService, @IQuickInputService private readonly quickInputService: IQuickInputService, @ITestCoverageService private readonly testCoverageService: ITestCoverageService, - @IKeybindingService keybindingService: IKeybindingService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, + @ITestService private readonly testService: ITestService, + @IKeybindingService private readonly keybindingService: IKeybindingService, @IInstantiationService instaService: IInstantiationService, ) { - this._domNode.perTestFilter.ariaLabel = this._domNode.perTestFilter.title = coverUtils.labels.clickToChangeFiltering; - this.bars = instaService.createInstance(ManagedTestCoverageBars, { + super(); + + this.bars = this._register(instaService.createInstance(ManagedTestCoverageBars, { compact: false, overall: false, container: this._domNode.bars, - }); + })); - const kb = keybindingService.lookupKeybinding(TOGGLE_INLINE_COMMAND_ID); - if (kb) { - this._domNode.toggleInline.title = `${TOGGLE_INLINE_COMMAND_TEXT} (${kb.getLabel()})`; - } + this.actionBar = this._register(instaService.createInstance(ActionBar, this._domNode.toolbar, { + orientation: ActionsOrientation.HORIZONTAL, + actionViewItemProvider: (action, options) => { + const vm = new CodiconActionViewItem(undefined, action, options); + if (action instanceof ActionWithIcon) { + vm.themeIcon = action.icon; + } + return vm; + } + })); + + + this._register(autorun(reader => { + CodeCoverageDecorations.showInline.read(reader); + this.setActions(); + })); + + this._register(dom.addStandardDisposableListener(this._domNode.root, dom.EventType.CONTEXT_MENU, e => { + this.contextMenuService.showContextMenu({ + menuId: MenuId.StickyScrollContext, + getAnchor: () => e, + }); + })); + } + + /** @inheritdoc */ + public getId(): string { + return 'coverage-summary-widget'; + } + + /** @inheritdoc */ + public getDomNode(): HTMLElement { + return this._domNode.root; + } + + /** @inheritdoc */ + public getPosition(): IOverlayWidgetPosition | null { + return { + preference: OverlayWidgetPositionPreference.TOP_CENTER, + stackOridinal: 9, + }; } public setCoverage(coverage: FileCoverage | undefined) { @@ -556,32 +611,13 @@ class CoverageSummaryWidget implements IDisposable { this.bars.setCoverageInfo(coverage); if (!coverage) { - return this.unregister(); + return this.hide(); } const displayStat = coverUtils.calculateDisplayedStat(coverage, getTestingConfiguration(this.configurationService, TestingConfigKeys.CoveragePercent)); this._domNode.stat.innerText = localize('testing.percentCoverage', '{0} Coverage', coverUtils.displayPercent(displayStat)); - - this._domNode.perTestFilter.classList.toggle('active', !!coverage.isForTest); - if (coverage.isForTest) { - const testItem = coverage.fromResult.getTestById(coverage.isForTest.id.toString()); - assert(!!testItem, 'got coverage for an unreported test'); - this._domNode.perTestFilter.style.display = 'inline'; - this._domNode.perTestFilter.innerText = coverUtils.labels.showingFilterFor(testItem.label); - } else if (coverage.perTestData?.size) { - this._domNode.perTestFilter.style.display = 'inline'; - this._domNode.perTestFilter.innerText = localize('testing.coverageForTestAvailable', "{0} test(s) in this file", coverage.perTestData.size); - } else { - this._domNode.perTestFilter.style.display = 'none'; - } - - this.register(); - } - - /** @inheritdoc */ - public dispose() { - this.unregister(); - this.bars.dispose(); + this.setActions(); + this.show(); } private filterTest() { @@ -603,65 +639,144 @@ class CoverageSummaryWidget implements IDisposable { ...tests.map(item => ({ label: coverUtils.getLabelForItem(result, item.isForTest!.id, commonPrefix), description: coverUtils.labels.percentCoverage(item.tpc), item })), ]; + // These handle the behavior that reveals the start of coverage when the + // user picks from the quickpick. Scroll position is restored if the user + // exits without picking an item, or picks "all tets". + const scrollTop = this.editor.getScrollTop(); + const revealScrollCts = new MutableDisposable(); + this.quickInputService.pick(items, { activeItem: items.find((item): item is TItem => 'item' in item && item.item === this.current), placeHolder: coverUtils.labels.pickShowCoverage, onDidFocus: (entry) => { - this.testCoverageService.filterToTest.set(entry.item?.isForTest!.id, undefined); + if (!entry.item) { + revealScrollCts.clear(); + this.editor.setScrollTop(scrollTop); + this.testCoverageService.filterToTest.set(undefined, undefined); + } else { + const cts = revealScrollCts.value = new CancellationTokenSource(); + entry.item.details(cts.token).then( + details => { + const first = details.find(d => d.type === DetailType.Statement); + if (!cts.token.isCancellationRequested && first) { + this.editor.revealLineNearTop(first.location instanceof Position ? first.location.lineNumber : first.location.startLineNumber); + } + }, + () => { /* ignored */ } + ); + this.testCoverageService.filterToTest.set(entry.item.isForTest!.id, undefined); + } }, }).then(selected => { + if (!selected) { + this.editor.setScrollTop(scrollTop); + } + + revealScrollCts.dispose(); this.testCoverageService.filterToTest.set(selected ? selected.item?.isForTest!.id : previousSelection, undefined); }); } - private register() { + private setActions() { + this.actionBar.clear(); + const coverage = this.current; + if (!coverage) { + return; + } + + const toggleAction = new ActionWithIcon( + 'toggleInline', + CodeCoverageDecorations.showInline.get() + ? localize('testing.hideInlineCoverage', 'Hide Inline Coverage') + : localize('testing.showInlineCoverage', 'Show Inline Coverage'), + testingCoverageIcon, + undefined, + () => CodeCoverageDecorations.showInline.set(!CodeCoverageDecorations.showInline.get(), undefined), + ); + + const kb = this.keybindingService.lookupKeybinding(TOGGLE_INLINE_COMMAND_ID); + if (kb) { + toggleAction.tooltip = `${TOGGLE_INLINE_COMMAND_TEXT} (${kb.getLabel()})`; + } + + this.actionBar.push(toggleAction); + + if (coverage.isForTest) { + const testItem = coverage.fromResult.getTestById(coverage.isForTest.id.toString()); + assert(!!testItem, 'got coverage for an unreported test'); + this.actionBar.push(new ActionWithIcon('perTestFilter', + coverUtils.labels.showingFilterFor(testItem.label), + testingFilterIcon, + undefined, + () => this.filterTest(), + )); + } else if (coverage.perTestData?.size) { + this.actionBar.push(new ActionWithIcon('perTestFilter', + localize('testing.coverageForTestAvailable', "{0} test(s) in this file", coverage.perTestData.size), + testingFilterIcon, + undefined, + () => this.filterTest(), + )); + } + + this.actionBar.push(new ActionWithIcon( + 'rerun', + localize('testing.rerun', 'Rerun'), + testingRerunIcon, + !this.isRunning, + () => this.rerunTest() + )); + } + + private show() { if (this.registered) { return; } this.registered = true; - let viewZoneId: string; + const ds = this.showStore; + + this.editor.addOverlayWidget(this); this.editor.changeViewZones(accessor => { - viewZoneId = accessor.addZone({ + viewZoneId = accessor.addZone({ // make space for the widget afterLineNumber: 0, afterColumn: 0, - domNode: this._domNode.root, + domNode: document.createElement('div'), heightInPx: 30, ordinal: -1, // show before code lenses }); }); - this.registration.add(toDisposable(() => { + ds.add(toDisposable(() => { + this.registered = false; + this.editor.removeOverlayWidget(this); this.editor.changeViewZones(accessor => { accessor.removeZone(viewZoneId); }); - this.registered = false; })); - this.registration.add(dom.addStandardDisposableListener(this._domNode.perTestFilter, 'click', () => { - this.filterTest(); - })); - - this.registration.add(this.configurationService.onDidChangeConfiguration(e => { + ds.add(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(TestingConfigKeys.CoverageBarThresholds) || e.affectsConfiguration(TestingConfigKeys.CoveragePercent)) { this.setCoverage(this.current); } })); - - this.registration.add(dom.addStandardDisposableListener(this._domNode.toggleInline, 'click', () => { - CodeCoverageDecorations.showInline.set(!CodeCoverageDecorations.showInline.get(), undefined); - })); - - this.registration.add(autorun(reader => { - this._domNode.toggleInline.innerText = CodeCoverageDecorations.showInline.read(reader) - ? localize('testing.hideInlineCoverage', 'Hide Inline Coverage') - : localize('testing.showInlineCoverage', 'Show Inline Coverage'); - })); } - private unregister() { - this.registration.clear(); + private rerunTest() { + const current = this.current; + if (current) { + this.isRunning = true; + this.setActions(); + this.testService.runResolvedTests(current.fromResult.request).finally(() => { + this.isRunning = false; + this.setActions(); + }); + } + } + + private hide() { + this.showStore.clear(); } } @@ -683,3 +798,47 @@ registerAction2(class ToggleInlineCoverage extends Action2 { CodeCoverageDecorations.showInline.set(!CodeCoverageDecorations.showInline.get(), undefined); } }); + +registerAction2(class ToggleCoverageToolbar extends Action2 { + constructor() { + super({ + id: TestCommandId.CoverageToggleToolbar, + title: localize2('testing.toggleToolbarTitle', "Toggle Coverage Toolbar"), + metadata: { + description: localize2('testing.toggleToolbarDesc', 'Toggle the sticky coverage bar in the editor.') + }, + category: Categories.Test, + toggled: { + condition: TestingContextKeys.coverageToolbarEnabled, + title: localize('cmd.toggle2', "Toggle Coverage Toolbar"), + }, + menu: [ + { id: MenuId.CommandPalette, when: TestingContextKeys.isTestCoverageOpen }, + { id: MenuId.StickyScrollContext, when: TestingContextKeys.isTestCoverageOpen }, + ] + }); + } + + run(accessor: ServicesAccessor): void { + const config = accessor.get(IConfigurationService); + const value = getTestingConfiguration(config, TestingConfigKeys.CoverageToolbarEnabled); + config.updateValue(TestingConfigKeys.CoverageToolbarEnabled, !value); + } +}); + +class ActionWithIcon extends Action { + constructor(id: string, title: string, public readonly icon: ThemeIcon, enabled: boolean | undefined, run: () => void) { + super(id, title, undefined, enabled, run); + } +} + +class CodiconActionViewItem extends ActionViewItem { + + public themeIcon?: ThemeIcon; + + protected override updateLabel(): void { + if (this.options.label && this.label && this.themeIcon) { + dom.reset(this.label, renderIcon(this.themeIcon), this.action.label); + } + } +} diff --git a/src/vs/workbench/contrib/testing/browser/media/testing.css b/src/vs/workbench/contrib/testing/browser/media/testing.css index 4271a033ccc..3068f3db57f 100644 --- a/src/vs/workbench/contrib/testing/browser/media/testing.css +++ b/src/vs/workbench/contrib/testing/browser/media/testing.css @@ -404,50 +404,50 @@ .coverage-summary-widget { color: var(--vscode-editor-foreground); z-index: 1; - line-height: 25px; + background: var(--vscode-editor-background); + left: 0; + width: 100%; + box-shadow: var(--vscode-editorStickyScroll-shadow) 0 3px 2px -2px; > div { display: flex; align-items: center; - border-bottom: 1px solid var(--vscode-menu-border); + padding: 0 22px; + height: 25px; } - .toggleInline, .perTestFilter { - border-left: 1px solid var(--vscode-menu-border); - padding: 0 6px; - } - - .stat, .toggleInline { - padding-right: 6px; - } - - > span, > a { - display: inline; + .btn { position: relative; - padding: 0 6px; + margin: 0 4px; + padding: 0 4px; &:first-child { - padding-left: 0; + margin-left: 0; } &:last-child { - padding-right: 0; + margin-right: 0; } } - - a { - color: var(--vscode-textLink-foreground); - cursor: pointer; + .stat, .action-label { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + margin: 0 3px; } - a:hover { - color: var(--vscode-textLink-activeForeground); + .action-label { + display: flex; + align-items: center; + font-size: 13px; + padding: 0 4px; + + .codicon { + margin-right: 4px; + } } - .toggleInline, .perTestFilter { - border-left: 1px solid var(--vscode-menu-border); - } } .test-coverage-tree-per-test-switcher { diff --git a/src/vs/workbench/contrib/testing/browser/testCoverageView.ts b/src/vs/workbench/contrib/testing/browser/testCoverageView.ts index ff7bda28075..841903f8096 100644 --- a/src/vs/workbench/contrib/testing/browser/testCoverageView.ts +++ b/src/vs/workbench/contrib/testing/browser/testCoverageView.ts @@ -23,6 +23,7 @@ import { URI } from 'vs/base/common/uri'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { localize, localize2 } from 'vs/nls'; +import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -685,6 +686,7 @@ registerAction2(class TestCoverageChangePerTestFilterAction extends Action2 { constructor() { super({ id: TestCommandId.CoverageFilterToTest, + category: Categories.Test, title: localize2('testing.changeCoverageFilter', 'Filter Coverage by Test...'), precondition: TestingContextKeys.hasPerTestCoverage, f1: true, diff --git a/src/vs/workbench/contrib/testing/common/configuration.ts b/src/vs/workbench/contrib/testing/common/configuration.ts index 1bbc290e9cd..ec9e50d67f0 100644 --- a/src/vs/workbench/contrib/testing/common/configuration.ts +++ b/src/vs/workbench/contrib/testing/common/configuration.ts @@ -23,6 +23,7 @@ export const enum TestingConfigKeys { CoveragePercent = 'testing.displayedCoveragePercent', ShowCoverageInExplorer = 'testing.showCoverageInExplorer', CoverageBarThresholds = 'testing.coverageBarThresholds', + CoverageToolbarEnabled = 'testing.coverageToolbarEnabled', } export const enum AutoOpenTesting { @@ -190,6 +191,11 @@ export const testingConfiguration: IConfigurationNode = { green: { type: 'number', minimum: 0, maximum: 100, default: 90 }, }, }, + [TestingConfigKeys.CoverageToolbarEnabled]: { + description: localize('testing.coverageToolbarEnabled', 'Controls whether the coverage toolbar is shown in the editor.'), + type: 'boolean', + default: false, // todo@connor4312: disabled by default until UI sync + }, } }; @@ -214,6 +220,7 @@ export interface ITestingConfiguration { [TestingConfigKeys.CoveragePercent]: TestingDisplayedCoveragePercent; [TestingConfigKeys.ShowCoverageInExplorer]: boolean; [TestingConfigKeys.CoverageBarThresholds]: ITestingCoverageBarThresholds; + [TestingConfigKeys.CoverageToolbarEnabled]: boolean; } export const getTestingConfiguration = (config: IConfigurationService, key: K) => config.getValue(key); diff --git a/src/vs/workbench/contrib/testing/common/constants.ts b/src/vs/workbench/contrib/testing/common/constants.ts index 2dfd8cf55c2..e879003b6a1 100644 --- a/src/vs/workbench/contrib/testing/common/constants.ts +++ b/src/vs/workbench/contrib/testing/common/constants.ts @@ -68,6 +68,7 @@ export const enum TestCommandId { CoverageFilterToTest = 'testing.coverageFilterToTest', CoverageLastRun = 'testing.coverageLastRun', CoverageSelectedAction = 'testing.coverageSelected', + CoverageToggleToolbar = 'testing.coverageToggleToolbar', CoverageViewChangeSorting = 'testing.coverageViewChangeSorting', DebugAction = 'testing.debug', DebugAllAction = 'testing.debugAll', diff --git a/src/vs/workbench/contrib/testing/common/testCoverageService.ts b/src/vs/workbench/contrib/testing/common/testCoverageService.ts index 1336f748f86..99e86e86a95 100644 --- a/src/vs/workbench/contrib/testing/common/testCoverageService.ts +++ b/src/vs/workbench/contrib/testing/common/testCoverageService.ts @@ -6,8 +6,11 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { IObservable, ISettableObservable, observableValue, transaction } from 'vs/base/common/observable'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { bindContextKey, observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; +import { TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; import { Testing } from 'vs/workbench/contrib/testing/common/constants'; import { TestCoverage } from 'vs/workbench/contrib/testing/common/testCoverage'; import { TestId } from 'vs/workbench/contrib/testing/common/testId'; @@ -45,8 +48,6 @@ export interface ITestCoverageService { export class TestCoverageService extends Disposable implements ITestCoverageService { declare readonly _serviceBrand: undefined; - private readonly _isOpenKey: IContextKey; - private readonly _hasPerTestCoverage: IContextKey; private readonly lastOpenCts = this._register(new MutableDisposable()); public readonly selected = observableValue('testCoverage', undefined); @@ -55,11 +56,29 @@ export class TestCoverageService extends Disposable implements ITestCoverageServ constructor( @IContextKeyService contextKeyService: IContextKeyService, @ITestResultService resultService: ITestResultService, + @IConfigurationService configService: IConfigurationService, @IViewsService private readonly viewsService: IViewsService, ) { super(); - this._isOpenKey = TestingContextKeys.isTestCoverageOpen.bindTo(contextKeyService); - this._hasPerTestCoverage = TestingContextKeys.hasPerTestCoverage.bindTo(contextKeyService); + + const toolbarConfig = observableConfigValue(TestingConfigKeys.CoverageToolbarEnabled, true, configService); + this._register(bindContextKey( + TestingContextKeys.coverageToolbarEnabled, + contextKeyService, + reader => toolbarConfig.read(reader), + )); + + this._register(bindContextKey( + TestingContextKeys.isTestCoverageOpen, + contextKeyService, + reader => !!this.selected.read(reader), + )); + + this._register(bindContextKey( + TestingContextKeys.hasPerTestCoverage, + contextKeyService, + reader => !!this.selected.read(reader)?.perTestCoverageIDs.size, + )); this._register(resultService.onResultsChanged(evt => { if ('completed' in evt) { @@ -92,8 +111,6 @@ export class TestCoverageService extends Disposable implements ITestCoverageServ this.filterToTest.set(undefined, tx); this.selected.set(coverage, tx); }); - this._isOpenKey.set(true); - this._hasPerTestCoverage.set(coverage.perTestCoverageIDs.size > 0); if (focus && !cts.token.isCancellationRequested) { this.viewsService.openView(Testing.CoverageViewId, true); @@ -102,8 +119,6 @@ export class TestCoverageService extends Disposable implements ITestCoverageServ /** @inheritdoc */ public closeCoverage() { - this._isOpenKey.set(false); - this._hasPerTestCoverage.set(false); this.selected.set(undefined, undefined); } } diff --git a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts index 7878be0ec9e..2c3d0b8c79f 100644 --- a/src/vs/workbench/contrib/testing/common/testingContextKeys.ts +++ b/src/vs/workbench/contrib/testing/common/testingContextKeys.ts @@ -23,6 +23,7 @@ export namespace TestingContextKeys { export const activeEditorHasTests = new RawContextKey('testing.activeEditorHasTests', false, { type: 'boolean', description: localize('testing.activeEditorHasTests', 'Indicates whether any tests are present in the current editor') }); export const isTestCoverageOpen = new RawContextKey('testing.isTestCoverageOpen', false, { type: 'boolean', description: localize('testing.isTestCoverageOpen', 'Indicates whether a test coverage report is open') }); export const hasPerTestCoverage = new RawContextKey('testing.hasPerTestCoverage', false, { type: 'boolean', description: localize('testing.hasPerTestCoverage', 'Indicates whether per-test coverage is available') }); + export const coverageToolbarEnabled = new RawContextKey('testing.coverageToolbarEnabled', true, { type: 'boolean', description: localize('testing.coverageToolbarEnabled', 'Indicates whether the coverage toolbar is enabled') }); export const capabilityToContextKey: { [K in TestRunProfileBitset]: RawContextKey } = { [TestRunProfileBitset.Run]: hasRunnableTests, diff --git a/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts b/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts index 850b58e1e6c..3695379f0e9 100644 --- a/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts +++ b/src/vs/workbench/services/textMate/browser/backgroundTokenization/textMateWorkerTokenizerController.ts @@ -5,7 +5,7 @@ import { importAMDNodeModule } from 'vs/amdX'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IObservable, autorun, keepObserved, observableFromEvent } from 'vs/base/common/observable'; +import { IObservable, autorun, keepObserved } from 'vs/base/common/observable'; import { countEOL } from 'vs/editor/common/core/eolCounter'; import { LineRange } from 'vs/editor/common/core/lineRange'; import { Range } from 'vs/editor/common/core/range'; @@ -15,6 +15,7 @@ import { TokenizationStateStore } from 'vs/editor/common/model/textModelTokens'; import { IModelContentChange, IModelContentChangedEvent } from 'vs/editor/common/textModelEvents'; import { ContiguousMultilineTokensBuilder } from 'vs/editor/common/tokens/contiguousMultilineTokensBuilder'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { observableConfigValue } from 'vs/platform/observable/common/platformObservableUtils'; import { ArrayEdit, MonotonousIndexTransformer, SingleArrayEdit } from 'vs/workbench/services/textMate/browser/arrayOperation'; import type { StateDeltas, TextMateTokenizationWorker } from 'vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateTokenizationWorker.worker'; import type { applyStateStackDiff, StateStack } from 'vscode-textmate'; @@ -237,13 +238,3 @@ function changesToString(changes: IModelContentChange[]): string { return changes.map(c => Range.lift(c.range).toString() + ' => ' + c.text).join(' & '); } -function observableConfigValue(key: string, defaultValue: T, configurationService: IConfigurationService): IObservable { - return observableFromEvent( - (handleChange) => configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(key)) { - handleChange(e); - } - }), - () => configurationService.getValue(key) ?? defaultValue, - ); -}