From e4f4cda803aacc52640bf8f8d8d476fcb9480c07 Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Thu, 16 Oct 2025 15:28:14 +0800 Subject: [PATCH 1/4] feat: adjust the format of context with javadoc --- .../jdtls/ext/core/ProjectCommand.java | 753 ++++++++++++++---- 1 file changed, 599 insertions(+), 154 deletions(-) diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java index aad11b68..241746b2 100644 --- a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java @@ -86,9 +86,13 @@ public MainClassInfo(String name, String path) { } } + /** + * ImportClassInfo - Conforms to Copilot CodeSnippet format + * Used to provide Java class context information and JavaDoc to Copilot + */ private static class ImportClassInfo { - public String uri; - public String className; + public String uri; // File URI (required) + public String className; // Human-readable class description with JavaDoc appended (required) public ImportClassInfo(String uri, String className) { this.uri = uri; @@ -348,9 +352,9 @@ public static boolean checkImportStatus() { return hasError; } - public static ImportClassInfo[] getImportClassContent(List arguments, IProgressMonitor monitor) { + public static List getImportClassContent(List arguments, IProgressMonitor monitor) { if (arguments == null || arguments.isEmpty()) { - return new ImportClassInfo[0]; + return Collections.emptyList(); } try { @@ -360,7 +364,7 @@ public static ImportClassInfo[] getImportClassContent(List arguments, IP java.net.URI uri = new java.net.URI(fileUri); String filePath = uri.getPath(); if (filePath == null) { - return new ImportClassInfo[0]; + return Collections.emptyList(); } IPath path = new Path(filePath); @@ -369,25 +373,25 @@ public static ImportClassInfo[] getImportClassContent(List arguments, IP IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); IFile file = root.getFileForLocation(path); if (file == null || !file.exists()) { - return new ImportClassInfo[0]; + return Collections.emptyList(); } // Get the Java project IJavaProject javaProject = JavaCore.create(file.getProject()); if (javaProject == null || !javaProject.exists()) { - return new ImportClassInfo[0]; + return Collections.emptyList(); } // Find the compilation unit IJavaElement javaElement = JavaCore.create(file); if (!(javaElement instanceof org.eclipse.jdt.core.ICompilationUnit)) { - return new ImportClassInfo[0]; + return Collections.emptyList(); } org.eclipse.jdt.core.ICompilationUnit compilationUnit = (org.eclipse.jdt.core.ICompilationUnit) javaElement; // Parse imports and resolve local project files - List result = new ArrayList<>(); + List classInfoList = new ArrayList<>(); // Get all imports from the compilation unit org.eclipse.jdt.core.IImportDeclaration[] imports = compilationUnit.getImports(); @@ -403,27 +407,27 @@ public static ImportClassInfo[] getImportClassContent(List arguments, IP if (isStatic) { // Handle static imports - resolveStaticImport(javaProject, importName, result, processedTypes); + resolveStaticImport(javaProject, importName, classInfoList, processedTypes, monitor); } else if (importName.endsWith(".*")) { // Handle package imports String packageName = importName.substring(0, importName.length() - 2); - resolvePackageTypes(javaProject, packageName, result, processedTypes); + resolvePackageTypes(javaProject, packageName, classInfoList, processedTypes, monitor); } else { // Handle single type imports - resolveSingleType(javaProject, importName, result, processedTypes); + resolveSingleType(javaProject, importName, classInfoList, processedTypes, monitor); } } - return result.toArray(new ImportClassInfo[0]); + return classInfoList; } catch (Exception e) { JdtlsExtActivator.logException("Error in resolveCopilotRequest", e); - return new ImportClassInfo[0]; + return Collections.emptyList(); } } - private static void resolveSingleType(IJavaProject javaProject, String typeName, List result, - Set processedTypes) { + private static void resolveSingleType(IJavaProject javaProject, String typeName, List classInfoList, + Set processedTypes, IProgressMonitor monitor) { try { if (processedTypes.contains(typeName)) { return; @@ -450,7 +454,7 @@ private static void resolveSingleType(IJavaProject javaProject, String typeName, // Found type - check if it's a source type we want to process if (!type.isBinary()) { // Source type found - extract information and return - extractTypeInfo(type, result); + extractTypeInfo(type, classInfoList, monitor); return; } // Note: Binary types (from JARs/JRE) are intentionally ignored @@ -476,7 +480,7 @@ private static void resolveSingleType(IJavaProject javaProject, String typeName, if (primaryType != null && primaryType.exists() && typeName.equals(primaryType.getFullyQualifiedName())) { // Found local project source type via fallback method - extractTypeInfo(primaryType, result); + extractTypeInfo(primaryType, classInfoList, monitor); return; } @@ -484,7 +488,7 @@ private static void resolveSingleType(IJavaProject javaProject, String typeName, org.eclipse.jdt.core.IType[] allTypes = cu.getAllTypes(); for (org.eclipse.jdt.core.IType type : allTypes) { if (typeName.equals(type.getFullyQualifiedName())) { - extractTypeInfo(type, result); + extractTypeInfo(type, classInfoList, monitor); return; } } @@ -498,20 +502,20 @@ private static void resolveSingleType(IJavaProject javaProject, String typeName, } } - private static void resolveStaticImport(IJavaProject javaProject, String staticImportName, List result, - Set processedTypes) { + private static void resolveStaticImport(IJavaProject javaProject, String staticImportName, List classInfoList, + Set processedTypes, IProgressMonitor monitor) { try { if (staticImportName.endsWith(".*")) { // Static import of all static members from a class: import static MyClass.*; String className = staticImportName.substring(0, staticImportName.length() - 2); - resolveStaticMembersFromClass(javaProject, className, result, processedTypes); + resolveStaticMembersFromClass(javaProject, className, classInfoList, processedTypes, monitor); } else { // Static import of specific member: import static MyClass.myMethod; int lastDotIndex = staticImportName.lastIndexOf('.'); if (lastDotIndex > 0) { String className = staticImportName.substring(0, lastDotIndex); String memberName = staticImportName.substring(lastDotIndex + 1); - resolveStaticMemberFromClass(javaProject, className, memberName, result, processedTypes); + resolveStaticMemberFromClass(javaProject, className, memberName, classInfoList, processedTypes, monitor); } } } catch (Exception e) { @@ -520,44 +524,60 @@ private static void resolveStaticImport(IJavaProject javaProject, String staticI } private static void resolveStaticMembersFromClass(IJavaProject javaProject, String className, - List result, Set processedTypes) { + List classInfoList, Set processedTypes, IProgressMonitor monitor) { try { // First resolve the class itself to get context information - resolveSingleType(javaProject, className, result, processedTypes); + resolveSingleType(javaProject, className, classInfoList, processedTypes, monitor); // Find the type and extract its static members org.eclipse.jdt.core.IType type = javaProject.findType(className); if (type != null && type.exists() && !type.isBinary()) { - String staticMemberInfo = "staticImport:" + className + ".*"; - - // Add information about available static members - List staticMembers = new ArrayList<>(); + StringBuilder description = new StringBuilder(); + description.append("Static Import: ").append(className).append(".*\n"); + description.append("All static members from ").append(className).append("\n\n"); // Get static methods IMethod[] methods = type.getMethods(); + List staticMethodSigs = new ArrayList<>(); for (IMethod method : methods) { int flags = method.getFlags(); if (org.eclipse.jdt.core.Flags.isStatic(flags) && org.eclipse.jdt.core.Flags.isPublic(flags)) { - staticMembers.add("method:" + method.getElementName() + getParameterTypes(method)); + if (staticMethodSigs.size() < 10) { + staticMethodSigs.add(generateMethodSignature(method)); + } } } // Get static fields org.eclipse.jdt.core.IField[] fields = type.getFields(); + List staticFieldSigs = new ArrayList<>(); for (org.eclipse.jdt.core.IField field : fields) { int flags = field.getFlags(); if (org.eclipse.jdt.core.Flags.isStatic(flags) && org.eclipse.jdt.core.Flags.isPublic(flags)) { - staticMembers.add("field:" + field.getElementName()); + if (staticFieldSigs.size() < 10) { + staticFieldSigs.add(generateFieldSignature(field)); + } } } - if (!staticMembers.isEmpty()) { - staticMemberInfo += "|members:" + String.join(",", staticMembers.subList(0, Math.min(staticMembers.size(), 10))); + if (!staticMethodSigs.isEmpty()) { + description.append("Static Methods:\n"); + for (String sig : staticMethodSigs) { + description.append(" - ").append(sig).append("\n"); + } + description.append("\n"); + } + + if (!staticFieldSigs.isEmpty()) { + description.append("Static Fields:\n"); + for (String sig : staticFieldSigs) { + description.append(" - ").append(sig).append("\n"); + } } String uri = getTypeUri(type); if (uri != null) { - result.add(new ImportClassInfo(uri, staticMemberInfo)); + classInfoList.add(new ImportClassInfo(uri, description.toString())); } } } catch (JavaModelException e) { @@ -566,15 +586,18 @@ private static void resolveStaticMembersFromClass(IJavaProject javaProject, Stri } private static void resolveStaticMemberFromClass(IJavaProject javaProject, String className, String memberName, - List result, Set processedTypes) { + List classInfoList, Set processedTypes, IProgressMonitor monitor) { try { // First resolve the class itself - resolveSingleType(javaProject, className, result, processedTypes); + resolveSingleType(javaProject, className, classInfoList, processedTypes, monitor); // Find the specific static member org.eclipse.jdt.core.IType type = javaProject.findType(className); if (type != null && type.exists() && !type.isBinary()) { - String memberInfo = "staticImport:" + className + "." + memberName; + StringBuilder description = new StringBuilder(); + description.append("Static Import: ").append(className).append(".").append(memberName).append("\n\n"); + + boolean found = false; // Check if it's a method IMethod[] methods = type.getMethods(); @@ -582,27 +605,35 @@ private static void resolveStaticMemberFromClass(IJavaProject javaProject, Strin if (method.getElementName().equals(memberName)) { int flags = method.getFlags(); if (org.eclipse.jdt.core.Flags.isStatic(flags)) { - memberInfo += "|type:method" + getParameterTypes(method); + description.append("Static Method:\n"); + description.append(" - ").append(generateMethodSignature(method)).append("\n"); + found = true; break; } } } // Check if it's a field - org.eclipse.jdt.core.IField[] fields = type.getFields(); - for (org.eclipse.jdt.core.IField field : fields) { - if (field.getElementName().equals(memberName)) { - int flags = field.getFlags(); - if (org.eclipse.jdt.core.Flags.isStatic(flags)) { - memberInfo += "|type:field"; - break; + if (!found) { + org.eclipse.jdt.core.IField[] fields = type.getFields(); + for (org.eclipse.jdt.core.IField field : fields) { + if (field.getElementName().equals(memberName)) { + int flags = field.getFlags(); + if (org.eclipse.jdt.core.Flags.isStatic(flags)) { + description.append("Static Field:\n"); + description.append(" - ").append(generateFieldSignature(field)).append("\n"); + found = true; + break; + } } } } - String uri = getTypeUri(type); - if (uri != null) { - result.add(new ImportClassInfo(uri, memberInfo)); + if (found) { + String uri = getTypeUri(type); + if (uri != null) { + classInfoList.add(new ImportClassInfo(uri, description.toString())); + } } } } catch (JavaModelException e) { @@ -610,8 +641,8 @@ private static void resolveStaticMemberFromClass(IJavaProject javaProject, Strin } } - private static void resolvePackageTypes(IJavaProject javaProject, String packageName, List result, - Set processedTypes) { + private static void resolvePackageTypes(IJavaProject javaProject, String packageName, List classInfoList, + Set processedTypes, IProgressMonitor monitor) { try { // Find all package fragments with this name IPackageFragmentRoot[] packageRoots = javaProject.getPackageFragmentRoots(); @@ -629,7 +660,7 @@ private static void resolvePackageTypes(IJavaProject javaProject, String package String fullTypeName = type.getFullyQualifiedName(); if (!processedTypes.contains(fullTypeName)) { processedTypes.add(fullTypeName); - extractTypeInfo(type, result); + extractTypeInfo(type, classInfoList, monitor); } } } @@ -642,128 +673,204 @@ private static void resolvePackageTypes(IJavaProject javaProject, String package } } - private static void extractTypeInfo(org.eclipse.jdt.core.IType type, List result) { + /** + * Extract type information and generate ImportClassInfo conforming to Copilot CodeSnippet format + * Also extracts JavaDoc if available and appends it to the class description + * Improved version: generates human-readable class descriptions with integrated JavaDoc + */ + private static void extractTypeInfo(org.eclipse.jdt.core.IType type, List classInfoList, + IProgressMonitor monitor) { try { - String typeName = type.getFullyQualifiedName(); - String typeInfo = ""; - - // Determine type kind - if (type.isInterface()) { - typeInfo = "interface:" + typeName; - } else if (type.isClass()) { - extractDetailedClassInfo(type, result); - return; // extractDetailedClassInfo handles adding to result - } else if (type.isEnum()) { - typeInfo = "enum:" + typeName; - } else if (type.isAnnotation()) { - typeInfo = "annotation:" + typeName; - } else { - typeInfo = "type:" + typeName; - } - - // Get URI for this type + // Get file URI String uri = getTypeUri(type); - if (uri != null) { - result.add(new ImportClassInfo(uri, typeInfo)); + if (uri == null) { + return; } - - // Also add nested types + + // Generate human-readable class description + String description = generateClassDescription(type); + + // Extract JavaDoc (MVP: class-level only) and append to description + String javadoc = extractClassJavaDoc(type, monitor); + if (javadoc != null && !javadoc.isEmpty()) { + description = description + "\n" + javadoc; + } + + // Create ImportClassInfo (conforms to Copilot CodeSnippet format) + ImportClassInfo info = new ImportClassInfo(uri, description); + classInfoList.add(info); + + // Recursively process nested types org.eclipse.jdt.core.IType[] nestedTypes = type.getTypes(); for (org.eclipse.jdt.core.IType nestedType : nestedTypes) { - extractTypeInfo(nestedType, result); + extractTypeInfo(nestedType, classInfoList, monitor); } - + } catch (JavaModelException e) { - // Log but continue processing other types JdtlsExtActivator.logException("Error extracting type info for: " + type.getElementName(), e); } } - private static void extractDetailedClassInfo(org.eclipse.jdt.core.IType type, List result) { + /** + * Extract class-level JavaDoc and convert to plain text + * Returns null if no JavaDoc is available + * + * Note: For source types, extracts from source comments using getJavadocRange() + * For binary types (JARs), uses getAttachedJavadoc() which requires javadoc.jar + */ + private static String extractClassJavaDoc(org.eclipse.jdt.core.IType type, IProgressMonitor monitor) { try { - if (!type.isClass()) { - return; // Only process classes - } - - String className = type.getFullyQualifiedName(); - List classDetails = new ArrayList<>(); - - // 1. Class declaration information - classDetails.add("class:" + className); - - // 2. Modifiers - int flags = type.getFlags(); - List modifiers = new ArrayList<>(); - if (org.eclipse.jdt.core.Flags.isPublic(flags)) - modifiers.add("public"); - if (org.eclipse.jdt.core.Flags.isAbstract(flags)) - modifiers.add("abstract"); - if (org.eclipse.jdt.core.Flags.isFinal(flags)) - modifiers.add("final"); - if (org.eclipse.jdt.core.Flags.isStatic(flags)) - modifiers.add("static"); - if (!modifiers.isEmpty()) { - classDetails.add("modifiers:" + String.join(",", modifiers)); - } - - // 3. Inheritance - String superclass = type.getSuperclassName(); - if (superclass != null && !"Object".equals(superclass)) { - classDetails.add("extends:" + superclass); - } - - // 4. Implemented interfaces - String[] interfaces = type.getSuperInterfaceNames(); - if (interfaces.length > 0) { - classDetails.add("implements:" + String.join(",", interfaces)); - } - - // 5. Constructors - IMethod[] methods = type.getMethods(); - List constructors = new ArrayList<>(); - List publicMethods = new ArrayList<>(); - - for (IMethod method : methods) { - if (method.isConstructor()) { - constructors.add(method.getElementName() + getParameterTypes(method)); - } else if (org.eclipse.jdt.core.Flags.isPublic(method.getFlags())) { - publicMethods.add(method.getElementName() + getParameterTypes(method)); + // For source types: Read JavaDoc directly from source comments + if (!type.isBinary()) { + org.eclipse.jdt.core.ISourceRange javadocRange = type.getJavadocRange(); + if (javadocRange != null) { + org.eclipse.jdt.core.ICompilationUnit cu = type.getCompilationUnit(); + if (cu != null) { + String source = cu.getSource(); + if (source != null) { + int offset = javadocRange.getOffset(); + int length = javadocRange.getLength(); + if (offset >= 0 && length > 0 && offset + length <= source.length()) { + String rawJavadoc = source.substring(offset, offset + length); + // Clean up JavaDoc comment markers + String cleanedJavadoc = cleanJavadocComment(rawJavadoc); + if (cleanedJavadoc != null && !cleanedJavadoc.trim().isEmpty()) { + return cleanedJavadoc.trim(); + } + } + } + } } - } - - if (!constructors.isEmpty()) { - classDetails.add("constructors:" + String.join(",", constructors)); - } - - if (!publicMethods.isEmpty()) { - classDetails.add("publicMethods:" - + String.join(",", publicMethods.subList(0, Math.min(publicMethods.size(), 10)))); - } - - // 6. Public fields - org.eclipse.jdt.core.IField[] fields = type.getFields(); - List publicFields = new ArrayList<>(); - for (org.eclipse.jdt.core.IField field : fields) { - if (org.eclipse.jdt.core.Flags.isPublic(field.getFlags())) { - publicFields.add(field.getElementName()); + } else { + // For binary types: Try to get attached JavaDoc from javadoc.jar + String htmlJavadoc = type.getAttachedJavadoc(monitor); + if (htmlJavadoc != null && !htmlJavadoc.isEmpty()) { + // Convert HTML to plain text + String plainText = stripHtmlTags(htmlJavadoc); + if (plainText != null && !plainText.trim().isEmpty()) { + return plainText.trim(); + } } } + } catch (JavaModelException e) { + // Silent fail - JavaDoc is optional + JdtlsExtActivator.logException("Failed to extract JavaDoc for: " + type.getElementName(), e); + } + return null; + } - if (!publicFields.isEmpty()) { - classDetails.add("publicFields:" + String.join(",", publicFields)); + /** + * Clean up raw JavaDoc comment by removing comment markers and asterisks + * + * Converts raw JavaDoc comment text into clean readable text + */ + private static String cleanJavadocComment(String rawJavadoc) { + if (rawJavadoc == null || rawJavadoc.isEmpty()) { + return ""; + } + + // Remove opening /** and closing */ + String cleaned = rawJavadoc; + cleaned = cleaned.replaceFirst("^/\\*\\*", ""); + cleaned = cleaned.replaceFirst("\\*/$", ""); + + // Split into lines and clean each line + String[] lines = cleaned.split("\\r?\\n"); + StringBuilder result = new StringBuilder(); + + for (String line : lines) { + // Remove leading whitespace and asterisk + String trimmed = line.trim(); + if (trimmed.startsWith("*")) { + trimmed = trimmed.substring(1).trim(); } - - // Get URI for this type - String uri = getTypeUri(type); - if (uri != null) { - // Combine all information into one string - String classInfo = String.join("|", classDetails); - result.add(new ImportClassInfo(uri, classInfo)); + + // Skip empty lines at the beginning + if (result.length() == 0 && trimmed.isEmpty()) { + continue; + } + + // Add line to result + if (result.length() > 0 && !trimmed.isEmpty()) { + result.append("\n"); } + result.append(trimmed); + } + + return result.toString(); + } - } catch (JavaModelException e) { - JdtlsExtActivator.logException("Error extracting detailed class info", e); + /** + * Strip HTML tags and convert HTML entities to plain text + * Preserves important structure like paragraphs, code blocks, and lists + */ + private static String stripHtmlTags(String html) { + if (html == null || html.isEmpty()) { + return ""; } + + // Preserve structure by converting HTML tags to newlines/formatting + String text = html; + + // 1. Preserve code blocks - mark them for special handling + text = text.replaceAll("(?i)]*>", "\n```\n"); + text = text.replaceAll("(?i)", "\n```\n"); + text = text.replaceAll("(?i)]*>", "`"); + text = text.replaceAll("(?i)", "`"); + + // 2. Preserve paragraphs and line breaks + text = text.replaceAll("(?i)]*>", "\n\n"); + text = text.replaceAll("(?i)

", ""); + text = text.replaceAll("(?i)]*>", "\n"); + text = text.replaceAll("(?i)]*>", "\n"); + text = text.replaceAll("(?i)", "\n"); + + // 3. Preserve lists + text = text.replaceAll("(?i)]*>", "\n"); + text = text.replaceAll("(?i)", "\n"); + text = text.replaceAll("(?i)]*>", "\n"); + text = text.replaceAll("(?i)", "\n"); + text = text.replaceAll("(?i)]*>", "\n • "); + text = text.replaceAll("(?i)", ""); + + // 4. Preserve definition lists (used for @param, @return, etc.) + text = text.replaceAll("(?i)]*>", "\n"); + text = text.replaceAll("(?i)", "\n"); + text = text.replaceAll("(?i)]*>", "\n"); + text = text.replaceAll("(?i)", ": "); + text = text.replaceAll("(?i)]*>", ""); + text = text.replaceAll("(?i)", "\n"); + + // 5. Handle headings + text = text.replaceAll("(?i)]*>", "\n\n"); + text = text.replaceAll("(?i)", "\n"); + + // 6. Remove remaining HTML tags + text = text.replaceAll("<[^>]+>", ""); + + // 7. Convert HTML entities + text = text.replace(" ", " "); + text = text.replace("<", "<"); + text = text.replace(">", ">"); + text = text.replace("&", "&"); + text = text.replace(""", "\""); + text = text.replace("'", "'"); + text = text.replace("'", "'"); + text = text.replace("—", "—"); + text = text.replace("–", "–"); + + // 8. Clean up excessive whitespace while preserving intentional line breaks + // Remove spaces at start/end of lines + text = text.replaceAll("[ \\t]+\\n", "\n"); + text = text.replaceAll("\\n[ \\t]+", "\n"); + // Collapse multiple spaces within a line + text = text.replaceAll("[ \\t]+", " "); + // Limit consecutive newlines to maximum 2 (one blank line) + text = text.replaceAll("\\n{3,}", "\n\n"); + + text = text.trim(); + + return text; } // Helper method: Get file URI/path for the type (instead of fully qualified class name) @@ -801,7 +908,345 @@ private static String getTypeUri(org.eclipse.jdt.core.IType type) { } } - // Helper method: Get method parameter types + /** + * Convert JDT type signature to human-readable format + * + * Examples: + * - QT; -> T + * - QString; -> String + * - I -> int, Z -> boolean + * - [QString; -> String[] + */ + private static String convertTypeSignature(String jdtSignature) { + if (jdtSignature == null || jdtSignature.isEmpty()) { + return "void"; + } + + // Handle array types + int arrayDimensions = 0; + while (jdtSignature.startsWith("[")) { + arrayDimensions++; + jdtSignature = jdtSignature.substring(1); + } + + String baseType; + + // Handle type parameters and reference types (starts with Q) + if (jdtSignature.startsWith("Q") && jdtSignature.endsWith(";")) { + baseType = jdtSignature.substring(1, jdtSignature.length() - 1); + baseType = baseType.replace('/', '.'); + + // Simplify package name: java.util.List -> List + if (baseType.contains(".")) { + String[] parts = baseType.split("\\."); + baseType = parts[parts.length - 1]; + } + } + // Handle fully qualified types (starts with L) + else if (jdtSignature.startsWith("L") && jdtSignature.endsWith(";")) { + baseType = jdtSignature.substring(1, jdtSignature.length() - 1); + baseType = baseType.replace('/', '.'); + + // Simplify package name + if (baseType.contains(".")) { + String[] parts = baseType.split("\\."); + baseType = parts[parts.length - 1]; + } + } + // Handle primitive types + else { + switch (jdtSignature.charAt(0)) { + case 'I': baseType = "int"; break; + case 'Z': baseType = "boolean"; break; + case 'V': baseType = "void"; break; + case 'J': baseType = "long"; break; + case 'F': baseType = "float"; break; + case 'D': baseType = "double"; break; + case 'B': baseType = "byte"; break; + case 'C': baseType = "char"; break; + case 'S': baseType = "short"; break; + default: baseType = jdtSignature; + } + } + + // Add array markers + for (int i = 0; i < arrayDimensions; i++) { + baseType += "[]"; + } + + return baseType; + } + + /** + * Generate human-readable method signature + * + * Example: public static Result success(T value) + */ + private static String generateMethodSignature(IMethod method) { + StringBuilder sb = new StringBuilder(); + + try { + // Access modifiers + int flags = method.getFlags(); + if (org.eclipse.jdt.core.Flags.isPublic(flags)) { + sb.append("public "); + } else if (org.eclipse.jdt.core.Flags.isProtected(flags)) { + sb.append("protected "); + } else if (org.eclipse.jdt.core.Flags.isPrivate(flags)) { + sb.append("private "); + } + + // static/final/abstract modifiers + if (org.eclipse.jdt.core.Flags.isStatic(flags)) { + sb.append("static "); + } + if (org.eclipse.jdt.core.Flags.isFinal(flags)) { + sb.append("final "); + } + if (org.eclipse.jdt.core.Flags.isAbstract(flags)) { + sb.append("abstract "); + } + + // Type parameters (if any) + String[] typeParameters = method.getTypeParameterSignatures(); + if (typeParameters != null && typeParameters.length > 0) { + sb.append("<"); + for (int i = 0; i < typeParameters.length; i++) { + if (i > 0) sb.append(", "); + sb.append(convertTypeSignature(typeParameters[i])); + } + sb.append("> "); + } + + // Return type (constructors don't have return type) + if (!method.isConstructor()) { + String returnType = convertTypeSignature(method.getReturnType()); + sb.append(returnType).append(" "); + } + + // Method name + sb.append(method.getElementName()).append("("); + + // Parameter list + String[] paramTypes = method.getParameterTypes(); + String[] paramNames = method.getParameterNames(); + for (int i = 0; i < paramTypes.length; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(convertTypeSignature(paramTypes[i])); + if (paramNames != null && i < paramNames.length) { + sb.append(" ").append(paramNames[i]); + } + } + + sb.append(")"); + + // Exception declarations + String[] exceptionTypes = method.getExceptionTypes(); + if (exceptionTypes != null && exceptionTypes.length > 0) { + sb.append(" throws "); + for (int i = 0; i < exceptionTypes.length; i++) { + if (i > 0) sb.append(", "); + sb.append(convertTypeSignature(exceptionTypes[i])); + } + } + + } catch (JavaModelException e) { + return method.getElementName() + "(...)"; + } + + return sb.toString(); + } + + /** + * Generate human-readable field signature + * + * Example: private final String message + */ + private static String generateFieldSignature(org.eclipse.jdt.core.IField field) { + StringBuilder sb = new StringBuilder(); + + try { + // Access modifiers + int flags = field.getFlags(); + if (org.eclipse.jdt.core.Flags.isPublic(flags)) { + sb.append("public "); + } else if (org.eclipse.jdt.core.Flags.isProtected(flags)) { + sb.append("protected "); + } else if (org.eclipse.jdt.core.Flags.isPrivate(flags)) { + sb.append("private "); + } + + // static/final modifiers + if (org.eclipse.jdt.core.Flags.isStatic(flags)) { + sb.append("static "); + } + if (org.eclipse.jdt.core.Flags.isFinal(flags)) { + sb.append("final "); + } + + // Type and name + String fieldType = convertTypeSignature(field.getTypeSignature()); + sb.append(fieldType).append(" ").append(field.getElementName()); + + // If it's a constant, try to get the initial value + if (org.eclipse.jdt.core.Flags.isStatic(flags) && org.eclipse.jdt.core.Flags.isFinal(flags)) { + Object constant = field.getConstant(); + if (constant != null) { + sb.append(" = "); + if (constant instanceof String) { + sb.append("\"").append(constant).append("\""); + } else { + sb.append(constant); + } + } + } + + } catch (JavaModelException e) { + return field.getElementName(); + } + + return sb.toString(); + } + + /** + * Generate complete class description (natural language format, similar to JavaDoc) + */ + private static String generateClassDescription(org.eclipse.jdt.core.IType type) { + StringBuilder description = new StringBuilder(); + + try { + String qualifiedName = type.getFullyQualifiedName(); + String simpleName = type.getElementName(); + + // === 1. Title and signature === + description.append("Class: ").append(qualifiedName).append("\n"); + + // Generate class signature + StringBuilder signature = new StringBuilder(); + int flags = type.getFlags(); + + if (org.eclipse.jdt.core.Flags.isPublic(flags)) signature.append("public "); + if (org.eclipse.jdt.core.Flags.isAbstract(flags)) signature.append("abstract "); + if (org.eclipse.jdt.core.Flags.isFinal(flags)) signature.append("final "); + + if (type.isInterface()) { + signature.append("interface "); + } else if (type.isEnum()) { + signature.append("enum "); + } else if (type.isAnnotation()) { + signature.append("@interface "); + } else { + signature.append("class "); + } + + signature.append(simpleName); + + // Type parameters + String[] typeParams = type.getTypeParameterSignatures(); + if (typeParams != null && typeParams.length > 0) { + signature.append("<"); + for (int i = 0; i < typeParams.length; i++) { + if (i > 0) signature.append(", "); + signature.append(convertTypeSignature(typeParams[i])); + } + signature.append(">"); + } + + // Inheritance relationship + String superclass = type.getSuperclassName(); + if (superclass != null && !superclass.equals("Object") && !type.isInterface()) { + signature.append(" extends ").append(superclass); + } + + // Implemented interfaces + String[] interfaces = type.getSuperInterfaceNames(); + if (interfaces != null && interfaces.length > 0) { + if (type.isInterface()) { + signature.append(" extends "); + } else { + signature.append(" implements "); + } + for (int i = 0; i < interfaces.length; i++) { + if (i > 0) signature.append(", "); + signature.append(interfaces[i]); + } + } + + description.append("Signature: ").append(signature).append("\n\n"); + + // === 2. Constructors === + IMethod[] methods = type.getMethods(); + List constructorSigs = new ArrayList<>(); + + for (IMethod method : methods) { + if (method.isConstructor()) { + constructorSigs.add(generateMethodSignature(method)); + } + } + + if (!constructorSigs.isEmpty()) { + description.append("Constructors:\n"); + for (String sig : constructorSigs) { + description.append(" - ").append(sig).append("\n"); + } + description.append("\n"); + } + + // === 3. Public methods (limited to first 10) === + List methodSigs = new ArrayList<>(); + int methodCount = 0; + + for (IMethod method : methods) { + if (!method.isConstructor() && org.eclipse.jdt.core.Flags.isPublic(method.getFlags())) { + if (methodCount < 10) { + methodSigs.add(generateMethodSignature(method)); + methodCount++; + } else { + break; + } + } + } + + if (!methodSigs.isEmpty()) { + description.append("Methods:\n"); + for (String sig : methodSigs) { + description.append(" - ").append(sig).append("\n"); + } + if (methodCount == 10 && methods.length > methodCount) { + description.append(" - ... (more methods available)\n"); + } + description.append("\n"); + } + + // === 4. Public fields (limited to first 10) === + org.eclipse.jdt.core.IField[] fields = type.getFields(); + List fieldSigs = new ArrayList<>(); + int fieldCount = 0; + + for (org.eclipse.jdt.core.IField field : fields) { + if (org.eclipse.jdt.core.Flags.isPublic(field.getFlags()) && fieldCount < 10) { + fieldSigs.add(generateFieldSignature(field)); + fieldCount++; + } + } + + if (!fieldSigs.isEmpty()) { + description.append("Fields:\n"); + for (String sig : fieldSigs) { + description.append(" - ").append(sig).append("\n"); + } + } + + } catch (JavaModelException e) { + return "Error generating description for type: " + e.getMessage(); + } + + return description.toString(); + } + + // Helper method: Get method parameter types (deprecated - use generateMethodSignature instead) private static String getParameterTypes(IMethod method) { String[] paramTypes = method.getParameterTypes(); if (paramTypes.length == 0) { From 6838ba3f5b6154f1e3dcac46f0e7a6947aecdf5b Mon Sep 17 00:00:00 2001 From: wenytang-ms Date: Thu, 16 Oct 2025 16:56:20 +0800 Subject: [PATCH 2/4] feat: update --- .../jdtls/ext/core/ProjectCommand.java | 869 +----------------- .../ext/core/parser/ContextResolver.java | 856 +++++++++++++++++ 2 files changed, 875 insertions(+), 850 deletions(-) create mode 100644 jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ContextResolver.java diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java index 241746b2..df41a432 100644 --- a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ProjectCommand.java @@ -48,7 +48,6 @@ import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.IMethod; import org.eclipse.jdt.core.IModuleDescription; -import org.eclipse.jdt.core.IPackageFragmentRoot; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.core.search.IJavaSearchConstants; @@ -70,6 +69,8 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.microsoft.jdtls.ext.core.parser.ContextResolver; +import com.microsoft.jdtls.ext.core.parser.ContextResolver.ImportClassInfo; import com.microsoft.jdtls.ext.core.model.PackageNode; public final class ProjectCommand { @@ -86,19 +87,7 @@ public MainClassInfo(String name, String path) { } } - /** - * ImportClassInfo - Conforms to Copilot CodeSnippet format - * Used to provide Java class context information and JavaDoc to Copilot - */ - private static class ImportClassInfo { - public String uri; // File URI (required) - public String className; // Human-readable class description with JavaDoc appended (required) - public ImportClassInfo(String uri, String className) { - this.uri = uri; - this.className = className; - } - } private static class Classpath { public String source; @@ -352,6 +341,14 @@ public static boolean checkImportStatus() { return hasError; } + /** + * Get import class content for Copilot integration. + * This method extracts information about imported classes from a Java file. + * + * @param arguments List containing the file URI as the first element + * @param monitor Progress monitor for cancellation support + * @return List of ImportClassInfo containing class information and JavaDoc + */ public static List getImportClassContent(List arguments, IProgressMonitor monitor) { if (arguments == null || arguments.isEmpty()) { return Collections.emptyList(); @@ -391,6 +388,7 @@ public static List getImportClassContent(List arguments org.eclipse.jdt.core.ICompilationUnit compilationUnit = (org.eclipse.jdt.core.ICompilationUnit) javaElement; // Parse imports and resolve local project files + // Delegate to JavaContentParser for processing List classInfoList = new ArrayList<>(); // Get all imports from the compilation unit @@ -406,855 +404,26 @@ public static List getImportClassContent(List arguments boolean isStatic = (importDecl.getFlags() & org.eclipse.jdt.core.Flags.AccStatic) != 0; if (isStatic) { - // Handle static imports - resolveStaticImport(javaProject, importName, classInfoList, processedTypes, monitor); + // Handle static imports - delegate to ContextResolver + ContextResolver.resolveStaticImport(javaProject, importName, classInfoList, processedTypes, monitor); } else if (importName.endsWith(".*")) { - // Handle package imports + // Handle package imports - delegate to ContextResolver String packageName = importName.substring(0, importName.length() - 2); - resolvePackageTypes(javaProject, packageName, classInfoList, processedTypes, monitor); + ContextResolver.resolvePackageTypes(javaProject, packageName, classInfoList, processedTypes, monitor); } else { - // Handle single type imports - resolveSingleType(javaProject, importName, classInfoList, processedTypes, monitor); + // Handle single type imports - delegate to ContextResolver + ContextResolver.resolveSingleType(javaProject, importName, classInfoList, processedTypes, monitor); } } return classInfoList; } catch (Exception e) { - JdtlsExtActivator.logException("Error in resolveCopilotRequest", e); + JdtlsExtActivator.logException("Error in getImportClassContent", e); return Collections.emptyList(); } } - private static void resolveSingleType(IJavaProject javaProject, String typeName, List classInfoList, - Set processedTypes, IProgressMonitor monitor) { - try { - if (processedTypes.contains(typeName)) { - return; - } - processedTypes.add(typeName); - - // Extract package and simple name from the fully qualified type name - int lastDotIndex = typeName.lastIndexOf('.'); - if (lastDotIndex == -1) { - // Default package or invalid type name - return; - } - - String packageName = typeName.substring(0, lastDotIndex); - String simpleName = typeName.substring(lastDotIndex + 1); - - // Strategy: Use JDT's global type resolution first (comprehensive), - // then fallback to manual package fragment traversal if needed - - // Primary path: Use JDT's findType which searches all sources and dependencies - try { - org.eclipse.jdt.core.IType type = javaProject.findType(typeName); - if (type != null && type.exists()) { - // Found type - check if it's a source type we want to process - if (!type.isBinary()) { - // Source type found - extract information and return - extractTypeInfo(type, classInfoList, monitor); - return; - } - // Note: Binary types (from JARs/JRE) are intentionally ignored - // as they don't provide useful context for code completion - } - } catch (JavaModelException e) { - JdtlsExtActivator.logException("Error in primary type search: " + typeName, e); - // Continue to fallback method - } - - // Fallback path: Manual search in local source package fragments - // This is used when findType() doesn't return results or fails - IPackageFragmentRoot[] packageRoots = javaProject.getPackageFragmentRoots(); - for (IPackageFragmentRoot packageRoot : packageRoots) { - if (packageRoot.getKind() == IPackageFragmentRoot.K_SOURCE) { - org.eclipse.jdt.core.IPackageFragment packageFragment = packageRoot.getPackageFragment(packageName); - if (packageFragment != null && packageFragment.exists()) { - // Look for compilation unit with matching name - org.eclipse.jdt.core.ICompilationUnit cu = packageFragment.getCompilationUnit(simpleName + ".java"); - if (cu != null && cu.exists() && cu.getResource() != null && cu.getResource().exists()) { - // Get primary type from compilation unit - org.eclipse.jdt.core.IType primaryType = cu.findPrimaryType(); - if (primaryType != null && primaryType.exists() && - typeName.equals(primaryType.getFullyQualifiedName())) { - // Found local project source type via fallback method - extractTypeInfo(primaryType, classInfoList, monitor); - return; - } - - // Also check for inner types in the compilation unit - org.eclipse.jdt.core.IType[] allTypes = cu.getAllTypes(); - for (org.eclipse.jdt.core.IType type : allTypes) { - if (typeName.equals(type.getFullyQualifiedName())) { - extractTypeInfo(type, classInfoList, monitor); - return; - } - } - } - } - } - } - } catch (JavaModelException e) { - // Log but continue processing other types - JdtlsExtActivator.logException("Error resolving type: " + typeName, e); - } - } - - private static void resolveStaticImport(IJavaProject javaProject, String staticImportName, List classInfoList, - Set processedTypes, IProgressMonitor monitor) { - try { - if (staticImportName.endsWith(".*")) { - // Static import of all static members from a class: import static MyClass.*; - String className = staticImportName.substring(0, staticImportName.length() - 2); - resolveStaticMembersFromClass(javaProject, className, classInfoList, processedTypes, monitor); - } else { - // Static import of specific member: import static MyClass.myMethod; - int lastDotIndex = staticImportName.lastIndexOf('.'); - if (lastDotIndex > 0) { - String className = staticImportName.substring(0, lastDotIndex); - String memberName = staticImportName.substring(lastDotIndex + 1); - resolveStaticMemberFromClass(javaProject, className, memberName, classInfoList, processedTypes, monitor); - } - } - } catch (Exception e) { - JdtlsExtActivator.logException("Error resolving static import: " + staticImportName, e); - } - } - - private static void resolveStaticMembersFromClass(IJavaProject javaProject, String className, - List classInfoList, Set processedTypes, IProgressMonitor monitor) { - try { - // First resolve the class itself to get context information - resolveSingleType(javaProject, className, classInfoList, processedTypes, monitor); - - // Find the type and extract its static members - org.eclipse.jdt.core.IType type = javaProject.findType(className); - if (type != null && type.exists() && !type.isBinary()) { - StringBuilder description = new StringBuilder(); - description.append("Static Import: ").append(className).append(".*\n"); - description.append("All static members from ").append(className).append("\n\n"); - - // Get static methods - IMethod[] methods = type.getMethods(); - List staticMethodSigs = new ArrayList<>(); - for (IMethod method : methods) { - int flags = method.getFlags(); - if (org.eclipse.jdt.core.Flags.isStatic(flags) && org.eclipse.jdt.core.Flags.isPublic(flags)) { - if (staticMethodSigs.size() < 10) { - staticMethodSigs.add(generateMethodSignature(method)); - } - } - } - - // Get static fields - org.eclipse.jdt.core.IField[] fields = type.getFields(); - List staticFieldSigs = new ArrayList<>(); - for (org.eclipse.jdt.core.IField field : fields) { - int flags = field.getFlags(); - if (org.eclipse.jdt.core.Flags.isStatic(flags) && org.eclipse.jdt.core.Flags.isPublic(flags)) { - if (staticFieldSigs.size() < 10) { - staticFieldSigs.add(generateFieldSignature(field)); - } - } - } - - if (!staticMethodSigs.isEmpty()) { - description.append("Static Methods:\n"); - for (String sig : staticMethodSigs) { - description.append(" - ").append(sig).append("\n"); - } - description.append("\n"); - } - - if (!staticFieldSigs.isEmpty()) { - description.append("Static Fields:\n"); - for (String sig : staticFieldSigs) { - description.append(" - ").append(sig).append("\n"); - } - } - - String uri = getTypeUri(type); - if (uri != null) { - classInfoList.add(new ImportClassInfo(uri, description.toString())); - } - } - } catch (JavaModelException e) { - JdtlsExtActivator.logException("Error resolving static members from: " + className, e); - } - } - - private static void resolveStaticMemberFromClass(IJavaProject javaProject, String className, String memberName, - List classInfoList, Set processedTypes, IProgressMonitor monitor) { - try { - // First resolve the class itself - resolveSingleType(javaProject, className, classInfoList, processedTypes, monitor); - - // Find the specific static member - org.eclipse.jdt.core.IType type = javaProject.findType(className); - if (type != null && type.exists() && !type.isBinary()) { - StringBuilder description = new StringBuilder(); - description.append("Static Import: ").append(className).append(".").append(memberName).append("\n\n"); - - boolean found = false; - - // Check if it's a method - IMethod[] methods = type.getMethods(); - for (IMethod method : methods) { - if (method.getElementName().equals(memberName)) { - int flags = method.getFlags(); - if (org.eclipse.jdt.core.Flags.isStatic(flags)) { - description.append("Static Method:\n"); - description.append(" - ").append(generateMethodSignature(method)).append("\n"); - found = true; - break; - } - } - } - - // Check if it's a field - if (!found) { - org.eclipse.jdt.core.IField[] fields = type.getFields(); - for (org.eclipse.jdt.core.IField field : fields) { - if (field.getElementName().equals(memberName)) { - int flags = field.getFlags(); - if (org.eclipse.jdt.core.Flags.isStatic(flags)) { - description.append("Static Field:\n"); - description.append(" - ").append(generateFieldSignature(field)).append("\n"); - found = true; - break; - } - } - } - } - - if (found) { - String uri = getTypeUri(type); - if (uri != null) { - classInfoList.add(new ImportClassInfo(uri, description.toString())); - } - } - } - } catch (JavaModelException e) { - JdtlsExtActivator.logException("Error resolving static member: " + className + "." + memberName, e); - } - } - - private static void resolvePackageTypes(IJavaProject javaProject, String packageName, List classInfoList, - Set processedTypes, IProgressMonitor monitor) { - try { - // Find all package fragments with this name - IPackageFragmentRoot[] packageRoots = javaProject.getPackageFragmentRoots(); - for (IPackageFragmentRoot packageRoot : packageRoots) { - if (packageRoot.getKind() == IPackageFragmentRoot.K_SOURCE) { - org.eclipse.jdt.core.IPackageFragment packageFragment = packageRoot.getPackageFragment(packageName); - if (packageFragment != null && packageFragment.exists()) { - // Get all compilation units in this package - org.eclipse.jdt.core.ICompilationUnit[] compilationUnits = packageFragment - .getCompilationUnits(); - for (org.eclipse.jdt.core.ICompilationUnit cu : compilationUnits) { - // Get all types in the compilation unit - org.eclipse.jdt.core.IType[] types = cu.getAllTypes(); - for (org.eclipse.jdt.core.IType type : types) { - String fullTypeName = type.getFullyQualifiedName(); - if (!processedTypes.contains(fullTypeName)) { - processedTypes.add(fullTypeName); - extractTypeInfo(type, classInfoList, monitor); - } - } - } - } - } - } - } catch (JavaModelException e) { - // Log but continue processing - JdtlsExtActivator.logException("Error resolving package: " + packageName, e); - } - } - - /** - * Extract type information and generate ImportClassInfo conforming to Copilot CodeSnippet format - * Also extracts JavaDoc if available and appends it to the class description - * Improved version: generates human-readable class descriptions with integrated JavaDoc - */ - private static void extractTypeInfo(org.eclipse.jdt.core.IType type, List classInfoList, - IProgressMonitor monitor) { - try { - // Get file URI - String uri = getTypeUri(type); - if (uri == null) { - return; - } - - // Generate human-readable class description - String description = generateClassDescription(type); - - // Extract JavaDoc (MVP: class-level only) and append to description - String javadoc = extractClassJavaDoc(type, monitor); - if (javadoc != null && !javadoc.isEmpty()) { - description = description + "\n" + javadoc; - } - - // Create ImportClassInfo (conforms to Copilot CodeSnippet format) - ImportClassInfo info = new ImportClassInfo(uri, description); - classInfoList.add(info); - - // Recursively process nested types - org.eclipse.jdt.core.IType[] nestedTypes = type.getTypes(); - for (org.eclipse.jdt.core.IType nestedType : nestedTypes) { - extractTypeInfo(nestedType, classInfoList, monitor); - } - - } catch (JavaModelException e) { - JdtlsExtActivator.logException("Error extracting type info for: " + type.getElementName(), e); - } - } - - /** - * Extract class-level JavaDoc and convert to plain text - * Returns null if no JavaDoc is available - * - * Note: For source types, extracts from source comments using getJavadocRange() - * For binary types (JARs), uses getAttachedJavadoc() which requires javadoc.jar - */ - private static String extractClassJavaDoc(org.eclipse.jdt.core.IType type, IProgressMonitor monitor) { - try { - // For source types: Read JavaDoc directly from source comments - if (!type.isBinary()) { - org.eclipse.jdt.core.ISourceRange javadocRange = type.getJavadocRange(); - if (javadocRange != null) { - org.eclipse.jdt.core.ICompilationUnit cu = type.getCompilationUnit(); - if (cu != null) { - String source = cu.getSource(); - if (source != null) { - int offset = javadocRange.getOffset(); - int length = javadocRange.getLength(); - if (offset >= 0 && length > 0 && offset + length <= source.length()) { - String rawJavadoc = source.substring(offset, offset + length); - // Clean up JavaDoc comment markers - String cleanedJavadoc = cleanJavadocComment(rawJavadoc); - if (cleanedJavadoc != null && !cleanedJavadoc.trim().isEmpty()) { - return cleanedJavadoc.trim(); - } - } - } - } - } - } else { - // For binary types: Try to get attached JavaDoc from javadoc.jar - String htmlJavadoc = type.getAttachedJavadoc(monitor); - if (htmlJavadoc != null && !htmlJavadoc.isEmpty()) { - // Convert HTML to plain text - String plainText = stripHtmlTags(htmlJavadoc); - if (plainText != null && !plainText.trim().isEmpty()) { - return plainText.trim(); - } - } - } - } catch (JavaModelException e) { - // Silent fail - JavaDoc is optional - JdtlsExtActivator.logException("Failed to extract JavaDoc for: " + type.getElementName(), e); - } - return null; - } - - /** - * Clean up raw JavaDoc comment by removing comment markers and asterisks - * - * Converts raw JavaDoc comment text into clean readable text - */ - private static String cleanJavadocComment(String rawJavadoc) { - if (rawJavadoc == null || rawJavadoc.isEmpty()) { - return ""; - } - - // Remove opening /** and closing */ - String cleaned = rawJavadoc; - cleaned = cleaned.replaceFirst("^/\\*\\*", ""); - cleaned = cleaned.replaceFirst("\\*/$", ""); - - // Split into lines and clean each line - String[] lines = cleaned.split("\\r?\\n"); - StringBuilder result = new StringBuilder(); - - for (String line : lines) { - // Remove leading whitespace and asterisk - String trimmed = line.trim(); - if (trimmed.startsWith("*")) { - trimmed = trimmed.substring(1).trim(); - } - - // Skip empty lines at the beginning - if (result.length() == 0 && trimmed.isEmpty()) { - continue; - } - - // Add line to result - if (result.length() > 0 && !trimmed.isEmpty()) { - result.append("\n"); - } - result.append(trimmed); - } - - return result.toString(); - } - - /** - * Strip HTML tags and convert HTML entities to plain text - * Preserves important structure like paragraphs, code blocks, and lists - */ - private static String stripHtmlTags(String html) { - if (html == null || html.isEmpty()) { - return ""; - } - - // Preserve structure by converting HTML tags to newlines/formatting - String text = html; - - // 1. Preserve code blocks - mark them for special handling - text = text.replaceAll("(?i)]*>", "\n```\n"); - text = text.replaceAll("(?i)", "\n```\n"); - text = text.replaceAll("(?i)]*>", "`"); - text = text.replaceAll("(?i)", "`"); - - // 2. Preserve paragraphs and line breaks - text = text.replaceAll("(?i)]*>", "\n\n"); - text = text.replaceAll("(?i)

