Skip to content

Commit 7360da7

Browse files
committed
extension/src: support lazy enum
This is the client side implementation for dynamically fetched enum. The extension will dynamically fetch enum entries from the language server (gopls) by calling interactive/listEnum with the user typed strings. gopls CL 743840 For golang/go#76331 Change-Id: Id3745407f718b160bee93d8978677d90216744aa Reviewed-on: https://go-review.googlesource.com/c/vscode-go/+/751740 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Peter Weinberger <pjw@google.com>
1 parent ca47843 commit 7360da7

3 files changed

Lines changed: 159 additions & 15 deletions

File tree

extension/src/language/form.ts

Lines changed: 157 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,58 @@ export interface FormFieldTypeNumber {
4141
kind: 'number';
4242
}
4343

44+
// FormEnumEntry represents a single option in an enumeration.
45+
export interface FormEnumEntry {
46+
// Value is the unique string identifier for this option.
47+
//
48+
// This is the value that will be sent back to the server in
49+
// 'FormAnswers' if the user selects this option.
50+
value: string;
51+
52+
// Description is the human-readable label presented to the user.
53+
description: string;
54+
}
55+
4456
// FormFieldTypeEnum defines a selection from a set of values.
57+
//
58+
// Use this type when:
59+
// - The number of options is small (e.g., < 20).
60+
// - All options are known at the time the form is created.
4561
export interface FormFieldTypeEnum {
4662
kind: 'enum';
4763

4864
// Name is an optional identifier for the enum type.
4965
name?: string;
5066

51-
// Values is the set of allowable options.
52-
values: string[];
67+
// Entries is the list of allowable options.
68+
entries: FormEnumEntry[];
69+
}
70+
71+
// FormFieldTypeLazyEnum defines a selection from a large or dynamic enum entry set.
72+
//
73+
// Use this type when:
74+
// 1. The dataset is too large to send efficiently in a single payload
75+
// (e.g., thousands of workspace symbols, file uri or cloud resources).
76+
// 2. The available options depend on the user's input (e.g., semantic search).
77+
// 3. Generating the list is expensive and should only be done if requested.
78+
//
79+
// The client is expected to render a search interface (e.g., a combo box with
80+
// a text input) and query the server via 'interactive/listEnum' as the user types.
81+
export interface FormFieldTypeLazyEnum {
82+
kind: 'lazyEnum';
83+
84+
// TODO(hxjiang): consider make debounce configurable since fetching
85+
// cloud resources could be expensive and slow.
5386

54-
// Description provides human-readable labels for the options.
55-
// This array must have the same length as values.
56-
description: string[];
87+
// Source identifies the data source on the server.
88+
//
89+
// Examples: "workspace/symbol", "database/schema", "git/tags".
90+
source: string;
91+
92+
// Config contains the static settings for the source.
93+
// The client treats this as opaque data and echoes it back in the
94+
// 'interactive/listEnum' request.
95+
config?: any;
5796
}
5897

5998
// FormFieldTypeList defines a homogenous list of items.
@@ -72,6 +111,7 @@ export type FormFieldType =
72111
| FormFieldTypeBool
73112
| FormFieldTypeNumber
74113
| FormFieldTypeEnum
114+
| FormFieldTypeLazyEnum
75115
| FormFieldTypeList;
76116

77117
// ----------------------------------------------------------------------------
@@ -253,6 +293,110 @@ export async function CollectAnswers(
253293
return answers;
254294
}
255295

