@@ -271,6 +271,31 @@ function registerCommands(context: vscode.ExtensionContext): void {
271271 }
272272 } )
273273 ) ;
274+
275+ context . subscriptions . push (
276+ vscode . commands . registerCommand ( 'vbnet.runTestsInContext' , async ( ) => {
277+ try {
278+ await runTestsInContext ( ) ;
279+ } catch ( error ) {
280+ const message = error instanceof Error ? error . message : String ( error ) ;
281+ outputChannel ?. appendLine ( `Failed to run tests: ${ message } ` ) ;
282+ vscode . window . showErrorMessage ( `Failed to run tests: ${ message } ` ) ;
283+ }
284+ } )
285+ ) ;
286+
287+ context . subscriptions . push (
288+ vscode . commands . registerCommand ( 'vbnet.debugTestsInContext' , async ( ) => {
289+ try {
290+ vscode . window . showInformationMessage ( 'Debug test support is in preview; running dotnet test for now.' ) ;
291+ await runTestsInContext ( true ) ;
292+ } catch ( error ) {
293+ const message = error instanceof Error ? error . message : String ( error ) ;
294+ outputChannel ?. appendLine ( `Failed to debug tests: ${ message } ` ) ;
295+ vscode . window . showErrorMessage ( `Failed to debug tests: ${ message } ` ) ;
296+ }
297+ } )
298+ ) ;
274299}
275300
276301interface SolutionPickItem extends vscode . QuickPickItem {
@@ -292,6 +317,10 @@ interface ProcessPickItem extends vscode.QuickPickItem {
292317 pid : number ;
293318}
294319
320+ interface TestPickItem extends vscode . QuickPickItem {
321+ targetPath ?: string ;
322+ }
323+
295324async function selectWorkspaceSolution ( ) : Promise < void > {
296325 const workspaceFolders = vscode . workspace . workspaceFolders ;
297326 if ( ! workspaceFolders || workspaceFolders . length === 0 ) {
@@ -429,7 +458,7 @@ async function restoreWorkspace(): Promise<void> {
429458 }
430459
431460 const label = candidateSolution ? `Restoring ${ path . basename ( candidateSolution ) } ` : 'Restoring workspace' ;
432- await runDotnetCommand ( args , workspaceRoot , label ) ;
461+ await runDotnetCommand ( args , workspaceRoot , label , 'Restore completed.' ) ;
433462}
434463
435464async function restoreProject ( ) : Promise < void > {
@@ -465,7 +494,30 @@ async function restoreProject(): Promise<void> {
465494 return ;
466495 }
467496
468- await runDotnetCommand ( [ 'restore' , pick . projectPath ] , workspaceRoot , `Restoring ${ pick . label } ` ) ;
497+ await runDotnetCommand ( [ 'restore' , pick . projectPath ] , workspaceRoot , `Restoring ${ pick . label } ` , 'Restore completed.' ) ;
498+ }
499+
500+ async function runTestsInContext ( debug : boolean = false ) : Promise < void > {
501+ const workspaceRoot = getWorkspaceRoot ( ) ;
502+ if ( ! workspaceRoot ) {
503+ return ;
504+ }
505+
506+ const target = await resolveTestTarget ( workspaceRoot ) ;
507+ const args = [ 'test' ] ;
508+ if ( target ) {
509+ args . push ( target ) ;
510+ }
511+
512+ const title = target
513+ ? `${ debug ? 'Debugging' : 'Running' } tests: ${ path . basename ( target ) } `
514+ : `${ debug ? 'Debugging' : 'Running' } tests` ;
515+
516+ const successMessage = debug
517+ ? 'Test run completed (debug attach not yet implemented).'
518+ : 'Test run completed.' ;
519+
520+ await runDotnetCommand ( args , workspaceRoot , title , successMessage ) ;
469521}
470522
471523function getWorkspaceRoot ( ) : string | undefined {
@@ -517,6 +569,59 @@ async function pickWorkspaceSolutionCandidate(workspaceRoot: string): Promise<st
517569 return candidates [ 0 ] ;
518570}
519571
572+ async function resolveTestTarget ( workspaceRoot : string ) : Promise < string | undefined > {
573+ const activeFile = vscode . window . activeTextEditor ?. document ?. uri ?. fsPath ;
574+ if ( activeFile && activeFile . startsWith ( workspaceRoot ) && activeFile . toLowerCase ( ) . endsWith ( '.vb' ) ) {
575+ const nearestProject = findNearestProjectForFile ( activeFile , workspaceRoot ) ;
576+ if ( nearestProject ) {
577+ return nearestProject ;
578+ }
579+ }
580+
581+ const configuredSolution = getConfiguredSolutionPath ( workspaceRoot ) ;
582+ if ( configuredSolution ) {
583+ return configuredSolution ;
584+ }
585+
586+ const candidateSolution = await pickWorkspaceSolutionCandidate ( workspaceRoot ) ;
587+ if ( candidateSolution ) {
588+ return candidateSolution ;
589+ }
590+
591+ const projects = await findWorkspaceProjects ( ) ;
592+ if ( projects . length === 1 ) {
593+ return projects [ 0 ] ;
594+ }
595+
596+ if ( projects . length > 1 ) {
597+ const items : TestPickItem [ ] = projects . map ( ( projectPath ) => {
598+ const relative = path . relative ( workspaceRoot , projectPath ) ;
599+ const label = relative && ! relative . startsWith ( '..' ) && ! path . isAbsolute ( relative )
600+ ? relative
601+ : path . basename ( projectPath ) ;
602+ return {
603+ label,
604+ detail : projectPath ,
605+ targetPath : projectPath
606+ } ;
607+ } ) ;
608+
609+ items . unshift ( {
610+ label : 'Workspace (dotnet test)' ,
611+ description : 'Run tests without an explicit project/solution'
612+ } ) ;
613+
614+ const pick = await vscode . window . showQuickPick ( items , {
615+ placeHolder : 'Select a project/solution to test' ,
616+ matchOnDescription : true
617+ } ) ;
618+
619+ return pick ?. targetPath ;
620+ }
621+
622+ return undefined ;
623+ }
624+
520625async function findWorkspaceSolutions ( ) : Promise < string [ ] > {
521626 const config = vscode . workspace . getConfiguration ( 'vbnet' ) ;
522627 const defaultExclude = '**/node_modules/**,**/.git/**,**/bower_components/**' ;
@@ -543,7 +648,7 @@ async function findWorkspaceProjects(): Promise<string[]> {
543648 return resources . map ( ( resource ) => resource . fsPath ) ;
544649}
545650
546- async function runDotnetCommand ( args : string [ ] , cwd : string , title : string ) : Promise < void > {
651+ async function runDotnetCommand ( args : string [ ] , cwd : string , title : string , successMessage ?: string ) : Promise < void > {
547652 outputChannel ?. show ( ) ;
548653 outputChannel ?. appendLine ( `Running: dotnet ${ args . join ( ' ' ) } ` ) ;
549654
@@ -578,7 +683,9 @@ async function runDotnetCommand(args: string[], cwd: string, title: string): Pro
578683 } )
579684 ) ;
580685
581- vscode . window . showInformationMessage ( 'Restore completed.' ) ;
686+ if ( successMessage ) {
687+ vscode . window . showInformationMessage ( successMessage ) ;
688+ }
582689}
583690
584691function fsPathExists ( filePath : string ) : boolean {
@@ -589,6 +696,34 @@ function fsPathExists(filePath: string): boolean {
589696 }
590697}
591698
699+ function findNearestProjectForFile ( filePath : string , workspaceRoot : string ) : string | undefined {
700+ let current = path . dirname ( filePath ) ;
701+ const root = path . resolve ( workspaceRoot ) ;
702+
703+ while ( current . startsWith ( root ) ) {
704+ try {
705+ const entries = fs . readdirSync ( current ) ;
706+ const candidates = entries . filter ( ( entry ) => entry . toLowerCase ( ) . endsWith ( '.vbproj' ) ) ;
707+ if ( candidates . length > 0 ) {
708+ return path . join ( current , candidates [ 0 ] ) ;
709+ }
710+ } catch {
711+ return undefined ;
712+ }
713+
714+ if ( current === root ) {
715+ break ;
716+ }
717+ const parent = path . dirname ( current ) ;
718+ if ( parent === current ) {
719+ break ;
720+ }
721+ current = parent ;
722+ }
723+
724+ return undefined ;
725+ }
726+
592727async function attachToProcess ( ) : Promise < void > {
593728 const workspaceFolder = vscode . workspace . workspaceFolders ?. [ 0 ] ;
594729 const processes = await listProcesses ( ) ;
0 commit comments