дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
fix(run): resolve stack frames from all exact source names (#2313)
Stack-trace navigation only searched the first 50 fuzzy file matches, so in a project with many files of the same name the project source could be skipped in favour of a library or decompiled class. Frames now look up every indexed file with the exact source basename (new find_files_named) and then apply the existing package-path and declaration checks. Covers Java, Kotlin (including sources outside their package directory), Scala and Groovy; older remote servers fall back to their full fuzzy list. Refs #1660.
Цей коміт міститься в:
1 джерело
658c38a193
коміт
226ac26954
9 files changed
+109
-7
No files matched your search
@@ -41,6 +41,11 @@ pub fn find_files(host: &Host, window: &str, query: String, limit: u32) -> IdeRe
|
||||
Ok(host.project(window)?.index.find_files(&query, limit as usize))
|
||||
}
|
||||
|
||||
pub async fn find_files_named(host: &Host, window: &str, name: String) -> IdeResult<Vec<IpcPath>> {
|
||||
let project = host.project(window)?;
|
||||
blocking(move || Ok(project.index.files_named(&name).into_iter().map(Into::into).collect())).await
|
||||
}
|
||||
|
||||
pub fn find_dirs(host: &Host, window: &str, query: String, limit: u32) -> IdeResult<Vec<FileMatch>> {
|
||||
Ok(host.project(window)?.index.find_dirs(&query, limit as usize))
|
||||
}
|
||||
@@ -167,6 +172,7 @@ pub(crate) fn register(d: &mut Dispatcher) {
|
||||
d.add("index_status", |c| async move { index_status(&c.host, &c.window) });
|
||||
d.add("index_file_extensions", |c| async move { index_file_extensions(&c.host, &c.window) });
|
||||
d.add("find_files", |c| async move { find_files(&c.host, &c.window, c.arg("query")?, c.arg("limit")?) });
|
||||
d.add("find_files_named", |c| async move { find_files_named(&c.host, &c.window, c.arg("name")?).await });
|
||||
d.add("find_dirs", |c| async move { find_dirs(&c.host, &c.window, c.arg("query")?, c.arg("limit")?) });
|
||||
d.add("find_symbols", |c| async move {
|
||||
find_symbols(&c.host, &c.window, c.arg("query")?, c.arg("filter")?, c.arg("limit")?)
|
||||
|
||||
@@ -290,6 +290,7 @@ pub fn restricted_command_allowed(cmd: &str) -> bool {
|
||||
| "deploy_coverage"
|
||||
| "deploy_test_connection"
|
||||
| "find_files"
|
||||
| "find_files_named"
|
||||
| "find_dirs"
|
||||
| "find_symbols"
|
||||
| "mark_recent"
|
||||
|
||||
@@ -234,6 +234,15 @@ impl ProjectIndex {
|
||||
matching::find_files(&d, &matching::Recents { paths: &recent }, query, limit)
|
||||
}
|
||||
|
||||
/// All indexed files with this exact basename, for resolving printed source locations.
|
||||
/// Unlike interactive fuzzy search, this must not truncate candidates before package matching.
|
||||
pub fn files_named(&self, name: &str) -> Vec<PathBuf> {
|
||||
let data = self.shared.data.read();
|
||||
let mut files: Vec<_> = data.files.iter().flatten().filter(|file| file.name() == name).collect();
|
||||
files.sort_by(|a, b| a.rel.len().cmp(&b.rel.len()).then_with(|| a.rel.cmp(&b.rel)));
|
||||
files.into_iter().map(|file| file.path.clone()).collect()
|
||||
}
|
||||
|
||||
/// Up to `limit` indexed files below `dir` (VCS-ignored ones left out) whose path relative to
|
||||
/// `dir` satisfies `matches`: a file list without walking the disk. `None` while nothing is
|
||||
/// indexed yet (first scan without a cache).
|
||||
|
||||
@@ -57,6 +57,27 @@ fn files(idx: &ProjectIndex, q: &str) -> Vec<String> {
|
||||
idx.find_files(q, 20).into_iter().map(|m| m.rel_path).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_file_names_keep_all_source_candidates() {
|
||||
let f = Fixture::new(&[
|
||||
("src/OtherOrders.kt", "package other"),
|
||||
("src/Orders.kt.bak", "backup"),
|
||||
("target/Orders.kt", "excluded"),
|
||||
("src/orders.kt", "different case"),
|
||||
]);
|
||||
for i in 0..65 {
|
||||
f.write(&format!("module{i}/Orders.kt"), &format!("package module{i}"));
|
||||
}
|
||||
let source = f.write("service/src/Orders.kt", "package com.example.orders");
|
||||
let idx = f.open();
|
||||
let candidates = idx.files_named("Orders.kt");
|
||||
assert_eq!(candidates.len(), 66);
|
||||
assert!(candidates.contains(&source));
|
||||
assert!(candidates.iter().all(|path| path.file_name().unwrap() == "Orders.kt"));
|
||||
assert_eq!(candidates, idx.files_named("Orders.kt"));
|
||||
assert!(idx.files_named("Missing.kt").is_empty());
|
||||
}
|
||||
|
||||
fn symbols(idx: &ProjectIndex, q: &str, filter: SymbolFilter) -> Vec<String> {
|
||||
idx.find_symbols(q, filter, 20).into_iter().map(|m| m.name).collect()
|
||||
}
|
||||
|
||||
@@ -209,6 +209,7 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
|
||||
nav::index_file_extensions,
|
||||
nav::spring_route_sources,
|
||||
nav::find_files,
|
||||
nav::find_files_named,
|
||||
nav::find_dirs,
|
||||
nav::find_symbols,
|
||||
nav::mark_recent,
|
||||
|
||||
@@ -28,6 +28,12 @@ pub fn find_files(window: Window, state: State<'_, AppState>, query: String, lim
|
||||
ide_host::ops::nav::find_files(&state.host, window.label(), query, limit)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn find_files_named(window: Window, state: State<'_, AppState>, name: String) -> IdeResult<Vec<IpcPath>> {
|
||||
ide_host::ops::nav::find_files_named(&state.host, window.label(), name).await
|
||||
}
|
||||
|
||||
/// Folders holding indexed files, for `@` references.
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
|
||||
@@ -368,6 +368,7 @@ export const commands = {
|
||||
/** Spring Web mapping sources for the request-path index of Search Everywhere. */
|
||||
springRouteSources: () => typedError<RouteSource[], IdeError>(__TAURI_INVOKE("spring_route_sources")),
|
||||
findFiles: (query: string, limit: number) => typedError<FileMatch[], IdeError>(__TAURI_INVOKE("find_files", { query, limit })),
|
||||
findFilesNamed: (name: string) => typedError<IpcPath[], IdeError>(__TAURI_INVOKE("find_files_named", { name })),
|
||||
/** Folders holding indexed files, for `@` references. */
|
||||
findDirs: (query: string, limit: number) => typedError<FileMatch[], IdeError>(__TAURI_INVOKE("find_dirs", { query, limit })),
|
||||
findSymbols: (query: string, filter: SymbolFilter, limit: number) => typedError<SymbolMatch[], IdeError>(__TAURI_INVOKE("find_symbols", { query, filter, limit })),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { jvmFrameTarget } from "./links";
|
||||
import { useRemote } from "@/platform/remote";
|
||||
|
||||
const files: Record<string, string> = {
|
||||
"/p/src/main/java/com/acme/Util.java": "package com.acme;\nclass Util {}",
|
||||
@@ -15,9 +16,12 @@ vi.mock("@/platform/ipc", () => ({
|
||||
commands: {
|
||||
fsReadDir: async () => [],
|
||||
fsReadText: async (path: string) => files[path] ?? "",
|
||||
findFiles: async (name: string) =>
|
||||
findFilesNamed: async (name: string) => Object.keys(files).filter((p) => p.endsWith(`/${name}`)),
|
||||
indexStatus: async () => ({ files: Object.keys(files).length }),
|
||||
findFiles: async (name: string, limit: number) =>
|
||||
Object.keys(files)
|
||||
.filter((p) => p.endsWith(`/${name}`))
|
||||
.slice(0, limit)
|
||||
.map((path) => ({ path, relPath: path.slice(3) })),
|
||||
lspWorkspaceSymbols: async () => symbols.list,
|
||||
},
|
||||
@@ -28,6 +32,7 @@ const { resolveTarget } = await import("./navigate");
|
||||
describe("JVM stack frame resolution", () => {
|
||||
afterEach(() => {
|
||||
symbols.list = [];
|
||||
useRemote.setState({ session: null });
|
||||
});
|
||||
|
||||
it("picks the file of the frame's package, not any file of that name", async () => {
|
||||
@@ -43,6 +48,51 @@ describe("JVM stack frame resolution", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.each(["java", "kt", "scala", "groovy"])("keeps the package in a crowded %s source lookup", async (ext) => {
|
||||
const otherPaths = Array.from({ length: 60 }, (_, i) => `/p/module${i}/src/other${i}/AService.${ext}`);
|
||||
const source = `/p/service/src/main/${ext}/com/example/AService.${ext}`;
|
||||
for (const [i, path] of otherPaths.entries()) files[path] = `package other${i};`;
|
||||
files[source] = "package com.example;";
|
||||
symbols.list = [
|
||||
{
|
||||
name: "AService",
|
||||
kind: "class",
|
||||
container: "com.example",
|
||||
location: { path: "jdt://contents/AService.class", uri: "jdt://contents/AService.class" },
|
||||
},
|
||||
];
|
||||
try {
|
||||
expect(await resolveTarget(jvmFrameTarget("com.example.AService", `AService.${ext}`, 52), "/p")).toBe(source);
|
||||
} finally {
|
||||
for (const path of [...otherPaths, source]) delete files[path];
|
||||
}
|
||||
});
|
||||
|
||||
it.each([false, true])(
|
||||
"finds nonstandard package directories beyond the fuzzy limit (old remote: %s)",
|
||||
async (legacy) => {
|
||||
if (legacy)
|
||||
useRemote.setState({
|
||||
session: {
|
||||
target: { kind: "wsl", distro: "Ubuntu" },
|
||||
root: "/p",
|
||||
host: "Ubuntu",
|
||||
os: "linux",
|
||||
commands: ["find_files", "index_status"],
|
||||
},
|
||||
});
|
||||
const others = Array.from({ length: 65 }, (_, i) => `/p/other${i}/Orders.kt`);
|
||||
const source = "/p/special/src/Orders.kt";
|
||||
for (const [i, path] of others.entries()) files[path] = `package other${i}`;
|
||||
files[source] = "package com.example.orders\nfun main() {}";
|
||||
try {
|
||||
expect(await resolveTarget(jvmFrameTarget("com.example.orders.OrdersKt", "Orders.kt", 1), "/p")).toBe(source);
|
||||
} finally {
|
||||
for (const path of [...others, source]) delete files[path];
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("library classes open the language server's source, else nothing", async () => {
|
||||
expect(await resolveTarget(jvmFrameTarget("java.util.ArrayList", "ArrayList.java", 427), "/p")).toBeNull();
|
||||
symbols.list = [
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/** Opens file locations printed in run output (resolving relative and bare names). */
|
||||
import { call, commands, isTauri, nextRequestId } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { useRemote } from "@/platform/remote";
|
||||
import { editorService, notifications } from "@/workbench/contributions";
|
||||
import { streamSearch } from "@/platform/textSearch";
|
||||
import { joinPath, type FileTarget } from "./links";
|
||||
@@ -36,18 +37,24 @@ async function declaresPackage(path: string, pkg: string): Promise<boolean> {
|
||||
async function findByName(target: FileTarget): Promise<string | null> {
|
||||
const name = target.path.split("/").pop() ?? target.path;
|
||||
try {
|
||||
const hits = await call(commands.findFiles(name, 50));
|
||||
const remote = useRemote.getState().session;
|
||||
// Older remote servers only have fuzzy search: request the whole inventory before filtering.
|
||||
const hits =
|
||||
remote && !remote.commands.includes("find_files_named")
|
||||
? (await call(commands.findFiles(name, (await call(commands.indexStatus())).files)))
|
||||
.filter((h) => h.relPath.split("/").pop() === name)
|
||||
.map((h) => h.path)
|
||||
: await call(commands.findFilesNamed(name));
|
||||
const tail = target.path.replace(/^\.?\//, "");
|
||||
const exact = hits.find((h) => h.relPath === tail || h.relPath.endsWith(`/${tail}`));
|
||||
if (exact) return exact.path;
|
||||
const named = hits.filter((h) => h.relPath.split("/").pop() === name);
|
||||
const exact = hits.find((path) => path === tail || path.endsWith(`/${tail}`));
|
||||
if (exact) return exact;
|
||||
// A stack frame names its class: a same-named file of another package is not it.
|
||||
if (target.className) {
|
||||
const pkg = packageOf(target.className);
|
||||
for (const h of named) if (await declaresPackage(h.path, pkg)) return h.path;
|
||||
for (const path of hits) if (await declaresPackage(path, pkg)) return path;
|
||||
return null;
|
||||
}
|
||||
return named[0]?.path ?? null;
|
||||
return hits[0] ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in new issue
Заблокувати користувача