From ff6cb567c604d6cfd334eab905f832350069d905 Mon Sep 17 00:00:00 2001 From: Dang Nguyen Date: Mon, 1 Jun 2026 14:45:29 +0700 Subject: [PATCH] feat: add Explorer context menu to add directory to Bazel project scope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Right-clicking a directory in VS Code Explorer now shows "Bazel: Add to Project" under a dedicated bazel-jdt group. The action appends the directory to the existing .bazelproject (or creates one), which triggers the file watcher to reimport with the expanded scope. Also fix handleImportProject() to create Eclipse projects for newly discovered targets — previously only BazelProjectImporter (run once at JDT.LS startup) created projects, so targets from a scope-change reimport were never registered with JDT, causing "Main class does not exist" errors when debugging. Co-Authored-By: Claude Sonnet 4.6 --- .../vscode-extension/package-lock.json | 4 +- .../vscode-extension/package.json | 13 +++++ .../vscode-extension/src/bazelproject.ts | 50 +++++++++++++++++++ .../vscode-extension/src/commands.ts | 32 +++++++++++- .../vscode-extension/src/extension.ts | 4 +- 5 files changed, 99 insertions(+), 4 deletions(-) diff --git a/bazel-jdt-bridge/vscode-extension/package-lock.json b/bazel-jdt-bridge/vscode-extension/package-lock.json index 0bf39d2..278477b 100644 --- a/bazel-jdt-bridge/vscode-extension/package-lock.json +++ b/bazel-jdt-bridge/vscode-extension/package-lock.json @@ -1,12 +1,12 @@ { "name": "bazel-jdt-bridge", - "version": "0.1.0-pre.4", + "version": "0.1.0-pre.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bazel-jdt-bridge", - "version": "0.1.0-pre.4", + "version": "0.1.0-pre.5", "devDependencies": { "@types/mocha": "^10.0.9", "@types/node": "^18.19.130", diff --git a/bazel-jdt-bridge/vscode-extension/package.json b/bazel-jdt-bridge/vscode-extension/package.json index 24d9476..cd70250 100644 --- a/bazel-jdt-bridge/vscode-extension/package.json +++ b/bazel-jdt-bridge/vscode-extension/package.json @@ -34,8 +34,21 @@ { "command": "bazel-jdt.createProjectForPackage", "title": "Bazel: Create Project for Package" + }, + { + "command": "bazel-jdt.addDirectoryToProject", + "title": "Bazel: Add to Project" } ], + "menus": { + "explorer/context": [ + { + "command": "bazel-jdt.addDirectoryToProject", + "when": "explorerResourceIsFolder", + "group": "bazel-jdt@1" + } + ] + }, "configuration": { "title": "Bazel JDT Bridge", "properties": { diff --git a/bazel-jdt-bridge/vscode-extension/src/bazelproject.ts b/bazel-jdt-bridge/vscode-extension/src/bazelproject.ts index 2c00163..3058bd5 100644 --- a/bazel-jdt-bridge/vscode-extension/src/bazelproject.ts +++ b/bazel-jdt-bridge/vscode-extension/src/bazelproject.ts @@ -116,6 +116,56 @@ export function parseBazelprojectContent(content: string): BazelProjectViewConfi return config; } +export function addDirectoryToBazelproject( + bazelprojectPath: string, + relativeDir: string +): 'added' | 'already-exists' | 'created' { + if (fs.existsSync(bazelprojectPath)) { + const content = fs.readFileSync(bazelprojectPath, 'utf-8'); + const config = parseBazelprojectContent(content); + if (config.directories.includes(relativeDir)) { + return 'already-exists'; + } + fs.writeFileSync(bazelprojectPath, insertDirectoryIntoContent(content, relativeDir), 'utf-8'); + return 'added'; + } else { + fs.writeFileSync( + bazelprojectPath, + `directories:\n ${relativeDir}\n\nderive_targets_from_directories: True\n`, + 'utf-8' + ); + return 'created'; + } +} + +function insertDirectoryIntoContent(content: string, dir: string): string { + const lines = content.split('\n'); + let inDirectories = false; + let lastDirLineIdx = -1; + + for (let i = 0; i < lines.length; i++) { + const trimmed = lines[i].trim(); + if (trimmed === 'directories:') { + inDirectories = true; + lastDirLineIdx = i; + continue; + } + if (inDirectories) { + if (trimmed.length === 0 || trimmed.startsWith('#') || /^[a-z_]+:/i.test(trimmed)) { + break; + } + lastDirLineIdx = i; + } + } + + if (lastDirLineIdx >= 0) { + lines.splice(lastDirLineIdx + 1, 0, ` ${dir}`); + } else { + lines.unshift('directories:', ` ${dir}`, ''); + } + return lines.join('\n'); +} + export function resolveScopePatterns(config: BazelProjectViewConfig): string[] { const patterns: string[] = []; diff --git a/bazel-jdt-bridge/vscode-extension/src/commands.ts b/bazel-jdt-bridge/vscode-extension/src/commands.ts index fd3b1f5..91f4ead 100644 --- a/bazel-jdt-bridge/vscode-extension/src/commands.ts +++ b/bazel-jdt-bridge/vscode-extension/src/commands.ts @@ -1,7 +1,9 @@ import * as vscode from 'vscode'; +import * as path from 'path'; +import * as fs from 'fs'; import { getConfig } from './config'; import { runImportWizard } from './importWizard'; -import { parseBazelprojectFile } from './bazelproject'; +import { parseBazelprojectFile, addDirectoryToBazelproject } from './bazelproject'; export function registerImportCommand(context: vscode.ExtensionContext) { const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || ''; @@ -44,6 +46,34 @@ export function registerImportCommand(context: vscode.ExtensionContext) { ); } +export function registerAddDirectoryCommand(context: vscode.ExtensionContext, workspaceRoot: string) { + context.subscriptions.push( + vscode.commands.registerCommand('bazel-jdt.addDirectoryToProject', async (uri: vscode.Uri) => { + if (!uri) return; + + const dirPath = uri.fsPath; + const hasBuild = fs.existsSync(path.join(dirPath, 'BUILD')) || + fs.existsSync(path.join(dirPath, 'BUILD.bazel')); + if (!hasBuild) { + vscode.window.showWarningMessage('No BUILD file found in selected directory.'); + return; + } + + const relativeDir = path.relative(workspaceRoot, dirPath); + const bazelprojectPath = path.join(workspaceRoot, '.bazelproject'); + const result = addDirectoryToBazelproject(bazelprojectPath, relativeDir); + + if (result === 'already-exists') { + vscode.window.showInformationMessage(`'${relativeDir}' is already in the Bazel project scope.`); + } else if (result === 'added') { + vscode.window.showInformationMessage(`Added '${relativeDir}' to Bazel project scope.`); + } else { + vscode.window.showInformationMessage(`Created .bazelproject with '${relativeDir}'.`); + } + }) + ); +} + export function registerRuntimeCommands(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand('bazel-jdt.syncProject', async () => { diff --git a/bazel-jdt-bridge/vscode-extension/src/extension.ts b/bazel-jdt-bridge/vscode-extension/src/extension.ts index 7fd87c3..78d7775 100644 --- a/bazel-jdt-bridge/vscode-extension/src/extension.ts +++ b/bazel-jdt-bridge/vscode-extension/src/extension.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as fs from 'fs'; -import { registerImportCommand, registerRuntimeCommands } from './commands'; +import { registerImportCommand, registerRuntimeCommands, registerAddDirectoryCommand } from './commands'; import { BazelDebugConfigurationProvider } from './debugAdapter'; import { createStatusBar } from './statusBar'; import { getConfig } from './config'; @@ -26,6 +26,7 @@ export async function activate(context: vscode.ExtensionContext) { activateFull(context, workspaceRoot); } else { registerImportCommand(context); + registerAddDirectoryCommand(context, workspaceRoot); setupCreationOnlyWatcher(context, workspaceRoot); } } @@ -34,6 +35,7 @@ function activateFull(context: vscode.ExtensionContext, workspaceRoot: string) { const statusBarItem = createStatusBar(context); registerImportCommand(context); registerRuntimeCommands(context); + registerAddDirectoryCommand(context, workspaceRoot); context.subscriptions.push( vscode.debug.registerDebugConfigurationProvider( 'java', new BazelDebugConfigurationProvider()