From 52c33a1b151c21c23582472491f3864337ac4f2d Mon Sep 17 00:00:00 2001 From: runchen0919 Date: Mon, 18 May 2026 15:29:00 +0800 Subject: [PATCH 1/9] fix(cache): eagerly create redb tables on open to prevent TableDoesNotExist on reload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BazelCache::open() created the database file but never created tables. Read transactions (used by load_all_classpaths on startup) cannot create tables in redb, causing "Table 'classpath' does not exist" errors on every VS Code Reload Window — forcing a full 18s re-import instead of using cached data. - Add ensure_tables_exist() called after every Database::create path - Add TableDoesNotExist defense-in-depth to all read-only methods Co-Authored-By: Claude Opus 4.6 --- .../crates/bazel-cache/src/redb_store.rs | 71 ++++++++++++++++--- 1 file changed, 60 insertions(+), 11 deletions(-) diff --git a/bazel-jdt-bridge/crates/bazel-cache/src/redb_store.rs b/bazel-jdt-bridge/crates/bazel-cache/src/redb_store.rs index 0f9e93a..9664c84 100644 --- a/bazel-jdt-bridge/crates/bazel-cache/src/redb_store.rs +++ b/bazel-jdt-bridge/crates/bazel-cache/src/redb_store.rs @@ -84,19 +84,37 @@ fn is_lock_error(err: &redb::DatabaseError) -> bool { } impl BazelCache { + /// Eagerly create tables so that subsequent read transactions can open them. + /// Write-transaction `open_table` creates the table if it doesn't exist; + /// read-transaction `open_table` returns `TableDoesNotExist` if it was never created. + fn ensure_tables_exist(db: &Database) -> Result<(), CacheError> { + let txn = db.begin_write()?; + { + txn.open_table(CLASSPATH_TABLE)?; + txn.open_table(BUILD_HASH_TABLE)?; + } + txn.commit()?; + Ok(()) + } + /// Open or create the cache database. /// /// Uses a three-stage strategy for lock conflicts: /// 1. Try to open normally /// 2. If locked, sleep 500ms and retry (covers transient cross-process locks) /// 3. If still locked, delete the .redb file and create a fresh database + /// + /// After opening, eagerly creates tables so read transactions always succeed. pub fn open(cache_dir: &Path) -> Result { std::fs::create_dir_all(cache_dir)?; let db_path = cache_dir.join("bazel-jdt-cache.redb"); // Stage 1: first attempt match Database::create(&db_path) { - Ok(db) => return Ok(Self { db }), + Ok(db) => { + Self::ensure_tables_exist(&db)?; + return Ok(Self { db }); + } Err(ref e) if is_lock_error(e) => { log::warn!( "Cache database locked, retrying in 500ms: {}", @@ -109,7 +127,10 @@ impl BazelCache { // Stage 2: retry after 500ms std::thread::sleep(std::time::Duration::from_millis(500)); match Database::create(&db_path) { - Ok(db) => return Ok(Self { db }), + Ok(db) => { + Self::ensure_tables_exist(&db)?; + return Ok(Self { db }); + } Err(ref e) if is_lock_error(e) => { log::warn!( "Cache database still locked after retry, recreating: {}", @@ -122,13 +143,19 @@ impl BazelCache { // Stage 3: delete and recreate let _ = std::fs::remove_file(&db_path); let db = Database::create(&db_path)?; + Self::ensure_tables_exist(&db)?; Ok(Self { db }) } - /// Get a cached classpath for a target + /// Get a cached classpath for a target. + /// Returns None if the table doesn't exist yet (fresh database). pub fn get_classpath(&self, label: &str) -> Result, CacheError> { let txn = self.db.begin_read()?; - let table = txn.open_table(CLASSPATH_TABLE)?; + let table = match txn.open_table(CLASSPATH_TABLE) { + Ok(t) => t, + Err(redb::TableError::TableDoesNotExist(_)) => return Ok(None), + Err(e) => return Err(CacheError::TableError(e)), + }; if let Some(value) = table.get(label)? { Ok(Some(value.value().to_string())) } else { @@ -147,10 +174,15 @@ impl BazelCache { Ok(()) } - /// Get a cached BUILD file hash + /// Get a cached BUILD file hash. + /// Returns None if the table doesn't exist yet (fresh database). pub fn get_build_hash(&self, path: &str) -> Result, CacheError> { let txn = self.db.begin_read()?; - let table = txn.open_table(BUILD_HASH_TABLE)?; + let table = match txn.open_table(BUILD_HASH_TABLE) { + Ok(t) => t, + Err(redb::TableError::TableDoesNotExist(_)) => return Ok(None), + Err(e) => return Err(CacheError::TableError(e)), + }; if let Some(value) = table.get(path)? { Ok(Some(value.value().to_string())) } else { @@ -182,10 +214,15 @@ impl BazelCache { Ok(()) } - /// Load all cached classpaths (bulk load for IDE restart) + /// Load all cached classpaths (bulk load for IDE restart). + /// Returns an empty Vec if the table doesn't exist yet (fresh database). pub fn load_all_classpaths(&self) -> Result, CacheError> { let txn = self.db.begin_read()?; - let table = txn.open_table(CLASSPATH_TABLE)?; + let table = match txn.open_table(CLASSPATH_TABLE) { + Ok(t) => t, + Err(redb::TableError::TableDoesNotExist(_)) => return Ok(Vec::new()), + Err(e) => return Err(CacheError::TableError(e)), + }; let mut result = Vec::new(); for entry in table.iter()? { let (key, value) = entry?; @@ -221,7 +258,11 @@ impl BazelCache { fn find_corrupted_entries(&self) -> Result, CacheError> { let txn = self.db.begin_read()?; - let table = txn.open_table(CLASSPATH_TABLE)?; + let table = match txn.open_table(CLASSPATH_TABLE) { + Ok(t) => t, + Err(redb::TableError::TableDoesNotExist(_)) => return Ok(Vec::new()), + Err(e) => return Err(CacheError::TableError(e)), + }; let mut corrupted = Vec::new(); for entry in table.iter()? { match entry { @@ -243,13 +284,21 @@ impl BazelCache { pub fn count_classpath_entries(&self) -> Result { let txn = self.db.begin_read()?; - let table = txn.open_table(CLASSPATH_TABLE)?; + let table = match txn.open_table(CLASSPATH_TABLE) { + Ok(t) => t, + Err(redb::TableError::TableDoesNotExist(_)) => return Ok(0), + Err(e) => return Err(CacheError::TableError(e)), + }; Ok(table.len()? as usize) } pub fn list_build_hash_keys(&self) -> Result, CacheError> { let txn = self.db.begin_read()?; - let table = txn.open_table(BUILD_HASH_TABLE)?; + let table = match txn.open_table(BUILD_HASH_TABLE) { + Ok(t) => t, + Err(redb::TableError::TableDoesNotExist(_)) => return Ok(Vec::new()), + Err(e) => return Err(CacheError::TableError(e)), + }; let mut keys = Vec::new(); for entry in table.iter()? { match entry { From 811d3ffa08170982b5309fd9c8dc62fe6df9235a Mon Sep 17 00:00:00 2001 From: runchen0919 Date: Mon, 18 May 2026 15:38:24 +0800 Subject: [PATCH 2/9] fix(classpath): deduplicate missing JAR/source warnings to reduce log noise During reload, identical "Skipping non-existent JAR" warnings were emitted once per target sharing the same missing dependency (293 warnings for 8 unique JARs). Use a static ConcurrentHashMap-backed set to log each unique missing path only once. The set is cleared on classpath refresh so warnings resurface after reimport if the JAR is still missing. Co-Authored-By: Claude Opus 4.6 --- .../bazel/jdt/BazelClasspathContainer.java | 19 +++++++++++++++---- .../com/bazel/jdt/BazelClasspathManager.java | 1 + 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathContainer.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathContainer.java index cda9321..7216a00 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathContainer.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathContainer.java @@ -3,6 +3,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.ILog; @@ -19,6 +21,7 @@ public class BazelClasspathContainer implements IClasspathContainer { private static final ILog LOG = Platform.getLog(BazelClasspathContainer.class); + private static final Set WARNED_MISSING_PATHS = ConcurrentHashMap.newKeySet(); public static final IPath CONTAINER_PATH = Path.fromPortableString("com.bazel.jdt.BAZEL_CONTAINER"); private static final String DESCRIPTION = "Bazel Dependencies"; @@ -73,14 +76,18 @@ private IClasspathEntry parseEntry(String raw) { case "LIB": IPath jarPath = Path.fromPortableString(path); if (!jarPath.toFile().exists()) { - LOG.log(new Status(IStatus.WARNING, "com.bazel.jdt", - "Skipping non-existent JAR: " + path)); + if (WARNED_MISSING_PATHS.add(path)) { + LOG.log(new Status(IStatus.WARNING, "com.bazel.jdt", + "Skipping non-existent JAR: " + path)); + } return null; } IPath srcPath = sourcePath != null ? Path.fromPortableString(sourcePath) : null; if (srcPath != null && !srcPath.toFile().exists()) { - LOG.log(new Status(IStatus.WARNING, "com.bazel.jdt", - "Source attachment path does not exist, ignoring: " + sourcePath)); + if (WARNED_MISSING_PATHS.add(sourcePath)) { + LOG.log(new Status(IStatus.WARNING, "com.bazel.jdt", + "Source attachment path does not exist, ignoring: " + sourcePath)); + } srcPath = null; } IAccessRule[] accessRules = parseAccessRules(accessRulesStr); @@ -169,6 +176,10 @@ private static String extractPackageName(String targetLabel) { return LabelUtils.extractPackageName(targetLabel); } + static void resetWarnings() { + WARNED_MISSING_PATHS.clear(); + } + @Override public IClasspathEntry[] getClasspathEntries() { return entries; 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 1584bb4..d2a9428 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 @@ -115,6 +115,7 @@ rawEntries, getTestSourcePatterns(project), * Called by BazelCommandHandler for import/sync commands. */ public static void refreshClasspath() { + BazelClasspathContainer.resetWarnings(); try { org.eclipse.core.resources.IWorkspace workspace = org.eclipse.core.resources.ResourcesPlugin.getWorkspace(); From b82517ea069c9928dcab5236cac5f66cf1671a9e Mon Sep 17 00:00:00 2001 From: runchen0919 Date: Mon, 18 May 2026 15:46:28 +0800 Subject: [PATCH 3/9] fix(classpath): set empty container when cached entries reference stale artifacts During reload, recoverFromCache() would construct a container from stale cached entries (JARs cleaned from bazel-out/) and set it even when all entries were invalid. Now explicitly sets EMPTY container and logs an info message, preventing JDT from re-invoking initialize() and making the stale-cache scenario visible in logs. Co-Authored-By: Claude Opus 4.6 --- .../bazel/jdt/BazelClasspathContainerInitializer.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathContainerInitializer.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathContainerInitializer.java index 47113a3..9ebcbca 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathContainerInitializer.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathContainerInitializer.java @@ -87,6 +87,17 @@ private void recoverFromCache(IJavaProject project, BazelBridge bridge) { allEntries.toArray(new String[0]), Collections.emptyList(), bridge.getDependencyResolutionMode(), project.getProject().getName()); + if (container.getClasspathEntries().length == 0) { + LOG.info("All cached classpath entries for " + project.getProject().getName() + + " reference stale artifacts — setting empty container"); + JavaCore.setClasspathContainer( + BazelClasspathContainer.CONTAINER_PATH, + new IJavaProject[]{project}, + new IClasspathContainer[]{BazelClasspathContainer.EMPTY}, + null + ); + return; + } JavaCore.setClasspathContainer( BazelClasspathContainer.CONTAINER_PATH, new IJavaProject[]{project}, From 987244d1d3b2689db0d3bb148cf1de2fd9febcfb Mon Sep 17 00:00:00 2001 From: runchen0919 Date: Mon, 18 May 2026 18:03:12 +0800 Subject: [PATCH 4/9] feat: add fast reload path to skip Bazel discovery on window reload When cached state from a previous session exists (project index + workspace config + classpath cache files), importToWorkspace() now restores projects from file caches (~2-3s) instead of running the full 3-phase Bazel discovery pipeline (~16s). A background Job then runs the full pipeline asynchronously to pick up any BUILD changes. Key changes: - TargetProjectMapping: file-based workspace config persistence, project index (_index) for enumerating cached projects, shared getStateDir() helper - BazelClasspathContainerInitializer: always try file cache first via tryRecoverFromCache(), regardless of bridge initialization state - BazelProjectImporter: tryFastReload() fast path + background refresh scheduling Co-Authored-By: Claude Opus 4.6 --- .../BazelClasspathContainerInitializer.java | 56 +++---- .../com/bazel/jdt/BazelProjectImporter.java | 151 ++++++++++++++++++ .../com/bazel/jdt/TargetProjectMapping.java | 117 +++++++++++++- 3 files changed, 288 insertions(+), 36 deletions(-) diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathContainerInitializer.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathContainerInitializer.java index 9ebcbca..e15dd7e 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathContainerInitializer.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathContainerInitializer.java @@ -42,33 +42,31 @@ public void initialize(IPath containerPath, IJavaProject project) throws CoreExc private void doInitialize(IJavaProject project) throws CoreException { BazelBridge bridge = BazelBridge.getInstance(); - if (!bridge.isInitialized()) { - recoverFromCache(project, bridge); + if (tryRecoverFromCache(project, bridge)) { return; } - List targetLabels = TargetProjectMapping.readTargets(project.getProject()); - if (targetLabels.isEmpty()) { - LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", - "No persisted target labels for project '" + project.getProject().getName() - + "' - setting empty container (importer will configure)")); - JavaCore.setClasspathContainer( - BazelClasspathContainer.CONTAINER_PATH, - new IJavaProject[]{project}, - new IClasspathContainer[]{BazelClasspathContainer.EMPTY}, - null - ); - return; - } else { - BazelClasspathManager.setMergedClasspathContainer(project.getProject()); + if (bridge.isInitialized()) { + List targetLabels = TargetProjectMapping.readTargets(project.getProject()); + if (!targetLabels.isEmpty()) { + BazelClasspathManager.setMergedClasspathContainer(project.getProject()); + return; + } } + LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", + "No persisted target labels for project '" + project.getProject().getName() + + "' - setting empty container (importer will configure)")); + JavaCore.setClasspathContainer( + BazelClasspathContainer.CONTAINER_PATH, + new IJavaProject[]{project}, + new IClasspathContainer[]{BazelClasspathContainer.EMPTY}, + null + ); } - private void recoverFromCache(IJavaProject project, BazelBridge bridge) { + private boolean tryRecoverFromCache(IJavaProject project, BazelBridge bridge) { List targetLabels = TargetProjectMapping.readTargets(project.getProject()); if (targetLabels.isEmpty()) { - LOG.info("No persisted targets for " + project.getProject().getName() - + " — skipping container initialization until import runs"); - return; + return false; } java.util.ArrayList allEntries = new java.util.ArrayList<>(); for (String label : targetLabels) { @@ -78,9 +76,7 @@ private void recoverFromCache(IJavaProject project, BazelBridge bridge) { } } if (allEntries.isEmpty()) { - LOG.info("No cached classpath for " + project.getProject().getName() - + " — skipping until import provides entries"); - return; + return false; } try { BazelClasspathContainer container = new BazelClasspathContainer( @@ -89,14 +85,8 @@ private void recoverFromCache(IJavaProject project, BazelBridge bridge) { project.getProject().getName()); if (container.getClasspathEntries().length == 0) { LOG.info("All cached classpath entries for " + project.getProject().getName() - + " reference stale artifacts — setting empty container"); - JavaCore.setClasspathContainer( - BazelClasspathContainer.CONTAINER_PATH, - new IJavaProject[]{project}, - new IClasspathContainer[]{BazelClasspathContainer.EMPTY}, - null - ); - return; + + " reference stale artifacts — skipping cache recovery"); + return false; } JavaCore.setClasspathContainer( BazelClasspathContainer.CONTAINER_PATH, @@ -104,9 +94,13 @@ private void recoverFromCache(IJavaProject project, BazelBridge bridge) { new IClasspathContainer[]{container}, null ); + LOG.info("Recovered classpath from file cache for " + project.getProject().getName() + + " (" + container.getClasspathEntries().length + " entries)"); + return true; } catch (Exception e) { LOG.warn("Failed to apply cached classpath for " + project.getProject().getName() + ": " + e.getMessage()); + return false; } } diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java index cdc711e..050c34e 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java @@ -36,6 +36,10 @@ public void importToWorkspace(IProgressMonitor monitor) throws CoreException { return; } + if (tryFastReload(monitor)) { + return; + } + String workspacePath = rootFolder.getAbsolutePath(); String cacheDir = BazelCommandHandler.DEFAULT_CACHE_DIR; @@ -167,6 +171,7 @@ public void run(IProgressMonitor pm) throws CoreException { if (firstProject && project != null) { TargetProjectMapping.storeWorkspaceConfig(project, workspacePath, bazelPath, cacheDir); + TargetProjectMapping.storeWorkspaceConfigFile(workspacePath, bazelPath, cacheDir); firstProject = false; } } catch (Exception e) { @@ -222,6 +227,152 @@ public void run(IProgressMonitor pm) throws CoreException { } + private boolean tryFastReload(IProgressMonitor monitor) throws CoreException { + String[] config = TargetProjectMapping.readWorkspaceConfigFile(); + if (config == null) { + return false; + } + java.util.List index = TargetProjectMapping.readProjectIndex(); + if (index.isEmpty()) { + return false; + } + + String workspacePath = config[0]; + String bazelPath = config[1]; + String cacheDir = config[2]; + + if (rootFolder != null) { + BazelProjectView projectView = BazelProjectView.parse(rootFolder); + if (projectView != null && !projectView.getBazelBinary().isEmpty()) { + bazelPath = projectView.getBazelBinary(); + } + } + + long startTime = System.currentTimeMillis(); + LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", + "Fast reload: found " + index.size() + " cached projects, skipping Bazel discovery")); + + BazelBridge bridge = BazelBridge.getInstance(); + bridge.initialize(workspacePath, bazelPath, cacheDir); + + if (rootFolder != null) { + BazelProjectView projectView = BazelProjectView.parse(rootFolder); + if (projectView != null && !projectView.getDirectories().isEmpty()) { + String[] watchDirs = projectView.getDirectories().toArray(new String[0]); + bridge.updateWatchPaths(watchDirs); + } + } + + final String wsPath = workspacePath; + final String bzPath = bazelPath; + final String cDir = cacheDir; + ResourcesPlugin.getWorkspace().run(new IWorkspaceRunnable() { + @Override + public void run(IProgressMonitor pm) throws CoreException { + boolean firstProject = true; + for (String[] entry : index) { + String projectName = entry[0]; + String targetLabel = entry[1]; + String packagePath = entry[2]; + try { + IProject project = BazelProjectCreator.createProjectForPackage( + wsPath, packagePath, targetLabel, pm, true); + if (firstProject && project != null) { + TargetProjectMapping.storeWorkspaceConfig(project, wsPath, bzPath, cDir); + firstProject = false; + } + } catch (Exception e) { + LOG.log(new Status(IStatus.ERROR, "com.bazel.jdt", + "Fast reload: failed to recreate project " + projectName, e)); + } + } + } + }, monitor); + + JobHelpers.waitUntilIndexesReady(); + + long elapsed = System.currentTimeMillis() - startTime; + LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", + "Fast reload complete: " + index.size() + " projects restored in " + elapsed + "ms. " + + "Scheduling background refresh for fresh data.")); + + scheduleBackgroundRefresh(); + return true; + } + + private void scheduleBackgroundRefresh() { + org.eclipse.core.runtime.jobs.Job job = new org.eclipse.core.runtime.jobs.Job("Bazel: Background sync") { + @Override + protected IStatus run(IProgressMonitor pm) { + try { + BazelBridge bridge = BazelBridge.getInstance(); + if (!bridge.isInitialized()) { + return Status.OK_STATUS; + } + + String[] scopePatterns = null; + String[] buildFlags = null; + if (rootFolder != null) { + BazelProjectView projectView = BazelProjectView.parse(rootFolder); + if (projectView != null && projectView.hasScope()) { + scopePatterns = projectView.getScopePatterns().toArray(new String[0]); + } + if (projectView != null && !projectView.getBuildFlags().isEmpty()) { + buildFlags = projectView.getBuildFlags().toArray(new String[0]); + } + } + + LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", + "Background sync: starting full pipeline")); + long totalStart = System.currentTimeMillis(); + + String[] targets = bridge.queryTargets(scopePatterns); + if (targets == null || targets.length == 0) { + LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", + "Background sync: no targets found")); + return Status.OK_STATUS; + } + + bridge.populateGraph(); + String[] finalTargets = bridge.runAspectBuild(targets, buildFlags); + + if (finalTargets != null && finalTargets.length > 0) { + String wsPath = rootFolder.getAbsolutePath(); + IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); + for (String targetLabel : finalTargets) { + String packagePath = extractPackageName(targetLabel); + String projectName = LabelUtils.toProjectName(packagePath); + if (!workspaceRoot.getProject(projectName).exists()) { + try { + BazelProjectCreator.createProjectForPackage( + wsPath, packagePath, targetLabel, pm, true); + LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", + "Background sync: created new project " + projectName)); + } catch (Exception e) { + LOG.log(new Status(IStatus.WARNING, "com.bazel.jdt", + "Background sync: failed to create project " + projectName, e)); + } + } + } + } + + BazelClasspathManager.refreshClasspath(); + + long totalElapsed = (System.currentTimeMillis() - totalStart) / 1000; + LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", + "Background sync complete in " + totalElapsed + "s")); + } catch (Exception e) { + LOG.log(new Status(IStatus.ERROR, "com.bazel.jdt", + "Background sync failed: " + e.getMessage(), e)); + } + return Status.OK_STATUS; + } + }; + job.setSystem(true); + job.setPriority(org.eclipse.core.runtime.jobs.Job.BUILD); + job.schedule(); + } + @Override public void reset() { // No-op: BazelBridge.initialize() in importToWorkspace() handles native handle diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/TargetProjectMapping.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/TargetProjectMapping.java index 9eb846d..10abc58 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/TargetProjectMapping.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/TargetProjectMapping.java @@ -46,10 +46,15 @@ private static QualifiedName propertyName() { return new QualifiedName(QUALIFIER, KEY); } - private static Path getTargetLabelsDir() throws IOException { + static Path getStateDir() throws IOException { Bundle bundle = Platform.getBundle("com.bazel.jdt"); Path stateDir = Platform.getStateLocation(bundle).toFile().toPath(); - Path labelsDir = stateDir.resolve("target-labels"); + Files.createDirectories(stateDir); + return stateDir; + } + + private static Path getTargetLabelsDir() throws IOException { + Path labelsDir = getStateDir().resolve("target-labels"); Files.createDirectories(labelsDir); return labelsDir; } @@ -64,6 +69,11 @@ public static void storeTargets(IProject project, List targetLabels) { String value = String.join("\n", targetLabels); Files.writeString(labelsFile, value); LOG.info("Stored target labels for project '" + project.getName() + "': " + targetLabels.size() + " labels"); + if (!targetLabels.isEmpty()) { + String firstLabel = targetLabels.get(0); + String packagePath = LabelUtils.extractPackageName(firstLabel); + updateProjectIndex(project.getName(), firstLabel, packagePath); + } } catch (IOException e) { LOG.error("Failed to store target labels for project '" + project.getName() + "'", e); } @@ -142,6 +152,7 @@ public static void clearTargets(IProject project) { } catch (IOException e) { LOG.error("Failed to clear target labels file for project '" + project.getName() + "'", e); } + removeFromProjectIndex(project.getName()); try { project.setPersistentProperty(propertyName(), null); } catch (CoreException e) { @@ -149,6 +160,45 @@ public static void clearTargets(IProject project) { } } + private static final String WORKSPACE_CONFIG_FILE = "_workspace_config"; + + public static void storeWorkspaceConfigFile(String workspacePath, String bazelPath, String cacheDir) { + try { + Path configFile = getStateDir().resolve(WORKSPACE_CONFIG_FILE); + String content = "workspacePath=" + workspacePath + "\n" + + "bazelPath=" + (bazelPath != null ? bazelPath : "bazel") + "\n" + + "cacheDir=" + (cacheDir != null ? cacheDir : BazelCommandHandler.DEFAULT_CACHE_DIR) + "\n"; + Files.writeString(configFile, content); + } catch (IOException e) { + LOG.error("Failed to store workspace config file", e); + } + } + + public static String[] readWorkspaceConfigFile() { + try { + Path configFile = getStateDir().resolve(WORKSPACE_CONFIG_FILE); + if (!Files.exists(configFile)) return null; + String content = Files.readString(configFile); + String ws = null, bp = null, cd = null; + for (String line : content.split("\n")) { + int eq = line.indexOf('='); + if (eq < 0) continue; + String key = line.substring(0, eq); + String val = line.substring(eq + 1); + switch (key) { + case "workspacePath": ws = val; break; + case "bazelPath": bp = val; break; + case "cacheDir": cd = val; break; + } + } + if (ws == null || ws.isEmpty()) return null; + return new String[]{ws, bp != null ? bp : "bazel", cd != null ? cd : BazelCommandHandler.DEFAULT_CACHE_DIR}; + } catch (IOException e) { + LOG.error("Failed to read workspace config file", e); + return null; + } + } + public static void storeWorkspaceConfig(IProject project, String workspacePath, String bazelPath, String cacheDir) { try { @@ -198,6 +248,65 @@ public static String[] readWorkspaceConfig(IProject project) { } } + private static final String INDEX_FILE = "_index"; + + public static void updateProjectIndex(String projectName, String targetLabel, String packagePath) { + try { + Path indexFile = getTargetLabelsDir().resolve(INDEX_FILE); + List lines = Files.exists(indexFile) + ? new ArrayList<>(Files.readAllLines(indexFile)) : new ArrayList<>(); + String prefix = projectName + "|"; + String newLine = projectName + "|" + targetLabel + "|" + packagePath; + boolean replaced = false; + for (int i = 0; i < lines.size(); i++) { + if (lines.get(i).startsWith(prefix)) { + lines.set(i, newLine); + replaced = true; + break; + } + } + if (!replaced) { + lines.add(newLine); + } + Files.writeString(indexFile, String.join("\n", lines) + "\n"); + } catch (IOException e) { + LOG.error("Failed to update project index for '" + projectName + "'", e); + } + } + + public static void removeFromProjectIndex(String projectName) { + try { + Path indexFile = getTargetLabelsDir().resolve(INDEX_FILE); + if (!Files.exists(indexFile)) return; + List lines = new ArrayList<>(Files.readAllLines(indexFile)); + String prefix = projectName + "|"; + lines.removeIf(line -> line.startsWith(prefix)); + Files.writeString(indexFile, String.join("\n", lines) + (lines.isEmpty() ? "" : "\n")); + } catch (IOException e) { + LOG.error("Failed to remove from project index for '" + projectName + "'", e); + } + } + + public static List readProjectIndex() { + try { + Path indexFile = getTargetLabelsDir().resolve(INDEX_FILE); + if (!Files.exists(indexFile)) return Collections.emptyList(); + List result = new ArrayList<>(); + for (String line : Files.readAllLines(indexFile)) { + String trimmed = line.trim(); + if (trimmed.isEmpty()) continue; + String[] parts = trimmed.split("\\|", 3); + if (parts.length == 3) { + result.add(parts); + } + } + return result; + } catch (IOException e) { + LOG.error("Failed to read project index", e); + return Collections.emptyList(); + } + } + private static String sanitizeLabel(String label) { try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); @@ -209,9 +318,7 @@ private static String sanitizeLabel(String label) { } private static Path getCacheDir() throws IOException { - Bundle bundle = Platform.getBundle("com.bazel.jdt"); - Path stateDir = Platform.getStateLocation(bundle).toFile().toPath(); - Path cacheDir = stateDir.resolve("classpath-cache"); + Path cacheDir = getStateDir().resolve("classpath-cache"); Files.createDirectories(cacheDir); return cacheDir; } From f4b00c0aa1ba7c5cf58254f8446e1fcb8000d715 Mon Sep 17 00:00:00 2001 From: runchen0919 Date: Mon, 18 May 2026 23:23:12 +0800 Subject: [PATCH 5/9] feat: skip redundant classpath updates and lower background sync priority MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compare computed classpath entries against file cache before calling JavaCore.setClasspathContainer() — skip the update when entries are identical, avoiding unnecessary JDT.LS workspace rebuilds. Lower background sync job priority from BUILD to DECORATE so language features (completion, go-to-definition) are never preempted. Co-Authored-By: Claude Opus 4.6 --- .../main/java/com/bazel/jdt/BazelClasspathManager.java | 8 ++++++++ .../src/main/java/com/bazel/jdt/BazelProjectImporter.java | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) 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 d2a9428..7d72340 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 @@ -10,6 +10,7 @@ import org.eclipse.jdt.core.JavaCore; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; @@ -45,6 +46,13 @@ public static void setMergedClasspathContainer(IProject project) { String[] labels = targetLabels.toArray(new String[0]); String[] rawEntries = bridge.computeClasspathMerged(labels); + String[] cachedEntries = TargetProjectMapping.readCachedClasspath(project, targetLabels.get(0)); + if (cachedEntries != null && Arrays.equals(rawEntries, cachedEntries)) { + LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", + "Classpath unchanged for project " + project.getName() + ", skipping container update")); + return; + } + BazelClasspathContainer container = new BazelClasspathContainer( rawEntries, getTestSourcePatterns(project), bridge.getDependencyResolutionMode(), project.getName()); diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java index 050c34e..5fbc1ab 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java @@ -369,7 +369,7 @@ protected IStatus run(IProgressMonitor pm) { } }; job.setSystem(true); - job.setPriority(org.eclipse.core.runtime.jobs.Job.BUILD); + job.setPriority(org.eclipse.core.runtime.jobs.Job.DECORATE); job.schedule(); } From 48c80ad185a74bc4ae17b17dc8ae04cba33f2965 Mon Sep 17 00:00:00 2001 From: runchen0919 Date: Tue, 19 May 2026 09:57:21 +0800 Subject: [PATCH 6/9] feat: store projects in .bazel-projects/ to survive Reload Window Move all Eclipse projects from default location (.metadata/) to a dedicated .bazel-projects// directory under the workspace root. Projects at this location pass JDT.LS's isContainedIn() check and survive cleanInvalidProjects() across Reload Window, eliminating the delete-recreate-refresh-reindex cycle. Key changes: - Always use custom location at .bazel-projects/ (no more default location) - Linked source folders work without overlap (project and source in different directory trees) - Standard src roots (src/main/java, src/test/java) use linked folders - Fast-skip for projects already at correct location (just appendTargets) - One-time migration for projects at old locations - Auto-manage .bazel-projects/ in .gitignore Co-Authored-By: Claude Opus 4.6 --- .../com/bazel/jdt/BazelProjectCreator.java | 63 +++++++++++++------ .../com/bazel/jdt/BazelProjectImporter.java | 40 +++++++++++- 2 files changed, 82 insertions(+), 21 deletions(-) diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectCreator.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectCreator.java index ca3881c..faf021e 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectCreator.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectCreator.java @@ -44,28 +44,35 @@ public static IProject createProjectForPackage( IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); IProject project = workspaceRoot.getProject(projectName); - String inferredSourceRoot = SourceRootUtils.inferSourceRoot(workspacePath, packagePath); - - if (project.exists() && inferredSourceRoot != null - && project.getDescription().getLocation() != null) { + File bazelProjectDir = new File(workspacePath, ".bazel-projects/" + projectName); + IPath expectedLocation = new Path(bazelProjectDir.getAbsolutePath()); + + if (project.exists()) { + IPath currentLocation = project.getDescription().getLocation(); + if (currentLocation != null && currentLocation.equals(expectedLocation)) { + TargetProjectMapping.appendTargets(project, Collections.singletonList(targetLabel)); + ensureNatures(project, monitor); + LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", + "Project '" + projectName + "' already at .bazel-projects/, skipping rebuild")); + return project; + } LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", - "Recreating project '" + projectName - + "' — stale custom location conflicts with linked source folder")); + "Migrating project '" + projectName + "' to .bazel-projects/ location")); project.delete(false, true, monitor); } + if (!bazelProjectDir.exists() && !bazelProjectDir.mkdirs()) { + LOG.log(new Status(IStatus.ERROR, "com.bazel.jdt", + "Failed to create .bazel-projects directory: " + bazelProjectDir.getAbsolutePath())); + return null; + } + if (!project.exists()) { org.eclipse.core.resources.IProjectDescription projDesc = project.getWorkspace().newProjectDescription(projectName); - if (inferredSourceRoot == null) { - File packageDir = new File(workspacePath, packagePath); - projDesc.setLocation(new Path(packageDir.getAbsolutePath())); - LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", - "Creating project '" + projectName + "' at " + packageDir.getAbsolutePath())); - } else { - LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", - "Creating project '" + projectName + "' with default location (source root: " + inferredSourceRoot + ")")); - } + projDesc.setLocation(expectedLocation); + LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", + "Creating project '" + projectName + "' at " + bazelProjectDir.getAbsolutePath())); project.create(projDesc, monitor); } if (!project.isOpen()) { @@ -75,6 +82,7 @@ public static IProject createProjectForPackage( TargetProjectMapping.appendTargets(project, Collections.singletonList(targetLabel)); ensureNatures(project, monitor); + String inferredSourceRoot = SourceRootUtils.inferSourceRoot(workspacePath, packagePath); configureClasspath(project, packagePath, workspacePath, targetLabel, inferredSourceRoot, monitor, deferContainerResolution); return project; @@ -117,7 +125,12 @@ private static void configureClasspath(IProject project, String packageName, for (String srcRoot : STANDARD_SRC_ROOTS) { java.io.File srcDir = new java.io.File(workspacePath, packageName + "/" + srcRoot); if (srcDir.isDirectory()) { - IPath sourcePath = new Path("/" + project.getName() + "/" + srcRoot); + String linkedName = SourceRootUtils.linkedFolderName(srcRoot); + org.eclipse.core.resources.IFolder linkedFolder = project.getFolder(linkedName); + if (!linkedFolder.exists()) { + linkedFolder.createLink(new Path(srcDir.getAbsolutePath()), 0, monitor); + } + IPath sourcePath = new Path("/" + project.getName() + "/" + linkedName); sourceEntries.add(JavaCore.newSourceEntry(sourcePath)); } } @@ -131,11 +144,11 @@ private static void configureClasspath(IProject project, String packageName, } catch (Exception e) { LOG.log(new Status(IStatus.WARNING, "com.bazel.jdt", "Failed to create linked source folder for " + packageName - + ", falling back to project root: " + e.getMessage())); - entries.add(JavaCore.newSourceEntry(new Path("/" + project.getName()))); + + ", falling back to linked package folder: " + e.getMessage())); + configureLinkedPackageFolder(project, workspacePath, packageName, entries, monitor); } } else { - entries.add(JavaCore.newSourceEntry(new Path("/" + project.getName()))); + configureLinkedPackageFolder(project, workspacePath, packageName, entries, monitor); } } else { entries.addAll(sourceEntries); @@ -153,6 +166,18 @@ private static void configureClasspath(IProject project, String packageName, javaProject.setOutputLocation(new Path("/" + project.getName() + "/bin"), monitor); } + private static void configureLinkedPackageFolder(IProject project, String workspacePath, + String packageName, List entries, + IProgressMonitor monitor) throws CoreException { + String linkedName = "_pkg"; + org.eclipse.core.resources.IFolder linkedFolder = project.getFolder(linkedName); + if (!linkedFolder.exists()) { + File packageDir = new File(workspacePath, packageName); + linkedFolder.createLink(new Path(packageDir.getAbsolutePath()), 0, monitor); + } + entries.add(JavaCore.newSourceEntry(new Path("/" + project.getName() + "/" + linkedName))); + } + private static void addJreContainerEntry(List entries) { try { Class javaRuntimeClass = Class.forName("org.eclipse.jdt.launching.JavaRuntime"); diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java index 5fbc1ab..38dafc3 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java @@ -59,6 +59,8 @@ public void importToWorkspace(IProgressMonitor monitor) throws CoreException { LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", "Importing Bazel workspace: " + workspacePath)); + ensureBazelProjectsGitignore(workspacePath); + if (projectView != null && !projectView.getDirectories().isEmpty()) { String[] watchDirs = projectView.getDirectories().toArray(new String[0]); bridge.updateWatchPaths(watchDirs); @@ -170,7 +172,9 @@ public void run(IProgressMonitor pm) throws CoreException { workspacePath, packagePath, targetLabel, pm, true); if (firstProject && project != null) { - TargetProjectMapping.storeWorkspaceConfig(project, workspacePath, bazelPath, cacheDir); + if (TargetProjectMapping.readWorkspaceConfig(project) == null) { + TargetProjectMapping.storeWorkspaceConfig(project, workspacePath, bazelPath, cacheDir); + } TargetProjectMapping.storeWorkspaceConfigFile(workspacePath, bazelPath, cacheDir); firstProject = false; } @@ -255,6 +259,8 @@ private boolean tryFastReload(IProgressMonitor monitor) throws CoreException { BazelBridge bridge = BazelBridge.getInstance(); bridge.initialize(workspacePath, bazelPath, cacheDir); + ensureBazelProjectsGitignore(workspacePath); + if (rootFolder != null) { BazelProjectView projectView = BazelProjectView.parse(rootFolder); if (projectView != null && !projectView.getDirectories().isEmpty()) { @@ -278,7 +284,9 @@ public void run(IProgressMonitor pm) throws CoreException { IProject project = BazelProjectCreator.createProjectForPackage( wsPath, packagePath, targetLabel, pm, true); if (firstProject && project != null) { - TargetProjectMapping.storeWorkspaceConfig(project, wsPath, bzPath, cDir); + if (TargetProjectMapping.readWorkspaceConfig(project) == null) { + TargetProjectMapping.storeWorkspaceConfig(project, wsPath, bzPath, cDir); + } firstProject = false; } } catch (Exception e) { @@ -373,6 +381,34 @@ protected IStatus run(IProgressMonitor pm) { job.schedule(); } + private static void ensureBazelProjectsGitignore(String workspacePath) { + File gitignore = new File(workspacePath, ".gitignore"); + String entry = ".bazel-projects/"; + try { + if (gitignore.exists()) { + String content = new String(java.nio.file.Files.readAllBytes(gitignore.toPath()), + java.nio.charset.StandardCharsets.UTF_8); + for (String line : content.split("\n")) { + if (line.trim().equals(entry)) { + return; + } + } + String separator = content.endsWith("\n") ? "" : "\n"; + java.nio.file.Files.write(gitignore.toPath(), + (separator + entry + "\n").getBytes(java.nio.charset.StandardCharsets.UTF_8), + java.nio.file.StandardOpenOption.APPEND); + } else { + java.nio.file.Files.write(gitignore.toPath(), + (entry + "\n").getBytes(java.nio.charset.StandardCharsets.UTF_8)); + } + LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", + "Added .bazel-projects/ to .gitignore")); + } catch (Exception e) { + LOG.log(new Status(IStatus.WARNING, "com.bazel.jdt", + "Failed to update .gitignore: " + e.getMessage())); + } + } + @Override public void reset() { // No-op: BazelBridge.initialize() in importToWorkspace() handles native handle From 77d98a219e7c8e0505af5b5d18d1fb69da4ebce4 Mon Sep 17 00:00:00 2001 From: runchen0919 Date: Tue, 19 May 2026 15:29:35 +0800 Subject: [PATCH 7/9] feat: batch container prefill and remove background sync on reload Replace per-project container initialization with a single batch JavaCore.setClasspathContainer() call from file cache, reducing 28 workspace sync events to 1. Remove the background sync pipeline (queryTargets + populateGraph + runAspectBuild + refreshClasspath) from fast reload since it does 15-40s of work with zero benefit when BUILD files are unchanged. Incremental sync via BazelBuildSupport handles BUILD file changes on-demand. Also optimize the fast-skip path: remove redundant ensureNatures() call (natures persist in .project file) and add idempotent check for appendTargets() to avoid unnecessary file I/O. Co-Authored-By: Claude Opus 4.6 --- .../com/bazel/jdt/BazelProjectCreator.java | 6 +- .../com/bazel/jdt/BazelProjectImporter.java | 137 +++++++++--------- 2 files changed, 74 insertions(+), 69 deletions(-) diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectCreator.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectCreator.java index faf021e..d53409e 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectCreator.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectCreator.java @@ -50,8 +50,10 @@ public static IProject createProjectForPackage( if (project.exists()) { IPath currentLocation = project.getDescription().getLocation(); if (currentLocation != null && currentLocation.equals(expectedLocation)) { - TargetProjectMapping.appendTargets(project, Collections.singletonList(targetLabel)); - ensureNatures(project, monitor); + List existingLabels = TargetProjectMapping.readTargets(project); + if (!existingLabels.contains(targetLabel)) { + TargetProjectMapping.appendTargets(project, Collections.singletonList(targetLabel)); + } LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", "Project '" + projectName + "' already at .bazel-projects/, skipping rebuild")); return project; diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java index 38dafc3..24c92f0 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java @@ -1,6 +1,9 @@ package com.bazel.jdt; import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IWorkspaceRoot; @@ -12,6 +15,9 @@ import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; +import org.eclipse.jdt.core.IClasspathContainer; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.ls.core.internal.AbstractProjectImporter; import org.eclipse.jdt.ls.core.internal.JobHelpers; @@ -294,6 +300,8 @@ public void run(IProgressMonitor pm) throws CoreException { "Fast reload: failed to recreate project " + projectName, e)); } } + + batchPrefillContainers(bridge); } }, monitor); @@ -301,84 +309,79 @@ public void run(IProgressMonitor pm) throws CoreException { long elapsed = System.currentTimeMillis() - startTime; LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", - "Fast reload complete: " + index.size() + " projects restored in " + elapsed + "ms. " - + "Scheduling background refresh for fresh data.")); + "Fast reload complete: " + index.size() + " projects restored in " + elapsed + "ms")); - scheduleBackgroundRefresh(); return true; } - private void scheduleBackgroundRefresh() { - org.eclipse.core.runtime.jobs.Job job = new org.eclipse.core.runtime.jobs.Job("Bazel: Background sync") { - @Override - protected IStatus run(IProgressMonitor pm) { - try { - BazelBridge bridge = BazelBridge.getInstance(); - if (!bridge.isInitialized()) { - return Status.OK_STATUS; - } - - String[] scopePatterns = null; - String[] buildFlags = null; - if (rootFolder != null) { - BazelProjectView projectView = BazelProjectView.parse(rootFolder); - if (projectView != null && projectView.hasScope()) { - scopePatterns = projectView.getScopePatterns().toArray(new String[0]); - } - if (projectView != null && !projectView.getBuildFlags().isEmpty()) { - buildFlags = projectView.getBuildFlags().toArray(new String[0]); - } - } + private static void batchPrefillContainers(BazelBridge bridge) { + IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); + IProject[] allProjects = workspaceRoot.getProjects(); - LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", - "Background sync: starting full pipeline")); - long totalStart = System.currentTimeMillis(); + List javaProjects = new ArrayList<>(); + List containers = new ArrayList<>(); + int skipped = 0; - String[] targets = bridge.queryTargets(scopePatterns); - if (targets == null || targets.length == 0) { - LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", - "Background sync: no targets found")); - return Status.OK_STATUS; - } + for (IProject project : allProjects) { + if (!project.isOpen()) continue; + try { + if (!project.hasNature(BazelNature.NATURE_ID)) continue; + } catch (CoreException e) { + continue; + } - bridge.populateGraph(); - String[] finalTargets = bridge.runAspectBuild(targets, buildFlags); - - if (finalTargets != null && finalTargets.length > 0) { - String wsPath = rootFolder.getAbsolutePath(); - IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); - for (String targetLabel : finalTargets) { - String packagePath = extractPackageName(targetLabel); - String projectName = LabelUtils.toProjectName(packagePath); - if (!workspaceRoot.getProject(projectName).exists()) { - try { - BazelProjectCreator.createProjectForPackage( - wsPath, packagePath, targetLabel, pm, true); - LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", - "Background sync: created new project " + projectName)); - } catch (Exception e) { - LOG.log(new Status(IStatus.WARNING, "com.bazel.jdt", - "Background sync: failed to create project " + projectName, e)); - } - } - } - } + List targetLabels = TargetProjectMapping.readTargets(project); + if (targetLabels.isEmpty()) { + skipped++; + continue; + } - BazelClasspathManager.refreshClasspath(); + List allEntries = new ArrayList<>(); + for (String label : targetLabels) { + String[] cached = TargetProjectMapping.readCachedClasspath(project, label); + if (cached != null) { + Collections.addAll(allEntries, cached); + } + } + if (allEntries.isEmpty()) { + skipped++; + continue; + } - long totalElapsed = (System.currentTimeMillis() - totalStart) / 1000; - LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", - "Background sync complete in " + totalElapsed + "s")); - } catch (Exception e) { - LOG.log(new Status(IStatus.ERROR, "com.bazel.jdt", - "Background sync failed: " + e.getMessage(), e)); + try { + BazelClasspathContainer container = new BazelClasspathContainer( + allEntries.toArray(new String[0]), Collections.emptyList(), + bridge.getDependencyResolutionMode(), + project.getName()); + if (container.getClasspathEntries().length == 0) { + skipped++; + continue; } - return Status.OK_STATUS; + javaProjects.add(JavaCore.create(project)); + containers.add(container); + } catch (Exception e) { + LOG.log(new Status(IStatus.WARNING, "com.bazel.jdt", + "Failed to build cached container for " + project.getName() + ": " + e.getMessage())); + skipped++; } - }; - job.setSystem(true); - job.setPriority(org.eclipse.core.runtime.jobs.Job.DECORATE); - job.schedule(); + } + + if (!javaProjects.isEmpty()) { + try { + JavaCore.setClasspathContainer( + BazelClasspathContainer.CONTAINER_PATH, + javaProjects.toArray(new IJavaProject[0]), + containers.toArray(new IClasspathContainer[0]), + null + ); + LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", + "Batch pre-filled " + javaProjects.size() + " classpath containers from cache" + + (skipped > 0 ? " (" + skipped + " skipped)" : ""))); + } catch (Exception e) { + LOG.log(new Status(IStatus.ERROR, "com.bazel.jdt", + "Failed to batch pre-fill containers: " + e.getMessage(), e)); + } + } } private static void ensureBazelProjectsGitignore(String workspacePath) { From 9d46e1cfe1d0431fabc049bd6b36465d9ad3cbca Mon Sep 17 00:00:00 2001 From: runchen0919 Date: Tue, 19 May 2026 17:50:05 +0800 Subject: [PATCH 8/9] feat: batch classpath init, suppress initializer during import, and remove Java builder Optimize JDT.LS initialization to eliminate post-Ready "Synchronizing project" and "Searching..." delays: - Add IMPORT_IN_PROGRESS flag to skip per-project container initialization - Add batchSetClasspathContainers() for single JavaCore.setClasspathContainer call - Add FILE_EXISTS_CACHE to avoid repeated filesystem checks during batch init - Remove org.eclipse.jdt.core.javabuilder from Bazel projects (redundant with Bazel build, and JDT code intelligence uses Java Model not builder output) - Refactor importToWorkspace() and tryFastReload() to use deferred+batch pattern Co-Authored-By: Claude Opus 4.6 --- .../bazel/jdt/BazelClasspathContainer.java | 10 +- .../BazelClasspathContainerInitializer.java | 18 ++ .../com/bazel/jdt/BazelClasspathManager.java | 91 +++++++ .../com/bazel/jdt/BazelProjectCreator.java | 19 ++ .../com/bazel/jdt/BazelProjectImporter.java | 240 +++++++----------- 5 files changed, 223 insertions(+), 155 deletions(-) diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathContainer.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathContainer.java index 7216a00..5d5c908 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathContainer.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathContainer.java @@ -22,6 +22,7 @@ public class BazelClasspathContainer implements IClasspathContainer { private static final ILog LOG = Platform.getLog(BazelClasspathContainer.class); private static final Set WARNED_MISSING_PATHS = ConcurrentHashMap.newKeySet(); + private static final java.util.concurrent.ConcurrentHashMap FILE_EXISTS_CACHE = new java.util.concurrent.ConcurrentHashMap<>(); public static final IPath CONTAINER_PATH = Path.fromPortableString("com.bazel.jdt.BAZEL_CONTAINER"); private static final String DESCRIPTION = "Bazel Dependencies"; @@ -75,7 +76,7 @@ private IClasspathEntry parseEntry(String raw) { switch (type) { case "LIB": IPath jarPath = Path.fromPortableString(path); - if (!jarPath.toFile().exists()) { + if (!fileExists(path, jarPath)) { if (WARNED_MISSING_PATHS.add(path)) { LOG.log(new Status(IStatus.WARNING, "com.bazel.jdt", "Skipping non-existent JAR: " + path)); @@ -83,7 +84,7 @@ private IClasspathEntry parseEntry(String raw) { return null; } IPath srcPath = sourcePath != null ? Path.fromPortableString(sourcePath) : null; - if (srcPath != null && !srcPath.toFile().exists()) { + if (srcPath != null && !fileExists(sourcePath, srcPath)) { if (WARNED_MISSING_PATHS.add(sourcePath)) { LOG.log(new Status(IStatus.WARNING, "com.bazel.jdt", "Source attachment path does not exist, ignoring: " + sourcePath)); @@ -176,8 +177,13 @@ private static String extractPackageName(String targetLabel) { return LabelUtils.extractPackageName(targetLabel); } + private static boolean fileExists(String pathKey, IPath ipath) { + return FILE_EXISTS_CACHE.computeIfAbsent(pathKey, k -> ipath.toFile().exists()); + } + static void resetWarnings() { WARNED_MISSING_PATHS.clear(); + FILE_EXISTS_CACHE.clear(); } @Override diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathContainerInitializer.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathContainerInitializer.java index e15dd7e..04b962c 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathContainerInitializer.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathContainerInitializer.java @@ -21,6 +21,11 @@ public class BazelClasspathContainerInitializer extends ClasspathContainerInitia private static final ILog LOG = Platform.getLog(BazelClasspathContainerInitializer.class); private static final Set INITIALIZING = ConcurrentHashMap.newKeySet(); + private static volatile boolean IMPORT_IN_PROGRESS = false; + + public static void setImportInProgress(boolean inProgress) { + IMPORT_IN_PROGRESS = inProgress; + } @Override public void initialize(IPath containerPath, IJavaProject project) throws CoreException { @@ -28,6 +33,19 @@ public void initialize(IPath containerPath, IJavaProject project) throws CoreExc return; } String projectName = project.getProject().getName(); + + if (IMPORT_IN_PROGRESS) { + LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", + "Skipping container resolve during import for project " + projectName)); + JavaCore.setClasspathContainer( + BazelClasspathContainer.CONTAINER_PATH, + new IJavaProject[]{project}, + new IClasspathContainer[]{BazelClasspathContainer.EMPTY}, + null + ); + return; + } + if (!INITIALIZING.add(projectName)) { LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", "Skipping recursive initialize for project " + projectName)); 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 7d72340..fd0bf67 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 @@ -171,6 +171,97 @@ public static void refreshClasspath() { } } + /** + * Batch-set classpath containers for all Bazel projects in a single call. + * When fromCache is true, reads cached classpath entries from file cache. + * When fromCache is false, computes classpath via JNI and persists to cache. + */ + public static void batchSetClasspathContainers(boolean fromCache) { + BazelClasspathContainer.resetWarnings(); + try { + org.eclipse.core.resources.IWorkspace workspace = + org.eclipse.core.resources.ResourcesPlugin.getWorkspace(); + IProject[] projects = workspace.getRoot().getProjects(); + BazelBridge bridge = BazelBridge.getInstance(); + + List javaProjects = new ArrayList<>(); + List containers = new ArrayList<>(); + int skipped = 0; + long startTime = System.currentTimeMillis(); + + for (IProject project : projects) { + if (!project.isOpen()) continue; + try { + if (!project.hasNature(BazelNature.NATURE_ID)) continue; + } catch (CoreException e) { + continue; + } + + List targetLabels = TargetProjectMapping.readTargets(project); + if (targetLabels.isEmpty()) { + skipped++; + continue; + } + + String[] rawEntries; + if (fromCache) { + List allEntries = new ArrayList<>(); + for (String label : targetLabels) { + String[] cached = TargetProjectMapping.readCachedClasspath(project, label); + if (cached != null) { + java.util.Collections.addAll(allEntries, cached); + } + } + if (allEntries.isEmpty()) { + skipped++; + continue; + } + rawEntries = allEntries.toArray(new String[0]); + } else { + String[] labels = targetLabels.toArray(new String[0]); + rawEntries = bridge.computeClasspathMerged(labels); + TargetProjectMapping.storeCachedClasspath(project, targetLabels.get(0), rawEntries); + } + + try { + BazelClasspathContainer container = new BazelClasspathContainer( + rawEntries, getTestSourcePatterns(project), + bridge.getDependencyResolutionMode(), + project.getName()); + if (container.getClasspathEntries().length == 0) { + skipped++; + continue; + } + javaProjects.add(JavaCore.create(project)); + containers.add(container); + } catch (Exception e) { + LOG.log(new Status(IStatus.WARNING, "com.bazel.jdt", + "Failed to build container for " + project.getName() + ": " + e.getMessage())); + skipped++; + } + } + + if (!javaProjects.isEmpty()) { + JavaCore.setClasspathContainer( + BazelClasspathContainer.CONTAINER_PATH, + javaProjects.toArray(new org.eclipse.jdt.core.IJavaProject[0]), + containers.toArray(new IClasspathContainer[0]), + null + ); + } + + long elapsed = System.currentTimeMillis() - startTime; + LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", + "Batch set " + javaProjects.size() + " classpath containers" + + (fromCache ? " from cache" : " via JNI") + + " in " + elapsed + "ms" + + (skipped > 0 ? " (" + skipped + " skipped)" : ""))); + } catch (Exception e) { + LOG.log(new Status(IStatus.ERROR, "com.bazel.jdt", + "Failed to batch set classpath containers", e)); + } + } + /** * Refresh classpath for projects affected by changed BUILD files. * Called by BazelBuildSupport when file changes are detected. diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectCreator.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectCreator.java index d53409e..1f263b3 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectCreator.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectCreator.java @@ -5,6 +5,8 @@ import java.util.Collections; import java.util.List; +import org.eclipse.core.resources.ICommand; + import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.resources.ResourcesPlugin; @@ -115,6 +117,23 @@ private static void ensureNatures(IProject project, IProgressMonitor monitor) th desc.setNatureIds(newNatureIds); project.setDescription(desc, monitor); } + + removeJavaBuilder(project, monitor); + } + + private static void removeJavaBuilder(IProject project, IProgressMonitor monitor) throws CoreException { + org.eclipse.core.resources.IProjectDescription desc = project.getDescription(); + ICommand[] buildSpec = desc.getBuildSpec(); + List filtered = new ArrayList<>(); + for (ICommand cmd : buildSpec) { + if (!"org.eclipse.jdt.core.javabuilder".equals(cmd.getBuilderName())) { + filtered.add(cmd); + } + } + if (filtered.size() < buildSpec.length) { + desc.setBuildSpec(filtered.toArray(new ICommand[0])); + project.setDescription(desc, monitor); + } } private static void configureClasspath(IProject project, String packageName, diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java index 24c92f0..0769586 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java @@ -1,9 +1,6 @@ package com.bazel.jdt; import java.io.File; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IWorkspaceRoot; @@ -15,9 +12,6 @@ import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; -import org.eclipse.jdt.core.IClasspathContainer; -import org.eclipse.jdt.core.IJavaProject; -import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.ls.core.internal.AbstractProjectImporter; import org.eclipse.jdt.ls.core.internal.JobHelpers; @@ -165,75 +159,80 @@ public void importToWorkspace(IProgressMonitor monitor) throws CoreException { if (finalTargets == null || finalTargets.length == 0) return; // Phase 1: Create all projects (deferred classpath resolution) - ResourcesPlugin.getWorkspace().run(new IWorkspaceRunnable() { - @Override - public void run(IProgressMonitor pm) throws CoreException { - IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); - boolean firstProject = true; - - for (String targetLabel : finalTargets) { - try { - String packagePath = extractPackageName(targetLabel); - IProject project = BazelProjectCreator.createProjectForPackage( - workspacePath, packagePath, targetLabel, pm, true); - - if (firstProject && project != null) { - if (TargetProjectMapping.readWorkspaceConfig(project) == null) { - TargetProjectMapping.storeWorkspaceConfig(project, workspacePath, bazelPath, cacheDir); + BazelClasspathContainerInitializer.setImportInProgress(true); + try { + ResourcesPlugin.getWorkspace().run(new IWorkspaceRunnable() { + @Override + public void run(IProgressMonitor pm) throws CoreException { + IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); + boolean firstProject = true; + + for (String targetLabel : finalTargets) { + try { + String packagePath = extractPackageName(targetLabel); + IProject project = BazelProjectCreator.createProjectForPackage( + workspacePath, packagePath, targetLabel, pm, true); + + if (firstProject && project != null) { + if (TargetProjectMapping.readWorkspaceConfig(project) == null) { + TargetProjectMapping.storeWorkspaceConfig(project, workspacePath, bazelPath, cacheDir); + } + TargetProjectMapping.storeWorkspaceConfigFile(workspacePath, bazelPath, cacheDir); + firstProject = false; } - TargetProjectMapping.storeWorkspaceConfigFile(workspacePath, bazelPath, cacheDir); - firstProject = false; + } catch (Exception e) { + LOG.log(new Status(IStatus.ERROR, "com.bazel.jdt", + "Failed to import target: " + targetLabel, e)); } - } catch (Exception e) { - LOG.log(new Status(IStatus.ERROR, "com.bazel.jdt", - "Failed to import target: " + targetLabel, e)); } - } - String loadingMode = bridge.getDependencySourceLoadingMode(); - String[] depEntries = bridge.getTransitiveWorkspaceDeps(finalTargets); - bridge.setCachedDependencyPackages(depEntries); - - if ("full-project".equals(loadingMode) && depEntries != null && depEntries.length > 0) { - for (String entry : depEntries) { - try { - String[] parts = entry.split("\\|", 2); - String packagePath = parts[0]; - String firstLabel = parts.length > 1 && !parts[1].isEmpty() - ? parts[1].split(",")[0] - : null; - - String projName = LabelUtils.toProjectName(packagePath); - if (workspaceRoot.getProject(projName).exists()) { - continue; - } - if (firstLabel == null) { + String loadingMode = bridge.getDependencySourceLoadingMode(); + String[] depEntries = bridge.getTransitiveWorkspaceDeps(finalTargets); + bridge.setCachedDependencyPackages(depEntries); + + if ("full-project".equals(loadingMode) && depEntries != null && depEntries.length > 0) { + for (String entry : depEntries) { + try { + String[] parts = entry.split("\\|", 2); + String packagePath = parts[0]; + String firstLabel = parts.length > 1 && !parts[1].isEmpty() + ? parts[1].split(",")[0] + : null; + + String projName = LabelUtils.toProjectName(packagePath); + if (workspaceRoot.getProject(projName).exists()) { + continue; + } + if (firstLabel == null) { + LOG.log(new Status(IStatus.WARNING, "com.bazel.jdt", + "No target label for dependency package: " + packagePath)); + continue; + } + BazelProjectCreator.createProjectForPackage( + workspacePath, packagePath, firstLabel, pm, true); + LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", + "Auto-created project for dependency package: " + packagePath)); + } catch (Exception e) { LOG.log(new Status(IStatus.WARNING, "com.bazel.jdt", - "No target label for dependency package: " + packagePath)); - continue; + "Failed to auto-create project for dependency: " + entry, e)); } - BazelProjectCreator.createProjectForPackage( - workspacePath, packagePath, firstLabel, pm, true); - LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", - "Auto-created project for dependency package: " + packagePath)); - } catch (Exception e) { - LOG.log(new Status(IStatus.WARNING, "com.bazel.jdt", - "Failed to auto-create project for dependency: " + entry, e)); } } } - } - }, monitor); + }, monitor); + + // Batch-set all classpath containers before indexer runs + BazelClasspathManager.batchSetClasspathContainers(false); + } finally { + BazelClasspathContainerInitializer.setImportInProgress(false); + } - // Wait for JDT indexer to process all queued projects (including JDK types) + // Wait for JDT indexer — containers are already set, so indexer runs once with real data LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", - "Phase 1 complete (" + finalTargets.length + " targets). Waiting for JDT indexes to be ready...")); + "All projects created and classpath containers set. Waiting for JDT indexes to be ready...")); JobHelpers.waitUntilIndexesReady(); LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", - "JDT indexes ready. Starting Phase 2: classpath resolution")); - - // Phase 2: Resolve classpaths (indexer is ready, reconciliation will find JDK types) - BazelClasspathManager.refreshClasspath(); + "JDT indexes ready. Import complete.")); } @@ -278,32 +277,37 @@ private boolean tryFastReload(IProgressMonitor monitor) throws CoreException { final String wsPath = workspacePath; final String bzPath = bazelPath; final String cDir = cacheDir; - ResourcesPlugin.getWorkspace().run(new IWorkspaceRunnable() { - @Override - public void run(IProgressMonitor pm) throws CoreException { - boolean firstProject = true; - for (String[] entry : index) { - String projectName = entry[0]; - String targetLabel = entry[1]; - String packagePath = entry[2]; - try { - IProject project = BazelProjectCreator.createProjectForPackage( - wsPath, packagePath, targetLabel, pm, true); - if (firstProject && project != null) { - if (TargetProjectMapping.readWorkspaceConfig(project) == null) { - TargetProjectMapping.storeWorkspaceConfig(project, wsPath, bzPath, cDir); + BazelClasspathContainerInitializer.setImportInProgress(true); + try { + ResourcesPlugin.getWorkspace().run(new IWorkspaceRunnable() { + @Override + public void run(IProgressMonitor pm) throws CoreException { + boolean firstProject = true; + for (String[] entry : index) { + String projectName = entry[0]; + String targetLabel = entry[1]; + String packagePath = entry[2]; + try { + IProject project = BazelProjectCreator.createProjectForPackage( + wsPath, packagePath, targetLabel, pm, true); + if (firstProject && project != null) { + if (TargetProjectMapping.readWorkspaceConfig(project) == null) { + TargetProjectMapping.storeWorkspaceConfig(project, wsPath, bzPath, cDir); + } + firstProject = false; } - firstProject = false; + } catch (Exception e) { + LOG.log(new Status(IStatus.ERROR, "com.bazel.jdt", + "Fast reload: failed to recreate project " + projectName, e)); } - } catch (Exception e) { - LOG.log(new Status(IStatus.ERROR, "com.bazel.jdt", - "Fast reload: failed to recreate project " + projectName, e)); } } + }, monitor); - batchPrefillContainers(bridge); - } - }, monitor); + BazelClasspathManager.batchSetClasspathContainers(true); + } finally { + BazelClasspathContainerInitializer.setImportInProgress(false); + } JobHelpers.waitUntilIndexesReady(); @@ -314,76 +318,6 @@ public void run(IProgressMonitor pm) throws CoreException { return true; } - private static void batchPrefillContainers(BazelBridge bridge) { - IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); - IProject[] allProjects = workspaceRoot.getProjects(); - - List javaProjects = new ArrayList<>(); - List containers = new ArrayList<>(); - int skipped = 0; - - for (IProject project : allProjects) { - if (!project.isOpen()) continue; - try { - if (!project.hasNature(BazelNature.NATURE_ID)) continue; - } catch (CoreException e) { - continue; - } - - List targetLabels = TargetProjectMapping.readTargets(project); - if (targetLabels.isEmpty()) { - skipped++; - continue; - } - - List allEntries = new ArrayList<>(); - for (String label : targetLabels) { - String[] cached = TargetProjectMapping.readCachedClasspath(project, label); - if (cached != null) { - Collections.addAll(allEntries, cached); - } - } - if (allEntries.isEmpty()) { - skipped++; - continue; - } - - try { - BazelClasspathContainer container = new BazelClasspathContainer( - allEntries.toArray(new String[0]), Collections.emptyList(), - bridge.getDependencyResolutionMode(), - project.getName()); - if (container.getClasspathEntries().length == 0) { - skipped++; - continue; - } - javaProjects.add(JavaCore.create(project)); - containers.add(container); - } catch (Exception e) { - LOG.log(new Status(IStatus.WARNING, "com.bazel.jdt", - "Failed to build cached container for " + project.getName() + ": " + e.getMessage())); - skipped++; - } - } - - if (!javaProjects.isEmpty()) { - try { - JavaCore.setClasspathContainer( - BazelClasspathContainer.CONTAINER_PATH, - javaProjects.toArray(new IJavaProject[0]), - containers.toArray(new IClasspathContainer[0]), - null - ); - LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", - "Batch pre-filled " + javaProjects.size() + " classpath containers from cache" - + (skipped > 0 ? " (" + skipped + " skipped)" : ""))); - } catch (Exception e) { - LOG.log(new Status(IStatus.ERROR, "com.bazel.jdt", - "Failed to batch pre-fill containers: " + e.getMessage(), e)); - } - } - } - private static void ensureBazelProjectsGitignore(String workspacePath) { File gitignore = new File(workspacePath, ".gitignore"); String entry = ".bazel-projects/"; From af08970b12aafa9182cbe1b09bb15925a99fa98e Mon Sep 17 00:00:00 2001 From: runchen0919 Date: Tue, 19 May 2026 19:02:57 +0800 Subject: [PATCH 9/9] feat: eliminate post-import Searching by pre-setting JDT.LS state Pre-set JavaCore compiler options and resource filters before JDT.LS framework code runs, so its later setOptions()/configureFilters() calls become no-ops and don't trigger a second round of search indexing. Add waitForIndexesReady workspace command as a safety net so the TS extension confirms indexes are truly complete before showing ready. Co-Authored-By: Claude Opus 4.6 --- .../com/bazel/jdt/BazelCommandHandler.java | 14 +++++++++ .../com/bazel/jdt/BazelProjectCreator.java | 29 ++++++++++++++++++- .../com/bazel/jdt/BazelProjectImporter.java | 27 ++++++++++++++++- .../vscode-extension/src/statusBar.ts | 8 +++++ 4 files changed, 76 insertions(+), 2 deletions(-) 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 67b741b..ea01eab 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 @@ -9,6 +9,7 @@ import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; import org.eclipse.jdt.ls.core.internal.IDelegateCommandHandler; +import org.eclipse.jdt.ls.core.internal.JobHelpers; public class BazelCommandHandler implements IDelegateCommandHandler { private static final ILog LOG = Platform.getLog(BazelCommandHandler.class); @@ -31,6 +32,8 @@ public Object executeCommand(String commandId, List arguments, IProgress return handleGetDependencyPackages(arguments); case "bazel-jdt.createProjectForPackage": return handleCreateProjectForPackage(arguments, monitor); + case "bazel-jdt.waitForIndexesReady": + return handleWaitForIndexesReady(); default: return null; } @@ -155,6 +158,17 @@ private Object handleGetDependencyPackages(List arguments) { } } + private Object handleWaitForIndexesReady() { + try { + JobHelpers.waitUntilIndexesReady(); + return true; + } catch (Exception e) { + LOG.log(new Status(IStatus.WARNING, "com.bazel.jdt", + "waitForIndexesReady failed: " + e.getMessage())); + return false; + } + } + private Object handleCreateProjectForPackage(List arguments, IProgressMonitor monitor) { try { if (arguments.isEmpty() || !(arguments.get(0) instanceof String)) { diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectCreator.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectCreator.java index 1f263b3..9f4a62b 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectCreator.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectCreator.java @@ -5,9 +5,11 @@ import java.util.Collections; import java.util.List; +import org.eclipse.core.resources.FileInfoMatcherDescription; import org.eclipse.core.resources.ICommand; - import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceFilterDescription; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; @@ -85,6 +87,7 @@ public static IProject createProjectForPackage( TargetProjectMapping.appendTargets(project, Collections.singletonList(targetLabel)); + preCreateResourceFilter(project); ensureNatures(project, monitor); String inferredSourceRoot = SourceRootUtils.inferSourceRoot(workspacePath, packagePath); configureClasspath(project, packagePath, workspacePath, targetLabel, inferredSourceRoot, monitor, deferContainerResolution); @@ -121,6 +124,30 @@ private static void ensureNatures(IProject project, IProgressMonitor monitor) th removeJavaBuilder(project, monitor); } + private static void preCreateResourceFilter(IProject project) { + try { + for (IResourceFilterDescription f : project.getFilters()) { + FileInfoMatcherDescription matcher = f.getFileInfoMatcherDescription(); + if ("org.eclipse.core.resources.regexFilterMatcher".equals(matcher.getId()) + && matcher.getArguments() instanceof String args + && args.contains("__CREATED_BY_JAVA_LANGUAGE_SERVER__")) { + return; + } + } + int filterType = IResourceFilterDescription.EXCLUDE_ALL + | IResourceFilterDescription.INHERITABLE + | IResourceFilterDescription.FILES + | IResourceFilterDescription.FOLDERS; + project.createFilter(filterType, + new FileInfoMatcherDescription("org.eclipse.core.resources.regexFilterMatcher", + "__CREATED_BY_JAVA_LANGUAGE_SERVER__"), + IResource.NONE, null); + } catch (CoreException e) { + LOG.log(new Status(IStatus.WARNING, "com.bazel.jdt", + "Failed to pre-create resource filter: " + e.getMessage())); + } + } + private static void removeJavaBuilder(IProject project, IProgressMonitor monitor) throws CoreException { org.eclipse.core.resources.IProjectDescription desc = project.getDescription(); ICommand[] buildSpec = desc.getBuildSpec(); diff --git a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java index 0769586..3289ab1 100644 --- a/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java +++ b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelProjectImporter.java @@ -1,6 +1,7 @@ package com.bazel.jdt; import java.io.File; +import java.util.Hashtable; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IWorkspaceRoot; @@ -12,6 +13,11 @@ import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.internal.compiler.impl.CompilerOptions; +import org.eclipse.jdt.launching.AbstractVMInstall; +import org.eclipse.jdt.launching.IVMInstall; +import org.eclipse.jdt.launching.JavaRuntime; import org.eclipse.jdt.ls.core.internal.AbstractProjectImporter; import org.eclipse.jdt.ls.core.internal.JobHelpers; @@ -221,7 +227,7 @@ public void run(IProgressMonitor pm) throws CoreException { } }, monitor); - // Batch-set all classpath containers before indexer runs + preSetJavaCoreOptions(); BazelClasspathManager.batchSetClasspathContainers(false); } finally { BazelClasspathContainerInitializer.setImportInProgress(false); @@ -304,6 +310,7 @@ public void run(IProgressMonitor pm) throws CoreException { } }, monitor); + preSetJavaCoreOptions(); BazelClasspathManager.batchSetClasspathContainers(true); } finally { BazelClasspathContainerInitializer.setImportInProgress(false); @@ -358,6 +365,24 @@ public boolean isResolved(java.io.File rootFolder) { return true; } + private void preSetJavaCoreOptions() { + try { + Hashtable defaultOptions = JavaCore.getDefaultOptions(); + IVMInstall defaultVM = JavaRuntime.getDefaultVMInstall(); + if (defaultVM instanceof AbstractVMInstall jvm) { + long jdkLevel = CompilerOptions.versionToJdkLevel(jvm.getJavaVersion()); + String compliance = CompilerOptions.versionFromJdkLevel(jdkLevel); + JavaCore.setComplianceOptions(compliance, defaultOptions); + } else { + JavaCore.setComplianceOptions(JavaCore.VERSION_11, defaultOptions); + } + JavaCore.setOptions(defaultOptions); + } catch (Exception e) { + LOG.log(new Status(IStatus.WARNING, "com.bazel.jdt", + "Failed to pre-set JavaCore options: " + e.getMessage())); + } + } + private String extractPackageName(String targetLabel) { return LabelUtils.extractPackageName(targetLabel); } diff --git a/bazel-jdt-bridge/vscode-extension/src/statusBar.ts b/bazel-jdt-bridge/vscode-extension/src/statusBar.ts index 8302aea..16d9daf 100644 --- a/bazel-jdt-bridge/vscode-extension/src/statusBar.ts +++ b/bazel-jdt-bridge/vscode-extension/src/statusBar.ts @@ -24,6 +24,14 @@ export function createStatusBar(context: vscode.ExtensionContext): vscode.Status if (typeof state === 'number') { if (state === 0) { + statusBarItem.text = '$(sync~spin) Indexing...'; + statusBarItem.backgroundColor = undefined; + try { + await vscode.commands.executeCommand('java.execute.workspaceCommand', 'bazel-jdt.waitForIndexesReady'); + } catch { + // Command not available or failed, proceed to ready state + } + if (stopped) return; statusBarItem.text = 'Bazel ✓'; statusBarItem.backgroundColor = undefined; timer = setTimeout(poll, IDLE_INTERVAL_MS);