дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
fix(http-client): index Spring endpoints from workspace members (#2192)
The Spring route index dropped every source outside the workspace folder, although the backend already returned sources for linked members. Routes now come from every member, with paths relative to the member containing the controller. The cache key includes the member list, so adding or removing a member rescans. Files outside all members stay excluded. Host lookup already checks the controller's module. Plugin endpoints use a separate rooted contract and are unchanged.
Цей коміт міститься в:
1 джерело
299fb14f80
коміт
e1be533bd2
2 files changed
+66
-8
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { commands, emitLocalIdeEvent } from "@/platform/ipc";
|
||||
import { ipcPath } from "@/platform/paths";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { searchEndpoints } from "@/workbench/contributions";
|
||||
import { clearRouteIndex, isRouteSource, registerSpringEndpointProvider, routeIndex } from "./routeIndex";
|
||||
|
||||
@@ -22,8 +23,22 @@ afterEach(() => {
|
||||
dispose = undefined;
|
||||
clearRouteIndex();
|
||||
scanSources.mockReset();
|
||||
useProject.setState({ project: null });
|
||||
});
|
||||
|
||||
function workspace(projects: string[]) {
|
||||
useProject.setState({
|
||||
project: {
|
||||
root: "/workspace",
|
||||
name: "Workspace",
|
||||
settings: {},
|
||||
trusted: true,
|
||||
isGit: false,
|
||||
workspace: { version: 1, name: "Workspace", projects },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("the request-path index follows the workspace", () => {
|
||||
it("is invalidated by a change to a mapping source", () => {
|
||||
for (const [path, expected] of [
|
||||
@@ -39,6 +54,43 @@ describe("the request-path index follows the workspace", () => {
|
||||
});
|
||||
|
||||
describe("the Spring endpoint provider", () => {
|
||||
it("includes linked members with member-relative paths and excludes files outside every member", async () => {
|
||||
workspace(["/p", "/linked", "/linked/nested"]);
|
||||
scanSources.mockResolvedValue({
|
||||
status: "ok",
|
||||
data: [
|
||||
source(),
|
||||
{ ...source(), path: "/linked/src/Controller.kt" },
|
||||
{ ...source(), path: "/linked/nested/src/NestedController.java" },
|
||||
{ ...source(), path: "/linked-other/Outside.java" },
|
||||
{ ...source(), path: "/workspace/State.java" },
|
||||
],
|
||||
});
|
||||
expect(await routeIndex("/workspace")).toMatchObject([
|
||||
{ path: "/p/DeepLinkController.java", rel: "DeepLinkController.java", line: 4, col: 11 },
|
||||
{ path: "/linked/src/Controller.kt", rel: "src/Controller.kt", line: 4, col: 11 },
|
||||
{ path: "/linked/nested/src/NestedController.java", rel: "src/NestedController.java" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("rescans when members are added or removed without changing the workspace root", async () => {
|
||||
scanSources.mockResolvedValue({
|
||||
status: "ok",
|
||||
data: [source(), { ...source(), path: "/linked/Controller.java" }],
|
||||
});
|
||||
workspace(["/p"]);
|
||||
expect(await routeIndex("/workspace")).toHaveLength(1);
|
||||
workspace(["/p", "/linked"]);
|
||||
const both = await routeIndex("/workspace");
|
||||
expect(both).toHaveLength(2);
|
||||
expect(await routeIndex("/workspace")).toBe(both);
|
||||
workspace(["/linked"]);
|
||||
expect(await routeIndex("/workspace")).toMatchObject([{ path: "/linked/Controller.java" }]);
|
||||
workspace([]);
|
||||
expect(await routeIndex("/workspace")).toEqual([]);
|
||||
expect(scanSources).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
it("scans once per root with method coordinates and rescans for another root", async () => {
|
||||
scanSources.mockResolvedValue({ status: "ok", data: [source()] });
|
||||
expect(await routeIndex(null)).toEqual([]);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/** Spring request-path index, cached per project until a mapping source changes. */
|
||||
import { call, commands, onIdeEvent } from "@/platform/ipc";
|
||||
import { call, commands, onIdeEvent, type ProjectInfo } from "@/platform/ipc";
|
||||
import { ipcPath, isSameOrInside } from "@/platform/paths";
|
||||
import { projectFolder, relativePath, useProject } from "@/platform/project";
|
||||
import { registerEndpointProvider, type EndpointEntry } from "@/workbench/contributions";
|
||||
import { springControllers } from "./endpoints";
|
||||
|
||||
@@ -17,7 +18,7 @@ export function registerSpringEndpointProvider(): () => void {
|
||||
};
|
||||
}
|
||||
|
||||
let cached: { root: string; promise: Promise<EndpointEntry[]> } | null = null;
|
||||
let cached: { key: string; promise: Promise<EndpointEntry[]> } | null = null;
|
||||
|
||||
/** Whether `path` is a source a mapping can live in (a Java or Kotlin file). */
|
||||
export function isRouteSource(path: string): boolean {
|
||||
@@ -32,18 +33,23 @@ export function clearRouteIndex() {
|
||||
/** The project's routes, scanned once per project and kept in memory. */
|
||||
export function routeIndex(root: string | null): Promise<EndpointEntry[]> {
|
||||
if (!root) return Promise.resolve([]);
|
||||
if (cached?.root === root) return cached.promise;
|
||||
const promise = scan(root);
|
||||
cached = { root, promise };
|
||||
const current = useProject.getState().project;
|
||||
const project = current?.root === root ? current : null;
|
||||
const key = JSON.stringify([root, project?.workspace?.projects ?? null]);
|
||||
if (cached?.key === key) return cached.promise;
|
||||
const promise = scan(root, project);
|
||||
cached = { key, promise };
|
||||
return promise;
|
||||
}
|
||||
|
||||
async function scan(root: string): Promise<EndpointEntry[]> {
|
||||
async function scan(root: string, project: ProjectInfo | null): Promise<EndpointEntry[]> {
|
||||
const sources = await call(commands.springRouteSources());
|
||||
const routes: EndpointEntry[] = [];
|
||||
const base = ipcPath(root).replace(/\/$/, "");
|
||||
for (const source of sources) {
|
||||
const path = ipcPath(source.path);
|
||||
const folder = project ? projectFolder(project, path)?.root : root;
|
||||
if (!folder) continue;
|
||||
const base = ipcPath(folder).replace(/\/$/, "");
|
||||
if (!isSameOrInside(path, base)) continue;
|
||||
for (const controller of springControllers(source.text)) {
|
||||
for (const endpoint of controller.endpoints) {
|
||||
@@ -57,7 +63,7 @@ async function scan(root: string): Promise<EndpointEntry[]> {
|
||||
controller: controller.name,
|
||||
methodName: endpoint.methodName,
|
||||
path,
|
||||
rel: path.slice(base.length + 1),
|
||||
rel: relativePath(base, path),
|
||||
line: at.line,
|
||||
col: at.col,
|
||||
});
|
||||
|
||||
Reference in new issue
Заблокувати користувача