", ""); - text = text.replaceAll("(?i)]*>", "\n"); - text = text.replaceAll("(?i)]*>", "\n"); - text = text.replaceAll("(?i)", "\n"); - - // 3. Preserve lists - text = text.replaceAll("(?i)]*>", "\n"); - text = text.replaceAll("(?i)", "\n"); - text = text.replaceAll("(?i)]*>", "\n"); - text = text.replaceAll("(?i)", "\n"); - text = text.replaceAll("(?i)]*>", "\n • "); - text = text.replaceAll("(?i)", ""); - - // 4. Preserve definition lists (used for @param, @return, etc.) - text = text.replaceAll("(?i)]*>", "\n"); - text = text.replaceAll("(?i)", "\n"); - text = text.replaceAll("(?i)]*>", "\n"); - text = text.replaceAll("(?i)", ": "); - text = text.replaceAll("(?i)]*>", ""); - text = text.replaceAll("(?i)", "\n"); - - // 5. Handle headings - text = text.replaceAll("(?i)]*>", "\n\n"); - text = text.replaceAll("(?i)", "\n"); - - // 6. Remove remaining HTML tags - text = text.replaceAll("<[^>]+>", ""); - - // 7. Convert HTML entities - text = text.replace(" ", " "); - text = text.replace("<", "<"); - text = text.replace(">", ">"); - text = text.replace("&", "&"); - text = text.replace(""", "\""); - text = text.replace("'", "'"); - text = text.replace("'", "'"); - text = text.replace("—", "—"); - text = text.replace("–", "–"); - - // 8. Clean up excessive whitespace while preserving intentional line breaks - // Remove spaces at start/end of lines - text = text.replaceAll("[ \\t]+\\n", "\n"); - text = text.replaceAll("\\n[ \\t]+", "\n"); - // Collapse multiple spaces within a line - text = text.replaceAll("[ \\t]+", " "); - // Limit consecutive newlines to maximum 2 (one blank line) - text = text.replaceAll("\\n{3,}", "\n\n"); - - text = text.trim(); - - return text; - } - - // Helper method: Get file URI/path for the type (instead of fully qualified class name) - private static String getTypeUri(org.eclipse.jdt.core.IType type) { - try { - // Get the compilation unit that contains this type - org.eclipse.jdt.core.ICompilationUnit compilationUnit = type.getCompilationUnit(); - if (compilationUnit != null) { - // Get the underlying resource (file) - org.eclipse.core.resources.IResource resource = compilationUnit.getUnderlyingResource(); - if (resource != null && resource instanceof org.eclipse.core.resources.IFile) { - org.eclipse.core.resources.IFile file = (org.eclipse.core.resources.IFile) resource; - // Get the file location as a file URI - java.net.URI fileUri = file.getLocationURI(); - if (fileUri != null) { - return fileUri.toString(); - } - - // Fallback: use workspace-relative path as URI - return file.getFullPath().toString(); - } - } - - // Fallback: if we can't get file URI, return the fully qualified class name - // This should rarely happen for source types - return type.getFullyQualifiedName(); - } catch (Exception e) { - JdtlsExtActivator.logException("Error getting file URI for type: " + type.getElementName(), e); - // Fallback to class name in case of error - try { - return type.getFullyQualifiedName(); - } catch (Exception e2) { - return null; - } - } - } - - /** - * Convert JDT type signature to human-readable format - * - * Examples: - * - QT; -> T - * - QString; -> String - * - I -> int, Z -> boolean - * - [QString; -> String[] - */ - private static String convertTypeSignature(String jdtSignature) { - if (jdtSignature == null || jdtSignature.isEmpty()) { - return "void"; - } - - // Handle array types - int arrayDimensions = 0; - while (jdtSignature.startsWith("[")) { - arrayDimensions++; - jdtSignature = jdtSignature.substring(1); - } - - String baseType; - - // Handle type parameters and reference types (starts with Q) - if (jdtSignature.startsWith("Q") && jdtSignature.endsWith(";")) { - baseType = jdtSignature.substring(1, jdtSignature.length() - 1); - baseType = baseType.replace('/', '.'); - - // Simplify package name: java.util.List -> List - if (baseType.contains(".")) { - String[] parts = baseType.split("\\."); - baseType = parts[parts.length - 1]; - } - } - // Handle fully qualified types (starts with L) - else if (jdtSignature.startsWith("L") && jdtSignature.endsWith(";")) { - baseType = jdtSignature.substring(1, jdtSignature.length() - 1); - baseType = baseType.replace('/', '.'); - - // Simplify package name - if (baseType.contains(".")) { - String[] parts = baseType.split("\\."); - baseType = parts[parts.length - 1]; - } - } - // Handle primitive types - else { - switch (jdtSignature.charAt(0)) { - case 'I': baseType = "int"; break; - case 'Z': baseType = "boolean"; break; - case 'V': baseType = "void"; break; - case 'J': baseType = "long"; break; - case 'F': baseType = "float"; break; - case 'D': baseType = "double"; break; - case 'B': baseType = "byte"; break; - case 'C': baseType = "char"; break; - case 'S': baseType = "short"; break; - default: baseType = jdtSignature; - } - } - - // Add array markers - for (int i = 0; i < arrayDimensions; i++) { - baseType += "[]"; - } - - return baseType; - } - - /** - * Generate human-readable method signature - * - * Example: public static Result success(T value) - */ - private static String generateMethodSignature(IMethod method) { - StringBuilder sb = new StringBuilder(); - - try { - // Access modifiers - int flags = method.getFlags(); - if (org.eclipse.jdt.core.Flags.isPublic(flags)) { - sb.append("public "); - } else if (org.eclipse.jdt.core.Flags.isProtected(flags)) { - sb.append("protected "); - } else if (org.eclipse.jdt.core.Flags.isPrivate(flags)) { - sb.append("private "); - } - - // static/final/abstract modifiers - if (org.eclipse.jdt.core.Flags.isStatic(flags)) { - sb.append("static "); - } - if (org.eclipse.jdt.core.Flags.isFinal(flags)) { - sb.append("final "); - } - if (org.eclipse.jdt.core.Flags.isAbstract(flags)) { - sb.append("abstract "); - } - - // Type parameters (if any) - String[] typeParameters = method.getTypeParameterSignatures(); - if (typeParameters != null && typeParameters.length > 0) { - sb.append("<"); - for (int i = 0; i < typeParameters.length; i++) { - if (i > 0) sb.append(", "); - sb.append(convertTypeSignature(typeParameters[i])); - } - sb.append("> "); - } - - // Return type (constructors don't have return type) - if (!method.isConstructor()) { - String returnType = convertTypeSignature(method.getReturnType()); - sb.append(returnType).append(" "); - } - - // Method name - sb.append(method.getElementName()).append("("); - - // Parameter list - String[] paramTypes = method.getParameterTypes(); - String[] paramNames = method.getParameterNames(); - for (int i = 0; i < paramTypes.length; i++) { - if (i > 0) { - sb.append(", "); - } - sb.append(convertTypeSignature(paramTypes[i])); - if (paramNames != null && i < paramNames.length) { - sb.append(" ").append(paramNames[i]); - } - } - - sb.append(")"); - - // Exception declarations - String[] exceptionTypes = method.getExceptionTypes(); - if (exceptionTypes != null && exceptionTypes.length > 0) { - sb.append(" throws "); - for (int i = 0; i < exceptionTypes.length; i++) { - if (i > 0) sb.append(", "); - sb.append(convertTypeSignature(exceptionTypes[i])); - } - } - - } catch (JavaModelException e) { - return method.getElementName() + "(...)"; - } - - return sb.toString(); - } - - /** - * Generate human-readable field signature - * - * Example: private final String message - */ - private static String generateFieldSignature(org.eclipse.jdt.core.IField field) { - StringBuilder sb = new StringBuilder(); - - try { - // Access modifiers - int flags = field.getFlags(); - if (org.eclipse.jdt.core.Flags.isPublic(flags)) { - sb.append("public "); - } else if (org.eclipse.jdt.core.Flags.isProtected(flags)) { - sb.append("protected "); - } else if (org.eclipse.jdt.core.Flags.isPrivate(flags)) { - sb.append("private "); - } - - // static/final modifiers - if (org.eclipse.jdt.core.Flags.isStatic(flags)) { - sb.append("static "); - } - if (org.eclipse.jdt.core.Flags.isFinal(flags)) { - sb.append("final "); - } - - // Type and name - String fieldType = convertTypeSignature(field.getTypeSignature()); - sb.append(fieldType).append(" ").append(field.getElementName()); - - // If it's a constant, try to get the initial value - if (org.eclipse.jdt.core.Flags.isStatic(flags) && org.eclipse.jdt.core.Flags.isFinal(flags)) { - Object constant = field.getConstant(); - if (constant != null) { - sb.append(" = "); - if (constant instanceof String) { - sb.append("\"").append(constant).append("\""); - } else { - sb.append(constant); - } - } - } - - } catch (JavaModelException e) { - return field.getElementName(); - } - - return sb.toString(); - } - - /** - * Generate complete class description (natural language format, similar to JavaDoc) - */ - private static String generateClassDescription(org.eclipse.jdt.core.IType type) { - StringBuilder description = new StringBuilder(); - - try { - String qualifiedName = type.getFullyQualifiedName(); - String simpleName = type.getElementName(); - - // === 1. Title and signature === - description.append("Class: ").append(qualifiedName).append("\n"); - - // Generate class signature - StringBuilder signature = new StringBuilder(); - int flags = type.getFlags(); - - if (org.eclipse.jdt.core.Flags.isPublic(flags)) signature.append("public "); - if (org.eclipse.jdt.core.Flags.isAbstract(flags)) signature.append("abstract "); - if (org.eclipse.jdt.core.Flags.isFinal(flags)) signature.append("final "); - - if (type.isInterface()) { - signature.append("interface "); - } else if (type.isEnum()) { - signature.append("enum "); - } else if (type.isAnnotation()) { - signature.append("@interface "); - } else { - signature.append("class "); - } - - signature.append(simpleName); - - // Type parameters - String[] typeParams = type.getTypeParameterSignatures(); - if (typeParams != null && typeParams.length > 0) { - signature.append("<"); - for (int i = 0; i < typeParams.length; i++) { - if (i > 0) signature.append(", "); - signature.append(convertTypeSignature(typeParams[i])); - } - signature.append(">"); - } - - // Inheritance relationship - String superclass = type.getSuperclassName(); - if (superclass != null && !superclass.equals("Object") && !type.isInterface()) { - signature.append(" extends ").append(superclass); - } - - // Implemented interfaces - String[] interfaces = type.getSuperInterfaceNames(); - if (interfaces != null && interfaces.length > 0) { - if (type.isInterface()) { - signature.append(" extends "); - } else { - signature.append(" implements "); - } - for (int i = 0; i < interfaces.length; i++) { - if (i > 0) signature.append(", "); - signature.append(interfaces[i]); - } - } - - description.append("Signature: ").append(signature).append("\n\n"); - - // === 2. Constructors === - IMethod[] methods = type.getMethods(); - List constructorSigs = new ArrayList<>(); - - for (IMethod method : methods) { - if (method.isConstructor()) { - constructorSigs.add(generateMethodSignature(method)); - } - } - - if (!constructorSigs.isEmpty()) { - description.append("Constructors:\n"); - for (String sig : constructorSigs) { - description.append(" - ").append(sig).append("\n"); - } - description.append("\n"); - } - - // === 3. Public methods (limited to first 10) === - List methodSigs = new ArrayList<>(); - int methodCount = 0; - - for (IMethod method : methods) { - if (!method.isConstructor() && org.eclipse.jdt.core.Flags.isPublic(method.getFlags())) { - if (methodCount < 10) { - methodSigs.add(generateMethodSignature(method)); - methodCount++; - } else { - break; - } - } - } - - if (!methodSigs.isEmpty()) { - description.append("Methods:\n"); - for (String sig : methodSigs) { - description.append(" - ").append(sig).append("\n"); - } - if (methodCount == 10 && methods.length > methodCount) { - description.append(" - ... (more methods available)\n"); - } - description.append("\n"); - } - - // === 4. Public fields (limited to first 10) === - org.eclipse.jdt.core.IField[] fields = type.getFields(); - List fieldSigs = new ArrayList<>(); - int fieldCount = 0; - - for (org.eclipse.jdt.core.IField field : fields) { - if (org.eclipse.jdt.core.Flags.isPublic(field.getFlags()) && fieldCount < 10) { - fieldSigs.add(generateFieldSignature(field)); - fieldCount++; - } - } - - if (!fieldSigs.isEmpty()) { - description.append("Fields:\n"); - for (String sig : fieldSigs) { - description.append(" - ").append(sig).append("\n"); - } - } - - } catch (JavaModelException e) { - return "Error generating description for type: " + e.getMessage(); - } - - return description.toString(); - } - - // Helper method: Get method parameter types (deprecated - use generateMethodSignature instead) - private static String getParameterTypes(IMethod method) { - String[] paramTypes = method.getParameterTypes(); - if (paramTypes.length == 0) { - return "()"; - } - return "(" + String.join(",", paramTypes) + ")"; - } - private static void reportExportJarMessage(String terminalId, int severity, String message) { if (StringUtils.isNotBlank(message) && StringUtils.isNotBlank(terminalId)) { String readableSeverity = getSeverityString(severity); @@ -1315,4 +484,4 @@ public boolean isBelongsToWorkspace() { return belongsToWorkspace; } } -} \ No newline at end of file +} diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ContextResolver.java b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ContextResolver.java new file mode 100644 index 00000000..2a0c0112 --- /dev/null +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ContextResolver.java @@ -0,0 +1,856 @@ +/******************************************************************************* + * Copyright (c) 2018 Microsoft Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Microsoft Corporation - initial API and implementation + *******************************************************************************/ + +package com.microsoft.jdtls.ext.core.parser; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jdt.core.IMethod; +import org.eclipse.jdt.core.IPackageFragmentRoot; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaModelException; + +import com.microsoft.jdtls.ext.core.JdtlsExtActivator; + +/** + * Parser for extracting Java class content information for Copilot integration. + * Handles import resolution, JavaDoc extraction, and class description generation. + */ +public class ContextResolver { + + /** + * ImportClassInfo - Conforms to Copilot CodeSnippet format + * Used to provide Java class context information and JavaDoc to Copilot + */ + public static class ImportClassInfo { + public String uri; // File URI (required) + public String className; // Human-readable class description with JavaDoc appended (required) + + public ImportClassInfo(String uri, String className) { + this.uri = uri; + this.className = className; + } + } + + /** + * Resolve a single type import and extract its information + */ + public static void resolveSingleType(IJavaProject javaProject, String typeName, List classInfoList, + Set processedTypes, IProgressMonitor monitor) { + try { + if (processedTypes.contains(typeName)) { + return; + } + processedTypes.add(typeName); + + // Extract package and simple name from the fully qualified type name + int lastDotIndex = typeName.lastIndexOf('.'); + if (lastDotIndex == -1) { + // Default package or invalid type name + return; + } + + String packageName = typeName.substring(0, lastDotIndex); + String simpleName = typeName.substring(lastDotIndex + 1); + + // Strategy: Use JDT's global type resolution first (comprehensive), + // then fallback to manual package fragment traversal if needed + + // Primary path: Use JDT's findType which searches all sources and dependencies + try { + org.eclipse.jdt.core.IType type = javaProject.findType(typeName); + if (type != null && type.exists()) { + // Found type - check if it's a source type we want to process + if (!type.isBinary()) { + // Source type found - extract information and return + extractTypeInfo(type, classInfoList, monitor); + return; + } + // Note: Binary types (from JARs/JRE) are intentionally ignored + // as they don't provide useful context for code completion + } + } catch (JavaModelException e) { + JdtlsExtActivator.logException("Error in primary type search: " + typeName, e); + // Continue to fallback method + } + + // Fallback path: Manual search in local source package fragments + // This is used when findType() doesn't return results or fails + IPackageFragmentRoot[] packageRoots = javaProject.getPackageFragmentRoots(); + for (IPackageFragmentRoot packageRoot : packageRoots) { + if (packageRoot.getKind() == IPackageFragmentRoot.K_SOURCE) { + org.eclipse.jdt.core.IPackageFragment packageFragment = packageRoot.getPackageFragment(packageName); + if (packageFragment != null && packageFragment.exists()) { + // Look for compilation unit with matching name + org.eclipse.jdt.core.ICompilationUnit cu = packageFragment.getCompilationUnit(simpleName + ".java"); + if (cu != null && cu.exists() && cu.getResource() != null && cu.getResource().exists()) { + // Get primary type from compilation unit + org.eclipse.jdt.core.IType primaryType = cu.findPrimaryType(); + if (primaryType != null && primaryType.exists() && + typeName.equals(primaryType.getFullyQualifiedName())) { + // Found local project source type via fallback method + extractTypeInfo(primaryType, classInfoList, monitor); + return; + } + + // Also check for inner types in the compilation unit + org.eclipse.jdt.core.IType[] allTypes = cu.getAllTypes(); + for (org.eclipse.jdt.core.IType type : allTypes) { + if (typeName.equals(type.getFullyQualifiedName())) { + extractTypeInfo(type, classInfoList, monitor); + return; + } + } + } + } + } + } + } catch (JavaModelException e) { + // Log but continue processing other types + JdtlsExtActivator.logException("Error resolving type: " + typeName, e); + } + } + + /** + * Resolve a static import statement + */ + public static void resolveStaticImport(IJavaProject javaProject, String staticImportName, List classInfoList, + Set processedTypes, IProgressMonitor monitor) { + try { + if (staticImportName.endsWith(".*")) { + // Static import of all static members from a class: import static MyClass.*; + String className = staticImportName.substring(0, staticImportName.length() - 2); + resolveStaticMembersFromClass(javaProject, className, classInfoList, processedTypes, monitor); + } else { + // Static import of specific member: import static MyClass.myMethod; + int lastDotIndex = staticImportName.lastIndexOf('.'); + if (lastDotIndex > 0) { + String className = staticImportName.substring(0, lastDotIndex); + String memberName = staticImportName.substring(lastDotIndex + 1); + resolveStaticMemberFromClass(javaProject, className, memberName, classInfoList, processedTypes, monitor); + } + } + } catch (Exception e) { + JdtlsExtActivator.logException("Error resolving static import: " + staticImportName, e); + } + } + + /** + * Resolve all static members from a class + */ + public static void resolveStaticMembersFromClass(IJavaProject javaProject, String className, + List classInfoList, Set processedTypes, IProgressMonitor monitor) { + try { + // First resolve the class itself to get context information + resolveSingleType(javaProject, className, classInfoList, processedTypes, monitor); + + // Find the type and extract its static members + org.eclipse.jdt.core.IType type = javaProject.findType(className); + if (type != null && type.exists() && !type.isBinary()) { + StringBuilder description = new StringBuilder(); + description.append("Static Import: ").append(className).append(".*\n"); + description.append("All static members from ").append(className).append("\n\n"); + + // Get static methods + IMethod[] methods = type.getMethods(); + List staticMethodSigs = new ArrayList<>(); + for (IMethod method : methods) { + int flags = method.getFlags(); + if (org.eclipse.jdt.core.Flags.isStatic(flags) && org.eclipse.jdt.core.Flags.isPublic(flags)) { + if (staticMethodSigs.size() < 10) { + staticMethodSigs.add(generateMethodSignature(method)); + } + } + } + + // Get static fields + org.eclipse.jdt.core.IField[] fields = type.getFields(); + List staticFieldSigs = new ArrayList<>(); + for (org.eclipse.jdt.core.IField field : fields) { + int flags = field.getFlags(); + if (org.eclipse.jdt.core.Flags.isStatic(flags) && org.eclipse.jdt.core.Flags.isPublic(flags)) { + if (staticFieldSigs.size() < 10) { + staticFieldSigs.add(generateFieldSignature(field)); + } + } + } + + if (!staticMethodSigs.isEmpty()) { + description.append("Static Methods:\n"); + for (String sig : staticMethodSigs) { + description.append(" - ").append(sig).append("\n"); + } + description.append("\n"); + } + + if (!staticFieldSigs.isEmpty()) { + description.append("Static Fields:\n"); + for (String sig : staticFieldSigs) { + description.append(" - ").append(sig).append("\n"); + } + } + + String uri = getTypeUri(type); + if (uri != null) { + classInfoList.add(new ImportClassInfo(uri, description.toString())); + } + } + } catch (JavaModelException e) { + JdtlsExtActivator.logException("Error resolving static members from: " + className, e); + } + } + + /** + * Resolve a specific static member from a class + */ + public static void resolveStaticMemberFromClass(IJavaProject javaProject, String className, String memberName, + List classInfoList, Set processedTypes, IProgressMonitor monitor) { + try { + // First resolve the class itself + resolveSingleType(javaProject, className, classInfoList, processedTypes, monitor); + + // Find the specific static member + org.eclipse.jdt.core.IType type = javaProject.findType(className); + if (type != null && type.exists() && !type.isBinary()) { + StringBuilder description = new StringBuilder(); + description.append("Static Import: ").append(className).append(".").append(memberName).append("\n\n"); + + boolean found = false; + + // Check if it's a method + IMethod[] methods = type.getMethods(); + for (IMethod method : methods) { + if (method.getElementName().equals(memberName)) { + int flags = method.getFlags(); + if (org.eclipse.jdt.core.Flags.isStatic(flags)) { + description.append("Static Method:\n"); + description.append(" - ").append(generateMethodSignature(method)).append("\n"); + found = true; + break; + } + } + } + + // Check if it's a field + if (!found) { + org.eclipse.jdt.core.IField[] fields = type.getFields(); + for (org.eclipse.jdt.core.IField field : fields) { + if (field.getElementName().equals(memberName)) { + int flags = field.getFlags(); + if (org.eclipse.jdt.core.Flags.isStatic(flags)) { + description.append("Static Field:\n"); + description.append(" - ").append(generateFieldSignature(field)).append("\n"); + found = true; + break; + } + } + } + } + + if (found) { + String uri = getTypeUri(type); + if (uri != null) { + classInfoList.add(new ImportClassInfo(uri, description.toString())); + } + } + } + } catch (JavaModelException e) { + JdtlsExtActivator.logException("Error resolving static member: " + className + "." + memberName, e); + } + } + + /** + * Resolve all types in a package (for wildcard imports) + */ + public static void resolvePackageTypes(IJavaProject javaProject, String packageName, List classInfoList, + Set processedTypes, IProgressMonitor monitor) { + try { + // Find all package fragments with this name + IPackageFragmentRoot[] packageRoots = javaProject.getPackageFragmentRoots(); + for (IPackageFragmentRoot packageRoot : packageRoots) { + if (packageRoot.getKind() == IPackageFragmentRoot.K_SOURCE) { + org.eclipse.jdt.core.IPackageFragment packageFragment = packageRoot.getPackageFragment(packageName); + if (packageFragment != null && packageFragment.exists()) { + // Get all compilation units in this package + org.eclipse.jdt.core.ICompilationUnit[] compilationUnits = packageFragment + .getCompilationUnits(); + for (org.eclipse.jdt.core.ICompilationUnit cu : compilationUnits) { + // Get all types in the compilation unit + org.eclipse.jdt.core.IType[] types = cu.getAllTypes(); + for (org.eclipse.jdt.core.IType type : types) { + String fullTypeName = type.getFullyQualifiedName(); + if (!processedTypes.contains(fullTypeName)) { + processedTypes.add(fullTypeName); + extractTypeInfo(type, classInfoList, monitor); + } + } + } + } + } + } + } catch (JavaModelException e) { + // Log but continue processing + JdtlsExtActivator.logException("Error resolving package: " + packageName, e); + } + } + + /** + * Extract type information and generate ImportClassInfo conforming to Copilot CodeSnippet format + * Also extracts JavaDoc if available and appends it to the class description + * Improved version: generates human-readable class descriptions with integrated JavaDoc + */ + public static void extractTypeInfo(org.eclipse.jdt.core.IType type, List classInfoList, + IProgressMonitor monitor) { + try { + // Get file URI + String uri = getTypeUri(type); + if (uri == null) { + return; + } + + // Generate human-readable class description + String description = generateClassDescription(type); + + // Extract relevant JavaDoc content (code snippets with fallback strategy) + // This uses a hybrid approach: AST extraction -> HTML extraction -> Markdown extraction -> fallback + String relevantJavadoc = extractRelevantJavaDocContent(type, monitor); + if (isNotEmpty(relevantJavadoc)) { + description = description + "\n" + relevantJavadoc; + } + + // Create ImportClassInfo (conforms to Copilot CodeSnippet format) + ImportClassInfo info = new ImportClassInfo(uri, description); + classInfoList.add(info); + + // Recursively process nested types + org.eclipse.jdt.core.IType[] nestedTypes = type.getTypes(); + for (org.eclipse.jdt.core.IType nestedType : nestedTypes) { + extractTypeInfo(nestedType, classInfoList, monitor); + } + + } catch (JavaModelException e) { + JdtlsExtActivator.logException("Error extracting type info for: " + type.getElementName(), e); + } + } + + /** + * Get file URI/path for the type (instead of fully qualified class name) + */ + public static String getTypeUri(org.eclipse.jdt.core.IType type) { + try { + // Get the compilation unit that contains this type + org.eclipse.jdt.core.ICompilationUnit compilationUnit = type.getCompilationUnit(); + if (compilationUnit != null) { + // Get the underlying resource (file) + org.eclipse.core.resources.IResource resource = compilationUnit.getUnderlyingResource(); + if (resource != null && resource instanceof org.eclipse.core.resources.IFile) { + org.eclipse.core.resources.IFile file = (org.eclipse.core.resources.IFile) resource; + // Get the file location as a file URI + java.net.URI fileUri = file.getLocationURI(); + if (fileUri != null) { + return fileUri.toString(); + } + + // Fallback: use workspace-relative path as URI + return file.getFullPath().toString(); + } + } + + // Fallback: if we can't get file URI, return the fully qualified class name + // This should rarely happen for source types + return type.getFullyQualifiedName(); + } catch (Exception e) { + JdtlsExtActivator.logException("Error getting file URI for type: " + type.getElementName(), e); + // Fallback to class name in case of error + try { + return type.getFullyQualifiedName(); + } catch (Exception e2) { + return null; + } + } + } + + /** + * Generate complete class description (natural language format, similar to JavaDoc) + */ + public static String generateClassDescription(org.eclipse.jdt.core.IType type) { + StringBuilder description = new StringBuilder(); + + try { + String qualifiedName = type.getFullyQualifiedName(); + String simpleName = type.getElementName(); + + // === 1. Title and signature === + description.append("Class: ").append(qualifiedName).append("\n"); + + // Generate class signature + StringBuilder signature = new StringBuilder(); + int flags = type.getFlags(); + + if (org.eclipse.jdt.core.Flags.isPublic(flags)) signature.append("public "); + if (org.eclipse.jdt.core.Flags.isAbstract(flags)) signature.append("abstract "); + if (org.eclipse.jdt.core.Flags.isFinal(flags)) signature.append("final "); + + if (type.isInterface()) { + signature.append("interface "); + } else if (type.isEnum()) { + signature.append("enum "); + } else if (type.isAnnotation()) { + signature.append("@interface "); + } else { + signature.append("class "); + } + + signature.append(simpleName); + + // Type parameters + String[] typeParams = type.getTypeParameterSignatures(); + if (typeParams != null && typeParams.length > 0) { + signature.append("<"); + for (int i = 0; i < typeParams.length; i++) { + if (i > 0) signature.append(", "); + signature.append(convertTypeSignature(typeParams[i])); + } + signature.append(">"); + } + + // Inheritance relationship + String superclass = type.getSuperclassName(); + if (superclass != null && !superclass.equals("Object") && !type.isInterface()) { + signature.append(" extends ").append(superclass); + } + + // Implemented interfaces + String[] interfaces = type.getSuperInterfaceNames(); + if (interfaces != null && interfaces.length > 0) { + if (type.isInterface()) { + signature.append(" extends "); + } else { + signature.append(" implements "); + } + for (int i = 0; i < interfaces.length; i++) { + if (i > 0) signature.append(", "); + signature.append(interfaces[i]); + } + } + + description.append("Signature: ").append(signature).append("\n\n"); + + // === 2. Constructors === + IMethod[] methods = type.getMethods(); + List constructorSigs = new ArrayList<>(); + + for (IMethod method : methods) { + if (method.isConstructor()) { + constructorSigs.add(generateMethodSignature(method)); + } + } + + if (!constructorSigs.isEmpty()) { + description.append("Constructors:\n"); + for (String sig : constructorSigs) { + description.append(" - ").append(sig).append("\n"); + } + description.append("\n"); + } + + // === 3. Public methods (limited to first 10) === + List methodSigs = new ArrayList<>(); + int methodCount = 0; + + for (IMethod method : methods) { + if (!method.isConstructor() && org.eclipse.jdt.core.Flags.isPublic(method.getFlags())) { + if (methodCount < 10) { + methodSigs.add(generateMethodSignature(method)); + methodCount++; + } else { + break; + } + } + } + + if (!methodSigs.isEmpty()) { + description.append("Methods:\n"); + for (String sig : methodSigs) { + description.append(" - ").append(sig).append("\n"); + } + if (methodCount == 10 && methods.length > methodCount) { + description.append(" - ... (more methods available)\n"); + } + description.append("\n"); + } + + // === 4. Public fields (limited to first 10) === + org.eclipse.jdt.core.IField[] fields = type.getFields(); + List fieldSigs = new ArrayList<>(); + int fieldCount = 0; + + for (org.eclipse.jdt.core.IField field : fields) { + if (org.eclipse.jdt.core.Flags.isPublic(field.getFlags()) && fieldCount < 10) { + fieldSigs.add(generateFieldSignature(field)); + fieldCount++; + } + } + + if (!fieldSigs.isEmpty()) { + description.append("Fields:\n"); + for (String sig : fieldSigs) { + description.append(" - ").append(sig).append("\n"); + } + } + + } catch (JavaModelException e) { + return "Error generating description for type: " + e.getMessage(); + } + + return description.toString(); + } + + // ================ JavaDoc Extraction Methods ================ + + /** + * Extracts relevant code snippets from Javadoc. + * This method is optimized to extract code from `` tags and markdown code fences, + * and formats them in an LLM-readable format. + * + * @param type the type to extract Javadoc from. + * @param monitor the progress monitor. + * @return A string containing all found code snippets, formatted as markdown code blocks. + */ + private static String extractRelevantJavaDocContent(org.eclipse.jdt.core.IType type, IProgressMonitor monitor) { + try { + String rawJavadoc; + boolean isHtml; + + if (type.isBinary()) { + rawJavadoc = type.getAttachedJavadoc(monitor); + isHtml = true; + } else { + org.eclipse.jdt.core.ISourceRange javadocRange = type.getJavadocRange(); + if (javadocRange == null) { + return ""; + } + rawJavadoc = type.getCompilationUnit().getSource().substring(javadocRange.getOffset(), javadocRange.getOffset() + javadocRange.getLength()); + isHtml = false; // Javadoc comment from source is not HTML + } + + if (!isNotEmpty(rawJavadoc)) { + return ""; + } + + StringBuilder allCodeSnippets = new StringBuilder(); + + // 1. Extract markdown code blocks (```...```) + Pattern markdownPattern = Pattern.compile("(?s)```(?:java)?\\n?(.*?)```"); + Matcher markdownMatcher = markdownPattern.matcher(rawJavadoc); + while (markdownMatcher.find()) { + String code = markdownMatcher.group(1).trim(); + if (isNotEmpty(code)) { + allCodeSnippets.append("```java\n").append(code).append("\n```\n\n"); + } + } + + // 2. Extract HTML
 and  blocks
