diff --git a/src/constants.telemetry.ts b/src/constants.telemetry.ts index 4148de80f3b59..98250c120f3f0 100644 --- a/src/constants.telemetry.ts +++ b/src/constants.telemetry.ts @@ -256,7 +256,12 @@ export type TelemetryEvents = { /** Sent when a launchpad operation is taking longer than a set timeout to complete */ 'launchpad/operation/slow': { timeout: number; - operation: 'getMyPullRequests' | 'getCodeSuggestions' | 'getEnrichedItems' | 'getCodeSuggestionCounts'; + operation: + | 'getMyPullRequests' + | 'getCodeSuggestions' + | 'getEnrichedItems' + | 'getCodeSuggestionCounts' + | 'getPullRequest'; duration: number; }; diff --git a/src/plus/launchpad/launchpad.ts b/src/plus/launchpad/launchpad.ts index b7483ddb3f816..e59236c295de1 100644 --- a/src/plus/launchpad/launchpad.ts +++ b/src/plus/launchpad/launchpad.ts @@ -524,11 +524,12 @@ export class LaunchpadCommand extends QuickCommand { const updateItems = async ( quickpick: QuickPick, + search?: string, ) => { quickpick.busy = true; try { - await updateContextItems(this.container, context, { force: true }); + await updateContextItems(this.container, context, { force: true, search: search }); const { items, placeholder } = getItemsAndPlaceholder(); quickpick.placeholder = placeholder; @@ -555,7 +556,7 @@ export class LaunchpadCommand extends QuickCommand { LaunchpadSettingsQuickInputButton, RefreshQuickInputButton, ], - onDidChangeValue: quickpick => { + onDidChangeValue: async quickpick => { const hideGroups = Boolean(quickpick.value?.length); if (groupsHidden !== hideGroups) { @@ -591,8 +592,13 @@ export class LaunchpadCommand extends QuickCommand { // This is a hack because the quickpick doesn't update until you change the items quickpick.items = [...quickpick.items]; } + // We have found an item that matches to the URL. + // Now it will be displayed as the found item and we exit this function now without sending any requests to API: + return true; } } + // Nothing is found above, so let's perform search in the API: + await updateItems(quickpick, value); } return true; @@ -1329,7 +1335,11 @@ function getIntegrationTitle(integrationId: string): string { } } -async function updateContextItems(container: Container, context: Context, options?: { force?: boolean }) { +async function updateContextItems( + container: Container, + context: Context, + options?: { force?: boolean; search?: string }, +) { context.result = await container.launchpad.getCategorizedItems(options); if (container.telemetry.enabled) { updateTelemetryContext(context); diff --git a/src/plus/launchpad/launchpadProvider.ts b/src/plus/launchpad/launchpadProvider.ts index f535cfd0fb1b3..8010a03b67631 100644 --- a/src/plus/launchpad/launchpadProvider.ts +++ b/src/plus/launchpad/launchpadProvider.ts @@ -41,6 +41,8 @@ import { showInspectView } from '../../webviews/commitDetails/actions'; import type { ShowWipArgs } from '../../webviews/commitDetails/protocol'; import type { IntegrationResult } from '../integrations/integration'; import type { ConnectionStateChangeEvent } from '../integrations/integrationService'; +import type { GitHubRepositoryDescriptor } from '../integrations/providers/github'; +import { getPullRequestIdentityValuesFromSearch } from '../integrations/providers/github'; import type { EnrichablePullRequest, ProviderActionablePullRequest } from '../integrations/providers/models'; import { fromProviderPullRequest, @@ -318,6 +320,34 @@ export class LaunchpadProvider implements Disposable { return { prs: prs, suggestionCounts: suggestionCounts }; } + private async getSearchedPullRequests(search: string) { + const { ownerAndRepo, prNumber } = getPullRequestIdentityValuesFromSearch(search); + let result: TimedResult | undefined; + + if (prNumber != null) { + if (ownerAndRepo != null) { + // TODO: This needs to be generalized to work outside of GitHub + const integration = await this.container.integrations.get(HostingIntegrationId.GitHub); + const [owner, repo] = ownerAndRepo.split('/', 2); + const descriptor: GitHubRepositoryDescriptor = { + key: ownerAndRepo, + owner: owner, + name: repo, + }; + const pr = await withDurationAndSlowEventOnTimeout( + integration?.getPullRequest(descriptor, prNumber), + 'getPullRequest', + this.container, + ); + if (pr?.value != null) { + result = { value: [{ pullRequest: pr.value, reasons: [] }], duration: pr.duration }; + return { prs: result, suggestionCounts: undefined }; + } + } + } + return { prs: undefined, suggestionCounts: undefined }; + } + private _enrichedItems: CachedLaunchpadPromise> | undefined; @debug({ args: { 0: o => `force=${o?.force}` } }) private async getEnrichedItems(options?: { cancellation?: CancellationToken; force?: boolean }) { @@ -612,12 +642,12 @@ export class LaunchpadProvider implements Disposable { @gate(o => `${o?.force ?? false}`) @log({ args: { 0: o => `force=${o?.force}`, 1: false } }) async getCategorizedItems( - options?: { force?: boolean }, + options?: { force?: boolean; search?: string }, cancellation?: CancellationToken, ): Promise { const scope = getLogScope(); - const fireRefresh = options?.force || this._prs == null; + const fireRefresh = !options?.search && (options?.force || this._prs == null); const ignoredRepositories = new Set( (configuration.get('launchpad.ignoredRepositories') ?? []).map(r => r.toLowerCase()), @@ -638,7 +668,9 @@ export class LaunchpadProvider implements Disposable { const [_, enrichedItemsResult, prsWithCountsResult] = await Promise.allSettled([ this.container.git.isDiscoveringRepositories, this.getEnrichedItems({ force: options?.force, cancellation: cancellation }), - this.getPullRequestsWithSuggestionCounts({ force: options?.force, cancellation: cancellation }), + options?.search + ? this.getSearchedPullRequests(options.search) + : this.getPullRequestsWithSuggestionCounts({ force: options?.force, cancellation: cancellation }), ]); if (cancellation?.isCancellationRequested) throw new CancellationError(); @@ -752,7 +784,7 @@ export class LaunchpadProvider implements Disposable { item.suggestedActionCategory, )!; // category overrides - if (staleDate != null && item.updatedDate.getTime() < staleDate.getTime()) { + if (!options?.search && staleDate != null && item.updatedDate.getTime() < staleDate.getTime()) { actionableCategory = 'other'; } else if (codeSuggestionsCount > 0 && item.viewer.isAuthor) { actionableCategory = 'code-suggestions'; @@ -788,7 +820,10 @@ export class LaunchpadProvider implements Disposable { }; return result; } finally { - this.updateGroupedIds(result?.items ?? []); + if (!options?.search) { + this.updateGroupedIds(result?.items ?? []); + } + if (result != null && fireRefresh) { this._onDidRefresh.fire(result); } @@ -1045,7 +1080,12 @@ const slowEventTimeout = 1000 * 30; // 30 seconds function withDurationAndSlowEventOnTimeout( promise: Promise, - name: 'getMyPullRequests' | 'getCodeSuggestionCounts' | 'getCodeSuggestions' | 'getEnrichedItems', + name: + | 'getMyPullRequests' + | 'getCodeSuggestionCounts' + | 'getCodeSuggestions' + | 'getEnrichedItems' + | 'getPullRequest', container: Container, ): Promise> { return timedWithSlowThreshold(promise, {