diff --git a/src/languageModelTool.ts b/src/languageModelTool.ts index 2ef6b7b9..0950aec3 100644 --- a/src/languageModelTool.ts +++ b/src/languageModelTool.ts @@ -58,6 +58,26 @@ const CONSTANTS = { MAX_FILE_SEARCH_DEPTH: 10 }; +const SEARCH_SKIPPED_DIRECTORIES = new Set([ + '.git', + '.gradle', + '.idea', + '.vscode', + 'bin', + 'build', + 'classes', + 'dist', + 'generated', + 'generated-sources', + 'node_modules', + 'out', + 'target', +]); + +function shouldSkipSearchDirectory(name: string): boolean { + return SEARCH_SKIPPED_DIRECTORIES.has(name); +} + const LAUNCH_FAILURE_GUIDANCE = '\n\nDo not automatically retry debug_java_application or start the program again ' + 'through a terminal command. Report the result and diagnose the cause first. ' + 'After a timeout, you may check get_debug_session_info once and inspect existing terminal output; ' @@ -923,14 +943,14 @@ function findJavaFile(dir: string, className: string, depth: number = 0): string const files = fs.readdirSync(dir); for (const file of files) { + if (shouldSkipSearchDirectory(file)) { + continue; + } + const filePath = path.join(dir, file); const stat = fs.statSync(filePath); if (stat.isDirectory()) { - // Skip common non-source directories - if (file === 'node_modules' || file === '.git' || file === 'target' || file === 'build') { - continue; - } const found = findJavaFile(filePath, className, depth + 1); if (found) { return found; @@ -972,6 +992,10 @@ function hasClassFiles(dir: string, depth: number = 0): boolean { try { const files = fs.readdirSync(dir); for (const file of files) { + if (shouldSkipSearchDirectory(file)) { + continue; + } + const filePath = path.join(dir, file); const stat = fs.statSync(filePath); diff --git a/src/mainClassPicker.ts b/src/mainClassPicker.ts index d34121d5..be0f5f9d 100644 --- a/src/mainClassPicker.ts +++ b/src/mainClassPicker.ts @@ -83,20 +83,21 @@ class MainClassPicker { return options[0]; } - // Sort the Main Class options with the recently used timestamp. - options.sort((a: IMainClassOption, b: IMainClassOption) => { + // Sort the Main Class options with the recently used timestamp without mutating the caller's + // array, and cache the active editor path so large result sets avoid repeated filesystem work. + const sortedOptions = [...options].sort((a: IMainClassOption, b: IMainClassOption) => { return this.getMRUTimestamp(b) - this.getMRUTimestamp(a); }); - const mostRecentlyUsedOption: IMainClassOption | undefined = (options.length && this.contains(options[0])) ? options[0] : undefined; + const activeEditor: TextEditor | undefined = window.activeTextEditor; + const currentActiveFile: string | undefined = _.get(activeEditor, "document.uri.fsPath"); + const mostRecentlyUsedOption: IMainClassOption | undefined = (sortedOptions.length && this.contains(sortedOptions[0])) ? sortedOptions[0] : undefined; const isMostRecentlyUsed = (option: IMainClassOption): boolean => { return !!mostRecentlyUsedOption && mostRecentlyUsedOption.mainClass === option.mainClass && mostRecentlyUsedOption.projectName === option.projectName; }; const isFromActiveEditor = (option: IMainClassOption): boolean => { - const activeEditor: TextEditor | undefined = window.activeTextEditor; - const currentActiveFile: string | undefined = _.get(activeEditor, "document.uri.fsPath"); if (option.filePath && currentActiveFile) { return path.relative(option.filePath, currentActiveFile) === ""; } @@ -110,12 +111,12 @@ class MainClassPicker { // then the Main Class from Active Editor as second, // finally other Main Class. const adjustedOptions: IMainClassOption[] = []; - options.forEach((option: IMainClassOption) => { + sortedOptions.forEach((option: IMainClassOption) => { if (isPrivileged(option)) { adjustedOptions.push(option); } }); - options.forEach((option: IMainClassOption) => { + sortedOptions.forEach((option: IMainClassOption) => { if (!isPrivileged(option)) { adjustedOptions.push(option); } diff --git a/test/languageModelToolLaunchPolicy.test.ts b/test/languageModelToolLaunchPolicy.test.ts index 4af9baa0..c616d16a 100644 --- a/test/languageModelToolLaunchPolicy.test.ts +++ b/test/languageModelToolLaunchPolicy.test.ts @@ -3,6 +3,7 @@ import * as assert from "assert"; import * as fs from "fs"; +import * as os from "os"; import * as path from "path"; import * as vscode from "vscode"; import * as telemetry from "vscode-extension-telemetry-wrapper"; @@ -124,6 +125,33 @@ suite("Language Model Tool launch retry policy", () => { assert.strictEqual(text.includes("Call debug_java_application again"), false); } + test("ignores generated build directories when resolving simple class names", async () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "java-debug-class-detection-")); + const sourceFile = path.join(tempRoot, "src", "main", "java", "com", "example", "App.java"); + const generatedFile = path.join(tempRoot, "build", "generated", "App.java"); + + fs.mkdirSync(path.dirname(sourceFile), { recursive: true }); + fs.mkdirSync(path.dirname(generatedFile), { recursive: true }); + + fs.writeFileSync(sourceFile, "package com.example;\npublic class App {}\n"); + fs.writeFileSync(generatedFile, "public class App {}\n"); + + try { + const text = await invoke({ + target: "App", + workspacePath: tempRoot, + skipBuild: true, + classpath: tempRoot, + }); + + assert.ok(text.includes("Debug session started") || text.includes("Startup is unconfirmed, not necessarily failed")); + assert.ok(!text.includes("Could not auto-detect package name")); + assert.strictEqual(commandsSent, 1); + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } + }); + for (const waitForSession of [false, true]) { test(`reports unconfirmed startup without relaunching (waitForSession=${waitForSession})`, async () => { const text = await invoke({