Skip to content

Commit 4f7bdf4

Browse files
committed
feat(extension): add restore commands
1 parent dfda674 commit 4f7bdf4

7 files changed

Lines changed: 231 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ Once installed, the extension automatically activates when you open a \`.vb\` fi
129129

130130
If you have multiple solutions, use the Command Palette action **"VB.NET: Select Workspace Solution"** to choose the active \`.sln/.slnf/.slnx\`.
131131
For diagnostics, use **"VB.NET: Show Logs"** and **"VB.NET: Toggle LSP Trace"**.
132+
If assets are missing, run **"VB.NET: Restore Workspace"** or **"VB.NET: Restore Project"**.
132133

133134
## Architecture
134135

docs/configuration.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ Last Updated: 2026-01-20
2828
`vbnet.workspace.solutionPath`. Pick "Auto-detect" to clear the override.
2929
- **`VB.NET: Show Logs`** — opens the main output channel and the LSP trace channel.
3030
- **`VB.NET: Toggle LSP Trace`** — toggles `vbnet.trace.server` between `off` and `verbose`.
31+
- **`VB.NET: Restore Workspace`** — runs `dotnet restore` using the selected solution (or workspace root).
32+
- **`VB.NET: Restore Project`** — picks a `.vbproj` file and runs `dotnet restore` for it.
3133

3234
---
3335

src/extension/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,16 @@
230230
"title": "Toggle VB.NET LSP Trace",
231231
"category": "VB.NET"
232232
},
233+
{
234+
"command": "vbnet.restoreWorkspace",
235+
"title": "Restore VB.NET Workspace",
236+
"category": "VB.NET"
237+
},
238+
{
239+
"command": "vbnet.restoreProject",
240+
"title": "Restore VB.NET Project",
241+
"category": "VB.NET"
242+
},
233243
{
234244
"command": "vbnet.showOutputChannel",
235245
"title": "Show VB.NET Output",

src/extension/src/extension.ts

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
import * as vscode from 'vscode';
77
import * as path from 'path';
8+
import * as cp from 'child_process';
9+
import * as fs from 'fs';
810
import { PlatformInformation } from './platform';
911
import { VbNetLanguageClient } from './languageClient';
1012
import { VbNetStatusBar } from './statusBar';
@@ -208,13 +210,41 @@ function registerCommands(context: vscode.ExtensionContext): void {
208210
}
209211
})
210212
);
213+
214+
context.subscriptions.push(
215+
vscode.commands.registerCommand('vbnet.restoreWorkspace', async () => {
216+
try {
217+
await restoreWorkspace();
218+
} catch (error) {
219+
const message = error instanceof Error ? error.message : String(error);
220+
outputChannel?.appendLine(`Failed to restore workspace: ${message}`);
221+
vscode.window.showErrorMessage(`Failed to restore workspace: ${message}`);
222+
}
223+
})
224+
);
225+
226+
context.subscriptions.push(
227+
vscode.commands.registerCommand('vbnet.restoreProject', async () => {
228+
try {
229+
await restoreProject();
230+
} catch (error) {
231+
const message = error instanceof Error ? error.message : String(error);
232+
outputChannel?.appendLine(`Failed to restore project: ${message}`);
233+
vscode.window.showErrorMessage(`Failed to restore project: ${message}`);
234+
}
235+
})
236+
);
211237
}
212238

213239
interface SolutionPickItem extends vscode.QuickPickItem {
214240
solutionPath?: string;
215241
action?: 'clear';
216242
}
217243

244+
interface ProjectPickItem extends vscode.QuickPickItem {
245+
projectPath: string;
246+
}
247+
218248
async function selectWorkspaceSolution(): Promise<void> {
219249
const workspaceFolders = vscode.workspace.workspaceFolders;
220250
if (!workspaceFolders || workspaceFolders.length === 0) {
@@ -338,6 +368,180 @@ async function toggleLspTrace(): Promise<void> {
338368
vscode.window.showInformationMessage(message);
339369
}
340370

371+
async function restoreWorkspace(): Promise<void> {
372+
const workspaceRoot = getWorkspaceRoot();
373+
if (!workspaceRoot) {
374+
return;
375+
}
376+
377+
const configuredSolution = getConfiguredSolutionPath(workspaceRoot);
378+
const candidateSolution = configuredSolution ?? await pickWorkspaceSolutionCandidate(workspaceRoot);
379+
const args = ['restore'];
380+
if (candidateSolution) {
381+
args.push(candidateSolution);
382+
}
383+
384+
const label = candidateSolution ? `Restoring ${path.basename(candidateSolution)}` : 'Restoring workspace';
385+
await runDotnetCommand(args, workspaceRoot, label);
386+
}
387+
388+
async function restoreProject(): Promise<void> {
389+
const workspaceRoot = getWorkspaceRoot();
390+
if (!workspaceRoot) {
391+
return;
392+
}
393+
394+
const projects = await findWorkspaceProjects();
395+
if (projects.length === 0) {
396+
vscode.window.showInformationMessage('No VB.NET project files were found in this workspace.');
397+
return;
398+
}
399+
400+
const items: ProjectPickItem[] = projects.map((projectPath) => {
401+
const relative = path.relative(workspaceRoot, projectPath);
402+
const label = relative && !relative.startsWith('..') && !path.isAbsolute(relative)
403+
? relative
404+
: path.basename(projectPath);
405+
return {
406+
label,
407+
detail: projectPath,
408+
projectPath
409+
};
410+
});
411+
412+
const pick = await vscode.window.showQuickPick(items, {
413+
placeHolder: 'Select a VB.NET project to restore',
414+
canPickMany: false
415+
});
416+
417+
if (!pick) {
418+
return;
419+
}
420+
421+
await runDotnetCommand(['restore', pick.projectPath], workspaceRoot, `Restoring ${pick.label}`);
422+
}
423+
424+
function getWorkspaceRoot(): string | undefined {
425+
const workspaceFolders = vscode.workspace.workspaceFolders;
426+
if (!workspaceFolders || workspaceFolders.length === 0) {
427+
vscode.window.showWarningMessage('No workspace folder is open.');
428+
return undefined;
429+
}
430+
431+
return workspaceFolders[0].uri.fsPath;
432+
}
433+
434+
function getConfiguredSolutionPath(workspaceRoot: string): string | undefined {
435+
const config = vscode.workspace.getConfiguration('vbnet');
436+
const configuredSolution = (config.get<string>('workspace.solutionPath', '') || '').trim();
437+
if (!configuredSolution) {
438+
return undefined;
439+
}
440+
441+
const resolved = path.resolve(workspaceRoot, configuredSolution);
442+
if (resolved && fsPathExists(resolved)) {
443+
return resolved;
444+
}
445+
446+
outputChannel?.appendLine(`Configured solution path not found: ${configuredSolution}`);
447+
return undefined;
448+
}
449+
450+
async function pickWorkspaceSolutionCandidate(workspaceRoot: string): Promise<string | undefined> {
451+
const solutions = await findWorkspaceSolutions();
452+
if (solutions.length === 0) {
453+
return undefined;
454+
}
455+
456+
const withVb = [];
457+
for (const solutionPath of solutions) {
458+
if (await solutionLikelyHasVbProjects(solutionPath)) {
459+
withVb.push(solutionPath);
460+
}
461+
}
462+
463+
const candidates = (withVb.length > 0 ? withVb : solutions)
464+
.sort((a, b) => {
465+
const depthA = a.split(path.sep).length;
466+
const depthB = b.split(path.sep).length;
467+
return depthA - depthB;
468+
});
469+
470+
return candidates[0];
471+
}
472+
473+
async function findWorkspaceSolutions(): Promise<string[]> {
474+
const config = vscode.workspace.getConfiguration('vbnet');
475+
const defaultExclude = '**/node_modules/**,**/.git/**,**/bower_components/**';
476+
const excludePattern = config.get<string>('workspace.projectFilesExcludePattern', defaultExclude);
477+
478+
const resources = await vscode.workspace.findFiles(
479+
'{**/*.sln,**/*.slnf,**/*.slnx}',
480+
`{${excludePattern}}`
481+
);
482+
483+
return resources.map((resource) => resource.fsPath);
484+
}
485+
486+
async function findWorkspaceProjects(): Promise<string[]> {
487+
const config = vscode.workspace.getConfiguration('vbnet');
488+
const defaultExclude = '**/node_modules/**,**/.git/**,**/bower_components/**';
489+
const excludePattern = config.get<string>('workspace.projectFilesExcludePattern', defaultExclude);
490+
491+
const resources = await vscode.workspace.findFiles(
492+
'**/*.vbproj',
493+
`{${excludePattern}}`
494+
);
495+
496+
return resources.map((resource) => resource.fsPath);
497+
}
498+
499+
async function runDotnetCommand(args: string[], cwd: string, title: string): Promise<void> {
500+
outputChannel?.show();
501+
outputChannel?.appendLine(`Running: dotnet ${args.join(' ')}`);
502+
503+
await vscode.window.withProgress(
504+
{
505+
location: vscode.ProgressLocation.Notification,
506+
title,
507+
cancellable: false
508+
},
509+
() => new Promise<void>((resolve, reject) => {
510+
const child = cp.spawn('dotnet', args, { cwd, env: process.env });
511+
512+
child.stdout?.on('data', (data: Buffer) => {
513+
outputChannel?.appendLine(data.toString().trimEnd());
514+
});
515+
516+
child.stderr?.on('data', (data: Buffer) => {
517+
outputChannel?.appendLine(data.toString().trimEnd());
518+
});
519+
520+
child.on('error', (error) => {
521+
reject(error);
522+
});
523+
524+
child.on('exit', (code) => {
525+
if (code === 0) {
526+
resolve();
527+
return;
528+
}
529+
reject(new Error(`dotnet ${args[0]} exited with code ${code}`));
530+
});
531+
})
532+
);
533+
534+
vscode.window.showInformationMessage('Restore completed.');
535+
}
536+
537+
function fsPathExists(filePath: string): boolean {
538+
try {
539+
return fs.statSync(filePath).isFile();
540+
} catch {
541+
return false;
542+
}
543+
}
544+
341545
/**
342546
* Extension deactivation.
343547
* Called when the extension is deactivated.

test-explore/TEST_RESULTS.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,15 @@ Optional flags:
4747

4848
## Recent runs
4949

50+
### 2026-01-22 — VS Code harness (VB.NET server, restore commands)
51+
52+
Command:
53+
- `cd test-explore\\clients\\vscode; npm test`
54+
55+
Outcome: PASS (14 passing, 5 pending)
56+
Notes:
57+
- DAP trace: `test-explore/clients/vscode/logs/dap-trace-2026-01-22T200313660Z.log`.
58+
5059
### 2026-01-22 — VS Code harness (VB.NET server, trace commands)
5160

5261
Command:

test-explore/clients/vscode/src/suite/vbnet-extension.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,8 @@ if (skipVbnetSmoke) {
277277
assert.ok(commands.includes("vbnet.selectWorkspaceSolution"), "Select workspace solution command not registered.");
278278
assert.ok(commands.includes("vbnet.showLogs"), "Show logs command not registered.");
279279
assert.ok(commands.includes("vbnet.toggleLspTrace"), "Toggle LSP trace command not registered.");
280+
assert.ok(commands.includes("vbnet.restoreWorkspace"), "Restore workspace command not registered.");
281+
assert.ok(commands.includes("vbnet.restoreProject"), "Restore project command not registered.");
280282
await config.update("trace.server", "verbose", vscode.ConfigurationTarget.Workspace);
281283
await config.update("server.transportType", "namedPipe", vscode.ConfigurationTarget.Workspace);
282284

test/VbNet.Extension.Tests.Vb/ExtensionManifestTests.vb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ Namespace VbNet.Extension.Tests
6868
Dim required = New String() {
6969
"vbnet.selectWorkspaceSolution",
7070
"vbnet.showLogs",
71-
"vbnet.toggleLspTrace"
71+
"vbnet.toggleLspTrace",
72+
"vbnet.restoreWorkspace",
73+
"vbnet.restoreProject"
7274
}
7375

7476
For Each commandName In required

0 commit comments

Comments
 (0)