-
Notifications
You must be signed in to change notification settings - Fork 964
Fix VS Code AppHost launch path resolution #17408
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Adam Ratzman (adamint)
merged 21 commits into
main
from
codex/vscode-apphost-launch-path
May 27, 2026
Merged
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
56e811c
Fix VS Code AppHost launch path resolution
davidfowl 8d5d73a
Use aspire ls for extension AppHost discovery
davidfowl dd5ef0b
Refresh AppHost discovery consumers on changes
davidfowl a16414d
Use CLI language ids for AppHost discovery
davidfowl 2cd4877
Handle AppHost discovery failures in editor commands
davidfowl 7991102
Add TypeScript AppHost launch discovery coverage
davidfowl c3c8505
Harden AppHost discovery process handling
davidfowl 63b84c5
Honor configured AppHosts in discovery
davidfowl befdbf2
Merge origin/main into PR branch
davidfowl 192ac6a
Consolidate extension AppHost discovery
davidfowl db92159
Fix workspace test path separators
davidfowl 6e8ce79
Merge branch 'main' into codex/vscode-apphost-launch-path
adamint 257c815
Fix AppHost configured path selection
adamint 6f0b51e
Keep extension-launched AppHost CLI alive
adamint 72eca70
Update extension build and CLI debug logging
adamint ec5d5a0
Address AppHost launch review feedback
adamint 0b79866
Address extension discovery review feedback
adamint dcb07bd
Stabilize pipeline unit tests without Docker
adamint 424c0d5
Use ordinal comparison in pipeline test provider
adamint 3c027ec
Revert pipeline test isolation changes
adamint 134d0ca
Merge upstream main into PR branch
adamint File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
73 changes: 73 additions & 0 deletions
73
extension/src/test/aspireDebugConfigurationProvider.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| /// <reference types="mocha" /> | ||
|
|
||
| import * as assert from 'assert'; | ||
| import * as fs from 'fs'; | ||
| import * as os from 'os'; | ||
| import * as path from 'path'; | ||
| import { AspireDebugConfigurationProvider } from '../debugger/AspireDebugConfigurationProvider'; | ||
|
|
||
| suite('AspireDebugConfigurationProvider', () => { | ||
| let tempDir: string; | ||
|
|
||
| setup(() => { | ||
| tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aspire-debug-configuration-provider-')); | ||
| }); | ||
|
|
||
| teardown(() => { | ||
| fs.rmSync(tempDir, { recursive: true, force: true }); | ||
| }); | ||
|
|
||
| test('resolves launch config SDK-style AppHost Program.cs to containing project file', async () => { | ||
| const appHostDirectory = path.join(tempDir, 'AppHost'); | ||
| fs.mkdirSync(appHostDirectory); | ||
|
|
||
| const programPath = path.join(appHostDirectory, 'Program.cs'); | ||
| const projectPath = path.join(appHostDirectory, 'AppHost.csproj'); | ||
| fs.writeFileSync(programPath, 'var builder = DistributedApplication.CreateBuilder(args);\nbuilder.Build().Run();'); | ||
| fs.writeFileSync(projectPath, '<Project Sdk="Microsoft.NET.Sdk" />'); | ||
|
|
||
| const provider = new AspireDebugConfigurationProvider(); | ||
| const config = await provider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { | ||
| name: 'Debug AppHost', | ||
| type: 'aspire', | ||
| request: 'launch', | ||
| program: programPath | ||
| }); | ||
|
|
||
| assert.strictEqual(config?.program, projectPath); | ||
| }); | ||
|
|
||
| test('leaves launch config single-file apphost.cs unchanged', async () => { | ||
| const appHostPath = path.join(tempDir, 'apphost.cs'); | ||
| fs.writeFileSync(appHostPath, '#:sdk Aspire.AppHost.Sdk\nvar builder = DistributedApplication.CreateBuilder(args);'); | ||
|
|
||
| const provider = new AspireDebugConfigurationProvider(); | ||
| const config = await provider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { | ||
| name: 'Debug AppHost', | ||
| type: 'aspire', | ||
| request: 'launch', | ||
| program: appHostPath | ||
| }); | ||
|
|
||
| assert.strictEqual(config?.program, appHostPath); | ||
| }); | ||
|
|
||
| test('leaves launch config non-AppHost C# source file unchanged', async () => { | ||
| const appDirectory = path.join(tempDir, 'App'); | ||
| fs.mkdirSync(appDirectory); | ||
|
|
||
| const programPath = path.join(appDirectory, 'Program.cs'); | ||
| fs.writeFileSync(programPath, 'Console.WriteLine("Hello");'); | ||
| fs.writeFileSync(path.join(appDirectory, 'App.csproj'), '<Project Sdk="Microsoft.NET.Sdk" />'); | ||
|
|
||
| const provider = new AspireDebugConfigurationProvider(); | ||
| const config = await provider.resolveDebugConfigurationWithSubstitutedVariables(undefined, { | ||
| name: 'Debug AppHost', | ||
| type: 'aspire', | ||
| request: 'launch', | ||
| program: programPath | ||
| }); | ||
|
|
||
| assert.strictEqual(config?.program, programPath); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| /// <reference types="mocha" /> | ||
|
|
||
| import * as assert from 'assert'; | ||
| import * as fs from 'fs'; | ||
| import * as os from 'os'; | ||
| import * as path from 'path'; | ||
| import * as sinon from 'sinon'; | ||
| import * as vscode from 'vscode'; | ||
| import { AspireEditorCommandProvider } from '../editor/AspireEditorCommandProvider'; | ||
|
|
||
| function createEditor(filePath: string): vscode.TextEditor { | ||
| return { | ||
| document: { | ||
| uri: vscode.Uri.file(filePath), | ||
| fileName: filePath, | ||
| languageId: 'csharp' | ||
| } as vscode.TextDocument | ||
| } as vscode.TextEditor; | ||
| } | ||
|
|
||
| suite('AspireEditorCommandProvider', () => { | ||
| let tempDir: string; | ||
| let activeEditor: vscode.TextEditor | undefined; | ||
| let activeEditorStub: sinon.SinonStub; | ||
| let workspaceFoldersStub: sinon.SinonStub; | ||
| let getWorkspaceFolderStub: sinon.SinonStub; | ||
| let onDidChangeWorkspaceFoldersStub: sinon.SinonStub; | ||
| let onDidChangeActiveTextEditorStub: sinon.SinonStub; | ||
| let executeCommandStub: sinon.SinonStub; | ||
|
|
||
| setup(() => { | ||
| tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aspire-editor-command-provider-')); | ||
| activeEditor = undefined; | ||
|
|
||
| activeEditorStub = sinon.stub(vscode.window, 'activeTextEditor').get(() => activeEditor); | ||
| workspaceFoldersStub = sinon.stub(vscode.workspace, 'workspaceFolders').value(undefined); | ||
| getWorkspaceFolderStub = sinon.stub(vscode.workspace, 'getWorkspaceFolder').callsFake((uri: vscode.Uri) => { | ||
| if (uri.fsPath.startsWith(tempDir)) { | ||
| return { uri: vscode.Uri.file(tempDir), name: 'test', index: 0 }; | ||
| } | ||
|
|
||
| return undefined; | ||
| }); | ||
| onDidChangeWorkspaceFoldersStub = sinon.stub(vscode.workspace, 'onDidChangeWorkspaceFolders').returns({ dispose: () => { } } as vscode.Disposable); | ||
| onDidChangeActiveTextEditorStub = sinon.stub(vscode.window, 'onDidChangeActiveTextEditor').returns({ dispose: () => { } } as vscode.Disposable); | ||
| executeCommandStub = sinon.stub(vscode.commands, 'executeCommand').resolves(undefined); | ||
| }); | ||
|
|
||
| teardown(() => { | ||
| executeCommandStub.restore(); | ||
| onDidChangeActiveTextEditorStub.restore(); | ||
| onDidChangeWorkspaceFoldersStub.restore(); | ||
| getWorkspaceFolderStub.restore(); | ||
| workspaceFoldersStub.restore(); | ||
| activeEditorStub.restore(); | ||
| fs.rmSync(tempDir, { recursive: true, force: true }); | ||
| }); | ||
|
|
||
| test('returns containing project file when active editor is SDK-style AppHost Program.cs', async () => { | ||
| const appHostDirectory = path.join(tempDir, 'AppHost'); | ||
| fs.mkdirSync(appHostDirectory); | ||
|
|
||
| const programPath = path.join(appHostDirectory, 'Program.cs'); | ||
| const projectPath = path.join(appHostDirectory, 'AppHost.csproj'); | ||
| fs.writeFileSync(programPath, 'var builder = DistributedApplication.CreateBuilder(args);\nbuilder.Build().Run();'); | ||
| fs.writeFileSync(projectPath, '<Project Sdk="Microsoft.NET.Sdk" />'); | ||
| activeEditor = createEditor(programPath); | ||
|
|
||
| const provider = new AspireEditorCommandProvider(); | ||
| try { | ||
| assert.strictEqual(await provider.getAppHostPath(), projectPath); | ||
| } | ||
| finally { | ||
| provider.dispose(); | ||
| } | ||
| }); | ||
|
|
||
| test('returns source file when active editor is single-file apphost.cs', async () => { | ||
| const appHostPath = path.join(tempDir, 'apphost.cs'); | ||
| fs.writeFileSync(appHostPath, '#:sdk Aspire.AppHost.Sdk\nvar builder = DistributedApplication.CreateBuilder(args);'); | ||
| activeEditor = createEditor(appHostPath); | ||
|
|
||
| const provider = new AspireEditorCommandProvider(); | ||
| try { | ||
| assert.strictEqual(await provider.getAppHostPath(), appHostPath); | ||
| } | ||
| finally { | ||
| provider.dispose(); | ||
| } | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| import type { Dirent } from 'fs'; | ||
| import * as fs from 'fs/promises'; | ||
| import * as path from 'path'; | ||
| import * as vscode from 'vscode'; | ||
|
|
||
| export async function resolveAppHostLaunchPath(filePath: string): Promise<string> { | ||
| if (path.extname(filePath).toLowerCase() !== '.cs') { | ||
| return filePath; | ||
| } | ||
|
|
||
| let fileText: string; | ||
| try { | ||
| fileText = await vscode.workspace.fs.readFile(vscode.Uri.file(filePath)).then(buffer => buffer.toString()); | ||
| } | ||
| catch { | ||
| return filePath; | ||
| } | ||
|
|
||
| const lines = fileText.split(/\r?\n/); | ||
|
|
||
| // Single-file C# AppHosts are launched directly from source and start with: | ||
| // #:sdk Aspire.AppHost.Sdk | ||
| // The CLI accepts this source file shape, so do not rewrite it to a project path. | ||
| if (lines.some(line => line.startsWith('#:sdk Aspire.AppHost.Sdk'))) { | ||
| return filePath; | ||
| } | ||
|
|
||
| if (!lines.some(line => line.includes('DistributedApplication.CreateBuilder'))) { | ||
| return filePath; | ||
| } | ||
|
|
||
| // SDK-style C# AppHosts usually launch from Program.cs: | ||
| // var builder = DistributedApplication.CreateBuilder(args); | ||
| // The CLI needs the containing .csproj instead of Program.cs so the AppHost SDK | ||
| // and project references load. | ||
| return await tryFindContainingProjectFile(filePath) ?? filePath; | ||
| } | ||
|
|
||
| async function tryFindContainingProjectFile(filePath: string): Promise<string | null> { | ||
| const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(filePath)); | ||
| const workspaceRoot = workspaceFolder?.uri.fsPath; | ||
| let directory = path.dirname(filePath); | ||
|
|
||
| while (true) { | ||
| const projectFile = await tryGetProjectFileInDirectory(directory); | ||
| if (projectFile !== undefined) { | ||
| return projectFile; | ||
| } | ||
|
|
||
| if (workspaceRoot && path.resolve(directory) === path.resolve(workspaceRoot)) { | ||
| return null; | ||
| } | ||
|
|
||
| const parent = path.dirname(directory); | ||
| if (parent === directory) { | ||
| return null; | ||
| } | ||
|
|
||
| directory = parent; | ||
| } | ||
| } | ||
|
|
||
| async function tryGetProjectFileInDirectory(directory: string): Promise<string | null | undefined> { | ||
| let entries: Dirent[]; | ||
| try { | ||
| entries = await fs.readdir(directory, { withFileTypes: true }); | ||
| } | ||
| catch { | ||
| return undefined; | ||
| } | ||
|
|
||
| const projectFiles = entries | ||
| .filter(entry => entry.isFile() && /\.(csproj|fsproj|vbproj)$/i.test(entry.name)) | ||
| .map(entry => entry.name); | ||
|
|
||
| if (projectFiles.length === 0) { | ||
| return undefined; | ||
| } | ||
|
|
||
| if (projectFiles.length === 1) { | ||
| return path.join(directory, projectFiles[0]); | ||
| } | ||
|
|
||
| const directoryName = path.basename(directory); | ||
| const matchingProjectFile = projectFiles.find(projectFile => | ||
| path.basename(projectFile, path.extname(projectFile)).toLowerCase() === directoryName.toLowerCase()); | ||
| return matchingProjectFile ? path.join(directory, matchingProjectFile) : null; | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.