From 0f83de4acf2d51ed4d694855cc28f78b947610c8 Mon Sep 17 00:00:00 2001 From: Ramin Tadayon <67011668+axosoft-ramint@users.noreply.github.com> Date: Mon, 2 Oct 2023 09:10:37 -0700 Subject: [PATCH] Adds workspace deep link support (#2942) --- package.json | 17 ++++- src/commands/base.ts | 9 +++ src/commands/copyDeepLink.ts | 15 ++++ src/constants.ts | 1 + src/uris/deepLinks/deepLink.ts | 44 +++++++++-- src/uris/deepLinks/deepLinkService.ts | 137 +++++++++++++++++++++------------- src/views/viewBase.ts | 28 ++++++- src/views/workspacesView.ts | 40 +++++++++- 8 files changed, 224 insertions(+), 67 deletions(-) diff --git a/package.json b/package.json index 05c7e50..330baea 100644 --- a/package.json +++ b/package.json @@ -5412,6 +5412,12 @@ "icon": "$(copy)" }, { + "command": "gitlens.copyDeepLinkToWorkspace", + "title": "Copy Link to Workspace", + "category": "GitLens", + "icon": "$(copy)" + }, + { "command": "gitlens.copyDeepLinkToTag", "title": "Copy Link to Tag", "category": "GitLens", @@ -8556,6 +8562,10 @@ "when": "false" }, { + "command": "gitlens.copyDeepLinkToWorkspace", + "when": "false" + }, + { "command": "gitlens.copyRemoteBranchUrl", "when": "false" }, @@ -11777,7 +11787,7 @@ }, { "submenu": "gitlens/share", - "when": "viewItem =~ /gitlens:(branch|commit|compare:results(?!:)|remote|repo-folder|repository|stash|tag|file\\b(?=.*?\\b\\+committed\\b))\\b/", + "when": "viewItem =~ /gitlens:(branch|commit|compare:results(?!:)|remote|repo-folder|repository|stash|tag|workspace|file\\b(?=.*?\\b\\+committed\\b))\\b/", "group": "7_gitlens_a_share@1" }, { @@ -13204,6 +13214,11 @@ "group": "1_gitlens@25" }, { + "command": "gitlens.copyDeepLinkToWorkspace", + "when": "viewItem =~ /gitlens:workspace\\b/", + "group": "1_gitlens@25" + }, + { "command": "gitlens.copyRemoteFileUrlWithoutRange", "when": "gitlens:hasRemotes && viewItem =~ /gitlens:(file\\b(?=.*?\\b\\+committed\\b)|history:(file|line)|status:file)\\b/", "group": "2_gitlens@1" diff --git a/src/commands/base.ts b/src/commands/base.ts index 6d29bda..de0f1e7 100644 --- a/src/commands/base.ts +++ b/src/commands/base.ts @@ -21,6 +21,7 @@ import { GitRemote } from '../git/models/remote'; import { Repository } from '../git/models/repository'; import type { GitTag } from '../git/models/tag'; import { isTag } from '../git/models/tag'; +import { CloudWorkspace, LocalWorkspace } from '../plus/workspaces/models'; import { registerCommand } from '../system/command'; import { sequentialize } from '../system/function'; import { ViewNode, ViewRefFileNode, ViewRefNode } from '../views/nodes/viewNode'; @@ -212,6 +213,14 @@ export function isCommandContextViewNodeHasTag( return isTag((context.node as ViewNode & { tag: GitTag }).tag); } +export function isCommandContextViewNodeHasWorkspace( + context: CommandContext, +): context is CommandViewNodeContext & { node: ViewNode & { workspace: CloudWorkspace | LocalWorkspace } } { + if (context.type !== 'viewItem') return false; + const workspace = (context.node as ViewNode & { workspace?: CloudWorkspace | LocalWorkspace }).workspace; + return workspace instanceof CloudWorkspace || workspace instanceof LocalWorkspace; +} + export type CommandContext = | CommandEditorLineContext | CommandGitTimelineItemContext diff --git a/src/commands/copyDeepLink.ts b/src/commands/copyDeepLink.ts index fbf548c..93cc1be 100644 --- a/src/commands/copyDeepLink.ts +++ b/src/commands/copyDeepLink.ts @@ -20,6 +20,7 @@ import { isCommandContextViewNodeHasComparison, isCommandContextViewNodeHasRemote, isCommandContextViewNodeHasTag, + isCommandContextViewNodeHasWorkspace, } from './base'; export interface CopyDeepLinkCommandArgs { @@ -28,6 +29,7 @@ export interface CopyDeepLinkCommandArgs { compareWithRef?: StoredNamedRef; remote?: string; prePickRemote?: boolean; + workspaceId?: string; } @command() @@ -39,6 +41,7 @@ export class CopyDeepLinkCommand extends ActiveEditorCommand { Commands.CopyDeepLinkToRepo, Commands.CopyDeepLinkToTag, Commands.CopyDeepLinkToComparison, + Commands.CopyDeepLinkToWorkspace, ]); } @@ -58,6 +61,8 @@ export class CopyDeepLinkCommand extends ActiveEditorCommand { compareRef: context.node.compareRef, compareWithRef: context.node.compareWithRef, }; + } else if (isCommandContextViewNodeHasWorkspace(context)) { + args = { workspaceId: context.node.workspace.id }; } } @@ -67,6 +72,16 @@ export class CopyDeepLinkCommand extends ActiveEditorCommand { async execute(editor?: TextEditor, uri?: Uri, args?: CopyDeepLinkCommandArgs) { args = { ...args }; + if (args.workspaceId != null) { + try { + await this.container.deepLinks.copyDeepLinkUrl(args.workspaceId); + } catch (ex) { + Logger.error(ex, 'CopyDeepLinkCommand'); + void showGenericErrorMessage('Unable to copy link'); + } + return; + } + let type; let repoPath; if (args?.refOrRepoPath == null) { diff --git a/src/constants.ts b/src/constants.ts index cd64b2c..818be5e 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -126,6 +126,7 @@ export const enum Commands { CopyDeepLinkToComparison = 'gitlens.copyDeepLinkToComparison', CopyDeepLinkToRepo = 'gitlens.copyDeepLinkToRepo', CopyDeepLinkToTag = 'gitlens.copyDeepLinkToTag', + CopyDeepLinkToWorkspace = 'gitlens.copyDeepLinkToWorkspace', CopyMessageToClipboard = 'gitlens.copyMessageToClipboard', CopyRemoteBranchesUrl = 'gitlens.copyRemoteBranchesUrl', CopyRemoteBranchUrl = 'gitlens.copyRemoteBranchUrl', diff --git a/src/uris/deepLinks/deepLink.ts b/src/uris/deepLinks/deepLink.ts index eac1e8e..4b7910d 100644 --- a/src/uris/deepLinks/deepLink.ts +++ b/src/uris/deepLinks/deepLink.ts @@ -11,6 +11,7 @@ export enum DeepLinkType { Comparison = 'compare', Repository = 'r', Tag = 't', + Workspace = 'workspace', } export function deepLinkTypeToString(type: DeepLinkType): string { @@ -25,6 +26,8 @@ export function deepLinkTypeToString(type: DeepLinkType): string { return 'Repository'; case DeepLinkType.Tag: return 'Tag'; + case DeepLinkType.Workspace: + return 'Workspace'; default: debugger; return 'Unknown'; @@ -46,7 +49,7 @@ export function refTypeToDeepLinkType(refType: GitReference['refType']): DeepLin export interface DeepLink { type: DeepLinkType; - repoId: string; + mainId: string; remoteUrl?: string; repoPath?: string; targetId?: string; @@ -58,8 +61,10 @@ export function parseDeepLinkUri(uri: Uri): DeepLink | undefined { // The link target id is everything after the link target. // For example, if the uri is /link/r/{repoId}/b/{branchName}?url={remoteUrl}, // the link target id is {branchName} - const [, type, prefix, repoId, target, ...rest] = uri.path.split('/'); - if (type !== 'link' || prefix !== DeepLinkType.Repository) return undefined; + const [, type, prefix, mainId, target, ...rest] = uri.path.split('/'); + if (type !== 'link' || (prefix !== DeepLinkType.Repository && prefix !== DeepLinkType.Workspace)) { + return undefined; + } const urlParams = new URLSearchParams(uri.query); let remoteUrl = urlParams.get('url') ?? undefined; @@ -70,12 +75,18 @@ export function parseDeepLinkUri(uri: Uri): DeepLink | undefined { if (repoPath != null) { repoPath = decodeURIComponent(repoPath); } - if (!remoteUrl && !repoPath) return undefined; + if (!remoteUrl && !repoPath && prefix !== DeepLinkType.Workspace) return undefined; + if (prefix === DeepLinkType.Workspace) { + return { + type: DeepLinkType.Workspace, + mainId: mainId, + }; + } if (target == null) { return { type: DeepLinkType.Repository, - repoId: repoId, + mainId: mainId, remoteUrl: remoteUrl, repoPath: repoPath, }; @@ -103,7 +114,7 @@ export function parseDeepLinkUri(uri: Uri): DeepLink | undefined { return { type: target as DeepLinkType, - repoId: repoId, + mainId: mainId, remoteUrl: remoteUrl, repoPath: repoPath, targetId: targetId, @@ -114,6 +125,7 @@ export function parseDeepLinkUri(uri: Uri): DeepLink | undefined { export const enum DeepLinkServiceState { Idle, + TypeMatch, RepoMatch, CloneOrAddRepo, OpeningRepo, @@ -125,6 +137,7 @@ export const enum DeepLinkServiceState { FetchedTargetMatch, OpenGraph, OpenComparison, + OpenWorkspace, } export const enum DeepLinkServiceAction { @@ -133,6 +146,8 @@ export const enum DeepLinkServiceAction { DeepLinkResolved, DeepLinkStored, DeepLinkErrored, + LinkIsRepoType, + LinkIsWorkspaceType, OpenRepo, RepoMatched, RepoMatchedInLocalMapping, @@ -154,7 +169,7 @@ export type DeepLinkRepoOpenType = 'clone' | 'folder' | 'workspace' | 'current'; export interface DeepLinkServiceContext { state: DeepLinkServiceState; url?: string | undefined; - repoId?: string | undefined; + mainId?: string | undefined; repo?: Repository | undefined; remoteUrl?: string | undefined; remote?: GitRemote | undefined; @@ -170,7 +185,14 @@ export interface DeepLinkServiceContext { export const deepLinkStateTransitionTable: Record> = { [DeepLinkServiceState.Idle]: { - [DeepLinkServiceAction.DeepLinkEventFired]: DeepLinkServiceState.RepoMatch, + [DeepLinkServiceAction.DeepLinkEventFired]: DeepLinkServiceState.TypeMatch, + [DeepLinkServiceAction.DeepLinkCancelled]: DeepLinkServiceState.Idle, + }, + [DeepLinkServiceState.TypeMatch]: { + [DeepLinkServiceAction.DeepLinkErrored]: DeepLinkServiceState.Idle, + [DeepLinkServiceAction.DeepLinkCancelled]: DeepLinkServiceState.Idle, + [DeepLinkServiceAction.LinkIsRepoType]: DeepLinkServiceState.RepoMatch, + [DeepLinkServiceAction.LinkIsWorkspaceType]: DeepLinkServiceState.OpenWorkspace, }, [DeepLinkServiceState.RepoMatch]: { [DeepLinkServiceAction.DeepLinkErrored]: DeepLinkServiceState.Idle, @@ -229,6 +251,10 @@ export const deepLinkStateTransitionTable: Record = { [DeepLinkServiceState.Idle]: { message: 'Done.', increment: 100 }, + [DeepLinkServiceState.TypeMatch]: { message: 'Matching link type...', increment: 5 }, [DeepLinkServiceState.RepoMatch]: { message: 'Finding a matching repository...', increment: 10 }, [DeepLinkServiceState.CloneOrAddRepo]: { message: 'Adding repository...', increment: 20 }, [DeepLinkServiceState.OpeningRepo]: { message: 'Opening repository...', increment: 30 }, @@ -249,4 +276,5 @@ export const deepLinkStateToProgress: Record = { [DeepLinkServiceState.FetchedTargetMatch]: { message: 'Finding a matching target...', increment: 90 }, [DeepLinkServiceState.OpenGraph]: { message: 'Opening graph...', increment: 95 }, [DeepLinkServiceState.OpenComparison]: { message: 'Opening comparison...', increment: 95 }, + [DeepLinkServiceState.OpenWorkspace]: { message: 'Opening workspace...', increment: 95 }, }; diff --git a/src/uris/deepLinks/deepLinkService.ts b/src/uris/deepLinks/deepLinkService.ts index 22b4481..3a9cfd2 100644 --- a/src/uris/deepLinks/deepLinkService.ts +++ b/src/uris/deepLinks/deepLinkService.ts @@ -21,6 +21,7 @@ import { deepLinkStateToProgress, deepLinkStateTransitionTable, DeepLinkType, + deepLinkTypeToString, parseDeepLinkUri, } from './deepLink'; @@ -46,7 +47,7 @@ export class DeepLinkService implements Disposable { await this.container.git.isDiscoveringRepositories; } - if (!link.type || (!link.repoId && !link.remoteUrl && !link.repoPath)) { + if (!link.type || (!link.mainId && !link.remoteUrl && !link.repoPath)) { void window.showErrorMessage('Unable to resolve link'); Logger.warn(`Unable to resolve link - missing basic properties: ${uri.toString()}`); return; @@ -58,9 +59,9 @@ export class DeepLinkService implements Disposable { return; } - if (link.type !== DeepLinkType.Repository && link.targetId == null) { + if (link.type !== DeepLinkType.Repository && link.targetId == null && link.mainId == null) { void window.showErrorMessage('Unable to resolve link'); - Logger.warn(`Unable to resolve link - no target id provided: ${uri.toString()}`); + Logger.warn(`Unable to resolve link - no main/target id provided: ${uri.toString()}`); return; } @@ -92,7 +93,7 @@ export class DeepLinkService implements Disposable { this._context = { state: DeepLinkServiceState.Idle, url: undefined, - repoId: undefined, + mainId: undefined, repo: undefined, remoteUrl: undefined, remote: undefined, @@ -109,7 +110,7 @@ export class DeepLinkService implements Disposable { private setContextFromDeepLink(link: DeepLink, url: string) { this._context = { ...this._context, - repoId: link.repoId, + mainId: link.mainId, targetType: link.type, url: url, remoteUrl: link.remoteUrl, @@ -359,9 +360,13 @@ export class DeepLinkService implements Disposable { ): Promise { let message = ''; let action = initialAction; + if (action === DeepLinkServiceAction.DeepLinkCancelled && this._context.state === DeepLinkServiceState.Idle) { + return; + } //Repo match let matchingLocalRepoPaths: string[] = []; + const { targetType } = this._context; queueMicrotask( () => @@ -369,7 +374,7 @@ export class DeepLinkService implements Disposable { { cancellable: true, location: ProgressLocation.Notification, - title: `Opening repository for link: ${this._context.url}}`, + title: `Opening ${deepLinkTypeToString(targetType ?? DeepLinkType.Repository)} link...`, }, (progress, token) => { progress.report({ increment: 0 }); @@ -379,14 +384,12 @@ export class DeepLinkService implements Disposable { resolve(); }); - this._disposables.push( - this._onDeepLinkProgressUpdated.event(({ message, increment }) => { - progress.report({ message: message, increment: increment }); - if (increment === 100) { - resolve(); - } - }), - ); + this._onDeepLinkProgressUpdated.event(({ message, increment }) => { + progress.report({ message: message, increment: increment }); + if (increment === 100) { + resolve(); + } + }); }); }, ), @@ -396,7 +399,7 @@ export class DeepLinkService implements Disposable { this._context.state = deepLinkStateTransitionTable[this._context.state][action]; const { state, - repoId, + mainId, repo, url, remoteUrl, @@ -422,9 +425,18 @@ export class DeepLinkService implements Disposable { this.resetContext(); return; } + case DeepLinkServiceState.TypeMatch: { + if (targetType === DeepLinkType.Workspace) { + action = DeepLinkServiceAction.LinkIsWorkspaceType; + } else { + action = DeepLinkServiceAction.LinkIsRepoType; + } + + break; + } case DeepLinkServiceState.RepoMatch: case DeepLinkServiceState.AddedRepoMatch: { - if (!repoId && !remoteUrl && !repoPath) { + if (!mainId && !remoteUrl && !repoPath) { action = DeepLinkServiceAction.DeepLinkErrored; message = 'No repository id, remote url or path was provided.'; break; @@ -459,10 +471,10 @@ export class DeepLinkService implements Disposable { } } - if (repoId != null && repoId !== '-') { + if (mainId != null && mainId !== '-') { // Repo ID can be any valid SHA in the repo, though standard practice is to use the // first commit SHA. - if (await this.container.git.validateReference(repo.path, repoId)) { + if (await this.container.git.validateReference(repo.path, mainId)) { this._context.repo = repo; action = DeepLinkServiceAction.RepoMatched; break; @@ -506,7 +518,7 @@ export class DeepLinkService implements Disposable { break; } case DeepLinkServiceState.CloneOrAddRepo: { - if (!repoId && !remoteUrl && !repoPath) { + if (!mainId && !remoteUrl && !repoPath) { action = DeepLinkServiceAction.DeepLinkErrored; message = 'Missing repository id, remote url and path.'; break; @@ -872,6 +884,22 @@ export class DeepLinkService implements Disposable { action = DeepLinkServiceAction.DeepLinkResolved; break; } + case DeepLinkServiceState.OpenWorkspace: { + if (!mainId) { + action = DeepLinkServiceAction.DeepLinkErrored; + message = 'Missing workspace id.'; + break; + } + + await this.container.workspacesView.revealWorkspaceNode(mainId, { + select: true, + focus: true, + expand: true, + }); + + action = DeepLinkServiceAction.DeepLinkResolved; + break; + } default: { action = DeepLinkServiceAction.DeepLinkErrored; message = 'Unknown state.'; @@ -881,6 +909,7 @@ export class DeepLinkService implements Disposable { } } + async copyDeepLinkUrl(workspaceId: string): Promise; async copyDeepLinkUrl(ref: GitReference, remoteUrl: string): Promise; async copyDeepLinkUrl( repoPath: string, @@ -889,17 +918,20 @@ export class DeepLinkService implements Disposable { compareWithRef?: StoredNamedRef, ): Promise; async copyDeepLinkUrl( - refOrRepoPath: string | GitReference, - remoteUrl: string, + refOrIdOrRepoPath: string | GitReference, + remoteUrl?: string, compareRef?: StoredNamedRef, compareWithRef?: StoredNamedRef, ): Promise { - const url = await (typeof refOrRepoPath === 'string' - ? this.generateDeepLinkUrl(refOrRepoPath, remoteUrl, compareRef, compareWithRef) - : this.generateDeepLinkUrl(refOrRepoPath, remoteUrl)); + const url = await (typeof refOrIdOrRepoPath === 'string' + ? remoteUrl != null + ? this.generateDeepLinkUrl(refOrIdOrRepoPath, remoteUrl, compareRef, compareWithRef) + : this.generateDeepLinkUrl(refOrIdOrRepoPath) + : this.generateDeepLinkUrl(refOrIdOrRepoPath, remoteUrl!)); await env.clipboard.writeText(url.toString()); } + async generateDeepLinkUrl(workspaceId: string): Promise; async generateDeepLinkUrl(ref: GitReference, remoteUrl: string): Promise; async generateDeepLinkUrl( repoPath: string, @@ -908,37 +940,50 @@ export class DeepLinkService implements Disposable { compareWithRef?: StoredNamedRef, ): Promise; async generateDeepLinkUrl( - refOrRepoPath: string | GitReference, - remoteUrl: string, + refOrIdOrRepoPath: string | GitReference, + remoteUrl?: string, compareRef?: StoredNamedRef, compareWithRef?: StoredNamedRef, ): Promise { - const repoPath = typeof refOrRepoPath !== 'string' ? refOrRepoPath.repoPath : refOrRepoPath; - let repoId; + let repoId: string | undefined; + let targetType: DeepLinkType | undefined; + let targetId: string | undefined; + let compareWithTargetId: string | undefined; + const schemeOverride = configuration.get('deepLinks.schemeOverride'); + const scheme = !schemeOverride ? 'vscode' : schemeOverride === true ? env.uriScheme : schemeOverride; + let modePrefixString = ''; + if (this.container.env === 'dev') { + modePrefixString = 'dev.'; + } else if (this.container.env === 'staging') { + modePrefixString = 'staging.'; + } + + if (remoteUrl == null && typeof refOrIdOrRepoPath === 'string') { + return new URL(`https://${modePrefixString}gitkraken.dev/link/workspaces/${refOrIdOrRepoPath}`); + } + + const repoPath = typeof refOrIdOrRepoPath !== 'string' ? refOrIdOrRepoPath.repoPath : refOrIdOrRepoPath; try { repoId = await this.container.git.getUniqueRepositoryId(repoPath); } catch { repoId = '-'; } - let targetType: DeepLinkType | undefined; - let targetId: string | undefined; - let compareWithTargetId: string | undefined; - if (typeof refOrRepoPath !== 'string') { - switch (refOrRepoPath.refType) { + if (typeof refOrIdOrRepoPath !== 'string') { + switch (refOrIdOrRepoPath.refType) { case 'branch': targetType = DeepLinkType.Branch; - targetId = refOrRepoPath.remote - ? getBranchNameWithoutRemote(refOrRepoPath.name) - : refOrRepoPath.name; + targetId = refOrIdOrRepoPath.remote + ? getBranchNameWithoutRemote(refOrIdOrRepoPath.name) + : refOrIdOrRepoPath.name; break; case 'revision': targetType = DeepLinkType.Commit; - targetId = refOrRepoPath.ref; + targetId = refOrIdOrRepoPath.ref; break; case 'tag': targetType = DeepLinkType.Tag; - targetId = refOrRepoPath.name; + targetId = refOrIdOrRepoPath.name; break; } } @@ -949,9 +994,6 @@ export class DeepLinkService implements Disposable { compareWithTargetId = compareWithRef.label ?? compareWithRef.ref; } - const schemeOverride = configuration.get('deepLinks.schemeOverride'); - - const scheme = !schemeOverride ? 'vscode' : schemeOverride === true ? env.uriScheme : schemeOverride; let target; if (targetType === DeepLinkType.Comparison) { target = `/${targetType}/${compareWithTargetId}...${targetId}`; @@ -968,16 +1010,9 @@ export class DeepLinkService implements Disposable { }/${repoId}${target}`, ); - // Add the remote URL as a query parameter - deepLink.searchParams.set('url', remoteUrl); - const params = new URLSearchParams(); - params.set('url', remoteUrl); - - let modePrefixString = ''; - if (this.container.env === 'dev') { - modePrefixString = 'dev.'; - } else if (this.container.env === 'staging') { - modePrefixString = 'staging.'; + if (remoteUrl != null) { + // Add the remote URL as a query parameter + deepLink.searchParams.set('url', remoteUrl); } const deepLinkRedirectUrl = new URL( diff --git a/src/views/viewBase.ts b/src/views/viewBase.ts index 696c94d..68bf794 100644 --- a/src/views/viewBase.ts +++ b/src/views/viewBase.ts @@ -115,6 +115,9 @@ export abstract class ViewBase< return `gitlens.views.${this.type}`; } + protected _onDidInitialize = new EventEmitter(); + private initialized = false; + protected _onDidChangeTreeData = new EventEmitter(); get onDidChangeTreeData(): Event { return this._onDidChangeTreeData.event; @@ -348,7 +351,22 @@ export abstract class ViewBase< if (node != null) return node.getChildren(); const root = this.ensureRoot(); - return root.getChildren(); + const children = root.getChildren(); + if (!this.initialized) { + if (isPromise(children)) { + void children.then(() => { + if (!this.initialized) { + this.initialized = true; + setTimeout(() => this._onDidInitialize.fire(), 1); + } + }); + } else { + this.initialized = true; + setTimeout(() => this._onDidInitialize.fire(), 1); + } + } + + return children; } getParent(node: ViewNode): ViewNode | undefined { @@ -455,12 +473,14 @@ export abstract class ViewBase< } } - if (this.root != null) return find.call(this); + if (this.initialized) return find.call(this); // If we have no root (e.g. never been initialized) force it so the tree will load properly - await this.show({ preserveFocus: true }); + void this.show({ preserveFocus: true }); // Since we have to show the view, give the view time to load and let the callstack unwind before we try to find the node - return new Promise(resolve => setTimeout(() => resolve(find.call(this)), 100)); + return new Promise(resolve => + once(this._onDidInitialize.event)(() => resolve(find.call(this)), this), + ); } private async findNodeCoreBFS( diff --git a/src/views/workspacesView.ts b/src/views/workspacesView.ts index 634736a..1cbe086 100644 --- a/src/views/workspacesView.ts +++ b/src/views/workspacesView.ts @@ -1,4 +1,4 @@ -import type { Disposable } from 'vscode'; +import type { CancellationToken, Disposable } from 'vscode'; import { env, ProgressLocation, Uri, window } from 'vscode'; import type { RepositoriesViewConfig } from '../config'; import { Commands } from '../constants'; @@ -45,8 +45,42 @@ export class WorkspacesView extends ViewBase<'workspaces', WorkspacesViewNode, R return super.show(options); } - override get canReveal(): boolean { - return false; + async findWorkspaceNode(workspaceId: string, token?: CancellationToken) { + return this.findNode((n: any) => n.workspace?.id === workspaceId, { + allowPaging: false, + maxDepth: 2, + canTraverse: n => { + if (n instanceof WorkspacesViewNode) return true; + + return false; + }, + token: token, + }); + } + + async revealWorkspaceNode( + workspaceId: string, + options?: { + select?: boolean; + focus?: boolean; + expand?: boolean | number; + }, + ) { + return window.withProgress( + { + location: ProgressLocation.Notification, + title: `Revealing workspace ${workspaceId} in the side bar...`, + cancellable: true, + }, + async (progress, token) => { + const node = await this.findWorkspaceNode(workspaceId, token); + if (node == null) return undefined; + + await this.ensureRevealNode(node, options); + + return node; + }, + ); } protected registerCommands(): Disposable[] {