Skip to content

Commit

Permalink
Enrich Git extension's remote source provider API (#147613)
Browse files Browse the repository at this point in the history
* Allow custom title in remote source picker

* Include forks in query search results

* Support `RemoteSource.detail`

* Allow showing quickpicks in `getRemoteSources`

* Allow custom placeholder and remote source icons

* Add ability to customize placeholder

* Register and show recently opened sources

* Allow custom remote url labels

* Add a separator label for remote sources

* Update git-base typings

* Make showing recent sources opt in

* Add concept of recent remote source to `RemoteSourceProvider` concept

* Recent sources should be sorted by timestamp

* Pass current query to `getRemoteSources`

* Fix applying query
  • Loading branch information
joyceerhl authored and aeschli committed May 2, 2022
1 parent 73bc5ad commit dd8cbf9
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 29 deletions.
17 changes: 16 additions & 1 deletion extensions/git-base/src/api/git-base.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,12 @@ export interface GitBaseExtension {

export interface PickRemoteSourceOptions {
readonly providerLabel?: (provider: RemoteSourceProvider) => string;
readonly urlLabel?: string;
readonly urlLabel?: string | ((url: string) => string);
readonly providerName?: string;
readonly title?: string;
readonly placeholder?: string;
readonly branch?: boolean; // then result is PickRemoteSourceResult
readonly showRecentSources?: boolean;
}

export interface PickRemoteSourceResult {
Expand All @@ -44,17 +47,29 @@ export interface PickRemoteSourceResult {
export interface RemoteSource {
readonly name: string;
readonly description?: string;
readonly detail?: string;
/**
* Codicon name
*/
readonly icon?: string;
readonly url: string | string[];
}

export interface RecentRemoteSource extends RemoteSource {
readonly timestamp: number;
}

export interface RemoteSourceProvider {
readonly name: string;
/**
* Codicon name
*/
readonly icon?: string;
readonly label?: string;
readonly placeholder?: string;
readonly supportsQuery?: boolean;

getBranches?(url: string): ProviderResult<string[]>;
getRecentRemoteSources?(query?: string): ProviderResult<RecentRemoteSource[]>;
getRemoteSources(query?: string): ProviderResult<RemoteSource[]>;
}
81 changes: 55 additions & 26 deletions extensions/git-base/src/remoteSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { QuickPickItem, window, QuickPick } from 'vscode';
import { QuickPickItem, window, QuickPick, QuickPickItemKind } from 'vscode';
import * as nls from 'vscode-nls';
import { RemoteSourceProvider, RemoteSource, PickRemoteSourceOptions, PickRemoteSourceResult } from './api/git-base';
import { Model } from './model';
Expand All @@ -24,17 +24,20 @@ async function getQuickPickResult<T extends QuickPickItem>(quickpick: QuickPick<

class RemoteSourceProviderQuickPick {

private quickpick: QuickPick<QuickPickItem & { remoteSource?: RemoteSource }>;
private quickpick: QuickPick<QuickPickItem & { remoteSource?: RemoteSource }> | undefined;

constructor(private provider: RemoteSourceProvider) {
this.quickpick = window.createQuickPick();
this.quickpick.ignoreFocusOut = true;
constructor(private provider: RemoteSourceProvider) { }

if (provider.supportsQuery) {
this.quickpick.placeholder = localize('type to search', "Repository name (type to search)");
this.quickpick.onDidChangeValue(this.onDidChangeValue, this);
} else {
this.quickpick.placeholder = localize('type to filter', "Repository name");
private ensureQuickPick() {
if (!this.quickpick) {
this.quickpick = window.createQuickPick();
this.quickpick.ignoreFocusOut = true;
if (this.provider.supportsQuery) {
this.quickpick.placeholder = this.provider.placeholder ?? localize('type to search', "Repository name (type to search)");
this.quickpick.onDidChangeValue(this.onDidChangeValue, this);
} else {
this.quickpick.placeholder = this.provider.placeholder ?? localize('type to filter', "Repository name");
}
}
}

Expand All @@ -45,35 +48,37 @@ class RemoteSourceProviderQuickPick {

@throttle
private async query(): Promise<void> {
this.quickpick.busy = true;

try {
const remoteSources = await this.provider.getRemoteSources(this.quickpick.value) || [];
const remoteSources = await this.provider.getRemoteSources(this.quickpick?.value) || [];

this.ensureQuickPick();
this.quickpick!.show();

if (remoteSources.length === 0) {
this.quickpick.items = [{
this.quickpick!.items = [{
label: localize('none found', "No remote repositories found."),
alwaysShow: true
}];
} else {
this.quickpick.items = remoteSources.map(remoteSource => ({
label: remoteSource.name,
this.quickpick!.items = remoteSources.map(remoteSource => ({
label: remoteSource.icon ? `$(${remoteSource.icon}) ${remoteSource.name}` : remoteSource.name,
description: remoteSource.description || (typeof remoteSource.url === 'string' ? remoteSource.url : remoteSource.url[0]),
detail: remoteSource.detail,
remoteSource,
alwaysShow: true
}));
}
} catch (err) {
this.quickpick.items = [{ label: localize('error', "$(error) Error: {0}", err.message), alwaysShow: true }];
this.quickpick!.items = [{ label: localize('error', "$(error) Error: {0}", err.message), alwaysShow: true }];
console.error(err);
} finally {
this.quickpick.busy = false;
this.quickpick!.busy = false;
}
}

async pick(): Promise<RemoteSource | undefined> {
this.query();
const result = await getQuickPickResult(this.quickpick);
await this.query();
const result = await getQuickPickResult(this.quickpick!);
return result?.remoteSource;
}
}
Expand All @@ -83,6 +88,7 @@ export async function pickRemoteSource(model: Model, options: PickRemoteSourceOp
export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions = {}): Promise<string | PickRemoteSourceResult | undefined> {
const quickpick = window.createQuickPick<(QuickPickItem & { provider?: RemoteSourceProvider; url?: string })>();
quickpick.ignoreFocusOut = true;
quickpick.title = options.title;

if (options.providerName) {
const provider = model.getRemoteProviders()
Expand All @@ -93,24 +99,47 @@ export async function pickRemoteSource(model: Model, options: PickRemoteSourceOp
}
}

const providers = model.getRemoteProviders()
const remoteProviders = model.getRemoteProviders()
.map(provider => ({ label: (provider.icon ? `$(${provider.icon}) ` : '') + (options.providerLabel ? options.providerLabel(provider) : provider.name), alwaysShow: true, provider }));

quickpick.placeholder = providers.length === 0
const recentSources: (QuickPickItem & { url?: string; timestamp: number })[] = [];
if (options.showRecentSources) {
for (const { provider } of remoteProviders) {
const sources = (await provider.getRecentRemoteSources?.() ?? []).map((item) => {
return {
...item,
label: (item.icon ? `$(${item.icon}) ` : '') + item.name,
url: typeof item.url === 'string' ? item.url : item.url[0],
};
});
recentSources.push(...sources);
}
}

const items = [
{ kind: QuickPickItemKind.Separator, label: localize('remote sources', 'remote sources') },
...remoteProviders,
{ kind: QuickPickItemKind.Separator, label: localize('recently opened', 'recently opened') },
...recentSources.sort((a, b) => b.timestamp - a.timestamp)
];

quickpick.placeholder = options.placeholder ?? (remoteProviders.length === 0
? localize('provide url', "Provide repository URL")
: localize('provide url or pick', "Provide repository URL or pick a repository source.");
: localize('provide url or pick', "Provide repository URL or pick a repository source."));

const updatePicks = (value?: string) => {
if (value) {
const label = (typeof options.urlLabel === 'string' ? options.urlLabel : options.urlLabel?.(value)) ?? localize('url', "URL");
quickpick.items = [{
label: options.urlLabel ?? localize('url', "URL"),
label: label,
description: value,
alwaysShow: true,
url: value
},
...providers];
...items
];
} else {
quickpick.items = providers;
quickpick.items = items;
}
};

Expand Down
6 changes: 5 additions & 1 deletion extensions/github/src/remoteSourceProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ function asRemoteSource(raw: any): RemoteSource {
const protocol = workspace.getConfiguration('github').get<'https' | 'ssh'>('gitProtocol');
return {
name: `$(github) ${raw.full_name}`,
description: raw.description || undefined,
description: `${raw.stargazers_count > 0 ? `$(star-full) ${raw.stargazers_count}` : ''
}`,
detail: raw.description || undefined,
url: protocol === 'https' ? raw.clone_url : raw.ssh_url
};
}
Expand Down Expand Up @@ -75,6 +77,8 @@ export class GithubRemoteSourceProvider implements RemoteSourceProvider {
return [];
}

query += ` fork:true`;

const raw = await octokit.search.repos({ q: query, sort: 'stars' });
return raw.data.items.map(asRemoteSource);
}
Expand Down
17 changes: 16 additions & 1 deletion extensions/github/src/typings/git-base.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,12 @@ export interface GitBaseExtension {

export interface PickRemoteSourceOptions {
readonly providerLabel?: (provider: RemoteSourceProvider) => string;
readonly urlLabel?: string;
readonly urlLabel?: string | ((url: string) => string);
readonly providerName?: string;
readonly title?: string;
readonly placeholder?: string;
readonly branch?: boolean; // then result is PickRemoteSourceResult
readonly showRecentSources?: boolean;
}

export interface PickRemoteSourceResult {
Expand All @@ -44,17 +47,29 @@ export interface PickRemoteSourceResult {
export interface RemoteSource {
readonly name: string;
readonly description?: string;
readonly detail?: string;
/**
* Codicon name
*/
readonly icon?: string;
readonly url: string | string[];
}

export interface RecentRemoteSource extends RemoteSource {
readonly timestamp: number;
}

export interface RemoteSourceProvider {
readonly name: string;
/**
* Codicon name
*/
readonly icon?: string;
readonly label?: string;
readonly placeholder?: string;
readonly supportsQuery?: boolean;

getBranches?(url: string): ProviderResult<string[]>;
getRecentRemoteSources?(query?: string): ProviderResult<RecentRemoteSource[]>;
getRemoteSources(query?: string): ProviderResult<RemoteSource[]>;
}

0 comments on commit dd8cbf9

Please sign in to comment.