diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index cf8b2a58caa..4377e0dcde9 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -1860,6 +1860,10 @@ export abstract class AbstractTree implements IDisposable return this.model.isCollapsed(location); } + expandTo(location: TRef): void { + this.model.expandTo(location); + } + triggerTypeNavigation(): void { this.view.triggerTypeNavigation(); } diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 5ba79157f2d..2ccd22354d3 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -559,6 +559,10 @@ export class AsyncDataTree implements IDisposable this.tree.resort(this.getDataNode(element), recursive); } + hasElement(element: TInput | T): boolean { + return this.tree.hasElement(this.getDataNode(element)); + } + hasNode(element: TInput | T): boolean { return element === this.root.element || this.nodes.has(element as T); } @@ -575,6 +579,11 @@ export class AsyncDataTree implements IDisposable this.tree.rerender(node); } + updateElementHeight(element: T, height: number | undefined): void { + const node = this.getDataNode(element); + this.tree.updateElementHeight(node, height); + } + updateWidth(element: T): void { const node = this.getDataNode(element); this.tree.updateWidth(node); @@ -636,6 +645,28 @@ export class AsyncDataTree implements IDisposable this.tree.expandAll(); } + async expandTo(element: T): Promise { + if (!this.dataSource.getParent) { + throw new Error('Can\'t expand to element without getParent method'); + } + + const elements: T[] = []; + + while (!this.hasNode(element)) { + element = this.dataSource.getParent(element) as T; + + if (element !== this.root.element) { + elements.push(element); + } + } + + for (const element of Iterable.reverse(elements)) { + await this.expand(element); + } + + this.tree.expandTo(this.getDataNode(element)); + } + collapseAll(): void { this.tree.collapseAll(); } diff --git a/src/vs/base/browser/ui/tree/tree.ts b/src/vs/base/browser/ui/tree/tree.ts index 262badedf9f..e929dc7de09 100644 --- a/src/vs/base/browser/ui/tree/tree.ts +++ b/src/vs/base/browser/ui/tree/tree.ts @@ -196,6 +196,7 @@ export interface IDataSource { export interface IAsyncDataSource { hasChildren(element: TInput | T): boolean; getChildren(element: TInput | T): Iterable | Promise>; + getParent?(element: T): TInput | T; } export const enum TreeDragOverBubble { diff --git a/src/vs/base/common/lifecycle.ts b/src/vs/base/common/lifecycle.ts index 4b621529946..fedbb85a6cf 100644 --- a/src/vs/base/common/lifecycle.ts +++ b/src/vs/base/common/lifecycle.ts @@ -748,6 +748,10 @@ export class DisposableMap implements ID this._store.delete(key); } + keys(): IterableIterator { + return this._store.keys(); + } + [Symbol.iterator](): IterableIterator<[K, V]> { return this._store[Symbol.iterator](); } diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index a9bbdd6b19e..1489bb842ba 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -1800,7 +1800,7 @@ export interface Comment { export interface PendingCommentThread { body: string; - range: IRange; + range: IRange | undefined; uri: URI; owner: string; isReply: boolean; diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 5aafcb30cb7..26ac1e78e03 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -7650,7 +7650,7 @@ declare namespace monaco.languages { export interface PendingCommentThread { body: string; - range: IRange; + range: IRange | undefined; uri: Uri; owner: string; isReply: boolean; diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 57dbf7b41f4..eacf95b3999 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -10,13 +10,14 @@ import { ISCMService, ISCMRepository, ISCMProvider, ISCMResource, ISCMResourceGr import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemDto, SCMActionButtonDto, SCMHistoryItemGroupDto, SCMInputActionButtonDto } from '../common/extHost.protocol'; import { Command } from 'vs/editor/common/languages'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { ISplice, Sequence } from 'vs/base/common/sequence'; import { CancellationToken } from 'vs/base/common/cancellation'; import { MarshalledId } from 'vs/base/common/marshallingIds'; import { ThemeIcon } from 'vs/base/common/themables'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IQuickDiffService, QuickDiffProvider } from 'vs/workbench/contrib/scm/common/quickDiff'; import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGroup, ISCMHistoryOptions, ISCMHistoryProvider } from 'vs/workbench/contrib/scm/common/history'; +import { ResourceTree } from 'vs/base/common/resourceTree'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; function getSCMInputBoxActionButtonIcon(actionButton: SCMInputActionButtonDto): URI | { light: URI; dark: URI } | ThemeIcon | undefined { if (!actionButton.icon) { @@ -46,23 +47,37 @@ function getSCMHistoryItemIcon(historyItem: SCMHistoryItemDto): URI | { light: U class MainThreadSCMResourceGroup implements ISCMResourceGroup { - readonly elements: ISCMResource[] = []; + readonly resources: ISCMResource[] = []; - private readonly _onDidSplice = new Emitter>(); - readonly onDidSplice = this._onDidSplice.event; + private _resourceTree: ResourceTree | undefined; + get resourceTree(): ResourceTree { + if (!this._resourceTree) { + const rootUri = this.provider.rootUri ?? URI.file('/'); + this._resourceTree = new ResourceTree(this, rootUri, this._uriIdentService.extUri); + for (const resource of this.resources) { + this._resourceTree.add(resource.sourceUri, resource); + } + } - get hideWhenEmpty(): boolean { return !!this.features.hideWhenEmpty; } + return this._resourceTree; + } private readonly _onDidChange = new Emitter(); readonly onDidChange: Event = this._onDidChange.event; + private readonly _onDidChangeResources = new Emitter(); + readonly onDidChangeResources = this._onDidChangeResources.event; + + get hideWhenEmpty(): boolean { return !!this.features.hideWhenEmpty; } + constructor( private readonly sourceControlHandle: number, private readonly handle: number, public provider: ISCMProvider, public features: SCMGroupFeatures, public label: string, - public id: string + public id: string, + private readonly _uriIdentService: IUriIdentityService ) { } toJSON(): any { @@ -74,8 +89,10 @@ class MainThreadSCMResourceGroup implements ISCMResourceGroup { } splice(start: number, deleteCount: number, toInsert: ISCMResource[]) { - this.elements.splice(start, deleteCount, ...toInsert); - this._onDidSplice.fire({ start, deleteCount, toInsert }); + this.resources.splice(start, deleteCount, ...toInsert); + this._resourceTree = undefined; + + this._onDidChangeResources.fire(); } $updateGroup(features: SCMGroupFeatures): void { @@ -172,7 +189,13 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { private _id = `scm${MainThreadSCMProvider.ID_HANDLE++}`; get id(): string { return this._id; } - readonly groups = new Sequence(); + readonly groups: MainThreadSCMResourceGroup[] = []; + private readonly _onDidChangeResourceGroups = new Emitter(); + readonly onDidChangeResourceGroups = this._onDidChangeResourceGroups.event; + + private readonly _onDidChangeResources = new Emitter(); + readonly onDidChangeResources = this._onDidChangeResources.event; + private readonly _groupsByHandle: { [handle: number]: MainThreadSCMResourceGroup } = Object.create(null); // get groups(): ISequence { @@ -185,8 +208,6 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { // // .filter(g => g.resources.elements.length > 0 || !g.features.hideWhenEmpty); // } - private readonly _onDidChangeResources = new Emitter(); - readonly onDidChangeResources: Event = this._onDidChangeResources.event; private features: SCMProviderFeatures = {}; @@ -227,7 +248,8 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { private readonly _label: string, private readonly _rootUri: URI | undefined, private readonly _inputBoxDocumentUri: URI, - private readonly _quickDiffService: IQuickDiffService + private readonly _quickDiffService: IQuickDiffService, + private readonly _uriIdentService: IUriIdentityService ) { } $updateSourceControl(features: SCMProviderFeatures): void { @@ -271,14 +293,16 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { this, features, label, - id + id, + this._uriIdentService ); this._groupsByHandle[handle] = group; return group; }); - this.groups.splice(this.groups.elements.length, 0, groups); + this.groups.splice(this.groups.length, 0, ...groups); + this._onDidChangeResourceGroups.fire(); } $updateGroup(handle: number, features: SCMGroupFeatures): void { @@ -357,8 +381,8 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { } delete this._groupsByHandle[handle]; - this.groups.splice(this.groups.elements.indexOf(group), 1); - this._onDidChangeResources.fire(); + this.groups.splice(this.groups.indexOf(group), 1); + this._onDidChangeResourceGroups.fire(); } async getOriginalResource(uri: URI): Promise { @@ -410,7 +434,8 @@ export class MainThreadSCM implements MainThreadSCMShape { extHostContext: IExtHostContext, @ISCMService private readonly scmService: ISCMService, @ISCMViewService private readonly scmViewService: ISCMViewService, - @IQuickDiffService private readonly quickDiffService: IQuickDiffService + @IQuickDiffService private readonly quickDiffService: IQuickDiffService, + @IUriIdentityService private readonly _uriIdentService: IUriIdentityService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostSCM); } @@ -426,7 +451,7 @@ export class MainThreadSCM implements MainThreadSCMShape { } $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined, inputBoxDocumentUri: UriComponents): void { - const provider = new MainThreadSCMProvider(this._proxy, handle, id, label, rootUri ? URI.revive(rootUri) : undefined, URI.revive(inputBoxDocumentUri), this.quickDiffService); + const provider = new MainThreadSCMProvider(this._proxy, handle, id, label, rootUri ? URI.revive(rootUri) : undefined, URI.revive(inputBoxDocumentUri), this.quickDiffService, this._uriIdentService); const repository = this.scmService.registerSCMProvider(provider); this._repositories.set(handle, repository); diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index 32f43a19837..76f90e4cda5 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -7,7 +7,7 @@ import { localize } from 'vs/nls'; import { MenuId, MenuRegistry, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ActivityBarPosition, IWorkbenchLayoutService, LayoutSettings, Parts, Position, positionToString } from 'vs/workbench/services/layout/browser/layoutService'; +import { IWorkbenchLayoutService, Parts, Position, positionToString } from 'vs/workbench/services/layout/browser/layoutService'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes'; import { isWindows, isLinux, isWeb, isMacintosh, isNative } from 'vs/base/common/platform'; @@ -67,30 +67,7 @@ registerAction2(class extends Action2 { } }); -// --- Toggle Activity Bar - -export class ToggleActivityBarVisibilityAction extends Action2 { - - static readonly ID = 'workbench.action.toggleActivityBarVisibility'; - - constructor() { - super({ - id: ToggleActivityBarVisibilityAction.ID, - title: { - value: localize('toggleActivityBar', "Toggle Activity Bar Visibility"), - original: 'Toggle Activity Bar Visibility' - }, - }); - } - - run(accessor: ServicesAccessor): void { - const configurationService = accessor.get(IConfigurationService); - const value = configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION); - configurationService.updateValue(LayoutSettings.ACTIVITY_BAR_LOCATION, value === ActivityBarPosition.HIDDEN ? undefined : ActivityBarPosition.HIDDEN); - } -} - -registerAction2(ToggleActivityBarVisibilityAction); +export const ToggleActivityBarVisibilityActionId = 'workbench.action.toggleActivityBarVisibility'; // --- Toggle Centered Layout @@ -1223,7 +1200,7 @@ if (!isMacintosh || !isNative) { } ToggleVisibilityActions.push(...[ - CreateToggleLayoutItem(ToggleActivityBarVisibilityAction.ID, ContextKeyExpr.equals('config.workbench.activityBar.visible', true), localize('activityBar', "Activity Bar"), { whenA: ContextKeyExpr.equals('config.workbench.sideBar.location', 'left'), iconA: activityBarLeftIcon, iconB: activityBarRightIcon }), + CreateToggleLayoutItem(ToggleActivityBarVisibilityActionId, ContextKeyExpr.equals('config.workbench.activityBar.visible', true), localize('activityBar', "Activity Bar"), { whenA: ContextKeyExpr.equals('config.workbench.sideBar.location', 'left'), iconA: activityBarLeftIcon, iconB: activityBarRightIcon }), CreateToggleLayoutItem(ToggleSidebarVisibilityAction.ID, SideBarVisibleContext, localize('sideBar', "Primary Side Bar"), { whenA: ContextKeyExpr.equals('config.workbench.sideBar.location', 'left'), iconA: panelLeftIcon, iconB: panelRightIcon }), CreateToggleLayoutItem(ToggleAuxiliaryBarAction.ID, AuxiliaryBarVisibleContext, localize('secondarySideBar', "Secondary Side Bar"), { whenA: ContextKeyExpr.equals('config.workbench.sideBar.location', 'left'), iconA: panelRightIcon, iconB: panelLeftIcon }), CreateToggleLayoutItem(TogglePanelAction.ID, PanelVisibleContext, localize('panel', "Panel"), panelIcon), diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 8cffcb5d781..b4b247137f4 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -134,20 +134,22 @@ export class ActivitybarPart extends Part { container.style.borderColor = borderColor ? borderColor : ''; } - show(): void { + show(focus?: boolean): void { if (!this.content) { return; } - if (this.compositeBar.value) { - return; + if (!this.compositeBar.value) { + this.compositeBar.value = this.createCompositeBar(); + this.compositeBar.value.create(this.content); + + if (this.dimension) { + this.layout(this.dimension.width, this.dimension.height); + } } - this.compositeBar.value = this.createCompositeBar(); - this.compositeBar.value.create(this.content); - - if (this.dimension) { - this.layout(this.dimension.width, this.dimension.height); + if (focus) { + this.focus(); } } diff --git a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts index 14ddb6a4249..691b373b90a 100644 --- a/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts +++ b/src/vs/workbench/browser/parts/sidebar/sidebarPart.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/sidebarpart'; import 'vs/workbench/browser/parts/sidebar/sidebarActions'; import { ActivityBarPosition, IWorkbenchLayoutService, LayoutSettings, Parts, Position as SideBarPosition } from 'vs/workbench/services/layout/browser/layoutService'; import { SidebarFocusContext, ActiveViewletContext } from 'vs/workbench/common/contextkeys'; -import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -33,6 +33,7 @@ import { ACCOUNTS_ACTIVITY_ID, GLOBAL_ACTIVITY_ID } from 'vs/workbench/common/ac import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Separator } from 'vs/base/common/actions'; +import { ToggleActivityBarVisibilityActionId } from 'vs/workbench/browser/actions/layoutActions'; export class SidebarPart extends AbstractPaneCompositePart { @@ -105,17 +106,14 @@ export class SidebarPart extends AbstractPaneCompositePart { ); this.acitivityBarPart = this._register(instantiationService.createInstance(ActivitybarPart, this)); + this.rememberActivityBarVisiblePosition(); this._register(configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(LayoutSettings.ACTIVITY_BAR_LOCATION)) { - this.updateTitleArea(); - const id = this.getActiveComposite()?.getId(); - if (id) { - this.onTitleAreaUpdate(id!); - } - this.updateActivityBarVisiblity(); + this.onDidChangeActivityBarLocation(); } })); + this.registerActions(); this.registerGlobalActions(); lifecycleService.when(LifecyclePhase.Eventually).then(() => { @@ -127,6 +125,16 @@ export class SidebarPart extends AbstractPaneCompositePart { }); } + private onDidChangeActivityBarLocation(): void { + this.updateTitleArea(); + const id = this.getActiveComposite()?.getId(); + if (id) { + this.onTitleAreaUpdate(id!); + } + this.updateActivityBarVisiblity(); + this.rememberActivityBarVisiblePosition(); + } + override updateStyles(): void { super.updateStyles(); @@ -210,6 +218,21 @@ export class SidebarPart extends AbstractPaneCompositePart { return this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) !== ActivityBarPosition.HIDDEN; } + private rememberActivityBarVisiblePosition(): void { + const activityBarPosition = this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION); + if (activityBarPosition !== ActivityBarPosition.HIDDEN) { + this.storageService.store(LayoutSettings.ACTIVITY_BAR_LOCATION, activityBarPosition, StorageScope.PROFILE, StorageTarget.USER); + } + } + + private getRememberedActivityBarVisiblePosition(): ActivityBarPosition { + const activityBarPosition = this.storageService.get(LayoutSettings.ACTIVITY_BAR_LOCATION, StorageScope.PROFILE); + switch (activityBarPosition) { + case ActivityBarPosition.SIDE: return ActivityBarPosition.SIDE; + default: return ActivityBarPosition.TOP; + } + } + private updateActivityBarVisiblity(): void { if (this.shouldShowActivityBar()) { this.acitivityBarPart.show(); @@ -226,17 +249,40 @@ export class SidebarPart extends AbstractPaneCompositePart { return this.shouldShowCompositeBar() ? super.getVisiblePaneCompositeIds() : this.acitivityBarPart.getVisiblePaneCompositeIds(); } - focusActivityBar(): void { + async focusActivityBar(): Promise { + if (this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) === ActivityBarPosition.HIDDEN) { + await this.configurationService.updateValue(LayoutSettings.ACTIVITY_BAR_LOCATION, this.getRememberedActivityBarVisiblePosition()); + this.onDidChangeActivityBarLocation(); + } if (this.shouldShowCompositeBar()) { this.focusComositeBar(); } else { if (!this.layoutService.isVisible(Parts.ACTIVITYBAR_PART)) { this.layoutService.setPartHidden(false, Parts.ACTIVITYBAR_PART); } - this.acitivityBarPart.focus(); + this.acitivityBarPart.show(true); } } + private registerActions(): void { + const that = this; + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: ToggleActivityBarVisibilityActionId, + title: { + value: localize('toggleActivityBar', "Toggle Activity Bar Visibility"), + original: 'Toggle Activity Bar Visibility' + }, + }); + } + run(): Promise { + const value = that.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) === ActivityBarPosition.HIDDEN ? that.getRememberedActivityBarVisiblePosition() : ActivityBarPosition.HIDDEN; + return that.configurationService.updateValue(LayoutSettings.ACTIVITY_BAR_LOCATION, value); + } + })); + } + private registerGlobalActions() { this._register(registerAction2( class extends Action2 { diff --git a/src/vs/workbench/contrib/comments/browser/commentService.ts b/src/vs/workbench/contrib/comments/browser/commentService.ts index 63b91ec40ad..a15deff2d34 100644 --- a/src/vs/workbench/contrib/comments/browser/commentService.ts +++ b/src/vs/workbench/contrib/comments/browser/commentService.ts @@ -105,7 +105,7 @@ export interface ICommentService { setCurrentCommentThread(commentThread: CommentThread | undefined): void; enableCommenting(enable: boolean): void; registerContinueOnCommentProvider(provider: IContinueOnCommentProvider): IDisposable; - removeContinueOnComment(pendingComment: { range: IRange; uri: URI; owner: string; isReply?: boolean }): PendingCommentThread | undefined; + removeContinueOnComment(pendingComment: { range: IRange | undefined; uri: URI; owner: string; isReply?: boolean }): PendingCommentThread | undefined; } const CONTINUE_ON_COMMENTS = 'comments.continueOnComments'; diff --git a/src/vs/workbench/contrib/comments/browser/commentsController.ts b/src/vs/workbench/contrib/comments/browser/commentsController.ts index 4e7ca0f1050..86632e72279 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsController.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsController.ts @@ -465,7 +465,7 @@ export class CommentController implements IEditorContribution { for (const zone of this._commentWidgets) { const zonePendingComments = zone.getPendingComments(); const pendingNewComment = zonePendingComments.newComment; - if (!pendingNewComment || !zone.commentThread.range) { + if (!pendingNewComment) { continue; } let lastCommentBody; @@ -842,7 +842,13 @@ export class CommentController implements IEditorContribution { return; } - const continueOnCommentIndex = this._inProcessContinueOnComments.get(e.owner)?.findIndex(pending => Range.lift(pending.range).equalsRange(thread.range)); + const continueOnCommentIndex = this._inProcessContinueOnComments.get(e.owner)?.findIndex(pending => { + if (pending.range === undefined) { + return thread.range === undefined; + } else { + return Range.lift(pending.range).equalsRange(thread.range); + } + }); let continueOnCommentText: string | undefined; if ((continueOnCommentIndex !== undefined) && continueOnCommentIndex >= 0) { continueOnCommentText = this._inProcessContinueOnComments.get(e.owner)?.splice(continueOnCommentIndex, 1)[0].body; @@ -892,7 +898,7 @@ export class CommentController implements IEditorContribution { this._inProcessContinueOnComments.set(thread.owner, []); } this._inProcessContinueOnComments.get(thread.owner)?.push(thread); - await this.commentService.createCommentThreadTemplate(thread.owner, thread.uri, Range.lift(thread.range)); + await this.commentService.createCommentThreadTemplate(thread.owner, thread.uri, thread.range ? Range.lift(thread.range) : undefined); } } diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts index bac592ba3a6..7ba7a009540 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts @@ -788,8 +788,8 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo } private getChangedResources(repository: ISCMRepository) { - return repository.provider.groups.elements.reduce((resources, resourceGroups) => { - resourceGroups.elements.forEach((resource) => resources.add(resource.sourceUri)); + return repository.provider.groups.reduce((resources, resourceGroups) => { + resourceGroups.resources.forEach((resource) => resources.add(resource.sourceUri)); return resources; }, new Set()); // A URI might appear in more than one resource group } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts index 1d316007d01..725fb24747d 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts @@ -281,7 +281,7 @@ export class FindModel extends Disposable { const index = this._notebookEditor.getCellIndex(findMatch.cell); if (index !== undefined) { // const range: ICellRange = { start: index, end: index + 1 }; - this._notebookEditor.revealCellOffsetInCenterAsync(findMatch.cell, outputOffset ?? 0); + this._notebookEditor.revealCellOffsetInCenter(findMatch.cell, outputOffset ?? 0); } } else { const match = findMatch.getMatch(matchIndex) as FindMatch; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index f525ec94a5c..36f38ac2bc0 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -326,12 +326,13 @@ export interface INotebookDeltaCellStatusBarItems { readonly items: readonly INotebookCellStatusBarItem[]; } -export const enum CellRevealSyncType { +export const enum CellRevealType { Default = 1, Top = 2, Center = 3, CenterIfOutsideViewport = 4, - FirstLineIfOutsideViewport = 5 + NearTopIfOutsideViewport = 5, + FirstLineIfOutsideViewport = 6 } export enum CellRevealRangeType { @@ -340,11 +341,6 @@ export enum CellRevealRangeType { CenterIfOutsideViewport = 3, } -export enum CellRevealType { - NearTopIfOutsideViewport, - CenterIfOutsideViewport -} - export interface INotebookEditorOptions extends ITextEditorOptions { readonly cellOptions?: ITextResourceEditorInput; readonly cellRevealType?: CellRevealType; @@ -642,7 +638,7 @@ export interface INotebookEditor { /** * Reveal a position with `offset` in a cell into viewport center. */ - revealCellOffsetInCenterAsync(cell: ICellViewModel, offset: number): Promise; + revealCellOffsetInCenter(cell: ICellViewModel, offset: number): void; /** * Convert the view range to model range diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index e391c3c2d56..f1a902a7271 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -52,7 +52,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { contrastBorder, errorForeground, focusBorder, foreground, listInactiveSelectionBackground, registerColor, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, transparent } from 'vs/platform/theme/common/colorRegistry'; import { EDITOR_PANE_BACKGROUND, PANEL_BORDER, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugColors'; -import { CellEditState, CellFindMatchWithIndex, CellFocusMode, CellLayoutContext, CellRevealRangeType, CellRevealSyncType, CellRevealType, IActiveNotebookEditorDelegate, IBaseCellEditorOptions, ICellOutputViewModel, ICellViewModel, ICommonCellInfo, IDisplayOutputLayoutUpdateRequest, IFocusNotebookCellOptions, IInsetRenderOutput, IModelDecorationsChangeAccessor, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorDelegate, INotebookEditorMouseEvent, INotebookEditorOptions, INotebookEditorViewState, INotebookViewCellsUpdateEvent, INotebookWebviewMessage, RenderOutputType, ScrollToRevealBehavior } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, CellFindMatchWithIndex, CellFocusMode, CellLayoutContext, CellRevealRangeType, CellRevealType, IActiveNotebookEditorDelegate, IBaseCellEditorOptions, ICellOutputViewModel, ICellViewModel, ICommonCellInfo, IDisplayOutputLayoutUpdateRequest, IFocusNotebookCellOptions, IInsetRenderOutput, IModelDecorationsChangeAccessor, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorDelegate, INotebookEditorMouseEvent, INotebookEditorOptions, INotebookEditorViewState, INotebookViewCellsUpdateEvent, INotebookWebviewMessage, RenderOutputType, ScrollToRevealBehavior } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; import { notebookDebug } from 'vs/workbench/contrib/notebook/browser/notebookLogger'; @@ -1231,10 +1231,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD cell.updateEditState(CellEditState.Editing, 'setOptions'); cell.focusMode = CellFocusMode.Editor; await this.revealRangeInCenterIfOutsideViewportAsync(cell, new Range(selection.startLineNumber, selection.startColumn, selection.endLineNumber || selection.startLineNumber, selection.endColumn || selection.startColumn)); - } else if (options?.cellRevealType === CellRevealType.NearTopIfOutsideViewport) { - await this._list.revealCellAsync(cell, CellRevealType.NearTopIfOutsideViewport); } else { - await this._list.revealCellAsync(cell, CellRevealType.CenterIfOutsideViewport); + this._list.revealCell(cell, options?.cellRevealType ?? CellRevealType.CenterIfOutsideViewport); } const editor = this._renderedEditors.get(cell)!; @@ -2058,55 +2056,55 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD } revealCellRangeInView(range: ICellRange) { - return this._list.revealCellsInView(range); + return this._list.revealCells(range); } revealInView(cell: ICellViewModel) { - this._list.revealCell(cell, CellRevealSyncType.Default); + this._list.revealCell(cell, CellRevealType.Default); } revealInViewAtTop(cell: ICellViewModel) { - this._list.revealCell(cell, CellRevealSyncType.Top); + this._list.revealCell(cell, CellRevealType.Top); } revealInCenter(cell: ICellViewModel) { - this._list.revealCell(cell, CellRevealSyncType.Center); + this._list.revealCell(cell, CellRevealType.Center); } revealInCenterIfOutsideViewport(cell: ICellViewModel) { - this._list.revealCell(cell, CellRevealSyncType.CenterIfOutsideViewport); + this._list.revealCell(cell, CellRevealType.CenterIfOutsideViewport); } revealFirstLineIfOutsideViewport(cell: ICellViewModel) { - this._list.revealCell(cell, CellRevealSyncType.FirstLineIfOutsideViewport); + this._list.revealCell(cell, CellRevealType.FirstLineIfOutsideViewport); } async revealLineInViewAsync(cell: ICellViewModel, line: number): Promise { - return this._list.revealCellRangeAsync(cell, new Range(line, 1, line, 1), CellRevealRangeType.Default); + return this._list.revealRangeInCell(cell, new Range(line, 1, line, 1), CellRevealRangeType.Default); } async revealLineInCenterAsync(cell: ICellViewModel, line: number): Promise { - return this._list.revealCellRangeAsync(cell, new Range(line, 1, line, 1), CellRevealRangeType.Center); + return this._list.revealRangeInCell(cell, new Range(line, 1, line, 1), CellRevealRangeType.Center); } async revealLineInCenterIfOutsideViewportAsync(cell: ICellViewModel, line: number): Promise { - return this._list.revealCellRangeAsync(cell, new Range(line, 1, line, 1), CellRevealRangeType.CenterIfOutsideViewport); + return this._list.revealRangeInCell(cell, new Range(line, 1, line, 1), CellRevealRangeType.CenterIfOutsideViewport); } async revealRangeInViewAsync(cell: ICellViewModel, range: Selection | Range): Promise { - return this._list.revealCellRangeAsync(cell, range, CellRevealRangeType.Default); + return this._list.revealRangeInCell(cell, range, CellRevealRangeType.Default); } async revealRangeInCenterAsync(cell: ICellViewModel, range: Selection | Range): Promise { - return this._list.revealCellRangeAsync(cell, range, CellRevealRangeType.Center); + return this._list.revealRangeInCell(cell, range, CellRevealRangeType.Center); } async revealRangeInCenterIfOutsideViewportAsync(cell: ICellViewModel, range: Selection | Range): Promise { - return this._list.revealCellRangeAsync(cell, range, CellRevealRangeType.CenterIfOutsideViewport); + return this._list.revealRangeInCell(cell, range, CellRevealRangeType.CenterIfOutsideViewport); } - async revealCellOffsetInCenterAsync(cell: ICellViewModel, offset: number): Promise { - return this._list.revealCellOffsetInCenterAsync(cell, offset); + revealCellOffsetInCenter(cell: ICellViewModel, offset: number) { + return this._list.revealCellOffsetInCenter(cell, offset); } getViewIndexByModelIndex(index: number): number { @@ -2341,9 +2339,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD const selectionsStartPosition = cell.getSelectionsStartPosition(); if (selectionsStartPosition?.length) { const firstSelectionPosition = selectionsStartPosition[0]; - await this.revealRangeInCenterIfOutsideViewportAsync(cell, Range.fromPositions(firstSelectionPosition, firstSelectionPosition)); + await this.revealRangeInViewAsync(cell, Range.fromPositions(firstSelectionPosition, firstSelectionPosition)); } else { - this.revealInCenterIfOutsideViewport(cell); + this.revealInView(cell); } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index 04f3c12567f..827a3195e21 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -18,7 +18,7 @@ import { PrefixSumComputer } from 'vs/editor/common/model/prefixSumComputer'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IListService, IWorkbenchListOptions, WorkbenchList } from 'vs/platform/list/browser/listService'; -import { CursorAtBoundary, ICellViewModel, CellEditState, CellFocusMode, ICellOutputViewModel, CellRevealType, CellRevealSyncType, CellRevealRangeType, CursorAtLineBoundary } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CursorAtBoundary, ICellViewModel, CellEditState, CellFocusMode, ICellOutputViewModel, CellRevealType, CellRevealRangeType, CursorAtLineBoundary } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl'; import { diff, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, CellKind, SelectionStateType, NOTEBOOK_EDITOR_CURSOR_LINE_BOUNDARY } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellRange, cellRangesToIndexes, reduceCellRanges, cellRangesEqual } from 'vs/workbench/contrib/notebook/common/notebookRange'; @@ -35,11 +35,6 @@ import { NotebookOptions } from 'vs/workbench/contrib/notebook/browser/notebookO import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { NotebookCellAnchor } from 'vs/workbench/contrib/notebook/browser/view/notebookCellAnchor'; -const enum CellEditorRevealType { - Line, - Range -} - const enum CellRevealPosition { Top, Center, @@ -790,7 +785,7 @@ export class NotebookCellList extends WorkbenchList implements ID /** * The range will be revealed with as little scrolling as possible. */ - revealCellsInView(range: ICellRange) { + revealCells(range: ICellRange) { const startIndex = this._getViewIndexUpperBound2(range.start); if (startIndex < 0) { @@ -853,8 +848,12 @@ export class NotebookCellList extends WorkbenchList implements ID this.view.setScrollTop(scrollHeight - (wrapperBottom - scrollTop)); } - //#region Reveal Cell synchronously - revealCell(cell: ICellViewModel, revealType: CellRevealSyncType) { + /** + * Reveals the given cell in the notebook cell list. The cell will come into view syncronously + * but the cell's editor will be attached asyncronously if it was previously out of view. + * @returns The promise to await for the cell editor to be attached + */ + async revealCell(cell: ICellViewModel, revealType: CellRevealType): Promise { const index = this._getViewIndexUpperBound(cell); if (index < 0) { @@ -862,22 +861,32 @@ export class NotebookCellList extends WorkbenchList implements ID } switch (revealType) { - case CellRevealSyncType.Top: + case CellRevealType.Top: this._revealInternal(index, false, CellRevealPosition.Top); break; - case CellRevealSyncType.Center: + case CellRevealType.Center: this._revealInternal(index, false, CellRevealPosition.Center); break; - case CellRevealSyncType.CenterIfOutsideViewport: + case CellRevealType.CenterIfOutsideViewport: this._revealInternal(index, true, CellRevealPosition.Center); break; - case CellRevealSyncType.FirstLineIfOutsideViewport: + case CellRevealType.NearTopIfOutsideViewport: + this._revealInternal(index, true, CellRevealPosition.NearTop); + break; + case CellRevealType.FirstLineIfOutsideViewport: this._revealInViewWithMinimalScrolling(index, true); break; - case CellRevealSyncType.Default: + case CellRevealType.Default: this._revealInViewWithMinimalScrolling(index); break; } + + // wait for the editor to be created only if the cell is in editing mode (meaning it has an editor and will focus the editor) + if (cell.getEditState() === CellEditState.Editing && !cell.editorAttached) { + return getEditorAttachedPromise(cell); + } + + return; } private _revealInternal(viewIndex: number, ignoreIfInsideViewport: boolean, revealPosition: CellRevealPosition, firstLine?: boolean) { @@ -942,31 +951,8 @@ export class NotebookCellList extends WorkbenchList implements ID } } - //#endregion - - //#region Reveal Cell asynchronously - async revealCellAsync(cell: ICellViewModel, revealType: CellRevealType) { - const viewIndex = this._getViewIndexUpperBound(cell); - - if (viewIndex < 0) { - return; - } - - const revealPosition = revealType === CellRevealType.NearTopIfOutsideViewport ? CellRevealPosition.NearTop : CellRevealPosition.Center; - this._revealInternal(viewIndex, true, revealPosition); - - // wait for the editor to be created only if the cell is in editing mode (meaning it has an editor and will focus the editor) - if (cell.getEditState() === CellEditState.Editing && !cell.editorAttached) { - return getEditorAttachedPromise(cell); - } - - return; - } - - //#endregion - //#region Reveal Cell Editor Range asynchronously - async revealCellRangeAsync(cell: ICellViewModel, range: Selection | Range, revealType: CellRevealRangeType): Promise { + async revealRangeInCell(cell: ICellViewModel, range: Selection | Range, revealType: CellRevealRangeType): Promise { const index = this._getViewIndexUpperBound(cell); if (index < 0) { @@ -975,34 +961,34 @@ export class NotebookCellList extends WorkbenchList implements ID switch (revealType) { case CellRevealRangeType.Default: - return this._revealRangeInternalAsync(index, range, CellEditorRevealType.Range); + return this._revealRangeInternalAsync(index, range); case CellRevealRangeType.Center: - return this._revealRangeInCenterInternalAsync(index, range, CellEditorRevealType.Range); + return this._revealRangeInCenterInternalAsync(index, range); case CellRevealRangeType.CenterIfOutsideViewport: - return this._revealRangeInCenterIfOutsideViewportInternalAsync(index, range, CellEditorRevealType.Range); + return this._revealRangeInCenterIfOutsideViewportInternalAsync(index, range); } } // List items have real dynamic heights, which means after we set `scrollTop` based on the `elementTop(index)`, the element at `index` might still be removed from the view once all relayouting tasks are done. // For example, we scroll item 10 into the view upwards, in the first round, items 7, 8, 9, 10 are all in the viewport. Then item 7 and 8 resize themselves to be larger and finally item 10 is removed from the view. // To ensure that item 10 is always there, we need to scroll item 10 to the top edge of the viewport. - private async _revealRangeInternalAsync(viewIndex: number, range: Selection | Range, revealType: CellEditorRevealType): Promise { + private async _revealRangeInternalAsync(viewIndex: number, range: Selection | Range): Promise { const scrollTop = this.getViewScrollTop(); const wrapperBottom = this.getViewScrollBottom(); const elementTop = this.view.elementTop(viewIndex); const element = this.view.element(viewIndex); if (element.editorAttached) { - this._revealRangeCommon(viewIndex, range, revealType, false, false); + this._revealRangeCommon(viewIndex, range, false, false); } else { const elementHeight = this.view.elementHeight(viewIndex); let upwards = false; - if (elementTop + elementHeight < scrollTop) { + if (elementTop + elementHeight <= scrollTop) { // scroll downwards this.view.setScrollTop(elementTop); upwards = false; - } else if (elementTop > wrapperBottom) { + } else if (elementTop >= wrapperBottom) { // scroll upwards this.view.setScrollTop(elementTop - this.view.renderHeight / 2); upwards = true; @@ -1015,21 +1001,18 @@ export class NotebookCellList extends WorkbenchList implements ID }); return editorAttachedPromise.then(() => { - this._revealRangeCommon(viewIndex, range, revealType, true, upwards); + this._revealRangeCommon(viewIndex, range, true, upwards); }); } } - private async _revealRangeInCenterInternalAsync(viewIndex: number, range: Selection | Range, revealType: CellEditorRevealType): Promise { - const reveal = (viewIndex: number, range: Range, revealType: CellEditorRevealType) => { + private async _revealRangeInCenterInternalAsync(viewIndex: number, range: Selection | Range): Promise { + const reveal = (viewIndex: number, range: Range) => { const element = this.view.element(viewIndex); const positionOffset = element.getPositionScrollTopOffset(range); const positionOffsetInView = this.view.elementTop(viewIndex) + positionOffset; this.view.setScrollTop(positionOffsetInView - this.view.renderHeight / 2); - - if (revealType === CellEditorRevealType.Range) { - element.revealRangeInCenter(range); - } + element.revealRangeInCenter(range); }; const elementTop = this.view.elementTop(viewIndex); @@ -1038,22 +1021,20 @@ export class NotebookCellList extends WorkbenchList implements ID const element = this.view.element(viewIndex); if (!element.editorAttached) { - return getEditorAttachedPromise(element).then(() => reveal(viewIndex, range, revealType)); + return getEditorAttachedPromise(element).then(() => reveal(viewIndex, range)); } else { - reveal(viewIndex, range, revealType); + reveal(viewIndex, range); } } - private async _revealRangeInCenterIfOutsideViewportInternalAsync(viewIndex: number, range: Selection | Range, revealType: CellEditorRevealType): Promise { - const reveal = (viewIndex: number, range: Range, revealType: CellEditorRevealType) => { + private async _revealRangeInCenterIfOutsideViewportInternalAsync(viewIndex: number, range: Selection | Range): Promise { + const reveal = (viewIndex: number, range: Range) => { const element = this.view.element(viewIndex); const positionOffset = element.getPositionScrollTopOffset(range); const positionOffsetInView = this.view.elementTop(viewIndex) + positionOffset; this.view.setScrollTop(positionOffsetInView - this.view.renderHeight / 2); - if (revealType === CellEditorRevealType.Range) { - element.revealRangeInCenter(range); - } + element.revealRangeInCenter(range); }; const scrollTop = this.getViewScrollTop(); @@ -1073,7 +1054,7 @@ export class NotebookCellList extends WorkbenchList implements ID // reveal editor if (!element.editorAttached) { - return getEditorAttachedPromise(element).then(() => reveal(viewIndex, range, revealType)); + return getEditorAttachedPromise(element).then(() => reveal(viewIndex, range)); } else { // for example markdown } @@ -1082,12 +1063,12 @@ export class NotebookCellList extends WorkbenchList implements ID element.revealRangeInCenter(range); } else { // for example, markdown cell in preview mode - return getEditorAttachedPromise(element).then(() => reveal(viewIndex, range, revealType)); + return getEditorAttachedPromise(element).then(() => reveal(viewIndex, range)); } } } - private _revealRangeCommon(viewIndex: number, range: Selection | Range, revealType: CellEditorRevealType, newlyCreated: boolean, alignToBottom: boolean) { + private _revealRangeCommon(viewIndex: number, range: Selection | Range, newlyCreated: boolean, alignToBottom: boolean) { const element = this.view.element(viewIndex); const scrollTop = this.getViewScrollTop(); const wrapperBottom = this.getViewScrollBottom(); @@ -1120,14 +1101,17 @@ export class NotebookCellList extends WorkbenchList implements ID } } - if (revealType === CellEditorRevealType.Range) { - element.revealRangeInCenter(range); - } + element.revealRangeInCenter(range); } //#endregion - //#region Reveal Cell offset - async revealCellOffsetInCenterAsync(cell: ICellViewModel, offset: number): Promise { + + + /** + * Reveals the specified offset of the given cell in the center of the viewport. + * This enables revealing locations in the output as well as the input. + */ + revealCellOffsetInCenter(cell: ICellViewModel, offset: number) { const viewIndex = this._getViewIndexUpperBound(cell); if (viewIndex >= 0) { @@ -1147,8 +1131,6 @@ export class NotebookCellList extends WorkbenchList implements ID this._revealInternal(viewIndex, true, CellRevealPosition.Center); } - //#endregion - domElementOfElement(element: ICellViewModel): HTMLElement | null { const index = this._getViewIndexUpperBound(element); if (index >= 0) { diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts index 3b4845bac0e..23fc30b1239 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookRenderingCommon.ts @@ -16,7 +16,7 @@ import { Selection } from 'vs/editor/common/core/selection'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IWorkbenchListOptionsUpdate } from 'vs/platform/list/browser/listService'; -import { CellRevealRangeType, CellRevealSyncType, CellRevealType, ICellOutputViewModel, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellRevealRangeType, CellRevealType, ICellOutputViewModel, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellPartsCollection } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; @@ -65,12 +65,11 @@ export interface INotebookCellList { selectElements(elements: ICellViewModel[]): void; getFocusedElements(): ICellViewModel[]; getSelectedElements(): ICellViewModel[]; - revealCellsInView(range: ICellRange): void; scrollToBottom(): void; - revealCell(cell: ICellViewModel, revealType: CellRevealSyncType): void; - revealCellAsync(cell: ICellViewModel, revealType: CellRevealType): Promise; - revealCellRangeAsync(cell: ICellViewModel, range: Selection | Range, revealType: CellRevealRangeType): Promise; - revealCellOffsetInCenterAsync(element: ICellViewModel, offset: number): Promise; + revealCell(cell: ICellViewModel, revealType: CellRevealType): void; + revealCells(range: ICellRange): void; + revealRangeInCell(cell: ICellViewModel, range: Selection | Range, revealType: CellRevealRangeType): Promise; + revealCellOffsetInCenter(element: ICellViewModel, offset: number): void; setHiddenAreas(_ranges: ICellRange[], triggerViewUpdate: boolean): boolean; domElementOfElement(element: ICellViewModel): HTMLElement | null; focusView(): void; diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookCellList.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookCellList.test.ts index 13804ee83f7..2e32a24b22f 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookCellList.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookCellList.test.ts @@ -65,17 +65,17 @@ suite('NotebookCellList', () => { assert.deepStrictEqual(cellList.getViewScrollBottom(), 215); // reveal cell 1, top 50, bottom 150, which is fully visible in the viewport - cellList.revealCellsInView({ start: 1, end: 2 }); + cellList.revealCells({ start: 1, end: 2 }); assert.deepStrictEqual(cellList.scrollTop, 5); assert.deepStrictEqual(cellList.getViewScrollBottom(), 215); // reveal cell 2, top 150, bottom 200, which is fully visible in the viewport - cellList.revealCellsInView({ start: 2, end: 3 }); + cellList.revealCells({ start: 2, end: 3 }); assert.deepStrictEqual(cellList.scrollTop, 5); assert.deepStrictEqual(cellList.getViewScrollBottom(), 215); // reveal cell 3, top 200, bottom 300, which is partially visible in the viewport - cellList.revealCellsInView({ start: 3, end: 4 }); + cellList.revealCells({ start: 3, end: 4 }); assert.deepStrictEqual(cellList.scrollTop, 90); }); }); @@ -110,7 +110,7 @@ suite('NotebookCellList', () => { assert.deepStrictEqual(cellList.getViewScrollBottom(), 210); // reveal cell 3, top 200, bottom 300, which is partially visible in the viewport - cellList.revealCellsInView({ start: 3, end: 4 }); + cellList.revealCells({ start: 3, end: 4 }); assert.deepStrictEqual(cellList.scrollTop, 90); // scroll to 5 @@ -119,7 +119,7 @@ suite('NotebookCellList', () => { assert.deepStrictEqual(cellList.getViewScrollBottom(), 215); // reveal cell 0, top 0, bottom 50 - cellList.revealCellsInView({ start: 0, end: 1 }); + cellList.revealCells({ start: 0, end: 1 }); assert.deepStrictEqual(cellList.scrollTop, 0); }); }); @@ -155,7 +155,7 @@ suite('NotebookCellList', () => { assert.deepStrictEqual(cellList.scrollTop, 0); assert.deepStrictEqual(cellList.getViewScrollBottom(), 210); - cellList.revealCellsInView({ start: 4, end: 5 }); + cellList.revealCells({ start: 4, end: 5 }); assert.deepStrictEqual(cellList.scrollTop, 140); // assert.deepStrictEqual(cellList.getViewScrollBottom(), 330); }); diff --git a/src/vs/workbench/contrib/scm/browser/activity.ts b/src/vs/workbench/contrib/scm/browser/activity.ts index b4edaa7f64e..68a280a56bc 100644 --- a/src/vs/workbench/contrib/scm/browser/activity.ts +++ b/src/vs/workbench/contrib/scm/browser/activity.ts @@ -23,7 +23,7 @@ function getCount(repository: ISCMRepository): number { if (typeof repository.provider.count === 'number') { return repository.provider.count; } else { - return repository.provider.groups.elements.reduce((r, g) => r + g.elements.length, 0); + return repository.provider.groups.reduce((r, g) => r + g.resources.length, 0); } } @@ -264,8 +264,8 @@ export class SCMActiveResourceContextKeyController implements IWorkbenchContribu this.activeResourceRepositoryContextKey.set(activeResourceRepository?.id); - for (const resourceGroup of activeResourceRepository?.provider.groups.elements ?? []) { - if (resourceGroup.elements + for (const resourceGroup of activeResourceRepository?.provider.groups ?? []) { + if (resourceGroup.resources .some(scmResource => this.uriIdentityService.extUri.isEqual(activeResource, scmResource.sourceUri))) { this.activeResourceHasChangesContextKey.set(true); diff --git a/src/vs/workbench/contrib/scm/browser/menus.ts b/src/vs/workbench/contrib/scm/browser/menus.ts index 9918414d4c0..e0100b69f5b 100644 --- a/src/vs/workbench/contrib/scm/browser/menus.ts +++ b/src/vs/workbench/contrib/scm/browser/menus.ts @@ -12,7 +12,6 @@ import { IAction } from 'vs/base/common/actions'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { ISCMResource, ISCMResourceGroup, ISCMProvider, ISCMRepository, ISCMService, ISCMMenus, ISCMRepositoryMenus } from 'vs/workbench/contrib/scm/common/scm'; import { equals } from 'vs/base/common/arrays'; -import { ISplice } from 'vs/base/common/sequence'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { localize } from 'vs/nls'; @@ -148,7 +147,6 @@ export class SCMRepositoryMenus implements ISCMRepositoryMenus, IDisposable { private contextKeyService: IContextKeyService; readonly titleMenu: SCMTitleMenu; - private readonly resourceGroups: ISCMResourceGroup[] = []; private readonly resourceGroupMenusItems = new Map(); private _repositoryMenu: IMenu | undefined; @@ -164,7 +162,7 @@ export class SCMRepositoryMenus implements ISCMRepositoryMenus, IDisposable { private readonly disposables = new DisposableStore(); constructor( - provider: ISCMProvider, + private readonly provider: ISCMProvider, @IContextKeyService contextKeyService: IContextKeyService, @IInstantiationService instantiationService: IInstantiationService, @IMenuService private readonly menuService: IMenuService @@ -179,8 +177,8 @@ export class SCMRepositoryMenus implements ISCMRepositoryMenus, IDisposable { instantiationService = instantiationService.createChild(serviceCollection); this.titleMenu = instantiationService.createInstance(SCMTitleMenu); - provider.groups.onDidSplice(this.onDidSpliceGroups, this, this.disposables); - this.onDidSpliceGroups({ start: 0, deleteCount: 0, toInsert: provider.groups.elements }); + provider.onDidChangeResourceGroups(this.onDidChangeResourceGroups, this, this.disposables); + this.onDidChangeResourceGroups(); } getResourceGroupMenu(group: ISCMResourceGroup): IMenu { @@ -210,13 +208,12 @@ export class SCMRepositoryMenus implements ISCMRepositoryMenus, IDisposable { return result; } - private onDidSpliceGroups({ start, deleteCount, toInsert }: ISplice): void { - const deleted = this.resourceGroups.splice(start, deleteCount, ...toInsert); - - for (const group of deleted) { - const item = this.resourceGroupMenusItems.get(group); - item?.dispose(); - this.resourceGroupMenusItems.delete(group); + private onDidChangeResourceGroups(): void { + for (const resourceGroup of this.resourceGroupMenusItems.keys()) { + if (!this.provider.groups.includes(resourceGroup)) { + this.resourceGroupMenusItems.get(resourceGroup)?.dispose(); + this.resourceGroupMenusItems.delete(resourceGroup); + } } } diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index 23a0f32b75e..b52d25709ba 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/scm'; import { Event, Emitter } from 'vs/base/common/event'; import { basename, dirname } from 'vs/base/common/resources'; -import { IDisposable, Disposable, DisposableStore, combinedDisposable, dispose, toDisposable, MutableDisposable, IReference } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable, DisposableStore, combinedDisposable, dispose, toDisposable, MutableDisposable, IReference, DisposableMap } from 'vs/base/common/lifecycle'; import { ViewPane, IViewPaneOptions, ViewAction } from 'vs/workbench/browser/parts/views/viewPane'; import { append, $, Dimension, asCSSUrl, trackFocus, clearNode } from 'vs/base/browser/dom'; import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list'; @@ -23,25 +23,23 @@ import { MenuItemAction, IMenuService, registerAction2, MenuId, IAction2Options, import { IAction, ActionRunner, Action, Separator } from 'vs/base/common/actions'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; import { IThemeService, IFileIconTheme } from 'vs/platform/theme/common/themeService'; -import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton } from './util'; -import { WorkbenchCompressibleObjectTree, IOpenEvent } from 'vs/platform/list/browser/listService'; +import { isSCMResource, isSCMResourceGroup, connectPrimaryMenuToInlineActionBar, isSCMRepository, isSCMInput, collectContextMenuActions, getActionViewItemProvider, isSCMActionButton, isSCMViewService } from './util'; +import { WorkbenchCompressibleAsyncDataTree, IOpenEvent } from 'vs/platform/list/browser/listService'; import { IConfigurationService, ConfigurationTarget, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; -import { disposableTimeout, ThrottledDelayer } from 'vs/base/common/async'; -import { ITreeNode, ITreeFilter, ITreeSorter, ITreeContextMenuEvent, ITreeDragAndDrop, ITreeDragOverReaction } from 'vs/base/browser/ui/tree/tree'; +import { disposableTimeout, ThrottledDelayer, Sequencer } from 'vs/base/common/async'; +import { ITreeNode, ITreeFilter, ITreeSorter, ITreeContextMenuEvent, ITreeDragAndDrop, ITreeDragOverReaction, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; import { ResourceTree, IResourceNode } from 'vs/base/common/resourceTree'; -import { ISplice } from 'vs/base/common/sequence'; import { ICompressibleTreeRenderer, ICompressibleKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/tree/objectTree'; import { Iterable } from 'vs/base/common/iterator'; -import { ICompressedTreeNode, ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; +import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; import { URI } from 'vs/base/common/uri'; import { FileKind } from 'vs/platform/files/common/files'; import { compareFileNames, comparePaths } from 'vs/base/common/comparers'; import { FuzzyScore, createMatches, IMatch } from 'vs/base/common/filters'; import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; import { localize } from 'vs/nls'; -import { coalesce, flatten } from 'vs/base/common/arrays'; -import { memoize } from 'vs/base/common/decorators'; -import { IStorageService, StorageScope, StorageTarget, WillSaveStateReason } from 'vs/platform/storage/common/storage'; +import { flatten } from 'vs/base/common/arrays'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; import { SIDE_BAR_BACKGROUND, PANEL_BACKGROUND } from 'vs/workbench/common/theme'; import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; @@ -73,7 +71,6 @@ import { ThemeIcon } from 'vs/base/common/themables'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; import { RepositoryRenderer } from 'vs/workbench/contrib/scm/browser/scmRepositoryRenderer'; import { ColorScheme } from 'vs/platform/theme/common/theme'; -import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { LabelFuzzyScore } from 'vs/base/browser/ui/tree/abstractTree'; import { Selection } from 'vs/editor/common/core/selection'; import { API_OPEN_DIFF_EDITOR_COMMAND_ID, API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; @@ -97,6 +94,8 @@ import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; import { CodeDataTransfers } from 'vs/platform/dnd/browser/dnd'; import { FormatOnType } from 'vs/editor/contrib/format/browser/formatActions'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { IAsyncDataTreeViewState, ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; type TreeElement = ISCMRepository | ISCMInput | ISCMActionButton | ISCMResourceGroup | IResourceNode | ISCMResource; @@ -392,7 +391,7 @@ class ResourceGroupRenderer implements ICompressibleTreeRenderer(); constructor( - private viewModelProvider: () => ViewModel, + private viewMode: () => ViewMode, private labels: ResourceLabels, private actionViewItemProvider: IActionViewItemProvider, private actionRunner: ActionRunner, @@ -492,8 +491,8 @@ class ResourceRenderer implements ICompressibleTreeRenderer { } } +class SCMTreeCompressionDelegate implements ITreeCompressionDelegate { + + isIncompressible(element: TreeElement): boolean { + if (ResourceTree.isResourceNode(element)) { + return element.childrenCount === 0 || !element.parent || !element.parent.parent; + } + + return true; + } + +} + class SCMTreeFilter implements ITreeFilter { filter(element: TreeElement): boolean { if (ResourceTree.isResourceNode(element)) { return true; } else if (isSCMResourceGroup(element)) { - return element.elements.length > 0 || !element.hideWhenEmpty; + return element.resources.length > 0 || !element.hideWhenEmpty; } else { return true; } @@ -731,10 +733,9 @@ class SCMTreeFilter implements ITreeFilter { export class SCMTreeSorter implements ITreeSorter { - @memoize - private get viewModel(): ViewModel { return this.viewModelProvider(); } - - constructor(private viewModelProvider: () => ViewModel) { } + constructor( + private readonly viewMode: () => ViewMode, + private readonly viewSortKey: () => ViewSortKey) { } compare(one: TreeElement, other: TreeElement): number { if (isSCMRepository(one)) { @@ -766,9 +767,9 @@ export class SCMTreeSorter implements ITreeSorter { } // List - if (this.viewModel.mode === ViewModelMode.List) { + if (this.viewMode() === ViewMode.List) { // FileName - if (this.viewModel.sortKey === ViewModelSortKey.Name) { + if (this.viewSortKey() === ViewSortKey.Name) { const oneName = basename((one as ISCMResource).sourceUri); const otherName = basename((other as ISCMResource).sourceUri); @@ -776,7 +777,7 @@ export class SCMTreeSorter implements ITreeSorter { } // Status - if (this.viewModel.sortKey === ViewModelSortKey.Status) { + if (this.viewSortKey() === ViewSortKey.Status) { const oneTooltip = (one as ISCMResource).decorations.tooltip ?? ''; const otherTooltip = (other as ISCMResource).decorations.tooltip ?? ''; @@ -810,7 +811,7 @@ export class SCMTreeSorter implements ITreeSorter { export class SCMTreeKeyboardNavigationLabelProvider implements ICompressibleKeyboardNavigationLabelProvider { constructor( - private viewModelProvider: () => ViewModel, + private viewMode: () => ViewMode, @ILabelService private readonly labelService: ILabelService, ) { } @@ -822,8 +823,7 @@ export class SCMTreeKeyboardNavigationLabelProvider implements ICompressibleKeyb } else if (isSCMResourceGroup(element)) { return element.label; } else { - const viewModel = this.viewModelProvider(); - if (viewModel.mode === ViewModelMode.List) { + if (this.viewMode() === ViewMode.List) { // In List mode match using the file name and the path. // Since we want to match both on the file name and the // full path we return an array of labels. A match in the @@ -927,46 +927,12 @@ export class SCMAccessibilityProvider implements IListAccessibilityProvider; - dispose(): void; -} - -interface IRepositoryItem { - readonly element: ISCMRepository; - readonly groupItems: IGroupItem[]; - dispose(): void; -} - -interface ITreeViewState { - readonly collapsed: string[]; -} - -function isRepositoryItem(item: IRepositoryItem | IGroupItem): item is IRepositoryItem { - return Array.isArray((item as IRepositoryItem).groupItems); -} - -function asTreeElement(node: IResourceNode, forceIncompressible: boolean, viewState?: ITreeViewState): ICompressedTreeElement { - const element = (node.childrenCount === 0 && node.element) ? node.element : node; - const collapsed = viewState ? viewState.collapsed.indexOf(getSCMResourceId(element)) > -1 : false; - - return { - element, - children: Iterable.map(node.children, node => asTreeElement(node, false, viewState)), - incompressible: !!node.element || forceIncompressible, - collapsed, - collapsible: node.childrenCount > 0 - }; -} - -const enum ViewModelMode { +const enum ViewMode { List = 'list', Tree = 'tree' } -const enum ViewModelSortKey { +const enum ViewSortKey { Path = 'path', Name = 'name', Status = 'status' @@ -978,10 +944,10 @@ const Menus = { }; const ContextKeys = { - ViewModelMode: new RawContextKey('scmViewModelMode', ViewModelMode.List), - ViewModelSortKey: new RawContextKey('scmViewModelSortKey', ViewModelSortKey.Path), - ViewModelAreAllRepositoriesCollapsed: new RawContextKey('scmViewModelAreAllRepositoriesCollapsed', false), - ViewModelIsAnyRepositoryCollapsible: new RawContextKey('scmViewModelIsAnyRepositoryCollapsible', false), + SCMViewMode: new RawContextKey('scmViewMode', ViewMode.List), + SCMViewSortKey: new RawContextKey('scmViewSortKey', ViewSortKey.Path), + SCMViewAreAllRepositoriesCollapsed: new RawContextKey('scmViewAreAllRepositoriesCollapsed', false), + SCMViewIsAnyRepositoryCollapsible: new RawContextKey('scmViewIsAnyRepositoryCollapsible', false), SCMProvider: new RawContextKey('scmProvider', undefined), SCMProviderRootUri: new RawContextKey('scmProviderRootUri', undefined), SCMProviderHasRootUri: new RawContextKey('scmProviderHasRootUri', undefined), @@ -1035,18 +1001,29 @@ interface RepositoryVisibilityItem { class RepositoryVisibilityActionController { + private alwaysShowRepositories = false; private items = new Map(); private repositoryCountContextKey: IContextKey; private repositoryVisibilityCountContextKey: IContextKey; + private scmProviderContextKey: IContextKey; + private scmProviderRootUriContextKey: IContextKey; + private scmProviderHasRootUriContextKey: IContextKey; private readonly disposables = new DisposableStore(); constructor( - @ISCMViewService private scmViewService: ISCMViewService, - @ISCMService scmService: ISCMService, - @IContextKeyService private contextKeyService: IContextKeyService + @IContextKeyService private contextKeyService: IContextKeyService, + @ISCMViewService private readonly scmViewService: ISCMViewService, + @IConfigurationService configurationService: IConfigurationService, + @ISCMService scmService: ISCMService ) { this.repositoryCountContextKey = ContextKeys.RepositoryCount.bindTo(contextKeyService); this.repositoryVisibilityCountContextKey = ContextKeys.RepositoryVisibilityCount.bindTo(contextKeyService); + this.scmProviderContextKey = ContextKeys.SCMProvider.bindTo(contextKeyService); + this.scmProviderRootUriContextKey = ContextKeys.SCMProviderRootUri.bindTo(contextKeyService); + this.scmProviderHasRootUriContextKey = ContextKeys.SCMProviderHasRootUri.bindTo(contextKeyService); + + configurationService.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables); + this.onDidChangeConfiguration(); scmViewService.onDidChangeVisibleRepositories(this.onDidChangeVisibleRepositories, this, this.disposables); scmService.onDidAddRepository(this.onDidAddRepository, this, this.disposables); @@ -1075,13 +1052,13 @@ class RepositoryVisibilityActionController { } }); - this.updateRepositoriesCounts(); + this.updateRepositoryContextKeys(); } private onDidRemoveRepository(repository: ISCMRepository): void { this.items.get(repository)?.dispose(); this.items.delete(repository); - this.updateRepositoriesCounts(); + this.updateRepositoryContextKeys(); } private onDidChangeVisibleRepositories(): void { @@ -1100,286 +1077,19 @@ class RepositoryVisibilityActionController { this.repositoryVisibilityCountContextKey.set(count); } - private updateRepositoriesCounts(): void { + private onDidChangeConfiguration(e?: IConfigurationChangeEvent): void { + if (!e || e.affectsConfiguration('scm.alwaysShowRepositories')) { + this.alwaysShowRepositories = this.contextKeyService.getContextKeyValue('scm.alwaysShowRepositories') === true; + this.updateRepositoryContextKeys(); + } + } + + private updateRepositoryContextKeys(): void { this.repositoryCountContextKey.set(this.items.size); this.repositoryVisibilityCountContextKey.set(Iterable.reduce(this.items.keys(), (r, repository) => r + (this.scmViewService.isVisible(repository) ? 1 : 0), 0)); - } - dispose(): void { - this.disposables.dispose(); - dispose(this.items.values()); - this.items.clear(); - } -} - -class ViewModel { - - private readonly _onDidChangeMode = new Emitter(); - readonly onDidChangeMode = this._onDidChangeMode.event; - - private readonly _onDidChangeSortKey = new Emitter(); - readonly onDidChangeSortKey = this._onDidChangeSortKey.event; - - private visible: boolean = false; - - get mode(): ViewModelMode { return this._mode; } - set mode(mode: ViewModelMode) { - if (this._mode === mode) { - return; - } - - this._mode = mode; - - for (const [, item] of this.items) { - for (const groupItem of item.groupItems) { - groupItem.tree.clear(); - - if (mode === ViewModelMode.Tree) { - for (const resource of groupItem.resources) { - groupItem.tree.add(resource.sourceUri, resource); - } - } - } - } - - // Update sort key based on view mode - this.sortKey = this.getViewModelSortKey(); - - this.refresh(); - this._onDidChangeMode.fire(mode); - this.modeContextKey.set(mode); - - this.storageService.store(`scm.viewMode`, mode, StorageScope.WORKSPACE, StorageTarget.USER); - } - - get sortKey(): ViewModelSortKey { return this._sortKey; } - set sortKey(sortKey: ViewModelSortKey) { - if (this._sortKey === sortKey) { - return; - } - - this._sortKey = sortKey; - - this.refresh(); - this._onDidChangeSortKey.fire(sortKey); - this.sortKeyContextKey.set(sortKey); - - if (this._mode === ViewModelMode.List) { - this.storageService.store(`scm.viewSortKey`, sortKey, StorageScope.WORKSPACE, StorageTarget.USER); - } - } - - private _treeViewStateIsStale = false; - get treeViewState(): ITreeViewState | undefined { - if (this.visible && this._treeViewStateIsStale) { - this.updateViewState(); - this._treeViewStateIsStale = false; - } - - return this._treeViewState; - } - - private items = new Map(); - private readonly visibilityDisposables = new DisposableStore(); - private scrollTop: number | undefined; - private alwaysShowRepositories = false; - private showActionButton = false; - private firstVisible = true; - private readonly disposables = new DisposableStore(); - - private modeContextKey: IContextKey; - private sortKeyContextKey: IContextKey; - private areAllRepositoriesCollapsedContextKey: IContextKey; - private isAnyRepositoryCollapsibleContextKey: IContextKey; - private scmProviderContextKey: IContextKey; - private scmProviderRootUriContextKey: IContextKey; - private scmProviderHasRootUriContextKey: IContextKey; - - private _mode: ViewModelMode; - private _sortKey: ViewModelSortKey; - private _treeViewState: ITreeViewState | undefined; - - constructor( - private tree: WorkbenchCompressibleObjectTree, - private inputRenderer: InputRenderer, - @IInstantiationService protected instantiationService: IInstantiationService, - @IEditorService protected editorService: IEditorService, - @IConfigurationService protected configurationService: IConfigurationService, - @ISCMViewService private scmViewService: ISCMViewService, - @IStorageService private storageService: IStorageService, - @IUriIdentityService private uriIdentityService: IUriIdentityService, - @IContextKeyService contextKeyService: IContextKeyService - ) { - // View mode and sort key - this._mode = this.getViewModelMode(); - this._sortKey = this.getViewModelSortKey(); - - // TreeView state - const storageViewState = this.storageService.get(`scm.viewState`, StorageScope.WORKSPACE); - if (storageViewState) { - try { - this._treeViewState = JSON.parse(storageViewState); - } catch {/* noop */ } - } - - this.modeContextKey = ContextKeys.ViewModelMode.bindTo(contextKeyService); - this.modeContextKey.set(this._mode); - this.sortKeyContextKey = ContextKeys.ViewModelSortKey.bindTo(contextKeyService); - this.sortKeyContextKey.set(this._sortKey); - this.areAllRepositoriesCollapsedContextKey = ContextKeys.ViewModelAreAllRepositoriesCollapsed.bindTo(contextKeyService); - this.isAnyRepositoryCollapsibleContextKey = ContextKeys.ViewModelIsAnyRepositoryCollapsible.bindTo(contextKeyService); - this.scmProviderContextKey = ContextKeys.SCMProvider.bindTo(contextKeyService); - this.scmProviderRootUriContextKey = ContextKeys.SCMProviderRootUri.bindTo(contextKeyService); - this.scmProviderHasRootUriContextKey = ContextKeys.SCMProviderHasRootUri.bindTo(contextKeyService); - - configurationService.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables); - this.onDidChangeConfiguration(); - - Event.filter(this.tree.onDidChangeCollapseState, e => isSCMRepository(e.node.element), this.disposables) - (this.updateRepositoryCollapseAllContextKeys, this, this.disposables); - - this.disposables.add(this.tree.onDidChangeCollapseState(() => this._treeViewStateIsStale = true)); - - this.disposables.add(this.storageService.onWillSaveState(e => { - if (e.reason === WillSaveStateReason.SHUTDOWN) { - this.storageService.store(`scm.viewState`, JSON.stringify(this.treeViewState), StorageScope.WORKSPACE, StorageTarget.MACHINE); - } - - this.mode = this.getViewModelMode(); - this.sortKey = this.getViewModelSortKey(); - })); - - this.disposables.add(this.storageService.onDidChangeValue(StorageScope.WORKSPACE, undefined, this.disposables)(e => { - switch (e.key) { - case 'scm.viewMode': - this.mode = this.getViewModelMode(); - break; - case 'scm.viewSortKey': - this.sortKey = this.getViewModelSortKey(); - break; - } - })); - } - - private onDidChangeConfiguration(e?: IConfigurationChangeEvent): void { - if (!e || e.affectsConfiguration('scm.alwaysShowRepositories') || e.affectsConfiguration('scm.showActionButton')) { - this.alwaysShowRepositories = this.configurationService.getValue('scm.alwaysShowRepositories'); - this.showActionButton = this.configurationService.getValue('scm.showActionButton'); - this.refresh(); - } - } - - private _onDidChangeVisibleRepositories({ added, removed }: ISCMViewVisibleRepositoryChangeEvent): void { - for (const repository of added) { - const disposable = combinedDisposable( - repository.provider.groups.onDidSplice(splice => this._onDidSpliceGroups(item, splice)), - repository.input.onDidChangeActionButton(() => this.refresh(item)), - repository.input.onDidChangeVisibility(() => this.refresh(item)), - repository.provider.onDidChange(() => { - if (this.showActionButton) { - this.refresh(item); - } - }) - ); - const groupItems = repository.provider.groups.elements.map(group => this.createGroupItem(group)); - const item: IRepositoryItem = { - element: repository, groupItems, dispose() { - dispose(this.groupItems); - disposable.dispose(); - } - }; - - this.items.set(repository, item); - } - - for (const repository of removed) { - const item = this.items.get(repository)!; - item.dispose(); - this.items.delete(repository); - } - - this.refresh(); - } - - private _onDidSpliceGroups(item: IRepositoryItem, { start, deleteCount, toInsert }: ISplice): void { - const itemsToInsert: IGroupItem[] = toInsert.map(group => this.createGroupItem(group)); - const itemsToDispose = item.groupItems.splice(start, deleteCount, ...itemsToInsert); - - for (const item of itemsToDispose) { - item.dispose(); - } - - this.refresh(); - } - - private createGroupItem(group: ISCMResourceGroup): IGroupItem { - const tree = new ResourceTree(group, group.provider.rootUri || URI.file('/'), this.uriIdentityService.extUri); - const resources: ISCMResource[] = [...group.elements]; - const disposable = combinedDisposable( - group.onDidChange(() => this.tree.refilter()), - group.onDidSplice(splice => this._onDidSpliceGroup(item, splice)) - ); - - const item: IGroupItem = { element: group, resources, tree, dispose() { disposable.dispose(); } }; - - if (this._mode === ViewModelMode.Tree) { - for (const resource of resources) { - item.tree.add(resource.sourceUri, resource); - } - } - - return item; - } - - private _onDidSpliceGroup(item: IGroupItem, { start, deleteCount, toInsert }: ISplice): void { - const before = item.resources.length; - const deleted = item.resources.splice(start, deleteCount, ...toInsert); - const after = item.resources.length; - - if (this._mode === ViewModelMode.Tree) { - for (const resource of deleted) { - item.tree.delete(resource.sourceUri); - } - - for (const resource of toInsert) { - item.tree.add(resource.sourceUri, resource); - } - } - - if (before !== after && (before === 0 || after === 0)) { - this.refresh(); - } else { - this.refresh(item); - } - } - - setVisible(visible: boolean): void { - if (visible) { - this.scmViewService.onDidChangeVisibleRepositories(this._onDidChangeVisibleRepositories, this, this.visibilityDisposables); - this._onDidChangeVisibleRepositories({ added: this.scmViewService.visibleRepositories, removed: Iterable.empty() }); - - if (typeof this.scrollTop === 'number') { - this.tree.scrollTop = this.scrollTop; - this.scrollTop = undefined; - } - - this.editorService.onDidActiveEditorChange(this.onDidActiveEditorChange, this, this.visibilityDisposables); - this.onDidActiveEditorChange(); - } else { - this.updateViewState(); - - this.visibilityDisposables.clear(); - this._onDidChangeVisibleRepositories({ added: Iterable.empty(), removed: [...this.items.keys()] }); - this.scrollTop = this.tree.scrollTop; - } - - this.visible = visible; - this.updateRepositoryCollapseAllContextKeys(); - } - - private refresh(item?: IRepositoryItem | IGroupItem): void { if (!this.alwaysShowRepositories && this.items.size === 1) { - const provider = Iterable.first(this.items.values())!.element.provider; + const provider = Iterable.first(this.items.keys())!.provider; this.scmProviderContextKey.set(provider.contextValue); this.scmProviderRootUriContextKey.set(provider.rootUri?.toString()); this.scmProviderHasRootUriContextKey.set(!!provider.rootUri); @@ -1388,207 +1098,9 @@ class ViewModel { this.scmProviderRootUriContextKey.set(undefined); this.scmProviderHasRootUriContextKey.set(false); } - - const focusedInput = this.inputRenderer.getFocusedInput(); - - if (!this.alwaysShowRepositories && (this.items.size === 1 && (!item || isRepositoryItem(item)))) { - const item = Iterable.first(this.items.values())!; - this.tree.setChildren(null, this.render(item, this.treeViewState).children); - } else if (item) { - this.tree.setChildren(item.element, this.render(item, this.treeViewState).children); - } else { - const items = coalesce(this.scmViewService.visibleRepositories.map(r => this.items.get(r))); - this.tree.setChildren(null, items.map(item => this.render(item, this.treeViewState))); - } - - if (focusedInput) { - this.inputRenderer.getRenderedInputWidget(focusedInput)?.focus(); - } - - this.updateRepositoryCollapseAllContextKeys(); - } - - private render(item: IRepositoryItem | IGroupItem, treeViewState?: ITreeViewState): ICompressedTreeElement { - if (isRepositoryItem(item)) { - const children: ICompressedTreeElement[] = []; - const hasSomeChanges = item.groupItems.some(item => item.element.elements.length > 0); - - if (item.element.input.visible) { - children.push({ element: item.element.input, incompressible: true, collapsible: false }); - } - - if (hasSomeChanges || (this.items.size === 1 && (!this.showActionButton || !item.element.provider.actionButton))) { - children.push(...item.groupItems.map(i => this.render(i, treeViewState))); - } - - if (this.showActionButton && item.element.provider.actionButton) { - const button: ICompressedTreeElement = { - element: { - type: 'actionButton', - repository: item.element, - button: item.element.provider.actionButton, - }, - incompressible: true, - collapsible: false - }; - children.push(button); - } - - const collapsed = treeViewState ? treeViewState.collapsed.indexOf(getSCMResourceId(item.element)) > -1 : false; - - return { element: item.element, children, incompressible: true, collapsed, collapsible: true }; - } else { - const children = this.mode === ViewModelMode.List - ? Iterable.map(item.resources, element => ({ element, incompressible: true })) - : Iterable.map(item.tree.root.children, node => asTreeElement(node, true, treeViewState)); - - const collapsed = treeViewState ? treeViewState.collapsed.indexOf(getSCMResourceId(item.element)) > -1 : false; - - return { element: item.element, children, incompressible: true, collapsed, collapsible: true }; - } - } - - private updateViewState(): void { - const collapsed: string[] = []; - const visit = (node: ITreeNode) => { - if (node.element && node.collapsible && node.collapsed) { - collapsed.push(getSCMResourceId(node.element)); - } - - for (const child of node.children) { - visit(child); - } - }; - - visit(this.tree.getNode()); - - this._treeViewState = { collapsed }; - } - - private onDidActiveEditorChange(): void { - if (!this.configurationService.getValue('scm.autoReveal')) { - return; - } - - if (this.firstVisible) { - this.firstVisible = false; - this.visibilityDisposables.add(disposableTimeout(() => this.onDidActiveEditorChange(), 250)); - return; - } - - const uri = EditorResourceAccessor.getOriginalUri(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY }); - - if (!uri) { - return; - } - - for (const repository of this.scmViewService.visibleRepositories) { - const item = this.items.get(repository); - - if (!item) { - continue; - } - - // go backwards from last group - for (let j = item.groupItems.length - 1; j >= 0; j--) { - const groupItem = item.groupItems[j]; - const resource = this.mode === ViewModelMode.Tree - ? groupItem.tree.getNode(uri)?.element - : groupItem.resources.find(r => this.uriIdentityService.extUri.isEqual(r.sourceUri, uri)); - - if (resource) { - this.tree.reveal(resource); - this.tree.setSelection([resource]); - this.tree.setFocus([resource]); - return; - } - } - } - } - - focus() { - if (this.tree.getFocus().length === 0) { - for (const repository of this.scmViewService.visibleRepositories) { - const widget = this.inputRenderer.getRenderedInputWidget(repository.input); - - if (widget) { - widget.focus(); - return; - } - } - } - - this.tree.domFocus(); - } - - private updateRepositoryCollapseAllContextKeys(): void { - if (!this.visible || this.scmViewService.visibleRepositories.length === 1) { - this.isAnyRepositoryCollapsibleContextKey.set(false); - this.areAllRepositoriesCollapsedContextKey.set(false); - return; - } - - this.isAnyRepositoryCollapsibleContextKey.set(this.scmViewService.visibleRepositories.some(r => this.tree.hasElement(r) && this.tree.isCollapsible(r))); - this.areAllRepositoriesCollapsedContextKey.set(this.scmViewService.visibleRepositories.every(r => this.tree.hasElement(r) && (!this.tree.isCollapsible(r) || this.tree.isCollapsed(r)))); - } - - collapseAllRepositories(): void { - for (const repository of this.scmViewService.visibleRepositories) { - if (this.tree.isCollapsible(repository)) { - this.tree.collapse(repository); - } - } - } - - expandAllRepositories(): void { - for (const repository of this.scmViewService.visibleRepositories) { - if (this.tree.isCollapsible(repository)) { - this.tree.expand(repository); - } - } - } - - private getViewModelMode(): ViewModelMode { - let mode = this.configurationService.getValue<'tree' | 'list'>('scm.defaultViewMode') === 'list' ? ViewModelMode.List : ViewModelMode.Tree; - const storageMode = this.storageService.get(`scm.viewMode`, StorageScope.WORKSPACE) as ViewModelMode; - if (typeof storageMode === 'string') { - mode = storageMode; - } - - return mode; - } - - private getViewModelSortKey(): ViewModelSortKey { - // Tree - if (this._mode === ViewModelMode.Tree) { - return ViewModelSortKey.Path; - } - - // List - let viewSortKey: ViewModelSortKey; - const viewSortKeyString = this.configurationService.getValue<'path' | 'name' | 'status'>('scm.defaultViewSortKey'); - switch (viewSortKeyString) { - case 'name': - viewSortKey = ViewModelSortKey.Name; - break; - case 'status': - viewSortKey = ViewModelSortKey.Status; - break; - default: - viewSortKey = ViewModelSortKey.Path; - break; - } - - const storageSortKey = this.storageService.get(`scm.viewSortKey`, StorageScope.WORKSPACE) as ViewModelSortKey; - if (typeof storageSortKey === 'string') { - viewSortKey = storageSortKey; - } - - return viewSortKey; } dispose(): void { - this.visibilityDisposables.dispose(); this.disposables.dispose(); dispose(this.items.values()); this.items.clear(); @@ -1603,13 +1115,13 @@ class SetListViewModeAction extends ViewAction { viewId: VIEW_PANE_ID, f1: false, icon: Codicon.listTree, - toggled: ContextKeys.ViewModelMode.isEqualTo(ViewModelMode.List), + toggled: ContextKeys.SCMViewMode.isEqualTo(ViewMode.List), menu: { id: Menus.ViewSort, group: '1_viewmode', ...menu } }); } async runInView(_: ServicesAccessor, view: SCMViewPane): Promise { - view.viewModel.mode = ViewModelMode.List; + view.viewMode = ViewMode.List; } } @@ -1617,7 +1129,7 @@ class SetListViewModeNavigationAction extends SetListViewModeAction { constructor() { super({ id: MenuId.SCMTitle, - when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_PANE_ID), ContextKeys.RepositoryCount.notEqualsTo(0), ContextKeys.ViewModelMode.isEqualTo(ViewModelMode.Tree)), + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_PANE_ID), ContextKeys.RepositoryCount.notEqualsTo(0), ContextKeys.SCMViewMode.isEqualTo(ViewMode.Tree)), group: 'navigation', order: -1000 }); @@ -1632,13 +1144,13 @@ class SetTreeViewModeAction extends ViewAction { viewId: VIEW_PANE_ID, f1: false, icon: Codicon.listFlat, - toggled: ContextKeys.ViewModelMode.isEqualTo(ViewModelMode.Tree), + toggled: ContextKeys.SCMViewMode.isEqualTo(ViewMode.Tree), menu: { id: Menus.ViewSort, group: '1_viewmode', ...menu } }); } async runInView(_: ServicesAccessor, view: SCMViewPane): Promise { - view.viewModel.mode = ViewModelMode.Tree; + view.viewMode = ViewMode.Tree; } } @@ -1646,7 +1158,7 @@ class SetTreeViewModeNavigationAction extends SetTreeViewModeAction { constructor() { super({ id: MenuId.SCMTitle, - when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_PANE_ID), ContextKeys.RepositoryCount.notEqualsTo(0), ContextKeys.ViewModelMode.isEqualTo(ViewModelMode.List)), + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_PANE_ID), ContextKeys.RepositoryCount.notEqualsTo(0), ContextKeys.SCMViewMode.isEqualTo(ViewMode.List)), group: 'navigation', order: -1000 }); @@ -1709,38 +1221,38 @@ registerAction2(RepositorySortByNameAction); registerAction2(RepositorySortByPathAction); abstract class SetSortKeyAction extends ViewAction { - constructor(private sortKey: ViewModelSortKey, title: string) { + constructor(private sortKey: ViewSortKey, title: string) { super({ id: `workbench.scm.action.setSortKey.${sortKey}`, title, viewId: VIEW_PANE_ID, f1: false, - toggled: ContextKeys.ViewModelSortKey.isEqualTo(sortKey), - precondition: ContextKeys.ViewModelMode.isEqualTo(ViewModelMode.List), + toggled: ContextKeys.SCMViewSortKey.isEqualTo(sortKey), + precondition: ContextKeys.SCMViewMode.isEqualTo(ViewMode.List), menu: { id: Menus.ViewSort, group: '2_sort' } }); } async runInView(_: ServicesAccessor, view: SCMViewPane): Promise { - view.viewModel.sortKey = this.sortKey; + view.viewSortKey = this.sortKey; } } class SetSortByNameAction extends SetSortKeyAction { constructor() { - super(ViewModelSortKey.Name, localize('sortChangesByName', "Sort Changes by Name")); + super(ViewSortKey.Name, localize('sortChangesByName', "Sort Changes by Name")); } } class SetSortByPathAction extends SetSortKeyAction { constructor() { - super(ViewModelSortKey.Path, localize('sortChangesByPath', "Sort Changes by Path")); + super(ViewSortKey.Path, localize('sortChangesByPath', "Sort Changes by Path")); } } class SetSortByStatusAction extends SetSortKeyAction { constructor() { - super(ViewModelSortKey.Status, localize('sortChangesByStatus', "Sort Changes by Status")); + super(ViewSortKey.Status, localize('sortChangesByStatus', "Sort Changes by Status")); } } @@ -1760,13 +1272,13 @@ class CollapseAllRepositoriesAction extends ViewAction { menu: { id: MenuId.SCMTitle, group: 'navigation', - when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_PANE_ID), ContextKeys.ViewModelIsAnyRepositoryCollapsible.isEqualTo(true), ContextKeys.ViewModelAreAllRepositoriesCollapsed.isEqualTo(false)) + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_PANE_ID), ContextKeys.SCMViewIsAnyRepositoryCollapsible.isEqualTo(true), ContextKeys.SCMViewAreAllRepositoriesCollapsed.isEqualTo(false)) } }); } async runInView(_: ServicesAccessor, view: SCMViewPane): Promise { - view.viewModel.collapseAllRepositories(); + view.collapseAllRepositories(); } } @@ -1782,13 +1294,13 @@ class ExpandAllRepositoriesAction extends ViewAction { menu: { id: MenuId.SCMTitle, group: 'navigation', - when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_PANE_ID), ContextKeys.ViewModelIsAnyRepositoryCollapsible.isEqualTo(true), ContextKeys.ViewModelAreAllRepositoriesCollapsed.isEqualTo(true)) + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_PANE_ID), ContextKeys.SCMViewIsAnyRepositoryCollapsible.isEqualTo(true), ContextKeys.SCMViewAreAllRepositoriesCollapsed.isEqualTo(true)) } }); } async runInView(_: ServicesAccessor, view: SCMViewPane): Promise { - view.viewModel.expandAllRepositories(); + view.expandAllRepositories(); } } @@ -2343,148 +1855,132 @@ export class SCMViewPane extends ViewPane { private _onDidLayout: Emitter; private layoutCache: ISCMLayout; - private listContainer!: HTMLElement; - private tree!: WorkbenchCompressibleObjectTree; - private _viewModel!: ViewModel; - get viewModel(): ViewModel { return this._viewModel; } + private treeScrollTop: number | undefined; + private treeContainer!: HTMLElement; + private tree!: WorkbenchCompressibleAsyncDataTree; + private listLabels!: ResourceLabels; private inputRenderer!: InputRenderer; private actionButtonRenderer!: ActionButtonRenderer; + + private _viewMode: ViewMode; + get viewMode(): ViewMode { return this._viewMode; } + set viewMode(mode: ViewMode) { + if (this._viewMode === mode) { + return; + } + + this._viewMode = mode; + + // Update sort key based on view mode + this.viewSortKey = this.getViewSortKey(); + + this.updateChildren(); + this.onDidActiveEditorChange(); + this._onDidChangeViewMode.fire(mode); + this.viewModeContextKey.set(mode); + + this.updateIndentStyles(this.themeService.getFileIconTheme()); + this.storageService.store(`scm.viewMode`, mode, StorageScope.WORKSPACE, StorageTarget.USER); + } + + private readonly _onDidChangeViewMode = new Emitter(); + readonly onDidChangeViewMode = this._onDidChangeViewMode.event; + + private _viewSortKey: ViewSortKey; + get viewSortKey(): ViewSortKey { return this._viewSortKey; } + set viewSortKey(sortKey: ViewSortKey) { + if (this._viewSortKey === sortKey) { + return; + } + + this._viewSortKey = sortKey; + + this.updateChildren(); + this.viewSortKeyContextKey.set(sortKey); + this._onDidChangeViewSortKey.fire(sortKey); + + if (this._viewMode === ViewMode.List) { + this.storageService.store(`scm.viewSortKey`, sortKey, StorageScope.WORKSPACE, StorageTarget.USER); + } + } + + private readonly _onDidChangeViewSortKey = new Emitter(); + readonly onDidChangeViewSortKey = this._onDidChangeViewSortKey.event; + + private _showActionButton = false; + get showActionButton(): boolean { return this._showActionButton; } + + private _alwaysShowRepositories = false; + get alwaysShowRepositories(): boolean { return this._alwaysShowRepositories; } + + private readonly items = new DisposableMap(); + private readonly visibilityDisposables = new DisposableStore(); + private readonly asyncOperationSequencer = new Sequencer(); + + private viewModeContextKey: IContextKey; + private viewSortKeyContextKey: IContextKey; + private areAllRepositoriesCollapsedContextKey: IContextKey; + private isAnyRepositoryCollapsibleContextKey: IContextKey; + private readonly disposables = new DisposableStore(); constructor( options: IViewPaneOptions, - @ISCMService private scmService: ISCMService, - @ISCMViewService private scmViewService: ISCMViewService, + @ICommandService private readonly commandService: ICommandService, + @IEditorService private readonly editorService: IEditorService, + @IMenuService private readonly menuService: IMenuService, + @ISCMService private readonly scmService: ISCMService, + @ISCMViewService private readonly scmViewService: ISCMViewService, + @IStorageService private readonly storageService: IStorageService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @IKeybindingService keybindingService: IKeybindingService, @IThemeService themeService: IThemeService, @IContextMenuService contextMenuService: IContextMenuService, - @ICommandService private commandService: ICommandService, - @IEditorService private editorService: IEditorService, @IInstantiationService instantiationService: IInstantiationService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IConfigurationService configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService, - @IMenuService private menuService: IMenuService, @IOpenerService openerService: IOpenerService, @ITelemetryService telemetryService: ITelemetryService, ) { super({ ...options, titleMenuId: MenuId.SCMTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + // View mode and sort key + this._viewMode = this.getViewMode(); + this._viewSortKey = this.getViewSortKey(); + + // Context Keys + this.viewModeContextKey = ContextKeys.SCMViewMode.bindTo(contextKeyService); + this.viewModeContextKey.set(this._viewMode); + this.viewSortKeyContextKey = ContextKeys.SCMViewSortKey.bindTo(contextKeyService); + this.viewSortKeyContextKey.set(this.viewSortKey); + this.areAllRepositoriesCollapsedContextKey = ContextKeys.SCMViewAreAllRepositoriesCollapsed.bindTo(contextKeyService); + this.isAnyRepositoryCollapsibleContextKey = ContextKeys.SCMViewIsAnyRepositoryCollapsible.bindTo(contextKeyService); + this._onDidLayout = new Emitter(); - this.layoutCache = { - height: undefined, - width: undefined, - onDidChange: this._onDidLayout.event - }; + this.layoutCache = { height: undefined, width: undefined, onDidChange: this._onDidLayout.event }; - this._register(this.instantiationService.createInstance(ScmInputContentProvider)); + this.storageService.onDidChangeValue(StorageScope.WORKSPACE, undefined, this.disposables)(e => { + switch (e.key) { + case 'scm.viewMode': + this.viewMode = this.getViewMode(); + break; + case 'scm.viewSortKey': + this.viewSortKey = this.getViewSortKey(); + break; + } + }, this, this.disposables); - this._register(Event.any(this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository)(() => this._onDidChangeViewWelcomeState.fire())); - } + this.storageService.onWillSaveState(e => { + this.viewMode = this.getViewMode(); + this.viewSortKey = this.getViewSortKey(); - protected override renderBody(container: HTMLElement): void { - super.renderBody(container); + this.storeTreeViewState(); + }, this, this.disposables); - // List - this.listContainer = append(container, $('.scm-view.show-file-icons')); - - const overflowWidgetsDomNode = $('.scm-overflow-widgets-container.monaco-editor'); - - const updateActionsVisibility = () => this.listContainer.classList.toggle('show-actions', this.configurationService.getValue('scm.alwaysShowActions')); - this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowActions'), this.disposables)(updateActionsVisibility)); - updateActionsVisibility(); - - const updateProviderCountVisibility = () => { - const value = this.configurationService.getValue<'hidden' | 'auto' | 'visible'>('scm.providerCountBadge'); - this.listContainer.classList.toggle('hide-provider-counts', value === 'hidden'); - this.listContainer.classList.toggle('auto-provider-counts', value === 'auto'); - }; - this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.providerCountBadge'), this.disposables)(updateProviderCountVisibility)); - updateProviderCountVisibility(); - - this.inputRenderer = this.instantiationService.createInstance(InputRenderer, this.layoutCache, overflowWidgetsDomNode, (input, height) => this.tree.updateElementHeight(input, height)); - const delegate = new ListDelegate(this.inputRenderer); - - this.actionButtonRenderer = this.instantiationService.createInstance(ActionButtonRenderer); - - this.listLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility }); - this._register(this.listLabels); - - const actionRunner = new RepositoryPaneActionRunner(() => this.getSelectedResources()); - this._register(actionRunner); - this._register(actionRunner.onWillRun(() => this.tree.domFocus())); - - const renderers: ICompressibleTreeRenderer[] = [ - this.instantiationService.createInstance(RepositoryRenderer, getActionViewItemProvider(this.instantiationService)), - this.inputRenderer, - this.actionButtonRenderer, - this.instantiationService.createInstance(ResourceGroupRenderer, getActionViewItemProvider(this.instantiationService)), - this._register(this.instantiationService.createInstance(ResourceRenderer, () => this._viewModel, this.listLabels, getActionViewItemProvider(this.instantiationService), actionRunner)) - ]; - - const filter = new SCMTreeFilter(); - const sorter = new SCMTreeSorter(() => this._viewModel); - const keyboardNavigationLabelProvider = this.instantiationService.createInstance(SCMTreeKeyboardNavigationLabelProvider, () => this._viewModel); - const identityProvider = new SCMResourceIdentityProvider(); - const dnd = new SCMTreeDragAndDrop(this.instantiationService); - - this.tree = this.instantiationService.createInstance( - WorkbenchCompressibleObjectTree, - 'SCM Tree Repo', - this.listContainer, - delegate, - renderers, - { - transformOptimization: false, - identityProvider, - dnd, - horizontalScrolling: false, - setRowLineHeight: false, - filter, - sorter, - keyboardNavigationLabelProvider, - overrideStyles: { - listBackground: this.viewDescriptorService.getViewLocationById(this.id) === ViewContainerLocation.Panel ? PANEL_BACKGROUND : SIDE_BAR_BACKGROUND - }, - accessibilityProvider: this.instantiationService.createInstance(SCMAccessibilityProvider) - }) as WorkbenchCompressibleObjectTree; - - this._register(this.tree.onDidOpen(this.open, this)); - - this._register(this.tree.onContextMenu(this.onListContextMenu, this)); - this._register(this.tree.onDidScroll(this.inputRenderer.clearValidation, this.inputRenderer)); - this._register(this.tree); - - append(this.listContainer, overflowWidgetsDomNode); - - this._register(this.instantiationService.createInstance(RepositoryVisibilityActionController)); - - this._viewModel = this.instantiationService.createInstance(ViewModel, this.tree, this.inputRenderer); - this._register(this._viewModel); - - this.listContainer.classList.add('file-icon-themable-tree'); - this.listContainer.classList.add('show-file-icons'); - - this.updateIndentStyles(this.themeService.getFileIconTheme()); - this._register(this.themeService.onDidFileIconThemeChange(this.updateIndentStyles, this)); - this._register(this._viewModel.onDidChangeMode(this.onDidChangeMode, this)); - - this._register(this.onDidChangeBodyVisibility(this._viewModel.setVisible, this._viewModel)); - - this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowRepositories'), this.disposables)(this.updateActions, this)); - this.updateActions(); - } - - private updateIndentStyles(theme: IFileIconTheme): void { - this.listContainer.classList.toggle('list-view-mode', this._viewModel.mode === ViewModelMode.List); - this.listContainer.classList.toggle('tree-view-mode', this._viewModel.mode === ViewModelMode.Tree); - this.listContainer.classList.toggle('align-icons-and-twisties', (this._viewModel.mode === ViewModelMode.List && theme.hasFileIcons) || (theme.hasFileIcons && !theme.hasFolderIcons)); - this.listContainer.classList.toggle('hide-arrows', this._viewModel.mode === ViewModelMode.Tree && theme.hidesExplorerArrows === true); - } - - private onDidChangeMode(): void { - this.updateIndentStyles(this.themeService.getFileIconTheme()); + this.disposables.add(this.instantiationService.createInstance(ScmInputContentProvider)); + Event.any(this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository)(() => this._onDidChangeViewWelcomeState.fire(), this, this.disposables); } protected override layoutBody(height: number | undefined = this.layoutCache.height, width: number | undefined = this.layoutCache.width): void { @@ -2500,16 +1996,130 @@ export class SCMViewPane extends ViewPane { this.layoutCache.width = width; this._onDidLayout.fire(); - this.listContainer.style.height = `${height}px`; + this.treeContainer.style.height = `${height}px`; this.tree.layout(height, width); } - override focus(): void { - super.focus(); + protected override renderBody(container: HTMLElement): void { + super.renderBody(container); - if (this.isExpanded()) { - this._viewModel.focus(); - } + // Tree + this.treeContainer = append(container, $('.scm-view.show-file-icons')); + this.treeContainer.classList.add('file-icon-themable-tree'); + this.treeContainer.classList.add('show-file-icons'); + + const updateActionsVisibility = () => this.treeContainer.classList.toggle('show-actions', this.configurationService.getValue('scm.alwaysShowActions')); + Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowActions'), this.disposables)(updateActionsVisibility, this, this.disposables); + updateActionsVisibility(); + + const updateProviderCountVisibility = () => { + const value = this.configurationService.getValue<'hidden' | 'auto' | 'visible'>('scm.providerCountBadge'); + this.treeContainer.classList.toggle('hide-provider-counts', value === 'hidden'); + this.treeContainer.classList.toggle('auto-provider-counts', value === 'auto'); + }; + Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.providerCountBadge'), this.disposables)(updateProviderCountVisibility, this, this.disposables); + updateProviderCountVisibility(); + + this.createTree(this.treeContainer); + + this.onDidChangeBodyVisibility(async visible => { + if (visible) { + await this.tree.setInput(this.scmViewService, this.loadTreeViewState()); + + const updateActionButtonVisibility = () => { + this._showActionButton = this.configurationService.getValue('scm.showActionButton'); + this.updateChildren(); + }; + Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.showActionButton'), this.visibilityDisposables)(updateActionButtonVisibility, this, this.visibilityDisposables); + updateActionButtonVisibility(); + + const updateRepositoryVisibility = () => { + this._alwaysShowRepositories = this.configurationService.getValue('scm.alwaysShowRepositories'); + this.updateChildren(); + this.updateActions(); + }; + Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('scm.alwaysShowRepositories'), this.visibilityDisposables)(updateRepositoryVisibility, this, this.visibilityDisposables); + updateRepositoryVisibility(); + + // Add visible repositories + this.scmViewService.onDidChangeVisibleRepositories(this.onDidChangeVisibleRepositories, this, this.visibilityDisposables); + this.onDidChangeVisibleRepositories({ added: this.scmViewService.visibleRepositories, removed: Iterable.empty() }); + + // Select resource for active editor + this.editorService.onDidActiveEditorChange(this.onDidActiveEditorChange, this, this.visibilityDisposables); + this.onDidActiveEditorChange(); + + // Restore scroll position + if (typeof this.treeScrollTop === 'number') { + this.tree.scrollTop = this.treeScrollTop; + this.treeScrollTop = undefined; + } + } else { + this.visibilityDisposables.clear(); + this.onDidChangeVisibleRepositories({ added: Iterable.empty(), removed: [...this.items.keys()] }); + this.treeScrollTop = this.tree.scrollTop; + } + + this.updateRepositoryCollapseAllContextKeys(); + }, this, this.disposables); + + this.disposables.add(this.instantiationService.createInstance(RepositoryVisibilityActionController)); + + this.themeService.onDidFileIconThemeChange(this.updateIndentStyles, this, this.disposables); + this.updateIndentStyles(this.themeService.getFileIconTheme()); + } + + private createTree(container: HTMLElement): void { + const overflowWidgetsDomNode = $('.scm-overflow-widgets-container.monaco-editor'); + + this.inputRenderer = this.instantiationService.createInstance(InputRenderer, this.layoutCache, overflowWidgetsDomNode, (input, height) => { this.tree.updateElementHeight(input, height); }); + this.actionButtonRenderer = this.instantiationService.createInstance(ActionButtonRenderer); + + this.listLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility }); + this.disposables.add(this.listLabels); + + const actionRunner = new RepositoryPaneActionRunner(() => this.getSelectedResources()); + actionRunner.onWillRun(() => this.tree.domFocus(), this, this.disposables); + this.disposables.add(actionRunner); + + this.tree = this.instantiationService.createInstance( + WorkbenchCompressibleAsyncDataTree, + 'SCM Tree Repo', + container, + new ListDelegate(this.inputRenderer), + new SCMTreeCompressionDelegate(), + [ + this.inputRenderer, + this.actionButtonRenderer, + this.instantiationService.createInstance(RepositoryRenderer, getActionViewItemProvider(this.instantiationService)), + this.instantiationService.createInstance(ResourceGroupRenderer, getActionViewItemProvider(this.instantiationService)), + this.instantiationService.createInstance(ResourceRenderer, () => this.viewMode, this.listLabels, getActionViewItemProvider(this.instantiationService), actionRunner) + ], + this.instantiationService.createInstance(SCMTreeDataSource, () => this.viewMode, () => this.alwaysShowRepositories, () => this.showActionButton), + { + horizontalScrolling: false, + setRowLineHeight: false, + transformOptimization: false, + filter: new SCMTreeFilter(), + dnd: new SCMTreeDragAndDrop(this.instantiationService), + identityProvider: new SCMResourceIdentityProvider(), + sorter: new SCMTreeSorter(() => this.viewMode, () => this.viewSortKey), + keyboardNavigationLabelProvider: this.instantiationService.createInstance(SCMTreeKeyboardNavigationLabelProvider, () => this.viewMode), + overrideStyles: { + listBackground: this.viewDescriptorService.getViewLocationById(this.id) === ViewContainerLocation.Panel ? PANEL_BACKGROUND : SIDE_BAR_BACKGROUND + }, + collapseByDefault: (e) => false, + accessibilityProvider: this.instantiationService.createInstance(SCMAccessibilityProvider) + }) as WorkbenchCompressibleAsyncDataTree; + + this.disposables.add(this.tree); + + this.tree.onDidOpen(this.open, this, this.disposables); + this.tree.onContextMenu(this.onListContextMenu, this, this.disposables); + this.tree.onDidScroll(this.inputRenderer.clearValidation, this.inputRenderer, this.disposables); + Event.filter(this.tree.onDidChangeCollapseState, e => isSCMRepository(e.node.element?.element), this.disposables)(this.updateRepositoryCollapseAllContextKeys, this, this.disposables); + + append(container, overflowWidgetsDomNode); } private async open(e: IOpenEvent): Promise { @@ -2580,6 +2190,94 @@ export class SCMViewPane extends ViewPane { } } + private onDidActiveEditorChange(): void { + if (!this.configurationService.getValue('scm.autoReveal')) { + return; + } + + const uri = EditorResourceAccessor.getOriginalUri(this.editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY }); + + if (!uri) { + return; + } + + this.asyncOperationSequencer.queue(async () => { + for (const repository of this.scmViewService.visibleRepositories) { + const item = this.items.get(repository); + + if (!item) { + continue; + } + + // go backwards from last group + for (let j = repository.provider.groups.length - 1; j >= 0; j--) { + const groupItem = repository.provider.groups[j]; + const resource = this.viewMode === ViewMode.Tree + ? groupItem.resourceTree.getNode(uri)?.element + : groupItem.resources.find(r => this.uriIdentityService.extUri.isEqual(r.sourceUri, uri)); + + if (resource) { + await this.tree.expandTo(resource); + this.tree.setSelection([resource]); + this.tree.setFocus([resource]); + return; + } + } + } + }); + } + + private onDidChangeVisibleRepositories({ added, removed }: ISCMViewVisibleRepositoryChangeEvent): void { + // Added repositories + for (const repository of added) { + const repositoryDisposables = new DisposableStore(); + + repositoryDisposables.add(repository.provider.onDidChange(() => this.updateChildren())); + repositoryDisposables.add(repository.input.onDidChangeActionButton(() => this.updateChildren())); + repositoryDisposables.add(repository.input.onDidChangeVisibility(() => this.updateChildren())); + repositoryDisposables.add(repository.provider.onDidChangeResourceGroups(() => { + this.updateChildren(); + this.onDidActiveEditorChange(); + })); + + const resourceGroupDisposables = repositoryDisposables.add(new DisposableMap()); + + const onDidChangeResourceGroups = () => { + for (const [resourceGroup] of resourceGroupDisposables) { + if (!repository.provider.groups.includes(resourceGroup)) { + resourceGroupDisposables.deleteAndDispose(resourceGroup); + } + } + + for (const resourceGroup of repository.provider.groups) { + if (!resourceGroupDisposables.has(resourceGroup)) { + const disposableStore = new DisposableStore(); + + disposableStore.add(resourceGroup.onDidChange(() => this.updateChildren())); + disposableStore.add(resourceGroup.onDidChangeResources(() => { + this.updateChildren(); + this.onDidActiveEditorChange(); + })); + resourceGroupDisposables.set(resourceGroup, disposableStore); + } + } + }; + + repositoryDisposables.add(repository.provider.onDidChangeResourceGroups(onDidChangeResourceGroups)); + onDidChangeResourceGroups(); + + this.items.set(repository, repositoryDisposables); + } + + // Removed repositories + for (const repository of removed) { + this.items.deleteAndDispose(repository); + } + + this.updateChildren(); + this.onDidActiveEditorChange(); + } + private onListContextMenu(e: ITreeContextMenuEvent): void { if (!e.element) { const menu = this.menuService.createMenu(Menus.ViewSort, this.contextKeyService); @@ -2642,6 +2340,111 @@ export class SCMViewPane extends ViewPane { .filter(r => !!r && !isSCMResourceGroup(r))! as any; } + private getViewMode(): ViewMode { + let mode = this.configurationService.getValue<'tree' | 'list'>('scm.defaultViewMode') === 'list' ? ViewMode.List : ViewMode.Tree; + const storageMode = this.storageService.get(`scm.viewMode`, StorageScope.WORKSPACE) as ViewMode; + if (typeof storageMode === 'string') { + mode = storageMode; + } + + return mode; + } + + private getViewSortKey(): ViewSortKey { + // Tree + if (this._viewMode === ViewMode.Tree) { + return ViewSortKey.Path; + } + + // List + let viewSortKey: ViewSortKey; + const viewSortKeyString = this.configurationService.getValue<'path' | 'name' | 'status'>('scm.defaultViewSortKey'); + switch (viewSortKeyString) { + case 'name': + viewSortKey = ViewSortKey.Name; + break; + case 'status': + viewSortKey = ViewSortKey.Status; + break; + default: + viewSortKey = ViewSortKey.Path; + break; + } + + const storageSortKey = this.storageService.get(`scm.viewSortKey`, StorageScope.WORKSPACE) as ViewSortKey; + if (typeof storageSortKey === 'string') { + viewSortKey = storageSortKey; + } + + return viewSortKey; + } + + private loadTreeViewState(): IAsyncDataTreeViewState | undefined { + const storageViewState = this.storageService.get('scm.viewState2', StorageScope.WORKSPACE); + if (!storageViewState) { + return undefined; + } + + try { + const treeViewState = JSON.parse(storageViewState); + return treeViewState; + } catch { + return undefined; + } + } + + private storeTreeViewState() { + this.storageService.store('scm.viewState2', JSON.stringify(this.tree.getViewState()), StorageScope.WORKSPACE, StorageTarget.MACHINE); + } + + private updateChildren(element?: ISCMRepository | ISCMResourceGroup, recursive?: boolean, rerender?: boolean) { + this.asyncOperationSequencer.queue(async () => { + const focusedInput = this.inputRenderer.getFocusedInput(); + + await this.tree.updateChildren(element, recursive, rerender); + + if (focusedInput) { + this.inputRenderer.getRenderedInputWidget(focusedInput)?.focus(); + } + + this.updateRepositoryCollapseAllContextKeys(); + }); + } + + private updateIndentStyles(theme: IFileIconTheme): void { + this.treeContainer.classList.toggle('list-view-mode', this.viewMode === ViewMode.List); + this.treeContainer.classList.toggle('tree-view-mode', this.viewMode === ViewMode.Tree); + this.treeContainer.classList.toggle('align-icons-and-twisties', (this.viewMode === ViewMode.List && theme.hasFileIcons) || (theme.hasFileIcons && !theme.hasFolderIcons)); + this.treeContainer.classList.toggle('hide-arrows', this.viewMode === ViewMode.Tree && theme.hidesExplorerArrows === true); + } + + private updateRepositoryCollapseAllContextKeys(): void { + if (!this.isBodyVisible() || this.items.size === 1) { + this.isAnyRepositoryCollapsibleContextKey.set(false); + this.areAllRepositoriesCollapsedContextKey.set(false); + return; + } + + this.isAnyRepositoryCollapsibleContextKey.set(this.scmViewService.visibleRepositories.some(r => this.tree.hasElement(r) && this.tree.isCollapsible(r))); + this.areAllRepositoriesCollapsedContextKey.set(this.scmViewService.visibleRepositories.every(r => this.tree.hasElement(r) && (!this.tree.isCollapsible(r) || this.tree.isCollapsed(r)))); + } + + collapseAllRepositories(): void { + for (const repository of this.scmViewService.visibleRepositories) { + if (this.tree.isCollapsible(repository)) { + this.tree.collapse(repository); + } + } + } + + expandAllRepositories(): void { + for (const repository of this.scmViewService.visibleRepositories) { + if (this.tree.isCollapsible(repository)) { + this.tree.expand(repository); + } + } + } + override shouldShowWelcome(): boolean { return this.scmService.repositoryCount === 0; } @@ -2650,12 +2453,150 @@ export class SCMViewPane extends ViewPane { return this.scmViewService.visibleRepositories.length === 1 ? this.scmViewService.visibleRepositories[0].provider : undefined; } + override focus(): void { + super.focus(); + + if (this.isExpanded()) { + if (this.tree.getFocus().length === 0) { + for (const repository of this.scmViewService.visibleRepositories) { + const widget = this.inputRenderer.getRenderedInputWidget(repository.input); + + if (widget) { + widget.focus(); + return; + } + } + } + + this.tree.domFocus(); + } + } + override dispose(): void { + this.visibilityDisposables.dispose(); this.disposables.dispose(); + this.items.dispose(); super.dispose(); } } +class SCMTreeDataSource implements IAsyncDataSource { + + constructor( + private readonly viewMode: () => ViewMode, + private readonly alwaysShowRepositories: () => boolean, + private readonly showActionButton: () => boolean, + @ISCMViewService private readonly scmViewService: ISCMViewService) { } + + hasChildren(inputOrElement: ISCMViewService | TreeElement): boolean { + if (isSCMViewService(inputOrElement)) { + return this.scmViewService.visibleRepositories.length !== 0; + } else if (isSCMRepository(inputOrElement)) { + return true; + } else if (isSCMInput(inputOrElement)) { + return false; + } else if (isSCMActionButton(inputOrElement)) { + return false; + } else if (isSCMResourceGroup(inputOrElement)) { + return inputOrElement.resources.length > 0; + } else if (isSCMResource(inputOrElement)) { + return false; + } else if (ResourceTree.isResourceNode(inputOrElement)) { + return inputOrElement.childrenCount > 0; + } else { + throw new Error('hasChildren not implemented.'); + } + } + + getChildren(inputOrElement: ISCMViewService | TreeElement): Iterable | Promise> { + const repositoryCount = this.scmViewService.visibleRepositories.length; + const alwaysShowRepositories = this.alwaysShowRepositories(); + + if (isSCMViewService(inputOrElement) && (repositoryCount > 1 || alwaysShowRepositories)) { + return this.scmViewService.visibleRepositories; + } else if ((isSCMViewService(inputOrElement) && repositoryCount === 1 && !alwaysShowRepositories) || isSCMRepository(inputOrElement)) { + const children: TreeElement[] = []; + + inputOrElement = isSCMRepository(inputOrElement) ? inputOrElement : this.scmViewService.visibleRepositories[0]; + const actionButton = inputOrElement.provider.actionButton; + const resourceGroups = inputOrElement.provider.groups; + const showActionButton = this.showActionButton(); + + // SCM Input + if (inputOrElement.input.visible) { + children.push(inputOrElement.input); + } + + // Action Button + if (showActionButton && actionButton) { + children.push({ + type: 'actionButton', + repository: inputOrElement, + button: actionButton + } as ISCMActionButton); + } + + // ResourceGroups + const hasSomeChanges = resourceGroups.some(group => group.resources.length > 0); + if (hasSomeChanges || (repositoryCount === 1 && (!showActionButton || !actionButton))) { + children.push(...resourceGroups); + } + + return children; + } else if (isSCMResourceGroup(inputOrElement)) { + if (this.viewMode() === ViewMode.List) { + // Resources (List) + return inputOrElement.resources; + } else if (this.viewMode() === ViewMode.Tree) { + // Resources (Tree) + const children: TreeElement[] = []; + for (const node of inputOrElement.resourceTree.root.children) { + children.push(node.childrenCount === 0 ? node.element ?? node : node); + } + + return children; + } + } else if (ResourceTree.isResourceNode(inputOrElement)) { + // Resources (Tree) + const children: TreeElement[] = []; + for (const node of inputOrElement.children) { + children.push(node.childrenCount === 0 ? node.element ?? node : node); + } + + return children; + } + + return []; + } + + getParent(element: TreeElement): ISCMViewService | TreeElement { + if (ResourceTree.isResourceNode(element)) { + if (element.parent === element.context.resourceTree.root) { + return element.context; + } else if (!element.parent) { + throw new Error('Invalid element passed to getParent'); + } else { + return element.parent; + } + } else if (isSCMResource(element)) { + if (this.viewMode() === ViewMode.List) { + return element.resourceGroup; + } + + const node = element.resourceGroup.resourceTree.getNode(element.sourceUri); + const result = node?.parent; + + if (!result) { + throw new Error('Invalid element passed to getParent'); + } + + return result; + } else { + throw new Error('Unexpected call to getParent'); + } + } +} + export class SCMActionButton implements IDisposable { private button: Button | ButtonWithDescription | ButtonWithDropdown | undefined; private readonly disposables = new MutableDisposable(); diff --git a/src/vs/workbench/contrib/scm/browser/util.ts b/src/vs/workbench/contrib/scm/browser/util.ts index 34b7170c71e..ad83f6369bb 100644 --- a/src/vs/workbench/contrib/scm/browser/util.ts +++ b/src/vs/workbench/contrib/scm/browser/util.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ISCMResource, ISCMRepository, ISCMResourceGroup, ISCMInput, ISCMActionButton } from 'vs/workbench/contrib/scm/common/scm'; +import { ISCMResource, ISCMRepository, ISCMResourceGroup, ISCMInput, ISCMActionButton, ISCMViewService } from 'vs/workbench/contrib/scm/common/scm'; import { IMenu } from 'vs/platform/actions/common/actions'; import { ActionBar, IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -21,6 +21,10 @@ export function isSCMRepositoryArray(element: any): element is ISCMRepository[] return Array.isArray(element) && element.every(r => isSCMRepository(r)); } +export function isSCMViewService(element: any): element is ISCMViewService { + return Array.isArray((element as ISCMViewService).repositories) && Array.isArray((element as ISCMViewService).visibleRepositories); +} + export function isSCMRepository(element: any): element is ISCMRepository { return !!(element as ISCMRepository).provider && !!(element as ISCMRepository).input; } @@ -34,7 +38,7 @@ export function isSCMActionButton(element: any): element is ISCMActionButton { } export function isSCMResourceGroup(element: any): element is ISCMResourceGroup { - return !!(element as ISCMResourceGroup).provider && !!(element as ISCMResourceGroup).elements; + return !!(element as ISCMResourceGroup).provider && !!(element as ISCMResourceGroup).resources; } export function isSCMResource(element: any): element is ISCMResource { diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index 3e52814ee60..5f36dd36800 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -8,11 +8,11 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Command } from 'vs/editor/common/languages'; -import { ISequence } from 'vs/base/common/sequence'; import { IAction } from 'vs/base/common/actions'; import { IMenu } from 'vs/platform/actions/common/actions'; import { ThemeIcon } from 'vs/base/common/themables'; import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { ResourceTree } from 'vs/base/common/resourceTree'; import { ISCMHistoryProvider } from 'vs/workbench/contrib/scm/common/history'; export const VIEWLET_ID = 'workbench.view.scm'; @@ -43,22 +43,26 @@ export interface ISCMResource { open(preserveFocus: boolean): Promise; } -export interface ISCMResourceGroup extends ISequence { - readonly provider: ISCMProvider; - readonly label: string; +export interface ISCMResourceGroup { readonly id: string; + readonly provider: ISCMProvider; + + readonly resources: readonly ISCMResource[]; + readonly resourceTree: ResourceTree; + readonly onDidChangeResources: Event; + + readonly label: string; readonly hideWhenEmpty: boolean; readonly onDidChange: Event; } export interface ISCMProvider extends IDisposable { - readonly label: string; readonly id: string; + readonly label: string; readonly contextValue: string; - readonly groups: ISequence; - - // TODO@Joao: remove + readonly groups: readonly ISCMResourceGroup[]; + readonly onDidChangeResourceGroups: Event; readonly onDidChangeResources: Event; readonly rootUri?: URI; diff --git a/src/vs/workbench/contrib/search/browser/searchModel.ts b/src/vs/workbench/contrib/search/browser/searchModel.ts index 14affe70e2a..82b4121cf6a 100644 --- a/src/vs/workbench/contrib/search/browser/searchModel.ts +++ b/src/vs/workbench/contrib/search/browser/searchModel.ts @@ -907,7 +907,7 @@ export class FileMatch extends Disposable implements IFileMatch { if (match.webviewIndex !== undefined) { const index = this._notebookEditorWidget.getCellIndex(match.cell); if (index !== undefined) { - this._notebookEditorWidget.revealCellOffsetInCenterAsync(match.cell, outputOffset ?? 0); + this._notebookEditorWidget.revealCellOffsetInCenter(match.cell, outputOffset ?? 0); } } else { match.cell.updateEditState(match.cell.getEditState(), 'focusNotebookCell'); diff --git a/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts b/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts index 8274fb572f6..428f6388799 100644 --- a/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts +++ b/src/vs/workbench/contrib/tags/electron-sandbox/workspaceTagsService.ts @@ -194,6 +194,7 @@ const PyModulesToLookFor = [ 'azure-ai-ml', 'azure-ai-translation-document', 'azure-appconfiguration', + 'azure-appconfiguration-provider', 'azure-loganalytics', 'azure-synapse-nspkg', 'azure-synapse-spark', @@ -205,7 +206,19 @@ const PyModulesToLookFor = [ 'azure-cognitiveservices-nspkg', 'azure-cognitiveservices-language-nspkg', 'azure-cognitiveservices-knowledge-nspkg', + 'azure-communication-identity', + 'azure-communication-phonenumbers', + 'azure-communication-email', + 'azure-communication-rooms', + 'azure-communication-callautomation', + 'azure-confidentialledger', + 'azure-containerregistry', + 'azure-developer-loadtesting', + 'azure-iot-deviceupdate', + 'azure-messaging-webpubsubservice', 'azure-monitor', + 'azure-monitor-query', + 'azure-monitor-ingestion', 'azure-mgmt-appcontainers', 'azure-mgmt-apimanagement', 'azure-mgmt-web', @@ -254,6 +267,7 @@ const PyModulesToLookFor = [ 'azure-ai-nspkg', 'azure-cognitiveservices-language-textanalytics', 'azure-ai-textanalytics', + 'azure-schemaregistry-avroencoder', 'azure-schemaregistry-avroserializer', 'azure-schemaregistry', 'azure-eventhub-checkpointstoreblob-aio', @@ -630,6 +644,7 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.py.azure-cognitiveservices-nspkg" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-cognitiveservices-language-nspkg" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-cognitiveservices-knowledge-nspkg" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-containerregistry" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-ai-metricsadvisor" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azureml-sdk" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-keyvault-nspkg" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -672,6 +687,19 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.py.azure-communication-chat" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-communication-administration" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-security-attestation" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-appconfiguration-provider" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-communication-identity" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-communication-phonenumbers" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-communication-email" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-communication-rooms" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-communication-callautomation" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-confidentialledger" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-iot-deviceupdate" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-developer-loadtesting" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-monitor-query" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-monitor-ingestion" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-schemaregistry-avroencoder" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.azure-messaging-webpubsubservice" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-data-nspkg" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.azure-data-tables" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.transformers" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, diff --git a/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html b/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html index 9a456c6f347..70c85003ce2 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html +++ b/src/vs/workbench/contrib/webview/browser/pre/index-no-csp.html @@ -123,7 +123,6 @@ code { font-family: var(--monaco-monospace-font); color: var(--vscode-textPreformat-foreground); - font-size: 12px; background-color: var(--vscode-textPreformat-background); padding: 1px 3px; border-radius: 4px; diff --git a/src/vs/workbench/contrib/webview/browser/pre/index.html b/src/vs/workbench/contrib/webview/browser/pre/index.html index ea6db911478..3d4b24036e0 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/index.html +++ b/src/vs/workbench/contrib/webview/browser/pre/index.html @@ -5,7 +5,7 @@ + content="default-src 'none'; script-src 'sha256-frEVWVmmI4TWHGHXZaCTWqGQI9jv+i8hv+sOa87Gqlc=' 'self'; frame-src 'self'; style-src 'unsafe-inline';"> 20; return this.hoverService.showHover({ ...options, persistence: { hideOnKeyDown: false, }, appearance: { - showHoverHint: true, + showHoverHint, skipFadeInAnimation: true, }, }, focus);