diff --git a/README.md b/README.md index 341985ff..ac4954d9 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ You can tell that the glob pattern is supported. And here's more - you can incl ## Requirements -- VS Code (version 1.83.1+) +- VS Code (version 1.95.0+) - [Language Support for Java by Red Hat](https://marketplace.visualstudio.com/items?itemName=redhat.java) diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/plugin.xml b/jdtls.ext/com.microsoft.jdtls.ext.core/plugin.xml index a469ec92..4966a319 100644 --- a/jdtls.ext/com.microsoft.jdtls.ext.core/plugin.xml +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/plugin.xml @@ -12,6 +12,7 @@ + JDK_PREFIXES = new HashSet<>(); + static { + JDK_PREFIXES.add("java."); + JDK_PREFIXES.add("javax."); + JDK_PREFIXES.add("jdk."); + JDK_PREFIXES.add("sun."); + JDK_PREFIXES.add("com.sun."); + JDK_PREFIXES.add("org.xml."); + JDK_PREFIXES.add("org.w3c."); + JDK_PREFIXES.add("jakarta."); // Jakarta EE (post Java EE) + } + + /** + * Get the classified import list of a Java file. + * This is a lightweight AST-only operation — it reads import declarations + * without doing any type resolution (findType) or classpath resolution. + * + * Typical response time: < 5ms + * + * @param arguments List containing the file URI as the first element + * @param monitor Progress monitor for cancellation support + * @return FileImportsResult with classified imports + */ + public static FileImportsResult getFileImports(List arguments, IProgressMonitor monitor) { + FileImportsResult result = new FileImportsResult(); + result.imports = new ArrayList<>(); + result.staticImports = new ArrayList<>(); + + if (arguments == null || arguments.isEmpty()) { + result.error = "No arguments provided"; + return result; + } + + try { + String fileUri = (String) arguments.get(0); + if (fileUri == null || fileUri.trim().isEmpty()) { + result.error = "Invalid file URI"; + return result; + } + + // Resolve compilation unit from URI — this is fast, just a model lookup + java.net.URI uri = JDTUtils.toURI(fileUri); + ICompilationUnit compilationUnit = JDTUtils.resolveCompilationUnit(uri); + + if (compilationUnit == null || !compilationUnit.exists()) { + result.error = "File not found or not a Java file: " + fileUri; + return result; + } + + // Get project-relative file path (strip the leading project segment + // from the Eclipse workspace-relative path, e.g. "/my-project/src/Foo.java" → "src/Foo.java") + IJavaProject javaProject = compilationUnit.getJavaProject(); + result.file = compilationUnit.getPath().removeFirstSegments(1).toString(); + + // Collect project source package names for classification + Set projectPackages = collectProjectPackages(javaProject); + + // Read import declarations — pure AST operation, no type resolution + IImportDeclaration[] imports = compilationUnit.getImports(); + if (imports == null || imports.length == 0) { + return result; // No imports, return empty (not an error) + } + + for (IImportDeclaration imp : imports) { + if (monitor.isCanceled()) { + break; + } + + String name = imp.getElementName(); + boolean isStatic = Flags.isStatic(imp.getFlags()); + boolean isOnDemand = name.endsWith(".*"); + + if (isStatic) { + StaticImportEntry entry = new StaticImportEntry(); + entry.name = name; + entry.memberKind = "unknown"; // Would need findType to know — skip + entry.source = classifyByPackageName(name, projectPackages); + result.staticImports.add(entry); + } else { + ImportEntry entry = new ImportEntry(); + entry.name = name; + entry.kind = "unknown"; // Would need findType to know — skip + entry.source = classifyByPackageName(name, projectPackages); + entry.artifact = null; // Would need classpath attributes — skip for now + entry.isOnDemand = isOnDemand; + result.imports.add(entry); + } + } + + return result; + + } catch (Exception e) { + JdtlsExtActivator.logException("Error in getFileImports", e); + result.error = "Exception: " + e.getMessage(); + return result; + } + } + + /** + * Classify an import by its package name prefix. + * This is a heuristic — no type resolution involved. + * + * @param qualifiedName the fully qualified name of the import + * @param projectPackages set of package names found in the project's source roots + * @return "jdk", "project", or "external" + */ + private static String classifyByPackageName(String qualifiedName, Set projectPackages) { + // Check JDK + for (String prefix : JDK_PREFIXES) { + if (qualifiedName.startsWith(prefix)) { + return "jdk"; + } + } + + // Check project packages + String packageName = getPackageName(qualifiedName); + if (packageName != null && projectPackages.contains(packageName)) { + return "project"; + } + + // Check if any project package is a prefix of this import + for (String projPkg : projectPackages) { + if (qualifiedName.startsWith(projPkg + ".")) { + return "project"; + } + } + + return "external"; + } + + /** + * Get the package name from a fully qualified name. + * e.g., "com.example.model.Order" → "com.example.model" + * "com.example.model.*" → "com.example.model" + */ + private static String getPackageName(String qualifiedName) { + if (qualifiedName == null) { + return null; + } + // Handle wildcard imports + if (qualifiedName.endsWith(".*")) { + return qualifiedName.substring(0, qualifiedName.length() - 2); + } + int lastDot = qualifiedName.lastIndexOf('.'); + if (lastDot > 0) { + return qualifiedName.substring(0, lastDot); + } + return null; + } + + /** + * Collect all package names that exist in the project's source roots. + * This uses getPackageFragmentRoots(K_SOURCE) which is fast — it reads + * the project model, not the filesystem. + */ + private static Set collectProjectPackages(IJavaProject javaProject) { + Set packages = new HashSet<>(); + if (javaProject == null) { + return packages; + } + + try { + IPackageFragmentRoot[] roots = javaProject.getPackageFragmentRoots(); + for (IPackageFragmentRoot root : roots) { + if (root.getKind() == IPackageFragmentRoot.K_SOURCE) { + org.eclipse.jdt.core.IJavaElement[] children = root.getChildren(); + for (org.eclipse.jdt.core.IJavaElement child : children) { + if (child instanceof org.eclipse.jdt.core.IPackageFragment) { + String pkgName = child.getElementName(); + if (pkgName != null && !pkgName.isEmpty()) { + packages.add(pkgName); + } + } + } + } + } + } catch (Exception e) { + // Non-critical — fall back to treating everything as external + JdtlsExtActivator.logException("Error collecting project packages", e); + } + + return packages; + } +} diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/CommandHandler.java b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/CommandHandler.java index 390dda27..49db346f 100644 --- a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/CommandHandler.java +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/CommandHandler.java @@ -41,6 +41,8 @@ public Object executeCommand(String commandId, List arguments, IProgress return ProjectCommand.getImportClassContent(arguments, monitor); case "java.project.getDependencies": return ProjectCommand.getProjectDependencies(arguments, monitor); + case "java.project.getFileImports": + return AiContextCommand.getFileImports(arguments, monitor); default: break; } diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/model/ClassDetailResult.java b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/model/ClassDetailResult.java new file mode 100644 index 00000000..1c050cdc --- /dev/null +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/model/ClassDetailResult.java @@ -0,0 +1,49 @@ +package com.microsoft.jdtls.ext.core.model; + +import java.util.List; + +/** + * L1: Detailed class information. + * AI calls this for specific classes it needs to understand, not for all imports. + */ +public class ClassDetailResult { + + public String qualifiedName; // "com.example.model.Order" + public String kind; // "class" | "interface" | "enum" | "annotation" + public String uri; // file URI (for project source) or jar URI + public String source; // "project" | "external" | "jdk" + public String artifact; // GAV for external: "com.google.code.gson:gson:2.10.1" + + public String signature; // "public class Order implements Serializable" + public String superClass; // "java.lang.Object" (null if Object) + public List interfaces; // ["java.io.Serializable"] + public List annotations; // ["@Entity", "@Table(name = \"orders\")"] + + public String javadocSummary; // First sentence only, null if none + + public List constructors; // ["Order()", "Order(String orderId, Customer customer)"] + public List methods; // ["String getOrderId()", "void setStatus(OrderStatus)"] + public List fields; // ["private String orderId", "private List items"] + + public int totalMethodCount; // actual total (methods list may be truncated) + public int totalFieldCount; // actual total + + public String error; // null if success + + /** + * Builder-style static factories for common cases + */ + public static ClassDetailResult notFound(String qualifiedName) { + ClassDetailResult r = new ClassDetailResult(); + r.qualifiedName = qualifiedName; + r.error = "Type not found: " + qualifiedName; + return r; + } + + public static ClassDetailResult error(String qualifiedName, String message) { + ClassDetailResult r = new ClassDetailResult(); + r.qualifiedName = qualifiedName; + r.error = message; + return r; + } +} diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/model/DependencyDetailsResult.java b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/model/DependencyDetailsResult.java new file mode 100644 index 00000000..dc373de8 --- /dev/null +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/model/DependencyDetailsResult.java @@ -0,0 +1,37 @@ +package com.microsoft.jdtls.ext.core.model; + +import java.util.List; + +/** + * L1: Detailed dependency information with query filtering. + * AI calls this when it needs to investigate specific dependencies. + */ +public class DependencyDetailsResult { + + public List dependencies; + public String error; // null if success + + public static class DependencyEntry { + public String groupId; // "com.google.code.gson" + public String artifactId; // "gson" + public String version; // "2.10.1" + public String scope; // "compile" | "test" | "runtime" | "provided" | "system" + public boolean isDirect; // true = declared in pom.xml/build.gradle + public String broughtBy; // for transitive: "com.google.guava:guava:32.1.3-jre" + public String jarFileName; // "gson-2.10.1.jar" + + public DependencyEntry() {} + + public DependencyEntry(String groupId, String artifactId, String version, + String scope, boolean isDirect, String broughtBy, + String jarFileName) { + this.groupId = groupId; + this.artifactId = artifactId; + this.version = version; + this.scope = scope; + this.isDirect = isDirect; + this.broughtBy = broughtBy; + this.jarFileName = jarFileName; + } + } +} diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/model/FileImportsResult.java b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/model/FileImportsResult.java new file mode 100644 index 00000000..5ef31fdc --- /dev/null +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/model/FileImportsResult.java @@ -0,0 +1,46 @@ +package com.microsoft.jdtls.ext.core.model; + +import java.util.List; + +/** + * L0: Import list for a Java file. + * Returns classified imports without expanding class details. + */ +public class FileImportsResult { + + public String file; // project-relative file path (e.g. "src/main/java/com/example/Foo.java") + public List imports; + public List staticImports; + public String error; // null if success + + public static class ImportEntry { + public String name; // fully qualified name: "com.example.model.Order" + public String kind; // "class" | "interface" | "enum" | "annotation" | "unknown" + public String source; // "project" | "external" | "jdk" + public String artifact; // only for "external": "spring-context", null for others + public boolean isOnDemand; // true for wildcard imports (e.g. "import com.example.model.*") + + public ImportEntry() {} + + public ImportEntry(String name, String kind, String source, String artifact) { + this.name = name; + this.kind = kind; + this.source = source; + this.artifact = artifact; + } + } + + public static class StaticImportEntry { + public String name; // "org.junit.Assert.assertEquals" + public String memberKind; // "method" | "field" | "unknown" + public String source; // "project" | "external" | "jdk" + + public StaticImportEntry() {} + + public StaticImportEntry(String name, String memberKind, String source) { + this.name = name; + this.memberKind = memberKind; + this.source = source; + } + } +} diff --git a/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/model/ProjectContextResult.java b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/model/ProjectContextResult.java new file mode 100644 index 00000000..113c0c9f --- /dev/null +++ b/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/model/ProjectContextResult.java @@ -0,0 +1,42 @@ +package com.microsoft.jdtls.ext.core.model; + +/** + * Structured result models for Java Context Tools. + * These models are designed for AI consumption — small, structured, layered. + * + * Design principles: + * 1. Each result should serialize to < 200 tokens of JSON + * 2. Use structured fields instead of freeform text + * 3. Only include information the AI actually needs at this granularity level + */ + +import java.util.List; + +/** + * L0: Project-level context overview. + * First thing AI should request when entering a Java project. + */ +public class ProjectContextResult { + + public ProjectMeta project; + public DependencySummary dependencies; + public List projectReferences; + public String error; // null if success + + public static class ProjectMeta { + public String name; + public String buildTool; // "Maven" | "Gradle" | "Unknown" + public String javaVersion; // compiler compliance level + public String sourceLevel; + public String targetLevel; + public List sourceRoots; // relative paths: ["src/main/java", "src/test/java"] + public String moduleName; // Java module name, null if not modular + } + + public static class DependencySummary { + public int total; + public int directCount; + public int transitiveCount; + public List direct; // GAV strings: ["group:artifact:version", ...] + } +} diff --git a/package-lock.json b/package-lock.json index 98009fcf..7a306550 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "@types/mocha": "^9.1.1", "@types/node": "20.x", "@types/semver": "^7.3.13", - "@types/vscode": "1.83.1", + "@types/vscode": "1.95.0", "@vscode/test-electron": "^2.4.1", "copy-webpack-plugin": "^11.0.0", "glob": "^7.2.3", @@ -42,7 +42,7 @@ "webpack-cli": "^4.10.0" }, "engines": { - "vscode": "^1.83.1" + "vscode": "^1.95.0" } }, "node_modules/@babel/code-frame": { @@ -843,10 +843,11 @@ "dev": true }, "node_modules/@types/vscode": { - "version": "1.83.1", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.83.1.tgz", - "integrity": "sha512-BHu51NaNKOtDf3BOonY3sKFFmZKEpRkzqkZVpSYxowLbs5JqjOQemYFob7Gs5rpxE5tiGhfpnMpcdF/oKrLg4w==", - "dev": true + "version": "1.95.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.95.0.tgz", + "integrity": "sha512-0LBD8TEiNbet3NvWsmn59zLzOFu/txSlGxnv5yAFHCrhG9WvAnR3IvfHzMOs2aeWqgvNjq9pO99IUw8d3n+unw==", + "dev": true, + "license": "MIT" }, "node_modules/@types/ws": { "version": "8.5.10", @@ -6891,9 +6892,9 @@ "dev": true }, "@types/vscode": { - "version": "1.83.1", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.83.1.tgz", - "integrity": "sha512-BHu51NaNKOtDf3BOonY3sKFFmZKEpRkzqkZVpSYxowLbs5JqjOQemYFob7Gs5rpxE5tiGhfpnMpcdF/oKrLg4w==", + "version": "1.95.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.95.0.tgz", + "integrity": "sha512-0LBD8TEiNbet3NvWsmn59zLzOFu/txSlGxnv5yAFHCrhG9WvAnR3IvfHzMOs2aeWqgvNjq9pO99IUw8d3n+unw==", "dev": true }, "@types/ws": { diff --git a/package.json b/package.json index eff7e216..35c08426 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,9 @@ "explorer" ], "engines": { - "vscode": "^1.83.1" + "vscode": "^1.95.0" }, + "enabledApiProposals": ["chatPromptFiles"], "repository": { "type": "git", "url": "https://github.com/Microsoft/vscode-java-dependency.git" @@ -48,6 +49,162 @@ "javaExtensions": [ "./server/com.microsoft.jdtls.ext.core-0.24.1.jar" ], + "languageModelTools": [ + { + "name": "lsp_java_getFileStructure", + "toolReferenceName": "javaFileStructure", + "modelDescription": "Get the structure (classes, methods, fields, inner classes) of a Java file. Returns a hierarchical outline with symbol kinds and line ranges. Use this first when you need to understand a file's layout before reading specific sections.", + "displayName": "Java: Get File Structure", + "canBeReferencedInPrompt": true, + "icon": "$(symbol-class)", + "when": "config.vscode-java-dependency.enableLspTools && javaLSReady", + "inputSchema": { + "type": "object", + "properties": { + "uri": { + "type": "string", + "description": "Java file path (workspace-relative or full URI)" + } + }, + "required": ["uri"] + } + }, + { + "name": "lsp_java_findSymbol", + "toolReferenceName": "javaFindSymbol", + "modelDescription": "Search for Java symbols (classes, interfaces, methods, fields) across the workspace by name. Supports partial/fuzzy matching. Use this to find where a class or method is defined.", + "displayName": "Java: Find Symbol", + "canBeReferencedInPrompt": true, + "icon": "$(search)", + "when": "config.vscode-java-dependency.enableLspTools && javaLSReady", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Symbol name or pattern to search for" + }, + "limit": { + "type": "number", + "description": "Maximum results (default: 20, max: 50)" + } + }, + "required": ["query"] + } + }, + { + "name": "lsp_java_getFileImports", + "toolReferenceName": "javaFileImports", + "modelDescription": "Get all import statements from a Java file, classified by source (jdk/project/external).", + "displayName": "Java: Get File Imports", + "canBeReferencedInPrompt": true, + "icon": "$(references)", + "when": "config.vscode-java-dependency.enableLspTools && javaLSReady", + "inputSchema": { + "type": "object", + "properties": { + "uri": { + "type": "string", + "description": "Java file path (workspace-relative or full URI)" + } + }, + "required": ["uri"] + } + }, + { + "name": "lsp_java_getTypeAtPosition", + "toolReferenceName": "javaTypeAtPosition", + "modelDescription": "Get the compiler-resolved type signature at a specific position. Returns fully qualified type, method signature, or field declaration. Use this when you see var, lambdas, or generics and need the exact type.", + "displayName": "Java: Get Type at Position", + "canBeReferencedInPrompt": true, + "icon": "$(symbol-field)", + "when": "config.vscode-java-dependency.enableLspTools && javaLSReady", + "inputSchema": { + "type": "object", + "properties": { + "uri": { + "type": "string", + "description": "Java file path (workspace-relative or full URI)" + }, + "line": { + "type": "number", + "description": "Zero-based line number" + }, + "character": { + "type": "number", + "description": "Zero-based character offset" + } + }, + "required": ["uri", "line", "character"] + } + }, + { + "name": "lsp_java_getCallHierarchy", + "toolReferenceName": "javaCallHierarchy", + "modelDescription": "Get incoming callers or outgoing callees of a method. PREFER THIS over grep_search when you need to find which methods call a specific method — returns precise call sites without noise from comments, imports, or string matches. Use 'incoming' for callers, 'outgoing' for callees.", + "displayName": "Java: Get Call Hierarchy", + "canBeReferencedInPrompt": true, + "icon": "$(call-incoming)", + "when": "config.vscode-java-dependency.enableLspTools && javaLSReady", + "inputSchema": { + "type": "object", + "properties": { + "uri": { + "type": "string", + "description": "Java file path (workspace-relative or full URI)" + }, + "line": { + "type": "number", + "description": "Zero-based line number of the method" + }, + "character": { + "type": "number", + "description": "Zero-based character offset within the method name" + }, + "direction": { + "type": "string", + "enum": ["incoming", "outgoing"], + "default": "incoming", + "description": "'incoming' for callers, 'outgoing' for callees" + } + }, + "required": ["uri", "line", "character", "direction"] + } + }, + { + "name": "lsp_java_getTypeHierarchy", + "toolReferenceName": "javaTypeHierarchy", + "modelDescription": "Get supertypes or subtypes/implementors of a type. PREFER THIS over grep_search('extends|implements') — returns only actual inheritance relationships. Use 'supertypes' for parents, 'subtypes' for children.", + "displayName": "Java: Get Type Hierarchy", + "canBeReferencedInPrompt": true, + "icon": "$(type-hierarchy)", + "when": "config.vscode-java-dependency.enableLspTools && javaLSReady", + "inputSchema": { + "type": "object", + "properties": { + "uri": { + "type": "string", + "description": "Java file path (workspace-relative or full URI)" + }, + "line": { + "type": "number", + "description": "Zero-based line number of the type" + }, + "character": { + "type": "number", + "description": "Zero-based character offset within the type name" + }, + "direction": { + "type": "string", + "enum": ["supertypes", "subtypes"], + "default": "supertypes", + "description": "'supertypes' for parents, 'subtypes' for children" + } + }, + "required": ["uri", "line", "character", "direction"] + } + } + ], "commands": [ { "command": "java.project.create", @@ -360,6 +517,16 @@ "type": "boolean", "description": "%configuration.java.project.explorer.showNonJavaResources%", "default": true + }, + "vscode-java-dependency.enableLspTools": { + "type": "boolean", + "scope": "application", + "description": "%configuration.vscode-java-dependency.enableLspTools.description%", + "default": false, + "tags": ["experimental"], + "experiment": { + "mode": "startup" + } } } }, @@ -1078,6 +1245,18 @@ } } } + ], + "chatSkills": [ + { + "path": "./resources/skills/java-lsp-tools/SKILL.md", + "when": "config.vscode-java-dependency.enableLspTools && javaLSReady" + } + ], + "chatInstructions": [ + { + "path": "./resources/instruments/javaLspContext.instructions.md", + "when": "config.vscode-java-dependency.enableLspTools && javaLSReady" + } ] }, "scripts": { @@ -1097,7 +1276,7 @@ "@types/mocha": "^9.1.1", "@types/node": "20.x", "@types/semver": "^7.3.13", - "@types/vscode": "1.83.1", + "@types/vscode": "1.95.0", "@vscode/test-electron": "^2.4.1", "copy-webpack-plugin": "^11.0.0", "glob": "^7.2.3", diff --git a/package.nls.json b/package.nls.json index 95052e18..a163a415 100644 --- a/package.nls.json +++ b/package.nls.json @@ -50,6 +50,7 @@ "configuration.java.project.exportJar.targetPath.customization": "The output path of the exported jar. Leave it empty if you want to manually pick the output location.", "configuration.java.project.exportJar.targetPath.workspaceFolder": "Export the jar file into the workspace folder. Its name is the same as the folder's.", "configuration.java.project.exportJar.targetPath.select": "Select output location manually when exporting the jar file.", + "configuration.vscode-java-dependency.enableLspTools.description": "Enable LSP tools for Java projects.", "taskDefinitions.java.project.exportJar.label": "The label of export jar task.", "taskDefinitions.java.project.exportJar.elements": "The content list of the exported jar.", "taskDefinitions.java.project.exportJar.mainClass": "The main class in the manifest of the exported jar.", diff --git a/resources/instruments/javaLspContext.instructions.md b/resources/instruments/javaLspContext.instructions.md new file mode 100644 index 00000000..492598c7 --- /dev/null +++ b/resources/instruments/javaLspContext.instructions.md @@ -0,0 +1,51 @@ +--- +description: Unconditionally loaded for all Java projects. Provides Java LSP tools (lsp_java_*) for compiler-accurate code navigation — significantly faster and more precise than grep_search, search_subagent, or read_file for locating Java symbols, callers, implementations, and types. +applyTo: '*' +--- + +# Java LSP Tools — Mandatory Initialization + +This workspace contains Java code. You have access to 6 compiler-accurate LSP tools that are **faster and more precise** than generic search for Java code navigation. These tools return structured results in ~20-100 tokens vs ~500-3000 tokens from grep_search, with zero false positives. + +## Step 1: Load Tools (REQUIRED — do this FIRST) + +Before ANY operation, load all 6 tools by calling `tool_search_tool_regex` **twice** (the API returns at most 5 tools per call): + +**Call 1** — basic navigation tools: +``` +lsp_java_findSymbol|lsp_java_getFileStructure|lsp_java_getFileImports +``` + +**Call 2** — hierarchy and type tools: +``` +lsp_java_getCallHierarchy|lsp_java_getTypeHierarchy|lsp_java_getTypeAtPosition +``` + +All 6 tools must be returned across the two calls. If any are missing, retry that call once. Do NOT use `grep_search`, `read_file`, `semantic_search`, `search_subagent`, or `runSubagent` on `.java` files until all tools are loaded. + +## Step 2: Always Prefer LSP Tools for Java + +| Task | ✅ Use | ❌ Not | +|---|---|---| +| Find class/method/field definition | `lsp_java_findSymbol` | `grep_search`, `semantic_search`, `search_subagent` | +| See file outline (methods, fields) | `lsp_java_getFileStructure` | `read_file` full scan | +| Find all callers of a method | `lsp_java_getCallHierarchy("incoming")` | `grep_search`, `search_subagent` | +| Find all implementations | `lsp_java_getTypeHierarchy("subtypes")` | `grep_search`, `search_subagent` | +| Resolve var/lambda/generic type | `lsp_java_getTypeAtPosition` | Guessing from code | +| Check imports | `lsp_java_getFileImports` | `read_file` first lines | +| Explore Java codebase structure | `lsp_java_*` tools above | `search_subagent`, `runSubagent` | + +**Keep using `grep_search`** for: non-Java files (xml, yaml, gradle), string literals, comments. + +**Self-check**: Before every `.java` operation, ask: "Is there a `lsp_java_*` tool for this?" If yes, use it instead of `grep_search`, `search_subagent`, or `runSubagent`. + +## Typical Workflow + +**findSymbol → getFileStructure → targeted tool → read_file (specific lines only)** + +## Fallback + +- `findSymbol` returns empty → retry with shorter keyword, then fall back to `grep_search` +- Path error → use `findSymbol` to discover correct path first +- Tool error / jdtls not ready → fall back to `read_file` + `grep_search`, don't retry more than once + diff --git a/resources/skills/java-lsp-tools/SKILL.md b/resources/skills/java-lsp-tools/SKILL.md new file mode 100644 index 00000000..6fd6633c --- /dev/null +++ b/resources/skills/java-lsp-tools/SKILL.md @@ -0,0 +1,93 @@ +--- +name: java-lsp-tools +description: Compiler-accurate Java code intelligence tools powered by the Java Language Server. ALWAYS load this skill when the workspace contains Java, Maven (pom.xml), or Gradle (build.gradle) projects. Use these tools to find symbol definitions, get type/call hierarchies, resolve types, inspect file outlines, and check imports in Java source files. Prefer over grep_search or read_file for any Java code navigation, understanding, debugging, or refactoring task. +--- + +# Java LSP Tools + +Compiler-accurate Java code intelligence via the Java Language Server (jdtls). These 6 tools provide structured, low-token answers that are more precise than `grep_search` or `read_file` for Java code. + +## Activation + +These tools are **deferred** and must be discovered before first use. Activate all 6 tools at once with `tool_search_tool_regex` using pattern: + +`lsp_java_findSymbol|lsp_java_getFileStructure|lsp_java_getCallHierarchy|lsp_java_getTypeHierarchy|lsp_java_getTypeAtPosition|lsp_java_getFileImports` + +You only need to do this **once per session**. + +## When to Replace grep_search + +For Java source files, **always prefer these tools over generic alternatives**: + +| You're doing... | Use instead | Why | +|---|---|---| +| Find where a class/method is defined | `lsp_java_findSymbol` | ~60 tokens vs ~500 for grep (no comment/import noise) | +| Find all callers of a method | `lsp_java_getCallHierarchy("incoming")` | ~80 tokens vs ~3000 for grep (precise call sites only) | +| Find all implementations of an interface | `lsp_java_getTypeHierarchy("subtypes")` | ~60 tokens vs ~1000 for grep | +| Check a `var`/lambda/generic type | `lsp_java_getTypeAtPosition` | ~20 tokens vs guessing wrong | +| Search in non-Java files (xml, yaml, gradle) | Keep using `grep_search` | lsp_java_* tools only work on Java source | +| Search for string literals or comments | Keep using `grep_search` | lsp_java_* tools return symbol definitions only | + +**Rule of thumb**: If you're searching for a Java symbol name in `.java` files, there is almost always a `lsp_java_*` tool that returns more precise results with fewer tokens than `grep_search`. + +## Anti-patterns (Avoid these) + +❌ **Don't**: Use `grep_search("decodeLbs")` to find who calls `decodeLbs()` + - Returns 8+ matches including declaration, comments, imports → ~3000 output tokens + +✅ **Do**: Use `lsp_java_getCallHierarchy(uri, line, char, "incoming")` + - Returns only actual call sites → ~80 output tokens + +❌ **Don't**: Use `grep_search("class.*extends BaseDecoder")` to find subclasses +✅ **Do**: Use `lsp_java_getTypeHierarchy(uri, line, char, "subtypes")` + +❌ **Don't**: Read entire 1000+ line file to understand its structure +✅ **Do**: Use `lsp_java_getFileStructure(uri)` first, then `read_file` on specific line ranges + +## Tools + +All tools accept **workspace-relative paths** (e.g. `src/main/java/com/example/MyClass.java`) or full file URIs. All return structured JSON, each response < 200 tokens. + +### `lsp_java_getFileStructure` +Get hierarchical outline (classes, methods, fields) with line ranges. +Input: `{ uri }` → Output: symbol tree with `[L start-end]` ranges (~100 tokens) + +### `lsp_java_findSymbol` +Search for symbol definitions by name across the workspace. Supports partial/fuzzy matching. +Input: `{ query }` → Output: up to 20 results with `{ name, kind, location }` (~60 tokens) + +### `lsp_java_getFileImports` +Get all imports classified by source (jdk/project/external). +Input: `{ uri }` → Output: classified import list (~80 tokens) + +### `lsp_java_getTypeAtPosition` +Get compiler-resolved type signature at a specific position. +Input: `{ uri, line, character }` (0-based) → Output: fully resolved type (~20 tokens) + +### `lsp_java_getCallHierarchy` +Find all callers (incoming) or callees (outgoing) of a method. +Input: `{ uri, line, character, direction }` (0-based, direction: `"incoming"` | `"outgoing"`) → Output: list of `{ name, detail, location }` (~80 tokens) + +### `lsp_java_getTypeHierarchy` +Find supertypes or subtypes/implementors of a type. +Input: `{ uri, line, character, direction }` (0-based, direction: `"supertypes"` | `"subtypes"`) → Output: list of `{ name, kind, location }` (~60 tokens) + +## Common Workflows + +Most tasks follow the pattern: **findSymbol → getFileStructure → targeted tool**. + +| Scenario | Workflow | Trigger | +|---|---|---| +| Debug a bug | `findSymbol` → `getFileStructure` → `read_file` (buggy method) → **`getCallHierarchy("incoming")`** → `read_file` (caller context) | When you found the buggy method and need to know ALL callers | +| Analyze impact | `findSymbol` → `getFileStructure` → `getCallHierarchy("incoming")` | Before editing a method, check who depends on it | +| Understand inheritance | `findSymbol` → `getTypeHierarchy("subtypes")` | When you see a base class and need all implementations | +| Check dependencies | `getFileImports` → `findSymbol` (dependency) → `getFileStructure` | When understanding external library usage | +| Resolve type ambiguity | `getFileStructure` → `getTypeAtPosition` | When you see `var`, generics, or lambda and need exact type | + +## Fallback + +- **`lsp_java_findSymbol` returns empty**: + - Symbol may not exist yet → switch to `read_file` + `grep_search` to confirm, then create it + - Spelling/query too specific → retry once with a shorter keyword (e.g. `"UserSvc"` instead of `"UserServiceImpl"`) +- **Path error** (e.g. "Unable to resolve nonexistent file"): Use `lsp_java_findSymbol` to discover the correct file path first, then retry. +- **Tool error / empty result** (jdtls not ready, file not in project): Fall back to `read_file` + `grep_search`. Don't retry more than once. diff --git a/src/commands.ts b/src/commands.ts index 0e7a69cd..5331ab3e 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -138,6 +138,8 @@ export namespace Commands { export const JAVA_PROJECT_GET_IMPORT_CLASS_CONTENT = "java.project.getImportClassContent"; + export const JAVA_PROJECT_GET_FILE_IMPORTS = "java.project.getFileImports"; + export const JAVA_UPGRADE_WITH_COPILOT = "_java.upgradeWithCopilot"; /** diff --git a/src/copilot/tools/javaContextTools.ts b/src/copilot/tools/javaContextTools.ts new file mode 100644 index 00000000..423f6eac --- /dev/null +++ b/src/copilot/tools/javaContextTools.ts @@ -0,0 +1,378 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Java Context Tools — First Batch (Zero-Blocking) + * + * These 6 tools are all non-blocking after jdtls is ready: + * 1. lsp_java_getFileStructure — LSP documentSymbol + * 2. lsp_java_findSymbol — LSP workspaceSymbol + * 3. lsp_java_getFileImports — jdtls AST-only command (no type resolution) + * 4. lsp_java_getTypeAtPosition — LSP hover (post-processed) + * 5. lsp_java_getCallHierarchy — LSP call hierarchy + * 6. lsp_java_getTypeHierarchy — LSP type hierarchy + * + * Design principles: + * - Each tool returns < 200 tokens + * - Structured JSON output + * - No classpath resolution, no dependency download + */ + +import * as vscode from "vscode"; +import { Commands } from "../../commands"; + +// Hard caps to keep tool responses within the < 200 token budget. +const MAX_SYMBOL_DEPTH = 3; +const MAX_SYMBOL_NODES = 80; +const MAX_CALL_RESULTS = 50; +const MAX_TYPE_RESULTS = 50; +const MAX_IMPORTS = 50; + +function toResult(data: unknown): vscode.LanguageModelToolResult { + const text = typeof data === "string" ? data : JSON.stringify(data, null, 2); + return new vscode.LanguageModelToolResult([ + new vscode.LanguageModelTextPart(text), + ]); +} + +/** + * Resolve a file path to a vscode.Uri. + * Accepts: + * - Full file URI: "file:///home/user/project/src/Main.java" + * - Relative path: "src/main/java/Main.java" + * - Absolute path: "/home/user/project/src/Main.java" or "C:\\Users\\...\\Main.java" + * + * Relative paths are resolved against the first workspace folder. + * The resolved URI must use the file: scheme and fall under a workspace folder. + */ +function resolveFileUri(input: string): vscode.Uri { + const folders = vscode.workspace.workspaceFolders; + if (!folders || folders.length === 0) { + throw new Error("No workspace folder is open."); + } + + let uri: vscode.Uri; + + // Full URI — only allow the file: scheme + if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(input) || /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(input)) { + uri = vscode.Uri.parse(input); + if (uri.scheme !== "file") { + throw new Error(`Unsupported URI scheme "${uri.scheme}". Only file: URIs are allowed.`); + } + } else if (input.startsWith("/") || /^[a-zA-Z]:[/\\]/.test(input)) { + // Absolute path (Unix or Windows) + uri = vscode.Uri.file(input); + } else { + // Relative path — resolve against first workspace folder + uri = vscode.Uri.joinPath(folders[0].uri, input); + } + + // Ensure the resolved path is under a workspace folder + const resolvedPath = uri.fsPath.toLowerCase(); + const isUnderWorkspace = folders.some(folder => { + const folderPath = folder.uri.fsPath.toLowerCase(); + return resolvedPath === folderPath || resolvedPath.startsWith(folderPath + (process.platform === "win32" ? "\\" : "/")); + }); + if (!isUnderWorkspace) { + throw new Error("The resolved path is outside the current workspace."); + } + + return uri; +} + +// ============================================================ +// Tool 1: lsp_java_getFileStructure (LSP — Document Symbol) +// ============================================================ + +interface FileStructureInput { + uri: string; +} + +const fileStructureTool: vscode.LanguageModelTool = { + async invoke(options, _token) { + const uri = resolveFileUri(options.input.uri); + const symbols = await vscode.commands.executeCommand( + "vscode.executeDocumentSymbolProvider", uri, + ); + if (!symbols || symbols.length === 0) { + return toResult({ error: "No symbols found. The file may not be recognized by the Java language server." }); + } + const counter = { count: 0 }; + const result = symbolsToJson(symbols, 0, counter); + const truncated = counter.count >= MAX_SYMBOL_NODES; + return toResult({ symbols: result, ...(truncated && { truncated: true }) }); + }, +}; + +interface SymbolNode { + name: string; + kind: string; + range: string; + detail?: string; + children?: SymbolNode[]; +} + +function symbolsToJson(symbols: vscode.DocumentSymbol[], depth: number, counter: { count: number }): SymbolNode[] { + const result: SymbolNode[] = []; + for (const s of symbols) { + if (counter.count >= MAX_SYMBOL_NODES) { + break; + } + counter.count++; + const node: SymbolNode = { + name: s.name, + kind: vscode.SymbolKind[s.kind], + range: `L${s.range.start.line + 1}-${s.range.end.line + 1}`, + }; + if (s.detail) { + node.detail = s.detail; + } + if (s.children?.length && depth < MAX_SYMBOL_DEPTH) { + node.children = symbolsToJson(s.children, depth + 1, counter); + } + result.push(node); + } + return result; +} + +// ============================================================ +// Tool 2: lsp_java_findSymbol (LSP — Workspace Symbol) +// ============================================================ + +interface FindSymbolInput { + query: string; + limit?: number; +} + +const findSymbolTool: vscode.LanguageModelTool = { + async invoke(options, _token) { + const symbols = await vscode.commands.executeCommand( + "vscode.executeWorkspaceSymbolProvider", options.input.query, + ); + if (!symbols || symbols.length === 0) { + return toResult({ results: [], message: `No symbols matching '${options.input.query}' found.` }); + } + const limit = Math.min(Math.max(options.input.limit || 20, 1), 50); + const results = symbols.slice(0, limit).map(s => ({ + name: s.name, + kind: vscode.SymbolKind[s.kind], + location: `${vscode.workspace.asRelativePath(s.location.uri)}:${s.location.range.start.line + 1}`, + })); + return toResult({ results, total: symbols.length }); + }, +}; + +// ============================================================ +// Tool 3: lsp_java_getFileImports (jdtls — AST-only, non-blocking) +// ============================================================ + +interface FileImportsInput { + uri: string; +} + +const fileImportsTool: vscode.LanguageModelTool = { + async invoke(options, _token) { + const uri = resolveFileUri(options.input.uri); + const result = await vscode.commands.executeCommand( + Commands.EXECUTE_WORKSPACE_COMMAND, + Commands.JAVA_PROJECT_GET_FILE_IMPORTS, + uri.toString(), + ); + if (!result) { + return toResult({ error: "No result from Java language server. It may still be loading." }); + } + if (Array.isArray(result) && result.length > MAX_IMPORTS) { + return toResult({ imports: result.slice(0, MAX_IMPORTS), total: result.length, truncated: true }); + } + return toResult(result); + }, +}; + +// ============================================================ +// Tool 4: lsp_java_getTypeAtPosition (LSP — Hover post-processed) +// ============================================================ + +interface TypeAtPositionInput { + uri: string; + line: number; + character: number; +} + +const typeAtPositionTool: vscode.LanguageModelTool = { + async invoke(options, _token) { + const uri = resolveFileUri(options.input.uri); + const position = new vscode.Position(options.input.line, options.input.character); + const hovers = await vscode.commands.executeCommand( + "vscode.executeHoverProvider", uri, position, + ); + return toResult(extractTypeSignature(hovers)); + }, +}; + +/** + * Extract type signature from jdtls hover result. + * jdtls returns Markdown with ```java code blocks containing the type info. + * We extract just the signature, stripping Javadoc to minimize tokens. + */ +function extractTypeSignature(hovers: vscode.Hover[] | undefined): object { + if (!hovers?.length) { + return { error: "No type information at this position" }; + } + for (const hover of hovers) { + for (const content of hover.contents) { + if (content instanceof vscode.MarkdownString) { + const match = content.value.match(/```java\n([\s\S]*?)```/); + if (match) { + const lines = match[1].trim().split("\n").filter(l => { + const trimmed = l.trim(); + if (trimmed.length === 0) { + return false; + } + // Strip Javadoc and block comment lines + if (trimmed.startsWith("/**") || trimmed.startsWith("*/") || trimmed.startsWith("* ") || trimmed === "*") { + return false; + } + // Strip single-line comments + if (trimmed.startsWith("//")) { + return false; + } + return true; + }); + return { type: lines.join("\n") }; + } + } + } + } + return { error: "Could not extract type from hover result" }; +} + +// ============================================================ +// Tool 5: lsp_java_getCallHierarchy (LSP — Call Hierarchy) +// ============================================================ + +interface CallHierarchyInput { + uri: string; + line: number; + character: number; + direction: "incoming" | "outgoing"; +} + +const callHierarchyTool: vscode.LanguageModelTool = { + async invoke(options, _token) { + const uri = resolveFileUri(options.input.uri); + const position = new vscode.Position(options.input.line, options.input.character); + + // Step 1: Prepare call hierarchy item at the given position + const items = await vscode.commands.executeCommand( + "vscode.prepareCallHierarchy", uri, position, + ); + if (!items?.length) { + return toResult({ error: "No callable symbol at this position" }); + } + + // Step 2: Get incoming or outgoing calls + const isIncoming = options.input.direction === "incoming"; + const command = isIncoming ? "vscode.provideIncomingCalls" : "vscode.provideOutgoingCalls"; + const calls = await vscode.commands.executeCommand(command, items[0]); + + if (!calls || calls.length === 0) { + return toResult({ + symbol: items[0].name, + direction: options.input.direction, + calls: [], + message: `No ${options.input.direction} calls found for '${items[0].name}'`, + }); + } + + const truncated = calls.length > MAX_CALL_RESULTS; + const capped = truncated ? calls.slice(0, MAX_CALL_RESULTS) : calls; + const results = capped.map((call: any) => { + const item = isIncoming ? call.from : call.to; + return { + name: item.name, + detail: item.detail || undefined, + location: `${vscode.workspace.asRelativePath(item.uri)}:${item.range.start.line + 1}`, + }; + }); + + return toResult({ + symbol: items[0].name, + direction: options.input.direction, + calls: results, + ...(truncated && { total: calls.length, truncated: true }), + }); + }, +}; + +// ============================================================ +// Tool 6: lsp_java_getTypeHierarchy (LSP — Type Hierarchy) +// ============================================================ + +interface TypeHierarchyInput { + uri: string; + line: number; + character: number; + direction: "supertypes" | "subtypes"; +} + +const typeHierarchyTool: vscode.LanguageModelTool = { + async invoke(options, _token) { + const uri = resolveFileUri(options.input.uri); + const position = new vscode.Position(options.input.line, options.input.character); + + // Step 1: Prepare type hierarchy item at the given position + const items = await vscode.commands.executeCommand( + "vscode.prepareTypeHierarchy", uri, position, + ); + if (!items?.length) { + return toResult({ error: "No type at this position" }); + } + + // Step 2: Get supertypes or subtypes + const isSuper = options.input.direction === "supertypes"; + const command = isSuper ? "vscode.provideSupertypes" : "vscode.provideSubtypes"; + const types = await vscode.commands.executeCommand(command, items[0]); + + if (!types || types.length === 0) { + return toResult({ + symbol: items[0].name, + direction: options.input.direction, + types: [], + message: `No ${options.input.direction} found for '${items[0].name}'`, + }); + } + + const truncated = types.length > MAX_TYPE_RESULTS; + const capped = truncated ? types.slice(0, MAX_TYPE_RESULTS) : types; + const results = capped.map(t => ({ + name: t.name, + kind: vscode.SymbolKind[t.kind], + detail: t.detail || undefined, + location: `${vscode.workspace.asRelativePath(t.uri)}:${t.range.start.line + 1}`, + })); + + return toResult({ + symbol: items[0].name, + direction: options.input.direction, + types: results, + ...(truncated && { total: types.length, truncated: true }), + }); + }, +}; + +// ============================================================ +// Registration +// ============================================================ + +export function registerJavaContextTools(context: vscode.ExtensionContext): void { + context.subscriptions.push( + vscode.lm.registerTool("lsp_java_getFileStructure", fileStructureTool), + vscode.lm.registerTool("lsp_java_findSymbol", findSymbolTool), + vscode.lm.registerTool("lsp_java_getFileImports", fileImportsTool), + vscode.lm.registerTool("lsp_java_getTypeAtPosition", typeAtPositionTool), + vscode.lm.registerTool("lsp_java_getCallHierarchy", callHierarchyTool), + vscode.lm.registerTool("lsp_java_getTypeHierarchy", typeHierarchyTool), + ); +} diff --git a/src/extension.ts b/src/extension.ts index c89b6547..aa915423 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -21,7 +21,7 @@ import { setContextForDeprecatedTasks, updateExportTaskType } from "./tasks/buil import { CodeActionProvider } from "./tasks/buildArtifact/migration/CodeActionProvider"; import { newJavaFile } from "./explorerCommands/new"; import upgradeManager from "./upgrade/upgradeManager"; -import { registerCopilotContextProviders } from "./copilot/contextProvider"; +import { registerJavaContextTools } from "./copilot/tools/javaContextTools"; import { languageServerApiManager } from "./languageServerApi/languageServerApiManager"; export async function activate(context: ExtensionContext): Promise { @@ -88,8 +88,10 @@ async function activateExtension(_operationId: string, context: ExtensionContext // Register Copilot context providers after Java Language Server is ready languageServerApiManager.ready().then((isReady) => { - if (isReady) { - registerCopilotContextProviders(context); + const config = workspace.getConfiguration("vscode-java-dependency"); + const isSettingEnabled = config.get("enableLspTools", true); + if (isReady && isSettingEnabled) { + registerJavaContextTools(context); } }); }