Skip to content

Commit

Permalink
Searches GitHub PR by the entered URL
Browse files Browse the repository at this point in the history
  • Loading branch information
sergeibbb committed Nov 6, 2024
1 parent 6f8b959 commit 9e3445c
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 34 deletions.
2 changes: 1 addition & 1 deletion docs/telemetry-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -1305,7 +1305,7 @@ void
```typescript
{
'timeout': number,
'operation': 'getMyPullRequests' | 'getCodeSuggestions' | 'getEnrichedItems' | 'getCodeSuggestionCounts',
'operation': 'getPullRequest' | 'getMyPullRequests' | 'getCodeSuggestions' | 'getEnrichedItems' | 'getCodeSuggestionCounts',
'duration': number
}
```
Expand Down
7 changes: 6 additions & 1 deletion src/constants.telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,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:
| 'getPullRequest'
| 'getMyPullRequests'
| 'getCodeSuggestions'
| 'getEnrichedItems'
| 'getCodeSuggestionCounts';
duration: number;
};

Expand Down
64 changes: 38 additions & 26 deletions src/plus/launchpad/launchpad.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ export class LaunchpadCommand extends QuickCommand<State> {
i: LaunchpadItem,
ui: LaunchpadGroup,
topItem: LaunchpadItem | undefined,
alwaysShow: boolean | undefined,
): LaunchpadItemQuickPickItem => {
const buttons = [];

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

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

const getItems = (result: LaunchpadCategorizedResult) => {
const getItems = (result: LaunchpadCategorizedResult, isSearching?: boolean) => {
const items: (LaunchpadItemQuickPickItem | DirectiveQuickPickItem | ConnectMoreIntegrationsItem)[] = [];

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

items.push(...buildGroupHeading(ui, groupItems.length));

if (context.collapsed.get(ui)) continue;
if (!isSearching) {
items.push(...buildGroupHeading(ui, groupItems.length));
if (context.collapsed.get(ui)) {
continue;
}
}

items.push(...groupItems.map(i => buildLaunchpadQuickPickItem(i, ui, topItem)));
items.push(...groupItems.map(i => buildLaunchpadQuickPickItem(i, ui, topItem, isSearching)));
}
}

return items;
};

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

return {
placeholder: 'Choose an item to focus on',
items: getItems(context.result),
items: getItems(context.result, isSearching),
};
}

const updateItems = async (
quickpick: QuickPick<LaunchpadItemQuickPickItem | DirectiveQuickPickItem | ConnectMoreIntegrationsItem>,
) => {
const search = quickpick.value;
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;
Expand All @@ -527,8 +533,7 @@ export class LaunchpadCommand extends QuickCommand<State> {
};

const { items, placeholder } = getItemsAndPlaceholder();

let groupsHidden = false;
const nonGroupedItems = items.filter(i => !isDirectiveQuickPickItem(i));

const step = createPickStep({
title: context.title,
Expand All @@ -543,29 +548,27 @@ export class LaunchpadCommand extends QuickCommand<State> {
LaunchpadSettingsQuickInputButton,
RefreshQuickInputButton,
],
onDidChangeValue: quickpick => {
onDidChangeValue: async quickpick => {
const { value } = quickpick;
const hideGroups = Boolean(quickpick.value?.length);

if (groupsHidden !== hideGroups) {
groupsHidden = hideGroups;
quickpick.items = hideGroups ? items.filter(i => !isDirectiveQuickPickItem(i)) : items;
}
const activeLaunchpadItems = quickpick.activeItems.filter(
(i): i is LaunchpadItemQuickPickItem => 'item' in i && !i.alwaysShow,
);
const hideGroups = Boolean(value?.length);
const consideredItems = hideGroups ? nonGroupedItems : items;

let updated = false;
for (const item of quickpick.items) {
for (const item of consideredItems) {
if (item.alwaysShow) {
item.alwaysShow = false;
updated = true;
}
}
if (updated) {
// Force quickpick to update by changing the items object:
quickpick.items = [...quickpick.items];
}

// By doing the following we make sure we operate with the PRs that belong to Launchpad initially.
// Also, when we re-create the array, we make sure that `alwaysShow` updates are applied.
quickpick.items =
updated && quickpick.items === consideredItems ? [...consideredItems] : consideredItems;

const activeLaunchpadItems = quickpick.activeItems.filter(
(i): i is LaunchpadItemQuickPickItem => 'item' in i,
);

if (!value?.length || activeLaunchpadItems.length) {
// Nothing to search
Expand All @@ -588,7 +591,12 @@ export class LaunchpadCommand extends QuickCommand<State> {
item.alwaysShow = true;
// Force quickpick to update by changing the items object:
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);
}
}

Expand Down Expand Up @@ -1377,7 +1385,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);
Expand Down
52 changes: 46 additions & 6 deletions src/plus/launchpad/launchpadProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type { PullRequest, SearchedPullRequest } from '../../git/models/pullRequ
import {
getComparisonRefsForPullRequest,
getOrOpenPullRequestRepository,
getPullRequestIdentityValuesFromSearch,
getRepositoryIdentityForPullRequest,
} from '../../git/models/pullRequest';
import type { GitRemote } from '../../git/models/remote';
Expand All @@ -41,6 +42,7 @@ 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 type { EnrichablePullRequest, ProviderActionablePullRequest } from '../integrations/providers/models';
import {
fromProviderPullRequest,
Expand Down Expand Up @@ -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<SearchedPullRequest[] | undefined> | 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<TimedResult<EnrichedItem[]>> | undefined;
@debug<LaunchpadProvider['getEnrichedItems']>({ args: { 0: o => `force=${o?.force}` } })
private async getEnrichedItems(options?: { cancellation?: CancellationToken; force?: boolean }) {
Expand Down Expand Up @@ -618,12 +648,12 @@ export class LaunchpadProvider implements Disposable {
@gate<LaunchpadProvider['getCategorizedItems']>(o => `${o?.force ?? false}`)
@log<LaunchpadProvider['getCategorizedItems']>({ args: { 0: o => `force=${o?.force}`, 1: false } })
async getCategorizedItems(
options?: { force?: boolean },
options?: { force?: boolean; search?: string },
cancellation?: CancellationToken,
): Promise<LaunchpadCategorizedResult> {
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()),
Expand All @@ -644,7 +674,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();
Expand Down Expand Up @@ -758,7 +790,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';
Expand Down Expand Up @@ -794,7 +826,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);
}
Expand Down Expand Up @@ -1065,7 +1100,12 @@ const slowEventTimeout = 1000 * 30; // 30 seconds

function withDurationAndSlowEventOnTimeout<T>(
promise: Promise<T>,
name: 'getMyPullRequests' | 'getCodeSuggestionCounts' | 'getCodeSuggestions' | 'getEnrichedItems',
name:
| 'getPullRequest'
| 'getMyPullRequests'
| 'getCodeSuggestionCounts'
| 'getCodeSuggestions'
| 'getEnrichedItems',
container: Container,
): Promise<TimedResult<T>> {
return timedWithSlowThreshold(promise, {
Expand Down

0 comments on commit 9e3445c

Please sign in to comment.