mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 16:08:05 +00:00
Refactor | Pull Members Up… and Push Members Down… for Java, from the Refactor menu, the editor context menu and Refactor This. A dialog picks the destination and the members (Make abstract / Keep abstract); problems are confirmed before anything changes; the edit is applied as one change or previewed first. JDT LS offers neither as a code action, so the app-owned JDT LS bundle (plugins/java-usages 1.3.0) adds two delegate commands that run JDT's PullUpRefactoringProcessor / PushDownRefactoringProcessor and return the problems and the edit without applying it, exposed as lsp_hierarchy_members / lsp_hierarchy_refactor (also over the remote host dispatcher). The bundled jar was rebuilt from source with JDT LS 1.63.0 and matches the recorded SHA-256. Co-authored-by: fhrmrk <81476595+fhrmrk@users.noreply.github.com>
173 lines
8.1 KiB
TypeScript
173 lines
8.1 KiB
TypeScript
/**
|
|
* Pull Members Up / Push Members Down. The language server is stubbed at the IPC layer: the
|
|
* Java side is covered by plugins/java-usages/tests/hierarchy.py against a real JDT LS.
|
|
*/
|
|
import type { Page } from "@playwright/test";
|
|
import { expect, openFromTree, test } from "./fixtures";
|
|
|
|
const captureDir = process.env.OPENIDEA_ACCEPTANCE_DIR;
|
|
|
|
/** Opens the dialog as the action does after the server answered, for the editor's view. */
|
|
async function openDialog(page: Page, kind: "pullUp" | "pushDown") {
|
|
await page.evaluate(async (kind) => {
|
|
const module = "/src/editor/refactor/hierarchy.ts";
|
|
const { useHierarchyDialog } = await import(/* @vite-ignore */ module);
|
|
const view = (document.querySelector(".cm-content") as unknown as { cmTile: { view: { state: { doc: unknown } } } })
|
|
.cmTile.view;
|
|
const pullUp = kind === "pullUp";
|
|
useHierarchyDialog.setState({
|
|
request: {
|
|
seq: 1,
|
|
view,
|
|
kind,
|
|
doc: 1,
|
|
pos: { line: 0, col: 0 },
|
|
text: view.state.doc,
|
|
info: {
|
|
className: pullUp ? "Dog" : "Animal",
|
|
targets: pullUp
|
|
? [
|
|
{ id: "animal", name: "Animal", qualifiedName: "zoo.Animal", interface: false },
|
|
{ id: "speaker", name: "Speaker", qualifiedName: "zoo.Speaker", interface: true },
|
|
]
|
|
: [
|
|
{ id: "dog", name: "Dog", qualifiedName: "zoo.Dog", interface: false },
|
|
{ id: "cat", name: "Cat", qualifiedName: "zoo.Cat", interface: false },
|
|
],
|
|
members: pullUp
|
|
? [
|
|
{ id: "tail", label: "tail : int", kind: "field", selected: false, abstractable: false },
|
|
{ id: "bark", label: "bark() : String", kind: "method", selected: true, abstractable: true },
|
|
{ id: "wag", label: "wag() : int", kind: "method", selected: false, abstractable: true },
|
|
]
|
|
: [
|
|
{ id: "name", label: "name : String", kind: "field", selected: false, abstractable: false },
|
|
{ id: "describe", label: "describe() : String", kind: "method", selected: true, abstractable: true },
|
|
],
|
|
},
|
|
},
|
|
});
|
|
}, kind);
|
|
}
|
|
|
|
/** Stubs the server's check and the edit application; returns what reached them. */
|
|
async function stubServer(page: Page, problems: { severity: string; message: string }[]) {
|
|
await page.evaluate(async (problems) => {
|
|
const module = "/src/platform/ipc.ts";
|
|
const { commands } = await import(/* @vite-ignore */ module);
|
|
// Only the stubbed commands answer; window lookups by other features keep working.
|
|
Object.defineProperty(window, "__TAURI_INTERNALS__", {
|
|
configurable: true,
|
|
value: {
|
|
transformCallback: () => 1,
|
|
invoke: async () => null,
|
|
metadata: { currentWindow: { label: "main" }, currentWebview: { label: "main", windowLabel: "main" } },
|
|
},
|
|
});
|
|
const seen = ((window as unknown as { hierarchySeen: unknown[] }).hierarchySeen = [] as unknown[]);
|
|
commands.lspHierarchyRefactor = async (...args: unknown[]) => {
|
|
seen.push({ refactor: args.slice(3) });
|
|
return {
|
|
status: "ok",
|
|
data: {
|
|
problems,
|
|
edit: {
|
|
files: [
|
|
{
|
|
kind: "edit",
|
|
path: "/dev/acme-api/README.md",
|
|
edits: [{ range: { start: { line: 0, col: 0 }, end: { line: 0, col: 0 } }, new_text: "" }],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
};
|
|
};
|
|
commands.applyWorkspaceEdit = async (edit: unknown) => {
|
|
seen.push({ apply: edit });
|
|
return { status: "ok", data: null };
|
|
};
|
|
}, problems);
|
|
}
|
|
|
|
const seen = (page: Page) => page.evaluate(() => (window as unknown as { hierarchySeen: unknown[] }).hierarchySeen);
|
|
|
|
test("the editor's Refactor menu offers Pull Members Up and Push Members Down", async ({ workbench: page }) => {
|
|
const editor = await openFromTree(page, "README.md");
|
|
await editor.locator(".cm-content").click({ button: "right" });
|
|
await page.getByRole("menuitem", { name: "Refactor", exact: true }).hover();
|
|
await expect(page.getByRole("menuitem", { name: /^Pull Members Up…/ })).toBeVisible();
|
|
await expect(page.getByRole("menuitem", { name: /^Push Members Down…/ })).toBeVisible();
|
|
if (captureDir) await page.screenshot({ path: `${captureDir}/01-refactor-menu.png` });
|
|
await page.keyboard.press("Escape");
|
|
});
|
|
|
|
test("Pull Members Up: members, interface destination, conflicts, then the edit", async ({ workbench: page }) => {
|
|
await openFromTree(page, "README.md");
|
|
await openDialog(page, "pullUp");
|
|
const dialog = page.getByRole("dialog", { name: "Pull Members Up" });
|
|
await expect(dialog).toBeVisible();
|
|
await expect(dialog.getByText("Pull up members of Dog to:")).toBeVisible();
|
|
const member = (label: string) => dialog.getByRole("row").filter({ hasText: label });
|
|
// The member at the caret starts checked; fields can't be made abstract.
|
|
await expect(member("bark() : String").getByRole("checkbox").first()).toBeChecked();
|
|
await expect(member("wag() : int").getByRole("checkbox").first()).not.toBeChecked();
|
|
await member("tail : int").getByRole("checkbox").first().check();
|
|
await expect(member("tail : int").getByRole("checkbox", { name: "Make abstract" })).toBeDisabled();
|
|
if (captureDir) await page.screenshot({ path: `${captureDir}/02-pull-up-dialog.png` });
|
|
|
|
// To an interface, methods are declared abstract there.
|
|
await dialog.getByRole("button", { name: "Destination class" }).click();
|
|
await page.getByRole("option", { name: /zoo\.Speaker/ }).click();
|
|
const abstractBark = member("bark() : String").getByRole("checkbox", { name: "Make abstract" });
|
|
await expect(abstractBark).toBeChecked();
|
|
await expect(abstractBark).toBeDisabled();
|
|
await expect(dialog.getByText("Methods pulled up to an interface are declared abstract there.")).toBeVisible();
|
|
if (captureDir) await page.screenshot({ path: `${captureDir}/03-pull-up-to-interface.png` });
|
|
|
|
await stubServer(page, [
|
|
{ severity: "error", message: "Field 'zoo.Dog.tail' is not accessible from type 'zoo.Speaker'" },
|
|
]);
|
|
await dialog.getByRole("button", { name: "Refactor", exact: true }).click();
|
|
const problems = page.getByRole("alertdialog").or(page.getByRole("dialog", { name: "Problems Detected" }));
|
|
await expect(problems.getByText(/is not accessible from type/)).toBeVisible();
|
|
if (captureDir) await page.screenshot({ path: `${captureDir}/04-problems-detected.png` });
|
|
// Cancel: nothing applied, the dialog stays.
|
|
await problems.getByRole("button", { name: "Cancel", exact: true }).click();
|
|
await expect(dialog).toBeVisible();
|
|
expect(await seen(page)).toEqual([
|
|
{
|
|
refactor: [
|
|
"pullUp",
|
|
"speaker",
|
|
[
|
|
{ id: "tail", makeAbstract: false },
|
|
{ id: "bark", makeAbstract: true },
|
|
],
|
|
],
|
|
},
|
|
]);
|
|
// Continue: the edit is applied and the dialog closes.
|
|
await dialog.getByRole("button", { name: "Refactor", exact: true }).click();
|
|
await problems.getByRole("button", { name: "Continue", exact: true }).click();
|
|
await expect(dialog).toBeHidden();
|
|
await expect.poll(async () => (await seen(page)).some((s) => "apply" in (s as object))).toBe(true);
|
|
});
|
|
|
|
test("Push Members Down keeps an abstract declaration when asked", async ({ workbench: page }) => {
|
|
await openFromTree(page, "README.md");
|
|
await openDialog(page, "pushDown");
|
|
const dialog = page.getByRole("dialog", { name: "Push Members Down" });
|
|
await expect(dialog.getByText("Push members of Animal down to:")).toBeVisible();
|
|
await expect(dialog.getByText("Dog, Cat")).toBeVisible();
|
|
const describe = dialog.getByRole("row").filter({ hasText: "describe() : String" });
|
|
await describe.getByRole("checkbox", { name: "Keep abstract" }).check();
|
|
if (captureDir) await page.screenshot({ path: `${captureDir}/05-push-down-dialog.png` });
|
|
await stubServer(page, []);
|
|
await dialog.getByRole("button", { name: "Refactor", exact: true }).click();
|
|
await expect(dialog).toBeHidden();
|
|
const calls = await seen(page);
|
|
expect(calls[0]).toEqual({ refactor: ["pushDown", null, [{ id: "describe", makeAbstract: true }]] });
|
|
expect(calls.some((s) => "apply" in (s as object))).toBe(true);
|
|
});
|