diff --git a/bazel-jdt-bridge/crates/bazel-jdt-core/src/jni_exports.rs b/bazel-jdt-bridge/crates/bazel-jdt-core/src/jni_exports.rs index 9e090e9..cc8d780 100644 --- a/bazel-jdt-bridge/crates/bazel-jdt-core/src/jni_exports.rs +++ b/bazel-jdt-bridge/crates/bazel-jdt-core/src/jni_exports.rs @@ -902,6 +902,54 @@ pub extern "system" fn Java_com_bazel_jdt_BazelBridge_nativeSyncIncremental( } } +#[no_mangle] +pub extern "system" fn Java_com_bazel_jdt_BazelBridge_nativeGetReverseDepsInProjects( + mut env: JNIEnv, + _class: JClass, + handle: jlong, + target_labels: JObjectArray, +) -> jobjectArray { + let state = match get_state(&mut env, handle) { + Some(s) => s, + None => return std::ptr::null_mut(), + }; + + let labels = match parse_java_string_array(&mut env, &target_labels) { + Some(l) => l, + None => { + return match create_string_array(&mut env, &[]) { + Ok(arr) => arr, + Err(_) => std::ptr::null_mut(), + } + } + }; + + let graph = state.graph.lock().unwrap_or_else(|e| e.into_inner()); + + let mut all_rdeps: std::collections::HashSet = std::collections::HashSet::new(); + for label in &labels { + let rdeps = graph.reverse_transitive_deps(label); + all_rdeps.extend(rdeps); + } + for label in &labels { + all_rdeps.remove(label); + } + + let mut result: Vec = all_rdeps.into_iter().collect(); + result.sort(); + + log::info!( + "Reverse deps for {} targets: {} rdep targets", + labels.len(), + result.len() + ); + + match create_string_array(&mut env, &result) { + Ok(arr) => arr, + Err(_) => std::ptr::null_mut(), + } +} + #[no_mangle] pub extern "system" fn Java_com_bazel_jdt_BazelBridge_nativeGetAspectBuildStats( mut env: JNIEnv, diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelBridge.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelBridge.java index b1c2e0a..1bf2e6d 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelBridge.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelBridge.java @@ -312,6 +312,23 @@ public String[] getTransitiveWorkspaceDeps(String[] targetLabels) { } } + public String[] getReverseDepsInProjects(String[] targetLabels) { + long h = snapshotHandle(); + try { + return jniExecutor.submit(() -> nativeGetReverseDepsInProjects(h, targetLabels)) + .get(JNI_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted during getReverseDepsInProjects", e); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + if (cause instanceof RuntimeException) throw (RuntimeException) cause; + throw new RuntimeException("getReverseDepsInProjects failed", cause); + } catch (TimeoutException e) { + throw new RuntimeException("getReverseDepsInProjects timed out", e); + } + } + public String[] syncIncremental(String[] changedFilePaths) { long h = snapshotHandle(); try { @@ -388,6 +405,7 @@ private long snapshotHandleNullable() { private native void nativeCleanCache(long handle); private native String[] nativeGetPendingChanges(long handle); private native String[] nativeGetTransitiveWorkspaceDeps(long handle, String[] targetLabels); + private native String[] nativeGetReverseDepsInProjects(long handle, String[] targetLabels); private native String[] nativeSyncIncremental(long handle, String[] changedFilePaths); private native String nativeGetAspectBuildStats(long handle); private native boolean nativeIsTestTarget(long handle, String targetLabel); diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathManager.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathManager.java index cfda34f..6e72b20 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathManager.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathManager.java @@ -296,6 +296,10 @@ public static void refreshClasspathForFiles(List changedFiles) { * Used by incremental sync to update only affected targets. */ public static void refreshClasspathForTargets(List targetLabels) { + refreshClasspathForTargets(targetLabels, false); + } + + public static void refreshClasspathForTargets(List targetLabels, boolean force) { if (targetLabels == null || targetLabels.isEmpty()) return; try { org.eclipse.core.resources.IWorkspace workspace = @@ -321,7 +325,7 @@ public static void refreshClasspathForTargets(List targetLabels) { } } for (IProject project : affectedProjects) { - setMergedClasspathContainer(project); + setMergedClasspathContainer(project, force); } } catch (Exception e) { LOG.log(new Status(IStatus.ERROR, "com.bazel.jdt", diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelCommandHandler.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelCommandHandler.java index 7545db1..a3af6d2 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelCommandHandler.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelCommandHandler.java @@ -1,8 +1,10 @@ package com.bazel.jdt; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import org.eclipse.core.resources.IProject; @@ -47,6 +49,8 @@ public Object executeCommand(String commandId, List arguments, IProgress return handleSetActiveDebugProject(arguments); case "bazel-jdt.clearActiveDebugProject": return handleClearActiveDebugProject(); + case "bazel-jdt.partialSync": + return handlePartialSync(arguments); default: return null; } @@ -183,6 +187,87 @@ private void createProjectsForTargets(String workspacePath, Set newTarge } } + private Object handlePartialSync(List arguments) { + try { + if (arguments.isEmpty() || !(arguments.get(0) instanceof String)) { + throw new IllegalArgumentException("Scope pattern required"); + } + String scopePattern = (String) arguments.get(0); + + BazelBridge bridge = BazelBridge.getInstance(); + if (!bridge.isInitialized()) { + throw new IllegalStateException( + "Bazel project not imported yet. Import the project first."); + } + + String syncMode = arguments.size() > 1 && arguments.get(1) instanceof String + ? (String) arguments.get(1) : bridge.getSyncMode(); + bridge.setSyncMode(syncMode); + + LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", + "Partial sync: querying targets for " + scopePattern)); + String[] targets = bridge.queryTargets(new String[]{scopePattern}); + if (targets == null || targets.length == 0) { + LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", + "Partial sync: no targets found for " + scopePattern)); + Map result = new HashMap<>(); + result.put("refreshed", 0); + result.put("newTargets", new ArrayList()); + return result; + } + + String[] rdeps = bridge.getReverseDepsInProjects(targets); + int rdepCount = rdeps != null ? rdeps.length : 0; + LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", + "Partial sync: " + targets.length + " scope targets, " + rdepCount + " rdep targets")); + + Set mergedSet = new java.util.LinkedHashSet<>(); + for (String t : targets) mergedSet.add(t); + if (rdeps != null) { + for (String r : rdeps) mergedSet.add(r); + } + String[] mergedTargets = mergedSet.toArray(new String[0]); + + LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", + "Partial sync: running aspect build for " + mergedTargets.length + " targets (scope + rdeps)")); + bridge.runAspectBuild(mergedTargets, bridge.getBuildFlags()); + + Set existingTargetLabels = getExistingTargetLabels(); + List existingTargets = new ArrayList<>(); + List newTargets = new ArrayList<>(); + for (String label : mergedTargets) { + if (!label.startsWith("//")) { + LOG.log(new Status(IStatus.WARNING, "com.bazel.jdt", + "Partial sync: skipping invalid label: " + label)); + continue; + } + if (existingTargetLabels.contains(label)) { + existingTargets.add(label); + } else { + newTargets.add(label); + } + } + + LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", + "Partial sync: " + existingTargets.size() + " existing, " + + newTargets.size() + " new targets")); + + if (!existingTargets.isEmpty()) { + BazelClasspathManager.refreshClasspathForTargets(existingTargets, true); + } + + Map result = new HashMap<>(); + result.put("refreshed", existingTargets.size()); + result.put("newTargets", newTargets); + result.put("rdeps", rdepCount); + return result; + } catch (Exception e) { + LOG.log(new Status(IStatus.ERROR, "com.bazel.jdt", + "Partial sync failed", e)); + throw new RuntimeException("Partial sync failed: " + e.getMessage(), e); + } + } + private Object handleSyncProject(List arguments) { try { if (!arguments.isEmpty() && arguments.get(0) instanceof String) { diff --git a/bazel-jdt-bridge/java-bridge/src/main/resources/plugin.xml b/bazel-jdt-bridge/java-bridge/src/main/resources/plugin.xml index e2dd134..4d54473 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/resources/plugin.xml +++ b/bazel-jdt-bridge/java-bridge/src/main/resources/plugin.xml @@ -27,6 +27,10 @@ + + + + diff --git a/bazel-jdt-bridge/vscode-extension/package.json b/bazel-jdt-bridge/vscode-extension/package.json index cd70250..f8fe3bf 100644 --- a/bazel-jdt-bridge/vscode-extension/package.json +++ b/bazel-jdt-bridge/vscode-extension/package.json @@ -38,6 +38,10 @@ { "command": "bazel-jdt.addDirectoryToProject", "title": "Bazel: Add to Project" + }, + { + "command": "bazel-jdt.partialSync", + "title": "Bazel: Partially Sync Package" } ], "menus": { @@ -46,6 +50,11 @@ "command": "bazel-jdt.addDirectoryToProject", "when": "explorerResourceIsFolder", "group": "bazel-jdt@1" + }, + { + "command": "bazel-jdt.partialSync", + "when": "explorerResourceIsFolder", + "group": "bazel-jdt@2" } ] }, diff --git a/bazel-jdt-bridge/vscode-extension/src/commands.ts b/bazel-jdt-bridge/vscode-extension/src/commands.ts index 91f4ead..a35cb00 100644 --- a/bazel-jdt-bridge/vscode-extension/src/commands.ts +++ b/bazel-jdt-bridge/vscode-extension/src/commands.ts @@ -5,6 +5,8 @@ import { getConfig } from './config'; import { runImportWizard } from './importWizard'; import { parseBazelprojectFile, addDirectoryToBazelproject } from './bazelproject'; +let syncInProgress = false; + export function registerImportCommand(context: vscode.ExtensionContext) { const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || ''; @@ -77,12 +79,19 @@ export function registerAddDirectoryCommand(context: vscode.ExtensionContext, wo export function registerRuntimeCommands(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand('bazel-jdt.syncProject', async () => { + if (syncInProgress) { + vscode.window.showWarningMessage('A sync is already in progress.'); + return; + } + syncInProgress = true; try { const config = getConfig(); await vscode.commands.executeCommand('java.execute.workspaceCommand', 'bazel-jdt.syncProject', config.dependencyResolution, config.dependencySourceLoading); } catch (error) { vscode.window.showErrorMessage(`Bazel sync failed: ${error}`); + } finally { + syncInProgress = false; } }) ); @@ -116,3 +125,70 @@ export function registerRuntimeCommands(context: vscode.ExtensionContext) { }) ); } + +export function registerPartialSyncCommand(context: vscode.ExtensionContext) { + const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || ''; + + context.subscriptions.push( + vscode.commands.registerCommand('bazel-jdt.partialSync', async (uri: vscode.Uri) => { + if (!uri) { + vscode.window.showWarningMessage('No folder selected.'); + return; + } + + if (syncInProgress) { + vscode.window.showWarningMessage('A sync is already in progress.'); + return; + } + + const dirPath = uri.fsPath; + const hasBuild = fs.existsSync(path.join(dirPath, 'BUILD')) + || fs.existsSync(path.join(dirPath, 'BUILD.bazel')); + if (!hasBuild) { + vscode.window.showInformationMessage('No BUILD file found in this directory.'); + return; + } + + const relativePath = path.relative(workspaceRoot, dirPath).replace(/\\/g, '/'); + const scopePattern = `//${relativePath}/...:all`; + + syncInProgress = true; + try { + const config = getConfig(); + const result = await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: `Partially syncing ${scopePattern}`, + cancellable: false, + }, + async (progress) => { + progress.report({ message: 'Querying targets...' }); + const syncResult = await vscode.commands.executeCommand( + 'java.execute.workspaceCommand', + 'bazel-jdt.partialSync', scopePattern, config.syncMode) as + { refreshed?: number; newTargets?: string[] } | null; + return syncResult; + } + ); + + const newTargets = (result?.newTargets ?? []).filter( + (label: string) => label.startsWith('//') && label.includes(':')); + + if (newTargets.length > 0) { + for (const targetLabel of newTargets) { + const packagePath = targetLabel.replace(/^\/\//, '').replace(/:.*$/, ''); + await vscode.commands.executeCommand('java.execute.workspaceCommand', + 'bazel-jdt.createProjectForPackage', workspaceRoot, config.bazelPath, + config.cacheDir, packagePath, targetLabel); + } + await vscode.commands.executeCommand('java.execute.workspaceCommand', + 'bazel-jdt.syncProject', config.dependencyResolution, config.dependencySourceLoading); + } + } catch (error) { + vscode.window.showErrorMessage(`Partial sync failed: ${error}`); + } finally { + syncInProgress = false; + } + }) + ); +} diff --git a/bazel-jdt-bridge/vscode-extension/src/extension.ts b/bazel-jdt-bridge/vscode-extension/src/extension.ts index 78d7775..3d728f3 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, registerAddDirectoryCommand } from './commands'; +import { registerImportCommand, registerRuntimeCommands, registerAddDirectoryCommand, registerPartialSyncCommand } from './commands'; import { BazelDebugConfigurationProvider } from './debugAdapter'; import { createStatusBar } from './statusBar'; import { getConfig } from './config'; @@ -36,6 +36,7 @@ function activateFull(context: vscode.ExtensionContext, workspaceRoot: string) { registerImportCommand(context); registerRuntimeCommands(context); registerAddDirectoryCommand(context, workspaceRoot); + registerPartialSyncCommand(context); context.subscriptions.push( vscode.debug.registerDebugConfigurationProvider( 'java', new BazelDebugConfigurationProvider()