296+
/**
297+
* InteractiveListEnumParams defines the parameters for the
298+
* 'interactive/listEnum' request.
299+
*/
300+
interface InteractiveListEnumParams {
301+
/**
302+
* Source identifies the data source on the server.
303+
*
304+
* The client treats this as opaque data and echoes it back in the
305+
* 'interactive/listEnum' request.
306+
*
307+
* Examples: "workspace/symbol", "database/schema", "git/tags".
308+
*/
309+
source: string;
310+
311+
/**
312+
* Config contains the static settings for the specified source.
313+
*
314+
* The client treats this as opaque data and echoes it back in the
315+
* 'interactive/listEnum' request.
316+
*/
317+
config?: any;
318+
319+
/**
320+
* A query string to filter enum entries by.
321+
*
322+
* The exact interpretation of this string (e.g., fuzzy matching, exact
323+
* match, prefix search, or regular expression) is entirely up to the
324+
* server and may vary depending on the source. This follows the similar
325+
* semantics as the standard 'workspace/symbol' request. Clients may
326+
* send an empty string here to request a default set of enum entries.
327+
*/
328+
query: string;
329+
}
330+
331+
/**
332+
* Opens a Quick Pick that dynamically fetches options from the Language Server.
333+
*/
334+
export async function pickLazyEnum(description: string, source: string, config: any = {}): Promise<string | undefined> {
335+
return new Promise((resolve) => {
336+
const quickPick = vscode.window.createQuickPick<vscode.QuickPickItem & { value: string }>();
337+
338+
quickPick.title = description;
339+
quickPick.placeholder = 'Type to search ' + source;
340+
quickPick.matchOnDescription = true;
341+
342+
let debounceTimeout: NodeJS.Timeout | undefined;
343+
let isResolved = false;
344+
345+
// Call "interactive/listEnum" and render result as entries as quick
346+
// pick items.
347+
const search = async (query: string) => {
348+
quickPick.busy = true;
349+
try {
350+
const params: InteractiveListEnumParams = {
351+
source: source,
352+
config: config,
353+
query: query
354+
};
355+
const result = await vscode.commands.executeCommand<FormEnumEntry[]>('gopls.lsp', {
356+
method: 'interactive/listEnum',
357+
param: params
358+
});
359+
360+
if (!result) {
361+
quickPick.items = [];
362+
return;
363+
}
364+
365+
quickPick.items = result.map((entry) => ({
366+
label: entry.description,
367+
detail: entry.value !== entry.description ? entry.value : undefined,
368+
value: entry.value
369+
}));
370+
} catch (e) {
371+
console.error('Error fetching enum options:', e);
372+
quickPick.items = [];
373+
} finally {
374+
quickPick.busy = false;
375+
}
376+
};
377+
378+
quickPick.onDidChangeValue((value) => {
379+
if (debounceTimeout) clearTimeout(debounceTimeout);
380+
debounceTimeout = setTimeout(() => search(value), 400);
381+
});
382+
383+
quickPick.onDidAccept(() => {
384+
const selection = quickPick.selectedItems[0];
385+
isResolved = true;
386+
resolve(selection ? selection.value : undefined);
387+
quickPick.hide();
388+
});
389+
390+
quickPick.onDidHide(() => {
391+
if (!isResolved) resolve(undefined);
392+
quickPick.dispose();
393+
});
394+
395+
quickPick.show();
396+
search(''); // Initial Trigger
397+
});
398+
}
399+
256400
/**
257401
* Helper to prompt for a single field based on its type.
258402
*/
@@ -344,17 +488,13 @@ async function promptForField(field: FormField, prevAnswer: any | undefined): Pr
344488
} as vscode.InputBoxOptions);
345489

346490
case 'enum': {
347-
const descriptions = type.description || [];
348-
349-
const pickItems = type.values.map((value, index) => {
350-
const description = descriptions[index];
351-
491+
const pickItems = type.entries.map((entry, _) => {
352492
return {
353493
// Use description if it exists, otherwise use value
354-
label: description || value,
494+
label: entry.description || entry.value,
355495
// Show value in detail if description exists
356-
description: description ? value : undefined,
357-
value: value
496+
description: entry.description ? entry.value : undefined,
497+
value: entry.value
358498
};
359499
});
360500

@@ -366,6 +506,10 @@ async function promptForField(field: FormField, prevAnswer: any | undefined): Pr
366506
return selected ? selected.value : undefined;
367507
}
368508

509+
case 'lazyEnum': {
510+
return await pickLazyEnum(field.description, type.source, type.config);
511+
}
512+
369513
case 'bool': {
370514
const boolItems = [
371515
{ label: 'Yes', value: true },

extension/src/language/goLanguageServer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ export class GoLanguageClient extends LanguageClient implements vscode.Disposabl
365365
// See https://github.com/microsoft/vscode-languageserver-node/issues/1607
366366
const experimental: LSPObject = {
367367
progressMessageStyles: ['log'],
368-
interactiveInputTypes: ['string', 'bool', 'number', 'enum', 'documentURI']
368+
interactiveInputTypes: ['bool', 'documentURI', 'enum', 'lazyEnum', 'number', 'string']
369369
};
370370
params.capabilities.experimental = experimental;
371371
}

extension/test/mocks/MockMemento.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export class MockMemento implements Memento {
1515
public get(key: any, defaultValue?: any): any;
1616
public get<T>(key: string, defaultValue?: T): T {
1717
const exists = this._value.hasOwnProperty(key);
18-
return exists ? this._value[key] : (defaultValue! as any);
18+
return exists ? (this._value[key] as T) : (defaultValue as T);
1919
}
2020

2121
public update(key: string, value: any): Thenable<void> {

0 commit comments

Comments
 (0)