From b0a7abcc63636937298416587ec85134eaa989fe Mon Sep 17 00:00:00 2001 From: Nathan Turinski Date: Thu, 30 Oct 2025 12:22:11 -0700 Subject: [PATCH] Group templates by filter rather than filtering it out --- package.json | 1 + .../createFunction/FunctionListStep.ts | 82 ++++++------------- src/constants.ts | 1 - src/templates/CentralTemplateProvider.ts | 24 +++--- src/templates/IFunctionTemplate.ts | 5 +- src/templates/dotnet/parseDotnetTemplates.ts | 5 +- .../verifyVSCodeConfigOnActivate.ts | 4 +- test/createFunction/FunctionTesterBase.ts | 6 +- test/global.test.ts | 5 +- test/templateCount.test.ts | 4 +- 10 files changed, 53 insertions(+), 84 deletions(-) diff --git a/package.json b/package.json index ffa8b5fcd..7933da663 100644 --- a/package.json +++ b/package.json @@ -1078,6 +1078,7 @@ "title": "Azure Functions", "properties": { "azureFunctions.templateFilter": { + "deprecationMessage": "This setting is no longer used to filter templates, but is being left to explain what the filter enumerations mean.", "scope": "resource", "type": "string", "default": "Verified", diff --git a/src/commands/createFunction/FunctionListStep.ts b/src/commands/createFunction/FunctionListStep.ts index a5650a0fb..7c095734a 100644 --- a/src/commands/createFunction/FunctionListStep.ts +++ b/src/commands/createFunction/FunctionListStep.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AzureWizardPromptStep, type IActionContext, type IAzureQuickPickItem, type IAzureQuickPickOptions, type IWizardOptions } from '@microsoft/vscode-azext-utils'; +import { AzureWizardPromptStep, type IAzureQuickPickItem, type IWizardOptions } from '@microsoft/vscode-azext-utils'; import * as escape from 'escape-string-regexp'; import { type FuncVersion } from '../../FuncVersion'; -import { JavaBuildTool, ProjectLanguage, TemplateFilter, templateFilterSetting } from '../../constants'; +import { JavaBuildTool, ProjectLanguage } from '../../constants'; import { ext } from '../../extensionVariables'; import { localize } from '../../localize'; import { type FunctionTemplateBase, type IFunctionTemplate } from '../../templates/IFunctionTemplate'; @@ -14,7 +14,7 @@ import { TemplateSchemaVersion } from '../../templates/TemplateProviderBase'; import { durableUtils } from '../../utils/durableUtils'; import { nonNullProp } from '../../utils/nonNull'; import { isNodeV4Plus, isPythonV2Plus, nodeV4Suffix } from '../../utils/programmingModelUtils'; -import { getWorkspaceSetting, updateWorkspaceSetting } from '../../vsCodeConfig/settings'; +import { getWorkspaceSetting } from '../../vsCodeConfig/settings'; import { FunctionSubWizard } from './FunctionSubWizard'; import { type IFunctionWizardContext } from './IFunctionWizardContext'; import { JobsListStep } from './JobsListStep'; @@ -39,7 +39,7 @@ export class FunctionListStep extends AzureWizardPromptStep { if (this._options.templateId) { const actualId: string = t.id.toLowerCase(); @@ -83,10 +83,6 @@ export class FunctionListStep extends AzureWizardPromptStep { - /* v2 schema doesn't have a template filter setting */ - let templateFilter: TemplateFilter = context.templateSchemaVersion === TemplateSchemaVersion.v2 ? TemplateFilter.All : - getWorkspaceSetting(templateFilterSetting, context.projectPath) || TemplateFilter.Verified; - const templateProvider = ext.templateProvider.get(context); while (!context.functionTemplate) { let placeHolder: string = this._isProjectWizard ? @@ -97,17 +93,13 @@ export class FunctionListStep extends AzureWizardPromptStep[]> { + private async getPicks(context: IFunctionWizardContext): Promise[]> { const language: ProjectLanguage = nonNullProp(context, 'language'); const languageModel = context.languageModel; const version: FuncVersion = nonNullProp(context, 'version'); const templateProvider = ext.templateProvider.get(context); - const templates: FunctionTemplateBase[] = await templateProvider.getFunctionTemplates(context, context.projectPath, language, context.languageModel, version, templateFilter, context.projectTemplateKey); + const templates: FunctionTemplateBase[] = await templateProvider.getFunctionTemplates(context, context.projectPath, language, context.languageModel, version, context.projectTemplateKey); context.telemetry.measurements.templateCount = templates.length; const picks: IAzureQuickPickItem[] = templates .filter((t) => !(doesTemplateRequireExistingStorageSetup(t.id, language) && !context.hasDurableStorage)) - .sort((a, b) => sortTemplates(a, b, templateFilter)) - .map(t => { return { label: t.name, data: t }; }); + .sort((a, b) => sortTemplates(a, b)) + .map(t => { return { label: t.name, data: t, group: t.templateFilter }; }); if (this._isProjectWizard) { picks.unshift({ @@ -165,15 +155,6 @@ export class FunctionListStep extends AzureWizardPromptStep('showReloadTemplates')) { picks.push({ @@ -193,18 +174,7 @@ interface IFunctionListStepOptions { functionSettings: { [key: string]: string | undefined } | undefined; } -type TemplatePromptResult = 'changeFilter' | 'skipForNow' | 'openAPI' | 'reloadTemplates'; - -async function promptForTemplateFilter(context: IActionContext): Promise { - const picks: IAzureQuickPickItem[] = [ - { label: TemplateFilter.Verified, description: localize('verifiedDescription', '(Subset of "Core" that has been verified in VS Code)'), data: TemplateFilter.Verified }, - { label: TemplateFilter.Core, data: TemplateFilter.Core }, - { label: TemplateFilter.All, data: TemplateFilter.All } - ]; - - const options: IAzureQuickPickOptions = { suppressPersistence: true, placeHolder: localize('selectFilter', 'Select a template filter') }; - return (await context.ui.showQuickPick(picks, options)).data; -} +type TemplatePromptResult = 'skipForNow' | 'openAPI' | 'reloadTemplates'; // Todo: https://github.com/microsoft/vscode-azurefunctions/issues/3529 // Identify and filter out Durable Function templates requiring a pre-existing storage setup @@ -226,21 +196,17 @@ function doesTemplateRequireExistingStorageSetup(templateId: string, language?: * If templateFilter is verified, puts HttpTrigger/TimerTrigger at the top since they're the most popular * Otherwise sort alphabetically */ -function sortTemplates(a: FunctionTemplateBase, b: FunctionTemplateBase, templateFilter: TemplateFilter): number { - if (templateFilter === TemplateFilter.Verified) { - function getPriority(id: string): number { - if (/\bhttptrigger\b/i.test(id)) { // Plain http trigger - return 1; - } else if (/\bhttptrigger/i.test(id)) { // Http trigger with any extra pizazz - return 2; - } else if (/\btimertrigger\b/i.test(id)) { - return 3; - } else { - return 4; - } +function sortTemplates(a: FunctionTemplateBase, b: FunctionTemplateBase): number { + function getPriority(id: string): number { + if (/\bhttptrigger\b/i.test(id)) { // Plain http trigger + return 1; + } else if (/\bhttptrigger/i.test(id)) { // Http trigger with any extra pizazz + return 2; + } else if (/\btimertrigger\b/i.test(id)) { + return 3; + } else { + return a.name.localeCompare(b.name) === -1 ? 4 : 5; } - return getPriority(a.id) - getPriority(b.id); } - - return a.name.localeCompare(b.name); + return getPriority(a.id) - getPriority(b.id); } diff --git a/src/constants.ts b/src/constants.ts index 8ad82b679..432af0fc1 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -11,7 +11,6 @@ export const projectLanguageSetting: string = 'projectLanguage'; export const projectLanguageModelSetting: string = 'projectLanguageModel'; export const funcVersionSetting: string = 'projectRuntime'; // Using this name for the sake of backwards compatability even though it's not the most accurate export const projectSubpathSetting: string = 'projectSubpath'; -export const templateFilterSetting: string = 'templateFilter'; export const deploySubpathSetting: string = 'deploySubpath'; export const templateVersionSetting: string = 'templateVersion'; export const preDeployTaskSetting: string = 'preDeployTask'; diff --git a/src/templates/CentralTemplateProvider.ts b/src/templates/CentralTemplateProvider.ts index ca5b1fa73..52895851c 100644 --- a/src/templates/CentralTemplateProvider.ts +++ b/src/templates/CentralTemplateProvider.ts @@ -83,18 +83,20 @@ export class CentralTemplateProvider implements Disposable { } /* Ignored by the v2 schema */ - public async getFunctionTemplates(context: IActionContext, projectPath: string | undefined, language: ProjectLanguage, languageModel: number | undefined, version: FuncVersion, templateFilter: TemplateFilter, projectTemplateKey: string | undefined): Promise { + public async getFunctionTemplates(context: IActionContext, projectPath: string | undefined, language: ProjectLanguage, languageModel: number | undefined, version: FuncVersion, projectTemplateKey: string | undefined): Promise { const templates: ITemplates = await this.getTemplates(context, projectPath, language, languageModel, version, projectTemplateKey); - switch (templateFilter) { - case TemplateFilter.All: - return templates.functionTemplates; - case TemplateFilter.Core: - return templates.functionTemplates.filter((t: IFunctionTemplate) => t.categories.find((c: TemplateCategory) => c === TemplateCategory.Core) !== undefined); - case TemplateFilter.Verified: - default: - const verifiedTemplateIds = getScriptVerifiedTemplateIds(version).concat(getDotnetVerifiedTemplateIds(version)).concat(getJavaVerifiedTemplateIds().concat(getBallerinaVerifiedTemplateIds())); - return templates.functionTemplates.filter((t: IFunctionTemplate) => verifiedTemplateIds.find(vt => typeof vt === 'string' ? vt === t.id : vt.test(t.id))); + for (const template of templates.functionTemplates) { + // by default, all templates will be categorized as 'All' + template.templateFilter = TemplateFilter.All; + const verifiedTemplateIds = getScriptVerifiedTemplateIds(version).concat(getDotnetVerifiedTemplateIds(version)).concat(getJavaVerifiedTemplateIds().concat(getBallerinaVerifiedTemplateIds())); + if (verifiedTemplateIds.find(vt => typeof vt === 'string' ? vt === template.id : vt.test(template.id))) { + template.templateFilter = TemplateFilter.Verified; + } else if ((template as IFunctionTemplate).categories.find((c: TemplateCategory) => c === TemplateCategory.Core) !== undefined) { + template.templateFilter = TemplateFilter.Core; + } } + + return templates.functionTemplates; } public async clearTemplateCache(context: IActionContext, projectPath: string | undefined, language: ProjectLanguage, languageModel: number | undefined, version: FuncVersion): Promise { @@ -117,7 +119,7 @@ export class CentralTemplateProvider implements Disposable { public async tryGetSampleData(context: IActionContext, version: FuncVersion, triggerBindingType: string): Promise { try { - const templates: IScriptFunctionTemplate[] = await this.getFunctionTemplates(context, undefined, ProjectLanguage.JavaScript, undefined, version, TemplateFilter.All, undefined); + const templates: IScriptFunctionTemplate[] = await this.getFunctionTemplates(context, undefined, ProjectLanguage.JavaScript, undefined, version, undefined); const template: IScriptFunctionTemplate | undefined = templates.find(t => t.functionJson.triggerBinding?.type?.toLowerCase() === triggerBindingType.toLowerCase()); return template?.templateFiles['sample.dat']; } catch { diff --git a/src/templates/IFunctionTemplate.ts b/src/templates/IFunctionTemplate.ts index 735e8c727..02719d68d 100644 --- a/src/templates/IFunctionTemplate.ts +++ b/src/templates/IFunctionTemplate.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { type ProjectLanguage } from '../constants'; +import { type ProjectLanguage, type TemplateFilter } from '../constants'; import { type IBindingSetting } from './IBindingTemplate'; import { type TemplateSchemaVersion } from './TemplateProviderBase'; import { type ParsedJob, type RawTemplateV2 } from './script/parseScriptTemplatesV2'; @@ -44,5 +44,6 @@ export interface FunctionTemplateBase { language: ProjectLanguage; isHttpTrigger: boolean; isTimerTrigger: boolean; - templateSchemaVersion: TemplateSchemaVersion + templateSchemaVersion: TemplateSchemaVersion; + templateFilter?: TemplateFilter; // defaults to All } diff --git a/src/templates/dotnet/parseDotnetTemplates.ts b/src/templates/dotnet/parseDotnetTemplates.ts index 58ce81b2f..9945c07e9 100644 --- a/src/templates/dotnet/parseDotnetTemplates.ts +++ b/src/templates/dotnet/parseDotnetTemplates.ts @@ -85,7 +85,8 @@ function parseDotnetTemplate(rawTemplate: IRawTemplate): IFunctionTemplate { userPromptedSettings: userPromptedSettings, categories: [TemplateCategory.Core], // Dotnet templates do not have category information, so display all templates as if they are in the 'core' category isDynamicConcurrent: (rawTemplate.Identity.includes('ServiceBusQueueTrigger') || rawTemplate.Identity.includes('BlobTrigger') || rawTemplate.Identity.includes('QueueTrigger')) ? true : false, - templateSchemaVersion: TemplateSchemaVersion.v1 + templateSchemaVersion: TemplateSchemaVersion.v1, + templateFilter: TemplateFilter.All }; } @@ -122,7 +123,7 @@ async function copyCSharpSettingsFromJS(csharpTemplates: IFunctionTemplate[], ve jsContext.telemetry.properties.isActivationEvent = 'true'; const templateProvider = ext.templateProvider.get(jsContext); - const jsTemplates: FunctionTemplateBase[] = await templateProvider.getFunctionTemplates(jsContext, undefined, ProjectLanguage.JavaScript, undefined, version, TemplateFilter.All, undefined); + const jsTemplates: FunctionTemplateBase[] = await templateProvider.getFunctionTemplates(jsContext, undefined, ProjectLanguage.JavaScript, undefined, version, undefined); for (const csharpTemplate of csharpTemplates) { assertTemplateIsV1(csharpTemplate); csharpTemplate.templateSchemaVersion = TemplateSchemaVersion.v1; diff --git a/src/vsCodeConfig/verifyVSCodeConfigOnActivate.ts b/src/vsCodeConfig/verifyVSCodeConfigOnActivate.ts index 4f3840edf..6c0c7c688 100644 --- a/src/vsCodeConfig/verifyVSCodeConfigOnActivate.ts +++ b/src/vsCodeConfig/verifyVSCodeConfigOnActivate.ts @@ -8,7 +8,7 @@ import * as path from 'path'; import type * as vscode from 'vscode'; import { tryGetFunctionProjectRoot } from '../commands/createNewProject/verifyIsProject'; import { initProjectForVSCode } from '../commands/initProjectForVSCode/initProjectForVSCode'; -import { funcVersionSetting, ProjectLanguage, projectLanguageModelSetting, projectLanguageSetting, TemplateFilter } from '../constants'; +import { funcVersionSetting, ProjectLanguage, projectLanguageModelSetting, projectLanguageSetting } from '../constants'; import { ext } from '../extensionVariables'; import { tryParseFuncVersion, type FuncVersion } from '../FuncVersion'; import { localize } from '../localize'; @@ -38,7 +38,7 @@ export async function verifyVSCodeConfigOnActivate(context: IActionContext, fold templatesContext.telemetry.properties.isActivationEvent = 'true'; templatesContext.errorHandling.suppressDisplay = true; const templateProvider = ext.templateProvider.get(templatesContext); - await templateProvider.getFunctionTemplates(templatesContext, projectPath, language, languageModel, version, TemplateFilter.Verified, undefined); + await templateProvider.getFunctionTemplates(templatesContext, projectPath, language, languageModel, version, undefined); }); let isDotnet: boolean = false; diff --git a/test/createFunction/FunctionTesterBase.ts b/test/createFunction/FunctionTesterBase.ts index 2c0d39c9d..c3ce4a1dd 100644 --- a/test/createFunction/FunctionTesterBase.ts +++ b/test/createFunction/FunctionTesterBase.ts @@ -8,7 +8,7 @@ import { AzExtFsExtra } from '@microsoft/vscode-azext-utils'; import * as assert from 'assert'; import * as path from 'path'; import { type Disposable } from 'vscode'; -import { createFunctionInternal, getRandomHexString, TemplateFilter, type FunctionTemplateBase, type FuncVersion, type ProjectLanguage, type TemplateSource } from '../../extension.bundle'; +import { createFunctionInternal, getRandomHexString, type FunctionTemplateBase, type FuncVersion, type ProjectLanguage, type TemplateSource } from '../../extension.bundle'; import { addParallelSuite, type ParallelSuiteOptions, type ParallelTest } from '../addParallelSuite'; import { runForTemplateSource, testFolderPath } from '../global.test'; @@ -47,7 +47,7 @@ export abstract class FunctionTesterBase implements Disposable { await this.initializeTestFolder(this.projectPath); // This will initialize and cache the templatesTask for this project. Better to do it here than during the first test - await templateProvider.getFunctionTemplates(context, this.projectPath, this.language, undefined, this.version, TemplateFilter.Verified, undefined); + await templateProvider.getFunctionTemplates(context, this.projectPath, this.language, undefined, this.version, undefined); }); }); } @@ -55,7 +55,7 @@ export abstract class FunctionTesterBase implements Disposable { public async dispose(): Promise { await runWithTestActionContext('testCreateFunctionDispose', async context => { await runForTemplateSource(context, this.source, async (templateProvider) => { - const templates: FunctionTemplateBase[] = await templateProvider.getFunctionTemplates(context, this.projectPath, this.language, undefined, this.version, TemplateFilter.Verified, undefined); + const templates: FunctionTemplateBase[] = await templateProvider.getFunctionTemplates(context, this.projectPath, this.language, undefined, this.version, undefined); assert.deepEqual(this.testedFunctions.sort(), templates.map(t => t.name).sort(), 'Not all "Verified" templates were tested'); }); }); diff --git a/test/global.test.ts b/test/global.test.ts index 304462e5d..46003ed5b 100644 --- a/test/global.test.ts +++ b/test/global.test.ts @@ -9,7 +9,7 @@ import * as assert from 'assert'; import * as os from 'os'; import * as path from 'path'; import * as vscode from 'vscode'; -import { CentralTemplateProvider, deploySubpathSetting, envUtils, ext, FuncVersion, funcVersionSetting, getGlobalSetting, getRandomHexString, parseError, preDeployTaskSetting, ProjectLanguage, projectLanguageSetting, pythonVenvSetting, registerOnActionStartHandler, TemplateFilter, templateFilterSetting, TemplateSource, updateGlobalSetting, updateWorkspaceSetting, type IActionContext } from '../extension.bundle'; +import { CentralTemplateProvider, deploySubpathSetting, envUtils, ext, FuncVersion, funcVersionSetting, getGlobalSetting, getRandomHexString, parseError, preDeployTaskSetting, ProjectLanguage, projectLanguageSetting, pythonVenvSetting, registerOnActionStartHandler, TemplateSource, updateGlobalSetting, updateWorkspaceSetting, type IActionContext } from '../extension.bundle'; /** * Folder for most tests that do not need a workspace open @@ -106,7 +106,7 @@ async function preLoadTemplates(): Promise { } for (const language of [ProjectLanguage.JavaScript, ProjectLanguage.CSharp]) { - tasks.push(provider.getFunctionTemplates(context, testWorkspaceFolders[0], language, undefined, version, TemplateFilter.Verified, undefined)); + tasks.push(provider.getFunctionTemplates(context, testWorkspaceFolders[0], language, undefined, version, undefined)); } } }); @@ -147,7 +147,6 @@ export async function cleanTestWorkspace(): Promise { const settings: string[] = [ projectLanguageSetting, funcVersionSetting, - templateFilterSetting, deploySubpathSetting, preDeployTaskSetting, pythonVenvSetting diff --git a/test/templateCount.test.ts b/test/templateCount.test.ts index e422a170e..c7c8f4df8 100644 --- a/test/templateCount.test.ts +++ b/test/templateCount.test.ts @@ -5,7 +5,7 @@ import { runWithTestActionContext } from '@microsoft/vscode-azext-dev'; import * as assert from 'assert'; -import { FuncVersion, ProjectLanguage, TemplateFilter, TemplateSource, type CentralTemplateProvider, type FunctionTemplateBase } from '../extension.bundle'; +import { FuncVersion, ProjectLanguage, TemplateSource, type CentralTemplateProvider, type FunctionTemplateBase } from '../extension.bundle'; import { getTestWorkspaceFolder, longRunningTestsEnabled, runForTemplateSource, shouldSkipVersion } from './global.test'; import { javaUtils } from './utils/javaUtils'; @@ -57,7 +57,7 @@ function addSuite(source: TemplateSource | undefined): void { await runWithTestActionContext('getFunctionTemplates', async context => { await runForTemplateSource(context, source, async (provider: CentralTemplateProvider) => { - const templates: FunctionTemplateBase[] = await provider.getFunctionTemplates(context, testWorkspacePath, language, undefined, version, TemplateFilter.Verified, projectTemplateKey); + const templates: FunctionTemplateBase[] = await provider.getFunctionTemplates(context, testWorkspacePath, language, undefined, version, projectTemplateKey); assert.equal(templates.length, expectedCount); }); });