Skip to content

Commit be3e65e

Browse files
committed
feat(extension): add workspace solution picker
1 parent 99faf9c commit be3e65e

9 files changed

Lines changed: 167 additions & 2 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ Once installed, the extension automatically activates when you open a \`.vb\` fi
127127
3. The extension will automatically discover and load your `VB.NET` projects
128128
4. Start coding with full IntelliSense support
129129

130+
If you have multiple solutions, use the Command Palette action **"VB.NET: Select Workspace Solution"** to choose the active \`.sln/.slnf/.slnx\`.
131+
130132
## Architecture
131133

132134
`VB.NET` Language Support follows the architecture of the "C# for Visual Studio Code" extension:

docs/configuration.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ Last Updated: 2026-01-20
2222
- **UI**: File > Preferences > Settings > Extensions > `VB.NET` Language Support
2323
- **JSON**: `.vscode/settings.json` (workspace) or User Settings (global)
2424

25+
### Commands
26+
27+
- **`VB.NET: Select Workspace Solution`** — choose a `.sln/.slnf/.slnx` file and update
28+
`vbnet.workspace.solutionPath`. Pick "Auto-detect" to clear the override.
29+
2530
---
2631

2732
## 2. VS Code Settings

src/VbNet.LanguageServer.Vb/Core/LanguageServer.vb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1274,6 +1274,9 @@ Namespace Core
12741274

12751275
Private Shared Function SolutionContainsVbProject(solutionPath As String) As Boolean
12761276
Try
1277+
If solutionPath.EndsWith(".slnx", StringComparison.OrdinalIgnoreCase) Then
1278+
Return True
1279+
End If
12771280
Dim content = File.ReadAllText(solutionPath)
12781281
Return content.IndexOf(".vbproj", StringComparison.OrdinalIgnoreCase) >= 0
12791282
Catch
@@ -1326,6 +1329,7 @@ Namespace Core
13261329

13271330
Dim solutionCandidates = Directory.EnumerateFiles(searchRoot, "*.sln", SearchOption.TopDirectoryOnly) _
13281331
.Concat(Directory.EnumerateFiles(searchRoot, "*.slnf", SearchOption.TopDirectoryOnly)) _
1332+
.Concat(Directory.EnumerateFiles(searchRoot, "*.slnx", SearchOption.TopDirectoryOnly)) _
13291333
.OrderBy(Function(path) path, StringComparer.OrdinalIgnoreCase) _
13301334
.ToList()
13311335

src/extension/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,11 @@
215215
"title": "Restart VB.NET Language Server",
216216
"category": "VB.NET"
217217
},
218+
{
219+
"command": "vbnet.selectWorkspaceSolution",
220+
"title": "Select VB.NET Workspace Solution",
221+
"category": "VB.NET"
222+
},
218223
{
219224
"command": "vbnet.showOutputChannel",
220225
"title": "Show VB.NET Output",

src/extension/src/extension.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as vscode from 'vscode';
7+
import * as path from 'path';
78
import { PlatformInformation } from './platform';
89
import { VbNetLanguageClient } from './languageClient';
910
import { VbNetStatusBar } from './statusBar';
@@ -175,6 +176,127 @@ function registerCommands(context: vscode.ExtensionContext): void {
175176
outputChannel?.show();
176177
})
177178
);
179+
180+
context.subscriptions.push(
181+
vscode.commands.registerCommand('vbnet.selectWorkspaceSolution', async () => {
182+
try {
183+
await selectWorkspaceSolution();
184+
} catch (error) {
185+
const message = error instanceof Error ? error.message : String(error);
186+
outputChannel?.appendLine(`Failed to select workspace solution: ${message}`);
187+
vscode.window.showErrorMessage(`Failed to select workspace solution: ${message}`);
188+
}
189+
})
190+
);
191+
}
192+
193+
interface SolutionPickItem extends vscode.QuickPickItem {
194+
solutionPath?: string;
195+
action?: 'clear';
196+
}
197+
198+
async function selectWorkspaceSolution(): Promise<void> {
199+
const workspaceFolders = vscode.workspace.workspaceFolders;
200+
if (!workspaceFolders || workspaceFolders.length === 0) {
201+
vscode.window.showWarningMessage('No workspace folder is open.');
202+
return;
203+
}
204+
205+
const config = vscode.workspace.getConfiguration('vbnet');
206+
const defaultExclude = '**/node_modules/**,**/.git/**,**/bower_components/**';
207+
const excludePattern = config.get<string>('workspace.projectFilesExcludePattern', defaultExclude);
208+
209+
const resources = await vscode.workspace.findFiles(
210+
'{**/*.sln,**/*.slnf,**/*.slnx}',
211+
`{${excludePattern}}`
212+
);
213+
214+
if (resources.length === 0) {
215+
vscode.window.showInformationMessage('No solution files were found in this workspace.');
216+
return;
217+
}
218+
219+
const workspaceRoot = workspaceFolders[0].uri.fsPath;
220+
const configuredSolution = (config.get<string>('workspace.solutionPath', '') || '').trim();
221+
const configuredResolved = configuredSolution
222+
? path.normalize(path.resolve(workspaceRoot, configuredSolution))
223+
: '';
224+
225+
const items: SolutionPickItem[] = [];
226+
items.push({
227+
label: 'Auto-detect',
228+
description: 'Clear workspace solution override',
229+
action: 'clear'
230+
});
231+
232+
const candidates = resources
233+
.map((resource) => resource.fsPath)
234+
.sort((a, b) => a.localeCompare(b));
235+
236+
for (const candidate of candidates) {
237+
const resolved = path.normalize(candidate);
238+
const relative = path.relative(workspaceRoot, candidate);
239+
const isRelative = relative && !relative.startsWith('..') && !path.isAbsolute(relative);
240+
const label = isRelative ? relative : path.basename(candidate);
241+
242+
const hasVb = await solutionLikelyHasVbProjects(candidate);
243+
let description: string | undefined = undefined;
244+
if (configuredResolved && resolved === configuredResolved) {
245+
description = 'Current selection';
246+
} else if (!hasVb) {
247+
description = 'No .vbproj references detected';
248+
}
249+
250+
items.push({
251+
label,
252+
description,
253+
detail: candidate,
254+
solutionPath: candidate
255+
});
256+
}
257+
258+
const pick = await vscode.window.showQuickPick(items, {
259+
placeHolder: 'Select a workspace solution for VB.NET (clears auto-detection)',
260+
canPickMany: false
261+
});
262+
263+
if (!pick) {
264+
return;
265+
}
266+
267+
if (pick.action === 'clear') {
268+
await config.update('workspace.solutionPath', '', vscode.ConfigurationTarget.Workspace);
269+
outputChannel?.appendLine('Workspace solution override cleared (auto-detect enabled).');
270+
vscode.window.showInformationMessage('Workspace solution override cleared (auto-detect enabled).');
271+
return;
272+
}
273+
274+
if (!pick.solutionPath) {
275+
return;
276+
}
277+
278+
const relative = path.relative(workspaceRoot, pick.solutionPath);
279+
const configValue = relative && !relative.startsWith('..') && !path.isAbsolute(relative)
280+
? relative
281+
: pick.solutionPath;
282+
283+
await config.update('workspace.solutionPath', configValue, vscode.ConfigurationTarget.Workspace);
284+
outputChannel?.appendLine(`Workspace solution override set to: ${configValue}`);
285+
vscode.window.showInformationMessage(`Workspace solution set to ${configValue}`);
286+
}
287+
288+
async function solutionLikelyHasVbProjects(solutionPath: string): Promise<boolean> {
289+
if (solutionPath.toLowerCase().endsWith('.slnx')) {
290+
return true;
291+
}
292+
293+
try {
294+
const content = await vscode.workspace.fs.readFile(vscode.Uri.file(solutionPath));
295+
const text = Buffer.from(content).toString('utf8');
296+
return text.toLowerCase().includes('.vbproj');
297+
} catch {
298+
return true;
299+
}
178300
}
179301

180302
/**

src/extension/src/languageClient.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import * as vscode from 'vscode';
77
import * as net from 'net';
8+
import * as path from 'path';
89
import {
910
LanguageClient,
1011
LanguageClientOptions,
@@ -207,6 +208,7 @@ export class VbNetLanguageClient implements vscode.Disposable {
207208
vscode.workspace.createFileSystemWatcher('**/*.vbproj'),
208209
vscode.workspace.createFileSystemWatcher('**/*.sln'),
209210
vscode.workspace.createFileSystemWatcher('**/*.slnf'),
211+
vscode.workspace.createFileSystemWatcher('**/*.slnx'),
210212
vscode.workspace.createFileSystemWatcher('**/Directory.Build.props'),
211213
vscode.workspace.createFileSystemWatcher('**/Directory.Build.targets')
212214
]
@@ -336,12 +338,12 @@ export class VbNetLanguageClient implements vscode.Disposable {
336338
}
337339

338340
const resources = await vscode.workspace.findFiles(
339-
'{**/*.sln,**/*.slnf,**/*.vbproj}',
341+
'{**/*.sln,**/*.slnf,**/*.slnx,**/*.vbproj}',
340342
`{${excludePattern}}`
341343
);
342344

343345
const workspaceRoot = workspaceFolders[0].uri.fsPath;
344-
const solutionCandidates = resources.filter((resource) => /\.slnf?$/i.test(resource.fsPath));
346+
const solutionCandidates = resources.filter((resource) => /\.(sln|slnf|slnx)$/i.test(resource.fsPath));
345347
const vbProjectFiles = resources.filter((resource) => /\.vbproj$/i.test(resource.fsPath));
346348

347349
const solutionPath = await this.pickSolutionWithVbProjects(solutionCandidates);
@@ -382,6 +384,11 @@ export class VbNetLanguageClient implements vscode.Disposable {
382384
const filtered = [];
383385
for (const candidate of candidates) {
384386
try {
387+
const extension = path.extname(candidate.fsPath).toLowerCase();
388+
if (extension === '.slnx') {
389+
filtered.push(candidate);
390+
continue;
391+
}
385392
const content = await vscode.workspace.fs.readFile(candidate);
386393
const text = Buffer.from(content).toString('utf8');
387394
if (text.toLowerCase().includes('.vbproj')) {

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)
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-22T195325297Z.log`.
58+
5059
### 2026-01-20 � test-explore suite (all, VB.NET)
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
@@ -271,8 +271,10 @@ if (skipVbnetSmoke) {
271271
const config = vscode.workspace.getConfiguration("vbnet");
272272
const originalTransport = config.get<string>("server.transportType", "auto");
273273
const originalTrace = config.get<string>("trace.server", "off");
274+
const commands = await vscode.commands.getCommands(true);
274275

275276
try {
277+
assert.ok(commands.includes("vbnet.selectWorkspaceSolution"), "Select workspace solution command not registered.");
276278
await config.update("trace.server", "verbose", vscode.ConfigurationTarget.Workspace);
277279
await config.update("server.transportType", "namedPipe", vscode.ConfigurationTarget.Workspace);
278280

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ Namespace VbNet.Extension.Tests
5858

5959
Assert.True(launchProps.TryGetProperty("projectPath", Nothing), "Expected launch configuration to include projectPath.")
6060
End Sub
61+
62+
<Fact>
63+
Public Sub CommandsIncludeWorkspaceSolutionPicker()
64+
Dim root = LoadPackageJson()
65+
Dim commands = root.GetProperty("contributes").GetProperty("commands")
66+
67+
Dim hasCommand = commands.EnumerateArray().Any(Function(item) item.GetProperty("command").GetString() = "vbnet.selectWorkspaceSolution")
68+
Assert.True(hasCommand, "Expected vbnet.selectWorkspaceSolution to be contributed in package.json.")
69+
End Sub
6170
End Class
6271

6372
End Namespace

0 commit comments

Comments
 (0)