Skip to content

Commit 9e3445c

Browse files
committed
Searches GitHub PR by the entered URL
(#3543, #3684)
1 parent 6f8b959 commit 9e3445c

File tree

4 files changed

+91
-34
lines changed

4 files changed

+91
-34
lines changed

docs/telemetry-events.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1305,7 +1305,7 @@ void
13051305
```typescript
13061306
{
13071307
'timeout': number,
1308-
'operation': 'getMyPullRequests' | 'getCodeSuggestions' | 'getEnrichedItems' | 'getCodeSuggestionCounts',
1308+
'operation': 'getPullRequest' | 'getMyPullRequests' | 'getCodeSuggestions' | 'getEnrichedItems' | 'getCodeSuggestionCounts',
13091309
'duration': number
13101310
}
13111311
```

src/constants.telemetry.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,12 @@ export type TelemetryEvents = {
294294
/** Sent when a launchpad operation is taking longer than a set timeout to complete */
295295
'launchpad/operation/slow': {
296296
timeout: number;
297-
operation: 'getMyPullRequests' | 'getCodeSuggestions' | 'getEnrichedItems' | 'getCodeSuggestionCounts';
297+
operation:
298+
| 'getPullRequest'
299+
| 'getMyPullRequests'
300+
| 'getCodeSuggestions'
301+
| 'getEnrichedItems'
302+
| 'getCodeSuggestionCounts';
298303
duration: number;
299304
};
300305

src/plus/launchpad/launchpad.ts

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,7 @@ export class LaunchpadCommand extends QuickCommand<State> {
419419
i: LaunchpadItem,
420420
ui: LaunchpadGroup,
421421
topItem: LaunchpadItem | undefined,
422+
alwaysShow: boolean | undefined,
422423
): LaunchpadItemQuickPickItem => {
423424
const buttons = [];
424425

@@ -449,6 +450,7 @@ export class LaunchpadCommand extends QuickCommand<State> {
449450
i.actionableCategory === 'other' ? '' : `${actionGroupMap.get(i.actionableCategory)![0]} \u2022 `
450451
}${fromNow(i.updatedDate)} by @${i.author!.username}`,
451452

453+
alwaysShow: alwaysShow,
452454
buttons: buttons,
453455
iconPath: i.author?.avatarUrl != null ? Uri.parse(i.author.avatarUrl) : undefined,
454456
item: i,
@@ -457,7 +459,7 @@ export class LaunchpadCommand extends QuickCommand<State> {
457459
};
458460
};
459461

460-
const getItems = (result: LaunchpadCategorizedResult) => {
462+
const getItems = (result: LaunchpadCategorizedResult, isSearching?: boolean) => {
461463
const items: (LaunchpadItemQuickPickItem | DirectiveQuickPickItem | ConnectMoreIntegrationsItem)[] = [];
462464

463465
if (result.items?.length) {
@@ -472,18 +474,21 @@ export class LaunchpadCommand extends QuickCommand<State> {
472474
for (const [ui, groupItems] of uiGroups) {
473475
if (!groupItems.length) continue;
474476

475-
items.push(...buildGroupHeading(ui, groupItems.length));
476-
477-
if (context.collapsed.get(ui)) continue;
477+
if (!isSearching) {
478+
items.push(...buildGroupHeading(ui, groupItems.length));
479+
if (context.collapsed.get(ui)) {
480+
continue;
481+
}
482+
}
478483

479-
items.push(...groupItems.map(i => buildLaunchpadQuickPickItem(i, ui, topItem)));
484+
items.push(...groupItems.map(i => buildLaunchpadQuickPickItem(i, ui, topItem, isSearching)));
480485
}
481486
}
482487

483488
return items;
484489
};
485490

486-
function getItemsAndPlaceholder() {
491+
function getItemsAndPlaceholder(isSearching?: boolean) {
487492
if (context.result.error != null) {
488493
return {
489494
placeholder: `Unable to load items (${
@@ -506,17 +511,18 @@ export class LaunchpadCommand extends QuickCommand<State> {
506511

507512
return {
508513
placeholder: 'Choose an item to focus on',
509-
items: getItems(context.result),
514+
items: getItems(context.result, isSearching),
510515
};
511516
}
512517

513518
const updateItems = async (
514519
quickpick: QuickPick<LaunchpadItemQuickPickItem | DirectiveQuickPickItem | ConnectMoreIntegrationsItem>,
515520
) => {
521+
const search = quickpick.value;
516522
quickpick.busy = true;
517523

518524
try {
519-
await updateContextItems(this.container, context, { force: true });
525+
await updateContextItems(this.container, context, { force: true, search: search });
520526

521527
const { items, placeholder } = getItemsAndPlaceholder();
522528
quickpick.placeholder = placeholder;
@@ -527,8 +533,7 @@ export class LaunchpadCommand extends QuickCommand<State> {
527533
};
528534

529535
const { items, placeholder } = getItemsAndPlaceholder();
530-
531-
let groupsHidden = false;
536+
const nonGroupedItems = items.filter(i => !isDirectiveQuickPickItem(i));
532537

533538
const step = createPickStep({
534539
title: context.title,
@@ -543,29 +548,27 @@ export class LaunchpadCommand extends QuickCommand<State> {
543548
LaunchpadSettingsQuickInputButton,
544549
RefreshQuickInputButton,
545550
],
546-
onDidChangeValue: quickpick => {
551+
onDidChangeValue: async quickpick => {
547552
const { value } = quickpick;
548-
const hideGroups = Boolean(quickpick.value?.length);
549-
550-
if (groupsHidden !== hideGroups) {
551-
groupsHidden = hideGroups;
552-
quickpick.items = hideGroups ? items.filter(i => !isDirectiveQuickPickItem(i)) : items;
553-
}
554-
const activeLaunchpadItems = quickpick.activeItems.filter(
555-
(i): i is LaunchpadItemQuickPickItem => 'item' in i && !i.alwaysShow,
556-
);
553+
const hideGroups = Boolean(value?.length);
554+
const consideredItems = hideGroups ? nonGroupedItems : items;
557555

558556
let updated = false;
559-
for (const item of quickpick.items) {
557+
for (const item of consideredItems) {
560558
if (item.alwaysShow) {
561559
item.alwaysShow = false;
562560
updated = true;
563561
}
564562
}
565-
if (updated) {
566-
// Force quickpick to update by changing the items object:
567-
quickpick.items = [...quickpick.items];
568-
}
563+
564+
// By doing the following we make sure we operate with the PRs that belong to Launchpad initially.
565+
// Also, when we re-create the array, we make sure that `alwaysShow` updates are applied.
566+
quickpick.items =
567+
updated && quickpick.items === consideredItems ? [...consideredItems] : consideredItems;
568+
569+
const activeLaunchpadItems = quickpick.activeItems.filter(
570+
(i): i is LaunchpadItemQuickPickItem => 'item' in i,
571+
);
569572

570573
if (!value?.length || activeLaunchpadItems.length) {
571574
// Nothing to search
@@ -588,7 +591,12 @@ export class LaunchpadCommand extends QuickCommand<State> {
588591
item.alwaysShow = true;
589592
// Force quickpick to update by changing the items object:
590593
quickpick.items = [...quickpick.items];
594+
// We have found an item that matches to the URL.
595+
// Now it will be displayed as the found item and we exit this function now without sending any requests to API:
596+
return true;
591597
}
598+
// Nothing is found above, so let's perform search in the API:
599+
await updateItems(quickpick);
592600
}
593601
}
594602

@@ -1377,7 +1385,11 @@ function getIntegrationTitle(integrationId: string): string {
13771385
}
13781386
}
13791387

1380-
async function updateContextItems(container: Container, context: Context, options?: { force?: boolean }) {
1388+
async function updateContextItems(
1389+
container: Container,
1390+
context: Context,
1391+
options?: { force?: boolean; search?: string },
1392+
) {
13811393
context.result = await container.launchpad.getCategorizedItems(options);
13821394
if (container.telemetry.enabled) {
13831395
updateTelemetryContext(context);

src/plus/launchpad/launchpadProvider.ts

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type { PullRequest, SearchedPullRequest } from '../../git/models/pullRequ
1919
import {
2020
getComparisonRefsForPullRequest,
2121
getOrOpenPullRequestRepository,
22+
getPullRequestIdentityValuesFromSearch,
2223
getRepositoryIdentityForPullRequest,
2324
} from '../../git/models/pullRequest';
2425
import type { GitRemote } from '../../git/models/remote';
@@ -41,6 +42,7 @@ import { showInspectView } from '../../webviews/commitDetails/actions';
4142
import type { ShowWipArgs } from '../../webviews/commitDetails/protocol';
4243
import type { IntegrationResult } from '../integrations/integration';
4344
import type { ConnectionStateChangeEvent } from '../integrations/integrationService';
45+
import type { GitHubRepositoryDescriptor } from '../integrations/providers/github';
4446
import type { EnrichablePullRequest, ProviderActionablePullRequest } from '../integrations/providers/models';
4547
import {
4648
fromProviderPullRequest,
@@ -318,6 +320,34 @@ export class LaunchpadProvider implements Disposable {
318320
return { prs: prs, suggestionCounts: suggestionCounts };
319321
}
320322

323+
private async getSearchedPullRequests(search: string) {
324+
const { ownerAndRepo, prNumber } = getPullRequestIdentityValuesFromSearch(search);
325+
let result: TimedResult<SearchedPullRequest[] | undefined> | undefined;
326+
327+
if (prNumber != null) {
328+
if (ownerAndRepo != null) {
329+
// TODO: This needs to be generalized to work outside of GitHub
330+
const integration = await this.container.integrations.get(HostingIntegrationId.GitHub);
331+
const [owner, repo] = ownerAndRepo.split('/', 2);
332+
const descriptor: GitHubRepositoryDescriptor = {
333+
key: ownerAndRepo,
334+
owner: owner,
335+
name: repo,
336+
};
337+
const pr = await withDurationAndSlowEventOnTimeout(
338+
integration?.getPullRequest(descriptor, prNumber),
339+
'getPullRequest',
340+
this.container,
341+
);
342+
if (pr?.value != null) {
343+
result = { value: [{ pullRequest: pr.value, reasons: [] }], duration: pr.duration };
344+
return { prs: result, suggestionCounts: undefined };
345+
}
346+
}
347+
}
348+
return { prs: undefined, suggestionCounts: undefined };
349+
}
350+
321351
private _enrichedItems: CachedLaunchpadPromise<TimedResult<EnrichedItem[]>> | undefined;
322352
@debug<LaunchpadProvider['getEnrichedItems']>({ args: { 0: o => `force=${o?.force}` } })
323353
private async getEnrichedItems(options?: { cancellation?: CancellationToken; force?: boolean }) {
@@ -618,12 +648,12 @@ export class LaunchpadProvider implements Disposable {
618648
@gate<LaunchpadProvider['getCategorizedItems']>(o => `${o?.force ?? false}`)
619649
@log<LaunchpadProvider['getCategorizedItems']>({ args: { 0: o => `force=${o?.force}`, 1: false } })
620650
async getCategorizedItems(
621-
options?: { force?: boolean },
651+
options?: { force?: boolean; search?: string },
622652
cancellation?: CancellationToken,
623653
): Promise<LaunchpadCategorizedResult> {
624654
const scope = getLogScope();
625655

626-
const fireRefresh = options?.force || this._prs == null;
656+
const fireRefresh = !options?.search && (options?.force || this._prs == null);
627657

628658
const ignoredRepositories = new Set(
629659
(configuration.get('launchpad.ignoredRepositories') ?? []).map(r => r.toLowerCase()),
@@ -644,7 +674,9 @@ export class LaunchpadProvider implements Disposable {
644674
const [_, enrichedItemsResult, prsWithCountsResult] = await Promise.allSettled([
645675
this.container.git.isDiscoveringRepositories,
646676
this.getEnrichedItems({ force: options?.force, cancellation: cancellation }),
647-
this.getPullRequestsWithSuggestionCounts({ force: options?.force, cancellation: cancellation }),
677+
options?.search
678+
? this.getSearchedPullRequests(options.search)
679+
: this.getPullRequestsWithSuggestionCounts({ force: options?.force, cancellation: cancellation }),
648680
]);
649681

650682
if (cancellation?.isCancellationRequested) throw new CancellationError();
@@ -758,7 +790,7 @@ export class LaunchpadProvider implements Disposable {
758790
item.suggestedActionCategory,
759791
)!;
760792
// category overrides
761-
if (staleDate != null && item.updatedDate.getTime() < staleDate.getTime()) {
793+
if (!options?.search && staleDate != null && item.updatedDate.getTime() < staleDate.getTime()) {
762794
actionableCategory = 'other';
763795
} else if (codeSuggestionsCount > 0 && item.viewer.isAuthor) {
764796
actionableCategory = 'code-suggestions';
@@ -794,7 +826,10 @@ export class LaunchpadProvider implements Disposable {
794826
};
795827
return result;
796828
} finally {
797-
this.updateGroupedIds(result?.items ?? []);
829+
if (!options?.search) {
830+
this.updateGroupedIds(result?.items ?? []);
831+
}
832+
798833
if (result != null && fireRefresh) {
799834
this._onDidRefresh.fire(result);
800835
}
@@ -1065,7 +1100,12 @@ const slowEventTimeout = 1000 * 30; // 30 seconds
10651100

10661101
function withDurationAndSlowEventOnTimeout<T>(
10671102
promise: Promise<T>,
1068-
name: 'getMyPullRequests' | 'getCodeSuggestionCounts' | 'getCodeSuggestions' | 'getEnrichedItems',
1103+
name:
1104+
| 'getPullRequest'
1105+
| 'getMyPullRequests'
1106+
| 'getCodeSuggestionCounts'
1107+
| 'getCodeSuggestions'
1108+
| 'getEnrichedItems',
10691109
container: Container,
10701110
): Promise<TimedResult<T>> {
10711111
return timedWithSlowThreshold(promise, {

0 commit comments

Comments
 (0)