Skip to content

Commit 29870e8

Browse files
committed
feat: fix path issue and aggregate project
1 parent 7e671c8 commit 29870e8

4 files changed

Lines changed: 268 additions & 72 deletions

File tree

jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ProjectResolver.java

Lines changed: 212 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ public DependencyInfo(String key, String value) {
229229
/**
230230
* Resolve project dependencies information including JDK version.
231231
* Uses cache with automatic invalidation on project changes.
232+
* Supports both single projects and multi-module aggregator projects.
232233
*
233234
* @param projectUri The project URI
234235
* @param monitor Progress monitor for cancellation support
@@ -252,8 +253,14 @@ public static List<DependencyInfo> resolveProjectDependencies(String projectUri,
252253
}
253254

254255
IJavaProject javaProject = JavaCore.create(project);
256+
257+
// Check if this is a Java project
255258
if (javaProject == null || !javaProject.exists()) {
256-
return result;
259+
// Not a Java project - might be an aggregator/parent project
260+
// Try to find Java sub-projects under this path
261+
JdtlsExtActivator.logInfo("Not a Java project: " + project.getName() +
262+
", checking for sub-projects");
263+
return resolveAggregatorProjectDependencies(root, projectPath, monitor);
257264
}
258265

259266
// Generate cache key based on project location
@@ -291,6 +298,210 @@ public static List<DependencyInfo> resolveProjectDependencies(String projectUri,
291298
return result;
292299
}
293300

301+
/**
302+
* Resolve dependencies for an aggregator/parent project by finding and processing all Java sub-projects.
303+
* This handles multi-module Maven/Gradle projects where the parent is not a Java project itself.
304+
* Returns aggregated information useful for AI context (Java version, common dependencies, build tool).
305+
*
306+
* @param root The workspace root
307+
* @param parentPath The path of the parent/aggregator project
308+
* @param monitor Progress monitor
309+
* @return Aggregated dependency information from all sub-projects
310+
*/
311+
private static List<DependencyInfo> resolveAggregatorProjectDependencies(
312+
IWorkspaceRoot root, IPath parentPath, IProgressMonitor monitor) {
313+
314+
List<DependencyInfo> result = new ArrayList<>();
315+
List<IJavaProject> javaProjects = new ArrayList<>();
316+
317+
// Find all Java projects under the parent path
318+
IProject[] allProjects = root.getProjects();
319+
for (IProject p : allProjects) {
320+
if (p.getLocation() != null && parentPath.isPrefixOf(p.getLocation())) {
321+
try {
322+
if (p.isAccessible() && p.hasNature(JavaCore.NATURE_ID)) {
323+
IJavaProject jp = JavaCore.create(p);
324+
if (jp != null && jp.exists()) {
325+
javaProjects.add(jp);
326+
}
327+
}
328+
} catch (CoreException e) {
329+
// Skip this project
330+
}
331+
}
332+
}
333+
334+
if (javaProjects.isEmpty()) {
335+
JdtlsExtActivator.logInfo("No Java sub-projects found under: " + parentPath.toOSString());
336+
return result;
337+
}
338+
339+
JdtlsExtActivator.logInfo("Found " + javaProjects.size() +
340+
" Java sub-project(s) under: " + parentPath.toOSString());
341+
342+
// Mark as aggregator project
343+
result.add(new DependencyInfo("aggregatorProject", "true"));
344+
result.add(new DependencyInfo("totalSubProjects", String.valueOf(javaProjects.size())));
345+
346+
// Collect sub-project names for reference
347+
StringBuilder projectNames = new StringBuilder();
348+
for (int i = 0; i < javaProjects.size(); i++) {
349+
if (i > 0) projectNames.append(", ");
350+
projectNames.append(javaProjects.get(i).getProject().getName());
351+
}
352+
result.add(new DependencyInfo("subProjectNames", projectNames.toString()));
353+
354+
// Determine the primary/representative Java version (most common or highest)
355+
String primaryJavaVersion = determinePrimaryJavaVersion(javaProjects);
356+
if (primaryJavaVersion != null) {
357+
result.add(new DependencyInfo(KEY_JAVA_VERSION, primaryJavaVersion));
358+
}
359+
360+
// Collect all unique libraries across sub-projects (top 10 most common)
361+
Map<String, Integer> libraryFrequency = collectLibraryFrequency(javaProjects, monitor);
362+
addTopLibraries(result, libraryFrequency, 10);
363+
364+
// Detect build tool from parent directory
365+
IProject parentProject = findProjectByPath(root, parentPath);
366+
if (parentProject != null) {
367+
detectBuildTool(result, parentProject);
368+
}
369+
370+
// Get JRE container info from first sub-project (usually consistent across modules)
371+
if (!javaProjects.isEmpty()) {
372+
extractJreInfo(result, javaProjects.get(0));
373+
}
374+
375+
return result;
376+
}
377+
378+
/**
379+
* Determine the primary Java version from all sub-projects.
380+
* Returns the most common version, or the highest if there's a tie.
381+
*/
382+
private static String determinePrimaryJavaVersion(List<IJavaProject> javaProjects) {
383+
Map<String, Integer> versionCount = new ConcurrentHashMap<>();
384+
385+
for (IJavaProject jp : javaProjects) {
386+
String version = jp.getOption(JavaCore.COMPILER_COMPLIANCE, true);
387+
if (version != null) {
388+
versionCount.put(version, versionCount.getOrDefault(version, 0) + 1);
389+
}
390+
}
391+
392+
if (versionCount.isEmpty()) {
393+
return null;
394+
}
395+
396+
// Find most common version (or highest if tie)
397+
return versionCount.entrySet().stream()
398+
.max((e1, e2) -> {
399+
int countCompare = Integer.compare(e1.getValue(), e2.getValue());
400+
if (countCompare != 0) return countCompare;
401+
// If same count, prefer higher version
402+
return e1.getKey().compareTo(e2.getKey());
403+
})
404+
.map(Map.Entry::getKey)
405+
.orElse(null);
406+
}
407+
408+
/**
409+
* Collect frequency of all libraries across sub-projects.
410+
* Returns a map of library name to frequency count.
411+
*/
412+
private static Map<String, Integer> collectLibraryFrequency(
413+
List<IJavaProject> javaProjects, IProgressMonitor monitor) {
414+
415+
Map<String, Integer> libraryFrequency = new ConcurrentHashMap<>();
416+
417+
for (IJavaProject jp : javaProjects) {
418+
if (monitor.isCanceled()) {
419+
break;
420+
}
421+
422+
try {
423+
IClasspathEntry[] entries = jp.getResolvedClasspath(true);
424+
for (IClasspathEntry entry : entries) {
425+
if (entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) {
426+
IPath libPath = entry.getPath();
427+
if (libPath != null) {
428+
String libName = libPath.lastSegment();
429+
libraryFrequency.put(libName,
430+
libraryFrequency.getOrDefault(libName, 0) + 1);
431+
}
432+
}
433+
}
434+
} catch (JavaModelException e) {
435+
// Skip this project
436+
}
437+
}
438+
439+
return libraryFrequency;
440+
}
441+
442+
/**
443+
* Add top N most common libraries to result.
444+
*/
445+
private static void addTopLibraries(List<DependencyInfo> result,
446+
Map<String, Integer> libraryFrequency, int topN) {
447+
448+
if (libraryFrequency.isEmpty()) {
449+
result.add(new DependencyInfo(KEY_TOTAL_LIBRARIES, "0"));
450+
return;
451+
}
452+
453+
// Sort by frequency (descending) and take top N
454+
List<Map.Entry<String, Integer>> topLibs = libraryFrequency.entrySet().stream()
455+
.sorted((e1, e2) -> Integer.compare(e2.getValue(), e1.getValue()))
456+
.limit(topN)
457+
.collect(java.util.stream.Collectors.toList());
458+
459+
result.add(new DependencyInfo(KEY_TOTAL_LIBRARIES,
460+
String.valueOf(libraryFrequency.size())));
461+
462+
// Add top common libraries
463+
int index = 1;
464+
for (Map.Entry<String, Integer> entry : topLibs) {
465+
result.add(new DependencyInfo("commonLibrary_" + index,
466+
entry.getKey() + " (used in " + entry.getValue() + " modules)"));
467+
index++;
468+
}
469+
}
470+
471+
/**
472+
* Extract JRE container information from a Java project.
473+
*/
474+
private static void extractJreInfo(List<DependencyInfo> result, IJavaProject javaProject) {
475+
try {
476+
IClasspathEntry[] entries = javaProject.getResolvedClasspath(true);
477+
for (IClasspathEntry entry : entries) {
478+
if (entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER) {
479+
String containerPath = entry.getPath().toString();
480+
if (containerPath.contains("JRE_CONTAINER")) {
481+
try {
482+
String vmInstallName = JavaRuntime.getVMInstallName(entry.getPath());
483+
addIfNotNull(result, KEY_JRE_CONTAINER, vmInstallName);
484+
return;
485+
} catch (Exception e) {
486+
// Fallback: extract from path
487+
if (containerPath.contains("JavaSE-")) {
488+
int startIdx = containerPath.lastIndexOf("JavaSE-");
489+
String version = containerPath.substring(startIdx);
490+
if (version.contains("/")) {
491+
version = version.substring(0, version.indexOf("/"));
492+
}
493+
result.add(new DependencyInfo(KEY_JRE_CONTAINER, version));
494+
return;
495+
}
496+
}
497+
}
498+
}
499+
}
500+
} catch (JavaModelException e) {
501+
// Ignore
502+
}
503+
}
504+
294505
/**
295506
* Find project by path from all projects in workspace.
296507
*/

src/copilot/contextProvider.ts

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -107,17 +107,7 @@ async function resolveJavaContext(request: ResolveRequest, copilotCancel: vscode
107107
const projectDependencyItems = await CopilotHelper.resolveAndConvertProjectDependencies(
108108
vscode.workspace.workspaceFolders,
109109
copilotCancel,
110-
JavaContextProviderUtils.checkCancellation,
111-
(action: string, status: string, reason?: string) => {
112-
const telemetryData: any = {
113-
"action": action,
114-
"status": status
115-
};
116-
if (reason) {
117-
telemetryData.ContextEmptyReason = reason;
118-
}
119-
sendInfo("", telemetryData);
120-
}
110+
JavaContextProviderUtils.checkCancellation
121111
);
122112
JavaContextProviderUtils.checkCancellation(copilotCancel);
123113

@@ -129,18 +119,7 @@ async function resolveJavaContext(request: ResolveRequest, copilotCancel: vscode
129119
const localImportItems = await CopilotHelper.resolveAndConvertLocalImports(
130120
vscode.window.activeTextEditor,
131121
copilotCancel,
132-
JavaContextProviderUtils.checkCancellation,
133-
(action: string, status: string, reason?: string) => {
134-
const telemetryData: any = {
135-
"action": action,
136-
"status": status
137-
};
138-
if (reason) {
139-
telemetryData.ContextEmptyReason = reason;
140-
}
141-
sendInfo("", telemetryData);
142-
},
143-
JavaContextProviderUtils.createContextItemsFromImports
122+
JavaContextProviderUtils.checkCancellation
144123
);
145124
JavaContextProviderUtils.checkCancellation(copilotCancel);
146125

0 commit comments

Comments
 (0)