Files
openidea/src/features/http-client/generatedRequests.ts
T
Arnaud Ritti 668206ddfd feat(plugins): share project HTTP endpoints with the HTTP client (#1991)
Adds the API 1.3 `endpoints-provider` export and `[[contributes.endpoints]]` (file globs, ^1.3, `project.read`). A plugin's routes join the Spring ones in Search Everywhere, `.http` URL completion and the declaration's Open in HTTP Client gutter action. The UI asks `plugin_endpoints` on demand; the host runs the plugin on a blocking thread, validates the whole list (bounds, project-relative locations, UTF-16 ranges) and caches it per project until a file matching the globs changes, then emits `EndpointsChanged`. A failed or invalid run keeps the last list; disabling, crashing or closing the project withdraws it, and late results can't restore it. Closes #1960.

Co-authored-by: arnaud-ritti <[email protected]>
2026-11-07 22:59:21 +08:00

61 lines
2.4 KiB
TypeScript

import { EditorView } from "@codemirror/view";
import { activeEditor, editorsFor, useEditorRegistry, type TextEditorHandle } from "@/editor/api";
import { tx } from "@/platform/i18n";
import { call, commands, emitLocalIdeEvent, IpcError } from "@/platform/ipc";
import { ipcPath } from "@/platform/paths";
import { editorService } from "@/workbench/contributions";
import { appendRequests } from "./spring/request";
export const GENERATED_REQUESTS = "generated-requests.http";
async function scratchFile(): Promise<{ path: string; created: boolean }> {
const { appDataDir } = await import("@tauri-apps/api/path");
// `appDataDir()` is native (`C:\…`): the editor is found by its `/` path.
const dir = `${ipcPath(await appDataDir()).replace(/\/+$/, "")}/scratches`;
const ignoreExisting = (e: unknown) => {
if (!(e instanceof IpcError && e.kind === "alreadyExists")) throw e;
};
await call(commands.fsCreateDir(dir)).catch(ignoreExisting);
const path = `${dir}/${GENERATED_REQUESTS}`;
let created = true;
await call(commands.fsCreateFile(path)).catch((e) => {
ignoreExisting(e);
created = false;
});
return { path, created };
}
/** The text editor of `path`, once the just opened tab has created it. */
function editorFor(path: string, timeoutMs = 5000): Promise<TextEditorHandle | null> {
const find = () => editorsFor(path).find((x) => x === activeEditor()) ?? editorsFor(path)[0] ?? null;
const now = find();
if (now) return Promise.resolve(now);
return new Promise((resolve) => {
const done = (e: TextEditorHandle | null) => {
clearTimeout(timer);
unsubscribe();
resolve(e);
};
const timer = setTimeout(() => done(null), timeoutMs);
const unsubscribe = useEditorRegistry.subscribe(() => {
const e = find();
if (e) done(e);
});
});
}
export async function appendGeneratedRequests(blocks: string[]): Promise<void> {
const file = await scratchFile();
if (file.created) emitLocalIdeEvent({ type: "fsChanged", created: [file.path], modified: [], removed: [] });
await editorService().openFile(file.path, { focus: true });
const e = await editorFor(file.path);
if (!e) throw new Error(`${tx("Cannot open file")}: ${file.path}`);
const { insert, from, caret } = appendRequests(e.view.state.doc.toString(), blocks);
e.view.dispatch({
changes: { from, insert },
selection: { anchor: caret },
effects: EditorView.scrollIntoView(caret, { y: "center" }),
userEvent: "input",
});
}