feat(http-client): Generate requests from Spring Web mappings (#104)

Spring MVC/WebFlux mapping methods (Java and Kotlin) in @RestController / @Controller classes get a gutter icon with "Generate Request in HTTP Client", and the class line gets "Generate Requests for Controller"; both are also actions (`http.generateSpringRequest`, `http.generateSpringControllerRequests`) in Find Action, the editor menu and ⌥↵. Requests are appended to the `generated-requests.http` scratch and opened at the new request.

The URL combines class and method paths ({id} and {id:regex} → {{id}}), with host, port and context path from application.properties / .yml (default http://localhost:8080). @RequestParam, @RequestHeader, params/headers, consumes/produces and @RequestBody (JSON skeleton from the body type, nested one level) are mapped; same-file constants are resolved. Adds an `intentionProviders` editor facet so features can contribute ⌥↵ items.

Fixes #100

Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-10-27 20:54:48 +08:00
1 джерело a065189516
коміт 14f72fc307
9 files changed
+1824 -7

No files matched your search

+22
Переглянути файл
@@ -283,6 +283,28 @@ export function contributedAt<T>(state: EditorState, key: string, pos: number):
return state.languageDataAt<T>(key, pos)[0];
}
/** A context action a feature offers at the caret, listed by Show Context Actions (⌥↵) before the language server's. */
export interface ContributedIntention {
id: string;
label: string;
icon?: string;
run: () => void | Promise<void>;
}
/** Providers of `ContributedIntention`s for the caret of `view` (cheap and synchronous: also drives the light bulb). */
export const intentionProviders = Facet.define<(view: EditorView) => ContributedIntention[]>();
export function contributedIntentions(view: EditorView): ContributedIntention[] {
return view.state.facet(intentionProviders).flatMap((p) => {
try {
return p(view);
} catch (e) {
console.error("intention provider failed", e);
return [];
}
});
}
// --- navigation history ------------------------------------------------------
/** One entry of the Back/Forward (⌘[ / ⌘]) history. 0-based line, UTF-16 col. */
+7 -6
Переглянути файл
@@ -7,7 +7,7 @@ import { StateEffect, StateField, type Extension } from "@codemirror/state";
import { EditorView, ViewPlugin, type ViewUpdate } from "@codemirror/view";
import { forEachDiagnostic } from "@codemirror/lint";
import { commands, isCancelled, type CodeAction } from "@/platform/ipc";
import { whenSynced } from "@/editor/api";
import { contributedIntentions, whenSynced } from "@/editor/api";
import { shortcutLabel } from "@/platform/keymap";
import { debounce, docOf, isUnavailable, lspRequest, reportLspError } from "./lsp";
import { offsetsToRange } from "./positions";
@@ -207,8 +207,8 @@ export function withInspectionControls(view: EditorView, items: ListPopupItem[])
/** ⌥↵ */
export async function showIntentions(view: EditorView) {
try {
// Typo fixes come first and need no language server.
const local = spellingIntentions(view);
// Typo fixes and feature-contributed actions come first and need no language server.
const local: ListPopupItem[] = [...spellingIntentions(view), ...contributedIntentions(view)];
let actions: CodeAction[] = [];
let failure: { error: unknown } | null = null;
try {
@@ -338,20 +338,21 @@ const bulbPlugin = ViewPlugin.fromClass(
if (!doc || doc.readOnly || !this.view.dom.isConnected) return;
const state = this.view.state;
const typoFix = spellingIntentions(this.view).length > 0;
const local = typoFix || contributedIntentions(this.view).length > 0;
const backoff = serverBackoff.active(doc.doc);
if (backoff && !typoFix) return;
if (backoff && !local) return;
let actions: CodeAction[] = [];
if (!backoff) {
try {
actions = await fetchIntentions(this.view);
} catch (e) {
if (isUnavailable(e)) serverBackoff.mark(doc.doc);
if (!typoFix) return;
if (!local) return;
}
}
if (this.view.state.doc !== state.doc || !this.view.state.selection.eq(state.selection)) return;
const line = state.doc.lineAt(state.selection.main.head).number;
const any = actions.length > 0 || typoFix;
const any = actions.length > 0 || local;
this.view.dispatch({
effects: setBulb.of(any ? { line, quickFix: typoFix || actions.some(isQuickFix) } : null),
});