+            // Clean Javadoc comment for HTML extraction
+            String cleanedForHtml = isHtml ? rawJavadoc : cleanJavadocComment(rawJavadoc);
+            cleanedForHtml = convertHtmlEntities(cleanedForHtml);
+
+            // Priority 1: 
 blocks (often contain well-formatted code)
+            Pattern prePattern = Pattern.compile("(?is)]*>(.*?)
"); + Matcher preMatcher = prePattern.matcher(cleanedForHtml); + while (preMatcher.find()) { + String code = preMatcher.group(1).replaceAll("(?i)]*>", "").replaceAll("(?i)
", "").trim(); + if (isNotEmpty(code)) { + allCodeSnippets.append("```java\n").append(code).append("\n```\n\n"); + } + } + + // Priority 2: blocks (for inline snippets) + Pattern codePattern = Pattern.compile("(?is)]*>(.*?)"); + Matcher codeMatcher = codePattern.matcher(cleanedForHtml); + while (codeMatcher.find()) { + String code = codeMatcher.group(1).trim(); + // Avoid adding duplicates that might be inside

+                if (isNotEmpty(code) && allCodeSnippets.indexOf(code) == -1) {
+                    allCodeSnippets.append("```java\n").append(code).append("\n```\n\n");
+                }
+            }
+
+            return allCodeSnippets.toString().trim();
+
+        } catch (Exception e) {
+            JdtlsExtActivator.logException("Error extracting relevant JavaDoc content for: " + type.getElementName(), e);
+            return "";
+        }
+    }
+
+    /**
+     * Clean up raw JavaDoc comment by removing comment markers and asterisks
+     */
+    private static String cleanJavadocComment(String rawJavadoc) {
+        if (rawJavadoc == null || rawJavadoc.isEmpty()) {
+            return "";
+        }
+        
+        // Remove opening /** and closing */
+        String cleaned = rawJavadoc;
+        cleaned = cleaned.replaceFirst("^/\\*\\*", "");
+        cleaned = cleaned.replaceFirst("\\*/$", "");
+        
+        // Split into lines and clean each line
+        String[] lines = cleaned.split("\\r?\\n");
+        StringBuilder result = new StringBuilder();
+        
+        for (String line : lines) {
+            // Remove leading whitespace and asterisk
+            String trimmed = line.trim();
+            if (trimmed.startsWith("*")) {
+                trimmed = trimmed.substring(1).trim();
+            }
+            
+            // Skip empty lines at the beginning
+            if (result.length() == 0 && trimmed.isEmpty()) {
+                continue;
+            }
+            
+            // Add line to result
+            if (result.length() > 0 && !trimmed.isEmpty()) {
+                result.append("\n");
+            }
+            result.append(trimmed);
+        }
+        
+        return result.toString();
+    }
+
+
+    /**
+     * Convert HTML entities to their plain text equivalents
+     */
+    private static String convertHtmlEntities(String text) {
+        if (text == null || text.isEmpty()) {
+            return text;
+        }
+        String result = text;
+        result = result.replace(" ", " ");
+        result = result.replace("<", "<");
+        result = result.replace(">", ">");
+        result = result.replace("&", "&");
+        result = result.replace(""", "\"");
+        result = result.replace("'", "'");
+        result = result.replace("'", "'");
+        result = result.replace("—", "-");
+        result = result.replace("–", "-");
+        return result;
+    }
+
+    /**
+     * Generate human-readable method signature
+     */
+    public static String generateMethodSignature(IMethod method) {
+        StringBuilder sb = new StringBuilder();
+        
+        try {
+            int flags = method.getFlags();
+            appendAccessModifiers(sb, flags);
+            appendOtherModifiers(sb, flags, true);
+            
+            // Type parameters (if any)
+            String[] typeParameters = method.getTypeParameterSignatures();
+            if (typeParameters != null && typeParameters.length > 0) {
+                sb.append("<");
+                for (int i = 0; i < typeParameters.length; i++) {
+                    if (i > 0) sb.append(", ");
+                    sb.append(convertTypeSignature(typeParameters[i]));
+                }
+                sb.append("> ");
+            }
+            
+            // Return type (constructors don't have return type)
+            if (!method.isConstructor()) {
+                String returnType = convertTypeSignature(method.getReturnType());
+                sb.append(returnType).append(" ");
+            }
+            
+            // Method name
+            sb.append(method.getElementName()).append("(");
+            
+            // Parameter list
+            String[] paramTypes = method.getParameterTypes();
+            String[] paramNames = method.getParameterNames();
+            for (int i = 0; i < paramTypes.length; i++) {
+                if (i > 0) {
+                    sb.append(", ");
+                }
+                sb.append(convertTypeSignature(paramTypes[i]));
+                if (paramNames != null && i < paramNames.length) {
+                    sb.append(" ").append(paramNames[i]);
+                }
+            }
+            
+            sb.append(")");
+            
+            // Exception declarations
+            String[] exceptionTypes = method.getExceptionTypes();
+            if (exceptionTypes != null && exceptionTypes.length > 0) {
+                sb.append(" throws ");
+                for (int i = 0; i < exceptionTypes.length; i++) {
+                    if (i > 0) sb.append(", ");
+                    sb.append(convertTypeSignature(exceptionTypes[i]));
+                }
+            }
+            
+        } catch (JavaModelException e) {
+            return method.getElementName() + "(...)";
+        }
+        
+        return sb.toString();
+    }
+
+    /**
+     * Generate human-readable field signature
+     */
+    public static String generateFieldSignature(org.eclipse.jdt.core.IField field) {
+        StringBuilder sb = new StringBuilder();
+        
+        try {
+            int flags = field.getFlags();
+            appendAccessModifiers(sb, flags);
+            appendOtherModifiers(sb, flags, false);
+            
+            // Type and name
+            String fieldType = convertTypeSignature(field.getTypeSignature());
+            sb.append(fieldType).append(" ").append(field.getElementName());
+            
+            // If it's a constant, try to get the initial value
+            if (org.eclipse.jdt.core.Flags.isStatic(flags) && org.eclipse.jdt.core.Flags.isFinal(flags)) {
+                Object constant = field.getConstant();
+                if (constant != null) {
+                    sb.append(" = ");
+                    if (constant instanceof String) {
+                        sb.append("\"").append(constant).append("\"");
+                    } else {
+                        sb.append(constant);
+                    }
+                }
+            }
+            
+        } catch (JavaModelException e) {
+            return field.getElementName();
+        }
+        
+        return sb.toString();
+    }
+
+    /**
+     * Append access modifiers (public/protected/private) to StringBuilder
+     */
+    private static void appendAccessModifiers(StringBuilder sb, int flags) {
+        if (org.eclipse.jdt.core.Flags.isPublic(flags)) {
+            sb.append("public ");
+        } else if (org.eclipse.jdt.core.Flags.isProtected(flags)) {
+            sb.append("protected ");
+        } else if (org.eclipse.jdt.core.Flags.isPrivate(flags)) {
+            sb.append("private ");
+        }
+    }
+
+    /**
+     * Append other modifiers (static/final/abstract) to StringBuilder
+     */
+    private static void appendOtherModifiers(StringBuilder sb, int flags, boolean isMethod) {
+        if (org.eclipse.jdt.core.Flags.isStatic(flags)) {
+            sb.append("static ");
+        }
+        if (org.eclipse.jdt.core.Flags.isFinal(flags)) {
+            sb.append("final ");
+        }
+        if (isMethod && org.eclipse.jdt.core.Flags.isAbstract(flags)) {
+            sb.append("abstract ");
+        }
+    }
+
+    /**
+     * Convert JDT type signature to human-readable format
+     */
+    public static String convertTypeSignature(String jdtSignature) {
+        if (jdtSignature == null || jdtSignature.isEmpty()) {
+            return "void";
+        }
+
+        // Handle array types
+        int arrayDimensions = 0;
+        while (jdtSignature.startsWith("[")) {
+            arrayDimensions++;
+            jdtSignature = jdtSignature.substring(1);
+        }
+
+        String baseType;
+
+        // Handle type parameters and reference types (starts with Q)
+        if (jdtSignature.startsWith("Q") && jdtSignature.endsWith(";")) {
+            baseType = jdtSignature.substring(1, jdtSignature.length() - 1);
+            baseType = baseType.replace('/', '.');
+            baseType = simplifyTypeName(baseType);
+        }
+        // Handle fully qualified types (starts with L)
+        else if (jdtSignature.startsWith("L") && jdtSignature.endsWith(";")) {
+            baseType = jdtSignature.substring(1, jdtSignature.length() - 1);
+            baseType = baseType.replace('/', '.');
+            baseType = simplifyTypeName(baseType);
+        }
+        // Handle primitive types
+        else {
+            switch (jdtSignature.charAt(0)) {
+                case 'I': baseType = "int"; break;
+                case 'Z': baseType = "boolean"; break;
+                case 'V': baseType = "void"; break;
+                case 'J': baseType = "long"; break;
+                case 'F': baseType = "float"; break;
+                case 'D': baseType = "double"; break;
+                case 'B': baseType = "byte"; break;
+                case 'C': baseType = "char"; break;
+                case 'S': baseType = "short"; break;
+                default: baseType = jdtSignature;
+            }
+        }
+
+        // Add array markers
+        for (int i = 0; i < arrayDimensions; i++) {
+            baseType += "[]";
+        }
+
+        return baseType;
+    }
+
+    /**
+     * Simplify fully qualified type name to just the simple name
+     */
+    private static String simplifyTypeName(String qualifiedName) {
+        if (qualifiedName == null || !qualifiedName.contains(".")) {
+            return qualifiedName;
+        }
+        String[] parts = qualifiedName.split("\\.");
+        return parts[parts.length - 1];
+    }
+
+    /**
+     * Utility method to check if a string is not empty or null
+     */
+    private static boolean isNotEmpty(String value) {
+        return value != null && !value.isEmpty();
+    }
+}

From 4ce0797d44f3d570bd1aca79d9c897b1eae0463d Mon Sep 17 00:00:00 2001
From: wenyutang-ms 
Date: Fri, 17 Oct 2025 10:03:57 +0800
Subject: [PATCH 3/4] feat: update the method

---
 .../ext/core/parser/ContextResolver.java      | 140 ++++++++++++++----
 1 file changed, 111 insertions(+), 29 deletions(-)

diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ContextResolver.java b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ContextResolver.java
index 2a0c0112..6419e142 100644
--- a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ContextResolver.java
+++ b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ContextResolver.java
@@ -12,6 +12,7 @@
 package com.microsoft.jdtls.ext.core.parser;
 
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 import java.util.regex.Matcher;
@@ -31,6 +32,17 @@
  */
 public class ContextResolver {
 
+    // Pre-compiled regex patterns for performance
+    private static final Pattern MARKDOWN_CODE_PATTERN = Pattern.compile("(?s)```(?:java)?\\n?(.*?)```");
+    private static final Pattern HTML_PRE_PATTERN = Pattern.compile("(?is)]*>(.*?)
"); + private static final Pattern HTML_CODE_PATTERN = Pattern.compile("(?is)]*>(.*?)"); + + // Constants for limiting displayed members + private static final int MAX_METHODS_TO_DISPLAY = 10; + private static final int MAX_FIELDS_TO_DISPLAY = 10; + private static final int MAX_STATIC_METHODS_TO_DISPLAY = 10; + private static final int MAX_STATIC_FIELDS_TO_DISPLAY = 10; + /** * ImportClassInfo - Conforms to Copilot CodeSnippet format * Used to provide Java class context information and JavaDoc to Copilot @@ -170,7 +182,7 @@ public static void resolveStaticMembersFromClass(IJavaProject javaProject, Strin for (IMethod method : methods) { int flags = method.getFlags(); if (org.eclipse.jdt.core.Flags.isStatic(flags) && org.eclipse.jdt.core.Flags.isPublic(flags)) { - if (staticMethodSigs.size() < 10) { + if (staticMethodSigs.size() < MAX_STATIC_METHODS_TO_DISPLAY) { staticMethodSigs.add(generateMethodSignature(method)); } } @@ -182,7 +194,7 @@ public static void resolveStaticMembersFromClass(IJavaProject javaProject, Strin for (org.eclipse.jdt.core.IField field : fields) { int flags = field.getFlags(); if (org.eclipse.jdt.core.Flags.isStatic(flags) && org.eclipse.jdt.core.Flags.isPublic(flags)) { - if (staticFieldSigs.size() < 10) { + if (staticFieldSigs.size() < MAX_STATIC_FIELDS_TO_DISPLAY) { staticFieldSigs.add(generateFieldSignature(field)); } } @@ -321,15 +333,12 @@ public static void extractTypeInfo(org.eclipse.jdt.core.IType type, List HTML extraction -> Markdown extraction -> fallback String relevantJavadoc = extractRelevantJavaDocContent(type, monitor); - if (isNotEmpty(relevantJavadoc)) { - description = description + "\n" + relevantJavadoc; - } + + // Generate human-readable class description with JavaDoc inserted after signature + String description = generateClassDescription(type, relevantJavadoc); // Create ImportClassInfo (conforms to Copilot CodeSnippet format) ImportClassInfo info = new ImportClassInfo(uri, description); @@ -385,8 +394,10 @@ public static String getTypeUri(org.eclipse.jdt.core.IType type) { /** * Generate complete class description (natural language format, similar to JavaDoc) + * @param type the Java type to describe + * @param javadoc optional JavaDoc content to insert after signature (can be null or empty) */ - public static String generateClassDescription(org.eclipse.jdt.core.IType type) { + public static String generateClassDescription(org.eclipse.jdt.core.IType type, String javadoc) { StringBuilder description = new StringBuilder(); try { @@ -449,7 +460,12 @@ public static String generateClassDescription(org.eclipse.jdt.core.IType type) { description.append("Signature: ").append(signature).append("\n\n"); - // === 2. Constructors === + // === 2. JavaDoc (inserted after signature) === + if (isNotEmpty(javadoc)) { + description.append("JavaDoc:\n").append(javadoc).append("\n\n"); + } + + // === 3. Constructors === IMethod[] methods = type.getMethods(); List constructorSigs = new ArrayList<>(); @@ -467,13 +483,13 @@ public static String generateClassDescription(org.eclipse.jdt.core.IType type) { description.append("\n"); } - // === 3. Public methods (limited to first 10) === + // === 4. Public methods (limited to first 10) === List methodSigs = new ArrayList<>(); int methodCount = 0; for (IMethod method : methods) { if (!method.isConstructor() && org.eclipse.jdt.core.Flags.isPublic(method.getFlags())) { - if (methodCount < 10) { + if (methodCount < MAX_METHODS_TO_DISPLAY) { methodSigs.add(generateMethodSignature(method)); methodCount++; } else { @@ -487,19 +503,19 @@ public static String generateClassDescription(org.eclipse.jdt.core.IType type) { for (String sig : methodSigs) { description.append(" - ").append(sig).append("\n"); } - if (methodCount == 10 && methods.length > methodCount) { + if (methodCount == MAX_METHODS_TO_DISPLAY && methods.length > methodCount) { description.append(" - ... (more methods available)\n"); } description.append("\n"); } - // === 4. Public fields (limited to first 10) === + // === 5. Public fields (limited to first 10) === org.eclipse.jdt.core.IField[] fields = type.getFields(); List fieldSigs = new ArrayList<>(); int fieldCount = 0; for (org.eclipse.jdt.core.IField field : fields) { - if (org.eclipse.jdt.core.Flags.isPublic(field.getFlags()) && fieldCount < 10) { + if (org.eclipse.jdt.core.Flags.isPublic(field.getFlags()) && fieldCount < MAX_FIELDS_TO_DISPLAY) { fieldSigs.add(generateFieldSignature(field)); fieldCount++; } @@ -552,13 +568,13 @@ private static String extractRelevantJavaDocContent(org.eclipse.jdt.core.IType t } StringBuilder allCodeSnippets = new StringBuilder(); + Set seenCodeSnippets = new HashSet<>(); // 1. Extract markdown code blocks (```...```) - Pattern markdownPattern = Pattern.compile("(?s)```(?:java)?\\n?(.*?)```"); - Matcher markdownMatcher = markdownPattern.matcher(rawJavadoc); + Matcher markdownMatcher = MARKDOWN_CODE_PATTERN.matcher(rawJavadoc); while (markdownMatcher.find()) { String code = markdownMatcher.group(1).trim(); - if (isNotEmpty(code)) { + if (isNotEmpty(code) && seenCodeSnippets.add(code)) { allCodeSnippets.append("```java\n").append(code).append("\n```\n\n"); } } @@ -569,22 +585,20 @@ private static String extractRelevantJavaDocContent(org.eclipse.jdt.core.IType t cleanedForHtml = convertHtmlEntities(cleanedForHtml); // Priority 1:
 blocks (often contain well-formatted code)
-            Pattern prePattern = Pattern.compile("(?is)]*>(.*?)
"); - Matcher preMatcher = prePattern.matcher(cleanedForHtml); + Matcher preMatcher = HTML_PRE_PATTERN.matcher(cleanedForHtml); while (preMatcher.find()) { String code = preMatcher.group(1).replaceAll("(?i)]*>", "").replaceAll("(?i)
", "").trim(); - if (isNotEmpty(code)) { + if (isNotEmpty(code) && seenCodeSnippets.add(code)) { allCodeSnippets.append("```java\n").append(code).append("\n```\n\n"); } } // Priority 2: blocks (for inline snippets) - Pattern codePattern = Pattern.compile("(?is)]*>(.*?)"); - Matcher codeMatcher = codePattern.matcher(cleanedForHtml); + Matcher codeMatcher = HTML_CODE_PATTERN.matcher(cleanedForHtml); while (codeMatcher.find()) { String code = codeMatcher.group(1).trim(); - // Avoid adding duplicates that might be inside

-                if (isNotEmpty(code) && allCodeSnippets.indexOf(code) == -1) {
+                // Use HashSet for O(1) duplicate checking
+                if (isNotEmpty(code) && seenCodeSnippets.add(code)) {
                     allCodeSnippets.append("```java\n").append(code).append("\n```\n\n");
                 }
             }
@@ -804,12 +818,18 @@ public static String convertTypeSignature(String jdtSignature) {
         if (jdtSignature.startsWith("Q") && jdtSignature.endsWith(";")) {
             baseType = jdtSignature.substring(1, jdtSignature.length() - 1);
             baseType = baseType.replace('/', '.');
+            
+            // Handle generic type parameters (e.g., "QResult;")
+            baseType = processGenericTypes(baseType);
             baseType = simplifyTypeName(baseType);
         }
         // Handle fully qualified types (starts with L)
         else if (jdtSignature.startsWith("L") && jdtSignature.endsWith(";")) {
             baseType = jdtSignature.substring(1, jdtSignature.length() - 1);
             baseType = baseType.replace('/', '.');
+            
+            // Handle generic type parameters
+            baseType = processGenericTypes(baseType);
             baseType = simplifyTypeName(baseType);
         }
         // Handle primitive types
@@ -835,16 +855,78 @@ public static String convertTypeSignature(String jdtSignature) {
 
         return baseType;
     }
+    
+    /**
+     * Process generic type parameters in a type name
+     * Example: "Result" -> "Result"
+     */
+    private static String processGenericTypes(String typeName) {
+        if (typeName == null || !typeName.contains("<")) {
+            return typeName;
+        }
+        
+        StringBuilder result = new StringBuilder();
+        int i = 0;
+        
+        while (i < typeName.length()) {
+            char c = typeName.charAt(i);
+            
+            if (c == '<' || c == ',' || c == ' ') {
+                // Keep angle brackets, commas, and spaces
+                result.append(c);
+                i++;
+                
+                // Skip whitespace after comma or opening bracket
+                while (i < typeName.length() && typeName.charAt(i) == ' ') {
+                    result.append(' ');
+                    i++;
+                }
+                
+                // Check if next is a type parameter (Q or L prefix)
+                if (i < typeName.length()) {
+                    char next = typeName.charAt(i);
+                    
+                    if (next == 'Q' || next == 'L') {
+                        // Find the end of this type parameter (marked by ;)
+                        int endIndex = typeName.indexOf(';', i);
+                        if (endIndex != -1) {
+                            // Extract the type parameter and convert it
+                            String typeParam = typeName.substring(i + 1, endIndex);
+                            
+                            // Recursively process nested generics
+                            typeParam = processGenericTypes(typeParam);
+                            typeParam = simplifyTypeName(typeParam);
+                            
+                            result.append(typeParam);
+                            i = endIndex + 1; // Skip past the semicolon
+                        } else {
+                            result.append(next);
+                            i++;
+                        }
+                    } else {
+                        // Not a type parameter, just append
+                        result.append(next);
+                        i++;
+                    }
+                }
+            } else {
+                result.append(c);
+                i++;
+            }
+        }
+        
+        return result.toString();
+    }
 
     /**
      * Simplify fully qualified type name to just the simple name
      */
     private static String simplifyTypeName(String qualifiedName) {
-        if (qualifiedName == null || !qualifiedName.contains(".")) {
+        if (qualifiedName == null) {
             return qualifiedName;
         }
-        String[] parts = qualifiedName.split("\\.");
-        return parts[parts.length - 1];
+        int lastDot = qualifiedName.lastIndexOf('.');
+        return lastDot == -1 ? qualifiedName : qualifiedName.substring(lastDot + 1);
     }
 
     /**

From df2dce14ae40de4347849a68e9880a1d86de0182 Mon Sep 17 00:00:00 2001
From: wenyutang-ms 
Date: Fri, 17 Oct 2025 11:13:18 +0800
Subject: [PATCH 4/4] feat: add interface describ

---
 .../ext/core/parser/ContextResolver.java      | 160 +++++++++++++++++-
 1 file changed, 158 insertions(+), 2 deletions(-)

diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ContextResolver.java b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ContextResolver.java
index 6419e142..e0218b0c 100644
--- a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ContextResolver.java
+++ b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/parser/ContextResolver.java
@@ -672,7 +672,151 @@ private static String convertHtmlEntities(String text) {
     }
 
     /**
-     * Generate human-readable method signature
+     * Extract summary description from method JavaDoc
+     * Returns the first sentence or paragraph of the JavaDoc as a brief description
+     */
+    private static String extractMethodJavaDocSummary(IMethod method) {
+        try {
+            // Try to get JavaDoc from source
+            org.eclipse.jdt.core.ISourceRange javadocRange = method.getJavadocRange();
+            if (javadocRange == null) {
+                return "";
+            }
+            
+            String rawJavadoc = method.getCompilationUnit().getSource()
+                .substring(javadocRange.getOffset(), javadocRange.getOffset() + javadocRange.getLength());
+            
+            if (!isNotEmpty(rawJavadoc)) {
+                return "";
+            }
+            
+            // Clean the JavaDoc comment
+            String cleaned = cleanJavadocComment(rawJavadoc);
+            
+            // Extract the description (before any @param, @return, @throws tags)
+            String description = extractJavadocDescription(cleaned);
+            
+            // Get first sentence or limit length
+            String summary = getFirstSentenceOrLimit(description, 120);
+            
+            return summary;
+            
+        } catch (Exception e) {
+            // Silently fail and return empty string
+            return "";
+        }
+    }
+
+    /**
+     * Extract the main description part from JavaDoc (before @tags)
+     */
+    private static String extractJavadocDescription(String cleanedJavadoc) {
+        if (cleanedJavadoc == null || cleanedJavadoc.isEmpty()) {
+            return "";
+        }
+        
+        // Split into lines and extract description before @tags
+        String[] lines = cleanedJavadoc.split("\\n");
+        StringBuilder description = new StringBuilder();
+        
+        for (String line : lines) {
+            String trimmedLine = line.trim();
+            // Check if line starts with @tag
+            if (trimmedLine.startsWith("@")) {
+                break; // Stop at first tag
+            }
+            
+            // Skip empty lines at the beginning
+            if (description.length() == 0 && trimmedLine.isEmpty()) {
+                continue;
+            }
+            
+            if (description.length() > 0) {
+                description.append(" ");
+            }
+            description.append(trimmedLine);
+        }
+        
+        return description.toString().trim();
+    }
+
+    /**
+     * Get the first sentence or limit the text to maxLength characters
+     */
+    private static String getFirstSentenceOrLimit(String text, int maxLength) {
+        if (text == null || text.isEmpty()) {
+            return "";
+        }
+        
+        // Try to find the first sentence (ending with ., !, or ?)
+        int firstPeriod = text.indexOf(". ");
+        int firstExclamation = text.indexOf("! ");
+        int firstQuestion = text.indexOf("? ");
+        
+        int firstSentenceEnd = -1;
+        if (firstPeriod != -1) firstSentenceEnd = firstPeriod;
+        if (firstExclamation != -1 && (firstSentenceEnd == -1 || firstExclamation < firstSentenceEnd)) {
+            firstSentenceEnd = firstExclamation;
+        }
+        if (firstQuestion != -1 && (firstSentenceEnd == -1 || firstQuestion < firstSentenceEnd)) {
+            firstSentenceEnd = firstQuestion;
+        }
+        
+        // If we found a sentence ending and it's within reasonable length
+        if (firstSentenceEnd != -1 && firstSentenceEnd < maxLength) {
+            return text.substring(0, firstSentenceEnd + 1).trim();
+        }
+        
+        // Otherwise, limit to maxLength
+        if (text.length() > maxLength) {
+            // Try to cut at a word boundary
+            int lastSpace = text.lastIndexOf(' ', maxLength);
+            if (lastSpace > maxLength / 2) {
+                return text.substring(0, lastSpace).trim() + "...";
+            }
+            return text.substring(0, maxLength).trim() + "...";
+        }
+        
+        return text.trim();
+    }
+
+    /**
+     * Extract summary description from field JavaDoc
+     */
+    private static String extractFieldJavaDocSummary(org.eclipse.jdt.core.IField field) {
+        try {
+            // Try to get JavaDoc from source
+            org.eclipse.jdt.core.ISourceRange javadocRange = field.getJavadocRange();
+            if (javadocRange == null) {
+                return "";
+            }
+            
+            String rawJavadoc = field.getCompilationUnit().getSource()
+                .substring(javadocRange.getOffset(), javadocRange.getOffset() + javadocRange.getLength());
+            
+            if (!isNotEmpty(rawJavadoc)) {
+                return "";
+            }
+            
+            // Clean the JavaDoc comment
+            String cleaned = cleanJavadocComment(rawJavadoc);
+            
+            // Extract the description (before any @tags)
+            String description = extractJavadocDescription(cleaned);
+            
+            // Get first sentence or limit length
+            String summary = getFirstSentenceOrLimit(description, 120);
+            
+            return summary;
+            
+        } catch (Exception e) {
+            // Silently fail and return empty string
+            return "";
+        }
+    }
+
+    /**
+     * Generate human-readable method signature with JavaDoc description
      */
     public static String generateMethodSignature(IMethod method) {
         StringBuilder sb = new StringBuilder();
@@ -731,11 +875,17 @@ public static String generateMethodSignature(IMethod method) {
             return method.getElementName() + "(...)";
         }
         
+        // Extract JavaDoc description and prepend if exists
+        String javadocSummary = extractMethodJavaDocSummary(method);
+        if (isNotEmpty(javadocSummary)) {
+            return "// " + javadocSummary + "\n      " + sb.toString();
+        }
+        
         return sb.toString();
     }
 
     /**
-     * Generate human-readable field signature
+     * Generate human-readable field signature with JavaDoc description
      */
     public static String generateFieldSignature(org.eclipse.jdt.core.IField field) {
         StringBuilder sb = new StringBuilder();
@@ -766,6 +916,12 @@ public static String generateFieldSignature(org.eclipse.jdt.core.IField field) {
             return field.getElementName();
         }
         
+        // Extract JavaDoc description and prepend if exists
+        String javadocSummary = extractFieldJavaDocSummary(field);
+        if (isNotEmpty(javadocSummary)) {
+            return "// " + javadocSummary + "\n      " + sb.toString();
+        }
+        
         return sb.toString();
     }