diff --git a/README.md b/README.md index 950b250..18ac6c9 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Kickstart your project with a Starter Template in VSCode - [Create Vue](https://github.com/vuejs/create-vue) - The recommended way to start a Vite-powered Vue project. - [Vitesse](https://github.com/antfu/vitesse) - Opinionated starter template. - [Vitesse Lite](https://github.com/antfu/vitesse-lite) - Lightweight version of Vitesse. +- [Create Next App](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) - The easiest way to get started with Next.js. - [Starter TS](https://github.com/antfu/starter-ts) - Starter template for TypeScript library. - [Vitesse WebExt](https://github.com/antfu/vitesse-webext) - WebExtension Vite Starter Template. - ... diff --git a/package.json b/package.json index 47e527f..6d32842 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "publisher": "YRM", "name": "starter-templates", "displayName": "Starter Templates", - "version": "0.5.0", + "version": "0.6.0", "description": "Kickstart your project with a Starter Template in VSCode", "license": "MIT", "repository": { @@ -118,6 +118,50 @@ "scope": "window" } } + }, + { + "title": "Create Next App(Official)", + "order": 3, + "properties": { + "starterTemplates.createNextApp.needsTypeScript": { + "type": "boolean", + "default": true, + "markdownDescription": "Would you like to use TypeScript?", + "scope": "window" + }, + "starterTemplates.createNextApp.needsEslint": { + "type": "boolean", + "default": true, + "markdownDescription": "Would you like to use ESLint?", + "scope": "window" + }, + "starterTemplates.createNextApp.needsTailwind": { + "type": "boolean", + "default": true, + "markdownDescription": "Would you like to use Tailwind CSS?", + "scope": "window" + }, + "starterTemplates.createNextApp.needsSrcDirectory": { + "type": "boolean", + "default": true, + "markdownDescription": "Would you like to use `src/` directory?", + "scope": "window" + }, + "starterTemplates.createNextApp.needsAppRouter": { + "type": "boolean", + "default": true, + "markdownDescription": "Would you like to use App Router? (recommended)", + "scope": "window" + }, + "starterTemplates.createNextApp.customizeTheDefaultImportAlias": { + "type": "string", + "default": "@/*", + "pattern": "^.+\\/\\*$", + "minLength": 3, + "description": "What import alias would you like configured? (Import alias must follow the pattern /*)", + "scope": "window" + } + } } ] }, @@ -143,7 +187,7 @@ "@vscode/test-electron": "^2.3.8", "degit": "^2.8.4", "eslint": "^8.54.0", - "execa": "^5.1.1", + "execa": "^8.0.1", "nypm": "^0.3.3", "tsup": "^8.0.1", "typescript": "^5.3.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d881065..c2b490a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,8 +39,8 @@ devDependencies: specifier: ^8.54.0 version: 8.56.0 execa: - specifier: ^5.1.1 - version: 5.1.1 + specifier: ^8.0.1 + version: 8.0.1 nypm: specifier: ^0.3.3 version: 0.3.3 diff --git a/resources/next.svg b/resources/next.svg new file mode 100644 index 0000000..595c03f --- /dev/null +++ b/resources/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/commands/starter.ts b/src/commands/starter.ts index d9d6fa6..7d8286b 100644 --- a/src/commands/starter.ts +++ b/src/commands/starter.ts @@ -1,10 +1,10 @@ import * as fs from 'node:fs' import * as path from 'node:path' import { chdir, cwd } from 'node:process' -import type { QuickPickItem } from 'vscode' +import type { InputBoxValidationMessage, QuickPickItem } from 'vscode' import { ProgressLocation, QuickPickItemKind, Uri, commands, window } from 'vscode' import degit from 'degit' -import execa from 'execa' +import { $ } from 'execa' import { installDependencies } from 'nypm' import type { Logger, ProjectTemplate, StarterCreateCommandArgs, StarterCreateTriggerData } from '../types' import { fsPath, nextAvailableFilename } from '../shared/utils/fs' @@ -31,65 +31,60 @@ export class StarterCommands extends BaseCommands { if (!triggerData) return - const { templateId, projectName } = triggerData - switch (templateId) { - case 'nuxt3-minimal-starter': - await degit('nuxt/starter/#v3').clone(`${projectPath}`) - break - case 'vitesse-nuxt3': - await degit('antfu/vitesse-nuxt3').clone(`${projectPath}`) - break - case 'create-vue': - chdir(projectPath) - chdir('..') - const dir = cwd() - const args = ['--force'] - if (config.createVueNeedsTypeScript) - args.push('--ts') - - if (config.createVueNeedsJsx) - args.push('--jsx') - - if (config.createVueNeedsRouter) - args.push('--router') - - if (config.createVueNeedsPinia) - args.push('--pinia') - - if (config.createVueNeedsVitest) - args.push('--vitest') - - if (config.createVueEndToEndTestingSolution !== 'No') - args.push(`--${config.createVueEndToEndTestingSolution.toLocaleLowerCase()}`) - else - args.push(`--tests false`) - - if (config.createVueNeedsEslint) { - if (config.createVueNeedsPrettier) - args.push('--eslint-with-prettier') - else - args.push('--eslint') + await window.withProgress({ + location: ProgressLocation.Notification, + }, async (progress) => { + progress.report({ + message: 'Creating project...', + }) + + const { templateId, projectName } = triggerData + chdir(projectPath!) + chdir('..') + const dir = cwd() + try { + switch (templateId) { + case 'nuxt3-minimal-starter': + await degit('nuxt/starter/#v3').clone(`${projectPath}`) + break + case 'vitesse-nuxt3': + await degit('antfu/vitesse-nuxt3').clone(`${projectPath}`) + break + case 'create-vue': + await this.handleCreateVue(projectName, dir) + break + case 'vitesse': + await degit('antfu/vitesse').clone(`${projectPath}`) + break + case 'vitesse-lite': + await degit('antfu/vitesse-lite').clone(`${projectPath}`) + break + case 'create-next-app': + await this.handleCreateNextApp(projectName) + break + case 'starter-ts': + await degit('antfu/starter-ts').clone(`${projectPath}`) + break + case 'vitesse-webext': + await degit('antfu/vitesse-webext').clone(`${projectPath}`) + break + + default: + break } - await execa('npx', ['create-vue', projectName, dir, ...args]) - break - case 'vitesse': - await degit('antfu/vitesse').clone(`${projectPath}`) - break - case 'vitesse-lite': - await degit('antfu/vitesse-lite').clone(`${projectPath}`) - break - case 'vitesse-webext': - await degit('antfu/vitesse-webext').clone(`${projectPath}`) - break - case 'starter-ts': - await degit('antfu/starter-ts').clone(`${projectPath}`) - break + } + catch (error) { + window.showErrorMessage('Failed to create prpject!') + } - default: - break - } + if (templateId !== 'create-next-app') + await this.handleCommonActions(projectPath!) + }) + } + + private async handleCommonActions(projectPath: string) { if (config.globalNeedsGitInit) - await execa('git', ['init', `${projectPath}`]) + await $`git init ${projectPath!}` if (config.globalNeedsInstall) { await window.withProgress({ @@ -98,18 +93,92 @@ export class StarterCommands extends BaseCommands { progress.report({ message: 'Installing dependencies...', }) - await installDependencies({ - cwd: projectPath, - silent: true, - packageManager: { - name: config.globalPackageManager, - command: config.globalPackageManager, - }, - }) + try { + await installDependencies({ + cwd: projectPath, + silent: true, + packageManager: { + name: config.globalPackageManager, + command: config.globalPackageManager, + }, + }) + } + catch (error) { + window.showErrorMessage('Failed to install dependencies!') + } }) - return 0 } - return 0 + } + + private async handleCreateNextApp(projectName: string) { + const args = [] + if (config.createNextAppNeedsTypeScript) + args.push('--ts') + else + args.push('--js') + + if (config.createNextAppNeedsEslint) + args.push('--eslint') + else + args.push('--no-eslint') + + if (config.createNextAppNeedsTailwind) + args.push('--tailwind') + else + args.push('--no-tailwind') + + if (config.createNextAppNeedsSrcDirectory) + args.push('--src-dir') + else + args.push('--no-src-dir') + + if (config.createNextAppNeedsAppRouter) + args.push('--app') + else + args.push('--no-app') + + if (config.createNextAppCustomizeTheDefaultImportAlias) { + args.push('--import-alias') + args.push(config.createNextAppCustomizeTheDefaultImportAlias) + } + if (config.globalPackageManager) + args.push(`--use-${config.globalPackageManager}`) + + await $`npx create-next-app ${projectName} ${args}` + } + + private async handleCreateVue(projectName: string, dir: string) { + const args = [] + args.push('--force') + if (config.createVueNeedsTypeScript) + args.push('--ts') + + if (config.createVueNeedsJsx) + args.push('--jsx') + + if (config.createVueNeedsRouter) + args.push('--router') + + if (config.createVueNeedsPinia) + args.push('--pinia') + + if (config.createVueNeedsVitest) + args.push('--vitest') + + if (config.createVueEndToEndTestingSolution !== 'No') + args.push(`--${config.createVueEndToEndTestingSolution.toLocaleLowerCase()}`) + + else + args.push(`--tests false`) + + if (config.createVueNeedsEslint) { + if (config.createVueNeedsPrettier) + args.push('--eslint-with-prettier') + + else + args.push('--eslint') + } + await $`npx create-vue ${projectName} ${dir} ${args}` } private async createProject() { @@ -182,6 +251,19 @@ export class StarterCommands extends BaseCommands { detail: 'Lightweight version of Vitesse', template: { id: 'vitesse-lite', defaultProjectName: 'vue-vitesse-lite-project' }, }, + { + kind: QuickPickItemKind.Separator, + label: 'Next', + }, + { + label: 'Create Next App(Official)', + iconPath: { + dark: Uri.file(this.context.asAbsolutePath('resources/next.svg')), + light: Uri.file(this.context.asAbsolutePath('resources/next.svg')), + }, + detail: 'The easiest way to get started with Next.js', + template: { id: 'create-next-app', defaultProjectName: 'next-project' }, + }, { kind: QuickPickItemKind.Separator, label: 'TypeScript Library', @@ -282,6 +364,17 @@ export class StarterCommands extends BaseCommands { return `A project with this name already exists within the selected directory` } + private getCurrentCreateSettings(template: ProjectTemplate): PickableSetting[] { + switch (template.id) { + case 'create-vue': + return [...this.getCurrentCreateSettingsOfCreateVue(), ...this.getGlobalSettings()] + case 'create-next-app': + return this.getCurrentCreateSettingsOfCreateNextApp() + default: + return this.getGlobalSettings() + } + } + private getGlobalSettings(): PickableSetting[] { return [ { @@ -316,89 +409,149 @@ export class StarterCommands extends BaseCommands { ] } - private getCurrentCreateSettings(template: ProjectTemplate): PickableSetting[] { - switch (template.id) { - case 'create-vue': - const createVueSettings: PickableSetting[] = [ - { - kind: QuickPickItemKind.Separator, - label: 'Create Vue', - }, - { - currentValue: config.createVueNeedsTypeScript ? 'Yes' : 'No', - description: config.createVueNeedsTypeScript ? 'Yes' : 'No', - detail: '', - label: 'Add TypeScript?', - setValue: (newValue: boolean) => config.setCreateVueNeedsTypeScript(newValue), - settingKind: 'BOOL', - }, - { - currentValue: config.createVueNeedsJsx ? 'Yes' : 'No', - description: config.createVueNeedsJsx ? 'Yes' : 'No', - detail: '', - label: 'Add JSX Support?', - setValue: (newValue: boolean) => config.setCreateVueNeedsJsx(newValue), - settingKind: 'BOOL', - }, - { - currentValue: config.createVueNeedsRouter ? 'Yes' : 'No', - description: config.createVueNeedsRouter ? 'Yes' : 'No', - detail: '', - label: 'Add Vue Router for Single Page Application development?', - setValue: (newValue: boolean) => config.setCreateVueNeedsRouter(newValue), - settingKind: 'BOOL', - }, - { - currentValue: config.createVueNeedsPinia ? 'Yes' : 'No', - description: config.createVueNeedsPinia ? 'Yes' : 'No', - detail: '', - label: 'Add Pinia for state management?', - setValue: (newValue: boolean) => config.setCreateVueNeedsPinia(newValue), - settingKind: 'BOOL', - }, - { - currentValue: config.createVueNeedsVitest ? 'Yes' : 'No', - description: config.createVueNeedsVitest ? 'Yes' : 'No', - detail: '', - label: 'Add Vitest for Unit Testing?', - setValue: (newValue: boolean) => config.setCreateVueNeedsVitest(newValue), - settingKind: 'BOOL', - }, - { - currentValue: config.createVueEndToEndTestingSolution || 'Cypress', - description: config.createVueEndToEndTestingSolution || 'Cypress', - detail: '', - enumValues: ['Cypress', 'Nightwatch', 'Playwright', 'No'], - label: 'Add an End-to-End Testing Solution?', - setValue: (newValue: 'Cypress' | 'Nightwatch' | 'Playwright' | 'No') => config.setCreateVueEndToEndTestingSolution(newValue), - settingKind: 'ENUM', - }, - { - currentValue: config.createVueNeedsEslint ? 'Yes' : 'No', - description: config.createVueNeedsEslint ? 'Yes' : 'No', - detail: '', - label: 'Add ESLint for code quality?', - setValue: (newValue: boolean) => config.setCreateVueNeedsEslint(newValue), - settingKind: 'BOOL', - }, - ] - - if (config.createVueNeedsEslint) { - createVueSettings.push( - { - currentValue: config.createVueNeedsPrettier ? 'Yes' : 'No', - description: config.createVueNeedsPrettier ? 'Yes' : 'No', - detail: '', - label: 'Add Prettier for code formatting?', - setValue: (newValue: boolean) => config.setCreateVueNeedsPrettier(newValue), - settingKind: 'BOOL', - }, - ) - } - return [...createVueSettings, ...this.getGlobalSettings()] - default: - return [...this.getGlobalSettings()] + private getCurrentCreateSettingsOfCreateVue() { + const createVueSettings: PickableSetting[] = [ + { + kind: QuickPickItemKind.Separator, + label: 'Create Vue', + }, + { + currentValue: config.createVueNeedsTypeScript ? 'Yes' : 'No', + description: config.createVueNeedsTypeScript ? 'Yes' : 'No', + detail: '', + label: 'Add TypeScript?', + setValue: (newValue: boolean) => config.setCreateVueNeedsTypeScript(newValue), + settingKind: 'BOOL', + }, + { + currentValue: config.createVueNeedsJsx ? 'Yes' : 'No', + description: config.createVueNeedsJsx ? 'Yes' : 'No', + detail: '', + label: 'Add JSX Support?', + setValue: (newValue: boolean) => config.setCreateVueNeedsJsx(newValue), + settingKind: 'BOOL', + }, + { + currentValue: config.createVueNeedsRouter ? 'Yes' : 'No', + description: config.createVueNeedsRouter ? 'Yes' : 'No', + detail: '', + label: 'Add Vue Router for Single Page Application development?', + setValue: (newValue: boolean) => config.setCreateVueNeedsRouter(newValue), + settingKind: 'BOOL', + }, + { + currentValue: config.createVueNeedsPinia ? 'Yes' : 'No', + description: config.createVueNeedsPinia ? 'Yes' : 'No', + detail: '', + label: 'Add Pinia for state management?', + setValue: (newValue: boolean) => config.setCreateVueNeedsPinia(newValue), + settingKind: 'BOOL', + }, + { + currentValue: config.createVueNeedsVitest ? 'Yes' : 'No', + description: config.createVueNeedsVitest ? 'Yes' : 'No', + detail: '', + label: 'Add Vitest for Unit Testing?', + setValue: (newValue: boolean) => config.setCreateVueNeedsVitest(newValue), + settingKind: 'BOOL', + }, + { + currentValue: config.createVueEndToEndTestingSolution || 'Cypress', + description: config.createVueEndToEndTestingSolution || 'Cypress', + detail: '', + enumValues: ['Cypress', 'Nightwatch', 'Playwright', 'No'], + label: 'Add an End-to-End Testing Solution?', + setValue: (newValue: 'Cypress' | 'Nightwatch' | 'Playwright' | 'No') => config.setCreateVueEndToEndTestingSolution(newValue), + settingKind: 'ENUM', + }, + { + currentValue: config.createVueNeedsEslint ? 'Yes' : 'No', + description: config.createVueNeedsEslint ? 'Yes' : 'No', + detail: '', + label: 'Add ESLint for code quality?', + setValue: (newValue: boolean) => config.setCreateVueNeedsEslint(newValue), + settingKind: 'BOOL', + }, + ] + + if (config.createVueNeedsEslint) { + createVueSettings.push( + { + currentValue: config.createVueNeedsPrettier ? 'Yes' : 'No', + description: config.createVueNeedsPrettier ? 'Yes' : 'No', + detail: '', + label: 'Add Prettier for code formatting?', + setValue: (newValue: boolean) => config.setCreateVueNeedsPrettier(newValue), + settingKind: 'BOOL', + }, + ) } + return createVueSettings + } + + private getCurrentCreateSettingsOfCreateNextApp() { + const createNextAppSettings: PickableSetting[] = [ + { + kind: QuickPickItemKind.Separator, + label: 'Create Next App', + }, + { + currentValue: config.createNextAppNeedsTypeScript ? 'Yes' : 'No', + description: config.createNextAppNeedsTypeScript ? 'Yes' : 'No', + detail: '', + label: 'Would you like to use TypeScript?', + setValue: (newValue: boolean) => config.setCreateNextAppNeedsTypeScript(newValue), + settingKind: 'BOOL', + }, + { + currentValue: config.createNextAppNeedsEslint ? 'Yes' : 'No', + description: config.createNextAppNeedsEslint ? 'Yes' : 'No', + detail: '', + label: 'Would you like to use ESLint?', + setValue: (newValue: boolean) => config.setCreateNextAppNeedsEslint(newValue), + settingKind: 'BOOL', + }, + { + currentValue: config.createNextAppNeedsTailwind ? 'Yes' : 'No', + description: config.createNextAppNeedsTailwind ? 'Yes' : 'No', + detail: '', + label: 'Would you like to use Tailwind CSS?', + setValue: (newValue: boolean) => config.setCreateNextAppNeedsTailwind(newValue), + settingKind: 'BOOL', + }, + { + currentValue: config.createNextAppNeedsSrcDirectory ? 'Yes' : 'No', + description: config.createNextAppNeedsSrcDirectory ? 'Yes' : 'No', + detail: '', + label: 'Would you like to use `src/` directory?', + setValue: (newValue: boolean) => config.setCreateNextAppNeedsSrcDirectory(newValue), + settingKind: 'BOOL', + }, + { + currentValue: config.createNextAppNeedsAppRouter ? 'Yes' : 'No', + description: config.createNextAppNeedsAppRouter ? 'Yes' : 'No', + detail: '', + label: 'Would you like to use App Router? (recommended)', + setValue: (newValue: boolean) => config.setCreateNextAppNeedsAppRouter(newValue), + settingKind: 'BOOL', + }, + { + currentValue: config.createNextAppCustomizeTheDefaultImportAlias || '@/*', + description: config.createNextAppCustomizeTheDefaultImportAlias || '@/*', + detail: 'Import alias must follow the pattern /*', + label: 'What import alias would you like configured?', + setValue: (newValue: string) => config.setCreateNextAppCustomizeTheDefaultImportAlias(newValue), + settingKind: 'STRING', + validation: s => this.validateCreateNextAppImportAlias(s), + }, + ] + return createNextAppSettings + } + + private validateCreateNextAppImportAlias(input: string): string | InputBoxValidationMessage | undefined | null | + Thenable { + if (!/^.+\/\*$/.test(input)) + return 'Import alias must follow the pattern /*' } } diff --git a/src/config.ts b/src/config.ts index 3147f21..db581a6 100644 --- a/src/config.ts +++ b/src/config.ts @@ -47,6 +47,13 @@ class Config { get createVueEndToEndTestingSolution(): 'Cypress' | 'Nightwatch' | 'Playwright' | 'No' { return this.getConfig<'Cypress' | 'Nightwatch' | 'Playwright' | 'No'>('createVue.endToEndTestingSolution', 'Cypress') } get createVueNeedsEslint(): boolean { return this.getConfig('createVue.needsEslint', true) } get createVueNeedsPrettier(): boolean { return this.getConfig('createVue.needsPrettier', true) } + // create next app + get createNextAppNeedsTypeScript(): boolean { return this.getConfig('createNextApp.needsTypeScript', true) } + get createNextAppNeedsEslint(): boolean { return this.getConfig('createNextApp.needsEslint', true) } + get createNextAppNeedsTailwind(): boolean { return this.getConfig('createNextApp.needsTailwind', true) } + get createNextAppNeedsSrcDirectory(): boolean { return this.getConfig('createNextApp.needsSrcDirectory', true) } + get createNextAppNeedsAppRouter(): boolean { return this.getConfig('createNextApp.needsAppRouter', true) } + get createNextAppCustomizeTheDefaultImportAlias(): string { return this.getConfig('createNextApp.customizeTheDefaultImportAlias', '@/*') } // global settings get globalNeedsGitInit(): boolean { return this.getConfig('globalSettings.needsGitInit', true) } get globalNeedsInstall(): boolean { return this.getConfig('globalSettings.needsInstall', true) } @@ -61,6 +68,13 @@ class Config { public setCreateVueEndToEndTestingSolution(value: 'Cypress' | 'Nightwatch' | 'Playwright' | 'No'): Promise { return this.setConfig('createVue.endToEndTestingSolution', value, ConfigurationTarget.Global) } public setCreateVueNeedsEslint(value: boolean): Promise { return this.setConfig('createVue.needsEslint', value, ConfigurationTarget.Global) } public setCreateVueNeedsPrettier(value: boolean): Promise { return this.setConfig('createVue.needsPrettier', value, ConfigurationTarget.Global) } + // create next app + public setCreateNextAppNeedsTypeScript(value: boolean): Promise { return this.setConfig('createNextApp.needsTypeScript', value, ConfigurationTarget.Global) } + public setCreateNextAppNeedsEslint(value: boolean): Promise { return this.setConfig('createNextApp.needsEslint', value, ConfigurationTarget.Global) } + public setCreateNextAppNeedsTailwind(value: boolean): Promise { return this.setConfig('createNextApp.needsTailwind', value, ConfigurationTarget.Global) } + public setCreateNextAppNeedsSrcDirectory(value: boolean): Promise { return this.setConfig('createNextApp.needsSrcDirectory', value, ConfigurationTarget.Global) } + public setCreateNextAppNeedsAppRouter(value: boolean): Promise { return this.setConfig('createNextApp.needsAppRouter', value, ConfigurationTarget.Global) } + public setCreateNextAppCustomizeTheDefaultImportAlias(value: string): Promise { return this.setConfig('createNextApp.customizeTheDefaultImportAlias', value, ConfigurationTarget.Global) } // global settings public setGlobalNeedsGitInit(value: boolean): Promise { return this.setConfig('globalSettings.needsGitInit', value, ConfigurationTarget.Global) } public setGlobalNeedsInstall(value: boolean): Promise { return this.setConfig('globalSettings.needsInstall', value, ConfigurationTarget.Global) } diff --git a/src/create.ts b/src/create.ts index dbcec19..0302bc8 100644 --- a/src/create.ts +++ b/src/create.ts @@ -34,9 +34,8 @@ async function handleStarterCreateTrigger(wf: WorkspaceFolder): Promise { try { markProjectCreationStarted() - const success = await createStarterProject(fsPath(wf.uri), json) - if (success) - handleStarterWelcome(wf, json) + await createStarterProject(fsPath(wf.uri), json) + handleStarterWelcome(wf, json) } finally { markProjectCreationEnded() diff --git a/src/shared/vscode/input.ts b/src/shared/vscode/input.ts index 8476412..8558e71 100644 --- a/src/shared/vscode/input.ts +++ b/src/shared/vscode/input.ts @@ -88,9 +88,10 @@ export async function editSetting(setting: PickableSetting) { const prompt = setting.detail // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const value = setting.currentValue + const validation = setting.validation switch (setting.settingKind) { case 'STRING': - const stringResult = await vs.window.showInputBox({ prompt, title, value: value as string | undefined }) + const stringResult = await vs.window.showInputBox({ prompt, title, value: value as string | undefined, validateInput: validation }) if (stringResult !== undefined) await setting.setValue!(stringResult) break @@ -159,9 +160,13 @@ export type PickableSetting = vs.QuickPickItem & Partial<({ currentValue: any setValue: (newValue: any) => Promise enumValues?: string[] + validation?: (s: string) => string | vs.InputBoxValidationMessage | undefined | null | + Thenable } | { settingKind: 'MULTI_ENUM' currentValue: any[] setValue: (newValue: any[]) => Promise enumValues: Array<{ group?: string, values: string[] }> + validation?: (s: string) => string | vs.InputBoxValidationMessage | undefined | null | + Thenable })>