Browse Source

Adds workspace deep link support (#2942)

main
Ramin Tadayon 1 year ago
committed by GitHub
parent
commit
0f83de4acf
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 224 additions and 67 deletions
  1. +16
    -1
      package.json
  2. +9
    -0
      src/commands/base.ts
  3. +15
    -0
      src/commands/copyDeepLink.ts
  4. +1
    -0
      src/constants.ts
  5. +36
    -8
      src/uris/deepLinks/deepLink.ts
  6. +86
    -51
      src/uris/deepLinks/deepLinkService.ts
  7. +24
    -4
      src/views/viewBase.ts
  8. +37
    -3
      src/views/workspacesView.ts

+ 16
- 1
package.json View File

@ -5412,6 +5412,12 @@
"icon": "$(copy)" "icon": "$(copy)"
}, },
{ {
"command": "gitlens.copyDeepLinkToWorkspace",
"title": "Copy Link to Workspace",
"category": "GitLens",
"icon": "$(copy)"
},
{
"command": "gitlens.copyDeepLinkToTag", "command": "gitlens.copyDeepLinkToTag",
"title": "Copy Link to Tag", "title": "Copy Link to Tag",
"category": "GitLens", "category": "GitLens",
@ -8556,6 +8562,10 @@
"when": "false" "when": "false"
}, },
{ {
"command": "gitlens.copyDeepLinkToWorkspace",
"when": "false"
},
{
"command": "gitlens.copyRemoteBranchUrl", "command": "gitlens.copyRemoteBranchUrl",
"when": "false" "when": "false"
}, },
@ -11777,7 +11787,7 @@
}, },
{ {
"submenu": "gitlens/share", "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" "group": "7_gitlens_a_share@1"
}, },
{ {
@ -13204,6 +13214,11 @@
"group": "1_gitlens@25" "group": "1_gitlens@25"
}, },
{ {
"command": "gitlens.copyDeepLinkToWorkspace",
"when": "viewItem =~ /gitlens:workspace\\b/",
"group": "1_gitlens@25"
},
{
"command": "gitlens.copyRemoteFileUrlWithoutRange", "command": "gitlens.copyRemoteFileUrlWithoutRange",
"when": "gitlens:hasRemotes && viewItem =~ /gitlens:(file\\b(?=.*?\\b\\+committed\\b)|history:(file|line)|status:file)\\b/", "when": "gitlens:hasRemotes && viewItem =~ /gitlens:(file\\b(?=.*?\\b\\+committed\\b)|history:(file|line)|status:file)\\b/",
"group": "2_gitlens@1" "group": "2_gitlens@1"

+ 9
- 0
src/commands/base.ts View File

@ -21,6 +21,7 @@ import { GitRemote } from '../git/models/remote';
import { Repository } from '../git/models/repository'; import { Repository } from '../git/models/repository';
import type { GitTag } from '../git/models/tag'; import type { GitTag } from '../git/models/tag';
import { isTag } from '../git/models/tag'; import { isTag } from '../git/models/tag';
import { CloudWorkspace, LocalWorkspace } from '../plus/workspaces/models';
import { registerCommand } from '../system/command'; import { registerCommand } from '../system/command';
import { sequentialize } from '../system/function'; import { sequentialize } from '../system/function';
import { ViewNode, ViewRefFileNode, ViewRefNode } from '../views/nodes/viewNode'; import { ViewNode, ViewRefFileNode, ViewRefNode } from '../views/nodes/viewNode';
@ -212,6 +213,14 @@ export function isCommandContextViewNodeHasTag(
return isTag((context.node as ViewNode & { tag: GitTag }).tag); 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 = export type CommandContext =
| CommandEditorLineContext | CommandEditorLineContext
| CommandGitTimelineItemContext | CommandGitTimelineItemContext

+ 15
- 0
src/commands/copyDeepLink.ts View File

@ -20,6 +20,7 @@ import {
isCommandContextViewNodeHasComparison, isCommandContextViewNodeHasComparison,
isCommandContextViewNodeHasRemote, isCommandContextViewNodeHasRemote,
isCommandContextViewNodeHasTag, isCommandContextViewNodeHasTag,
isCommandContextViewNodeHasWorkspace,
} from './base'; } from './base';
export interface CopyDeepLinkCommandArgs { export interface CopyDeepLinkCommandArgs {
@ -28,6 +29,7 @@ export interface CopyDeepLinkCommandArgs {
compareWithRef?: StoredNamedRef; compareWithRef?: StoredNamedRef;
remote?: string; remote?: string;
prePickRemote?: boolean; prePickRemote?: boolean;
workspaceId?: string;
} }
@command() @command()
@ -39,6 +41,7 @@ export class CopyDeepLinkCommand extends ActiveEditorCommand {
Commands.CopyDeepLinkToRepo, Commands.CopyDeepLinkToRepo,
Commands.CopyDeepLinkToTag, Commands.CopyDeepLinkToTag,
Commands.CopyDeepLinkToComparison, Commands.CopyDeepLinkToComparison,
Commands.CopyDeepLinkToWorkspace,
]); ]);
} }
@ -58,6 +61,8 @@ export class CopyDeepLinkCommand extends ActiveEditorCommand {
compareRef: context.node.compareRef, compareRef: context.node.compareRef,
compareWithRef: context.node.compareWithRef, 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) { async execute(editor?: TextEditor, uri?: Uri, args?: CopyDeepLinkCommandArgs) {
args = { ...args }; 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 type;
let repoPath; let repoPath;
if (args?.refOrRepoPath == null) { if (args?.refOrRepoPath == null) {

+ 1
- 0
src/constants.ts View File

@ -126,6 +126,7 @@ export const enum Commands {
CopyDeepLinkToComparison = 'gitlens.copyDeepLinkToComparison', CopyDeepLinkToComparison = 'gitlens.copyDeepLinkToComparison',
CopyDeepLinkToRepo = 'gitlens.copyDeepLinkToRepo', CopyDeepLinkToRepo = 'gitlens.copyDeepLinkToRepo',
CopyDeepLinkToTag = 'gitlens.copyDeepLinkToTag', CopyDeepLinkToTag = 'gitlens.copyDeepLinkToTag',
CopyDeepLinkToWorkspace = 'gitlens.copyDeepLinkToWorkspace',
CopyMessageToClipboard = 'gitlens.copyMessageToClipboard', CopyMessageToClipboard = 'gitlens.copyMessageToClipboard',
CopyRemoteBranchesUrl = 'gitlens.copyRemoteBranchesUrl', CopyRemoteBranchesUrl = 'gitlens.copyRemoteBranchesUrl',
CopyRemoteBranchUrl = 'gitlens.copyRemoteBranchUrl', CopyRemoteBranchUrl = 'gitlens.copyRemoteBranchUrl',

+ 36
- 8
src/uris/deepLinks/deepLink.ts View File

@ -11,6 +11,7 @@ export enum DeepLinkType {
Comparison = 'compare', Comparison = 'compare',
Repository = 'r', Repository = 'r',
Tag = 't', Tag = 't',
Workspace = 'workspace',
} }
export function deepLinkTypeToString(type: DeepLinkType): string { export function deepLinkTypeToString(type: DeepLinkType): string {
@ -25,6 +26,8 @@ export function deepLinkTypeToString(type: DeepLinkType): string {
return 'Repository'; return 'Repository';
case DeepLinkType.Tag: case DeepLinkType.Tag:
return 'Tag'; return 'Tag';
case DeepLinkType.Workspace:
return 'Workspace';
default: default:
debugger; debugger;
return 'Unknown'; return 'Unknown';
@ -46,7 +49,7 @@ export function refTypeToDeepLinkType(refType: GitReference['refType']): DeepLin
export interface DeepLink { export interface DeepLink {
type: DeepLinkType; type: DeepLinkType;
repoId: string;
mainId: string;
remoteUrl?: string; remoteUrl?: string;
repoPath?: string; repoPath?: string;
targetId?: string; targetId?: string;
@ -58,8 +61,10 @@ export function parseDeepLinkUri(uri: Uri): DeepLink | undefined {
// The link target id is everything after the link target. // The link target id is everything after the link target.
// For example, if the uri is /link/r/{repoId}/b/{branchName}?url={remoteUrl}, // For example, if the uri is /link/r/{repoId}/b/{branchName}?url={remoteUrl},
// the link target id is {branchName} // 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); const urlParams = new URLSearchParams(uri.query);
let remoteUrl = urlParams.get('url') ?? undefined; let remoteUrl = urlParams.get('url') ?? undefined;
@ -70,12 +75,18 @@ export function parseDeepLinkUri(uri: Uri): DeepLink | undefined {
if (repoPath != null) { if (repoPath != null) {
repoPath = decodeURIComponent(repoPath); 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) { if (target == null) {
return { return {
type: DeepLinkType.Repository, type: DeepLinkType.Repository,
repoId: repoId,
mainId: mainId,
remoteUrl: remoteUrl, remoteUrl: remoteUrl,
repoPath: repoPath, repoPath: repoPath,
}; };
@ -103,7 +114,7 @@ export function parseDeepLinkUri(uri: Uri): DeepLink | undefined {
return { return {
type: target as DeepLinkType, type: target as DeepLinkType,
repoId: repoId,
mainId: mainId,
remoteUrl: remoteUrl, remoteUrl: remoteUrl,
repoPath: repoPath, repoPath: repoPath,
targetId: targetId, targetId: targetId,
@ -114,6 +125,7 @@ export function parseDeepLinkUri(uri: Uri): DeepLink | undefined {
export const enum DeepLinkServiceState { export const enum DeepLinkServiceState {
Idle, Idle,
TypeMatch,
RepoMatch, RepoMatch,
CloneOrAddRepo, CloneOrAddRepo,
OpeningRepo, OpeningRepo,
@ -125,6 +137,7 @@ export const enum DeepLinkServiceState {
FetchedTargetMatch, FetchedTargetMatch,
OpenGraph, OpenGraph,
OpenComparison, OpenComparison,
OpenWorkspace,
} }
export const enum DeepLinkServiceAction { export const enum DeepLinkServiceAction {
@ -133,6 +146,8 @@ export const enum DeepLinkServiceAction {
DeepLinkResolved, DeepLinkResolved,
DeepLinkStored, DeepLinkStored,
DeepLinkErrored, DeepLinkErrored,
LinkIsRepoType,
LinkIsWorkspaceType,
OpenRepo, OpenRepo,
RepoMatched, RepoMatched,
RepoMatchedInLocalMapping, RepoMatchedInLocalMapping,
@ -154,7 +169,7 @@ export type DeepLinkRepoOpenType = 'clone' | 'folder' | 'workspace' | 'current';
export interface DeepLinkServiceContext { export interface DeepLinkServiceContext {
state: DeepLinkServiceState; state: DeepLinkServiceState;
url?: string | undefined; url?: string | undefined;
repoId?: string | undefined;
mainId?: string | undefined;
repo?: Repository | undefined; repo?: Repository | undefined;
remoteUrl?: string | undefined; remoteUrl?: string | undefined;
remote?: GitRemote | undefined; remote?: GitRemote | undefined;
@ -170,7 +185,14 @@ export interface DeepLinkServiceContext {
export const deepLinkStateTransitionTable: Record<string, Record<string, DeepLinkServiceState>> = { export const deepLinkStateTransitionTable: Record<string, Record<string, DeepLinkServiceState>> = {
[DeepLinkServiceState.Idle]: { [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]: { [DeepLinkServiceState.RepoMatch]: {
[DeepLinkServiceAction.DeepLinkErrored]: DeepLinkServiceState.Idle, [DeepLinkServiceAction.DeepLinkErrored]: DeepLinkServiceState.Idle,
@ -229,6 +251,10 @@ export const deepLinkStateTransitionTable: Record
[DeepLinkServiceAction.DeepLinkResolved]: DeepLinkServiceState.Idle, [DeepLinkServiceAction.DeepLinkResolved]: DeepLinkServiceState.Idle,
[DeepLinkServiceAction.DeepLinkErrored]: DeepLinkServiceState.Idle, [DeepLinkServiceAction.DeepLinkErrored]: DeepLinkServiceState.Idle,
}, },
[DeepLinkServiceState.OpenWorkspace]: {
[DeepLinkServiceAction.DeepLinkResolved]: DeepLinkServiceState.Idle,
[DeepLinkServiceAction.DeepLinkErrored]: DeepLinkServiceState.Idle,
},
}; };
export interface DeepLinkProgress { export interface DeepLinkProgress {
@ -238,6 +264,7 @@ export interface DeepLinkProgress {
export const deepLinkStateToProgress: Record<string, DeepLinkProgress> = { export const deepLinkStateToProgress: Record<string, DeepLinkProgress> = {
[DeepLinkServiceState.Idle]: { message: 'Done.', increment: 100 }, [DeepLinkServiceState.Idle]: { message: 'Done.', increment: 100 },
[DeepLinkServiceState.TypeMatch]: { message: 'Matching link type...', increment: 5 },
[DeepLinkServiceState.RepoMatch]: { message: 'Finding a matching repository...', increment: 10 }, [DeepLinkServiceState.RepoMatch]: { message: 'Finding a matching repository...', increment: 10 },
[DeepLinkServiceState.CloneOrAddRepo]: { message: 'Adding repository...', increment: 20 }, [DeepLinkServiceState.CloneOrAddRepo]: { message: 'Adding repository...', increment: 20 },
[DeepLinkServiceState.OpeningRepo]: { message: 'Opening repository...', increment: 30 }, [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.FetchedTargetMatch]: { message: 'Finding a matching target...', increment: 90 },
[DeepLinkServiceState.OpenGraph]: { message: 'Opening graph...', increment: 95 }, [DeepLinkServiceState.OpenGraph]: { message: 'Opening graph...', increment: 95 },
[DeepLinkServiceState.OpenComparison]: { message: 'Opening comparison...', increment: 95 }, [DeepLinkServiceState.OpenComparison]: { message: 'Opening comparison...', increment: 95 },
[DeepLinkServiceState.OpenWorkspace]: { message: 'Opening workspace...', increment: 95 },
}; };

+ 86
- 51
src/uris/deepLinks/deepLinkService.ts View File

@ -21,6 +21,7 @@ import {
deepLinkStateToProgress, deepLinkStateToProgress,
deepLinkStateTransitionTable, deepLinkStateTransitionTable,
DeepLinkType, DeepLinkType,
deepLinkTypeToString,
parseDeepLinkUri, parseDeepLinkUri,
} from './deepLink'; } from './deepLink';
@ -46,7 +47,7 @@ export class DeepLinkService implements Disposable {
await this.container.git.isDiscoveringRepositories; 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'); void window.showErrorMessage('Unable to resolve link');
Logger.warn(`Unable to resolve link - missing basic properties: ${uri.toString()}`); Logger.warn(`Unable to resolve link - missing basic properties: ${uri.toString()}`);
return; return;
@ -58,9 +59,9 @@ export class DeepLinkService implements Disposable {
return; 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'); 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; return;
} }
@ -92,7 +93,7 @@ export class DeepLinkService implements Disposable {
this._context = { this._context = {
state: DeepLinkServiceState.Idle, state: DeepLinkServiceState.Idle,
url: undefined, url: undefined,
repoId: undefined,
mainId: undefined,
repo: undefined, repo: undefined,
remoteUrl: undefined, remoteUrl: undefined,
remote: undefined, remote: undefined,
@ -109,7 +110,7 @@ export class DeepLinkService implements Disposable {
private setContextFromDeepLink(link: DeepLink, url: string) { private setContextFromDeepLink(link: DeepLink, url: string) {
this._context = { this._context = {
...this._context, ...this._context,
repoId: link.repoId,
mainId: link.mainId,
targetType: link.type, targetType: link.type,
url: url, url: url,
remoteUrl: link.remoteUrl, remoteUrl: link.remoteUrl,
@ -359,9 +360,13 @@ export class DeepLinkService implements Disposable {
): Promise<void> { ): Promise<void> {
let message = ''; let message = '';
let action = initialAction; let action = initialAction;
if (action === DeepLinkServiceAction.DeepLinkCancelled && this._context.state === DeepLinkServiceState.Idle) {
return;
}
//Repo match //Repo match
let matchingLocalRepoPaths: string[] = []; let matchingLocalRepoPaths: string[] = [];
const { targetType } = this._context;
queueMicrotask( queueMicrotask(
() => () =>
@ -369,7 +374,7 @@ export class DeepLinkService implements Disposable {
{ {
cancellable: true, cancellable: true,
location: ProgressLocation.Notification, location: ProgressLocation.Notification,
title: `Opening repository for link: ${this._context.url}}`,
title: `Opening ${deepLinkTypeToString(targetType ?? DeepLinkType.Repository)} link...`,
}, },
(progress, token) => { (progress, token) => {
progress.report({ increment: 0 }); progress.report({ increment: 0 });
@ -379,14 +384,12 @@ export class DeepLinkService implements Disposable {
resolve(); 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]; this._context.state = deepLinkStateTransitionTable[this._context.state][action];
const { const {
state, state,
repoId,
mainId,
repo, repo,
url, url,
remoteUrl, remoteUrl,
@ -422,9 +425,18 @@ export class DeepLinkService implements Disposable {
this.resetContext(); this.resetContext();
return; return;
} }
case DeepLinkServiceState.TypeMatch: {
if (targetType === DeepLinkType.Workspace) {
action = DeepLinkServiceAction.LinkIsWorkspaceType;
} else {
action = DeepLinkServiceAction.LinkIsRepoType;
}
break;
}
case DeepLinkServiceState.RepoMatch: case DeepLinkServiceState.RepoMatch:
case DeepLinkServiceState.AddedRepoMatch: { case DeepLinkServiceState.AddedRepoMatch: {
if (!repoId && !remoteUrl && !repoPath) {
if (!mainId && !remoteUrl && !repoPath) {
action = DeepLinkServiceAction.DeepLinkErrored; action = DeepLinkServiceAction.DeepLinkErrored;
message = 'No repository id, remote url or path was provided.'; message = 'No repository id, remote url or path was provided.';
break; 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 // Repo ID can be any valid SHA in the repo, though standard practice is to use the
// first commit SHA. // 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; this._context.repo = repo;
action = DeepLinkServiceAction.RepoMatched; action = DeepLinkServiceAction.RepoMatched;
break; break;
@ -506,7 +518,7 @@ export class DeepLinkService implements Disposable {
break; break;
} }
case DeepLinkServiceState.CloneOrAddRepo: { case DeepLinkServiceState.CloneOrAddRepo: {
if (!repoId && !remoteUrl && !repoPath) {
if (!mainId && !remoteUrl && !repoPath) {
action = DeepLinkServiceAction.DeepLinkErrored; action = DeepLinkServiceAction.DeepLinkErrored;
message = 'Missing repository id, remote url and path.'; message = 'Missing repository id, remote url and path.';
break; break;
@ -872,6 +884,22 @@ export class DeepLinkService implements Disposable {
action = DeepLinkServiceAction.DeepLinkResolved; action = DeepLinkServiceAction.DeepLinkResolved;
break; 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: { default: {
action = DeepLinkServiceAction.DeepLinkErrored; action = DeepLinkServiceAction.DeepLinkErrored;
message = 'Unknown state.'; message = 'Unknown state.';
@ -881,6 +909,7 @@ export class DeepLinkService implements Disposable {
} }
} }
async copyDeepLinkUrl(workspaceId: string): Promise<void>;
async copyDeepLinkUrl(ref: GitReference, remoteUrl: string): Promise<void>; async copyDeepLinkUrl(ref: GitReference, remoteUrl: string): Promise<void>;
async copyDeepLinkUrl( async copyDeepLinkUrl(
repoPath: string, repoPath: string,
@ -889,17 +918,20 @@ export class DeepLinkService implements Disposable {
compareWithRef?: StoredNamedRef, compareWithRef?: StoredNamedRef,
): Promise<void>; ): Promise<void>;
async copyDeepLinkUrl( async copyDeepLinkUrl(
refOrRepoPath: string | GitReference,
remoteUrl: string,
refOrIdOrRepoPath: string | GitReference,
remoteUrl?: string,
compareRef?: StoredNamedRef, compareRef?: StoredNamedRef,
compareWithRef?: StoredNamedRef, compareWithRef?: StoredNamedRef,
): Promise<void> { ): Promise<void> {
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()); await env.clipboard.writeText(url.toString());
} }
async generateDeepLinkUrl(workspaceId: string): Promise<URL>;
async generateDeepLinkUrl(ref: GitReference, remoteUrl: string): Promise<URL>; async generateDeepLinkUrl(ref: GitReference, remoteUrl: string): Promise<URL>;
async generateDeepLinkUrl( async generateDeepLinkUrl(
repoPath: string, repoPath: string,
@ -908,37 +940,50 @@ export class DeepLinkService implements Disposable {
compareWithRef?: StoredNamedRef, compareWithRef?: StoredNamedRef,
): Promise<URL>; ): Promise<URL>;
async generateDeepLinkUrl( async generateDeepLinkUrl(
refOrRepoPath: string | GitReference,
remoteUrl: string,
refOrIdOrRepoPath: string | GitReference,
remoteUrl?: string,
compareRef?: StoredNamedRef, compareRef?: StoredNamedRef,
compareWithRef?: StoredNamedRef, compareWithRef?: StoredNamedRef,
): Promise<URL> { ): Promise<URL> {
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 { try {
repoId = await this.container.git.getUniqueRepositoryId(repoPath); repoId = await this.container.git.getUniqueRepositoryId(repoPath);
} catch { } catch {
repoId = '-'; 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': case 'branch':
targetType = DeepLinkType.Branch; targetType = DeepLinkType.Branch;
targetId = refOrRepoPath.remote
? getBranchNameWithoutRemote(refOrRepoPath.name)
: refOrRepoPath.name;
targetId = refOrIdOrRepoPath.remote
? getBranchNameWithoutRemote(refOrIdOrRepoPath.name)
: refOrIdOrRepoPath.name;
break; break;
case 'revision': case 'revision':
targetType = DeepLinkType.Commit; targetType = DeepLinkType.Commit;
targetId = refOrRepoPath.ref;
targetId = refOrIdOrRepoPath.ref;
break; break;
case 'tag': case 'tag':
targetType = DeepLinkType.Tag; targetType = DeepLinkType.Tag;
targetId = refOrRepoPath.name;
targetId = refOrIdOrRepoPath.name;
break; break;
} }
} }
@ -949,9 +994,6 @@ export class DeepLinkService implements Disposable {
compareWithTargetId = compareWithRef.label ?? compareWithRef.ref; compareWithTargetId = compareWithRef.label ?? compareWithRef.ref;
} }
const schemeOverride = configuration.get('deepLinks.schemeOverride');
const scheme = !schemeOverride ? 'vscode' : schemeOverride === true ? env.uriScheme : schemeOverride;
let target; let target;
if (targetType === DeepLinkType.Comparison) { if (targetType === DeepLinkType.Comparison) {
target = `/${targetType}/${compareWithTargetId}...${targetId}`; target = `/${targetType}/${compareWithTargetId}...${targetId}`;
@ -968,16 +1010,9 @@ export class DeepLinkService implements Disposable {
}/${repoId}${target}`, }/${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( const deepLinkRedirectUrl = new URL(

+ 24
- 4
src/views/viewBase.ts View File

@ -115,6 +115,9 @@ export abstract class ViewBase<
return `gitlens.views.${this.type}`; return `gitlens.views.${this.type}`;
} }
protected _onDidInitialize = new EventEmitter<void>();
private initialized = false;
protected _onDidChangeTreeData = new EventEmitter<ViewNode | undefined>(); protected _onDidChangeTreeData = new EventEmitter<ViewNode | undefined>();
get onDidChangeTreeData(): Event<ViewNode | undefined> { get onDidChangeTreeData(): Event<ViewNode | undefined> {
return this._onDidChangeTreeData.event; return this._onDidChangeTreeData.event;
@ -348,7 +351,22 @@ export abstract class ViewBase<
if (node != null) return node.getChildren(); if (node != null) return node.getChildren();
const root = this.ensureRoot(); 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 { 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 // 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 // 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<ViewNode | undefined>(resolve => setTimeout(() => resolve(find.call(this)), 100));
return new Promise<ViewNode | undefined>(resolve =>
once(this._onDidInitialize.event)(() => resolve(find.call(this)), this),
);
} }
private async findNodeCoreBFS( private async findNodeCoreBFS(

+ 37
- 3
src/views/workspacesView.ts View File

@ -1,4 +1,4 @@
import type { Disposable } from 'vscode';
import type { CancellationToken, Disposable } from 'vscode';
import { env, ProgressLocation, Uri, window } from 'vscode'; import { env, ProgressLocation, Uri, window } from 'vscode';
import type { RepositoriesViewConfig } from '../config'; import type { RepositoriesViewConfig } from '../config';
import { Commands } from '../constants'; import { Commands } from '../constants';
@ -45,8 +45,42 @@ export class WorkspacesView extends ViewBase<'workspaces', WorkspacesViewNode, R
return super.show(options); 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[] { protected registerCommands(): Disposable[] {

Loading…
Cancel
Save