From fad711d599fd4913d3ead40113e7aa8c4037a52a Mon Sep 17 00:00:00 2001 From: SteVen Batten Date: Thu, 19 Mar 2020 01:27:01 -0400 Subject: [PATCH 01/44] invert feedback --- src/vs/workbench/browser/media/part.css | 13 ++++++++ .../parts/activitybar/activitybarPart.ts | 24 +-------------- .../activitybar/media/activitybarpart.css | 13 +++----- .../browser/parts/editor/editorPart.ts | 15 ++++++++++ .../browser/parts/panel/panelPart.ts | 30 ++----------------- .../browser/parts/sidebar/sidebarPart.ts | 28 ++--------------- 6 files changed, 38 insertions(+), 85 deletions(-) diff --git a/src/vs/workbench/browser/media/part.css b/src/vs/workbench/browser/media/part.css index 89dd7fe4c9b..e77d77ec0d0 100644 --- a/src/vs/workbench/browser/media/part.css +++ b/src/vs/workbench/browser/media/part.css @@ -8,6 +8,19 @@ overflow: hidden; } +.monaco-workbench .part > .drop-block-overlay { + visibility: hidden; /* use visibility to ensure transitions */ + transition-property: opacity; + transition-timing-function: linear; + transition-duration: 250ms; + width: 100%; + height: 100%; + position: absolute; + top: 0; + opacity: 0; + pointer-events: none; +} + .monaco-workbench .part > .title { display: none; /* Parts have to opt in to show title area */ } diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index e35fcf6f990..742c593e180 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -17,7 +17,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IDisposable, toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; import { ToggleActivityBarVisibilityAction, ToggleMenuBarAction } from 'vs/workbench/browser/actions/layoutActions'; import { IThemeService, IColorTheme } from 'vs/platform/theme/common/themeService'; -import { ACTIVITY_BAR_BACKGROUND, ACTIVITY_BAR_BORDER, ACTIVITY_BAR_FOREGROUND, ACTIVITY_BAR_ACTIVE_BORDER, ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND, ACTIVITY_BAR_DRAG_AND_DROP_BACKGROUND, ACTIVITY_BAR_INACTIVE_FOREGROUND, ACTIVITY_BAR_ACTIVE_BACKGROUND, EDITOR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; +import { ACTIVITY_BAR_BACKGROUND, ACTIVITY_BAR_BORDER, ACTIVITY_BAR_FOREGROUND, ACTIVITY_BAR_ACTIVE_BORDER, ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND, ACTIVITY_BAR_DRAG_AND_DROP_BACKGROUND, ACTIVITY_BAR_INACTIVE_FOREGROUND, ACTIVITY_BAR_ACTIVE_BACKGROUND } from 'vs/workbench/common/theme'; import { contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { CompositeBar, ICompositeBarItem, CompositeDragAndDrop } from 'vs/workbench/browser/parts/compositeBar'; import { Dimension, addClass, removeNode } from 'vs/base/browser/dom'; @@ -39,7 +39,6 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { getMenuBarVisibility } from 'vs/platform/windows/common/windows'; import { isWeb } from 'vs/base/common/platform'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { CompositeDragAndDropObserver } from 'vs/workbench/browser/dnd'; interface ICachedViewlet { id: string; @@ -298,10 +297,6 @@ export class ActivitybarPart extends Part implements IActivityBarService { createContentArea(parent: HTMLElement): HTMLElement { this.element = parent; - const overlay = document.createElement('div'); - addClass(overlay, 'drag-overlay'); - parent.appendChild(overlay); - this.content = document.createElement('div'); addClass(this.content, 'content'); parent.appendChild(this.content); @@ -321,23 +316,6 @@ export class ActivitybarPart extends Part implements IActivityBarService { this.createGlobalActivityActionBar(globalActivities); - CompositeDragAndDropObserver.INSTANCE.registerTarget(this.element, { - onDragStart: e => { - overlay.style.backgroundColor = this.theme.getColor(EDITOR_DRAG_AND_DROP_BACKGROUND, true)?.toString() || ''; - overlay.style.opacity = '.8'; - }, - onDragEnd: e => { - overlay.style.opacity = ''; - }, - onDragEnter: e => { - overlay.style.opacity = ''; - }, - onDragLeave: e => { - overlay.style.backgroundColor = this.theme.getColor(EDITOR_DRAG_AND_DROP_BACKGROUND, true)?.toString() || ''; - overlay.style.opacity = '.8'; - } - }); - return this.content; } diff --git a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css b/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css index 3b355971ce7..677bed9952a 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css +++ b/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css @@ -7,15 +7,10 @@ width: 48px; } -.monaco-workbench .part > .drag-overlay { - transition-property: opacity; - transition-duration: .2s; - width: 100%; - height: 100%; - position: absolute; - opacity: 0; - top: 0; - pointer-events: none; +.monaco-workbench .part > .drop-block-overlay.visible { + visibility: visible; + backdrop-filter: brightness(97%) blur(2px); + opacity: 1; } .monaco-workbench .activitybar > .content { diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index a3b7654dd45..1044b6e4601 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -32,6 +32,7 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { MementoObject } from 'vs/workbench/common/memento'; import { assertIsDefined } from 'vs/base/common/types'; import { IBoundarySashes } from 'vs/base/browser/ui/grid/gridview'; +import { CompositeDragAndDropObserver } from 'vs/workbench/browser/dnd'; interface IEditorPartUIState { serializedGrid: ISerializedGrid; @@ -826,6 +827,20 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro // Drop support this._register(this.createEditorDropTarget(this.container, {})); + // No drop in the editor + const overlay = document.createElement('div'); + addClass(overlay, 'drop-block-overlay'); + parent.appendChild(overlay); + + CompositeDragAndDropObserver.INSTANCE.registerTarget(this.element, { + onDragStart: e => { + toggleClass(overlay, 'visible', true); + }, + onDragEnd: e => { + toggleClass(overlay, 'visible', false); + } + }); + return this.container; } diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index 06c1b202099..1692eb5d471 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -20,13 +20,13 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ClosePanelAction, PanelActivityAction, ToggleMaximizedPanelAction, TogglePanelAction, PlaceHolderPanelActivityAction, PlaceHolderToggleCompositePinnedAction, PositionPanelActionConfigs, SetPanelPositionAction } from 'vs/workbench/browser/parts/panel/panelActions'; import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; -import { PANEL_BACKGROUND, PANEL_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_INACTIVE_TITLE_FOREGROUND, PANEL_ACTIVE_TITLE_BORDER, PANEL_DRAG_AND_DROP_BACKGROUND, PANEL_INPUT_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; +import { PANEL_BACKGROUND, PANEL_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_INACTIVE_TITLE_FOREGROUND, PANEL_ACTIVE_TITLE_BORDER, PANEL_DRAG_AND_DROP_BACKGROUND, PANEL_INPUT_BORDER } from 'vs/workbench/common/theme'; import { activeContrastBorder, focusBorder, contrastBorder, editorBackground, badgeBackground, badgeForeground } from 'vs/platform/theme/common/colorRegistry'; import { CompositeBar, ICompositeBarItem, CompositeDragAndDrop } from 'vs/workbench/browser/parts/compositeBar'; import { ToggleCompositePinnedAction } from 'vs/workbench/browser/parts/compositeBarActions'; import { IBadge } from 'vs/workbench/services/activity/common/activity'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { Dimension, trackFocus, addClass } from 'vs/base/browser/dom'; +import { Dimension, trackFocus } from 'vs/base/browser/dom'; import { localize } from 'vs/nls'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IContextKey, IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -37,7 +37,6 @@ import { ViewContainer, IViewContainersRegistry, Extensions as ViewContainerExte import { MenuId } from 'vs/platform/actions/common/actions'; import { ViewMenuActions } from 'vs/workbench/browser/parts/views/viewMenuActions'; import { IPaneComposite } from 'vs/workbench/common/panecomposite'; -import { CompositeDragAndDropObserver } from 'vs/workbench/browser/dnd'; interface ICachedPanel { id: string; @@ -341,31 +340,6 @@ export class PanelPart extends CompositePart implements IPanelService { super.create(parent); - const overlay = document.createElement('div'); - addClass(overlay, 'drag-overlay'); - parent.appendChild(overlay); - - CompositeDragAndDropObserver.INSTANCE.registerTarget(this.element, { - onDragStart: e => { - // this.element.style.outline = `1px solid`; - // this.element.style.outlineOffset = '-1px'; - overlay.style.backgroundColor = this.theme.getColor(EDITOR_DRAG_AND_DROP_BACKGROUND, true)?.toString() || ''; - overlay.style.opacity = '.8'; - }, - onDragEnd: e => { - // this.element.style.outline = ''; - overlay.style.opacity = ''; - }, - onDragEnter: e => { - overlay.style.opacity = ''; - }, - onDragLeave: e => { - overlay.style.backgroundColor = this.theme.getColor(EDITOR_DRAG_AND_DROP_BACKGROUND, true)?.toString() || ''; - overlay.style.opacity = '.8'; - } - }); - - const focusTracker = this._register(trackFocus(parent)); this._register(focusTracker.onDidFocus(() => this.panelFocusContextKey.set(true))); this._register(focusTracker.onDidBlur(() => this.panelFocusContextKey.set(false))); diff --git a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts index 51c20b30732..e2777c725e1 100644 --- a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts +++ b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts @@ -23,9 +23,9 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { Event, Emitter } from 'vs/base/common/event'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { contrastBorder } from 'vs/platform/theme/common/colorRegistry'; -import { SIDE_BAR_TITLE_FOREGROUND, SIDE_BAR_BACKGROUND, SIDE_BAR_FOREGROUND, SIDE_BAR_BORDER, SIDE_BAR_DRAG_AND_DROP_BACKGROUND, EDITOR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; +import { SIDE_BAR_TITLE_FOREGROUND, SIDE_BAR_BACKGROUND, SIDE_BAR_FOREGROUND, SIDE_BAR_BORDER, SIDE_BAR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { EventType, addDisposableListener, trackFocus, addClass } from 'vs/base/browser/dom'; +import { EventType, addDisposableListener, trackFocus } from 'vs/base/browser/dom'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; @@ -33,7 +33,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { LayoutPriority } from 'vs/base/browser/ui/grid/grid'; import { assertIsDefined } from 'vs/base/common/types'; -import { LocalSelectionTransfer, CompositeDragAndDropObserver, DraggedViewIdentifier, DraggedCompositeIdentifier } from 'vs/workbench/browser/dnd'; +import { LocalSelectionTransfer, DraggedViewIdentifier, DraggedCompositeIdentifier } from 'vs/workbench/browser/dnd'; export class SidebarPart extends CompositePart implements IViewletService { @@ -152,28 +152,6 @@ export class SidebarPart extends CompositePart implements IViewletServi super.create(parent); - const overlay = document.createElement('div'); - addClass(overlay, 'drag-overlay'); - parent.appendChild(overlay); - - CompositeDragAndDropObserver.INSTANCE.registerTarget(this.element, { - onDragStart: e => { - overlay.style.backgroundColor = this.theme.getColor(EDITOR_DRAG_AND_DROP_BACKGROUND, true)?.toString() || ''; - overlay.style.opacity = '.8'; - }, - onDragEnd: e => { - // this.element.style.outline = ''; - overlay.style.opacity = ''; - }, - onDragEnter: e => { - overlay.style.opacity = ''; - }, - onDragLeave: e => { - overlay.style.backgroundColor = this.theme.getColor(EDITOR_DRAG_AND_DROP_BACKGROUND, true)?.toString() || ''; - overlay.style.opacity = '.8'; - } - }); - const focusTracker = this._register(trackFocus(parent)); this._register(focusTracker.onDidFocus(() => this.sideBarFocusContextKey.set(true))); this._register(focusTracker.onDidBlur(() => this.sideBarFocusContextKey.set(false))); From 61d5f2b82f17bf9f99f56405204caab88a7e8747 Mon Sep 17 00:00:00 2001 From: SteVen Batten Date: Thu, 19 Mar 2020 01:38:37 -0400 Subject: [PATCH 02/44] prevent forbidden flicker --- src/vs/workbench/browser/parts/compositeBar.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts index 5df76cb9a96..badf730c7cd 100644 --- a/src/vs/workbench/browser/parts/compositeBar.ts +++ b/src/vs/workbench/browser/parts/compositeBar.ts @@ -241,6 +241,9 @@ export class CompositeBar extends Widget implements ICompositeBar { // Contextmenu for composites this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, e => this.showContextMenu(e))); + // Register a drop target on the whole bar to prevent forbidden feedback + this._register(CompositeDragAndDropObserver.INSTANCE.registerTarget(parent, {})); + // Allow to drop at the end to move composites to the end this._register(CompositeDragAndDropObserver.INSTANCE.registerTarget(excessDiv, { onDragEnter: (e: IDraggedCompositeData) => { From 106046c1ad0069b965390207cb8535df1bc78942 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 19 Mar 2020 07:54:35 +0100 Subject: [PATCH 03/44] Keep selected item when typing line numbers (fix #86215) --- .../parts/quickinput/browser/quickInput.ts | 6 +- .../quickinput/browser/quickInputList.ts | 6 +- .../quickinput/browser/pickerQuickAccess.ts | 13 ++++- src/vs/workbench/browser/style.ts | 20 +++---- src/vs/workbench/browser/workbench.ts | 4 +- .../search/browser/anythingQuickAccess.ts | 56 +++++++++++++------ .../workbench/contrib/search/common/search.ts | 7 ++- 7 files changed, 75 insertions(+), 37 deletions(-) diff --git a/src/vs/base/parts/quickinput/browser/quickInput.ts b/src/vs/base/parts/quickinput/browser/quickInput.ts index f6dd7e826e3..7e9da18df64 100644 --- a/src/vs/base/parts/quickinput/browser/quickInput.ts +++ b/src/vs/base/parts/quickinput/browser/quickInput.ts @@ -622,8 +622,10 @@ class QuickPick extends QuickInput implements IQuickPi return; } this._value = value; - this.ui.list.filter(this.filterValue(this.ui.inputBox.value)); - this.trySelectFirst(); + const didFilter = this.ui.list.filter(this.filterValue(this.ui.inputBox.value)); + if (didFilter) { + this.trySelectFirst(); + } this.onDidChangeValueEmitter.fire(value); })); this.visibleDisposables.add(this.ui.inputBox.onMouseDown(event => { diff --git a/src/vs/base/parts/quickinput/browser/quickInputList.ts b/src/vs/base/parts/quickinput/browser/quickInputList.ts index 9c72c887375..9979a4fff39 100644 --- a/src/vs/base/parts/quickinput/browser/quickInputList.ts +++ b/src/vs/base/parts/quickinput/browser/quickInputList.ts @@ -498,10 +498,10 @@ export class QuickInputList { this.list.layout(); } - filter(query: string) { + filter(query: string): boolean { if (!(this.sortByLabel || this.matchOnLabel || this.matchOnDescription || this.matchOnDetail)) { this.list.layout(); - return; + return false; } query = query.trim(); @@ -559,6 +559,8 @@ export class QuickInputList { this._onChangedAllVisibleChecked.fire(this.getAllVisibleChecked()); this._onChangedVisibleCount.fire(shownElements.length); + + return true; } toggleCheckbox() { diff --git a/src/vs/platform/quickinput/browser/pickerQuickAccess.ts b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts index 69ac750247b..83798e7505f 100644 --- a/src/vs/platform/quickinput/browser/pickerQuickAccess.ts +++ b/src/vs/platform/quickinput/browser/pickerQuickAccess.ts @@ -97,7 +97,14 @@ export abstract class PickerQuickAccessProvider | Promise> | FastAndSlowPicksType; + protected abstract getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Array | Promise> | FastAndSlowPicksType | null; } diff --git a/src/vs/workbench/browser/style.ts b/src/vs/workbench/browser/style.ts index 1ea7327bedc..407c1033b97 100644 --- a/src/vs/workbench/browser/style.ts +++ b/src/vs/workbench/browser/style.ts @@ -14,18 +14,22 @@ import { isSafari, isStandalone } from 'vs/base/browser/browser'; registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { - // Icon defaults - const iconForegroundColor = theme.getColor(iconForeground); - if (iconForegroundColor) { - collector.addRule(`.monaco-workbench .codicon { color: ${iconForegroundColor}; }`); - } - // Foreground const windowForeground = theme.getColor(foreground); if (windowForeground) { collector.addRule(`.monaco-workbench { color: ${windowForeground}; }`); } + // Background (We need to set the workbench background color so that on Windows we get subpixel-antialiasing) + const workbenchBackground = WORKBENCH_BACKGROUND(theme); + collector.addRule(`.monaco-workbench { background-color: ${workbenchBackground}; }`); + + // Icon defaults + const iconForegroundColor = theme.getColor(iconForeground); + if (iconForegroundColor) { + collector.addRule(`.monaco-workbench .codicon { color: ${iconForegroundColor}; }`); + } + // Selection const windowSelectionBackground = theme.getColor(selectionBackground); if (windowSelectionBackground) { @@ -58,10 +62,6 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = `); } - // We need to set the workbench background color so that on Windows we get subpixel-antialiasing. - const workbenchBackground = WORKBENCH_BACKGROUND(theme); - collector.addRule(`.monaco-workbench { background-color: ${workbenchBackground}; }`); - // Scrollbars const scrollbarShadowColor = theme.getColor(scrollbarShadow); if (scrollbarShadowColor) { diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index 743441a13ee..f55ecabf845 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -321,6 +321,9 @@ export class Workbench extends Layout { private renderWorkbench(instantiationService: IInstantiationService, notificationService: NotificationService, storageService: IStorageService, configurationService: IConfigurationService): void { + // ARIA + this.container.setAttribute('role', 'application'); + // State specific classes const platformClass = isWindows ? 'windows' : isLinux ? 'linux' : 'mac'; const workbenchClasses = coalesce([ @@ -333,7 +336,6 @@ export class Workbench extends Layout { addClasses(this.container, ...workbenchClasses); addClass(document.body, platformClass); // used by our fonts - this.container.setAttribute('role', 'application'); if (isWeb) { addClass(document.body, 'web'); diff --git a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts index b04f60dae01..77d4898041b 100644 --- a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts @@ -56,11 +56,20 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { + protected getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): FastAndSlowPicksType | null { // Find a suitable range from the pattern looking for ":", "#" or "," - let range: IRange | undefined = undefined; const filterWithRange = extractRangeFromFilter(filter); if (filterWithRange) { filter = filterWithRange.filter; - range = filterWithRange.range; } + // Remember as last range + this.pickState.lastRange = filterWithRange?.range; + + // If the filter has not changed, return early and signal this + // with a `null` result. This allows to keep the picker stable + // when the user types a range pattern after the filter (:) + if (this.pickState.lastFilter === filter) { + return null; + } + + // Remember as last filter + this.pickState.lastFilter = filter; + const query = prepareQuery(filter); - const historyEditorPicks = this.getEditorHistoryPicks(query, range); + const historyEditorPicks = this.getEditorHistoryPicks(query); return { @@ -139,7 +159,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider, token: CancellationToken): Promise> { + private async getAdditionalPicks(query: IPreparedQuery, excludes: ResourceMap, token: CancellationToken): Promise> { // Resolve file and symbol picks (if enabled) const [filePicks, symbolPicks] = await Promise.all([ - this.getFilePicks(query, range, excludes, token), - this.getSymbolPicks(query, range, token) + this.getFilePicks(query, excludes, token), + this.getSymbolPicks(query, token) ]); if (token.isCancellationRequested) { @@ -193,11 +213,11 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { + protected getEditorHistoryPicks(query: IPreparedQuery): Array { // Just return all history entries if not searching if (!query.value) { - return this.historyService.getHistory().map(editor => this.createAnythingPick(editor, range)); + return this.historyService.getHistory().map(editor => this.createAnythingPick(editor)); } if (!this.configuration.includeHistory) { @@ -215,7 +235,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider, token: CancellationToken): Promise> { + protected async getFilePicks(query: IPreparedQuery, excludes: ResourceMap, token: CancellationToken): Promise> { if (!query.value) { return []; } @@ -290,7 +310,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider !excludes.has(fileMatch.resource)) - .map(fileMatch => this.createAnythingPick(fileMatch.resource, range)); + .map(fileMatch => this.createAnythingPick(fileMatch.resource)); } private async doFileSearch(query: IPreparedQuery, token: CancellationToken): Promise { @@ -358,11 +378,11 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider> { + protected async getSymbolPicks(query: IPreparedQuery, token: CancellationToken): Promise> { if ( !query.value || // we need a value for search for !this.configuration.includeSymbols || // we need to enable symbols in search - range // a range is an indicator for just searching for files + this.pickState.lastRange // a range is an indicator for just searching for files ) { return []; } @@ -377,7 +397,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider this.openAnything(resourceOrEditor, { keyMods, range, preserveFocus: event.inBackground }) + accept: (keyMods, event) => this.openAnything(resourceOrEditor, { keyMods, range: this.pickState.lastRange, preserveFocus: event.inBackground }) }; } diff --git a/src/vs/workbench/contrib/search/common/search.ts b/src/vs/workbench/contrib/search/common/search.ts index 7a52ac9d3e2..2b216966bf4 100644 --- a/src/vs/workbench/contrib/search/common/search.ts +++ b/src/vs/workbench/contrib/search/common/search.ts @@ -102,7 +102,12 @@ export function getOutOfWorkspaceEditorResources(accessor: ServicesAccessor): UR // Supports patterns of <#|:|(><#|:|,> const LINE_COLON_PATTERN = /\s?[#:\(](\d*)([#:,](\d*))?\)?\s*$/; -export function extractRangeFromFilter(filter: string): { filter: string, range: IRange } | undefined { +export interface IFilterAndRange { + filter: string; + range: IRange; +} + +export function extractRangeFromFilter(filter: string): IFilterAndRange | undefined { if (!filter) { return undefined; } From 9f2cc0cc799929002cebaebc065c3a13ceed4948 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 19 Mar 2020 07:57:02 +0100 Subject: [PATCH 04/44] test - disable another flaky terminal test (#92826) --- .../vscode-api-tests/src/singlefolder-tests/terminal.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts index f986ff4dfc6..0b2016a8711 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts @@ -235,7 +235,8 @@ suite('window namespace tests', () => { }); suite('window.onDidWriteTerminalData', () => { - test('should listen to all future terminal data events', (done) => { + // TODO@Daniel done called multiple times (https://github.com/microsoft/vscode/issues/92826) + ((env.uiKind === UIKind.Web) ? test.skip : test)('should listen to all future terminal data events', (done) => { const openEvents: string[] = []; const dataEvents: { name: string, data: string }[] = []; const closeEvents: string[] = []; From ec4fe07fd717ccff48138c5d217e385803bc1b37 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 19 Mar 2020 08:02:54 +0100 Subject: [PATCH 05/44] quick access - allow relative path matches even if excluded --- .../search/browser/anythingQuickAccess.ts | 94 +++++++++++++++---- 1 file changed, 74 insertions(+), 20 deletions(-) diff --git a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts index 77d4898041b..f7563aa5e25 100644 --- a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts @@ -10,7 +10,7 @@ import { prepareQuery, IPreparedQuery, compareItemsByScore, scoreItem, ScorerCac import { IFileQueryBuilderOptions, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { getOutOfWorkspaceEditorResources, extractRangeFromFilter, IWorkbenchSearchConfiguration } from 'vs/workbench/contrib/search/common/search'; -import { ISearchService, IFileMatch } from 'vs/workbench/services/search/common/search'; +import { ISearchService } from 'vs/workbench/services/search/common/search'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { untildify } from 'vs/base/common/labels'; import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService'; @@ -258,7 +258,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider(AnythingQuickAccessProvider.TYPING_SEARCH_DELAY)); + private fileQueryDelayer = this._register(new ThrottledDelayer(AnythingQuickAccessProvider.TYPING_SEARCH_DELAY)); private fileQueryBuilder = this.instantiationService.createInstance(QueryBuilder); @@ -283,9 +283,9 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider>; + let fileMatches: Array; if (absolutePathResult) { - fileMatches = [{ resource: absolutePathResult }]; + fileMatches = [absolutePathResult]; } // Otherwise run the file search (with a delayer if cache is not ready yet) @@ -309,22 +309,35 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider !excludes.has(fileMatch.resource)) - .map(fileMatch => this.createAnythingPick(fileMatch.resource)); + .filter(resource => !excludes.has(resource)) + .map(resource => this.createAnythingPick(resource)); } - private async doFileSearch(query: IPreparedQuery, token: CancellationToken): Promise { - const { results } = await this.searchService.fileSearch( - this.fileQueryBuilder.file( - this.contextService.getWorkspace().folders, - this.getFileQueryOptions({ - filePattern: query.original, - cacheKey: this.pickState.fileQueryCache?.cacheKey, - maxResults: AnythingQuickAccessProvider.MAX_RESULTS - }) - ), token); + private async doFileSearch(query: IPreparedQuery, token: CancellationToken): Promise { + const [fileSearchResults, relativePathFileResults] = await Promise.all([ - return results; + // File search: this is a search over all files of the workspace using the provided pattern + this.searchService.fileSearch( + this.fileQueryBuilder.file( + this.contextService.getWorkspace().folders, + this.getFileQueryOptions({ + filePattern: query.original, + cacheKey: this.pickState.fileQueryCache?.cacheKey, + maxResults: AnythingQuickAccessProvider.MAX_RESULTS + }) + ), token), + + // Relative path search: we also want to consider results that match files inside the workspace + // by looking for relative paths that the user typed as query. This allows to return even excluded + // results into the picker if found (e.g. helps for opening compilation results that are otherwise + // excluded) + this.getRelativePathFileResults(query, token) + ]); + + return [ + ...fileSearchResults.results.map(result => result.resource), + ...(relativePathFileResults || []) + ]; } private getFileQueryOptions(input: { filePattern?: string, cacheKey?: string, maxResults?: number }): IFileQueryBuilderOptions { @@ -341,7 +354,11 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { - const detildifiedQuery = untildify(query.original, (await this.remotePathService.userHome).path); + if (!query.containsPathSeparator) { + return; + } + + const detildifiedQuery = untildify(query.value, (await this.remotePathService.userHome).path); if (token.isCancellationRequested) { return; } @@ -362,15 +379,52 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { + if (!query.containsPathSeparator) { + return; + } + + // Convert relative paths to absolute paths over all folders of the workspace + // and return them as results if the absolute paths exist + const isAbsolutePathQuery = (await this.remotePathService.path).isAbsolute(query.value); + if (!isAbsolutePathQuery) { + const resources: URI[] = []; + for (const folder of this.contextService.getWorkspace().folders) { + if (token.isCancellationRequested) { + break; + } + + const resource = toLocalResource( + folder.toResource(query.value), + this.environmentService.configuration.remoteAuthority + ); + + try { + if ((await this.fileService.resolve(resource)).isFile) { + resources.push(resource); + } + } catch (error) { + // ignore if file does not exist + } + } + + return resources; + } + + return; + } + //#endregion From fe183c9109f28aa984093a4f59000a7729ce9b14 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 19 Mar 2020 08:40:10 +0100 Subject: [PATCH 06/44] quick access - fix broken refresh in anything picker --- .../search/browser/anythingQuickAccess.ts | 27 ++++++++++++------- .../workbench/contrib/search/common/search.ts | 2 +- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts index f7563aa5e25..04655ad44da 100644 --- a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts @@ -54,10 +54,11 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider | null { + protected getPicks(originalFilter: string, disposables: DisposableStore, token: CancellationToken): FastAndSlowPicksType | null { // Find a suitable range from the pattern looking for ":", "#" or "," - const filterWithRange = extractRangeFromFilter(filter); + const filterWithRange = extractRangeFromFilter(originalFilter); + + // Update filter with normalized values + let filter: string; if (filterWithRange) { filter = filterWithRange.filter; + } else { + filter = originalFilter; } // Remember as last range this.pickState.lastRange = filterWithRange?.range; - // If the filter has not changed, return early and signal this - // with a `null` result. This allows to keep the picker stable - // when the user types a range pattern after the filter (:) - if (this.pickState.lastFilter === filter) { + // If the original filter value has changed but the normalized + // one has not, we return early with a `null` result indicating + // that the results should preserve because the range information + // (::) does not need to trigger any re-sorting. + if (originalFilter !== this.pickState.lastOriginalFilter && filter === this.pickState.lastFilter) { return null; } // Remember as last filter + this.pickState.lastOriginalFilter = originalFilter; this.pickState.lastFilter = filter; const query = prepareQuery(filter); diff --git a/src/vs/workbench/contrib/search/common/search.ts b/src/vs/workbench/contrib/search/common/search.ts index 2b216966bf4..69286ebd2e0 100644 --- a/src/vs/workbench/contrib/search/common/search.ts +++ b/src/vs/workbench/contrib/search/common/search.ts @@ -156,7 +156,7 @@ export function extractRangeFromFilter(filter: string): IFilterAndRange | undefi if (patternMatch && range) { return { filter: filter.substr(0, patternMatch.index), // clear range suffix from search value - range: range + range }; } From 44e906343ea8563db229c2435d83fbb597aab314 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 18 Mar 2020 21:12:05 +0100 Subject: [PATCH 07/44] - stop synchronising if disabled - use file opertion error --- .../userDataSync/common/abstractSynchronizer.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index 3946c276a07..f00644b7772 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from 'vs/base/common/lifecycle'; -import { IFileService, IFileContent, FileChangesEvent, FileSystemProviderError, FileSystemProviderErrorCode, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files'; +import { IFileService, IFileContent, FileChangesEvent, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files'; import { VSBuffer } from 'vs/base/common/buffer'; import { URI } from 'vs/base/common/uri'; import { SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict } from 'vs/platform/userDataSync/common/userDataSync'; @@ -110,6 +110,9 @@ export abstract class AbstractSynchroniser extends Disposable { async sync(ref?: string): Promise { if (!this.isEnabled()) { + if (this.status !== SyncStatus.Idle) { + await this.stop(); + } this.logService.info(`${this.syncResourceLogLabel}: Skipped synchronizing ${this.resource.toLowerCase()} as it is disabled.`); return; } @@ -264,6 +267,7 @@ export abstract class AbstractSynchroniser extends Disposable { protected abstract readonly version: number; protected abstract performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise; + abstract stop(): Promise; } export interface IFileSyncPreviewResult { @@ -299,7 +303,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser { async stop(): Promise { this.cancel(); - this.logService.trace(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.resource.toLowerCase()}.`); + this.logService.info(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.resource.toLowerCase()}.`); try { await this.fileService.del(this.localPreviewResource); } catch (e) { /* ignore */ } @@ -339,7 +343,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser { await this.fileService.createFile(this.file, VSBuffer.fromString(newContent), { overwrite: false }); } } catch (e) { - if ((e instanceof FileSystemProviderError && e.code === FileSystemProviderErrorCode.FileExists) || + if ((e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) || (e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_MODIFIED_SINCE)) { throw new UserDataSyncError(e.message, UserDataSyncErrorCode.LocalPreconditionFailed); } else { From 9e89ada95f4c2767f3a8e220d0541b3038da4be4 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 19 Mar 2020 09:32:16 +0100 Subject: [PATCH 08/44] fix compilation --- src/vs/platform/userDataSync/test/common/synchronizer.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts index d8abc061edf..006947f829c 100644 --- a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts +++ b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts @@ -44,7 +44,7 @@ class TestSynchroniser extends AbstractSynchroniser { await this.updateLastSyncUserData({ ref, syncData: { content: '', version: this.version } }); } - stop(): void { + async stop(): Promise { this.cancelled = true; this.syncBarrier.open(); } From c0eea2a712471a4d242d15704cfa968241d63280 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 19 Mar 2020 09:37:11 +0100 Subject: [PATCH 09/44] tests - disable terminal integration tests for web --- .../src/singlefolder-tests/commands.test.ts | 2 +- .../singlefolder-tests/configuration.test.ts | 2 +- .../src/singlefolder-tests/debug.test.ts | 2 +- .../src/singlefolder-tests/editor.test.ts | 2 +- .../src/singlefolder-tests/env.test.ts | 2 +- .../src/singlefolder-tests/languages.test.ts | 2 +- .../src/singlefolder-tests/quickInput.test.ts | 324 +++++++++--------- .../src/singlefolder-tests/terminal.test.ts | 13 +- .../src/singlefolder-tests/types.test.ts | 5 +- .../src/singlefolder-tests/webview.test.ts | 2 +- .../src/singlefolder-tests/window.test.ts | 2 +- .../workspace.event.test.ts | 2 +- .../singlefolder-tests/workspace.fs.test.ts | 2 +- .../workspace.tasks.test.ts | 2 +- .../src/singlefolder-tests/workspace.test.ts | 2 +- .../src/workspace-tests/workspace.test.ts | 2 +- 16 files changed, 181 insertions(+), 187 deletions(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/commands.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/commands.test.ts index 739ce386371..1d62a12c170 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/commands.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/commands.test.ts @@ -8,7 +8,7 @@ import * as assert from 'assert'; import { join } from 'path'; import { commands, workspace, window, Uri, Range, Position, ViewColumn } from 'vscode'; -suite('commands namespace tests', () => { +suite('vscode API - commands', () => { test('getCommands', function (done) { diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/configuration.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/configuration.test.ts index ffd8b53e06c..0b6f13fb01f 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/configuration.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/configuration.test.ts @@ -7,7 +7,7 @@ import 'mocha'; import * as assert from 'assert'; import * as vscode from 'vscode'; -suite('Configuration tests', () => { +suite('vscode API - configuration', () => { test('configurations, language defaults', function () { const defaultLanguageSettings = vscode.workspace.getConfiguration().get('[abcLang]'); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts index 7a60146353b..ec976ff5334 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/debug.test.ts @@ -8,7 +8,7 @@ import { debug, workspace, Disposable, commands, window } from 'vscode'; import { disposeAll } from '../utils'; import { basename } from 'path'; -suite('Debug', function () { +suite('vscode API - debug', function () { test('breakpoints', async function () { assert.equal(debug.breakpoints.length, 0); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts index 4b6ee382120..20bf80b0b05 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/editor.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { workspace, window, Position, Range, commands, TextEditor, TextDocument, TextEditorCursorStyle, TextEditorLineNumbersStyle, SnippetString, Selection, Uri } from 'vscode'; import { createRandomFile, deleteFile, closeAllEditors } from '../utils'; -suite('editor tests', () => { +suite('vscode API - editors', () => { teardown(closeAllEditors); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/env.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/env.test.ts index 318b0d07bb3..7bc2f75973f 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/env.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/env.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { env, extensions, ExtensionKind, UIKind, Uri } from 'vscode'; -suite('env-namespace', () => { +suite('vscode API - env', () => { test('env is set', function () { assert.equal(typeof env.language, 'string'); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/languages.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/languages.test.ts index 0a828ad8d73..9c30a873e85 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/languages.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/languages.test.ts @@ -8,7 +8,7 @@ import { join } from 'path'; import * as vscode from 'vscode'; import { createRandomFile, testFs } from '../utils'; -suite('languages namespace tests', () => { +suite('vscode API - languages', () => { const isWindows = process.platform === 'win32'; diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/quickInput.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/quickInput.test.ts index d1237703dac..55fbe0655a7 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/quickInput.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/quickInput.test.ts @@ -18,190 +18,188 @@ interface QuickPickExpected { }; } -suite('window namespace tests', function () { +suite('vscode API - quick input', function () { - suite('QuickInput tests', function () { - teardown(closeAllEditors); + teardown(closeAllEditors); - test('createQuickPick, select second', function (_done) { - let done = (err?: any) => { - done = () => {}; - _done(err); - }; + test('createQuickPick, select second', function (_done) { + let done = (err?: any) => { + done = () => { }; + _done(err); + }; - const quickPick = createQuickPick({ - events: ['active', 'active', 'selection', 'accept', 'hide'], - activeItems: [['eins'], ['zwei']], - selectionItems: [['zwei']], - acceptedItems: { - active: [['zwei']], - selection: [['zwei']], - dispose: [true] - }, - }, (err?: any) => done(err)); - quickPick.items = ['eins', 'zwei', 'drei'].map(label => ({ label })); - quickPick.show(); + const quickPick = createQuickPick({ + events: ['active', 'active', 'selection', 'accept', 'hide'], + activeItems: [['eins'], ['zwei']], + selectionItems: [['zwei']], + acceptedItems: { + active: [['zwei']], + selection: [['zwei']], + dispose: [true] + }, + }, (err?: any) => done(err)); + quickPick.items = ['eins', 'zwei', 'drei'].map(label => ({ label })); + quickPick.show(); - (async () => { - await commands.executeCommand('workbench.action.quickOpenSelectNext'); - await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); - })() - .catch(err => done(err)); - }); + (async () => { + await commands.executeCommand('workbench.action.quickOpenSelectNext'); + await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); + })() + .catch(err => done(err)); + }); - test('createQuickPick, focus second', function (_done) { - let done = (err?: any) => { - done = () => {}; - _done(err); - }; + test('createQuickPick, focus second', function (_done) { + let done = (err?: any) => { + done = () => { }; + _done(err); + }; - const quickPick = createQuickPick({ - events: ['active', 'selection', 'accept', 'hide'], - activeItems: [['zwei']], - selectionItems: [['zwei']], - acceptedItems: { - active: [['zwei']], - selection: [['zwei']], - dispose: [true] - }, - }, (err?: any) => done(err)); - quickPick.items = ['eins', 'zwei', 'drei'].map(label => ({ label })); - quickPick.activeItems = [quickPick.items[1]]; - quickPick.show(); + const quickPick = createQuickPick({ + events: ['active', 'selection', 'accept', 'hide'], + activeItems: [['zwei']], + selectionItems: [['zwei']], + acceptedItems: { + active: [['zwei']], + selection: [['zwei']], + dispose: [true] + }, + }, (err?: any) => done(err)); + quickPick.items = ['eins', 'zwei', 'drei'].map(label => ({ label })); + quickPick.activeItems = [quickPick.items[1]]; + quickPick.show(); - (async () => { - await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); - })() - .catch(err => done(err)); - }); + (async () => { + await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); + })() + .catch(err => done(err)); + }); - test('createQuickPick, select first and second', function (_done) { - let done = (err?: any) => { - done = () => {}; - _done(err); - }; + test('createQuickPick, select first and second', function (_done) { + let done = (err?: any) => { + done = () => { }; + _done(err); + }; - const quickPick = createQuickPick({ - events: ['active', 'selection', 'active', 'selection', 'accept', 'hide'], - activeItems: [['eins'], ['zwei']], - selectionItems: [['eins'], ['eins', 'zwei']], - acceptedItems: { - active: [['zwei']], - selection: [['eins', 'zwei']], - dispose: [true] - }, - }, (err?: any) => done(err)); - quickPick.canSelectMany = true; - quickPick.items = ['eins', 'zwei', 'drei'].map(label => ({ label })); - quickPick.show(); + const quickPick = createQuickPick({ + events: ['active', 'selection', 'active', 'selection', 'accept', 'hide'], + activeItems: [['eins'], ['zwei']], + selectionItems: [['eins'], ['eins', 'zwei']], + acceptedItems: { + active: [['zwei']], + selection: [['eins', 'zwei']], + dispose: [true] + }, + }, (err?: any) => done(err)); + quickPick.canSelectMany = true; + quickPick.items = ['eins', 'zwei', 'drei'].map(label => ({ label })); + quickPick.show(); - (async () => { - await commands.executeCommand('workbench.action.quickOpenSelectNext'); - await commands.executeCommand('workbench.action.quickPickManyToggle'); - await commands.executeCommand('workbench.action.quickOpenSelectNext'); - await commands.executeCommand('workbench.action.quickPickManyToggle'); - await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); - })() - .catch(err => done(err)); - }); + (async () => { + await commands.executeCommand('workbench.action.quickOpenSelectNext'); + await commands.executeCommand('workbench.action.quickPickManyToggle'); + await commands.executeCommand('workbench.action.quickOpenSelectNext'); + await commands.executeCommand('workbench.action.quickPickManyToggle'); + await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); + })() + .catch(err => done(err)); + }); - test('createQuickPick, selection events', function (_done) { - let done = (err?: any) => { - done = () => {}; - _done(err); - }; + test('createQuickPick, selection events', function (_done) { + let done = (err?: any) => { + done = () => { }; + _done(err); + }; - const quickPick = createQuickPick({ - events: ['active', 'selection', 'accept', 'selection', 'accept', 'hide'], - activeItems: [['eins']], - selectionItems: [['zwei'], ['drei']], - acceptedItems: { - active: [['eins'], ['eins']], - selection: [['zwei'], ['drei']], - dispose: [false, true] - }, - }, (err?: any) => done(err)); - quickPick.items = ['eins', 'zwei', 'drei'].map(label => ({ label })); - quickPick.show(); + const quickPick = createQuickPick({ + events: ['active', 'selection', 'accept', 'selection', 'accept', 'hide'], + activeItems: [['eins']], + selectionItems: [['zwei'], ['drei']], + acceptedItems: { + active: [['eins'], ['eins']], + selection: [['zwei'], ['drei']], + dispose: [false, true] + }, + }, (err?: any) => done(err)); + quickPick.items = ['eins', 'zwei', 'drei'].map(label => ({ label })); + quickPick.show(); - quickPick.selectedItems = [quickPick.items[1]]; - setTimeout(() => { - quickPick.selectedItems = [quickPick.items[2]]; - }, 0); - }); + quickPick.selectedItems = [quickPick.items[1]]; + setTimeout(() => { + quickPick.selectedItems = [quickPick.items[2]]; + }, 0); + }); - test('createQuickPick, continue after first accept', function (_done) { - let done = (err?: any) => { - done = () => {}; - _done(err); - }; + test('createQuickPick, continue after first accept', function (_done) { + let done = (err?: any) => { + done = () => { }; + _done(err); + }; - const quickPick = createQuickPick({ - events: ['active', 'selection', 'accept', 'active', 'selection', 'active', 'selection', 'accept', 'hide'], - activeItems: [['eins'], [], ['drei']], - selectionItems: [['eins'], [], ['drei']], - acceptedItems: { - active: [['eins'], ['drei']], - selection: [['eins'], ['drei']], - dispose: [false, true] - }, - }, (err?: any) => done(err)); - quickPick.items = ['eins', 'zwei'].map(label => ({ label })); - quickPick.show(); + const quickPick = createQuickPick({ + events: ['active', 'selection', 'accept', 'active', 'selection', 'active', 'selection', 'accept', 'hide'], + activeItems: [['eins'], [], ['drei']], + selectionItems: [['eins'], [], ['drei']], + acceptedItems: { + active: [['eins'], ['drei']], + selection: [['eins'], ['drei']], + dispose: [false, true] + }, + }, (err?: any) => done(err)); + quickPick.items = ['eins', 'zwei'].map(label => ({ label })); + quickPick.show(); - (async () => { - await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); + (async () => { + await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); + await timeout(async () => { + quickPick.items = ['drei', 'vier'].map(label => ({ label })); await timeout(async () => { - quickPick.items = ['drei', 'vier'].map(label => ({ label })); - await timeout(async () => { - await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); - }, 0); + await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); }, 0); - })() - .catch(err => done(err)); + }, 0); + })() + .catch(err => done(err)); + }); + + test('createQuickPick, dispose in onDidHide', function (_done) { + let done = (err?: any) => { + done = () => { }; + _done(err); + }; + + let hidden = false; + const quickPick = window.createQuickPick(); + quickPick.onDidHide(() => { + if (hidden) { + done(new Error('Already hidden')); + } else { + hidden = true; + quickPick.dispose(); + setTimeout(done, 0); + } }); + quickPick.show(); + quickPick.hide(); + }); - test('createQuickPick, dispose in onDidHide', function (_done) { - let done = (err?: any) => { - done = () => {}; - _done(err); - }; + test('createQuickPick, hide and dispose', function (_done) { + let done = (err?: any) => { + done = () => { }; + _done(err); + }; - let hidden = false; - const quickPick = window.createQuickPick(); - quickPick.onDidHide(() => { - if (hidden) { - done(new Error('Already hidden')); - } else { - hidden = true; - quickPick.dispose(); - setTimeout(done, 0); - } - }); - quickPick.show(); - quickPick.hide(); - }); - - test('createQuickPick, hide and dispose', function (_done) { - let done = (err?: any) => { - done = () => {}; - _done(err); - }; - - let hidden = false; - const quickPick = window.createQuickPick(); - quickPick.onDidHide(() => { - if (hidden) { - done(new Error('Already hidden')); - } else { - hidden = true; - setTimeout(done, 0); - } - }); - quickPick.show(); - quickPick.hide(); - quickPick.dispose(); + let hidden = false; + const quickPick = window.createQuickPick(); + quickPick.onDidHide(() => { + if (hidden) { + done(new Error('Already hidden')); + } else { + hidden = true; + setTimeout(done, 0); + } }); + quickPick.show(); + quickPick.hide(); + quickPick.dispose(); }); }); @@ -276,4 +274,4 @@ function createQuickPick(expected: QuickPickExpected, done: (err?: any) => void, async function timeout(run: () => Promise | T, ms: number): Promise { return new Promise(resolve => setTimeout(() => resolve(run()), ms)); -} \ No newline at end of file +} diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts index 0b2016a8711..943d641571c 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts @@ -6,7 +6,8 @@ import { window, Pseudoterminal, EventEmitter, TerminalDimensions, workspace, ConfigurationTarget, Disposable, UIKind, env } from 'vscode'; import { doesNotThrow, equal, ok, deepEqual, throws } from 'assert'; -suite('window namespace tests', () => { +// TODO@Daniel flaky tests (https://github.com/microsoft/vscode/issues/92826) +((env.uiKind === UIKind.Web) ? suite.skip : suite)('vscode API - terminal', () => { suiteSetup(async () => { // Disable conpty in integration tests because of https://github.com/microsoft/vscode/issues/76548 await workspace.getConfiguration('terminal.integrated').update('windowsEnableConpty', false, ConfigurationTarget.Global); @@ -19,8 +20,8 @@ suite('window namespace tests', () => { disposables.length = 0; }); - // TODO@Daniel flaky test (https://github.com/microsoft/vscode/issues/92826) - ((env.uiKind === UIKind.Web) ? test.skip : test)('sendText immediately after createTerminal should not throw', (done) => { + + test('sendText immediately after createTerminal should not throw', (done) => { disposables.push(window.onDidOpenTerminal(term => { try { equal(terminal, term); @@ -34,8 +35,7 @@ suite('window namespace tests', () => { doesNotThrow(terminal.sendText.bind(terminal, 'echo "foo"')); }); - // TODO@Daniel done called multiple times (https://github.com/microsoft/vscode/issues/92826) - ((env.uiKind === UIKind.Web) ? test.skip : test)('onDidCloseTerminal event fires when terminal is disposed', (done) => { + test('onDidCloseTerminal event fires when terminal is disposed', (done) => { disposables.push(window.onDidOpenTerminal(term => { try { equal(terminal, term); @@ -235,8 +235,7 @@ suite('window namespace tests', () => { }); suite('window.onDidWriteTerminalData', () => { - // TODO@Daniel done called multiple times (https://github.com/microsoft/vscode/issues/92826) - ((env.uiKind === UIKind.Web) ? test.skip : test)('should listen to all future terminal data events', (done) => { + test('should listen to all future terminal data events', (done) => { const openEvents: string[] = []; const dataEvents: { name: string, data: string }[] = []; const closeEvents: string[] = []; diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/types.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/types.test.ts index b2ad43d30b0..53265b35e99 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/types.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/types.test.ts @@ -7,12 +7,9 @@ import 'mocha'; import * as assert from 'assert'; import * as vscode from 'vscode'; - -suite('types', () => { +suite('vscode API - types', () => { test('static properties, es5 compat class', function () { - - assert.ok(vscode.ThemeIcon.File instanceof vscode.ThemeIcon); assert.ok(vscode.ThemeIcon.Folder instanceof vscode.ThemeIcon); assert.ok(vscode.CodeActionKind.Empty instanceof vscode.CodeActionKind); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/webview.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/webview.test.ts index 06d284c3762..704d2f115ed 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/webview.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/webview.test.ts @@ -14,7 +14,7 @@ const webviewId = 'myWebview'; const testDocument = join(vscode.workspace.rootPath || '', './bower.json'); -suite('Webview tests', () => { +suite('vscode API - webview', () => { const disposables: vscode.Disposable[] = []; function _register(disposable: T) { diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts index 620ce762632..d1d6f3e7fb4 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts @@ -8,7 +8,7 @@ import { workspace, window, commands, ViewColumn, TextEditorViewColumnChangeEven import { join } from 'path'; import { closeAllEditors, pathEquals, createRandomFile } from '../utils'; -suite('window namespace tests', () => { +suite('vscode API - window', () => { teardown(closeAllEditors); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.event.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.event.test.ts index 58f0b48f126..e192c63c0ab 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.event.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.event.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import * as vscode from 'vscode'; import { createRandomFile, withLogDisabled } from '../utils'; -suite('workspace-event', () => { +suite('vscode API - workspace events', () => { const disposables: vscode.Disposable[] = []; diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.fs.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.fs.test.ts index f3c69fbbe67..2eb21a4c1f9 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.fs.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.fs.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import * as vscode from 'vscode'; import { posix } from 'path'; -suite('workspace-fs', () => { +suite('vscode API - workspace-fs', () => { let root: vscode.Uri; diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts index 1f7a9c93197..f3e56977125 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { window, tasks, Disposable, TaskDefinition, Task, EventEmitter, CustomExecution, Pseudoterminal, TaskScope, commands, Task2 } from 'vscode'; -suite('workspace-namespace', () => { +suite('vscode API - tasks', () => { suite('Tasks', () => { let disposables: Disposable[] = []; diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts index c26c88671dd..2a58c4630e0 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts @@ -9,7 +9,7 @@ import { createRandomFile, deleteFile, closeAllEditors, pathEquals, rndName, dis import { join, posix, basename } from 'path'; import * as fs from 'fs'; -suite('workspace-namespace', () => { +suite('vscode API - workspace', () => { teardown(closeAllEditors); diff --git a/extensions/vscode-api-tests/src/workspace-tests/workspace.test.ts b/extensions/vscode-api-tests/src/workspace-tests/workspace.test.ts index f018f581c42..1b4ef88325a 100644 --- a/extensions/vscode-api-tests/src/workspace-tests/workspace.test.ts +++ b/extensions/vscode-api-tests/src/workspace-tests/workspace.test.ts @@ -8,7 +8,7 @@ import * as vscode from 'vscode'; import { closeAllEditors, pathEquals } from '../utils'; import { join } from 'path'; -suite('workspace-namespace', () => { +suite('vscode API - workspace', () => { teardown(closeAllEditors); From 6cf2f6b6f66b7bea2fc96b2f2443906ba8ec8438 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 19 Mar 2020 10:11:48 +0100 Subject: [PATCH 10/44] quick access - keep viewstate to restore up to date when quick open is not closing on focus lost --- .../editorNavigationQuickAccess.ts | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts b/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts index 5926c74dce5..fd67274474c 100644 --- a/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts @@ -13,7 +13,7 @@ import { IQuickPick, IQuickPickItem, IKeyMods } from 'vs/platform/quickinput/com import { CancellationToken } from 'vs/base/common/cancellation'; import { IDisposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; -import { isDiffEditor } from 'vs/editor/browser/editorBrowser'; +import { isDiffEditor, getCodeEditor } from 'vs/editor/browser/editorBrowser'; import { withNullAsUndefined } from 'vs/base/common/types'; import { once } from 'vs/base/common/functional'; @@ -59,12 +59,24 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu // Restore any view state if this picker was closed // without actually going to a line - const lastKnownEditorViewState = withNullAsUndefined(editor.saveViewState()); - once(token.onCancellationRequested)(() => { - if (lastKnownEditorViewState) { - editor.restoreViewState(lastKnownEditorViewState); - } - }); + const codeEditor = getCodeEditor(editor); + if (codeEditor) { + + // Remember view state and update it when the cursor position + // changes even later because it could be that the user has + // configured quick open to remain open when focus is lost and + // we always want to restore the current location. + let lastKnownEditorViewState = withNullAsUndefined(editor.saveViewState()); + disposables.add(codeEditor.onDidChangeCursorPosition(() => { + lastKnownEditorViewState = withNullAsUndefined(editor.saveViewState()); + })); + + once(token.onCancellationRequested)(() => { + if (lastKnownEditorViewState) { + editor.restoreViewState(lastKnownEditorViewState); + } + }); + } // Clean up decorations on dispose disposables.add(toDisposable(() => this.clearDecorations(editor))); From d778f85cd5f23391cf94fa0965ada594c68e1fc5 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 19 Mar 2020 10:14:42 +0100 Subject: [PATCH 11/44] finalize API for FileSystemError#code, https://github.com/microsoft/vscode/issues/90517 --- src/vs/vscode.d.ts | 8 ++++++++ src/vs/vscode.proposed.d.ts | 15 --------------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index c5b6fe3f2dd..99cfc855029 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -6020,6 +6020,14 @@ declare module 'vscode' { * @param messageOrUri Message or uri. */ constructor(messageOrUri?: string | Uri); + + /** + * A code that identifies this error. + * + * Possible values are names of errors, like [`FileNotFound`](#FileSystemError.FileNotFound), + * or `Unknown` for unspecified errors. + */ + readonly code: string; } /** diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 720a82e0610..4fe548e15c7 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -2023,21 +2023,6 @@ declare module 'vscode' { //#endregion - //#region https://github.com/microsoft/vscode/issues/90517 - - export interface FileSystemError { - /** - * A code that identifies this error. - * - * Possible values are names of errors, like [`FileNotFound`](#FileSystemError.FileNotFound), - * or `Unknown` for an unspecified error. - */ - readonly code: string; - } - - //#endregion - - //#region https://github.com/microsoft/vscode/issues/90208 export namespace Uri { From f2efe3e2dc76240e6598555e382d835f3f9ac828 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 19 Mar 2020 11:17:22 +0100 Subject: [PATCH 12/44] quick access - add a setting to control symbols filter (#52773, #86761) --- .../search/browser/anythingQuickAccess.ts | 14 +++++--- .../search/browser/search.contribution.ts | 13 +++++++- .../search/browser/symbolsQuickAccess.ts | 32 ++++++++++++++----- .../workbench/contrib/search/common/search.ts | 1 + 4 files changed, 47 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts index 04655ad44da..618a2f26687 100644 --- a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts @@ -102,6 +102,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider> { + const configuration = this.configuration; if ( - !query.value || // we need a value for search for - !this.configuration.includeSymbols || // we need to enable symbols in search - this.pickState.lastRange // a range is an indicator for just searching for files + !query.value || // we need a value for search for + !configuration.includeSymbols || // we need to enable symbols in search + this.pickState.lastRange // a range is an indicator for just searching for files ) { return []; } // Delegate to the existing symbols quick access // but skip local results and also do not sort - return this.symbolsQuickAccess.getSymbolPicks(query.value, { skipLocal: true, skipSorting: true, delay: AnythingQuickAccessProvider.TYPING_SEARCH_DELAY }, token); + return this.symbolsQuickAccess.getSymbolPicks(query.value, { + skipLocal: configuration.workspaceSymbolsFilter !== 'all', + skipSorting: true, + delay: AnythingQuickAccessProvider.TYPING_SEARCH_DELAY + }, token); } //#endregion diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index 55dc1003bef..7d9af9f955d 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -734,6 +734,17 @@ configurationRegistry.registerConfiguration({ description: nls.localize('search.quickOpen.includeSymbols', "Whether to include results from a global symbol search in the file results for Quick Open."), default: false }, + 'search.quickOpen.workspaceSymbolsFilter': { + type: 'string', + enum: ['default', 'reduced', 'all'], + markdownEnumDescriptions: [ + nls.localize('search.quickOpen.workspaceSymbolsFilter.default', "All symbols including local variables are included in the specific workspace symbols picker but excluded from the files picker when `#search.quickOpen.includeSymbols#` is enabled."), + nls.localize('search.quickOpen.workspaceSymbolsFilter.reduced', "Some symbols like local variables are excluded in all pickers."), + nls.localize('search.quickOpen.workspaceSymbolsFilter.all', "All symbols including local variables are included in all pickers.") + ], + default: 'default', + description: nls.localize('search.quickOpen.workspaceSymbolsFilter', "Controls the filter to apply for the workspace symbols search in quick open. Depending on the setting, some symbols like local variables will be excluded to reduce the total number of results."), + }, 'search.quickOpen.includeHistory': { type: 'boolean', description: nls.localize('search.quickOpen.includeHistory', "Whether to include results from recently opened files in the file results for Quick Open."), @@ -766,7 +777,7 @@ configurationRegistry.registerConfiguration({ type: 'string', enum: ['auto', 'alwaysCollapse', 'alwaysExpand'], enumDescriptions: [ - 'Files with less than 10 results are expanded. Others are collapsed.', + nls.localize('search.collapseResults.auto', "Files with less than 10 results are expanded. Others are collapsed."), '', '' ], diff --git a/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts b/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts index 16e4ca2ef75..74a9fa14c5e 100644 --- a/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts @@ -10,8 +10,8 @@ import { stripWildcards } from 'vs/base/common/strings'; import { CancellationToken } from 'vs/base/common/cancellation'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { ThrottledDelayer } from 'vs/base/common/async'; -import { getWorkspaceSymbols, IWorkspaceSymbol, IWorkspaceSymbolProvider } from 'vs/workbench/contrib/search/common/search'; -import { SymbolKinds, SymbolTag } from 'vs/editor/common/modes'; +import { getWorkspaceSymbols, IWorkspaceSymbol, IWorkspaceSymbolProvider, IWorkbenchSearchConfiguration } from 'vs/workbench/contrib/search/common/search'; +import { SymbolKinds, SymbolTag, SymbolKind } from 'vs/editor/common/modes'; import { ILabelService } from 'vs/platform/label/common/label'; import { Schemas } from 'vs/base/common/network'; import { IOpenerService } from 'vs/platform/opener/common/opener'; @@ -37,6 +37,16 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider([ + SymbolKind.Class, + SymbolKind.Enum, + SymbolKind.File, + SymbolKind.Interface, + SymbolKind.Namespace, + SymbolKind.Package, + SymbolKind.Module + ]); + private delayer = this._register(new ThrottledDelayer(SymbolsQuickAccessProvider.TYPING_SEARCH_DELAY)); private readonly resourceExcludeMatcher = this._register(createResourceExcludeMatcher(this.instantiationService, this.configurationService)); @@ -53,18 +63,20 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider().workbench.editor; + const searchConfig = this.configurationService.getValue(); return { openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, - openSideBySideDirection: editorConfig.openSideBySideDirection + openSideBySideDirection: editorConfig.openSideBySideDirection, + workspaceSymbolsFilter: searchConfig.search.quickOpen.workspaceSymbolsFilter }; } protected getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Promise> { - return this.getSymbolPicks(filter, undefined, token); + return this.getSymbolPicks(filter, { skipLocal: this.configuration.workspaceSymbolsFilter === 'reduced' }, token); } - async getSymbolPicks(filter: string, options: { skipLocal: boolean, skipSorting: boolean, delay: number } | undefined, token: CancellationToken): Promise> { + async getSymbolPicks(filter: string, options: { skipLocal?: boolean, skipSorting?: boolean, delay?: number } | undefined, token: CancellationToken): Promise> { return this.delayer.trigger(async () => { if (token.isCancellationRequested) { return []; @@ -74,7 +86,7 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider> { + private async doGetSymbolPicks(filter: string, options: { skipLocal?: boolean, skipSorting?: boolean } | undefined, token: CancellationToken): Promise> { const workspaceSymbols = await getWorkspaceSymbols(filter, token); if (token.isCancellationRequested) { return []; @@ -92,8 +104,12 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider(); for (const [provider, symbols] of workspaceSymbols) { for (const symbol of symbols) { - if (options?.skipLocal && !!symbol.containerName) { - continue; // ignore local symbols if we are told so + + // Depending on the workspace symbols filter setting, skip over symbols that: + // - do not have a container + // - and are not treated explicitly as global symbols (e.g. classes) + if (options?.skipLocal && !SymbolsQuickAccessProvider.TREAT_AS_GLOBAL_SYMBOL_TYPES.has(symbol.kind) && !!symbol.containerName) { + continue; } // Score by symbol label diff --git a/src/vs/workbench/contrib/search/common/search.ts b/src/vs/workbench/contrib/search/common/search.ts index 69286ebd2e0..b3932374a09 100644 --- a/src/vs/workbench/contrib/search/common/search.ts +++ b/src/vs/workbench/contrib/search/common/search.ts @@ -77,6 +77,7 @@ export interface IWorkbenchSearchConfigurationProperties extends ISearchConfigur quickOpen: { includeSymbols: boolean; includeHistory: boolean; + workspaceSymbolsFilter: 'default' | 'reduced' | 'all'; }; } From d514636c3040caa2aaaf467f23f341e6b54aa1cd Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 19 Mar 2020 11:30:25 +0100 Subject: [PATCH 13/44] Tasks with failed dependsOn can't run Fixes #92814 --- .../contrib/tasks/browser/terminalTaskSystem.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index 969c0f9c1ea..9137ba2d1b3 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -368,11 +368,7 @@ export class TerminalTaskSystem implements ITaskSystem { }); } - private removeFromActiveTasks(task: Task): void { - if (!this.activeTasks[task.getMapKey()]) { - return; - } - delete this.activeTasks[task.getMapKey()]; + private removeInstances(task: Task) { let commonKey = task._id.split('|')[0]; if (this.instances[commonKey]) { this.instances[commonKey].removeInstance(); @@ -382,6 +378,14 @@ export class TerminalTaskSystem implements ITaskSystem { } } + private removeFromActiveTasks(task: Task): void { + if (!this.activeTasks[task.getMapKey()]) { + return; + } + delete this.activeTasks[task.getMapKey()]; + this.removeInstances(task); + } + public terminate(task: Task): Promise { let activeTerminal = this.activeTasks[task.getMapKey()]; if (!activeTerminal) { @@ -466,6 +470,7 @@ export class TerminalTaskSystem implements ITaskSystem { return Promise.all(promises).then((summaries): Promise | ITaskSummary => { for (let summary of summaries) { if (summary.exitCode !== 0) { + this.removeInstances(task); return { exitCode: summary.exitCode }; } } From bf86f521186d9954b7d9c520743f441222e95f0b Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 19 Mar 2020 11:33:09 +0100 Subject: [PATCH 14/44] quick access - reduce constant config lookup --- .../quickaccess/gotoLineQuickAccess.ts | 16 +++++------ .../quickaccess/gotoSymbolQuickAccess.ts | 18 ++++++------ .../search/browser/anythingQuickAccess.ts | 28 +++++++++---------- .../search/browser/symbolsQuickAccess.ts | 24 ++++++++-------- 4 files changed, 43 insertions(+), 43 deletions(-) diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts index 0e8c8f29aa5..4d801b49ac2 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts @@ -18,6 +18,14 @@ export class GotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProv protected readonly onDidActiveTextEditorControlChange = this.editorService.onDidActiveEditorChange; + private readonly configuration = (() => { + const editorConfig = this.configurationService.getValue().workbench.editor; + + return { + openEditorPinned: !editorConfig.enablePreviewFromQuickOpen + }; + })(); + constructor( @IEditorService private readonly editorService: IEditorService, @IConfigurationService private readonly configurationService: IConfigurationService @@ -25,14 +33,6 @@ export class GotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProv super(); } - private get configuration() { - const editorConfig = this.configurationService.getValue().workbench.editor; - - return { - openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, - }; - } - protected get activeTextEditorControl() { return this.editorService.activeTextEditorControl; } diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts index e3a213f9084..099fcd763b3 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts @@ -18,6 +18,15 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess protected readonly onDidActiveTextEditorControlChange = this.editorService.onDidActiveEditorChange; + private readonly configuration = (() => { + const editorConfig = this.configurationService.getValue().workbench.editor; + + return { + openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, + openSideBySideDirection: editorConfig.openSideBySideDirection + }; + })(); + constructor( @IEditorService private readonly editorService: IEditorService, @IConfigurationService private readonly configurationService: IConfigurationService @@ -27,15 +36,6 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess }); } - private get configuration() { - const editorConfig = this.configurationService.getValue().workbench.editor; - - return { - openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, - openSideBySideDirection: editorConfig.openSideBySideDirection - }; - } - protected get activeTextEditorControl() { return this.editorService.activeTextEditorControl; } diff --git a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts index 618a2f26687..c48c0441a0f 100644 --- a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts @@ -75,6 +75,20 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { + const editorConfig = this.configurationService.getValue().workbench.editor; + const searchConfig = this.configurationService.getValue(); + + return { + openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, + openSideBySideDirection: editorConfig.openSideBySideDirection, + includeSymbols: searchConfig.search.quickOpen.includeSymbols, + workspaceSymbolsFilter: searchConfig.search.quickOpen.workspaceSymbolsFilter, + includeHistory: searchConfig.search.quickOpen.includeHistory, + shortAutoSaveDelay: this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY + }; + })(); + constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @ISearchService private readonly searchService: ISearchService, @@ -94,20 +108,6 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider().workbench.editor; - const searchConfig = this.configurationService.getValue(); - - return { - openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, - openSideBySideDirection: editorConfig.openSideBySideDirection, - includeSymbols: searchConfig.search.quickOpen.includeSymbols, - workspaceSymbolsFilter: searchConfig.search.quickOpen.workspaceSymbolsFilter, - includeHistory: searchConfig.search.quickOpen.includeHistory, - shortAutoSaveDelay: this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY - }; - } - provide(picker: IQuickPick, token: CancellationToken): IDisposable { // Reset the pick state for this run diff --git a/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts b/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts index 74a9fa14c5e..0560cc5df2b 100644 --- a/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts @@ -47,7 +47,18 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider(SymbolsQuickAccessProvider.TYPING_SEARCH_DELAY)); + private readonly configuration = (() => { + const editorConfig = this.configurationService.getValue().workbench.editor; + const searchConfig = this.configurationService.getValue(); + + return { + openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, + openSideBySideDirection: editorConfig.openSideBySideDirection, + workspaceSymbolsFilter: searchConfig.search.quickOpen.workspaceSymbolsFilter + }; + })(); + + private readonly delayer = this._register(new ThrottledDelayer(SymbolsQuickAccessProvider.TYPING_SEARCH_DELAY)); private readonly resourceExcludeMatcher = this._register(createResourceExcludeMatcher(this.instantiationService, this.configurationService)); @@ -61,17 +72,6 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider().workbench.editor; - const searchConfig = this.configurationService.getValue(); - - return { - openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, - openSideBySideDirection: editorConfig.openSideBySideDirection, - workspaceSymbolsFilter: searchConfig.search.quickOpen.workspaceSymbolsFilter - }; - } - protected getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Promise> { return this.getSymbolPicks(filter, { skipLocal: this.configuration.workspaceSymbolsFilter === 'reduced' }, token); } From f68ab12c6fd9bdc33d7b9093a29d84cf8f155f10 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 19 Mar 2020 12:04:38 +0100 Subject: [PATCH 15/44] fix copy-paste errors with inserting markdown cells --- src/vs/workbench/contrib/notebook/browser/constants.ts | 2 +- .../contrib/notebook/browser/contrib/notebookActions.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/constants.ts b/src/vs/workbench/contrib/notebook/browser/constants.ts index 3a9456fab05..dcf51f7ccca 100644 --- a/src/vs/workbench/contrib/notebook/browser/constants.ts +++ b/src/vs/workbench/contrib/notebook/browser/constants.ts @@ -6,7 +6,7 @@ export const INSERT_CODE_CELL_ABOVE_COMMAND_ID = 'workbench.notebook.code.insertCellAbove'; export const INSERT_CODE_CELL_BELOW_COMMAND_ID = 'workbench.notebook.code.insertCellBelow'; export const INSERT_MARKDOWN_CELL_ABOVE_COMMAND_ID = 'workbench.notebook.markdown.insertCellAbove'; -export const INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID = 'workbench.notebook.markdown.insertCellAbove'; +export const INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID = 'workbench.notebook.markdown.insertCellBelow'; export const EDIT_CELL_COMMAND_ID = 'workbench.notebook.cell.edit'; export const SAVE_CELL_COMMAND_ID = 'workbench.notebook.cell.save'; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts index f33cec5c58f..51269881e93 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts @@ -413,7 +413,7 @@ registerAction2(class extends InsertCellCommand { constructor() { super( { - id: INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID, + id: INSERT_MARKDOWN_CELL_ABOVE_COMMAND_ID, title: localize('notebookActions.insertMarkdownCellAbove', "Insert Markdown Cell Above"), }, CellKind.Markdown, @@ -428,7 +428,7 @@ registerAction2(class extends InsertCellCommand { id: INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID, title: localize('notebookActions.insertMarkdownCellBelow', "Insert Markdown Cell Below"), }, - CellKind.Code, + CellKind.Markdown, 'below'); } }); From 620ac0aebfcc5a939eae0b32e0e6d0d484f93bac Mon Sep 17 00:00:00 2001 From: isidor Date: Thu, 19 Mar 2020 12:31:45 +0100 Subject: [PATCH 16/44] debug: respect progress update event #92253 --- .../contrib/debug/browser/debugProgress.ts | 14 +++++++++++++- .../contrib/debug/browser/debugSession.ts | 8 ++++++++ .../contrib/debug/browser/rawDebugSession.ts | 8 ++++++++ src/vs/workbench/contrib/debug/common/debug.ts | 1 + .../contrib/debug/test/common/mockDebug.ts | 4 ++++ 5 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/debug/browser/debugProgress.ts b/src/vs/workbench/contrib/debug/browser/debugProgress.ts index c6c29122cd9..df29f7160c4 100644 --- a/src/vs/workbench/contrib/debug/browser/debugProgress.ts +++ b/src/vs/workbench/contrib/debug/browser/debugProgress.ts @@ -39,7 +39,19 @@ export class DebugProgressContribution implements IWorkbenchContribution { title: progressStartEvent.body.title, cancellable: progressStartEvent.body.cancellable, silent: true - }, () => promise, () => session.cancel(progressStartEvent.body.progressId)); + }, progressStep => { + const progressUpdateListener = session.onDidProgressUpdate(e => { + if (e.body.progressId === progressStartEvent.body.progressId) { + progressStep.report({ + message: e.body.message, + increment: e.body.percentage, + total: e.body.percentage ? 100 : undefined + }); + } + }); + + return promise.then(() => progressUpdateListener.dispose()); + }, () => session.cancel(progressStartEvent.body.progressId)); }); } }; diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 9636ba66420..5ca1d17547c 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -55,6 +55,7 @@ export class DebugSession implements IDebugSession { private readonly _onDidLoadedSource = new Emitter(); private readonly _onDidCustomEvent = new Emitter(); private readonly _onDidProgressStart = new Emitter(); + private readonly _onDidProgressUpdate = new Emitter(); private readonly _onDidProgressEnd = new Emitter(); private readonly _onDidChangeREPLElements = new Emitter(); @@ -190,6 +191,10 @@ export class DebugSession implements IDebugSession { return this._onDidProgressStart.event; } + get onDidProgressUpdate(): Event { + return this._onDidProgressUpdate.event; + } + get onDidProgressEnd(): Event { return this._onDidProgressEnd.event; } @@ -935,6 +940,9 @@ export class DebugSession implements IDebugSession { this.rawListeners.push(this.raw.onDidProgressStart(event => { this._onDidProgressStart.fire(event); })); + this.rawListeners.push(this.raw.onDidProgressUpdate(event => { + this._onDidProgressUpdate.fire(event); + })); this.rawListeners.push(this.raw.onDidProgressEnd(event => { this._onDidProgressEnd.fire(event); })); diff --git a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts index 2f11032ac12..9194cd7686c 100644 --- a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts @@ -66,6 +66,7 @@ export class RawDebugSession implements IDisposable { private readonly _onDidBreakpoint = new Emitter(); private readonly _onDidLoadedSource = new Emitter(); private readonly _onDidProgressStart = new Emitter(); + private readonly _onDidProgressUpdate = new Emitter(); private readonly _onDidProgressEnd = new Emitter(); private readonly _onDidCustomEvent = new Emitter(); private readonly _onDidEvent = new Emitter(); @@ -142,6 +143,9 @@ export class RawDebugSession implements IDisposable { case 'progressStart': this._onDidProgressStart.fire(event as DebugProtocol.ProgressStartEvent); break; + case 'progressUpdate': + this._onDidProgressUpdate.fire(event as DebugProtocol.ProgressUpdateEvent); + break; case 'progressEnd': this._onDidProgressEnd.fire(event as DebugProtocol.ProgressEndEvent); break; @@ -217,6 +221,10 @@ export class RawDebugSession implements IDisposable { return this._onDidProgressStart.event; } + get onDidProgressUpdate(): Event { + return this._onDidProgressUpdate.event; + } + get onDidProgressEnd(): Event { return this._onDidProgressEnd.event; } diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index a538a757e80..70f22a3291e 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -200,6 +200,7 @@ export interface IDebugSession extends ITreeElement { readonly onDidLoadedSource: Event; readonly onDidCustomEvent: Event; readonly onDidProgressStart: Event; + readonly onDidProgressUpdate: Event; readonly onDidProgressEnd: Event; // DAP request diff --git a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts index ef626b3cde8..12e47d63cde 100644 --- a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts @@ -230,6 +230,10 @@ export class MockSession implements IDebugSession { throw new Error('not implemented'); } + get onDidProgressUpdate(): Event { + throw new Error('not implemented'); + } + get onDidProgressEnd(): Event { throw new Error('not implemented'); } From a3c9a09f1e9bc79469a2c0683a9b7ea1a5ee0838 Mon Sep 17 00:00:00 2001 From: isidor Date: Thu, 19 Mar 2020 12:34:47 +0100 Subject: [PATCH 17/44] debug progress proper increment computation #92253 --- src/vs/workbench/contrib/debug/browser/debugProgress.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/debugProgress.ts b/src/vs/workbench/contrib/debug/browser/debugProgress.ts index df29f7160c4..b5f82277262 100644 --- a/src/vs/workbench/contrib/debug/browser/debugProgress.ts +++ b/src/vs/workbench/contrib/debug/browser/debugProgress.ts @@ -40,12 +40,16 @@ export class DebugProgressContribution implements IWorkbenchContribution { cancellable: progressStartEvent.body.cancellable, silent: true }, progressStep => { + let increment = 0; const progressUpdateListener = session.onDidProgressUpdate(e => { if (e.body.progressId === progressStartEvent.body.progressId) { + if (typeof e.body.percentage === 'number') { + increment = e.body.percentage - increment; + } progressStep.report({ message: e.body.message, - increment: e.body.percentage, - total: e.body.percentage ? 100 : undefined + increment: typeof e.body.percentage === 'number' ? increment : undefined, + total: typeof e.body.percentage === 'number' ? 100 : undefined }); } }); From 4cc811df160e5b6678c5f0d9e2d794cf71f2d7fd Mon Sep 17 00:00:00 2001 From: isidor Date: Thu, 19 Mar 2020 12:56:12 +0100 Subject: [PATCH 18/44] fixes #92849 --- src/vs/workbench/contrib/files/browser/views/explorerViewer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index d3b8e0b336a..0b5733d8388 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -592,7 +592,7 @@ export class FilesFilter implements ITreeFilter { // Hide those that match Hidden Patterns const cached = this.hiddenExpressionPerRoot.get(stat.root.resource.toString()); - if (cached && cached.parsed(path.relative(stat.root.resource.path, stat.resource.path), stat.name, name => !!(stat.parent && stat.parent.getChild(name)))) { + if ((cached && cached.parsed(path.relative(stat.root.resource.path, stat.resource.path), stat.name, name => !!(stat.parent && stat.parent.getChild(name)))) || stat.parent?.isExcluded) { stat.isExcluded = true; const editors = this.editorService.visibleEditors; const editor = editors.filter(e => e.resource && isEqualOrParent(e.resource, stat.resource)).pop(); From 9724c8b8bf4306a83b890c1d5602822a24b38197 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 19 Mar 2020 13:59:10 +0100 Subject: [PATCH 19/44] quick access - add a setting to enable new quick access --- .../browser/ui/selectBox/selectBoxCustom.ts | 1 - .../quickinput/browser/media/quickInput.css | 10 ++++ .../parts/quickinput/browser/quickInput.ts | 23 ++++++++- .../quickinput/browser/quickAccess.ts | 7 +-- .../platform/quickinput/common/quickAccess.ts | 17 ++++++- .../browser/actions/windowActions.ts | 3 +- .../parts/quickopen/quickOpenController.ts | 47 +++++++++++++++---- src/vs/workbench/browser/quickopen.ts | 4 ++ .../browser/workbench.contribution.ts | 5 ++ 9 files changed, 97 insertions(+), 20 deletions(-) diff --git a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts index 1867910f0bb..c8ae939e455 100644 --- a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts +++ b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts @@ -357,7 +357,6 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi // Match quickOpen outline styles - ignore for disabled options if (this.styles.listFocusOutline) { content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.focused { outline: 1.6px dotted ${this.styles.listFocusOutline} !important; outline-offset: -1.6px !important; }`); - } if (this.styles.listHoverOutline) { diff --git a/src/vs/base/parts/quickinput/browser/media/quickInput.css b/src/vs/base/parts/quickinput/browser/media/quickInput.css index fb4fb012937..94237b63913 100644 --- a/src/vs/base/parts/quickinput/browser/media/quickInput.css +++ b/src/vs/base/parts/quickinput/browser/media/quickInput.css @@ -55,6 +55,12 @@ margin-bottom: -2px; } +.quick-input-widget.quick-navigate-mode .quick-input-header { + /* reduce margins and paddings in quick navigate mode */ + padding: 0; + margin-bottom: 0; +} + .quick-input-and-message { display: flex; flex-direction: column; @@ -126,6 +132,10 @@ margin-top: 6px; } +.quick-input-widget.quick-navigate-mode .quick-input-list { + margin-top: 0; /* reduce margins in quick navigate mode */ +} + .quick-input-list .monaco-list { overflow: hidden; max-height: calc(20 * 22px); diff --git a/src/vs/base/parts/quickinput/browser/quickInput.ts b/src/vs/base/parts/quickinput/browser/quickInput.ts index 7e9da18df64..b13b721e64c 100644 --- a/src/vs/base/parts/quickinput/browser/quickInput.ts +++ b/src/vs/base/parts/quickinput/browser/quickInput.ts @@ -125,6 +125,7 @@ type Visibilities = { list?: boolean; ok?: boolean; customButton?: boolean; + progressBar?: boolean; }; class QuickInput extends Disposable implements IQuickInput { @@ -406,8 +407,16 @@ class QuickPick extends QuickInput implements IQuickPi private _customButton = false; private _customButtonLabel: string | undefined; private _customButtonHover: string | undefined; + private _quickNavigate: IQuickNavigateConfiguration | undefined; - quickNavigate: IQuickNavigateConfiguration | undefined; + get quickNavigate() { + return this._quickNavigate; + } + + set quickNavigate(quickNavigate: IQuickNavigateConfiguration | undefined) { + this._quickNavigate = quickNavigate; + this.update(); + } get value() { return this._value; @@ -798,8 +807,12 @@ class QuickPick extends QuickInput implements IQuickPi if (!this.visible) { return; } + dom.toggleClass(this.ui.container, 'quick-navigate-mode', !!this._quickNavigate); const ok = this.ok === 'default' ? this.canSelectMany : this.ok; - this.ui.setVisibilities(this.canSelectMany ? { title: !!this.title || !!this.step, description: !!this.description, checkAll: true, inputBox: true, visibleCount: true, count: true, ok, list: true, message: !!this.validationMessage, customButton: this.customButton } : { title: !!this.title || !!this.step, description: !!this.description, inputBox: true, visibleCount: true, list: true, message: !!this.validationMessage, customButton: this.customButton, ok }); + const visibilities: Visibilities = this.canSelectMany ? + { title: !!this.title || !!this.step, description: !!this.description, checkAll: true, inputBox: !this._quickNavigate, progressBar: !this._quickNavigate, visibleCount: true, count: true, ok, list: true, message: !!this.validationMessage, customButton: this.customButton } : + { title: !!this.title || !!this.step, description: !!this.description, inputBox: !this._quickNavigate, progressBar: !this._quickNavigate, visibleCount: true, list: true, message: !!this.validationMessage, customButton: this.customButton, ok }; + this.ui.setVisibilities(visibilities); super.update(); if (this.ui.inputBox.value !== this.value) { this.ui.inputBox.value = this.value; @@ -864,6 +877,11 @@ class QuickPick extends QuickInput implements IQuickPi this.ui.customButton.label = this.customLabel || ''; this.ui.customButton.element.title = this.customHover || ''; this.ui.setComboboxAccessibility(true); + if (!visibilities.inputBox) { + // we need to move focus into the tree to detect keybindings + // properly when the input box is not visible (quick nav) + this.ui.list.domFocus(); + } } } @@ -1442,6 +1460,7 @@ export class QuickInputController extends Disposable { ui.okContainer.style.display = visibilities.ok ? '' : 'none'; ui.customButtonContainer.style.display = visibilities.customButton ? '' : 'none'; ui.message.style.display = visibilities.message ? '' : 'none'; + ui.progressBar.getContainer().style.display = visibilities.progressBar ? '' : 'none'; ui.list.display(!!visibilities.list); ui.container.classList[visibilities.checkAll ? 'add' : 'remove']('show-checkboxes'); this.updateLayout(); // TODO diff --git a/src/vs/platform/quickinput/browser/quickAccess.ts b/src/vs/platform/quickinput/browser/quickAccess.ts index ca96d93fdbe..4e42a64c51d 100644 --- a/src/vs/platform/quickinput/browser/quickAccess.ts +++ b/src/vs/platform/quickinput/browser/quickAccess.ts @@ -5,7 +5,7 @@ import { IQuickInputService, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; -import { IQuickAccessController, IQuickAccessProvider, IQuickAccessRegistry, Extensions, IQuickAccessProviderDescriptor } from 'vs/platform/quickinput/common/quickAccess'; +import { IQuickAccessController, IQuickAccessProvider, IQuickAccessRegistry, Extensions, IQuickAccessProviderDescriptor, IQuickAccessOptions } from 'vs/platform/quickinput/common/quickAccess'; import { Registry } from 'vs/platform/registry/common/platform'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -25,7 +25,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon super(); } - show(value = ''): void { + show(value = '', options?: IQuickAccessOptions): void { const disposables = new DisposableStore(); // Hide any previous picker if any @@ -39,7 +39,8 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon const picker = disposables.add(this.quickInputService.createQuickPick()); picker.placeholder = descriptor?.placeholder; picker.value = value; - picker.valueSelection = [value.length, value.length]; + picker.quickNavigate = options?.quickNavigateConfiguration; + picker.valueSelection = options?.inputSelection ? [options.inputSelection.start, options.inputSelection.end] : [value.length, value.length]; picker.contextKey = descriptor?.contextKey; picker.filterValue = (value: string) => value.substring(descriptor ? descriptor.prefix.length : 0); diff --git a/src/vs/platform/quickinput/common/quickAccess.ts b/src/vs/platform/quickinput/common/quickAccess.ts index 9da4698d02b..bdac015721d 100644 --- a/src/vs/platform/quickinput/common/quickAccess.ts +++ b/src/vs/platform/quickinput/common/quickAccess.ts @@ -3,19 +3,32 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickPick, IQuickPickItem, IQuickNavigateConfiguration } from 'vs/platform/quickinput/common/quickInput'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Registry } from 'vs/platform/registry/common/platform'; import { first, coalesce } from 'vs/base/common/arrays'; import { startsWith } from 'vs/base/common/strings'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +export interface IQuickAccessOptions { + + /** + * Allows to control the part of text in the input field that should be selected. + */ + inputSelection?: { start: number; end: number; }; + + /** + * Allows to enable quick navigate support in quick input. + */ + quickNavigateConfiguration?: IQuickNavigateConfiguration; +} + export interface IQuickAccessController { /** * Open the quick access picker with the optional value prefilled. */ - show(value?: string): void; + show(value?: string, options?: IQuickAccessOptions): void; } export interface IQuickAccessProvider { diff --git a/src/vs/workbench/browser/actions/windowActions.ts b/src/vs/workbench/browser/actions/windowActions.ts index f2c406df86a..725374a03c4 100644 --- a/src/vs/workbench/browser/actions/windowActions.ts +++ b/src/vs/workbench/browser/actions/windowActions.ts @@ -16,7 +16,7 @@ import { IsFullscreenContext } from 'vs/workbench/browser/contextkeys'; import { IsMacNativeContext, IsDevelopmentContext } from 'vs/platform/contextkey/common/contextkeys'; import { IWorkbenchActionRegistry, Extensions } from 'vs/workbench/common/actions'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { IQuickInputButton, IQuickInputService, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickInputButton, IQuickInputService, IQuickPickSeparator, IKeyMods } from 'vs/platform/quickinput/common/quickInput'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ILabelService } from 'vs/platform/label/common/label'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -27,7 +27,6 @@ import { URI } from 'vs/base/common/uri'; import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; import { FileKind } from 'vs/platform/files/common/files'; import { splitName } from 'vs/base/common/labels'; -import { IKeyMods } from 'vs/base/parts/quickopen/common/quickOpen'; import { isMacintosh } from 'vs/base/common/platform'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { inQuickOpenContext, getQuickNavigateHandler } from 'vs/workbench/browser/parts/quickopen/quickopen'; diff --git a/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts b/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts index 9da034ae867..5db013a2e95 100644 --- a/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts +++ b/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts @@ -26,7 +26,7 @@ import { EditorInput, IWorkbenchEditorConfiguration, IEditorInput } from 'vs/wor import { Component } from 'vs/workbench/common/component'; import { Event, Emitter } from 'vs/base/common/event'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import { QuickOpenHandler, QuickOpenHandlerDescriptor, IQuickOpenRegistry, Extensions, EditorQuickOpenEntry, CLOSE_ON_FOCUS_LOST_CONFIG, SEARCH_EDITOR_HISTORY, PRESERVE_INPUT_CONFIG } from 'vs/workbench/browser/quickopen'; +import { QuickOpenHandler, QuickOpenHandlerDescriptor, IQuickOpenRegistry, Extensions, EditorQuickOpenEntry, CLOSE_ON_FOCUS_LOST_CONFIG, SEARCH_EDITOR_HISTORY, PRESERVE_INPUT_CONFIG, ENABLE_EXPERIMENTAL_VERSION_CONFIG } from 'vs/workbench/browser/quickopen'; import * as errors from 'vs/base/common/errors'; import { IQuickOpenService, IShowOptions } from 'vs/platform/quickOpen/common/quickOpen'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -86,6 +86,10 @@ export class QuickOpenController extends Component implements IQuickOpenService private editorHistoryHandler: EditorHistoryHandler; private pendingGetResultsInvocation: CancellationTokenSource | null = null; + private get useNewExperimentalVersion() { + return this.configurationService.getValue(ENABLE_EXPERIMENTAL_VERSION_CONFIG) === true; + } + constructor( @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @INotificationService private readonly notificationService: INotificationService, @@ -95,7 +99,8 @@ export class QuickOpenController extends Component implements IQuickOpenService @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IEnvironmentService private readonly environmentService: IEnvironmentService, @IThemeService themeService: IThemeService, - @IStorageService storageService: IStorageService + @IStorageService storageService: IStorageService, + @IQuickInputService private readonly quickInputService: IQuickInputService ) { super(QuickOpenController.ID, themeService, storageService); @@ -125,26 +130,42 @@ export class QuickOpenController extends Component implements IQuickOpenService } navigate(next: boolean, quickNavigate?: IQuickNavigateConfiguration): void { - if (this.quickOpenWidget) { - this.quickOpenWidget.navigate(next, quickNavigate); + if (this.useNewExperimentalVersion) { + // already handled + } else { + if (this.quickOpenWidget) { + this.quickOpenWidget.navigate(next, quickNavigate); + } } } accept(): void { - if (this.quickOpenWidget && this.quickOpenWidget.isVisible()) { - this.quickOpenWidget.accept(); + if (this.useNewExperimentalVersion) { + // already handled + } else { + if (this.quickOpenWidget && this.quickOpenWidget.isVisible()) { + this.quickOpenWidget.accept(); + } } } focus(): void { - if (this.quickOpenWidget && this.quickOpenWidget.isVisible()) { - this.quickOpenWidget.focus(); + if (this.useNewExperimentalVersion) { + // already handled + } else { + if (this.quickOpenWidget && this.quickOpenWidget.isVisible()) { + this.quickOpenWidget.focus(); + } } } close(): void { - if (this.quickOpenWidget && this.quickOpenWidget.isVisible()) { - this.quickOpenWidget.hide(HideReason.CANCELED); + if (this.useNewExperimentalVersion) { + // already handled + } else { + if (this.quickOpenWidget && this.quickOpenWidget.isVisible()) { + this.quickOpenWidget.hide(HideReason.CANCELED); + } } } @@ -157,6 +178,12 @@ export class QuickOpenController extends Component implements IQuickOpenService } show(prefix?: string, options?: IShowOptions): Promise { + if (this.useNewExperimentalVersion) { + this.quickInputService.quickAccess.show(prefix, options); + + return Promise.resolve(); + } + let quickNavigateConfiguration = options ? options.quickNavigateConfiguration : undefined; let inputSelection = options ? options.inputSelection : undefined; let autoFocus = options ? options.autoFocus : undefined; diff --git a/src/vs/workbench/browser/quickopen.ts b/src/vs/workbench/browser/quickopen.ts index 82ee8ca9d46..73553e42cce 100644 --- a/src/vs/workbench/browser/quickopen.ts +++ b/src/vs/workbench/browser/quickopen.ts @@ -21,6 +21,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; export const CLOSE_ON_FOCUS_LOST_CONFIG = 'workbench.quickOpen.closeOnFocusLost'; export const PRESERVE_INPUT_CONFIG = 'workbench.quickOpen.preserveInput'; +export const ENABLE_EXPERIMENTAL_VERSION_CONFIG = 'workbench.quickOpen.enableExperimentalNewVersion'; export const SEARCH_EDITOR_HISTORY = 'search.quickOpen.includeHistory'; export interface IWorkbenchQuickOpenConfiguration { @@ -28,6 +29,9 @@ export interface IWorkbenchQuickOpenConfiguration { commandPalette: { history: number; preserveInput: boolean; + }, + quickOpen: { + enableExperimentalNewVersion: boolean; } }; } diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 2fa180e6968..055cb1f93eb 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -179,6 +179,11 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio 'description': nls.localize('workbench.quickOpen.preserveInput', "Controls whether the last typed input to Quick Open should be restored when opening it the next time."), 'default': false }, + 'workbench.quickOpen.enableExperimentalNewVersion': { + 'type': 'boolean', + 'description': nls.localize('workbench.quickOpen.enableExperimentalNewVersion', "Will use the new quick open implementation for testing purposes."), + 'default': false + }, 'workbench.settings.openDefaultSettings': { 'type': 'boolean', 'description': nls.localize('openDefaultSettings', "Controls whether opening settings also opens an editor showing all default settings."), From 50b115d68239f37837d1d26e687211bc1a47378a Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 19 Mar 2020 14:39:32 +0100 Subject: [PATCH 20/44] quick access - fix config related lookup regression --- .../quickaccess/gotoLineQuickAccess.ts | 16 +++---- .../quickaccess/gotoSymbolQuickAccess.ts | 18 ++++---- .../search/browser/anythingQuickAccess.ts | 42 ++++++++++--------- .../search/browser/symbolsQuickAccess.ts | 24 +++++------ 4 files changed, 51 insertions(+), 49 deletions(-) diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts index 4d801b49ac2..0e8c8f29aa5 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts @@ -18,14 +18,6 @@ export class GotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProv protected readonly onDidActiveTextEditorControlChange = this.editorService.onDidActiveEditorChange; - private readonly configuration = (() => { - const editorConfig = this.configurationService.getValue().workbench.editor; - - return { - openEditorPinned: !editorConfig.enablePreviewFromQuickOpen - }; - })(); - constructor( @IEditorService private readonly editorService: IEditorService, @IConfigurationService private readonly configurationService: IConfigurationService @@ -33,6 +25,14 @@ export class GotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProv super(); } + private get configuration() { + const editorConfig = this.configurationService.getValue().workbench.editor; + + return { + openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, + }; + } + protected get activeTextEditorControl() { return this.editorService.activeTextEditorControl; } diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts index 099fcd763b3..e3a213f9084 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts @@ -18,15 +18,6 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess protected readonly onDidActiveTextEditorControlChange = this.editorService.onDidActiveEditorChange; - private readonly configuration = (() => { - const editorConfig = this.configurationService.getValue().workbench.editor; - - return { - openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, - openSideBySideDirection: editorConfig.openSideBySideDirection - }; - })(); - constructor( @IEditorService private readonly editorService: IEditorService, @IConfigurationService private readonly configurationService: IConfigurationService @@ -36,6 +27,15 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess }); } + private get configuration() { + const editorConfig = this.configurationService.getValue().workbench.editor; + + return { + openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, + openSideBySideDirection: editorConfig.openSideBySideDirection + }; + } + protected get activeTextEditorControl() { return this.editorService.activeTextEditorControl; } diff --git a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts index c48c0441a0f..919f37af443 100644 --- a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts @@ -75,20 +75,6 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { - const editorConfig = this.configurationService.getValue().workbench.editor; - const searchConfig = this.configurationService.getValue(); - - return { - openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, - openSideBySideDirection: editorConfig.openSideBySideDirection, - includeSymbols: searchConfig.search.quickOpen.includeSymbols, - workspaceSymbolsFilter: searchConfig.search.quickOpen.workspaceSymbolsFilter, - includeHistory: searchConfig.search.quickOpen.includeHistory, - shortAutoSaveDelay: this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY - }; - })(); - constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @ISearchService private readonly searchService: ISearchService, @@ -108,6 +94,20 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider().workbench.editor; + const searchConfig = this.configurationService.getValue(); + + return { + openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, + openSideBySideDirection: editorConfig.openSideBySideDirection, + includeSymbols: searchConfig.search.quickOpen.includeSymbols, + workspaceSymbolsFilter: searchConfig.search.quickOpen.workspaceSymbolsFilter, + includeHistory: searchConfig.search.quickOpen.includeHistory, + shortAutoSaveDelay: this.filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY + }; + } + provide(picker: IQuickPick, token: CancellationToken): IDisposable { // Reset the pick state for this run @@ -224,10 +224,11 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { + const configuration = this.configuration; // Just return all history entries if not searching if (!query.value) { - return this.historyService.getHistory().map(editor => this.createAnythingPick(editor)); + return this.historyService.getHistory().map(editor => this.createAnythingPick(editor, configuration)); } if (!this.configuration.includeHistory) { @@ -245,7 +246,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider !excludes.has(resource)) - .map(resource => this.createAnythingPick(resource)); + .map(resource => this.createAnythingPick(resource, configuration)); } private async doFileSearch(query: IPreparedQuery, token: CancellationToken): Promise { @@ -466,7 +468,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider { - const openSideBySideDirection = this.configuration.openSideBySideDirection; + const openSideBySideDirection = configuration.openSideBySideDirection; const buttons: IQuickInputButton[] = []; // Open to side / below diff --git a/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts b/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts index 0560cc5df2b..74a9fa14c5e 100644 --- a/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts @@ -47,18 +47,7 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider { - const editorConfig = this.configurationService.getValue().workbench.editor; - const searchConfig = this.configurationService.getValue(); - - return { - openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, - openSideBySideDirection: editorConfig.openSideBySideDirection, - workspaceSymbolsFilter: searchConfig.search.quickOpen.workspaceSymbolsFilter - }; - })(); - - private readonly delayer = this._register(new ThrottledDelayer(SymbolsQuickAccessProvider.TYPING_SEARCH_DELAY)); + private delayer = this._register(new ThrottledDelayer(SymbolsQuickAccessProvider.TYPING_SEARCH_DELAY)); private readonly resourceExcludeMatcher = this._register(createResourceExcludeMatcher(this.instantiationService, this.configurationService)); @@ -72,6 +61,17 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider().workbench.editor; + const searchConfig = this.configurationService.getValue(); + + return { + openEditorPinned: !editorConfig.enablePreviewFromQuickOpen, + openSideBySideDirection: editorConfig.openSideBySideDirection, + workspaceSymbolsFilter: searchConfig.search.quickOpen.workspaceSymbolsFilter + }; + } + protected getPicks(filter: string, disposables: DisposableStore, token: CancellationToken): Promise> { return this.getSymbolPicks(filter, { skipLocal: this.configuration.workspaceSymbolsFilter === 'reduced' }, token); } From 4c00ba9cf0ea0db49e378df274039719ec539154 Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Thu, 19 Mar 2020 10:11:33 -0400 Subject: [PATCH 21/44] Fixes view progress showing when it shouldn't --- src/vs/workbench/browser/parts/views/viewPaneContainer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index 6264a15277d..24591c76064 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -336,7 +336,7 @@ export abstract class ViewPane extends Pane implements IView { } if (this.progressIndicator === undefined) { - this.progressIndicator = this.instantiationService.createInstance(CompositeProgressIndicator, assertIsDefined(this.progressBar), this.id, this.isVisible()); + this.progressIndicator = this.instantiationService.createInstance(CompositeProgressIndicator, assertIsDefined(this.progressBar), this.id, this.isBodyVisible()); } return this.progressIndicator; } From 17958017682a60f58d0b190dc7c5eb62e39f2b18 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 19 Mar 2020 15:28:05 +0100 Subject: [PATCH 22/44] add min max height for zone widgets, fixes https://github.com/microsoft/vscode/issues/92974 --- src/vs/editor/contrib/zoneWidget/zoneWidget.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/vs/editor/contrib/zoneWidget/zoneWidget.ts b/src/vs/editor/contrib/zoneWidget/zoneWidget.ts index 6a0346a2137..aa731fe46b5 100644 --- a/src/vs/editor/contrib/zoneWidget/zoneWidget.ts +++ b/src/vs/editor/contrib/zoneWidget/zoneWidget.ts @@ -360,10 +360,8 @@ export abstract class ZoneWidget implements IHorizontalSashLayoutProvider { const lineHeight = this.editor.getOption(EditorOption.lineHeight); // adjust heightInLines to viewport - const maxHeightInLines = (this.editor.getLayoutInfo().height / lineHeight) * 0.8; - if (heightInLines >= maxHeightInLines) { - heightInLines = maxHeightInLines; - } + const maxHeightInLines = Math.max(12, (this.editor.getLayoutInfo().height / lineHeight) * 0.8); + heightInLines = Math.min(heightInLines, maxHeightInLines); let arrowHeight = 0; let frameThickness = 0; From 6e0649086a1321232b98fba1b3aa656a37054778 Mon Sep 17 00:00:00 2001 From: SteVen Batten Date: Thu, 19 Mar 2020 10:45:53 -0400 Subject: [PATCH 23/44] remove transition as it lags on low spec dev --- src/vs/workbench/browser/media/part.css | 14 +++++++++----- .../parts/activitybar/media/activitybarpart.css | 6 ------ 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/browser/media/part.css b/src/vs/workbench/browser/media/part.css index e77d77ec0d0..f31b7454f8c 100644 --- a/src/vs/workbench/browser/media/part.css +++ b/src/vs/workbench/browser/media/part.css @@ -8,16 +8,20 @@ overflow: hidden; } + +.monaco-workbench .part > .drop-block-overlay.visible { + display: block; + backdrop-filter: brightness(97%) blur(2px); + opacity: 1; + z-index: 10; +} + .monaco-workbench .part > .drop-block-overlay { - visibility: hidden; /* use visibility to ensure transitions */ - transition-property: opacity; - transition-timing-function: linear; - transition-duration: 250ms; + display: none; width: 100%; height: 100%; position: absolute; top: 0; - opacity: 0; pointer-events: none; } diff --git a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css b/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css index 677bed9952a..6e3ff5bf0f4 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css +++ b/src/vs/workbench/browser/parts/activitybar/media/activitybarpart.css @@ -7,12 +7,6 @@ width: 48px; } -.monaco-workbench .part > .drop-block-overlay.visible { - visibility: visible; - backdrop-filter: brightness(97%) blur(2px); - opacity: 1; -} - .monaco-workbench .activitybar > .content { height: 100%; display: flex; From f50e303304283cc06044c5565f0992cd690666b3 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 19 Mar 2020 09:50:13 +0100 Subject: [PATCH 24/44] #86678 define snippets home property in env service --- src/vs/platform/environment/common/environment.ts | 1 + src/vs/platform/environment/node/environmentService.ts | 3 +++ .../workbench/contrib/snippets/browser/configureSnippets.ts | 4 ++-- src/vs/workbench/contrib/snippets/browser/snippetsService.ts | 2 +- .../contrib/telemetry/browser/telemetry.contribution.ts | 4 ++-- .../services/environment/browser/environmentService.ts | 3 +++ 6 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index ee908ae2ddd..f5c2a935247 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -124,6 +124,7 @@ export interface IEnvironmentService extends IUserHomeProvider { keybindingsResource: URI; keyboardLayoutResource: URI; argvResource: URI; + snippetsHome: URI; // sync resources userDataSyncLogResource: URI; diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index 07c266d77a1..55ed6cb34e1 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -142,6 +142,9 @@ export class EnvironmentService implements IEnvironmentService { return URI.file(path.join(this.userHome, product.dataFolderName, 'argv.json')); } + @memoize + get snippetsHome(): URI { return resources.joinPath(this.userRoamingDataHome, 'snippets'); } + @memoize get isExtensionDevelopment(): boolean { return !!this._args.extensionDevelopmentPath; } diff --git a/src/vs/workbench/contrib/snippets/browser/configureSnippets.ts b/src/vs/workbench/contrib/snippets/browser/configureSnippets.ts index 7906d62de80..755b5b40fed 100644 --- a/src/vs/workbench/contrib/snippets/browser/configureSnippets.ts +++ b/src/vs/workbench/contrib/snippets/browser/configureSnippets.ts @@ -87,7 +87,7 @@ async function computePicks(snippetService: ISnippetsService, envService: IEnvir } } - const dir = joinPath(envService.userRoamingDataHome, 'snippets'); + const dir = envService.snippetsHome; for (const mode of modeService.getRegisteredModes()) { const label = modeService.getLanguageName(mode); if (label && !seen.has(mode)) { @@ -219,7 +219,7 @@ CommandsRegistry.registerCommand(id, async (accessor): Promise => { const globalSnippetPicks: SnippetPick[] = [{ scope: nls.localize('new.global_scope', 'global'), label: nls.localize('new.global', "New Global Snippets file..."), - uri: joinPath(envService.userRoamingDataHome, 'snippets') + uri: envService.snippetsHome }]; const workspaceSnippetPicks: SnippetPick[] = []; diff --git a/src/vs/workbench/contrib/snippets/browser/snippetsService.ts b/src/vs/workbench/contrib/snippets/browser/snippetsService.ts index f97863e2398..0355799e9ad 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetsService.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetsService.ts @@ -289,7 +289,7 @@ class SnippetsService implements ISnippetsService { } private _initUserSnippets(): Promise { - const userSnippetsFolder = resources.joinPath(this._environmentService.userRoamingDataHome, 'snippets'); + const userSnippetsFolder = this._environmentService.snippetsHome; return this._fileService.createFolder(userSnippetsFolder).then(() => this._initFolderSnippets(SnippetSource.User, userSnippetsFolder, this._disposables)); } diff --git a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts index ee6a1f86d1e..bd8c574e3d5 100644 --- a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts +++ b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts @@ -20,7 +20,7 @@ import { configurationTelemetry } from 'vs/platform/telemetry/common/telemetryUt import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { ITextFileService, ITextFileSaveEvent, ITextFileLoadEvent } from 'vs/workbench/services/textfile/common/textfiles'; -import { extname, basename, isEqual, isEqualOrParent, joinPath } from 'vs/base/common/resources'; +import { extname, basename, isEqual, isEqualOrParent } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { guessMimeTypes } from 'vs/base/common/mime'; @@ -175,7 +175,7 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr } // Check for snippets - if (isEqualOrParent(resource, joinPath(this.environmentService.userRoamingDataHome, 'snippets'))) { + if (isEqualOrParent(resource, this.environmentService.snippetsHome)) { return 'snippets'; } diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index a4ee8d92f87..93e05e6d14a 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -102,6 +102,9 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment @memoize get argvResource(): URI { return joinPath(this.userRoamingDataHome, 'argv.json'); } + @memoize + get snippetsHome(): URI { return joinPath(this.userRoamingDataHome, 'snippets'); } + @memoize get userDataSyncHome(): URI { return joinPath(this.userRoamingDataHome, 'sync'); } From 9dca5617344422f33cf0fb825f4b2add61e2c63b Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 19 Mar 2020 10:00:32 +0100 Subject: [PATCH 25/44] #86678 Implement syncing snippets --- .../userDataSync/common/snippetsMerge.ts | 169 ++++++++ .../userDataSync/common/snippetsSync.ts | 399 ++++++++++++++++++ .../userDataSync/common/userDataSync.ts | 10 +- .../common/userDataSyncService.ts | 5 +- .../userDataSync/browser/userDataSync.ts | 106 ++++- 5 files changed, 661 insertions(+), 28 deletions(-) create mode 100644 src/vs/platform/userDataSync/common/snippetsMerge.ts create mode 100644 src/vs/platform/userDataSync/common/snippetsSync.ts diff --git a/src/vs/platform/userDataSync/common/snippetsMerge.ts b/src/vs/platform/userDataSync/common/snippetsMerge.ts new file mode 100644 index 00000000000..b16ab9ce883 --- /dev/null +++ b/src/vs/platform/userDataSync/common/snippetsMerge.ts @@ -0,0 +1,169 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { values } from 'vs/base/common/map'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { deepClone } from 'vs/base/common/objects'; + +export interface IMergeResult { + added: IStringDictionary; + updated: IStringDictionary; + removed: string[]; + conflicts: string[]; + remote: IStringDictionary | null; +} + +export function merge(local: IStringDictionary, remote: IStringDictionary | null, base: IStringDictionary | null): IMergeResult { + const added: IStringDictionary = {}; + const updated: IStringDictionary = {}; + const removed: string[] = []; + + if (!remote) { + return { + added, + removed, + updated, + conflicts: [], + remote: local + }; + } + + const localToRemote = compare(local, remote); + if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) { + // No changes found between local and remote. + return { + added, + removed, + updated, + conflicts: [], + remote: null + }; + } + + const baseToLocal = compare(base, local); + const baseToRemote = compare(base, remote); + const conflicts: Set = new Set(); + let remoteContent: IStringDictionary = deepClone(remote); + + // Removed snippets in Local + for (const key of values(baseToLocal.removed)) { + // Conflict - Got updated in remote. + if (baseToRemote.updated.has(key)) { + // Add to local + added[key] = remote[key]; + } + // Remove it in remote + else { + delete remoteContent[key]; + } + } + + // Removed snippets in Remote + for (const key of values(baseToRemote.removed)) { + if (conflicts.has(key)) { + continue; + } + // Conflict - Got updated in local + if (baseToLocal.updated.has(key)) { + conflicts.add(key); + } + // Also remove in Local + else { + removed.push(key); + } + } + + // Updated snippets in Local + for (const key of values(baseToLocal.updated)) { + if (conflicts.has(key)) { + continue; + } + // Got updated in remote + if (baseToRemote.updated.has(key)) { + // Has different value + if (localToRemote.updated.has(key)) { + conflicts.add(key); + } + } else { + remoteContent[key] = local[key]; + } + } + + // Updated snippets in Remote + for (const key of values(baseToRemote.updated)) { + if (conflicts.has(key)) { + continue; + } + // Got updated in local + if (baseToLocal.updated.has(key)) { + // Has different value + if (localToRemote.updated.has(key)) { + conflicts.add(key); + } + } else if (local[key] !== undefined) { + updated[key] = remote[key]; + } + } + + // Added snippets in Local + for (const key of values(baseToLocal.added)) { + if (conflicts.has(key)) { + continue; + } + // Got added in remote + if (baseToRemote.added.has(key)) { + // Has different value + if (localToRemote.updated.has(key)) { + conflicts.add(key); + } + } else { + remoteContent[key] = local[key]; + } + } + + // Added snippets in remote + for (const key of values(baseToRemote.added)) { + if (conflicts.has(key)) { + continue; + } + // Got added in local + if (baseToLocal.added.has(key)) { + // Has different value + if (localToRemote.updated.has(key)) { + conflicts.add(key); + } + } else { + added[key] = remote[key]; + } + } + + return { added, removed, updated, conflicts: values(conflicts), remote: areSame(remote, remoteContent) ? null : remoteContent }; +} + +function compare(from: IStringDictionary | null, to: IStringDictionary | null): { added: Set, removed: Set, updated: Set } { + const fromKeys = from ? Object.keys(from) : []; + const toKeys = to ? Object.keys(to) : []; + const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set()); + const removed = fromKeys.filter(key => toKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set()); + const updated: Set = new Set(); + + for (const key of fromKeys) { + if (removed.has(key)) { + continue; + } + const fromSnippet = from![key]!; + const toSnippet = to![key]!; + if (fromSnippet !== toSnippet) { + updated.add(key); + } + } + + return { added, removed, updated }; +} + +function areSame(a: IStringDictionary, b: IStringDictionary): boolean { + const { added, removed, updated } = compare(a, b); + return added.size === 0 && removed.size === 0 && updated.size === 0; +} diff --git a/src/vs/platform/userDataSync/common/snippetsSync.ts b/src/vs/platform/userDataSync/common/snippetsSync.ts new file mode 100644 index 00000000000..6a23ddade0b --- /dev/null +++ b/src/vs/platform/userDataSync/common/snippetsSync.ts @@ -0,0 +1,399 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME, UserDataSyncError, UserDataSyncErrorCode } from 'vs/platform/userDataSync/common/userDataSync'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IFileService, FileChangesEvent, IFileStat, IFileContent, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { AbstractSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { URI } from 'vs/base/common/uri'; +import { joinPath, extname, relativePath, isEqualOrParent, isEqual, basename } from 'vs/base/common/resources'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { merge } from 'vs/platform/userDataSync/common/snippetsMerge'; +import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; +import { CancellationToken } from 'vs/base/common/cancellation'; + +interface ISyncPreviewResult { + readonly local: IStringDictionary; + readonly remoteUserData: IRemoteUserData; + readonly lastSyncUserData: IRemoteUserData | null; + readonly added: IStringDictionary; + readonly updated: IStringDictionary; + readonly removed: string[]; + readonly conflicts: Conflict[]; + readonly remote: IStringDictionary | null; +} + +export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser { + + protected readonly version: number = 1; + private readonly snippetsFolder: URI; + private readonly snippetsPreviewFolder: URI; + private syncPreviewResultPromise: CancelablePromise | null = null; + + constructor( + @IEnvironmentService environmentService: IEnvironmentService, + @IFileService fileService: IFileService, + @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, + @IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, + @IUserDataSyncLogService logService: IUserDataSyncLogService, + @IConfigurationService configurationService: IConfigurationService, + @IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService, + @ITelemetryService telemetryService: ITelemetryService, + ) { + super(SyncResource.Snippets, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); + this.snippetsFolder = environmentService.snippetsHome; + this.snippetsPreviewFolder = joinPath(this.syncFolder, PREVIEW_DIR_NAME); + this._register(this.fileService.watch(environmentService.userRoamingDataHome)); + this._register(this.fileService.watch(this.snippetsFolder)); + this._register(this.fileService.onDidFilesChange(e => this.onFileChanges(e))); + } + + private onFileChanges(e: FileChangesEvent): void { + if (!e.changes.some(change => isEqualOrParent(change.resource, this.snippetsFolder))) { + return; + } + if (!this.isEnabled()) { + return; + } + // Sync again if local file has changed and current status is in conflicts + if (this.status === SyncStatus.HasConflicts) { + this.syncPreviewResultPromise!.then(result => { + this.cancel(); + this.doSync(result.remoteUserData, result.lastSyncUserData).then(status => this.setStatus(status)); + }); + } + // Otherwise fire change event + else { + this._onDidChangeLocal.fire(); + } + } + + async pull(): Promise { + if (!this.isEnabled()) { + this.logService.info(`${this.syncResourceLogLabel}: Skipped pulling snippets as it is disabled.`); + return; + } + + this.stop(); + + try { + this.logService.info(`${this.syncResourceLogLabel}: Started pulling snippets...`); + this.setStatus(SyncStatus.Syncing); + + const lastSyncUserData = await this.getLastSyncUserData(); + const remoteUserData = await this.getRemoteUserData(lastSyncUserData); + + if (remoteUserData.syncData !== null) { + const local = await this.getSnippetsFileContents(); + const localSnippets = this.toSnippetsContents(local); + const remoteSnippets = this.parseSnippets(remoteUserData.syncData); + const { added, updated, remote, removed } = merge(localSnippets, remoteSnippets, localSnippets); + this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve({ + added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [] + })); + await this.apply(); + } + + // No remote exists to pull + else { + this.logService.info(`${this.syncResourceLogLabel}: Remote snippets does not exist.`); + } + + this.logService.info(`${this.syncResourceLogLabel}: Finished pulling snippets.`); + } finally { + this.setStatus(SyncStatus.Idle); + } + } + + async push(): Promise { + if (!this.isEnabled()) { + this.logService.info(`${this.syncResourceLogLabel}: Skipped pushing snippets as it is disabled.`); + return; + } + + this.stop(); + + try { + this.logService.info(`${this.syncResourceLogLabel}: Started pushing snippets...`); + this.setStatus(SyncStatus.Syncing); + + const local = await this.getSnippetsFileContents(); + const localSnippets = this.toSnippetsContents(local); + const { added, removed, updated, remote } = merge(localSnippets, null, null); + const lastSyncUserData = await this.getLastSyncUserData(); + const remoteUserData = await this.getRemoteUserData(lastSyncUserData); + this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve({ + added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [] + })); + + await this.apply(true); + + this.logService.info(`${this.syncResourceLogLabel}: Finished pushing snippets.`); + } finally { + this.setStatus(SyncStatus.Idle); + } + + } + + async stop(): Promise { + await this.clearConflicts(); + this.cancel(); + this.logService.info(`${this.syncResourceLogLabel}: Stopped synchronizing ${this.syncResourceLogLabel}.`); + this.setStatus(SyncStatus.Idle); + } + + async getConflictContent(conflictResource: URI): Promise { + if (isEqualOrParent(conflictResource.with({ scheme: this.syncFolder.scheme }), this.snippetsPreviewFolder) && this.syncPreviewResultPromise) { + const result = await this.syncPreviewResultPromise; + const key = relativePath(this.snippetsPreviewFolder, conflictResource.with({ scheme: this.snippetsPreviewFolder.scheme }))!; + if (conflictResource.scheme === this.snippetsPreviewFolder.scheme) { + return result.local[key] ? result.local[key].value.toString() : null; + } else if (result.remoteUserData && result.remoteUserData.syncData) { + const snippets = this.parseSnippets(result.remoteUserData.syncData); + return snippets[key] || null; + } + } + return null; + } + + async getRemoteContent(ref?: string, fragment?: string): Promise { + const content = await super.getRemoteContent(ref); + if (content !== null && fragment) { + return this.getFragment(content, fragment); + } + return content; + } + + async getLocalBackupContent(ref?: string, fragment?: string): Promise { + let content = await super.getLocalBackupContent(ref); + if (content !== null && fragment) { + return this.getFragment(content, fragment); + } + return content; + } + + private getFragment(content: string, fragment: string): string | null { + const syncData = this.parseSyncData(content); + return syncData ? this.getFragmentFromSyncData(syncData, fragment) : null; + } + + private getFragmentFromSyncData(syncData: ISyncData, fragment: string): string | null { + switch (fragment) { + case 'snippets': + return syncData.content; + default: + const remoteSnippets = this.parseSnippets(syncData); + return remoteSnippets[fragment] || null; + } + } + + async acceptConflict(conflictResource: URI, content: string): Promise { + const conflict = this.conflicts.filter(({ local, remote }) => isEqual(local, conflictResource) || isEqual(remote, conflictResource))[0]; + if (this.status === SyncStatus.HasConflicts && conflict) { + const key = relativePath(this.snippetsPreviewFolder, conflict.local)!; + const result = await this.syncPreviewResultPromise!; + if (content) { + if (result.local[key]) { + result.updated[key] = content; + } else { + result.added[key] = content; + } + } else { + result.removed.push(key); + } + await this.fileService.del(conflict.local); + this.setConflicts(this.conflicts.filter(c => c !== conflict)); + if (!this.conflicts.length) { + await this.apply(); + this.setStatus(SyncStatus.Idle); + } + } + } + + async hasLocalData(): Promise { + try { + const localSnippets = await this.getSnippetsFileContents(); + if (Object.keys(localSnippets).length) { + return true; + } + } catch (error) { + /* ignore error */ + } + return false; + } + + protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { + try { + const previewResult = await this.getPreview(remoteUserData, lastSyncUserData); + if (previewResult.conflicts.length) { + return SyncStatus.HasConflicts; + } + await this.apply(); + return SyncStatus.Idle; + } catch (e) { + this.syncPreviewResultPromise = null; + if (e instanceof UserDataSyncError) { + switch (e.code) { + case UserDataSyncErrorCode.LocalPreconditionFailed: + // Rejected as there is a new local version. Syncing again. + this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize snippets as there is a new local version available. Synchronizing again...`); + return this.performSync(remoteUserData, lastSyncUserData); + } + } + throw e; + } + } + + private getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { + if (!this.syncPreviewResultPromise) { + this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(remoteUserData, lastSyncUserData, token)); + } + return this.syncPreviewResultPromise; + } + + protected cancel(): void { + if (this.syncPreviewResultPromise) { + this.syncPreviewResultPromise.cancel(); + this.syncPreviewResultPromise = null; + } + } + + private async clearConflicts(): Promise { + if (this.conflicts.length) { + await Promise.all(this.conflicts.map(({ local }) => this.fileService.del(local))); + this.setConflicts([]); + } + } + + private async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { + await this.clearConflicts(); + + const remoteSnippets: IStringDictionary | null = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null; + const lastSyncSnippets: IStringDictionary | null = lastSyncUserData ? this.parseSnippets(lastSyncUserData.syncData!) : null; + + const local = await this.getSnippetsFileContents(); + const localSnippets = this.toSnippetsContents(local); + + if (remoteSnippets) { + this.logService.trace(`${this.syncResourceLogLabel}: Merging remote snippets with local snippets...`); + } else { + this.logService.trace(`${this.syncResourceLogLabel}: Remote snippets does not exist. Synchronizing snippets for the first time.`); + } + + const mergeResult = merge(localSnippets, remoteSnippets, lastSyncSnippets); + const conflicts: Conflict[] = []; + for (const key of mergeResult.conflicts) { + const localPreview = joinPath(this.snippetsPreviewFolder, key); + conflicts.push({ local: localPreview, remote: localPreview.with({ scheme: USER_DATA_SYNC_SCHEME }) }); + const content = local[key]; + if (!token.isCancellationRequested) { + await this.fileService.writeFile(localPreview, content ? content.value : VSBuffer.fromString('')); + } + } + + this.setConflicts(!token.isCancellationRequested ? conflicts : []); + + return { remoteUserData, local, lastSyncUserData, added: mergeResult.added, removed: mergeResult.removed, updated: mergeResult.updated, conflicts, remote: mergeResult.remote }; + } + + private async apply(forcePush?: boolean): Promise { + if (!this.syncPreviewResultPromise) { + return; + } + + let { added, removed, updated, local, remote, remoteUserData, lastSyncUserData } = await this.syncPreviewResultPromise; + + const hasChanges = Object.keys(added).length || removed.length || Object.keys(updated).length || remote; + + if (!hasChanges) { + this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing snippets.`); + } + + if (Object.keys(added).length || removed.length || Object.keys(updated).length) { + // back up all snippets + await this.backupLocal(JSON.stringify(this.toSnippetsContents(local))); + await this.updateLocalSnippets(added, removed, updated, local); + } + + if (remote) { + // update remote + this.logService.trace(`${this.syncResourceLogLabel}: Updating remote snippets...`); + const content = JSON.stringify(remote); + remoteUserData = await this.updateRemoteUserData(content, forcePush ? null : remoteUserData.ref); + this.logService.info(`${this.syncResourceLogLabel}: Updated remote snippets`); + } + + if (lastSyncUserData?.ref !== remoteUserData.ref) { + // update last sync + this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized snippets...`); + await this.updateLastSyncUserData(remoteUserData); + this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized snippets`); + } + + this.syncPreviewResultPromise = null; + } + + private async updateLocalSnippets(added: IStringDictionary, removed: string[], updated: IStringDictionary, local: IStringDictionary): Promise { + for (const key of removed) { + const resource = joinPath(this.snippetsFolder, key); + this.logService.trace(`${this.syncResourceLogLabel}: Deleting snippet...`, basename(resource)); + await this.fileService.del(resource); + this.logService.info(`${this.syncResourceLogLabel}: Deleted snippet`, basename(resource)); + } + + for (const key of Object.keys(added)) { + const resource = joinPath(this.snippetsFolder, key); + this.logService.trace(`${this.syncResourceLogLabel}: Creating snippet...`, basename(resource)); + await this.fileService.createFile(resource, VSBuffer.fromString(added[key]), { overwrite: false }); + this.logService.info(`${this.syncResourceLogLabel}: Created snippet`, basename(resource)); + } + + for (const key of Object.keys(updated)) { + const resource = joinPath(this.snippetsFolder, key); + this.logService.trace(`${this.syncResourceLogLabel}: Updating snippet...`, basename(resource)); + await this.fileService.writeFile(resource, VSBuffer.fromString(updated[key]), local[key]); + this.logService.info(`${this.syncResourceLogLabel}: Updated snippet`, basename(resource)); + } + } + + private parseSnippets(syncData: ISyncData): IStringDictionary { + return JSON.parse(syncData.content); + } + + private toSnippetsContents(snippetsFileContents: IStringDictionary): IStringDictionary { + const snippets: IStringDictionary = {}; + for (const key of Object.keys(snippetsFileContents)) { + snippets[key] = snippetsFileContents[key].value.toString(); + } + return snippets; + } + + private async getSnippetsFileContents(): Promise> { + const snippets: IStringDictionary = {}; + let stat: IFileStat; + try { + stat = await this.fileService.resolve(this.snippetsFolder); + } catch (e) { + // No snippets + if (e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) { + return snippets; + } else { + throw e; + } + } + for (const entry of stat.children || []) { + const resource = entry.resource; + if (extname(resource) === '.json') { + const key = relativePath(this.snippetsFolder, resource)!; + const content = await this.fileService.readFile(resource); + snippets[key] = content; + } + } + return snippets; + } +} diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 4785ab9d216..553d241c96c 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -138,10 +138,11 @@ export function getUserDataSyncStore(productService: IProductService, configurat export const enum SyncResource { Settings = 'settings', Keybindings = 'keybindings', + Snippets = 'snippets', Extensions = 'extensions', GlobalState = 'globalState' } -export const ALL_SYNC_RESOURCES: SyncResource[] = [SyncResource.Settings, SyncResource.Keybindings, SyncResource.Extensions, SyncResource.GlobalState]; +export const ALL_SYNC_RESOURCES: SyncResource[] = [SyncResource.Settings, SyncResource.Keybindings, SyncResource.Snippets, SyncResource.Extensions, SyncResource.GlobalState]; export interface IUserDataManifest { latest?: Record @@ -373,10 +374,3 @@ export function getSyncResourceFromLocalPreview(localPreview: URI, environmentSe localPreview = localPreview.with({ scheme: environmentService.userDataSyncHome.scheme }); return ALL_SYNC_RESOURCES.filter(syncResource => isEqualOrParent(localPreview, joinPath(environmentService.userDataSyncHome, syncResource, PREVIEW_DIR_NAME)))[0]; } -export function getSyncResourceFromRemotePreview(remotePreview: URI, environmentService: IEnvironmentService): SyncResource | undefined { - if (remotePreview.scheme !== USER_DATA_SYNC_SCHEME) { - return undefined; - } - remotePreview = remotePreview.with({ scheme: environmentService.userDataSyncHome.scheme }); - return ALL_SYNC_RESOURCES.filter(syncResource => isEqualOrParent(remotePreview, joinPath(environmentService.userDataSyncHome, syncResource, PREVIEW_DIR_NAME)))[0]; -} diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index 4049ab3c30d..92a79046411 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -18,6 +18,7 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag import { URI } from 'vs/base/common/uri'; import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync'; import { isEqual } from 'vs/base/common/resources'; +import { SnippetsSynchroniser } from 'vs/platform/userDataSync/common/snippetsSync'; type SyncErrorClassification = { source: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; @@ -55,6 +56,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ private readonly settingsSynchroniser: SettingsSynchroniser; private readonly keybindingsSynchroniser: KeybindingsSynchroniser; + private readonly snippetsSynchroniser: SnippetsSynchroniser; private readonly extensionsSynchroniser: ExtensionsSynchroniser; private readonly globalStateSynchroniser: GlobalStateSynchroniser; @@ -68,9 +70,10 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ super(); this.settingsSynchroniser = this._register(this.instantiationService.createInstance(SettingsSynchroniser)); this.keybindingsSynchroniser = this._register(this.instantiationService.createInstance(KeybindingsSynchroniser)); + this.snippetsSynchroniser = this._register(this.instantiationService.createInstance(SnippetsSynchroniser)); this.globalStateSynchroniser = this._register(this.instantiationService.createInstance(GlobalStateSynchroniser)); this.extensionsSynchroniser = this._register(this.instantiationService.createInstance(ExtensionsSynchroniser)); - this.synchronisers = [this.settingsSynchroniser, this.keybindingsSynchroniser, this.globalStateSynchroniser, this.extensionsSynchroniser]; + this.synchronisers = [this.settingsSynchroniser, this.keybindingsSynchroniser, this.snippetsSynchroniser, this.globalStateSynchroniser, this.extensionsSynchroniser]; this.updateStatus(); if (this.userDataSyncStoreService.userDataSyncStore) { diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 9b778c41ba5..3f972a36c94 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -9,7 +9,7 @@ import { canceled, isPromiseCanceledError } from 'vs/base/common/errors'; import { Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, dispose, MutableDisposable, toDisposable, IDisposable } from 'vs/base/common/lifecycle'; import { isWeb } from 'vs/base/common/platform'; -import { isEqual } from 'vs/base/common/resources'; +import { isEqual, basename } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import type { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; @@ -32,7 +32,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { CONTEXT_SYNC_STATE, getUserDataSyncStore, ISyncConfiguration, IUserDataAutoSyncService, IUserDataSyncService, IUserDataSyncStore, registerConfiguration, SyncResource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME, IUserDataSyncEnablementService, CONTEXT_SYNC_ENABLEMENT, - SyncResourceConflicts, Conflict, getSyncResourceFromLocalPreview, getSyncResourceFromRemotePreview + SyncResourceConflicts, Conflict, getSyncResourceFromLocalPreview } from 'vs/platform/userDataSync/common/userDataSync'; import { FloatingClickWidget } from 'vs/workbench/browser/parts/editor/editorWidgets'; import { GLOBAL_ACTIVITY_ID } from 'vs/workbench/common/activity'; @@ -69,6 +69,7 @@ function getSyncAreaLabel(source: SyncResource): string { switch (source) { case SyncResource.Settings: return localize('settings', "Settings"); case SyncResource.Keybindings: return localize('keybindings', "Keyboard Shortcuts"); + case SyncResource.Snippets: return localize('snippets', "User Snippets"); case SyncResource.Extensions: return localize('extensions', "Extensions"); case SyncResource.GlobalState: return localize('ui state label', "UI State"); } @@ -100,6 +101,7 @@ const signInCommand = { id: 'workbench.userData.actions.signin', title: localize const stopSyncCommand = { id: 'workbench.userData.actions.stopSync', title(authenticationProviderId: string, account: AuthenticationSession | undefined, authenticationService: IAuthenticationService) { return getIdentityTitle(localize('stop sync', "Sync: Turn off Sync"), authenticationProviderId, account, authenticationService); } }; const resolveSettingsConflictsCommand = { id: 'workbench.userData.actions.resolveSettingsConflicts', title: localize('showConflicts', "Sync: Show Settings Conflicts") }; const resolveKeybindingsConflictsCommand = { id: 'workbench.userData.actions.resolveKeybindingsConflicts', title: localize('showKeybindingsConflicts', "Sync: Show Keybindings Conflicts") }; +const resolveSnippetsConflictsCommand = { id: 'workbench.userData.actions.resolveSnippetsConflicts', title: localize('showSnippetsConflicts', "Sync: Show User Snippets Conflicts") }; const configureSyncCommand = { id: 'workbench.userData.actions.configureSync', title: localize('configure sync', "Sync: Configure") }; const showSyncActivityCommand = { id: 'workbench.userData.actions.showSyncActivity', title(userDataSyncService: IUserDataSyncService): string { @@ -291,6 +293,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo if (conflicts.length) { const conflictsSources: SyncResource[] = conflicts.map(conflict => conflict.syncResource); this.conflictsSources.set(conflictsSources.join(',')); + if (conflictsSources.indexOf(SyncResource.Snippets) !== -1) { + this.registerShowSnippetsConflictsAction(); + } // Clear and dispose conflicts those were cleared this.conflictsDisposables.forEach((disposable, conflictsSource) => { @@ -301,8 +306,19 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo }); for (const { syncResource, conflicts } of this.userDataSyncService.conflicts) { - const conflictsEditorInput = this.getConflictsEditorInput(syncResource); - if (!conflictsEditorInput && !this.conflictsDisposables.has(syncResource)) { + const conflictsEditorInputs = this.getConflictsEditorInputs(syncResource); + + // close stale conflicts editor previews + if (conflictsEditorInputs.length) { + conflictsEditorInputs.forEach(input => { + if (!conflicts.some(({ local }) => isEqual(local, input.master.resource))) { + input.dispose(); + } + }); + } + + // Show conflicts notification if not shown before + else if (!this.conflictsDisposables.has(syncResource)) { const conflictsArea = getSyncAreaLabel(syncResource); const handle = this.notificationService.prompt(Severity.Warning, localize('conflicts detected', "Unable to sync due to conflicts in {0}. Please resolve them to continue.", conflictsArea.toLowerCase()), [ @@ -338,9 +354,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo handle.close(); // close opened conflicts editor previews - const conflictsEditorInput = this.getConflictsEditorInput(syncResource); - if (conflictsEditorInput) { - conflictsEditorInput.dispose(); + const conflictsEditorInputs = this.getConflictsEditorInputs(syncResource); + if (conflictsEditorInputs.length) { + conflictsEditorInputs.forEach(input => input.dispose()); } this.conflictsDisposables.delete(syncResource); @@ -496,7 +512,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo if (this.userDataSyncService.status !== SyncStatus.Uninitialized && this.userDataSyncEnablementService.isEnabled() && this.authenticationState.get() === AuthStatus.SignedOut) { badge = new NumberBadge(1, () => localize('sign in to sync', "Sign in to Sync")); } else if (this.userDataSyncService.conflicts.length) { - badge = new NumberBadge(this.userDataSyncService.conflicts.length, () => localize('has conflicts', "Sync: Conflicts Detected")); + badge = new NumberBadge(this.userDataSyncService.conflicts.reduce((result, syncResourceConflict) => { return result + syncResourceConflict.conflicts.length; }, 0), () => localize('has conflicts', "Sync: Conflicts Detected")); } if (badge) { @@ -605,6 +621,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo }, { id: SyncResource.Keybindings, label: getSyncAreaLabel(SyncResource.Keybindings) + }, { + id: SyncResource.Snippets, + label: getSyncAreaLabel(SyncResource.Snippets) }, { id: SyncResource.Extensions, label: getSyncAreaLabel(SyncResource.Extensions) @@ -712,6 +731,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo switch (source) { case SyncResource.Settings: return this.userDataSyncEnablementService.setResourceEnablement(SyncResource.Settings, false); case SyncResource.Keybindings: return this.userDataSyncEnablementService.setResourceEnablement(SyncResource.Keybindings, false); + case SyncResource.Snippets: return this.userDataSyncEnablementService.setResourceEnablement(SyncResource.Snippets, false); case SyncResource.Extensions: return this.userDataSyncEnablementService.setResourceEnablement(SyncResource.Extensions, false); case SyncResource.GlobalState: return this.userDataSyncEnablementService.setResourceEnablement(SyncResource.GlobalState, false); } @@ -727,8 +747,11 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo } } - private getConflictsEditorInput(syncResource: SyncResource): IEditorInput | undefined { - return this.editorService.editors.filter(input => input instanceof DiffEditorInput && getSyncResourceFromLocalPreview(input.master.resource!, this.workbenchEnvironmentService) === syncResource)[0]; + private getConflictsEditorInputs(syncResource: SyncResource): DiffEditorInput[] { + return this.editorService.editors.filter(input => { + const resource = input instanceof DiffEditorInput ? input.master.resource : input.resource; + return getSyncResourceFromLocalPreview(resource!, this.workbenchEnvironmentService) === syncResource; + }) as DiffEditorInput[]; } private getAllConflictsEditorInputs(): IEditorInput[] { @@ -752,6 +775,8 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo label = localize('settings conflicts preview', "Settings Conflicts (Remote ↔ Local)"); } else if (syncResource === SyncResource.Keybindings) { label = localize('keybindings conflicts preview', "Keybindings Conflicts (Remote ↔ Local)"); + } else if (syncResource === SyncResource.Snippets) { + label = localize('snippets conflicts preview', "User Snippet Conflicts (Remote ↔ Local) - {0}", basename(conflict.local)); } await this.editorService.openEditor({ leftResource: conflict.remote, @@ -775,6 +800,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo this.registerSignInAction(); this.registerShowSettingsConflictsAction(); this.registerShowKeybindingsConflictsAction(); + this.registerShowSnippetsConflictsAction(); this.registerSyncStatusAction(); this.registerTurnOffSyncAction(); @@ -894,7 +920,36 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo command: resolveKeybindingsConflictsCommand, when: resolveKeybindingsConflictsWhenContext, }); + } + private _snippetsConflictsActionsDisposable: DisposableStore = new DisposableStore(); + private registerShowSnippetsConflictsAction(): void { + this._snippetsConflictsActionsDisposable.clear(); + const resolveSnippetsConflictsWhenContext = ContextKeyExpr.regex(CONTEXT_CONFLICTS_SOURCES.keys()[0], /.*snippets.*/i); + const conflicts: Conflict[] | undefined = this.userDataSyncService.conflicts.filter(({ syncResource }) => syncResource === SyncResource.Snippets)[0]?.conflicts; + this._snippetsConflictsActionsDisposable.add(CommandsRegistry.registerCommand(resolveSnippetsConflictsCommand.id, () => this.handleSyncResourceConflicts(SyncResource.Snippets))); + this._snippetsConflictsActionsDisposable.add(MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { + group: '5_sync', + command: { + id: resolveSnippetsConflictsCommand.id, + title: localize('resolveSnippetsConflicts_global', "Sync: Show User Snippets Conflicts ({0})", conflicts?.length || 1), + }, + when: resolveSnippetsConflictsWhenContext, + order: 2 + })); + this._snippetsConflictsActionsDisposable.add(MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, { + group: '5_sync', + command: { + id: resolveSnippetsConflictsCommand.id, + title: localize('resolveSnippetsConflicts_global', "Sync: Show User Snippets Conflicts ({0})", conflicts?.length || 1), + }, + when: resolveSnippetsConflictsWhenContext, + order: 2 + })); + this._snippetsConflictsActionsDisposable.add(MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: resolveSnippetsConflictsCommand, + when: resolveSnippetsConflictsWhenContext, + })); } private registerSyncStatusAction(): void { @@ -938,6 +993,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo case SyncResource.Keybindings: items.push({ id: resolveKeybindingsConflictsCommand.id, label: resolveKeybindingsConflictsCommand.title }); break; + case SyncResource.Snippets: + items.push({ id: resolveSnippetsConflictsCommand.id, label: resolveSnippetsConflictsCommand.title }); + break; } } items.push({ type: 'separator' }); @@ -1074,7 +1132,6 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio constructor( private editor: ICodeEditor, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService, @INotificationService private readonly notificationService: INotificationService, @IDialogService private readonly dialogService: IDialogService, @@ -1088,7 +1145,8 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio } private registerListeners(): void { - this._register(this.editor.onDidChangeModel(e => this.update())); + this._register(this.editor.onDidChangeModel(() => this.update())); + this._register(this.userDataSyncService.onDidChangeConflicts(() => this.update())); this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('diffEditor.renderSideBySide'))(() => this.update())); } @@ -1107,11 +1165,16 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio return false; // we need a model } - if (getSyncResourceFromLocalPreview(model.uri, this.environmentService) !== undefined) { + const syncResourceConflicts = this.getSyncResourceConflicts(model.uri); + if (!syncResourceConflicts) { + return false; + } + + if (syncResourceConflicts.conflicts.some(({ local }) => isEqual(local, model.uri))) { return true; } - if (getSyncResourceFromRemotePreview(model.uri, this.environmentService) !== undefined) { + if (syncResourceConflicts.conflicts.some(({ remote }) => isEqual(remote, model.uri))) { return this.configurationService.getValue('diffEditor.renderSideBySide'); } @@ -1121,16 +1184,17 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio private createAcceptChangesWidgetRenderer(): void { if (!this.acceptChangesButton) { - const isRemote = getSyncResourceFromRemotePreview(this.editor.getModel()!.uri, this.environmentService) !== undefined; + const resource = this.editor.getModel()!.uri; + const syncResourceConflicts = this.getSyncResourceConflicts(resource)!; + const isRemote = syncResourceConflicts.conflicts.some(({ remote }) => isEqual(remote, resource)); const acceptRemoteLabel = localize('accept remote', "Accept Remote"); const acceptLocalLabel = localize('accept local', "Accept Local"); this.acceptChangesButton = this.instantiationService.createInstance(FloatingClickWidget, this.editor, isRemote ? acceptRemoteLabel : acceptLocalLabel, null); this._register(this.acceptChangesButton.onClick(async () => { const model = this.editor.getModel(); if (model) { - const conflictsSource = (getSyncResourceFromLocalPreview(model.uri, this.environmentService) || getSyncResourceFromRemotePreview(model.uri, this.environmentService))!; - this.telemetryService.publicLog2<{ source: string, action: string }, SyncConflictsClassification>('sync/handleConflicts', { source: conflictsSource, action: isRemote ? 'acceptRemote' : 'acceptLocal' }); - const syncAreaLabel = getSyncAreaLabel(conflictsSource); + this.telemetryService.publicLog2<{ source: string, action: string }, SyncConflictsClassification>('sync/handleConflicts', { source: syncResourceConflicts.syncResource, action: isRemote ? 'acceptRemote' : 'acceptLocal' }); + const syncAreaLabel = getSyncAreaLabel(syncResourceConflicts.syncResource); const result = await this.dialogService.confirm({ type: 'info', title: isRemote @@ -1146,7 +1210,7 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio await this.userDataSyncService.acceptConflict(model.uri, model.getValue()); } catch (e) { if (e instanceof UserDataSyncError && e.code === UserDataSyncErrorCode.LocalPreconditionFailed) { - const syncResourceCoflicts = this.userDataSyncService.conflicts.filter(({ syncResource }) => syncResource === conflictsSource)[0]; + const syncResourceCoflicts = this.userDataSyncService.conflicts.filter(({ syncResource }) => syncResource === syncResourceConflicts.syncResource)[0]; if (syncResourceCoflicts && syncResourceCoflicts.conflicts.some(conflict => isEqual(conflict.local, model.uri) || isEqual(conflict.remote, model.uri))) { this.notificationService.warn(localize('update conflicts', "Could not resolve conflicts as there is new local version available. Please try again.")); } @@ -1162,6 +1226,10 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio } } + private getSyncResourceConflicts(resource: URI): SyncResourceConflicts | undefined { + return this.userDataSyncService.conflicts.filter(({ conflicts }) => conflicts.some(({ local, remote }) => isEqual(local, resource) || isEqual(remote, resource)))[0]; + } + private disposeAcceptChangesWidgetRenderer(): void { dispose(this.acceptChangesButton); this.acceptChangesButton = undefined; From 6a9b88cac1d40f54652053a3a2d0d8204a879001 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 19 Mar 2020 14:26:31 +0100 Subject: [PATCH 26/44] #86678 add merge tests --- .../userDataSync/common/snippetsMerge.ts | 67 ++- .../userDataSync/common/snippetsSync.ts | 48 +- .../test/common/snippetsMerge.test.ts | 436 ++++++++++++++++++ 3 files changed, 512 insertions(+), 39 deletions(-) create mode 100644 src/vs/platform/userDataSync/test/common/snippetsMerge.test.ts diff --git a/src/vs/platform/userDataSync/common/snippetsMerge.ts b/src/vs/platform/userDataSync/common/snippetsMerge.ts index b16ab9ce883..42a9dfaae05 100644 --- a/src/vs/platform/userDataSync/common/snippetsMerge.ts +++ b/src/vs/platform/userDataSync/common/snippetsMerge.ts @@ -15,15 +15,15 @@ export interface IMergeResult { remote: IStringDictionary | null; } -export function merge(local: IStringDictionary, remote: IStringDictionary | null, base: IStringDictionary | null): IMergeResult { +export function merge(local: IStringDictionary, remote: IStringDictionary | null, base: IStringDictionary | null, resolvedConflicts: IStringDictionary = {}): IMergeResult { const added: IStringDictionary = {}; const updated: IStringDictionary = {}; - const removed: string[] = []; + const removed: Set = new Set(); if (!remote) { return { added, - removed, + removed: values(removed), updated, conflicts: [], remote: local @@ -35,7 +35,7 @@ export function merge(local: IStringDictionary, remote: IStringDictionar // No changes found between local and remote. return { added, - removed, + removed: values(removed), updated, conflicts: [], remote: null @@ -44,8 +44,41 @@ export function merge(local: IStringDictionary, remote: IStringDictionar const baseToLocal = compare(base, local); const baseToRemote = compare(base, remote); + const remoteContent: IStringDictionary = deepClone(remote); const conflicts: Set = new Set(); - let remoteContent: IStringDictionary = deepClone(remote); + const handledConflicts: Set = new Set(); + const handleConflict = (key: string): void => { + if (handledConflicts.has(key)) { + return; + } + handledConflicts.add(key); + const conflictContent = resolvedConflicts[key]; + + // add to conflicts + if (conflictContent === undefined) { + conflicts.add(key); + } + + // remove the snippet + else if (conflictContent === null) { + delete remote[key]; + if (local[key]) { + removed.add(key); + } + } + + // add/update the snippet + else { + if (local[key]) { + if (local[key] !== conflictContent) { + updated[key] = conflictContent; + } + } else { + added[key] = conflictContent; + } + remoteContent[key] = conflictContent; + } + }; // Removed snippets in Local for (const key of values(baseToLocal.removed)) { @@ -62,29 +95,29 @@ export function merge(local: IStringDictionary, remote: IStringDictionar // Removed snippets in Remote for (const key of values(baseToRemote.removed)) { - if (conflicts.has(key)) { + if (handledConflicts.has(key)) { continue; } // Conflict - Got updated in local if (baseToLocal.updated.has(key)) { - conflicts.add(key); + handleConflict(key); } // Also remove in Local else { - removed.push(key); + removed.add(key); } } // Updated snippets in Local for (const key of values(baseToLocal.updated)) { - if (conflicts.has(key)) { + if (handledConflicts.has(key)) { continue; } // Got updated in remote if (baseToRemote.updated.has(key)) { // Has different value if (localToRemote.updated.has(key)) { - conflicts.add(key); + handleConflict(key); } } else { remoteContent[key] = local[key]; @@ -93,14 +126,14 @@ export function merge(local: IStringDictionary, remote: IStringDictionar // Updated snippets in Remote for (const key of values(baseToRemote.updated)) { - if (conflicts.has(key)) { + if (handledConflicts.has(key)) { continue; } // Got updated in local if (baseToLocal.updated.has(key)) { // Has different value if (localToRemote.updated.has(key)) { - conflicts.add(key); + handleConflict(key); } } else if (local[key] !== undefined) { updated[key] = remote[key]; @@ -109,14 +142,14 @@ export function merge(local: IStringDictionary, remote: IStringDictionar // Added snippets in Local for (const key of values(baseToLocal.added)) { - if (conflicts.has(key)) { + if (handledConflicts.has(key)) { continue; } // Got added in remote if (baseToRemote.added.has(key)) { // Has different value if (localToRemote.updated.has(key)) { - conflicts.add(key); + handleConflict(key); } } else { remoteContent[key] = local[key]; @@ -125,21 +158,21 @@ export function merge(local: IStringDictionary, remote: IStringDictionar // Added snippets in remote for (const key of values(baseToRemote.added)) { - if (conflicts.has(key)) { + if (handledConflicts.has(key)) { continue; } // Got added in local if (baseToLocal.added.has(key)) { // Has different value if (localToRemote.updated.has(key)) { - conflicts.add(key); + handleConflict(key); } } else { added[key] = remote[key]; } } - return { added, removed, updated, conflicts: values(conflicts), remote: areSame(remote, remoteContent) ? null : remoteContent }; + return { added, removed: values(removed), updated, conflicts: values(conflicts), remote: areSame(remote, remoteContent) ? null : remoteContent }; } function compare(from: IStringDictionary | null, to: IStringDictionary | null): { added: Set, removed: Set, updated: Set } { diff --git a/src/vs/platform/userDataSync/common/snippetsSync.ts b/src/vs/platform/userDataSync/common/snippetsSync.ts index 6a23ddade0b..9f1b4805c46 100644 --- a/src/vs/platform/userDataSync/common/snippetsSync.ts +++ b/src/vs/platform/userDataSync/common/snippetsSync.ts @@ -196,18 +196,13 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD const conflict = this.conflicts.filter(({ local, remote }) => isEqual(local, conflictResource) || isEqual(remote, conflictResource))[0]; if (this.status === SyncStatus.HasConflicts && conflict) { const key = relativePath(this.snippetsPreviewFolder, conflict.local)!; - const result = await this.syncPreviewResultPromise!; - if (content) { - if (result.local[key]) { - result.updated[key] = content; - } else { - result.added[key] = content; - } - } else { - result.removed.push(key); - } - await this.fileService.del(conflict.local); - this.setConflicts(this.conflicts.filter(c => c !== conflict)); + const previewResult = await this.syncPreviewResultPromise!; + this.cancel(); + const resolvedConflicts: IStringDictionary = {}; + resolvedConflicts[key] = content || null; + this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(previewResult.local, previewResult.remoteUserData, previewResult.lastSyncUserData, resolvedConflicts, token)); + await this.syncPreviewResultPromise; + this.setConflicts(previewResult.conflicts); if (!this.conflicts.length) { await this.apply(); this.setStatus(SyncStatus.Idle); @@ -230,7 +225,8 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { try { const previewResult = await this.getPreview(remoteUserData, lastSyncUserData); - if (previewResult.conflicts.length) { + this.setConflicts(previewResult.conflicts); + if (this.conflicts.length) { return SyncStatus.HasConflicts; } await this.apply(); @@ -251,7 +247,8 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD private getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { if (!this.syncPreviewResultPromise) { - this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(remoteUserData, lastSyncUserData, token)); + this.syncPreviewResultPromise = createCancelablePromise(token => this.getSnippetsFileContents() + .then(local => this.generatePreview(local, remoteUserData, lastSyncUserData, {}, token))); } return this.syncPreviewResultPromise; } @@ -270,22 +267,19 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD } } - private async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise { - await this.clearConflicts(); - + private async generatePreview(local: IStringDictionary, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: IStringDictionary, token: CancellationToken): Promise { + const localSnippets = this.toSnippetsContents(local); const remoteSnippets: IStringDictionary | null = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null; const lastSyncSnippets: IStringDictionary | null = lastSyncUserData ? this.parseSnippets(lastSyncUserData.syncData!) : null; - const local = await this.getSnippetsFileContents(); - const localSnippets = this.toSnippetsContents(local); - if (remoteSnippets) { this.logService.trace(`${this.syncResourceLogLabel}: Merging remote snippets with local snippets...`); } else { this.logService.trace(`${this.syncResourceLogLabel}: Remote snippets does not exist. Synchronizing snippets for the first time.`); } - const mergeResult = merge(localSnippets, remoteSnippets, lastSyncSnippets); + const mergeResult = merge(localSnippets, remoteSnippets, lastSyncSnippets, resolvedConflicts); + const conflicts: Conflict[] = []; for (const key of mergeResult.conflicts) { const localPreview = joinPath(this.snippetsPreviewFolder, key); @@ -296,7 +290,17 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD } } - this.setConflicts(!token.isCancellationRequested ? conflicts : []); + for (const conflict of this.conflicts) { + // clear obsolete conflicts + if (!conflicts.some(({ local }) => isEqual(local, conflict.local))) { + try { + await this.fileService.del(conflict.local); + } catch (error) { + // Ignore & log + this.logService.error(error); + } + } + } return { remoteUserData, local, lastSyncUserData, added: mergeResult.added, removed: mergeResult.removed, updated: mergeResult.updated, conflicts, remote: mergeResult.remote }; } diff --git a/src/vs/platform/userDataSync/test/common/snippetsMerge.test.ts b/src/vs/platform/userDataSync/test/common/snippetsMerge.test.ts new file mode 100644 index 00000000000..5a396c4a6da --- /dev/null +++ b/src/vs/platform/userDataSync/test/common/snippetsMerge.test.ts @@ -0,0 +1,436 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { merge } from 'vs/platform/userDataSync/common/snippetsMerge'; + +const tsSnippet1 = `{ + + // Place your snippets for TypeScript here. Each snippet is defined under a snippet name and has a prefix, body and + // description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are: + // $1, $2 for tab stops, $0 for the final cursor position, Placeholders with the + // same ids are connected. + "Print to console": { + // Example: + "prefix": "log", + "body": [ + "console.log('$1');", + "$2" + ], + "description": "Log output to console", + } + +}`; + +const tsSnippet2 = `{ + + // Place your snippets for TypeScript here. Each snippet is defined under a snippet name and has a prefix, body and + // description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are: + // $1, $2 for tab stops, $0 for the final cursor position, Placeholders with the + // same ids are connected. + "Print to console": { + // Example: + "prefix": "log", + "body": [ + "console.log('$1');", + "$2" + ], + "description": "Log output to console always", + } + +}`; + +const htmlSnippet1 = `{ +/* + // Place your snippets for HTML here. Each snippet is defined under a snippet name and has a prefix, body and + // description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. + // Example: + "Print to console": { + "prefix": "log", + "body": [ + "console.log('$1');", + "$2" + ], + "description": "Log output to console" + } +*/ +"Div": { + "prefix": "div", + "body": [ + "
", + "", + "
" + ], + "description": "New div" + } +}`; + +const htmlSnippet2 = `{ +/* + // Place your snippets for HTML here. Each snippet is defined under a snippet name and has a prefix, body and + // description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. + // Example: + "Print to console": { + "prefix": "log", + "body": [ + "console.log('$1');", + "$2" + ], + "description": "Log output to console" + } +*/ +"Div": { + "prefix": "div", + "body": [ + "
", + "", + "
" + ], + "description": "New div changed" + } +}`; + +const cSnippet = `{ + // Place your snippets for c here. Each snippet is defined under a snippet name and has a prefix, body and + // description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are: + // $1, $2 for tab stops, $0 for the final cursor position.Placeholders with the + // same ids are connected. + // Example: + "Print to console": { + "prefix": "log", + "body": [ + "console.log('$1');", + "$2" + ], + "description": "Log output to console" + } +}`; + +suite('SnippetsMerge', () => { + + test('merge when local and remote are same with one snippet', async () => { + const local = { 'html.json': htmlSnippet1 }; + const remote = { 'html.json': htmlSnippet1 }; + + const actual = merge(local, remote, null); + + assert.deepEqual(actual.added, {}); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, []); + assert.equal(actual.remote, null); + }); + + test('merge when local and remote are same with multiple entries', async () => { + const local = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + + const actual = merge(local, remote, null); + + assert.deepEqual(actual.added, {}); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, []); + assert.equal(actual.remote, null); + }); + + test('merge when local and remote are same with multiple entries in different order', async () => { + const local = { 'typescript.json': tsSnippet1, 'html.json': htmlSnippet1 }; + const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + + const actual = merge(local, remote, null); + + assert.deepEqual(actual.added, {}); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, []); + assert.equal(actual.remote, null); + }); + + test('merge when local and remote are same with different base content', async () => { + const local = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + const base = { 'html.json': htmlSnippet2, 'typescript.json': tsSnippet2 }; + + const actual = merge(local, remote, base); + + assert.deepEqual(actual.added, {}); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, []); + assert.equal(actual.remote, null); + }); + + test('merge when a new entry is added to remote', async () => { + const local = { 'html.json': htmlSnippet1 }; + const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + + const actual = merge(local, remote, null); + + assert.deepEqual(actual.added, { 'typescript.json': tsSnippet1 }); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, []); + assert.equal(actual.remote, null); + }); + + test('merge when multiple new entries are added to remote', async () => { + const local = {}; + const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + + const actual = merge(local, remote, null); + + assert.deepEqual(actual.added, remote); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, []); + assert.equal(actual.remote, null); + }); + + test('merge when new entry is added to remote from base and local has not changed', async () => { + const local = { 'html.json': htmlSnippet1 }; + const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + + const actual = merge(local, remote, local); + + assert.deepEqual(actual.added, { 'typescript.json': tsSnippet1 }); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, []); + assert.equal(actual.remote, null); + }); + + test('merge when an entry is removed from remote from base and local has not changed', async () => { + const local = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + const remote = { 'html.json': htmlSnippet1 }; + + const actual = merge(local, remote, local); + + assert.deepEqual(actual.added, {}); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, ['typescript.json']); + assert.deepEqual(actual.conflicts, []); + assert.equal(actual.remote, null); + }); + + test('merge when all entries are removed from base and local has not changed', async () => { + const local = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + const remote = {}; + + const actual = merge(local, remote, local); + + assert.deepEqual(actual.added, {}); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, ['html.json', 'typescript.json']); + assert.deepEqual(actual.conflicts, []); + assert.equal(actual.remote, null); + }); + + test('merge when an entry is updated in remote from base and local has not changed', async () => { + const local = { 'html.json': htmlSnippet1 }; + const remote = { 'html.json': htmlSnippet2 }; + + const actual = merge(local, remote, local); + + assert.deepEqual(actual.added, {}); + assert.deepEqual(actual.updated, { 'html.json': htmlSnippet2 }); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, []); + assert.equal(actual.remote, null); + }); + + test('merge when remote has moved forwarded with multiple changes and local stays with base', async () => { + const local = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + const remote = { 'html.json': htmlSnippet2, 'c.json': cSnippet }; + + const actual = merge(local, remote, local); + + assert.deepEqual(actual.added, { 'c.json': cSnippet }); + assert.deepEqual(actual.updated, { 'html.json': htmlSnippet2 }); + assert.deepEqual(actual.removed, ['typescript.json']); + assert.deepEqual(actual.conflicts, []); + assert.deepEqual(actual.remote, null); + }); + + test('merge when a new entries are added to local', async () => { + const local = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1, 'c.json': cSnippet }; + const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + + const actual = merge(local, remote, null); + + assert.deepEqual(actual.added, {}); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, []); + assert.deepEqual(actual.remote, local); + }); + + test('merge when multiple new entries are added to local from base and remote is not changed', async () => { + const local = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1, 'c.json': cSnippet }; + const remote = { 'typescript.json': tsSnippet1 }; + + const actual = merge(local, remote, remote); + + assert.deepEqual(actual.added, {}); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, []); + assert.deepEqual(actual.remote, { 'typescript.json': tsSnippet1, 'html.json': htmlSnippet1, 'c.json': cSnippet }); + }); + + test('merge when an entry is removed from local from base and remote has not changed', async () => { + const local = { 'html.json': htmlSnippet1 }; + const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + + const actual = merge(local, remote, remote); + + assert.deepEqual(actual.added, {}); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, []); + assert.deepEqual(actual.remote, local); + }); + + test('merge when an entry is updated in local from base and remote has not changed', async () => { + const local = { 'html.json': htmlSnippet2, 'typescript.json': tsSnippet1 }; + const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + + const actual = merge(local, remote, remote); + + assert.deepEqual(actual.added, {}); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, []); + assert.deepEqual(actual.remote, local); + }); + + test('merge when local has moved forwarded with multiple changes and remote stays with base', async () => { + const local = { 'html.json': htmlSnippet2, 'c.json': cSnippet }; + const remote = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + + const actual = merge(local, remote, remote); + + assert.deepEqual(actual.added, {}); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, []); + assert.deepEqual(actual.remote, local); + }); + + test('merge when local and remote with one entry but different value', async () => { + const local = { 'html.json': htmlSnippet1 }; + const remote = { 'html.json': htmlSnippet2 }; + + const actual = merge(local, remote, null); + + assert.deepEqual(actual.added, {}); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, ['html.json']); + assert.deepEqual(actual.remote, null); + }); + + test('merge when the entry is removed in remote but updated in local and a new entry is added in remote', async () => { + const base = { 'html.json': htmlSnippet1 }; + const local = { 'html.json': htmlSnippet2 }; + const remote = { 'typescript.json': tsSnippet1 }; + + const actual = merge(local, remote, base); + + assert.deepEqual(actual.added, { 'typescript.json': tsSnippet1 }); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, ['html.json']); + assert.deepEqual(actual.remote, null); + }); + + test('merge with single entry and local is empty', async () => { + const base = { 'html.json': htmlSnippet1 }; + const local = {}; + const remote = { 'html.json': htmlSnippet2 }; + + const actual = merge(local, remote, base); + + assert.deepEqual(actual.added, { 'html.json': htmlSnippet2 }); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, []); + assert.deepEqual(actual.remote, null); + }); + + test('merge when local and remote has moved forwareded with conflicts', async () => { + const base = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + const local = { 'html.json': htmlSnippet2, 'c.json': cSnippet }; + const remote = { 'typescript.json': tsSnippet2 }; + + const actual = merge(local, remote, base); + + assert.deepEqual(actual.added, { 'typescript.json': tsSnippet2 }); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, ['html.json']); + assert.deepEqual(actual.remote, { 'typescript.json': tsSnippet2, 'c.json': cSnippet }); + }); + + test('merge when local and remote has moved forwareded with resolved conflicts - update', async () => { + const base = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + const local = { 'html.json': htmlSnippet2, 'c.json': cSnippet }; + const remote = { 'typescript.json': tsSnippet2 }; + const resolvedConflicts = { 'html.json': htmlSnippet2 }; + + const actual = merge(local, remote, base, resolvedConflicts); + + assert.deepEqual(actual.added, { 'typescript.json': tsSnippet2 }); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, []); + assert.deepEqual(actual.remote, { 'typescript.json': tsSnippet2, 'html.json': htmlSnippet2, 'c.json': cSnippet }); + }); + + test('merge when local and remote has moved forwareded with resolved conflicts - remove', async () => { + const base = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + const local = { 'html.json': htmlSnippet2, 'c.json': cSnippet }; + const remote = { 'typescript.json': tsSnippet2 }; + const resolvedConflicts = { 'html.json': null }; + + const actual = merge(local, remote, base, resolvedConflicts); + + assert.deepEqual(actual.added, { 'typescript.json': tsSnippet2 }); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, ['html.json']); + assert.deepEqual(actual.conflicts, []); + assert.deepEqual(actual.remote, { 'typescript.json': tsSnippet2, 'c.json': cSnippet }); + }); + + test('merge when local and remote has moved forwareded with multiple conflicts', async () => { + const base = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + const local = { 'html.json': htmlSnippet2, 'typescript.json': tsSnippet2, 'c.json': cSnippet }; + const remote = { 'c.json': cSnippet }; + + const actual = merge(local, remote, base); + + assert.deepEqual(actual.added, {}); + assert.deepEqual(actual.updated, {}); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, ['html.json', 'typescript.json']); + assert.deepEqual(actual.remote, null); + }); + + test('merge when local and remote has moved forwareded with multiple conflicts and resolving one conflict', async () => { + const base = { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }; + const local = { 'html.json': htmlSnippet2, 'typescript.json': tsSnippet2, 'c.json': cSnippet }; + const remote = { 'c.json': cSnippet }; + const resolvedConflicts = { 'html.json': htmlSnippet1 }; + + const actual = merge(local, remote, base, resolvedConflicts); + + assert.deepEqual(actual.added, {}); + assert.deepEqual(actual.updated, { 'html.json': htmlSnippet1 }); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.conflicts, ['typescript.json']); + assert.deepEqual(actual.remote, { 'c.json': cSnippet, 'html.json': htmlSnippet1 }); + }); + +}); From eb49d950321a257aa680e518c43129369862c30f Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 19 Mar 2020 15:41:14 +0100 Subject: [PATCH 27/44] #86678 Add snippets sync tests --- .../userDataSync/common/snippetsSync.ts | 16 +- .../test/common/snippetsSync.test.ts | 614 ++++++++++++++++++ .../test/common/userDataSyncClient.ts | 6 +- 3 files changed, 624 insertions(+), 12 deletions(-) create mode 100644 src/vs/platform/userDataSync/test/common/snippetsSync.test.ts diff --git a/src/vs/platform/userDataSync/common/snippetsSync.ts b/src/vs/platform/userDataSync/common/snippetsSync.ts index 9f1b4805c46..f9d70f18abc 100644 --- a/src/vs/platform/userDataSync/common/snippetsSync.ts +++ b/src/vs/platform/userDataSync/common/snippetsSync.ts @@ -25,6 +25,7 @@ interface ISyncPreviewResult { readonly updated: IStringDictionary; readonly removed: string[]; readonly conflicts: Conflict[]; + readonly resolvedConflicts: IStringDictionary; readonly remote: IStringDictionary | null; } @@ -94,7 +95,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD const remoteSnippets = this.parseSnippets(remoteUserData.syncData); const { added, updated, remote, removed } = merge(localSnippets, remoteSnippets, localSnippets); this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve({ - added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [] + added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {} })); await this.apply(); } @@ -128,7 +129,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD const lastSyncUserData = await this.getLastSyncUserData(); const remoteUserData = await this.getRemoteUserData(lastSyncUserData); this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve({ - added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [] + added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {} })); await this.apply(true); @@ -196,12 +197,11 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD const conflict = this.conflicts.filter(({ local, remote }) => isEqual(local, conflictResource) || isEqual(remote, conflictResource))[0]; if (this.status === SyncStatus.HasConflicts && conflict) { const key = relativePath(this.snippetsPreviewFolder, conflict.local)!; - const previewResult = await this.syncPreviewResultPromise!; + let previewResult = await this.syncPreviewResultPromise!; this.cancel(); - const resolvedConflicts: IStringDictionary = {}; - resolvedConflicts[key] = content || null; - this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(previewResult.local, previewResult.remoteUserData, previewResult.lastSyncUserData, resolvedConflicts, token)); - await this.syncPreviewResultPromise; + previewResult.resolvedConflicts[key] = content || null; + this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(previewResult.local, previewResult.remoteUserData, previewResult.lastSyncUserData, previewResult.resolvedConflicts, token)); + previewResult = await this.syncPreviewResultPromise; this.setConflicts(previewResult.conflicts); if (!this.conflicts.length) { await this.apply(); @@ -302,7 +302,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD } } - return { remoteUserData, local, lastSyncUserData, added: mergeResult.added, removed: mergeResult.removed, updated: mergeResult.updated, conflicts, remote: mergeResult.remote }; + return { remoteUserData, local, lastSyncUserData, added: mergeResult.added, removed: mergeResult.removed, updated: mergeResult.updated, conflicts, remote: mergeResult.remote, resolvedConflicts }; } private async apply(forcePush?: boolean): Promise { diff --git a/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts b/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts new file mode 100644 index 00000000000..2e1f2b7659e --- /dev/null +++ b/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts @@ -0,0 +1,614 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { IUserDataSyncStoreService, IUserDataSyncService, SyncResource, SyncStatus, Conflict, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME } from 'vs/platform/userDataSync/common/userDataSync'; +import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { SnippetsSynchroniser } from 'vs/platform/userDataSync/common/snippetsSync'; +import { joinPath } from 'vs/base/common/resources'; +import { IStringDictionary } from 'vs/base/common/collections'; + +const tsSnippet1 = `{ + + // Place your snippets for TypeScript here. Each snippet is defined under a snippet name and has a prefix, body and + // description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are: + // $1, $2 for tab stops, $0 for the final cursor position, Placeholders with the + // same ids are connected. + "Print to console": { + // Example: + "prefix": "log", + "body": [ + "console.log('$1');", + "$2" + ], + "description": "Log output to console", + } + +}`; + +const tsSnippet2 = `{ + + // Place your snippets for TypeScript here. Each snippet is defined under a snippet name and has a prefix, body and + // description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are: + // $1, $2 for tab stops, $0 for the final cursor position, Placeholders with the + // same ids are connected. + "Print to console": { + // Example: + "prefix": "log", + "body": [ + "console.log('$1');", + "$2" + ], + "description": "Log output to console always", + } + +}`; + +const htmlSnippet1 = `{ +/* + // Place your snippets for HTML here. Each snippet is defined under a snippet name and has a prefix, body and + // description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. + // Example: + "Print to console": { + "prefix": "log", + "body": [ + "console.log('$1');", + "$2" + ], + "description": "Log output to console" + } +*/ +"Div": { + "prefix": "div", + "body": [ + "
", + "", + "
" + ], + "description": "New div" + } +}`; + +const htmlSnippet2 = `{ +/* + // Place your snippets for HTML here. Each snippet is defined under a snippet name and has a prefix, body and + // description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. + // Example: + "Print to console": { + "prefix": "log", + "body": [ + "console.log('$1');", + "$2" + ], + "description": "Log output to console" + } +*/ +"Div": { + "prefix": "div", + "body": [ + "
", + "", + "
" + ], + "description": "New div changed" + } +}`; + +const htmlSnippet3 = `{ +/* + // Place your snippets for HTML here. Each snippet is defined under a snippet name and has a prefix, body and + // description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. + // Example: + "Print to console": { + "prefix": "log", + "body": [ + "console.log('$1');", + "$2" + ], + "description": "Log output to console" + } +*/ +"Div": { + "prefix": "div", + "body": [ + "
", + "", + "
" + ], + "description": "New div changed again" + } +}`; + +suite('SnippetsSync', () => { + + const disposableStore = new DisposableStore(); + const server = new UserDataSyncTestServer(); + let testClient: UserDataSyncClient; + let client2: UserDataSyncClient; + + let testObject: SnippetsSynchroniser; + + setup(async () => { + testClient = disposableStore.add(new UserDataSyncClient(server)); + await testClient.setUp(); + testObject = (testClient.instantiationService.get(IUserDataSyncService) as UserDataSyncService).getSynchroniser(SyncResource.Snippets) as SnippetsSynchroniser; + disposableStore.add(toDisposable(() => testClient.instantiationService.get(IUserDataSyncStoreService).clear())); + + client2 = disposableStore.add(new UserDataSyncClient(server)); + await client2.setUp(); + }); + + teardown(() => disposableStore.clear()); + + test('first time sync - outgoing to server (no snippets)', async () => { + await updateSnippet('html.json', htmlSnippet1, testClient); + await updateSnippet('typescript.json', tsSnippet1, testClient); + + await testObject.sync(); + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseSnippets(content!); + assert.deepEqual(actual, { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }); + }); + + test('first time sync - incoming from server (no snippets)', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await updateSnippet('typescript.json', tsSnippet1, client2); + await client2.sync(); + + await testObject.sync(); + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const actual1 = await readSnippet('html.json', testClient); + assert.equal(actual1, htmlSnippet1); + const actual2 = await readSnippet('typescript.json', testClient); + assert.equal(actual2, tsSnippet1); + }); + + test('first time sync when snippets exists', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await client2.sync(); + + await updateSnippet('typescript.json', tsSnippet1, testClient); + await testObject.sync(); + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const actual1 = await readSnippet('html.json', testClient); + assert.equal(actual1, htmlSnippet1); + const actual2 = await readSnippet('typescript.json', testClient); + assert.equal(actual2, tsSnippet1); + + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseSnippets(content!); + assert.deepEqual(actual, { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }); + }); + + test('first time sync when snippets exists - has conflicts', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await client2.sync(); + + await updateSnippet('html.json', htmlSnippet2, testClient); + await testObject.sync(); + + assert.equal(testObject.status, SyncStatus.HasConflicts); + const environmentService = testClient.instantiationService.get(IEnvironmentService); + const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'); + assertConflicts(testObject.conflicts, [{ local, remote: local.with({ scheme: USER_DATA_SYNC_SCHEME }) }]); + }); + + test('first time sync when snippets exists - has conflicts and accept conflicts', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await client2.sync(); + + await updateSnippet('html.json', htmlSnippet2, testClient); + await testObject.sync(); + const conflicts = testObject.conflicts; + await testObject.acceptConflict(conflicts[0].local, htmlSnippet1); + + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + const fileService = testClient.instantiationService.get(IFileService); + assert.ok(!await fileService.exists(conflicts[0].local)); + + const actual1 = await readSnippet('html.json', testClient); + assert.equal(actual1, htmlSnippet1); + + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseSnippets(content!); + assert.deepEqual(actual, { 'html.json': htmlSnippet1 }); + }); + + test('first time sync when snippets exists - has multiple conflicts', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await updateSnippet('typescript.json', tsSnippet1, client2); + await client2.sync(); + + await updateSnippet('html.json', htmlSnippet2, testClient); + await updateSnippet('typescript.json', tsSnippet2, testClient); + await testObject.sync(); + + assert.equal(testObject.status, SyncStatus.HasConflicts); + const environmentService = testClient.instantiationService.get(IEnvironmentService); + const local1 = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'); + const local2 = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'); + assertConflicts(testObject.conflicts, [ + { local: local1, remote: local1.with({ scheme: USER_DATA_SYNC_SCHEME }) }, + { local: local2, remote: local2.with({ scheme: USER_DATA_SYNC_SCHEME }) } + ]); + }); + + test('first time sync when snippets exists - has multiple conflicts and accept one conflict', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await updateSnippet('typescript.json', tsSnippet1, client2); + await client2.sync(); + + await updateSnippet('html.json', htmlSnippet2, testClient); + await updateSnippet('typescript.json', tsSnippet2, testClient); + await testObject.sync(); + + let conflicts = testObject.conflicts; + await testObject.acceptConflict(conflicts[0].local, htmlSnippet2); + const fileService = testClient.instantiationService.get(IFileService); + assert.ok(!await fileService.exists(conflicts[0].local)); + + conflicts = testObject.conflicts; + assert.equal(testObject.status, SyncStatus.HasConflicts); + const environmentService = testClient.instantiationService.get(IEnvironmentService); + const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'typescript.json'); + assertConflicts(testObject.conflicts, [{ local, remote: local.with({ scheme: USER_DATA_SYNC_SCHEME }) }]); + }); + + test('first time sync when snippets exists - has multiple conflicts and accept all conflicts', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await updateSnippet('typescript.json', tsSnippet1, client2); + await client2.sync(); + + await updateSnippet('html.json', htmlSnippet2, testClient); + await updateSnippet('typescript.json', tsSnippet2, testClient); + await testObject.sync(); + + const conflicts = testObject.conflicts; + await testObject.acceptConflict(conflicts[0].local, htmlSnippet2); + await testObject.acceptConflict(conflicts[1].local, tsSnippet1); + + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + const fileService = testClient.instantiationService.get(IFileService); + assert.ok(!await fileService.exists(conflicts[0].local)); + assert.ok(!await fileService.exists(conflicts[1].local)); + + const actual1 = await readSnippet('html.json', testClient); + assert.equal(actual1, htmlSnippet2); + const actual2 = await readSnippet('typescript.json', testClient); + assert.equal(actual2, tsSnippet1); + + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseSnippets(content!); + assert.deepEqual(actual, { 'html.json': htmlSnippet2, 'typescript.json': tsSnippet1 }); + }); + + test('sync adding a snippet', async () => { + await updateSnippet('html.json', htmlSnippet1, testClient); + await testObject.sync(); + + await updateSnippet('typescript.json', tsSnippet1, testClient); + await testObject.sync(); + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const actual1 = await readSnippet('html.json', testClient); + assert.equal(actual1, htmlSnippet1); + const actual2 = await readSnippet('typescript.json', testClient); + assert.equal(actual2, tsSnippet1); + + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseSnippets(content!); + assert.deepEqual(actual, { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }); + }); + + test('sync adding a snippet - accept', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await client2.sync(); + await testObject.sync(); + + await updateSnippet('typescript.json', tsSnippet1, client2); + await client2.sync(); + + await testObject.sync(); + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const actual1 = await readSnippet('html.json', testClient); + assert.equal(actual1, htmlSnippet1); + const actual2 = await readSnippet('typescript.json', testClient); + assert.equal(actual2, tsSnippet1); + }); + + test('sync updating a snippet', async () => { + await updateSnippet('html.json', htmlSnippet1, testClient); + await testObject.sync(); + + await updateSnippet('html.json', htmlSnippet2, testClient); + await testObject.sync(); + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const actual1 = await readSnippet('html.json', testClient); + assert.equal(actual1, htmlSnippet2); + + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseSnippets(content!); + assert.deepEqual(actual, { 'html.json': htmlSnippet2 }); + }); + + test('sync updating a snippet - accept', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await client2.sync(); + await testObject.sync(); + + await updateSnippet('html.json', htmlSnippet2, client2); + await client2.sync(); + + await testObject.sync(); + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const actual1 = await readSnippet('html.json', testClient); + assert.equal(actual1, htmlSnippet2); + }); + + test('sync updating a snippet - conflict', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await client2.sync(); + await testObject.sync(); + + await updateSnippet('html.json', htmlSnippet2, client2); + await client2.sync(); + + await updateSnippet('html.json', htmlSnippet3, testClient); + await testObject.sync(); + assert.equal(testObject.status, SyncStatus.HasConflicts); + const environmentService = testClient.instantiationService.get(IEnvironmentService); + const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'); + assertConflicts(testObject.conflicts, [{ local, remote: local.with({ scheme: USER_DATA_SYNC_SCHEME }) }]); + }); + + test('sync updating a snippet - resolve conflict', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await client2.sync(); + await testObject.sync(); + + await updateSnippet('html.json', htmlSnippet2, client2); + await client2.sync(); + + await updateSnippet('html.json', htmlSnippet3, testClient); + await testObject.sync(); + await testObject.acceptConflict(testObject.conflicts[0].local, htmlSnippet2); + + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const actual1 = await readSnippet('html.json', testClient); + assert.equal(actual1, htmlSnippet2); + + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseSnippets(content!); + assert.deepEqual(actual, { 'html.json': htmlSnippet2 }); + }); + + test('sync removing a snippet', async () => { + await updateSnippet('html.json', htmlSnippet1, testClient); + await updateSnippet('typescript.json', tsSnippet1, testClient); + await testObject.sync(); + + await removeSnippet('html.json', testClient); + await testObject.sync(); + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const actual1 = await readSnippet('typescript.json', testClient); + assert.equal(actual1, tsSnippet1); + const actual2 = await readSnippet('html.json', testClient); + assert.equal(actual2, null); + + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseSnippets(content!); + assert.deepEqual(actual, { 'typescript.json': tsSnippet1 }); + }); + + test('sync removing a snippet - accept', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await updateSnippet('typescript.json', tsSnippet1, client2); + await client2.sync(); + await testObject.sync(); + + await removeSnippet('html.json', client2); + await client2.sync(); + + await testObject.sync(); + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const actual1 = await readSnippet('typescript.json', testClient); + assert.equal(actual1, tsSnippet1); + const actual2 = await readSnippet('html.json', testClient); + assert.equal(actual2, null); + }); + + test('sync removing a snippet locally and updating it remotely', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await updateSnippet('typescript.json', tsSnippet1, client2); + await client2.sync(); + await testObject.sync(); + + await updateSnippet('html.json', htmlSnippet2, client2); + await client2.sync(); + + await removeSnippet('html.json', testClient); + await testObject.sync(); + + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const actual1 = await readSnippet('typescript.json', testClient); + assert.equal(actual1, tsSnippet1); + const actual2 = await readSnippet('html.json', testClient); + assert.equal(actual2, htmlSnippet2); + }); + + test('sync removing a snippet - conflict', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await updateSnippet('typescript.json', tsSnippet1, client2); + await client2.sync(); + await testObject.sync(); + + await removeSnippet('html.json', client2); + await client2.sync(); + + await updateSnippet('html.json', htmlSnippet2, testClient); + await testObject.sync(); + + assert.equal(testObject.status, SyncStatus.HasConflicts); + const environmentService = testClient.instantiationService.get(IEnvironmentService); + const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'); + assertConflicts(testObject.conflicts, [{ local, remote: local.with({ scheme: USER_DATA_SYNC_SCHEME }) }]); + }); + + test('sync removing a snippet - resolve conflict', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await updateSnippet('typescript.json', tsSnippet1, client2); + await client2.sync(); + await testObject.sync(); + + await removeSnippet('html.json', client2); + await client2.sync(); + + await updateSnippet('html.json', htmlSnippet2, testClient); + await testObject.sync(); + await testObject.acceptConflict(testObject.conflicts[0].local, htmlSnippet3); + + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const actual1 = await readSnippet('typescript.json', testClient); + assert.equal(actual1, tsSnippet1); + const actual2 = await readSnippet('html.json', testClient); + assert.equal(actual2, htmlSnippet3); + + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseSnippets(content!); + assert.deepEqual(actual, { 'typescript.json': tsSnippet1, 'html.json': htmlSnippet3 }); + }); + + test('sync removing a snippet - resolve conflict by removing', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await updateSnippet('typescript.json', tsSnippet1, client2); + await client2.sync(); + await testObject.sync(); + + await removeSnippet('html.json', client2); + await client2.sync(); + + await updateSnippet('html.json', htmlSnippet2, testClient); + await testObject.sync(); + await testObject.acceptConflict(testObject.conflicts[0].local, ''); + + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const actual1 = await readSnippet('typescript.json', testClient); + assert.equal(actual1, tsSnippet1); + const actual2 = await readSnippet('html.json', testClient); + assert.equal(actual2, null); + + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseSnippets(content!); + assert.deepEqual(actual, { 'typescript.json': tsSnippet1 }); + }); + + test('first time sync - push', async () => { + await updateSnippet('html.json', htmlSnippet1, testClient); + await updateSnippet('typescript.json', tsSnippet1, testClient); + + await testObject.push(); + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const { content } = await testClient.read(testObject.resource); + assert.ok(content !== null); + const actual = parseSnippets(content!); + assert.deepEqual(actual, { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 }); + }); + + test('first time sync - pull', async () => { + await updateSnippet('html.json', htmlSnippet1, client2); + await updateSnippet('typescript.json', tsSnippet1, client2); + await client2.sync(); + + await testObject.pull(); + assert.equal(testObject.status, SyncStatus.Idle); + assert.deepEqual(testObject.conflicts, []); + + const actual1 = await readSnippet('html.json', testClient); + assert.equal(actual1, htmlSnippet1); + const actual2 = await readSnippet('typescript.json', testClient); + assert.equal(actual2, tsSnippet1); + }); + + function parseSnippets(content: string): IStringDictionary { + const syncData: ISyncData = JSON.parse(content); + return JSON.parse(syncData.content); + } + + async function updateSnippet(name: string, content: string, client: UserDataSyncClient): Promise { + const fileService = client.instantiationService.get(IFileService); + const environmentService = client.instantiationService.get(IEnvironmentService); + const snippetsResource = joinPath(environmentService.snippetsHome, name); + await fileService.writeFile(snippetsResource, VSBuffer.fromString(content)); + } + + async function removeSnippet(name: string, client: UserDataSyncClient): Promise { + const fileService = client.instantiationService.get(IFileService); + const environmentService = client.instantiationService.get(IEnvironmentService); + const snippetsResource = joinPath(environmentService.snippetsHome, name); + await fileService.del(snippetsResource); + } + + async function readSnippet(name: string, client: UserDataSyncClient): Promise { + const fileService = client.instantiationService.get(IFileService); + const environmentService = client.instantiationService.get(IEnvironmentService); + const snippetsResource = joinPath(environmentService.snippetsHome, name); + if (await fileService.exists(snippetsResource)) { + const content = await fileService.readFile(snippetsResource); + return content.value.toString(); + } + return null; + } + + function assertConflicts(actual: Conflict[], expected: Conflict[]) { + assert.deepEqual(actual.map(({ local, remote }) => ({ local: local.toString(), remote: remote.toString() })), expected.map(({ local, remote }) => ({ local: local.toString(), remote: remote.toString() }))); + } + +}); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index a3c62e50951..216b4add3b2 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -53,6 +53,7 @@ export class UserDataSyncClient extends Disposable { userDataSyncHome, settingsResource: joinPath(userDataDirectory, 'settings.json'), keybindingsResource: joinPath(userDataDirectory, 'keybindings.json'), + snippetsHome: joinPath(userDataDirectory, 'snippets'), argvResource: joinPath(userDataDirectory, 'argv.json'), args: {} }); @@ -201,16 +202,13 @@ export class UserDataSyncTestServer implements IRequestService { } private async writeData(resource: string, content: string = '', headers: IHeaders = {}): Promise { - if (!headers['If-Match']) { - return this.toResponse(428); - } if (!this.session) { this.session = generateUuid(); } const resourceKey = ALL_SYNC_RESOURCES.find(key => key === resource); if (resourceKey) { const data = this.data.get(resourceKey); - if (headers['If-Match'] !== (data ? data.ref : '0')) { + if (headers['If-Match'] !== undefined && headers['If-Match'] !== (data ? data.ref : '0')) { return this.toResponse(412); } const ref = `${parseInt(data?.ref || '0') + 1}`; From c8781c6f7da3dbe612a392697bfac7d5a4ca52b9 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 19 Mar 2020 15:54:24 +0100 Subject: [PATCH 28/44] #86678 fix tests --- .../test/common/snippetsSync.test.ts | 4 +-- .../test/common/userDataSyncClient.ts | 1 + .../test/common/userDataSyncService.test.ts | 28 ++++++++++++++++++- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts b/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts index 2e1f2b7659e..664d2654d95 100644 --- a/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts @@ -138,12 +138,12 @@ suite('SnippetsSync', () => { setup(async () => { testClient = disposableStore.add(new UserDataSyncClient(server)); - await testClient.setUp(); + await testClient.setUp(true); testObject = (testClient.instantiationService.get(IUserDataSyncService) as UserDataSyncService).getSynchroniser(SyncResource.Snippets) as SnippetsSynchroniser; disposableStore.add(toDisposable(() => testClient.instantiationService.get(IUserDataSyncStoreService).clear())); client2 = disposableStore.add(new UserDataSyncClient(server)); - await client2.setUp(); + await client2.setUp(true); }); teardown(() => disposableStore.clear()); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index 216b4add3b2..059120340dc 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -109,6 +109,7 @@ export class UserDataSyncClient extends Disposable { if (!empty) { await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({}))); await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([]))); + await fileService.writeFile(joinPath(environmentService.snippetsHome, 'c.json'), VSBuffer.fromString(`{}`)); await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'en' }))); } await configurationService.reloadConfiguration(); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts index aadf8b52690..58544432f2a 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts @@ -10,6 +10,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { IFileService } from 'vs/platform/files/common/files'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { VSBuffer } from 'vs/base/common/buffer'; +import { joinPath } from 'vs/base/common/resources'; suite('UserDataSyncService', () => { @@ -36,6 +37,9 @@ suite('UserDataSyncService', () => { // Keybindings { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, { type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '0' } }, + // Snippets + { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '0' } }, // Global state { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } }, @@ -65,6 +69,9 @@ suite('UserDataSyncService', () => { { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, // Keybindings { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, + // Snippets + { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '0' } }, // Global state { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } }, @@ -102,6 +109,8 @@ suite('UserDataSyncService', () => { { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, // Keybindings { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, + // Snippets + { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} }, // Global state { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, // Extensions @@ -140,6 +149,8 @@ suite('UserDataSyncService', () => { { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, // Keybindings { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, + // Snippets + { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} }, // Global state { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, // Extensions @@ -174,6 +185,8 @@ suite('UserDataSyncService', () => { { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, // Keybindings { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, + // Snippets + { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} }, // Global state { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, // Extensions @@ -198,6 +211,7 @@ suite('UserDataSyncService', () => { await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 }))); await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }]))); await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' }))); + await fileService.writeFile(joinPath(environmentService.snippetsHome, 'html.json'), VSBuffer.fromString(`{}`)); const testObject = testClient.instantiationService.get(IUserDataSyncService); // Sync (merge) from the test client @@ -215,6 +229,9 @@ suite('UserDataSyncService', () => { // Keybindings { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, { type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '1' } }, + // Snippets + { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '1' } }, // Global state { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '1' } }, @@ -258,6 +275,7 @@ suite('UserDataSyncService', () => { const environmentService = client.instantiationService.get(IEnvironmentService); await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 }))); await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }]))); + await fileService.writeFile(joinPath(environmentService.snippetsHome, 'html.json'), VSBuffer.fromString(`{}`)); await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' }))); // Sync from the client @@ -270,6 +288,8 @@ suite('UserDataSyncService', () => { { type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '1' } }, // Keybindings { type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '1' } }, + // Snippets + { type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '1' } }, // Global state { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '1' } }, ]); @@ -294,6 +314,7 @@ suite('UserDataSyncService', () => { const environmentService = client.instantiationService.get(IEnvironmentService); await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 }))); await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }]))); + await fileService.writeFile(joinPath(environmentService.snippetsHome, 'html.json'), VSBuffer.fromString(`{ "a": "changed" }`)); await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' }))); await client.instantiationService.get(IUserDataSyncService).sync(); @@ -308,6 +329,8 @@ suite('UserDataSyncService', () => { { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: { 'If-None-Match': '1' } }, // Keybindings { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: { 'If-None-Match': '1' } }, + // Snippets + { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: { 'If-None-Match': '1' } }, // Global state { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: { 'If-None-Match': '1' } }, ]); @@ -359,6 +382,9 @@ suite('UserDataSyncService', () => { // Keybindings { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, { type: 'POST', url: `${target.url}/v1/resource/keybindings`, headers: { 'If-Match': '0' } }, + // Snippets + { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '0' } }, // Global state { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } }, @@ -454,7 +480,7 @@ suite('UserDataSyncService', () => { await testObject.sync(); disposable.dispose(); - assert.deepEqual(actualStatuses, [SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle]); + assert.deepEqual(actualStatuses, [SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle, SyncStatus.Syncing, SyncStatus.Idle]); }); test('test sync conflicts status', async () => { From e2b5a54822a2125910bfea747731612a6e3f8373 Mon Sep 17 00:00:00 2001 From: isidor Date: Thu, 19 Mar 2020 15:54:48 +0100 Subject: [PATCH 29/44] diff view: go to next, go to previosu should also work when focus is not in editor #88695 --- src/vs/editor/browser/widget/diffReview.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/browser/widget/diffReview.ts b/src/vs/editor/browser/widget/diffReview.ts index eb22ce3dba0..7ac0566266e 100644 --- a/src/vs/editor/browser/widget/diffReview.ts +++ b/src/vs/editor/browser/widget/diffReview.ts @@ -869,9 +869,14 @@ class DiffReviewPrev extends EditorAction { function findFocusedDiffEditor(accessor: ServicesAccessor): DiffEditorWidget | null { const codeEditorService = accessor.get(ICodeEditorService); const diffEditors = codeEditorService.listDiffEditors(); + const activeCodeEditor = codeEditorService.getActiveCodeEditor(); + if (!activeCodeEditor) { + return null; + } + for (let i = 0, len = diffEditors.length; i < len; i++) { const diffEditor = diffEditors[i]; - if (diffEditor.hasWidgetFocus()) { + if (diffEditor.getModifiedEditor().getId() === activeCodeEditor.getId() || diffEditor.getOriginalEditor().getId() === activeCodeEditor.getId()) { return diffEditor; } } From b9a877dedd31bfc88dcf3ef8d4d8b83608da2f9a Mon Sep 17 00:00:00 2001 From: isidor Date: Thu, 19 Mar 2020 16:01:10 +0100 Subject: [PATCH 30/44] diffReview: read line count at the end #88695 --- src/vs/editor/browser/widget/diffReview.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/vs/editor/browser/widget/diffReview.ts b/src/vs/editor/browser/widget/diffReview.ts index 7ac0566266e..84f49273afb 100644 --- a/src/vs/editor/browser/widget/diffReview.ts +++ b/src/vs/editor/browser/widget/diffReview.ts @@ -747,13 +747,13 @@ export class DiffReview extends Disposable { let ariaLabel: string = ''; switch (type) { case DiffEntryType.Equal: - ariaLabel = nls.localize('equalLine', "original {0}, modified {1}: {2}", originalLine, modifiedLine, lineContent); + ariaLabel = nls.localize('equalLine', "{0} original line {1} modified line {2}", lineContent, originalLine, modifiedLine); break; case DiffEntryType.Insert: - ariaLabel = nls.localize('insertLine', "+ modified {0}: {1}", modifiedLine, lineContent); + ariaLabel = nls.localize('insertLine', "+ {0} modified line {1}", lineContent, modifiedLine); break; case DiffEntryType.Delete: - ariaLabel = nls.localize('deleteLine', "- original {0}: {1}", originalLine, lineContent); + ariaLabel = nls.localize('deleteLine', "- {0} original line {1}", lineContent, originalLine); break; } row.setAttribute('aria-label', ariaLabel); From 89522537066163dd038bb8e6e98c2ce75a728d76 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 19 Mar 2020 16:16:23 +0100 Subject: [PATCH 31/44] quick access - fix quick navigate to not record any keyMods --- src/vs/base/parts/quickinput/browser/quickInput.ts | 9 ++++++++- src/vs/base/parts/quickinput/common/quickInput.ts | 2 ++ .../contrib/quickAccess/editorNavigationQuickAccess.ts | 6 +++--- .../editor/contrib/quickAccess/gotoLineQuickAccess.ts | 2 +- .../contrib/quickAccess/gotoSymbolQuickAccess.ts | 4 ++-- .../browser/quickaccess/gotoLineQuickAccess.ts | 10 +++++----- .../browser/quickaccess/gotoSymbolQuickAccess.ts | 10 +++++----- 7 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/vs/base/parts/quickinput/browser/quickInput.ts b/src/vs/base/parts/quickinput/browser/quickInput.ts index b13b721e64c..8523c13172c 100644 --- a/src/vs/base/parts/quickinput/browser/quickInput.ts +++ b/src/vs/base/parts/quickinput/browser/quickInput.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/quickInput'; -import { IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInput, IQuickInputButton, IInputBox, IQuickPickItemButtonEvent, QuickPickInput, IQuickPickSeparator, IKeyMods, IQuickPickAcceptEvent } from 'vs/base/parts/quickinput/common/quickInput'; +import { IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInput, IQuickInputButton, IInputBox, IQuickPickItemButtonEvent, QuickPickInput, IQuickPickSeparator, IKeyMods, IQuickPickAcceptEvent, NO_KEY_MODS } from 'vs/base/parts/quickinput/common/quickInput'; import * as dom from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; import { QuickInputList } from './quickInputList'; @@ -549,6 +549,13 @@ class QuickPick extends QuickInput implements IQuickPi } get keyMods() { + if (this._quickNavigate) { + // Disable keyMods when quick navigate is enabled + // because in this model the interaction is purely + // keyboard driven and Ctrl/Alt are typically + // pressed and hold during this interaction. + return NO_KEY_MODS; + } return this.ui.keyMods; } diff --git a/src/vs/base/parts/quickinput/common/quickInput.ts b/src/vs/base/parts/quickinput/common/quickInput.ts index d71b82cdbed..398a7e6b922 100644 --- a/src/vs/base/parts/quickinput/common/quickInput.ts +++ b/src/vs/base/parts/quickinput/common/quickInput.ts @@ -49,6 +49,8 @@ export interface IKeyMods { readonly alt: boolean; } +export const NO_KEY_MODS: IKeyMods = { ctrlCmd: false, alt: false }; + export interface IQuickNavigateConfiguration { keybindings: ResolvedKeybinding[]; } diff --git a/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts b/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts index fd67274474c..5fcd0072247 100644 --- a/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/editorNavigationQuickAccess.ts @@ -110,9 +110,9 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu */ protected abstract provideWithoutTextEditor(picker: IQuickPick, token: CancellationToken): IDisposable; - protected gotoLocation(editor: IEditor, range: IRange, keyMods: IKeyMods, forceSideBySide?: boolean): void { - editor.setSelection(range); - editor.revealRangeInCenter(range, ScrollType.Smooth); + protected gotoLocation(editor: IEditor, options: { range: IRange, keyMods: IKeyMods, forceSideBySide?: boolean }): void { + editor.setSelection(options.range); + editor.revealRangeInCenter(options.range, ScrollType.Smooth); editor.focus(); } diff --git a/src/vs/editor/contrib/quickAccess/gotoLineQuickAccess.ts b/src/vs/editor/contrib/quickAccess/gotoLineQuickAccess.ts index e24cf811a69..835b99d634d 100644 --- a/src/vs/editor/contrib/quickAccess/gotoLineQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/gotoLineQuickAccess.ts @@ -38,7 +38,7 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor return; } - this.gotoLocation(editor, this.toRange(item.lineNumber, item.column), picker.keyMods); + this.gotoLocation(editor, { range: this.toRange(item.lineNumber, item.column), keyMods: picker.keyMods }); picker.hide(); } diff --git a/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts b/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts index c70d4e06383..60ac2ca9464 100644 --- a/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/gotoSymbolQuickAccess.ts @@ -95,7 +95,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit disposables.add(picker.onDidAccept(() => { const [item] = picker.selectedItems; if (item && item.range) { - this.gotoLocation(editor, item.range.selection, picker.keyMods); + this.gotoLocation(editor, { range: item.range.selection, keyMods: picker.keyMods }); picker.hide(); } @@ -104,7 +104,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit // Goto symbol side by side if enabled disposables.add(picker.onDidTriggerItemButton(({ item }) => { if (item && item.range) { - this.gotoLocation(editor, item.range.selection, picker.keyMods, true); + this.gotoLocation(editor, { range: item.range.selection, keyMods: picker.keyMods, forceSideBySide: true }); picker.hide(); } diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts index 0e8c8f29aa5..3087dfeb240 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts @@ -37,19 +37,19 @@ export class GotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProv return this.editorService.activeTextEditorControl; } - protected gotoLocation(editor: IEditor, range: IRange, keyMods: IKeyMods, forceSideBySide?: boolean): void { + protected gotoLocation(editor: IEditor, options: { range: IRange, keyMods: IKeyMods, forceSideBySide?: boolean }): void { // Check for sideBySide use - if ((keyMods.ctrlCmd || forceSideBySide) && this.editorService.activeEditor) { + if ((options.keyMods.ctrlCmd || options.forceSideBySide) && this.editorService.activeEditor) { this.editorService.openEditor(this.editorService.activeEditor, { - selection: range, - pinned: keyMods.alt || this.configuration.openEditorPinned + selection: options.range, + pinned: options.keyMods.alt || this.configuration.openEditorPinned }, SIDE_GROUP); } // Otherwise let parent handle it else { - super.gotoLocation(editor, range, keyMods); + super.gotoLocation(editor, options); } } } diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts index e3a213f9084..acff6f07b55 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts @@ -40,19 +40,19 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess return this.editorService.activeTextEditorControl; } - protected gotoLocation(editor: IEditor, range: IRange, keyMods: IKeyMods, forceSideBySide?: boolean): void { + protected gotoLocation(editor: IEditor, options: { range: IRange, keyMods: IKeyMods, forceSideBySide?: boolean }): void { // Check for sideBySide use - if ((keyMods.ctrlCmd || forceSideBySide) && this.editorService.activeEditor) { + if ((options.keyMods.ctrlCmd || options.forceSideBySide) && this.editorService.activeEditor) { this.editorService.openEditor(this.editorService.activeEditor, { - selection: range, - pinned: keyMods.alt || this.configuration.openEditorPinned + selection: options.range, + pinned: options.keyMods.alt || this.configuration.openEditorPinned }, SIDE_GROUP); } // Otherwise let parent handle it else { - super.gotoLocation(editor, range, keyMods); + super.gotoLocation(editor, options); } } } From b092489ffd1c64e63cf5c9863d425a3befdf67ff Mon Sep 17 00:00:00 2001 From: Andre Weinand Date: Thu, 19 Mar 2020 16:38:00 +0100 Subject: [PATCH 32/44] updated DAP to latest --- .../contrib/debug/common/debugProtocol.d.ts | 281 +++++++++++++----- 1 file changed, 208 insertions(+), 73 deletions(-) diff --git a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts index 9fc4d468b56..4307295e632 100644 --- a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts +++ b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - + /** Declaration module describing the VS Code debug protocol. Auto-generated from json schema. Do not edit manually. */ @@ -72,12 +72,15 @@ declare module DebugProtocol { /** Cancel request; value of command field is 'cancel'. The 'cancel' request is used by the frontend in two situations: - to indicate that it is no longer interested in the result produced by a specific request issued earlier - - to cancel a progress sequence. + - to cancel a progress sequence. Clients should only call this request if the capability 'supportsCancelRequest' is true. This request has a hint characteristic: a debug adapter can only be expected to make a 'best effort' in honouring this request but there are no guarantees. The 'cancel' request may return an error if it could not cancel an operation but a frontend should refrain from presenting this error to end users. A frontend client should only call this request if the capability 'supportsCancelRequest' is true. - The request that got canceled still needs to send a response back. This can either be a normal result ('success' attribute true) or an error response ('success' attribute false and the 'message' set to 'cancelled'). Returning partial results from a cancelled request is possible but please note that a frontend client has no generic way for detecting that a response is partial or not. - The progress that got cancelled still needs to send a 'progressEnd' event back. A client should not assume that progress just got cancelled after sending the 'cancel' request. + The request that got canceled still needs to send a response back. This can either be a normal result ('success' attribute true) + or an error response ('success' attribute false and the 'message' set to 'cancelled'). + Returning partial results from a cancelled request is possible but please note that a frontend client has no generic way for detecting that a response is partial or not. + The progress that got cancelled still needs to send a 'progressEnd' event back. + A client should not assume that progress just got cancelled after sending the 'cancel' request. */ export interface CancelRequest extends Request { // command: 'cancel'; @@ -86,9 +89,13 @@ declare module DebugProtocol { /** Arguments for 'cancel' request. */ export interface CancelArguments { - /** The ID (attribute 'seq') of the request to cancel. If missing no request is cancelled. Both a 'requestId' and a 'progressId' can be specified in one request. */ + /** The ID (attribute 'seq') of the request to cancel. If missing no request is cancelled. + Both a 'requestId' and a 'progressId' can be specified in one request. + */ requestId?: number; - /** The ID (attribute 'progressId') of the progress to cancel. If missing no progress is cancelled. Both a 'requestId' and a 'progressId' can be specified in one request. */ + /** The ID (attribute 'progressId') of the progress to cancel. If missing no progress is cancelled. + Both a 'requestId' and a 'progressId' can be specified in one request. + */ progressId?: string; } @@ -309,11 +316,14 @@ declare module DebugProtocol { The event signals that a long running operation is about to start and provides additional information for the client to set up a corresponding progress and cancellation UI. The client is free to delay the showing of the UI in order to reduce flicker. + This event should only be sent if the client has passed the value true for the 'supportsProgressReporting' capability of the 'initialize' request. */ export interface ProgressStartEvent extends Event { // event: 'progressStart'; body: { - /** An ID that must be used in subsequent 'progressUpdate' and 'progressEnd' events to make them refer to the same progress reporting. IDs must be unique within a debug session. */ + /** An ID that must be used in subsequent 'progressUpdate' and 'progressEnd' events to make them refer to the same progress reporting. + IDs must be unique within a debug session. + */ progressId: string; /** Mandatory (short) title of the progress reporting. Shown in the UI to describe the long running operation. */ title: string; @@ -337,6 +347,7 @@ declare module DebugProtocol { /** Event message for 'progressUpdate' event type. The event signals that the progress reporting needs to updated with a new message and/or percentage. The client does not have to update the UI immediately, but the clients needs to keep track of the message and/or percentage values. + This event should only be sent if the client has passed the value true for the 'supportsProgressReporting' capability of the 'initialize' request. */ export interface ProgressUpdateEvent extends Event { // event: 'progressUpdate'; @@ -352,6 +363,7 @@ declare module DebugProtocol { /** Event message for 'progressEnd' event type. The event signals the end of the progress reporting with an optional final message. + This event should only be sent if the client has passed the value true for the 'supportsProgressReporting' capability of the 'initialize' request. */ export interface ProgressEndEvent extends Event { // event: 'progressEnd'; @@ -364,7 +376,9 @@ declare module DebugProtocol { } /** RunInTerminal request; value of command field is 'runInTerminal'. - This request is sent from the debug adapter to the client to run a command in a terminal. This is typically used to launch the debuggee in a terminal provided by the client. + This optional request is sent from the debug adapter to the client to run a command in a terminal. + This is typically used to launch the debuggee in a terminal provided by the client. + This request should only be called if the client has passed the value true for the 'supportsRunInTerminalRequest' capability of the 'initialize' request. */ export interface RunInTerminalRequest extends Request { // command: 'runInTerminal'; @@ -396,8 +410,10 @@ declare module DebugProtocol { } /** Initialize request; value of command field is 'initialize'. - The 'initialize' request is sent as the first request from the client to the debug adapter in order to configure it with client capabilities and to retrieve capabilities from the debug adapter. - Until the debug adapter has responded to with an 'initialize' response, the client must not send any additional requests or events to the debug adapter. In addition the debug adapter is not allowed to send any requests or events to the client until it has responded with an 'initialize' response. + The 'initialize' request is sent as the first request from the client to the debug adapter + in order to configure it with client capabilities and to retrieve capabilities from the debug adapter. + Until the debug adapter has responded to with an 'initialize' response, the client must not send any additional requests or events to the debug adapter. + In addition the debug adapter is not allowed to send any requests or events to the client until it has responded with an 'initialize' response. The 'initialize' request may only be sent once. */ export interface InitializeRequest extends Request { @@ -442,7 +458,9 @@ declare module DebugProtocol { } /** ConfigurationDone request; value of command field is 'configurationDone'. - The client of the debug protocol must send this request at the end of the sequence of configuration requests (which was started by the 'initialized' event). + This optional request indicates that the client has finished initialization of the debug adapter. + So it is the last request in the sequence of configuration requests (which was started by the 'initialized' event). + Clients should only call this request if the capability 'supportsConfigurationDoneRequest' is true. */ export interface ConfigurationDoneRequest extends Request { // command: 'configurationDone'; @@ -458,7 +476,8 @@ declare module DebugProtocol { } /** Launch request; value of command field is 'launch'. - The launch request is sent from the client to the debug adapter to start the debuggee with or without debugging (if 'noDebug' is true). Since launching is debugger/runtime specific, the arguments for this request are not part of this specification. + This launch request is sent from the client to the debug adapter to start the debuggee with or without debugging (if 'noDebug' is true). + Since launching is debugger/runtime specific, the arguments for this request are not part of this specification. */ export interface LaunchRequest extends Request { // command: 'launch'; @@ -481,7 +500,8 @@ declare module DebugProtocol { } /** Attach request; value of command field is 'attach'. - The attach request is sent from the client to the debug adapter to attach to a debuggee that is already running. Since attaching is debugger/runtime specific, the arguments for this request are not part of this specification. + The attach request is sent from the client to the debug adapter to attach to a debuggee that is already running. + Since attaching is debugger/runtime specific, the arguments for this request are not part of this specification. */ export interface AttachRequest extends Request { // command: 'attach'; @@ -502,10 +522,8 @@ declare module DebugProtocol { } /** Restart request; value of command field is 'restart'. - Restarts a debug session. If the capability 'supportsRestartRequest' is missing or has the value false, - the client will implement 'restart' by terminating the debug adapter first and then launching it anew. - A debug adapter can override this default behaviour by implementing a restart request - and setting the capability 'supportsRestartRequest' to true. + Restarts a debug session. Clients should only call this request if the capability 'supportsRestartRequest' is true. + If the capability is missing or has the value false, a typical client will emulate 'restart' by terminating the debug adapter first and then launching it anew. */ export interface RestartRequest extends Request { // command: 'restart'; @@ -521,7 +539,11 @@ declare module DebugProtocol { } /** Disconnect request; value of command field is 'disconnect'. - The 'disconnect' request is sent from the client to the debug adapter in order to stop debugging. It asks the debug adapter to disconnect from the debuggee and to terminate the debug adapter. If the debuggee has been started with the 'launch' request, the 'disconnect' request terminates the debuggee. If the 'attach' request was used to connect to the debuggee, 'disconnect' does not terminate the debuggee. This behavior can be controlled with the 'terminateDebuggee' argument (if supported by the debug adapter). + The 'disconnect' request is sent from the client to the debug adapter in order to stop debugging. + It asks the debug adapter to disconnect from the debuggee and to terminate the debug adapter. + If the debuggee has been started with the 'launch' request, the 'disconnect' request terminates the debuggee. + If the 'attach' request was used to connect to the debuggee, 'disconnect' does not terminate the debuggee. + This behavior can be controlled with the 'terminateDebuggee' argument (if supported by the debug adapter). */ export interface DisconnectRequest extends Request { // command: 'disconnect'; @@ -534,7 +556,7 @@ declare module DebugProtocol { restart?: boolean; /** Indicates whether the debuggee should be terminated when the debugger is disconnected. If unspecified, the debug adapter is free to do whatever it thinks is best. - A client can only rely on this attribute being properly honored if a debug adapter returns true for the 'supportTerminateDebuggee' capability. + The attribute is only honored by a debug adapter if the capability 'supportTerminateDebuggee' is true. */ terminateDebuggee?: boolean; } @@ -545,6 +567,7 @@ declare module DebugProtocol { /** Terminate request; value of command field is 'terminate'. The 'terminate' request is sent from the client to the debug adapter in order to give the debuggee a chance for terminating itself. + Clients should only call this request if the capability 'supportsTerminateRequest' is true. */ export interface TerminateRequest extends Request { // command: 'terminate'; @@ -563,6 +586,7 @@ declare module DebugProtocol { /** BreakpointLocations request; value of command field is 'breakpointLocations'. The 'breakpointLocations' request returns all possible locations for source breakpoints in a given range. + Clients should only call this request if the capability 'supportsBreakpointLocationsRequest' is true. */ export interface BreakpointLocationsRequest extends Request { // command: 'breakpointLocations'; @@ -623,7 +647,9 @@ declare module DebugProtocol { */ export interface SetBreakpointsResponse extends Response { body: { - /** Information about the breakpoints. The array elements are in the same order as the elements of the 'breakpoints' (or the deprecated 'lines') array in the arguments. */ + /** Information about the breakpoints. + The array elements are in the same order as the elements of the 'breakpoints' (or the deprecated 'lines') array in the arguments. + */ breakpoints: Breakpoint[]; }; } @@ -632,6 +658,7 @@ declare module DebugProtocol { Replaces all existing function breakpoints with new function breakpoints. To clear all function breakpoints, specify an empty array. When a function breakpoint is hit, a 'stopped' event (with reason 'function breakpoint') is generated. + Clients should only call this request if the capability 'supportsFunctionBreakpoints' is true. */ export interface SetFunctionBreakpointsRequest extends Request { // command: 'setFunctionBreakpoints'; @@ -655,7 +682,9 @@ declare module DebugProtocol { } /** SetExceptionBreakpoints request; value of command field is 'setExceptionBreakpoints'. - The request configures the debuggers response to thrown exceptions. If an exception is configured to break, a 'stopped' event is fired (with reason 'exception'). + The request configures the debuggers response to thrown exceptions. + If an exception is configured to break, a 'stopped' event is fired (with reason 'exception'). + Clients should only call this request if the capability 'exceptionBreakpointFilters' returns one or more filters. */ export interface SetExceptionBreakpointsRequest extends Request { // command: 'setExceptionBreakpoints'; @@ -666,7 +695,9 @@ declare module DebugProtocol { export interface SetExceptionBreakpointsArguments { /** IDs of checked exception options. The set of IDs is returned via the 'exceptionBreakpointFilters' capability. */ filters: string[]; - /** Configuration options for selected exceptions. */ + /** Configuration options for selected exceptions. + The attribute is only honored by a debug adapter if the capability 'supportsExceptionOptions' is true. + */ exceptionOptions?: ExceptionOptions[]; } @@ -676,6 +707,7 @@ declare module DebugProtocol { /** DataBreakpointInfo request; value of command field is 'dataBreakpointInfo'. Obtains information on a possible data breakpoint that could be set on an expression or variable. + Clients should only call this request if the capability 'supportsDataBreakpoints' is true. */ export interface DataBreakpointInfoRequest extends Request { // command: 'dataBreakpointInfo'; @@ -686,7 +718,9 @@ declare module DebugProtocol { export interface DataBreakpointInfoArguments { /** Reference to the Variable container if the data breakpoint is requested for a child of the container. */ variablesReference?: number; - /** The name of the Variable's child to obtain data breakpoint information for. If variableReference isn’t provided, this can be an expression. */ + /** The name of the Variable's child to obtain data breakpoint information for. + If variableReference isn’t provided, this can be an expression. + */ name: string; } @@ -708,6 +742,7 @@ declare module DebugProtocol { Replaces all existing data breakpoints with new data breakpoints. To clear all data breakpoints, specify an empty array. When a data breakpoint is hit, a 'stopped' event (with reason 'data breakpoint') is generated. + Clients should only call this request if the capability 'supportsDataBreakpoints' is true. */ export interface SetDataBreakpointsRequest extends Request { // command: 'setDataBreakpoints'; @@ -740,14 +775,18 @@ declare module DebugProtocol { /** Arguments for 'continue' request. */ export interface ContinueArguments { - /** Continue execution for the specified thread (if possible). If the backend cannot continue on a single thread but will continue on all threads, it should set the 'allThreadsContinued' attribute in the response to true. */ + /** Continue execution for the specified thread (if possible). + If the backend cannot continue on a single thread but will continue on all threads, it should set the 'allThreadsContinued' attribute in the response to true. + */ threadId: number; } /** Response to 'continue' request. */ export interface ContinueResponse extends Response { body: { - /** If true, the 'continue' request has ignored the specified thread and continued all threads instead. If this attribute is missing a value of 'true' is assumed for backward compatibility. */ + /** If true, the 'continue' request has ignored the specified thread and continued all threads instead. + If this attribute is missing a value of 'true' is assumed for backward compatibility. + */ allThreadsContinued?: boolean; }; } @@ -817,7 +856,8 @@ declare module DebugProtocol { /** StepBack request; value of command field is 'stepBack'. The request starts the debuggee to run one step backwards. - The debug adapter first sends the response and then a 'stopped' event (with reason 'step') after the step has completed. Clients should only call this request if the capability 'supportsStepBack' is true. + The debug adapter first sends the response and then a 'stopped' event (with reason 'step') after the step has completed. + Clients should only call this request if the capability 'supportsStepBack' is true. */ export interface StepBackRequest extends Request { // command: 'stepBack'; @@ -835,7 +875,8 @@ declare module DebugProtocol { } /** ReverseContinue request; value of command field is 'reverseContinue'. - The request starts the debuggee to run backward. Clients should only call this request if the capability 'supportsStepBack' is true. + The request starts the debuggee to run backward. + Clients should only call this request if the capability 'supportsStepBack' is true. */ export interface ReverseContinueRequest extends Request { // command: 'reverseContinue'; @@ -855,6 +896,7 @@ declare module DebugProtocol { /** RestartFrame request; value of command field is 'restartFrame'. The request restarts execution of the specified stackframe. The debug adapter first sends the response and then a 'stopped' event (with reason 'restart') after the restart has completed. + Clients should only call this request if the capability 'supportsRestartFrame' is true. */ export interface RestartFrameRequest extends Request { // command: 'restartFrame'; @@ -876,6 +918,7 @@ declare module DebugProtocol { This makes it possible to skip the execution of code or to executed code again. The code between the current location and the goto target is not executed but skipped. The debug adapter first sends the response and then a 'stopped' event with reason 'goto'. + Clients should only call this request if the capability 'supportsGotoTargetsRequest' is true (because only then goto targets exist that can be passed as arguments). */ export interface GotoRequest extends Request { // command: 'goto'; @@ -929,7 +972,9 @@ declare module DebugProtocol { startFrame?: number; /** The maximum number of frames to return. If levels is not specified or 0, all frames are returned. */ levels?: number; - /** Specifies details on how to format the stack frames. */ + /** Specifies details on how to format the stack frames. + The attribute is only honored by a debug adapter if the capability 'supportsValueFormattingOptions' is true. + */ format?: StackFrameFormat; } @@ -986,7 +1031,9 @@ declare module DebugProtocol { start?: number; /** The number of variables to return. If count is missing or 0, all variables are returned. */ count?: number; - /** Specifies details on how to format the Variable values. */ + /** Specifies details on how to format the Variable values. + The attribute is only honored by a debug adapter if the capability 'supportsValueFormattingOptions' is true. + */ format?: ValueFormat; } @@ -999,7 +1046,7 @@ declare module DebugProtocol { } /** SetVariable request; value of command field is 'setVariable'. - Set the variable with the given name in the variable container to a new value. + Set the variable with the given name in the variable container to a new value. Clients should only call this request if the capability 'supportsSetVariable' is true. */ export interface SetVariableRequest extends Request { // command: 'setVariable'; @@ -1025,14 +1072,18 @@ declare module DebugProtocol { value: string; /** The type of the new value. Typically shown in the UI when hovering over the value. */ type?: string; - /** If variablesReference is > 0, the new value is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. The value should be less than or equal to 2147483647 (2^31 - 1). */ + /** If variablesReference is > 0, the new value is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. + The value should be less than or equal to 2147483647 (2^31 - 1). + */ variablesReference?: number; /** The number of named child variables. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. The value should be less than or equal to 2147483647 (2^31 - 1). + The client can use this optional information to present the variables in a paged UI and fetch them in chunks. + The value should be less than or equal to 2147483647 (2^31 - 1). */ namedVariables?: number; /** The number of indexed child variables. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. The value should be less than or equal to 2147483647 (2^31 - 1). + The client can use this optional information to present the variables in a paged UI and fetch them in chunks. + The value should be less than or equal to 2147483647 (2^31 - 1). */ indexedVariables?: number; }; @@ -1050,7 +1101,9 @@ declare module DebugProtocol { export interface SourceArguments { /** Specifies the source content to load. Either source.path or source.sourceReference must be specified. */ source?: Source; - /** The reference to the source. This is the same as source.sourceReference. This is provided for backward compatibility since old backends do not understand the 'source' attribute. */ + /** The reference to the source. This is the same as source.sourceReference. + This is provided for backward compatibility since old backends do not understand the 'source' attribute. + */ sourceReference: number; } @@ -1081,6 +1134,7 @@ declare module DebugProtocol { /** TerminateThreads request; value of command field is 'terminateThreads'. The request terminates the threads with the given ids. + Clients should only call this request if the capability 'supportsTerminateThreadsRequest' is true. */ export interface TerminateThreadsRequest extends Request { // command: 'terminateThreads'; @@ -1098,7 +1152,8 @@ declare module DebugProtocol { } /** Modules request; value of command field is 'modules'. - Modules can be retrieved from the debug adapter with the ModulesRequest which can either return all modules or a range of modules to support paging. + Modules can be retrieved from the debug adapter with this request which can either return all modules or a range of modules to support paging. + Clients should only call this request if the capability 'supportsModulesRequest' is true. */ export interface ModulesRequest extends Request { // command: 'modules'; @@ -1125,6 +1180,7 @@ declare module DebugProtocol { /** LoadedSources request; value of command field is 'loadedSources'. Retrieves the set of all sources currently loaded by the debugged process. + Clients should only call this request if the capability 'supportsLoadedSourcesRequest' is true. */ export interface LoadedSourcesRequest extends Request { // command: 'loadedSources'; @@ -1164,10 +1220,13 @@ declare module DebugProtocol { 'repl': evaluate is run from REPL console. 'hover': evaluate is run from a data hover. 'clipboard': evaluate is run to generate the value that will be stored in the clipboard. + The attribute is only honored by a debug adapter if the capability 'supportsClipboardContext' is true. etc. */ context?: string; - /** Specifies details on how to format the Evaluate result. */ + /** Specifies details on how to format the Evaluate result. + The attribute is only honored by a debug adapter if the capability 'supportsValueFormattingOptions' is true. + */ format?: ValueFormat; } @@ -1176,21 +1235,30 @@ declare module DebugProtocol { body: { /** The result of the evaluate request. */ result: string; - /** The optional type of the evaluate result. */ + /** The optional type of the evaluate result. + This attribute should only be returned by a debug adapter if the client has passed the value true for the 'supportsVariableType' capability of the 'initialize' request. + */ type?: string; /** Properties of a evaluate result that can be used to determine how to render the result in the UI. */ presentationHint?: VariablePresentationHint; - /** If variablesReference is > 0, the evaluate result is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. The value should be less than or equal to 2147483647 (2^31 - 1). */ + /** If variablesReference is > 0, the evaluate result is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. + The value should be less than or equal to 2147483647 (2^31 - 1). + */ variablesReference: number; /** The number of named child variables. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. The value should be less than or equal to 2147483647 (2^31 - 1). + The client can use this optional information to present the variables in a paged UI and fetch them in chunks. + The value should be less than or equal to 2147483647 (2^31 - 1). */ namedVariables?: number; /** The number of indexed child variables. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. The value should be less than or equal to 2147483647 (2^31 - 1). + The client can use this optional information to present the variables in a paged UI and fetch them in chunks. + The value should be less than or equal to 2147483647 (2^31 - 1). */ indexedVariables?: number; - /** Memory reference to a location appropriate for this result. For pointer type eval results, this is generally a reference to the memory address contained in the pointer. */ + /** Optional memory reference to a location appropriate for this result. + For pointer type eval results, this is generally a reference to the memory address contained in the pointer. + This attribute should be returned by a debug adapter if the client has passed the value true for the 'supportsMemoryReferences' capability of the 'initialize' request. + */ memoryReference?: string; }; } @@ -1198,6 +1266,7 @@ declare module DebugProtocol { /** SetExpression request; value of command field is 'setExpression'. Evaluates the given 'value' expression and assigns it to the 'expression' which must be a modifiable l-value. The expressions have access to any variables and arguments that are in scope of the specified frame. + Clients should only call this request if the capability 'supportsSetExpression' is true. */ export interface SetExpressionRequest extends Request { // command: 'setExpression'; @@ -1221,18 +1290,24 @@ declare module DebugProtocol { body: { /** The new value of the expression. */ value: string; - /** The optional type of the value. */ + /** The optional type of the value. + This attribute should only be returned by a debug adapter if the client has passed the value true for the 'supportsVariableType' capability of the 'initialize' request. + */ type?: string; /** Properties of a value that can be used to determine how to render the result in the UI. */ presentationHint?: VariablePresentationHint; - /** If variablesReference is > 0, the value is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. The value should be less than or equal to 2147483647 (2^31 - 1). */ + /** If variablesReference is > 0, the value is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. + The value should be less than or equal to 2147483647 (2^31 - 1). + */ variablesReference?: number; /** The number of named child variables. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. The value should be less than or equal to 2147483647 (2^31 - 1). + The client can use this optional information to present the variables in a paged UI and fetch them in chunks. + The value should be less than or equal to 2147483647 (2^31 - 1). */ namedVariables?: number; /** The number of indexed child variables. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. The value should be less than or equal to 2147483647 (2^31 - 1). + The client can use this optional information to present the variables in a paged UI and fetch them in chunks. + The value should be less than or equal to 2147483647 (2^31 - 1). */ indexedVariables?: number; }; @@ -1242,6 +1317,7 @@ declare module DebugProtocol { This request retrieves the possible stepIn targets for the specified stack frame. These targets can be used in the 'stepIn' request. The StepInTargets may only be called if the 'supportsStepInTargetsRequest' capability exists and is true. + Clients should only call this request if the capability 'supportsStepInTargetsRequest' is true. */ export interface StepInTargetsRequest extends Request { // command: 'stepInTargets'; @@ -1265,7 +1341,7 @@ declare module DebugProtocol { /** GotoTargets request; value of command field is 'gotoTargets'. This request retrieves the possible goto targets for the specified source location. These targets can be used in the 'goto' request. - The GotoTargets request may only be called if the 'supportsGotoTargetsRequest' capability exists and is true. + Clients should only call this request if the capability 'supportsGotoTargetsRequest' is true. */ export interface GotoTargetsRequest extends Request { // command: 'gotoTargets'; @@ -1292,7 +1368,7 @@ declare module DebugProtocol { /** Completions request; value of command field is 'completions'. Returns a list of possible completions for a given caret position and text. - The CompletionsRequest may only be called if the 'supportsCompletionsRequest' capability exists and is true. + Clients should only call this request if the capability 'supportsCompletionsRequest' is true. */ export interface CompletionsRequest extends Request { // command: 'completions'; @@ -1321,6 +1397,7 @@ declare module DebugProtocol { /** ExceptionInfo request; value of command field is 'exceptionInfo'. Retrieves the details of the exception that caused this event to be raised. + Clients should only call this request if the capability 'supportsExceptionInfoRequest' is true. */ export interface ExceptionInfoRequest extends Request { // command: 'exceptionInfo'; @@ -1349,6 +1426,7 @@ declare module DebugProtocol { /** ReadMemory request; value of command field is 'readMemory'. Reads bytes from memory at the provided location. + Clients should only call this request if the capability 'supportsReadMemoryRequest' is true. */ export interface ReadMemoryRequest extends Request { // command: 'readMemory'; @@ -1368,9 +1446,13 @@ declare module DebugProtocol { /** Response to 'readMemory' request. */ export interface ReadMemoryResponse extends Response { body?: { - /** The address of the first byte of data returned. Treated as a hex value if prefixed with '0x', or as a decimal value otherwise. */ + /** The address of the first byte of data returned. + Treated as a hex value if prefixed with '0x', or as a decimal value otherwise. + */ address: string; - /** The number of unreadable bytes encountered after the last successfully read byte. This can be used to determine the number of bytes that must be skipped before a subsequent 'readMemory' request will succeed. */ + /** The number of unreadable bytes encountered after the last successfully read byte. + This can be used to determine the number of bytes that must be skipped before a subsequent 'readMemory' request will succeed. + */ unreadableBytes?: number; /** The bytes read from memory, encoded using base64. */ data?: string; @@ -1379,6 +1461,7 @@ declare module DebugProtocol { /** Disassemble request; value of command field is 'disassemble'. Disassembles code stored at the provided location. + Clients should only call this request if the capability 'supportsDisassembleRequest' is true. */ export interface DisassembleRequest extends Request { // command: 'disassemble'; @@ -1393,7 +1476,9 @@ declare module DebugProtocol { offset?: number; /** Optional offset (in instructions) to be applied after the byte offset (if any) before disassembling. Can be negative. */ instructionOffset?: number; - /** Number of instructions to disassemble starting at the specified location and offset. An adapter must return exactly this number of instructions - any unavailable instructions should be replaced with an implementation-defined 'invalid instruction' value. */ + /** Number of instructions to disassemble starting at the specified location and offset. + An adapter must return exactly this number of instructions - any unavailable instructions should be replaced with an implementation-defined 'invalid instruction' value. + */ instructionCount: number; /** If true, the adapter should attempt to resolve memory addresses and other values to symbolic names. */ resolveSymbols?: boolean; @@ -1543,7 +1628,8 @@ declare module DebugProtocol { addressRange?: string; } - /** A ColumnDescriptor specifies what module attribute to show in a column of the ModulesView, how to format it, and what the column's label should be. + /** A ColumnDescriptor specifies what module attribute to show in a column of the ModulesView, how to format it, + and what the column's label should be. It is only used if the underlying UI actually supports this level of customization. */ export interface ColumnDescriptor { @@ -1574,21 +1660,34 @@ declare module DebugProtocol { name: string; } - /** A Source is a descriptor for source code. It is returned from the debug adapter as part of a StackFrame and it is used by clients when specifying breakpoints. */ + /** A Source is a descriptor for source code. + It is returned from the debug adapter as part of a StackFrame and it is used by clients when specifying breakpoints. + */ export interface Source { - /** The short name of the source. Every source returned from the debug adapter has a name. When sending a source to the debug adapter this name is optional. */ + /** The short name of the source. Every source returned from the debug adapter has a name. + When sending a source to the debug adapter this name is optional. + */ name?: string; - /** The path of the source to be shown in the UI. It is only used to locate and load the content of the source if no sourceReference is specified (or its value is 0). */ + /** The path of the source to be shown in the UI. + It is only used to locate and load the content of the source if no sourceReference is specified (or its value is 0). + */ path?: string; - /** If sourceReference > 0 the contents of the source must be retrieved through the SourceRequest (even if a path is specified). A sourceReference is only valid for a session, so it must not be used to persist a source. The value should be less than or equal to 2147483647 (2^31 - 1). */ + /** If sourceReference > 0 the contents of the source must be retrieved through the SourceRequest (even if a path is specified). + A sourceReference is only valid for a session, so it must not be used to persist a source. + The value should be less than or equal to 2147483647 (2^31 - 1). + */ sourceReference?: number; - /** An optional hint for how to present the source in the UI. A value of 'deemphasize' can be used to indicate that the source is not available or that it is skipped on stepping. */ + /** An optional hint for how to present the source in the UI. + A value of 'deemphasize' can be used to indicate that the source is not available or that it is skipped on stepping. + */ presentationHint?: 'normal' | 'emphasize' | 'deemphasize'; /** The (optional) origin of this source: possible values 'internal module', 'inlined content from source map', etc. */ origin?: string; /** An optional list of sources that are related to this source. These may be the source that generated this source. */ sources?: Source[]; - /** Optional data that a debug adapter might want to loop through the client. The client should leave the data intact and persist it across sessions. The client should not interpret the data. */ + /** Optional data that a debug adapter might want to loop through the client. + The client should leave the data intact and persist it across sessions. The client should not interpret the data. + */ adapterData?: any; /** The checksums associated with this file. */ checksums?: Checksum[]; @@ -1596,7 +1695,9 @@ declare module DebugProtocol { /** A Stackframe contains the source location. */ export interface StackFrame { - /** An identifier for the stack frame. It must be unique across all threads. This id can be used to retrieve the scopes of the frame with the 'scopesRequest' or to restart the execution of a stackframe. */ + /** An identifier for the stack frame. It must be unique across all threads. + This id can be used to retrieve the scopes of the frame with the 'scopesRequest' or to restart the execution of a stackframe. + */ id: number; /** The name of the stack frame, typically a method name. */ name: string; @@ -1614,7 +1715,9 @@ declare module DebugProtocol { instructionPointerReference?: string; /** The module associated with this frame, if any. */ moduleId?: number | string; - /** An optional hint for how to present this frame in the UI. A value of 'label' can be used to indicate that the frame is an artificial frame that is used as a visual label or separator. A value of 'subtle' can be used to change the appearance of a frame in a 'subtle' way. */ + /** An optional hint for how to present this frame in the UI. + A value of 'label' can be used to indicate that the frame is an artificial frame that is used as a visual label or separator. A value of 'subtle' can be used to change the appearance of a frame in a 'subtle' way. + */ presentationHint?: 'normal' | 'label' | 'subtle'; } @@ -1666,7 +1769,9 @@ declare module DebugProtocol { name: string; /** The variable's value. This can be a multi-line text, e.g. for a function the body of a function. */ value: string; - /** The type of the variable's value. Typically shown in the UI when hovering over the value. */ + /** The type of the variable's value. Typically shown in the UI when hovering over the value. + This attribute should only be returned by a debug adapter if the client has passed the value true for the 'supportsVariableType' capability of the 'initialize' request. + */ type?: string; /** Properties of a variable that can be used to determine how to render the variable in the UI. */ presentationHint?: VariablePresentationHint; @@ -1682,7 +1787,9 @@ declare module DebugProtocol { The client can use this optional information to present the children in a paged UI and fetch them in chunks. */ indexedVariables?: number; - /** Optional memory reference for the variable if the variable represents executable code, such as a function pointer. */ + /** Optional memory reference for the variable if the variable represents executable code, such as a function pointer. + This attribute is only required if the client has passed the value true for the 'supportsMemoryReferences' capability of the 'initialize' request. + */ memoryReference?: string; } @@ -1699,7 +1806,8 @@ declare module DebugProtocol { 'innerClass': Indicates that the object is an inner class. 'interface': Indicates that the object is an interface. 'mostDerivedClass': Indicates that the object is the most derived class. - 'virtual': Indicates that the object is virtual, that means it is a synthetic object introduced by the adapter for rendering purposes, e.g. an index range for large arrays. + 'virtual': Indicates that the object is virtual, that means it is a synthetic object introducedby the + adapter for rendering purposes, e.g. an index range for large arrays. 'dataBreakpoint': Indicates that a data breakpoint is registered for the object. etc. */ @@ -1740,11 +1848,19 @@ declare module DebugProtocol { line: number; /** An optional source column of the breakpoint. */ column?: number; - /** An optional expression for conditional breakpoints. */ + /** An optional expression for conditional breakpoints. + It is only honored by a debug adapter if the capability 'supportsConditionalBreakpoints' is true. + */ condition?: string; - /** An optional expression that controls how many hits of the breakpoint are ignored. The backend is expected to interpret the expression as needed. */ + /** An optional expression that controls how many hits of the breakpoint are ignored. + The backend is expected to interpret the expression as needed. + The attribute is only honored by a debug adapter if the capability 'supportsHitConditionalBreakpoints' is true. + */ hitCondition?: string; - /** If this attribute exists and is non-empty, the backend must not 'break' (stop) but log the message instead. Expressions within {} are interpolated. */ + /** If this attribute exists and is non-empty, the backend must not 'break' (stop) + but log the message instead. Expressions within {} are interpolated. + The attribute is only honored by a debug adapter if the capability 'supportsLogPoints' is true. + */ logMessage?: string; } @@ -1752,9 +1868,14 @@ declare module DebugProtocol { export interface FunctionBreakpoint { /** The name of the function. */ name: string; - /** An optional expression for conditional breakpoints. */ + /** An optional expression for conditional breakpoints. + It is only honored by a debug adapter if the capability 'supportsConditionalBreakpoints' is true. + */ condition?: string; - /** An optional expression that controls how many hits of the breakpoint are ignored. The backend is expected to interpret the expression as needed. */ + /** An optional expression that controls how many hits of the breakpoint are ignored. + The backend is expected to interpret the expression as needed. + The attribute is only honored by a debug adapter if the capability 'supportsHitConditionalBreakpoints' is true. + */ hitCondition?: string; } @@ -1769,7 +1890,9 @@ declare module DebugProtocol { accessType?: DataBreakpointAccessType; /** An optional expression for conditional breakpoints. */ condition?: string; - /** An optional expression that controls how many hits of the breakpoint are ignored. The backend is expected to interpret the expression as needed. */ + /** An optional expression that controls how many hits of the breakpoint are ignored. + The backend is expected to interpret the expression as needed. + */ hitCondition?: string; } @@ -1779,7 +1902,9 @@ declare module DebugProtocol { id?: number; /** If true breakpoint could be set (but not necessarily at the desired location). */ verified: boolean; - /** An optional message about the state of the breakpoint. This is shown to the user and can be used to explain why a breakpoint could not be verified. */ + /** An optional message about the state of the breakpoint. + This is shown to the user and can be used to explain why a breakpoint could not be verified. + */ message?: string; /** The source where the breakpoint is located. */ source?: Source; @@ -1789,7 +1914,9 @@ declare module DebugProtocol { column?: number; /** An optional end line of the actual range covered by the breakpoint. */ endLine?: number; - /** An optional end column of the actual range covered by the breakpoint. If no end line is given, then the end column is assumed to be in the start line. */ + /** An optional end column of the actual range covered by the breakpoint. + If no end line is given, then the end column is assumed to be in the start line. + */ endColumn?: number; } @@ -1891,7 +2018,9 @@ declare module DebugProtocol { /** An ExceptionOptions assigns configuration options to a set of exceptions. */ export interface ExceptionOptions { - /** A path that selects a single or multiple exceptions in a tree. If 'path' is missing, the whole tree is selected. By convention the first segment of the path is a category that is used to group exceptions in the UI. */ + /** A path that selects a single or multiple exceptions in a tree. If 'path' is missing, the whole tree is selected. + By convention the first segment of the path is a category that is used to group exceptions in the UI. + */ path?: ExceptionPathSegment[]; /** Condition when a thrown exception should result in a break. */ breakMode: ExceptionBreakMode; @@ -1905,7 +2034,10 @@ declare module DebugProtocol { */ export type ExceptionBreakMode = 'never' | 'always' | 'unhandled' | 'userUnhandled'; - /** An ExceptionPathSegment represents a segment in a path that is used to match leafs or nodes in a tree of exceptions. If a segment consists of more than one name, it matches the names provided if 'negate' is false or missing or it matches anything except the names provided if 'negate' is true. */ + /** An ExceptionPathSegment represents a segment in a path that is used to match leafs or nodes in a tree of exceptions. + If a segment consists of more than one name, it matches the names provided if 'negate' is false or missing or + it matches anything except the names provided if 'negate' is true. + */ export interface ExceptionPathSegment { /** If false or missing this segment matches the names provided, otherwise it matches anything except the names provided. */ negate?: boolean; @@ -1939,7 +2071,10 @@ declare module DebugProtocol { instruction: string; /** Name of the symbol that corresponds with the location of this instruction, if any. */ symbol?: string; - /** Source location that corresponds to this instruction, if any. Should always be set (if available) on the first instruction returned, but can be omitted afterwards if this instruction maps to the same source file as the previous instruction. */ + /** Source location that corresponds to this instruction, if any. + Should always be set (if available) on the first instruction returned, + but can be omitted afterwards if this instruction maps to the same source file as the previous instruction. + */ location?: Source; /** The line within the source location that corresponds to this instruction, if any. */ line?: number; From cb58105623ef7a6d738b4bd7a7147e6f59b650cf Mon Sep 17 00:00:00 2001 From: Rachel Macfarlane Date: Thu, 19 Mar 2020 08:41:57 -0700 Subject: [PATCH 33/44] Handle state double encoding in Microsoft auth provider --- extensions/vscode-account/src/AADHelper.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/vscode-account/src/AADHelper.ts b/extensions/vscode-account/src/AADHelper.ts index 16e7990be0b..ac588b76a6b 100644 --- a/extensions/vscode-account/src/AADHelper.ts +++ b/extensions/vscode-account/src/AADHelper.ts @@ -341,7 +341,8 @@ export class AzureActiveDirectoryService { const query = parseQuery(uri); const code = query.code; - if (query.state !== state) { + // Workaround double encoding issues of state in web + if (query.state !== state && decodeURIComponent(query.state) !== state) { throw new Error('State does not match.'); } From 9ac8206ecaa8fe304444a446387e55c67246d345 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 19 Mar 2020 16:43:05 +0100 Subject: [PATCH 34/44] quick access - more fixes for quick navigate --- src/vs/base/parts/quickinput/browser/quickInput.ts | 10 ++++++++++ src/vs/base/parts/quickinput/browser/quickInputList.ts | 4 ++++ 2 files changed, 14 insertions(+) diff --git a/src/vs/base/parts/quickinput/browser/quickInput.ts b/src/vs/base/parts/quickinput/browser/quickInput.ts index 8523c13172c..b047ef93c08 100644 --- a/src/vs/base/parts/quickinput/browser/quickInput.ts +++ b/src/vs/base/parts/quickinput/browser/quickInput.ts @@ -460,6 +460,10 @@ class QuickPick extends QuickInput implements IQuickPi set items(items: Array) { this._items = items; this.itemsUpdated = true; + if (this._items.length === 0) { + // quick-navigate requires at least 1 item + this._quickNavigate = undefined; + } this.update(); } @@ -840,12 +844,18 @@ class QuickPick extends QuickInput implements IQuickPi this.ui.list.sortByLabel = this.sortByLabel; if (this.itemsUpdated) { this.itemsUpdated = false; + const previousItemCount = this.ui.list.getElementsCount(); this.ui.list.setElements(this.items); this.ui.list.filter(this.filterValue(this.ui.inputBox.value)); this.ui.checkAll.checked = this.ui.list.getAllVisibleChecked(); this.ui.visibleCount.setCount(this.ui.list.getVisibleCount()); this.ui.count.setCount(this.ui.list.getCheckedCount()); this.trySelectFirst(); + if (this._quickNavigate && previousItemCount === 0 && this.items.length > 1) { + // quick navigate: automatically focus the second entry + // so that upon release the item is picked directly + this.ui.list.focus('Next'); + } } if (this.ui.container.classList.contains('show-checkboxes') !== !!this.canSelectMany) { if (this.canSelectMany) { diff --git a/src/vs/base/parts/quickinput/browser/quickInputList.ts b/src/vs/base/parts/quickinput/browser/quickInputList.ts index 9979a4fff39..6b674c3d59e 100644 --- a/src/vs/base/parts/quickinput/browser/quickInputList.ts +++ b/src/vs/base/parts/quickinput/browser/quickInputList.ts @@ -416,6 +416,10 @@ export class QuickInputList { this._onChangedVisibleCount.fire(this.elements.length); } + getElementsCount(): number { + return this.inputElements.length; + } + getFocusedElements() { return this.list.getFocusedElements() .map(e => e.item); From c7293f39c431f7e7ad5b6ed933fcef6f32340bbe Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Thu, 19 Mar 2020 16:45:04 +0100 Subject: [PATCH 35/44] add completion item kinds for Issue and User, https://github.com/microsoft/vscode/issues/91541 --- src/vs/editor/common/config/editorOptions.ts | 22 +++++++ src/vs/editor/common/modes.ts | 59 ++++++++++--------- .../editor/contrib/suggest/suggestWidget.ts | 3 +- src/vs/vscode.proposed.d.ts | 9 +++ .../api/common/extHostTypeConverters.ts | 4 ++ src/vs/workbench/api/common/extHostTypes.ts | 4 +- 6 files changed, 71 insertions(+), 30 deletions(-) diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index e6217a65275..231e586394e 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -2835,6 +2835,14 @@ export interface ISuggestOptions { * Show typeParameter-suggestions. */ showTypeParameters?: boolean; + /** + * Show issue-suggestions. + */ + showIssues?: boolean; + /** + * Show user-suggestions. + */ + showUsers?: boolean; /** * Show snippet-suggestions. */ @@ -2889,6 +2897,8 @@ class EditorSuggest extends BaseEditorOption= 0) { diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 4fe548e15c7..5514a139580 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -2038,4 +2038,13 @@ declare module 'vscode' { //#endregion + //#region https://github.com/microsoft/vscode/issues/91541 + + export enum CompletionItemKind { + User = 25, + Issue = 26, + } + + //#endregion + } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 68869ca7237..6ef467bd8a2 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -835,6 +835,8 @@ export namespace CompletionItemKind { case types.CompletionItemKind.Event: return modes.CompletionItemKind.Event; case types.CompletionItemKind.Operator: return modes.CompletionItemKind.Operator; case types.CompletionItemKind.TypeParameter: return modes.CompletionItemKind.TypeParameter; + case types.CompletionItemKind.Issue: return modes.CompletionItemKind.Issue; + case types.CompletionItemKind.User: return modes.CompletionItemKind.User; } return modes.CompletionItemKind.Property; } @@ -866,6 +868,8 @@ export namespace CompletionItemKind { case modes.CompletionItemKind.Event: return types.CompletionItemKind.Event; case modes.CompletionItemKind.Operator: return types.CompletionItemKind.Operator; case modes.CompletionItemKind.TypeParameter: return types.CompletionItemKind.TypeParameter; + case modes.CompletionItemKind.User: return types.CompletionItemKind.User; + case modes.CompletionItemKind.Issue: return types.CompletionItemKind.Issue; } return types.CompletionItemKind.Property; } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 27e443c0440..8f93ace6539 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -1348,7 +1348,9 @@ export enum CompletionItemKind { Struct = 21, Event = 22, Operator = 23, - TypeParameter = 24 + TypeParameter = 24, + User = 25, + Issue = 26 } export enum CompletionItemTag { From 32d52f3f47e0ae147c8945c3c7c527f33f7ce708 Mon Sep 17 00:00:00 2001 From: isidor Date: Thu, 19 Mar 2020 17:48:26 +0100 Subject: [PATCH 36/44] debug: report source of progress #92253 --- .../contrib/debug/browser/debugConfigurationManager.ts | 9 +++++++++ src/vs/workbench/contrib/debug/browser/debugProgress.ts | 6 ++++-- src/vs/workbench/contrib/debug/common/debug.ts | 1 + 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts index 319ccdb451e..796e8e286f0 100644 --- a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts @@ -162,6 +162,15 @@ export class ConfigurationManager implements IConfigurationManager { return Promise.resolve(undefined); } + getDebuggerLabel(session: IDebugSession): string | undefined { + const dbgr = this.getDebugger(session.configuration.type); + if (dbgr) { + return dbgr.label; + } + + return undefined; + } + get onDidRegisterDebugger(): Event { return this._onDidRegisterDebugger.event; } diff --git a/src/vs/workbench/contrib/debug/browser/debugProgress.ts b/src/vs/workbench/contrib/debug/browser/debugProgress.ts index b5f82277262..4885cb7946e 100644 --- a/src/vs/workbench/contrib/debug/browser/debugProgress.ts +++ b/src/vs/workbench/contrib/debug/browser/debugProgress.ts @@ -34,11 +34,13 @@ export class DebugProgressContribution implements IWorkbenchContribution { }); this.progressService.withProgress({ location: VIEWLET_ID }, () => promise); + const source = this.debugService.getConfigurationManager().getDebuggerLabel(session); this.progressService.withProgress({ location: ProgressLocation.Notification, title: progressStartEvent.body.title, cancellable: progressStartEvent.body.cancellable, - silent: true + silent: true, + source }, progressStep => { let increment = 0; const progressUpdateListener = session.onDidProgressUpdate(e => { @@ -49,7 +51,7 @@ export class DebugProgressContribution implements IWorkbenchContribution { progressStep.report({ message: e.body.message, increment: typeof e.body.percentage === 'number' ? increment : undefined, - total: typeof e.body.percentage === 'number' ? 100 : undefined + total: typeof e.body.percentage === 'number' ? 100 : undefined, }); } }); diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 70f22a3291e..3cef7e8d142 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -663,6 +663,7 @@ export interface IConfigurationManager { resolveConfigurationByProviders(folderUri: uri | undefined, type: string | undefined, debugConfiguration: any, token: CancellationToken): Promise; getDebugAdapterDescriptor(session: IDebugSession): Promise; + getDebuggerLabel(session: IDebugSession): string | undefined; registerDebugAdapterFactory(debugTypes: string[], debugAdapterFactory: IDebugAdapterFactory): IDisposable; createDebugAdapter(session: IDebugSession): IDebugAdapter | undefined; From 3cde29e3d938d239af973668212081188e852108 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 19 Mar 2020 19:00:29 +0100 Subject: [PATCH 37/44] Fix #count-badge-wrapper --- .../contrib/extensions/browser/media/extensionsViewlet.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css index f61faa096cb..f972d5d1a8a 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css @@ -28,6 +28,7 @@ height: calc(100% - 41px); } +.extensions-viewlet > .extensions .extension-view-header .count-badge-wrapper, .extensions-viewlet > .extensions .extension-view-header .monaco-action-bar { margin-right: 4px; } From f8083181c5335380f7523a939f16de659fd03353 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 19 Mar 2020 19:05:46 +0100 Subject: [PATCH 38/44] fix build --- src/vs/editor/common/standalone/standaloneEnums.ts | 4 +++- src/vs/monaco.d.ts | 12 +++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index 30200a230ea..784dd6f80d7 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -53,7 +53,9 @@ export enum CompletionItemKind { Customcolor = 22, Folder = 23, TypeParameter = 24, - Snippet = 25 + User = 25, + Issue = 26, + Snippet = 27 } export enum CompletionItemTag { diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index fea311268e3..fff64d28c4c 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -3767,6 +3767,14 @@ declare namespace monaco.editor { * Show typeParameter-suggestions. */ showTypeParameters?: boolean; + /** + * Show issue-suggestions. + */ + showIssues?: boolean; + /** + * Show user-suggestions. + */ + showUsers?: boolean; /** * Show snippet-suggestions. */ @@ -5389,7 +5397,9 @@ declare namespace monaco.languages { Customcolor = 22, Folder = 23, TypeParameter = 24, - Snippet = 25 + User = 25, + Issue = 26, + Snippet = 27 } export interface CompletionItemLabel { From 7e56fc02c0515d2268b8638f4a56a6db6081a951 Mon Sep 17 00:00:00 2001 From: rebornix Date: Thu, 19 Mar 2020 11:35:57 -0700 Subject: [PATCH 39/44] Contributions for notebook cell toolbar --- src/vs/platform/actions/common/actions.ts | 1 + .../workbench/api/common/extHost.api.impl.ts | 2 +- .../workbench/api/common/extHostNotebook.ts | 25 +++++++- .../api/common/menusExtensionPoint.ts | 6 ++ .../contrib/notebook/browser/constants.ts | 3 + .../notebook/browser/notebookEditor.ts | 6 ++ .../browser/view/renderers/cellMenus.ts | 38 +++++++++++ .../browser/view/renderers/cellRenderer.ts | 64 +++++++++++++++---- .../viewModel/notebookCellViewModel.ts | 6 ++ 9 files changed, 135 insertions(+), 16 deletions(-) create mode 100644 src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index f5a1a2e00bc..d99fbcc2ea3 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -114,6 +114,7 @@ export class MenuId { static readonly CommentThreadActions = new MenuId('CommentThreadActions'); static readonly CommentTitle = new MenuId('CommentTitle'); static readonly CommentActions = new MenuId('CommentActions'); + static readonly NotebookCellTitle = new MenuId('NotebookCellTitle'); static readonly BulkEditTitle = new MenuId('BulkEditTitle'); static readonly BulkEditContext = new MenuId('BulkEditContext'); static readonly TimelineItemContext = new MenuId('TimelineItemContext'); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index e7a3f40a091..f9c38f65db6 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -131,7 +131,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostWindow = rpcProtocol.set(ExtHostContext.ExtHostWindow, new ExtHostWindow(rpcProtocol)); const extHostProgress = rpcProtocol.set(ExtHostContext.ExtHostProgress, new ExtHostProgress(rpcProtocol.getProxy(MainContext.MainThreadProgress))); const extHostLabelService = rpcProtocol.set(ExtHostContext.ExtHosLabelService, new ExtHostLabelService(rpcProtocol)); - const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostDocumentsAndEditors)); + const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors)); const extHostTheming = rpcProtocol.set(ExtHostContext.ExtHostTheming, new ExtHostTheming(rpcProtocol)); const extHostAuthentication = rpcProtocol.set(ExtHostContext.ExtHostAuthentication, new ExtHostAuthentication(rpcProtocol)); const extHostTimeline = rpcProtocol.set(ExtHostContext.ExtHostTimeline, new ExtHostTimeline(rpcProtocol, extHostCommands)); diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 22df2ef1ddf..9c8dccfe5c7 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -14,6 +14,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { INotebookDisplayOrder, ITransformedDisplayOutputDto, IOrderedMimeType, IStreamOutput, IErrorOutput, mimeTypeSupportedByCore, IOutput, sortMimeTypes, diff, CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ISplice } from 'vs/base/common/sequence'; +import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; export class ExtHostCell implements vscode.NotebookCell { @@ -415,7 +416,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN private static _handlePool: number = 0; private readonly _proxy: MainThreadNotebookShape; - private readonly _notebookProviders = new Map(); + private readonly _notebookProviders = new Map(); private readonly _documents = new Map(); private readonly _editors = new Map(); private readonly _notebookOutputRenderers = new Map(); @@ -431,8 +432,28 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN return this._activeNotebookDocument; } - constructor(mainContext: IMainContext, private _documentsAndEditors: ExtHostDocumentsAndEditors) { + constructor(mainContext: IMainContext, commands: ExtHostCommands, private _documentsAndEditors: ExtHostDocumentsAndEditors) { this._proxy = mainContext.getProxy(MainContext.MainThreadNotebook); + + commands.registerArgumentProcessor({ + processArgument: arg => { + if (arg && arg.$mid === 12) { + const documentHandle = arg.notebookEditor?.notebookHandle; + const cellHandle = arg.cell.handle; + + for (let value of this._editors) { + if (value[1].document.handle === documentHandle) { + const cell = value[1].document.getCell(cellHandle); + if (cell) { + return cell; + } + } + } + + return arg; + } + } + }); } registerNotebookOutputRenderer( diff --git a/src/vs/workbench/api/common/menusExtensionPoint.ts b/src/vs/workbench/api/common/menusExtensionPoint.ts index ab474c9825f..1118fcbb725 100644 --- a/src/vs/workbench/api/common/menusExtensionPoint.ts +++ b/src/vs/workbench/api/common/menusExtensionPoint.ts @@ -51,6 +51,7 @@ namespace schema { case 'comments/commentThread/context': return MenuId.CommentThreadActions; case 'comments/comment/title': return MenuId.CommentTitle; case 'comments/comment/context': return MenuId.CommentActions; + case 'notebook/cell/title': return MenuId.NotebookCellTitle; case 'extension/context': return MenuId.ExtensionContext; case 'timeline/title': return MenuId.TimelineTitle; case 'timeline/item/context': return MenuId.TimelineItemContext; @@ -212,6 +213,11 @@ namespace schema { type: 'array', items: menuItem }, + 'notebook/cell/title': { + description: localize('notebook.cell.title', "The contributed notebook cell title menu"), + type: 'array', + items: menuItem + }, 'extension/context': { description: localize('menus.extensionContext', "The extension context menu"), type: 'array', diff --git a/src/vs/workbench/contrib/notebook/browser/constants.ts b/src/vs/workbench/contrib/notebook/browser/constants.ts index dcf51f7ccca..0b4e662a5a2 100644 --- a/src/vs/workbench/contrib/notebook/browser/constants.ts +++ b/src/vs/workbench/contrib/notebook/browser/constants.ts @@ -24,3 +24,6 @@ export const CELL_MARGIN = 32; export const EDITOR_TOP_PADDING = 8; export const EDITOR_BOTTOM_PADDING = 8; export const EDITOR_TOOLBAR_HEIGHT = 22; + +// Context Keys +export const NOTEBOOK_CELL_TYPE_CONTEXT_KEY = 'notebookCellType'; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index bfdd63c5373..756623f6d97 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -698,6 +698,12 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { } //#endregion + + toJSON(): any { + return { + notebookHandle: this.viewModel?.handle + }; + } } const embeddedEditorBackground = 'walkThrough.embeddedEditorBackground'; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts new file mode 100644 index 00000000000..9ef1bb22fa3 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellMenus.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions'; +import { IAction } from 'vs/base/common/actions'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; + +export class CellMenus implements IDisposable { + constructor( + @IMenuService private readonly menuService: IMenuService, + @IContextMenuService private readonly contextMenuService: IContextMenuService + ) { } + + getCellTitleActions(contextKeyService: IContextKeyService): IMenu { + return this.getMenu(MenuId.NotebookCellTitle, contextKeyService); + } + + private getMenu(menuId: MenuId, contextKeyService: IContextKeyService): IMenu { + const menu = this.menuService.createMenu(menuId, contextKeyService); + + const primary: IAction[] = []; + const secondary: IAction[] = []; + const result = { primary, secondary }; + + createAndFillInContextMenuActions(menu, { shouldForwardArgs: true }, result, this.contextMenuService, g => /^inline/.test(g)); + + return menu; + } + + dispose(): void { + + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index 64761922fe6..c1250ada0b3 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -28,7 +28,9 @@ import { ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser import { MenuItemAction } from 'vs/platform/actions/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_PADDING, EDITOR_BOTTOM_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; +import { EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_PADDING, EDITOR_BOTTOM_PADDING, NOTEBOOK_CELL_TYPE_CONTEXT_KEY } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CellMenus } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellMenus'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; export class NotebookCellListDelegate implements IListVirtualDelegate { private _lineHeight: number; @@ -68,6 +70,7 @@ abstract class AbstractCellRenderer { private readonly configurationService: IConfigurationService, private readonly keybindingService: IKeybindingService, private readonly notificationService: INotificationService, + protected readonly contextKeyService: IContextKeyService, language: string, ) { const editorOptions = deepClone(this.configurationService.getValue('editor', { overrideIdentifier: language })); @@ -108,6 +111,11 @@ abstract class AbstractCellRenderer { return toolbar; } + protected createMenu(): CellMenus { + const menu = this.instantiationService.createInstance(CellMenus); + return menu; + } + showContextMenu(listIndex: number | undefined, element: CellViewModel, x: number, y: number) { const actions: IAction[] = [ this.instantiationService.createInstance(InsertCodeCellAboveAction), @@ -150,8 +158,9 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR @IContextMenuService contextMenuService: IContextMenuService, @IKeybindingService keybindingService: IKeybindingService, @INotificationService notificationService: INotificationService, + @IContextKeyService contextKeyService: IContextKeyService ) { - super(instantiationService, notehookEditor, contextMenuService, configurationService, keybindingService, notificationService, 'markdown'); + super(instantiationService, notehookEditor, contextMenuService, configurationService, keybindingService, notificationService, contextKeyService, 'markdown'); } get templateId() { @@ -165,14 +174,6 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR const disposables = new DisposableStore(); const toolbar = this.createToolbar(container); - toolbar.setActions([ - this.instantiationService.createInstance(MoveCellUpAction), - this.instantiationService.createInstance(MoveCellDownAction), - this.instantiationService.createInstance(InsertCodeCellBelowAction), - this.instantiationService.createInstance(EditCellAction), - this.instantiationService.createInstance(SaveCellAction), - this.instantiationService.createInstance(DeleteCellAction) - ])(); disposables.add(toolbar); container.appendChild(codeInnerContent); @@ -230,11 +231,30 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR })); elementDisposable!.add(new StatefullMarkdownCell(this.notebookEditor, element, templateData, this.editorOptions, this.instantiationService)); + + const contextKeyService = this.contextKeyService.createScoped(templateData.container); + contextKeyService.createKey(NOTEBOOK_CELL_TYPE_CONTEXT_KEY, 'markdown'); + const menu = this.createMenu().getCellTitleActions(this.contextKeyService); + const actions: IAction[] = []; + for (let [, actions] of menu.getActions({ shouldForwardArgs: true })) { + actions.push(...actions); + } + + templateData.toolbar!.setActions([ + ...actions, + this.instantiationService.createInstance(MoveCellUpAction), + this.instantiationService.createInstance(MoveCellDownAction), + this.instantiationService.createInstance(InsertCodeCellBelowAction), + this.instantiationService.createInstance(EditCellAction), + this.instantiationService.createInstance(SaveCellAction), + this.instantiationService.createInstance(DeleteCellAction) + ])(); } templateData.toolbar!.context = { cell: element, - notebookEditor: this.notebookEditor + notebookEditor: this.notebookEditor, + $mid: 12 }; } @@ -269,8 +289,9 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende @IInstantiationService instantiationService: IInstantiationService, @IKeybindingService keybindingService: IKeybindingService, @INotificationService notificationService: INotificationService, + @IContextKeyService contextKeyService: IContextKeyService ) { - super(instantiationService, notebookEditor, contextMenuService, configurationService, keybindingService, notificationService, 'python'); + super(instantiationService, notebookEditor, contextMenuService, configurationService, keybindingService, notificationService, contextKeyService, 'python'); } get templateId() { @@ -364,9 +385,26 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende templateData.focusIndicator!.style.height = `${element.getIndicatorHeight()}px`; })); + const contextKeyService = this.contextKeyService.createScoped(templateData.container); + contextKeyService.createKey(NOTEBOOK_CELL_TYPE_CONTEXT_KEY, 'code'); + const menu = this.createMenu().getCellTitleActions(contextKeyService); + const actions: IAction[] = []; + for (let [, items] of menu.getActions({ shouldForwardArgs: true })) { + actions.push(...items); + } + + templateData.toolbar!.setActions([ + ...actions, + this.instantiationService.createInstance(MoveCellUpAction), + this.instantiationService.createInstance(MoveCellDownAction), + this.instantiationService.createInstance(InsertCodeCellBelowAction), + this.instantiationService.createInstance(DeleteCellAction) + ])(); + templateData.toolbar!.context = { cell: element, - notebookEditor: this.notebookEditor + notebookEditor: this.notebookEditor, + $mid: 12 }; } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel.ts index d62cfd87051..318ff94c41d 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel.ts @@ -519,4 +519,10 @@ export class CellViewModel extends Disposable implements ICellViewModel { this._outputsTop = new PrefixSumComputer(values); } } + + toJSON(): any { + return { + handle: this.handle + }; + } } From a47badb3d682b079e59e3f852f5bffdd29377915 Mon Sep 17 00:00:00 2001 From: isidor Date: Thu, 19 Mar 2020 20:21:42 +0100 Subject: [PATCH 40/44] debug progress: use 500ms delay --- src/vs/workbench/contrib/debug/browser/debugProgress.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/debug/browser/debugProgress.ts b/src/vs/workbench/contrib/debug/browser/debugProgress.ts index 4885cb7946e..45a03b3dd32 100644 --- a/src/vs/workbench/contrib/debug/browser/debugProgress.ts +++ b/src/vs/workbench/contrib/debug/browser/debugProgress.ts @@ -40,7 +40,8 @@ export class DebugProgressContribution implements IWorkbenchContribution { title: progressStartEvent.body.title, cancellable: progressStartEvent.body.cancellable, silent: true, - source + source, + delay: 500 }, progressStep => { let increment = 0; const progressUpdateListener = session.onDidProgressUpdate(e => { From 56660bef3621efaa54ebbe47506dd29454b5e715 Mon Sep 17 00:00:00 2001 From: rebornix Date: Thu, 19 Mar 2020 12:48:12 -0700 Subject: [PATCH 41/44] messages channel between ext and webview for outputs --- src/vs/vscode.proposed.d.ts | 13 +++++ .../api/browser/mainThreadNotebook.ts | 25 +++++++++- .../workbench/api/common/extHost.protocol.ts | 2 + .../workbench/api/common/extHostNotebook.ts | 33 ++++++++++--- .../notebook/browser/notebookBrowser.ts | 7 +++ .../notebook/browser/notebookEditor.ts | 9 ++++ .../notebook/browser/notebookService.ts | 10 ++++ .../view/renderers/backLayerWebView.ts | 49 ++++++++++++------- .../notebook/test/testNotebookEditor.ts | 6 +++ 9 files changed, 129 insertions(+), 25 deletions(-) diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 5514a139580..557293f8720 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1656,6 +1656,19 @@ declare module 'vscode' { export interface NotebookEditor { readonly document: NotebookDocument; viewColumn?: ViewColumn; + /** + * Fired when the output hosting webview posts a message. + */ + readonly onDidReceiveMessage: Event; + /** + * Post a message to the output hosting webview. + * + * Messages are only delivered if the editor is live. + * + * @param message Body of the message. This must be a string or other json serilizable object. + */ + postMessage(message: any): Thenable; + /** * Create a notebook cell. The cell is not inserted into current document when created. Extensions should insert the cell into the document by [TextDocument.cells](#TextDocument.cells) */ diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index 9330413e36a..52b7fcf1301 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -12,6 +12,8 @@ import { INotebookTextModel, INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; export class MainThreadNotebookDocument extends Disposable { private _textModel: NotebookTextModel; @@ -54,7 +56,9 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo constructor( extHostContext: IExtHostContext, @INotebookService private _notebookService: INotebookService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IEditorService private readonly editorService: IEditorService, + ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebook); @@ -141,6 +145,21 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo async executeNotebook(viewType: string, uri: URI): Promise { return this._proxy.$executeNotebook(viewType, uri, undefined); } + + async $postMessage(handle: number, value: any): Promise { + + const activeEditorPane = this.editorService.activeEditorPane as any | undefined; + if (activeEditorPane?.isNotebookEditor) { + const notebookEditor = (activeEditorPane as INotebookEditor); + + if (notebookEditor.viewModel?.handle === handle) { + notebookEditor.postMessage(value); + return true; + } + } + + return false; + } } export class MainThreadNotebookController implements IMainNotebookController { @@ -186,6 +205,10 @@ export class MainThreadNotebookController implements IMainNotebookController { this._mainThreadNotebook.executeNotebook(viewType, uri); } + onDidReceiveMessage(uri: UriComponents, message: any): void { + this._proxy.$onDidReceiveMessage(uri, message); + } + // Methods for ExtHost async createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise { let document = new MainThreadNotebookDocument(this._proxy, handle, viewType, URI.revive(resource)); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 7bf429a3d82..a5b76f17618 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -688,6 +688,7 @@ export interface MainThreadNotebookShape extends IDisposable { $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise; $spliceNotebookCells(viewType: string, resource: UriComponents, splices: NotebookCellsSplice[], renderers: number[]): Promise; $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise; + $postMessage(handle: number, value: any): Promise; } export interface MainThreadUrlsShape extends IDisposable { @@ -1527,6 +1528,7 @@ export interface ExtHostNotebookShape { $updateActiveEditor(viewType: string, uri: UriComponents): Promise; $destoryNotebookDocument(viewType: string, uri: UriComponents): Promise; $acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void; + $onDidReceiveMessage(uri: UriComponents, message: any): void; } export interface ExtHostStorageShape { diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 9c8dccfe5c7..4b3234a5fae 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -331,11 +331,14 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo export class ExtHostNotebookEditor extends Disposable implements vscode.NotebookEditor { private _viewColumn: vscode.ViewColumn | undefined; private static _cellhandlePool: number = 0; + onDidReceiveMessage: vscode.Event = this._onDidReceiveMessage.event; constructor( viewType: string, readonly id: string, public uri: URI, + private _proxy: MainThreadNotebookShape, + private _onDidReceiveMessage: Emitter, public document: ExtHostNotebookDocument, private _documentsAndEditors: ExtHostDocumentsAndEditors ) { @@ -377,6 +380,11 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook set viewColumn(value) { throw readonly('viewColumn'); } + + async postMessage(message: any): Promise { + return this._proxy.$postMessage(this.document.handle, message); + } + } export class ExtHostNotebookOutputRenderer { @@ -418,7 +426,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN private readonly _proxy: MainThreadNotebookShape; private readonly _notebookProviders = new Map(); private readonly _documents = new Map(); - private readonly _editors = new Map(); + private readonly _editors = new Map }>(); private readonly _notebookOutputRenderers = new Map(); private _outputDisplayOrder: INotebookDisplayOrder | undefined; @@ -442,8 +450,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN const cellHandle = arg.cell.handle; for (let value of this._editors) { - if (value[1].document.handle === documentHandle) { - const cell = value[1].document.getCell(cellHandle); + if (value[1].editor.document.handle === documentHandle) { + const cell = value[1].editor.document.getCell(cellHandle); if (cell) { return cell; } @@ -515,15 +523,19 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN this._documents.set(URI.revive(uri).toString(), document); } + const onDidReceiveMessage = new Emitter(); + let editor = new ExtHostNotebookEditor( viewType, `${ExtHostNotebookController._handlePool++}`, URI.revive(uri), + this._proxy, + onDidReceiveMessage, this._documents.get(URI.revive(uri).toString())!, this._documentsAndEditors ); - this._editors.set(URI.revive(uri).toString(), editor); + this._editors.set(URI.revive(uri).toString(), { editor, onDidReceiveMessage }); await provider.provider.resolveNotebook(editor); // await editor.document.$updateCells(); return editor.document.handle; @@ -556,7 +568,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN let editor = this._editors.get(URI.revive(uri).toString()); let document = this._documents.get(URI.revive(uri).toString()); - let rawCell = editor?.createCell('', language, type, []) as ExtHostCell; + let rawCell = editor?.editor.createCell('', language, type, []) as ExtHostCell; document?.insertCell(index, rawCell!); let allDocuments = this._documentsAndEditors.allDocuments(); @@ -629,7 +641,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN let editor = this._editors.get(URI.revive(uri).toString()); if (editor) { - editor.dispose(); + editor.editor.dispose(); + editor.onDidReceiveMessage.dispose(); this._editors.delete(URI.revive(uri).toString()); } @@ -639,4 +652,12 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN $acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void { this._outputDisplayOrder = displayOrder; } + + $onDidReceiveMessage(uri: UriComponents, message: any): void { + let editor = this._editors.get(URI.revive(uri).toString()); + + if (editor) { + editor.onDidReceiveMessage.fire(message); + } + } } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index c3d724e887a..d5dcbd267f8 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -44,6 +44,8 @@ export interface INotebookEditor { */ viewModel: NotebookViewModel | undefined; + isNotebookEditor: boolean; + /** * Focus the notebook editor cell list */ @@ -121,6 +123,11 @@ export interface INotebookEditor { */ removeInset(output: IOutput): void; + /** + * Send message to the webview for outputs. + */ + postMessage(message: any): void; + /** * Trigger the editor to scroll from scroll event programmatically */ diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 756623f6d97..d9d7e88998b 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -219,6 +219,11 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { this.control = new NotebookCodeEditors(this.list, this.renderedEditors); this.webview = new BackLayerWebView(this.webviewService, this.notebookService, this, this.environmentSerice); + this._register(this.webview.onMessage(message => { + if (this.viewModel) { + this.notebookService.onDidReceiveMessage(this.viewModel.viewType, this.viewModel.uri, message); + } + })); this.list.rowsContainer.appendChild(this.webview.element); this._register(this.list); } @@ -697,6 +702,10 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { return this.outputRenderer; } + postMessage(message: any) { + this.webview?.webview.sendMessage(message); + } + //#endregion toJSON(): any { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookService.ts b/src/vs/workbench/contrib/notebook/browser/notebookService.ts index 2746196d2c6..ce6a5306496 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookService.ts @@ -30,6 +30,7 @@ export interface IMainNotebookController { createRawCell(uri: URI, index: number, language: string, type: CellKind): Promise; deleteCell(uri: URI, index: number): Promise executeNotebookActiveCell(uri: URI): void; + onDidReceiveMessage(uri: URI, message: any): void; destoryNotebookDocument(notebook: INotebookTextModel): Promise; save(uri: URI): Promise; } @@ -54,6 +55,7 @@ export interface INotebookService { destoryNotebookDocument(viewType: string, notebook: INotebookTextModel): void; updateActiveNotebookDocument(viewType: string, resource: URI): void; save(viewType: string, resource: URI): Promise; + onDidReceiveMessage(viewType: string, uri: URI, message: any): void; } export class NotebookProviderInfoStore { @@ -325,6 +327,14 @@ export class NotebookService extends Disposable implements INotebookService { return false; } + onDidReceiveMessage(viewType: string, uri: URI, message: any): void { + let provider = this._notebookProviders.get(viewType); + + if (provider) { + return provider.controller.onDidReceiveMessage(uri, message); + } + } + private _onWillDispose(model: INotebookTextModel): void { let modelId = MODEL_ID(model.uri); let modelData = this._models[modelId]; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 5ae70e8e485..0ffe2f407dd 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -16,8 +16,10 @@ import { IWebviewService, WebviewElement } from 'vs/workbench/contrib/webview/br import { WebviewResourceScheme } from 'vs/workbench/contrib/webview/common/resourceLoader'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel'; import { CELL_MARGIN } from 'vs/workbench/contrib/notebook/browser/constants'; +import { Emitter, Event } from 'vs/base/common/event'; export interface IDimentionMessage { + __vscode_notebook_message: boolean; type: 'dimension'; id: string; data: DOM.Dimension; @@ -25,6 +27,7 @@ export interface IDimentionMessage { export interface IScrollAckMessage { + __vscode_notebook_message: boolean; type: 'scroll-ack'; data: { top: number }; version: number; @@ -78,6 +81,9 @@ export class BackLayerWebView extends Disposable { preloadsCache: Map = new Map(); localResourceRootsCache: URI[] | undefined = undefined; rendererRootsCache: URI[] = []; + private readonly _onMessage = this._register(new Emitter()); + public readonly onMessage: Event = this._onMessage.event; + constructor(public webviewService: IWebviewService, public notebookService: INotebookService, public notebookEditor: INotebookEditor, public environmentSerice: IEnvironmentService) { super(); @@ -154,6 +160,7 @@ export class BackLayerWebView extends Disposable { for (let entry of entries) { if (entry.target.id === id && entry.contentRect) { vscode.postMessage({ + __vscode_notebook_message: true, type: 'dimension', id: id, data: { @@ -198,6 +205,7 @@ export class BackLayerWebView extends Disposable { resizeObserve(outputNode, outputId); vscode.postMessage({ + __vscode_notebook_message: true, type: 'dimension', id: outputId, data: { @@ -255,27 +263,32 @@ export class BackLayerWebView extends Disposable { })); this._register(this.webview.onMessage((data: IMessage) => { - if (data.type === 'dimension') { - let output = this.reversedInsetMapping.get(data.id); + if (data.__vscode_notebook_message) { + if (data.type === 'dimension') { + let output = this.reversedInsetMapping.get(data.id); - if (!output) { - return; + if (!output) { + return; + } + + let cell = this.insetMapping.get(output)!.cell; + let height = data.data.height; + let outputHeight = height === 0 ? 0 : height + 16; + + if (cell) { + let outputIndex = cell.outputs.indexOf(output); + cell.updateOutputHeight(outputIndex, outputHeight); + this.notebookEditor.layoutNotebookCell(cell, cell.getCellTotalHeight()); + } + } else if (data.type === 'scroll-ack') { + // const date = new Date(); + // const top = data.data.top; + // console.log('ack top ', top, ' version: ', data.version, ' - ', date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds()); } - - let cell = this.insetMapping.get(output)!.cell; - let height = data.data.height; - let outputHeight = height === 0 ? 0 : height + 16; - - if (cell) { - let outputIndex = cell.outputs.indexOf(output); - cell.updateOutputHeight(outputIndex, outputHeight); - this.notebookEditor.layoutNotebookCell(cell, cell.getCellTotalHeight()); - } - } else if (data.type === 'scroll-ack') { - // const date = new Date(); - // const top = data.data.top; - // console.log('ack top ', top, ' version: ', data.version, ' - ', date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds()); + return; } + + this._onMessage.fire(data); })); } diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts index 889e61c1f8d..86198b35b2c 100644 --- a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -69,6 +69,12 @@ export class TestNotebookEditor implements INotebookEditor { constructor( ) { } + isNotebookEditor = true; + + postMessage(message: any): void { + throw new Error('Method not implemented.'); + } + setCellSelection(cell: CellViewModel, selection: Range): void { throw new Error('Method not implemented.'); } From ae4a7512375a132f79d5020f70e3adb797186fde Mon Sep 17 00:00:00 2001 From: rebornix Date: Thu, 19 Mar 2020 13:44:53 -0700 Subject: [PATCH 42/44] Allow multiple api acquire --- .../notebook/browser/view/renderers/backLayerWebView.ts | 2 ++ src/vs/workbench/contrib/webview/browser/pre/main.js | 6 +++--- src/vs/workbench/contrib/webview/browser/webview.ts | 1 + .../contrib/webview/browser/webviewWorkbenchService.ts | 1 + 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 0ffe2f407dd..ee45296a157 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -297,6 +297,7 @@ export class BackLayerWebView extends Disposable { const webview = webviewService.createWebviewElement('' + UUID.generateUuid(), { enableFindWidget: false, }, { + allowMultipleAPIAcquire: true, allowScripts: true, localResourceRoots: this.localResourceRootsCache }); @@ -409,6 +410,7 @@ export class BackLayerWebView extends Disposable { const mixedResourceRoots = [...(this.localResourceRootsCache || []), ...this.rendererRootsCache]; this.webview.contentOptions = { + allowMultipleAPIAcquire: true, allowScripts: true, enableCommandUris: true, localResourceRoots: mixedResourceRoots diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js index 3fed6a33da9..dc853a7f950 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/main.js +++ b/src/vs/workbench/contrib/webview/browser/pre/main.js @@ -128,7 +128,7 @@ * @param {*} [state] * @return {string} */ - function getVsCodeApiScript(state) { + function getVsCodeApiScript(allowMultipleAPIAcquire, state) { return ` const acquireVsCodeApi = (function() { const originalPostMessage = window.parent.postMessage.bind(window.parent); @@ -138,7 +138,7 @@ let state = ${state ? `JSON.parse(${JSON.stringify(state)})` : undefined}; return () => { - if (acquired) { + if (acquired && !${allowMultipleAPIAcquire}) { throw new Error('An instance of the VS Code API has already been acquired'); } acquired = true; @@ -325,7 +325,7 @@ if (options.allowScripts) { const defaultScript = newDocument.createElement('script'); defaultScript.id = '_vscodeApiScript'; - defaultScript.textContent = getVsCodeApiScript(data.state); + defaultScript.textContent = getVsCodeApiScript(options.allowMultipleAPIAcquire, data.state); newDocument.head.prepend(defaultScript); } diff --git a/src/vs/workbench/contrib/webview/browser/webview.ts b/src/vs/workbench/contrib/webview/browser/webview.ts index 6502720cd90..dccddca8426 100644 --- a/src/vs/workbench/contrib/webview/browser/webview.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.ts @@ -59,6 +59,7 @@ export interface WebviewOptions { } export interface WebviewContentOptions { + readonly allowMultipleAPIAcquire?: boolean; readonly allowScripts?: boolean; readonly localResourceRoots?: ReadonlyArray; readonly portMapping?: ReadonlyArray; diff --git a/src/vs/workbench/contrib/webview/browser/webviewWorkbenchService.ts b/src/vs/workbench/contrib/webview/browser/webviewWorkbenchService.ts index e1c27ce0262..c9c4d1c7cfb 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewWorkbenchService.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewWorkbenchService.ts @@ -36,6 +36,7 @@ export function areWebviewInputOptionsEqual(a: WebviewInputOptions, b: WebviewIn return a.enableCommandUris === b.enableCommandUris && a.enableFindWidget === b.enableFindWidget && a.allowScripts === b.allowScripts + && a.allowMultipleAPIAcquire === b.allowMultipleAPIAcquire && a.retainContextWhenHidden === b.retainContextWhenHidden && a.tryRestoreScrollPosition === b.tryRestoreScrollPosition && equals(a.localResourceRoots, b.localResourceRoots, isEqual) From f0d8464e90ce6fe7d2fd5210322c90ced6663a9f Mon Sep 17 00:00:00 2001 From: rebornix Date: Thu, 19 Mar 2020 16:03:42 -0700 Subject: [PATCH 43/44] revert scrollable element overflow. --- src/vs/workbench/contrib/notebook/browser/notebook.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.css b/src/vs/workbench/contrib/notebook/browser/notebook.css index 808fee55939..0f37e0607c6 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/notebook.css @@ -17,9 +17,9 @@ white-space: initial; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-list-container .monaco-scrollable-element { +/* .monaco-workbench .part.editor > .content .notebook-editor .cell-list-container .monaco-scrollable-element { overflow: visible !important; -} +} */ .monaco-workbench .part.editor > .content .notebook-editor .cell-list-container .monaco-list-rows { min-height: 100%; From b75371ce023b2b7deaf6ab956064008e730cc09e Mon Sep 17 00:00:00 2001 From: rebornix Date: Thu, 19 Mar 2020 16:24:40 -0700 Subject: [PATCH 44/44] update jsdoc --- src/vs/workbench/contrib/webview/browser/pre/main.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js index dc853a7f950..b378daa5a06 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/main.js +++ b/src/vs/workbench/contrib/webview/browser/pre/main.js @@ -125,6 +125,7 @@ }`; /** + * @param {boolean} allowMultipleAPIAcquire * @param {*} [state] * @return {string} */