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 { 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..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 @@ -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,8 @@ 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"; @@ -72,15 +76,19 @@ private IClasspathEntry parseEntry(String raw) { switch (type) { 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 (!fileExists(path, jarPath)) { + 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 (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)); + } srcPath = null; } IAccessRule[] accessRules = parseAccessRules(accessRulesStr); @@ -169,6 +177,15 @@ 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 public IClasspathEntry[] getClasspathEntries() { return entries; 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..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)); @@ -42,33 +60,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,24 +94,31 @@ 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( 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 — skipping cache recovery"); + return false; + } JavaCore.setClasspathContainer( BazelClasspathContainer.CONTAINER_PATH, new IJavaProject[]{project}, 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/BazelClasspathManager.java b/bazel-jdt-bridge/java-bridge/src/main/java/com/bazel/jdt/BazelClasspathManager.java index 1584bb4..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 @@ -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()); @@ -115,6 +123,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(); @@ -162,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/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 ca3881c..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,7 +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; @@ -44,28 +48,37 @@ public static IProject createProjectForPackage( IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot(); IProject project = workspaceRoot.getProject(projectName); - String inferredSourceRoot = SourceRootUtils.inferSourceRoot(workspacePath, packagePath); + File bazelProjectDir = new File(workspacePath, ".bazel-projects/" + projectName); + IPath expectedLocation = new Path(bazelProjectDir.getAbsolutePath()); - if (project.exists() && inferredSourceRoot != null - && project.getDescription().getLocation() != null) { + if (project.exists()) { + IPath currentLocation = project.getDescription().getLocation(); + if (currentLocation != null && currentLocation.equals(expectedLocation)) { + 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; + } 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()) { @@ -74,7 +87,9 @@ 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); return project; @@ -105,6 +120,47 @@ private static void ensureNatures(IProject project, IProgressMonitor monitor) th desc.setNatureIds(newNatureIds); project.setDescription(desc, monitor); } + + 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(); + 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, @@ -117,7 +173,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 +192,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 +214,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 cdc711e..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; @@ -36,6 +42,10 @@ public void importToWorkspace(IProgressMonitor monitor) throws CoreException { return; } + if (tryFastReload(monitor)) { + return; + } + String workspacePath = rootFolder.getAbsolutePath(); String cacheDir = BazelCommandHandler.DEFAULT_CACHE_DIR; @@ -55,6 +65,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); @@ -153,73 +165,192 @@ 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) { - TargetProjectMapping.storeWorkspaceConfig(project, workspacePath, bazelPath, cacheDir); - firstProject = false; + 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; + } + } 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) { + 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", + "Failed to auto-create project for dependency: " + entry, e)); + } } - } catch (Exception e) { - LOG.log(new Status(IStatus.ERROR, "com.bazel.jdt", - "Failed to import target: " + targetLabel, e)); } } + }, monitor); + + preSetJavaCoreOptions(); + BazelClasspathManager.batchSetClasspathContainers(false); + } finally { + BazelClasspathContainerInitializer.setImportInProgress(false); + } + + // Wait for JDT indexer — containers are already set, so indexer runs once with real data + LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", + "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. Import complete.")); + + } + + 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); - String loadingMode = bridge.getDependencySourceLoadingMode(); - String[] depEntries = bridge.getTransitiveWorkspaceDeps(finalTargets); - bridge.setCachedDependencyPackages(depEntries); + ensureBazelProjectsGitignore(workspacePath); - if ("full-project".equals(loadingMode) && depEntries != null && depEntries.length > 0) { - for (String entry : depEntries) { + 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; + 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 { - 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; + 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; } - 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", - "Failed to auto-create project for dependency: " + entry, e)); + LOG.log(new Status(IStatus.ERROR, "com.bazel.jdt", + "Fast reload: failed to recreate project " + projectName, e)); } } } - } - }, monitor); + }, monitor); + + preSetJavaCoreOptions(); + BazelClasspathManager.batchSetClasspathContainers(true); + } finally { + BazelClasspathContainerInitializer.setImportInProgress(false); + } - // Wait for JDT indexer to process all queued projects (including JDK types) - LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", - "Phase 1 complete (" + finalTargets.length + " targets). Waiting for JDT indexes to be ready...")); JobHelpers.waitUntilIndexesReady(); + + long elapsed = System.currentTimeMillis() - startTime; LOG.log(new Status(IStatus.INFO, "com.bazel.jdt", - "JDT indexes ready. Starting Phase 2: classpath resolution")); + "Fast reload complete: " + index.size() + " projects restored in " + elapsed + "ms")); - // Phase 2: Resolve classpaths (indexer is ready, reconciliation will find JDK types) - BazelClasspathManager.refreshClasspath(); + return true; + } + 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 @@ -234,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/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; } 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);