|
5 | 5 |
|
6 | 6 | import * as vscode from 'vscode'; |
7 | 7 | import * as path from 'path'; |
| 8 | +import * as cp from 'child_process'; |
| 9 | +import * as fs from 'fs'; |
8 | 10 | import { PlatformInformation } from './platform'; |
9 | 11 | import { VbNetLanguageClient } from './languageClient'; |
10 | 12 | import { VbNetStatusBar } from './statusBar'; |
@@ -208,13 +210,41 @@ function registerCommands(context: vscode.ExtensionContext): void { |
208 | 210 | } |
209 | 211 | }) |
210 | 212 | ); |
| 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 | + ); |
211 | 237 | } |
212 | 238 |
|
213 | 239 | interface SolutionPickItem extends vscode.QuickPickItem { |
214 | 240 | solutionPath?: string; |
215 | 241 | action?: 'clear'; |
216 | 242 | } |
217 | 243 |
|
| 244 | +interface ProjectPickItem extends vscode.QuickPickItem { |
| 245 | + projectPath: string; |
| 246 | +} |
| 247 | + |
218 | 248 | async function selectWorkspaceSolution(): Promise<void> { |
219 | 249 | const workspaceFolders = vscode.workspace.workspaceFolders; |
220 | 250 | if (!workspaceFolders || workspaceFolders.length === 0) { |
@@ -338,6 +368,180 @@ async function toggleLspTrace(): Promise<void> { |
338 | 368 | vscode.window.showInformationMessage(message); |
339 | 369 | } |
340 | 370 |
|
| 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 | + |
341 | 545 | /** |
342 | 546 | * Extension deactivation. |
343 | 547 | * Called when the extension is deactivated. |
|
0 commit comments