diff --git a/apps/vs-code-designer/src/app/languageServer/__test__/languageServer.test.ts b/apps/vs-code-designer/src/app/languageServer/__test__/languageServer.test.ts index e22e95f406e..15e33157e57 100644 --- a/apps/vs-code-designer/src/app/languageServer/__test__/languageServer.test.ts +++ b/apps/vs-code-designer/src/app/languageServer/__test__/languageServer.test.ts @@ -12,7 +12,6 @@ const mocks = vi.hoisted(() => ({ readFile: vi.fn(), readdir: vi.fn(), showWarningMessage: vi.fn(), - tryGetLogicAppCustomCodeFunctionsProjects: vi.fn(), tryGetLogicAppProjectRoot: vi.fn(), getDotNetCommand: vi.fn(), })); @@ -52,10 +51,6 @@ vi.mock('../../utils/verifyIsProject', () => ({ tryGetLogicAppProjectRoot: mocks.tryGetLogicAppProjectRoot, })); -vi.mock('../../utils/customCodeUtils', () => ({ - tryGetLogicAppCustomCodeFunctionsProjects: mocks.tryGetLogicAppCustomCodeFunctionsProjects, -})); - vi.mock('../../utils/dotnet/dotnet', () => ({ getDotNetCommand: mocks.getDotNetCommand, })); @@ -93,14 +88,21 @@ describe('LogicAppsLanguageServer', () => { beforeEach(() => { vi.clearAllMocks(); mocks.createFileSystemWatcher.mockReturnValue({ onDidChange: vi.fn() }); - mocks.getGlobalSetting.mockReturnValue(dependenciesPath); + mocks.getGlobalSetting.mockImplementation((key: string) => { + if (key === 'autoRuntimeDependenciesValidationAndInstallation') { + return true; + } + if (key === 'autoRuntimeDependenciesPath') { + return dependenciesPath; + } + return undefined; + }); mocks.getWorkspaceFolderPath.mockResolvedValue('D:\\workspace'); mocks.languageClient.mockImplementation(() => ({ start: vi.fn().mockResolvedValue(undefined) })); mocks.pathExists.mockResolvedValue(false); mocks.readFile.mockResolvedValue('{}'); mocks.readdir.mockResolvedValue([]); mocks.tryGetLogicAppProjectRoot.mockResolvedValue(projectPath); - mocks.tryGetLogicAppCustomCodeFunctionsProjects.mockResolvedValue(['D:\\workspace\\custom-code-project']); mocks.getDotNetCommand.mockReturnValue('D:\\dependencies\\DotNetSDK\\dotnet.exe'); mocks.getAzureConnectorDetailsForLocalProject.mockResolvedValue({ accessToken: 'Bearer token', @@ -120,19 +122,6 @@ describe('LogicAppsLanguageServer', () => { expect(mocks.showWarningMessage).not.toHaveBeenCalled(); }); - it('does not start or prompt for Azure connector metadata when no linked custom code project is found', async () => { - mocks.tryGetLogicAppCustomCodeFunctionsProjects.mockResolvedValue([]); - - await new LogicAppsLanguageServer({} as any).start(); - - expect(mocks.tryGetLogicAppCustomCodeFunctionsProjects).toHaveBeenCalledWith(projectPath); - expect(mocks.pathExists).not.toHaveBeenCalled(); - expect(mocks.readdir).not.toHaveBeenCalled(); - expect(mocks.getAzureConnectorDetailsForLocalProject).not.toHaveBeenCalled(); - expect(mocks.languageClient).not.toHaveBeenCalled(); - expect(mocks.showWarningMessage).not.toHaveBeenCalled(); - }); - it('does not scan a missing SDK directory', async () => { mocks.pathExists.mockImplementation(async (filePath: string) => filePath === lspServerPath); @@ -195,4 +184,93 @@ describe('LogicAppsLanguageServer', () => { ); expect(languageClient.start).toHaveBeenCalledOnce(); }); + + describe('manual mode (autoRuntimeDependenciesValidationAndInstallation = false)', () => { + const manualDllPath = 'C:\\custom\\LSPServer\\SdkLspServer.dll'; + const manualNupkgPath = 'C:\\custom\\LanguageServerLogicApps\\Microsoft.Azure.Workflows.Sdk.1.0.0-preview.1.nupkg'; + + beforeEach(() => { + mocks.getGlobalSetting.mockImplementation((key: string) => { + if (key === 'autoRuntimeDependenciesValidationAndInstallation') { + return false; + } + if (key === 'languageServerDLLPath') { + return manualDllPath; + } + if (key === 'languageServerNupkgPath') { + return manualNupkgPath; + } + return undefined; + }); + }); + + it('uses explicit path settings when auto-validation is off', async () => { + const languageClient = { start: vi.fn().mockResolvedValue(undefined) }; + mocks.languageClient.mockReturnValue(languageClient); + mocks.pathExists.mockResolvedValue(true); + + await new LogicAppsLanguageServer({} as any).start(); + + expect(mocks.languageClient).toHaveBeenCalledWith( + 'logicAppsLanguageServer', + 'Logic Apps language server', + { + run: { + command: 'D:\\dependencies\\DotNetSDK\\dotnet.exe', + args: [manualDllPath, '--sdk', manualNupkgPath], + }, + debug: { + command: 'D:\\dependencies\\DotNetSDK\\dotnet.exe', + args: [manualDllPath, '--sdk', manualNupkgPath], + }, + }, + expect.objectContaining({ + initializationOptions: expect.objectContaining({ + apiConfig: expect.objectContaining({ + bearerToken: 'Bearer token', + }), + }), + }) + ); + expect(languageClient.start).toHaveBeenCalledOnce(); + }); + + it('warns and does not start when DLL path does not exist', async () => { + mocks.pathExists.mockImplementation(async (filePath: string) => filePath !== manualDllPath); + + await new LogicAppsLanguageServer({} as any).start(); + + expect(mocks.showWarningMessage).toHaveBeenCalledWith( + expect.stringContaining('Install or repair Logic Apps language server dependencies') + ); + expect(mocks.languageClient).not.toHaveBeenCalled(); + }); + + it('warns and does not start when nupkg path does not exist', async () => { + mocks.pathExists.mockImplementation(async (filePath: string) => filePath !== manualNupkgPath); + + await new LogicAppsLanguageServer({} as any).start(); + + expect(mocks.showWarningMessage).toHaveBeenCalledWith( + expect.stringContaining('Install or repair Logic Apps language server SDK dependencies') + ); + expect(mocks.languageClient).not.toHaveBeenCalled(); + }); + + it('returns undefined for both paths when settings are empty', async () => { + mocks.getGlobalSetting.mockImplementation((key: string) => { + if (key === 'autoRuntimeDependenciesValidationAndInstallation') { + return false; + } + return undefined; + }); + + await new LogicAppsLanguageServer({} as any).start(); + + expect(mocks.showWarningMessage).toHaveBeenCalledWith( + 'Install or repair Logic Apps language server dependencies before starting C# workflow authoring.' + ); + expect(mocks.languageClient).not.toHaveBeenCalled(); + }); + }); }); diff --git a/apps/vs-code-designer/src/app/languageServer/languageServer.ts b/apps/vs-code-designer/src/app/languageServer/languageServer.ts index fe6d6cbba8b..7f5e0e02535 100644 --- a/apps/vs-code-designer/src/app/languageServer/languageServer.ts +++ b/apps/vs-code-designer/src/app/languageServer/languageServer.ts @@ -2,7 +2,10 @@ import type { IActionContext } from '@microsoft/vscode-azext-utils'; import { callWithTelemetryAndErrorHandling } from '@microsoft/vscode-azext-utils'; import { autoRuntimeDependenciesPathSettingKey, + autoRuntimeDependenciesValidationAndInstallationSetting, connectionsFileName, + languageServerDLLPathSettingKey, + languageServerNupkgPathSettingKey, lspDirectory, onStartLanguageServerProtocol, workflowAppApiVersion, @@ -20,7 +23,6 @@ import type { AzureConnectorDetails } from '@microsoft/vscode-extension-logic-ap import { getAzureConnectorDetailsForLocalProject } from '../utils/codeless/common'; import * as vscode from 'vscode'; import { filterCompletionResult } from './completionFilter'; -import { tryGetLogicAppCustomCodeFunctionsProjects } from '../utils/customCodeUtils'; import { getDotNetCommand } from '../utils/dotnet/dotnet'; export default class LogicAppsLanguageServer { @@ -46,11 +48,6 @@ export default class LogicAppsLanguageServer { return; } - const customCodeProjectPaths = await tryGetLogicAppCustomCodeFunctionsProjects(this.projectPath); - if (!customCodeProjectPaths || customCodeProjectPaths.length === 0) { - return; - } - const { lspServerPath, sdkNupkgPath } = await this.getSDKPaths(); this.lspServerPath = lspServerPath; @@ -178,6 +175,25 @@ export default class LogicAppsLanguageServer { } private async getSDKPaths() { + const autoValidate = getGlobalSetting(autoRuntimeDependenciesValidationAndInstallationSetting); + + if (!autoValidate) { + // Manual mode: read explicit path settings + const lspServerPath = getGlobalSetting(languageServerDLLPathSettingKey) || undefined; + const sdkNupkgPath = getGlobalSetting(languageServerNupkgPathSettingKey) || undefined; + + if (lspServerPath && !(await fse.pathExists(lspServerPath))) { + return { lspServerPath: undefined, sdkNupkgPath }; + } + + if (sdkNupkgPath && !(await fse.pathExists(sdkNupkgPath))) { + return { lspServerPath, sdkNupkgPath: undefined }; + } + + return { lspServerPath, sdkNupkgPath }; + } + + // Auto mode: construct paths from dependenciesPath const dependenciesPath = getGlobalSetting(autoRuntimeDependenciesPathSettingKey); if (!dependenciesPath) { return { lspServerPath: undefined, sdkNupkgPath: undefined }; diff --git a/apps/vs-code-designer/src/app/utils/__test__/binaries.test.ts b/apps/vs-code-designer/src/app/utils/__test__/binaries.test.ts index 1b2a3246a3e..8f73387ae7c 100644 --- a/apps/vs-code-designer/src/app/utils/__test__/binaries.test.ts +++ b/apps/vs-code-designer/src/app/utils/__test__/binaries.test.ts @@ -575,8 +575,37 @@ describe('binaries', () => { expect(updateGlobalSetting).toHaveBeenCalledWith('funcCoreToolsBinaryPath', 'func'); }); - it('should set default paths in devContainer workspace', async () => { - (getGlobalSetting as Mock).mockReturnValue(true); + it('should not overwrite existing paths in devContainer workspace', async () => { + (getGlobalSetting as Mock).mockImplementation((key: string) => { + if (key === 'autoRuntimeDependenciesValidationAndInstallation') { + return true; + } + if (key === 'dotnetBinaryPath') { + return '/custom/path/dotnet'; + } + if (key === 'nodeJsBinaryPath') { + return '/custom/path/node'; + } + if (key === 'funcCoreToolsBinaryPath') { + return '/custom/path/func'; + } + return undefined; + }); + const devContainerModule = await import('../devContainerUtils'); + vi.mocked(devContainerModule.isDevContainerWorkspace).mockResolvedValue(true); + + await installBinaries(context); + + expect(updateGlobalSetting).not.toHaveBeenCalled(); + }); + + it('should set default paths in devContainer workspace when paths are not set', async () => { + (getGlobalSetting as Mock).mockImplementation((key: string) => { + if (key === 'autoRuntimeDependenciesValidationAndInstallation') { + return true; + } + return undefined; + }); const devContainerModule = await import('../devContainerUtils'); vi.mocked(devContainerModule.isDevContainerWorkspace).mockResolvedValue(true); diff --git a/apps/vs-code-designer/src/app/utils/binaries.ts b/apps/vs-code-designer/src/app/utils/binaries.ts index 2776be288f4..af036e2777e 100644 --- a/apps/vs-code-designer/src/app/utils/binaries.ts +++ b/apps/vs-code-designer/src/app/utils/binaries.ts @@ -476,9 +476,15 @@ export async function installBinaries(context: IActionContext) { await onboardBinaries(context); context.telemetry.properties.autoRuntimeDependenciesValidationAndInstallationSetting = 'true'; } else { - await updateGlobalSetting(dotNetBinaryPathSettingKey, DependencyDefaultPath.dotnet); - await updateGlobalSetting(nodeJsBinaryPathSettingKey, DependencyDefaultPath.node); - await updateGlobalSetting(funcCoreToolsBinaryPathSettingKey, DependencyDefaultPath.funcCoreTools); + if (!getGlobalSetting(dotNetBinaryPathSettingKey)) { + await updateGlobalSetting(dotNetBinaryPathSettingKey, DependencyDefaultPath.dotnet); + } + if (!getGlobalSetting(nodeJsBinaryPathSettingKey)) { + await updateGlobalSetting(nodeJsBinaryPathSettingKey, DependencyDefaultPath.node); + } + if (!getGlobalSetting(funcCoreToolsBinaryPathSettingKey)) { + await updateGlobalSetting(funcCoreToolsBinaryPathSettingKey, DependencyDefaultPath.funcCoreTools); + } context.telemetry.properties.autoRuntimeDependenciesValidationAndInstallationSetting = 'false'; } } diff --git a/apps/vs-code-designer/src/constants.ts b/apps/vs-code-designer/src/constants.ts index b2bdf08880f..d94cd6163b4 100644 --- a/apps/vs-code-designer/src/constants.ts +++ b/apps/vs-code-designer/src/constants.ts @@ -279,6 +279,8 @@ export const dotNetBinaryPathSettingKey = 'dotnetBinaryPath'; export const nodeJsBinaryPathSettingKey = 'nodeJsBinaryPath'; export const funcCoreToolsBinaryPathSettingKey = 'funcCoreToolsBinaryPath'; export const dependencyTimeoutSettingKey = 'dependencyTimeout'; +export const languageServerDLLPathSettingKey = 'languageServerDLLPath'; +export const languageServerNupkgPathSettingKey = 'languageServerNupkgPath'; export const unitTestExplorer = 'unitTestExplorer'; export const verifyConnectionKeysSetting = 'verifyConnectionKeys'; export const useSmbDeployment = 'useSmbDeploymentForHybrid';