дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
feat(refactor): Java Pull Members Up / Push Members Down (#1183)
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 <[email protected]>
Цей коміт міститься в:
1 джерело
5b46844f88
коміт
ed145e3046
39 files changed
+2008
-14
No files matched your search
@@ -418,6 +418,10 @@ export const commands = {
|
||||
} | null, IdeError>(__TAURI_INVOKE("lsp_prepare_rename", { doc, pos })),
|
||||
/** Computes the rename edit without applying it, so the UI can preview conflicts. */
|
||||
lspRename: (request: RequestId, doc: DocId, pos: Pos, newName: string) => typedError<Renamed, IdeError>(__TAURI_INVOKE("lsp_rename", { request, doc, pos, newName })),
|
||||
/** The class at `pos` and the members Pull Members Up / Push Members Down can move. */
|
||||
lspHierarchyMembers: (request: RequestId, doc: DocId, pos: Pos, kind: HierarchyRefactoring) => typedError<HierarchyMembers, IdeError>(__TAURI_INVOKE("lsp_hierarchy_members", { request, doc, pos, kind })),
|
||||
/** Checks Pull Members Up / Push Members Down and computes the edit without applying it. */
|
||||
lspHierarchyRefactor: (request: RequestId, doc: DocId, pos: Pos, kind: HierarchyRefactoring, target: string | null, choices: HierarchyChoice[]) => typedError<HierarchyRefactored, IdeError>(__TAURI_INVOKE("lsp_hierarchy_refactor", { request, doc, pos, kind, target, choices })),
|
||||
lspFormat: (doc: DocId, range: {
|
||||
start: Pos,
|
||||
end: Pos,
|
||||
@@ -3822,6 +3826,19 @@ export type GradleTask = {
|
||||
description: string | null,
|
||||
};
|
||||
|
||||
export type HierarchyChoice = {
|
||||
id: string,
|
||||
makeAbstract: boolean,
|
||||
};
|
||||
|
||||
export type HierarchyClass = {
|
||||
/** Server-owned identity, passed back as the pull-up destination. */
|
||||
id: string,
|
||||
name: string,
|
||||
qualifiedName: string,
|
||||
interface: boolean,
|
||||
};
|
||||
|
||||
export type HierarchyDirection =
|
||||
/** Callers / supertypes. */
|
||||
"incoming" |
|
||||
@@ -3842,6 +3859,38 @@ export type HierarchyItem = {
|
||||
fromRanges: Range[],
|
||||
};
|
||||
|
||||
export type HierarchyMember = {
|
||||
id: string,
|
||||
/** `name(Params) : Type`, as the server renders it. */
|
||||
label: string,
|
||||
kind: HierarchyMemberKind,
|
||||
/** The member at the caret. */
|
||||
selected: boolean,
|
||||
/** Can be declared abstract in the destination (pull up) or kept abstract (push down). */
|
||||
abstractable: boolean,
|
||||
};
|
||||
|
||||
export type HierarchyMemberKind = "field" | "method" | "type";
|
||||
|
||||
/**
|
||||
* The class at a position and what can move: pull-up destinations, or the subclasses
|
||||
* members are pushed into.
|
||||
*/
|
||||
export type HierarchyMembers = {
|
||||
className: string,
|
||||
targets: HierarchyClass[],
|
||||
members: HierarchyMember[],
|
||||
};
|
||||
|
||||
/** A checked refactoring: what the server found, and the edit unless a problem is fatal. */
|
||||
export type HierarchyRefactored = {
|
||||
problems: RefactoringProblem[],
|
||||
edit: WorkspaceEdit | null,
|
||||
};
|
||||
|
||||
/** IDEA's class-hierarchy refactorings. */
|
||||
export type HierarchyRefactoring = "pullUp" | "pushDown";
|
||||
|
||||
export type Highlight = {
|
||||
range: Range,
|
||||
kind: HighlightKind,
|
||||
@@ -5368,6 +5417,10 @@ export type PreparedRequest = {
|
||||
body: string | null,
|
||||
};
|
||||
|
||||
export type ProblemSeverity = "info" | "warning" | "error" |
|
||||
/** The refactoring can't run at all; there is no edit. */
|
||||
"fatal";
|
||||
|
||||
/** Memory of one row of the breakdown. */
|
||||
export type ProcessMemory = {
|
||||
category: MemoryCategory,
|
||||
@@ -5624,6 +5677,11 @@ export type RecentProject = {
|
||||
branch: string | null,
|
||||
};
|
||||
|
||||
export type RefactoringProblem = {
|
||||
severity: ProblemSeverity,
|
||||
message: string,
|
||||
};
|
||||
|
||||
export type ReferenceAccess = "read" | "write" | "readWrite" | "unknown";
|
||||
|
||||
export type ReferenceKind = "getter" | "setter" | "field" | "unknown";
|
||||
|
||||
@@ -716,6 +716,8 @@ export class EditorController implements SlotHost {
|
||||
{ kind: "action", action: "refactor.extractConstant" },
|
||||
{ kind: "action", action: "refactor.extractMethod" },
|
||||
{ kind: "action", action: "refactor.inline" },
|
||||
{ kind: "action", action: "refactor.pullMembersUp" },
|
||||
{ kind: "action", action: "refactor.pushMembersDown" },
|
||||
{ kind: "separator" },
|
||||
{ kind: "action", action: "refactor.safeDelete" },
|
||||
],
|
||||
|
||||
@@ -19,7 +19,13 @@ import { workflowSettings, type ActionsOnSaveValid } from "@/platform/workflowSe
|
||||
import { startRename } from "./rename";
|
||||
import { formatDocument, reformatDialog } from "./format";
|
||||
import { showHint } from "./hint";
|
||||
import { refactorThis, runRefactoring, safeDelete, showNothingHere } from "@/editor/refactor";
|
||||
import {
|
||||
refactorThis,
|
||||
runRefactoring,
|
||||
safeDelete,
|
||||
showNothingHere,
|
||||
startHierarchyRefactoring,
|
||||
} from "@/editor/refactor";
|
||||
import { evaluateWhen } from "@/platform/context";
|
||||
|
||||
/** Editor actions apply to the active text editor unless a tool window or popup owns the keyboard. */
|
||||
@@ -314,6 +320,12 @@ export function registerLspActions(): () => void {
|
||||
{ keyWhen: GENERATE_KEY_WHEN },
|
||||
),
|
||||
editorAction("refactor.inline", "Inline…", "Refactor", (v) => runRefactoring(v, "inline")),
|
||||
editorAction("refactor.pullMembersUp", "Pull Members Up…", "Refactor", (v) =>
|
||||
startHierarchyRefactoring(v, "pullUp"),
|
||||
),
|
||||
editorAction("refactor.pushMembersDown", "Push Members Down…", "Refactor", (v) =>
|
||||
startHierarchyRefactoring(v, "pushDown"),
|
||||
),
|
||||
// Implement / Override Methods: the server's code actions for them (jdtls "Override/Implement
|
||||
// Methods…", rust-analyzer "Implement missing members", TS "Implement interface"…).
|
||||
editorAction("code.implementMethods", "Implement Methods…", "Code", (v) =>
|
||||
|
||||
@@ -39,6 +39,7 @@ import { DOCUMENTATION_WINDOW } from "./docs";
|
||||
import { prefetchMarkdown } from "./markdown";
|
||||
import { outsideProjectBanner } from "./outsideProject";
|
||||
import { implementationGutter, registerNavigationActions } from "@/editor/navigation";
|
||||
import { HierarchyDialog } from "@/editor/refactor";
|
||||
|
||||
export function lspExtensions(ctx: EditorDocContext): Extension[] {
|
||||
return [
|
||||
@@ -74,6 +75,7 @@ export function activate(): () => void {
|
||||
// After the templates (order 90): live template / postfix expansion keeps ⇥ first.
|
||||
registerEditorContribution({ id: "lsp.parameterTab", order: 95, extensions: () => parameterTabKeys }),
|
||||
registerOverlay({ id: "editor-lsp.popups", component: LspPopups }),
|
||||
registerOverlay({ id: "editor-lsp.hierarchyDialog", component: HierarchyDialog }),
|
||||
registerEditorBanner(outsideProjectBanner),
|
||||
registerStatusItem({ id: "editor-lsp.usagesStatus", align: "left", priority: 10, component: UsagesStatus }),
|
||||
// IDEA's Documentation tool window: right side, listed once F1 moved documentation there.
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 14px;
|
||||
font-size: var(--fs-ui);
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.targets {
|
||||
color: var(--fg-muted);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.head,
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 110px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 8px;
|
||||
min-height: var(--h-list-row);
|
||||
}
|
||||
|
||||
.head {
|
||||
color: var(--fg-muted);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.rows {
|
||||
max-height: 280px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.row:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.member {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-family: var(--font-mono);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: var(--fg-subtle);
|
||||
font-size: var(--fs-small);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--danger);
|
||||
font-size: var(--fs-small);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.busy {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--fg-muted);
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/** IDEA's Pull Members Up and Push Members Down dialogs (`hierarchy.ts`). */
|
||||
import { useState } from "react";
|
||||
import { Checkbox, Dialog, Select, Spinner, T } from "@/ui";
|
||||
import { useTx } from "@/platform/i18n";
|
||||
import {
|
||||
abstractForced,
|
||||
choicesOf,
|
||||
initialRows,
|
||||
refactorHierarchy,
|
||||
titleOf,
|
||||
useHierarchyDialog,
|
||||
type HierarchyRequest,
|
||||
type MemberRow,
|
||||
} from "./hierarchy";
|
||||
import styles from "./HierarchyDialog.module.css";
|
||||
|
||||
export function HierarchyDialog() {
|
||||
const request = useHierarchyDialog((s) => s.request);
|
||||
if (!request) return null;
|
||||
return <HierarchyDialogInner key={request.seq} request={request} />;
|
||||
}
|
||||
|
||||
function HierarchyDialogInner({ request }: { request: HierarchyRequest }) {
|
||||
const tx = useTx();
|
||||
const { kind, info } = request;
|
||||
const pullUp = kind === "pullUp";
|
||||
const [target, setTarget] = useState<string | null>(info.targets[0]?.id ?? null);
|
||||
const [rows, setRows] = useState<MemberRow[]>(() => initialRows(info));
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const toInterface = pullUp && !!info.targets.find((t) => t.id === target)?.interface;
|
||||
const ready = rows.some((r) => r.checked) && (!pullUp || !!target) && !busy;
|
||||
|
||||
const close = () => {
|
||||
useHierarchyDialog.setState({ request: null });
|
||||
request.view.focus();
|
||||
};
|
||||
const update = (id: string, change: Partial<MemberRow>) =>
|
||||
setRows((all) => all.map((r) => (r.id === id ? { ...r, ...change } : r)));
|
||||
const submit = async (preview: boolean) => {
|
||||
if (!ready) return;
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
const outcome = await refactorHierarchy(
|
||||
request,
|
||||
pullUp ? target : null,
|
||||
choicesOf(rows, kind, toInterface),
|
||||
preview,
|
||||
);
|
||||
if (useHierarchyDialog.getState().request !== request) return;
|
||||
setBusy(false);
|
||||
if (outcome.status === "done") close();
|
||||
else if (outcome.error) setError(outcome.error);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open
|
||||
onClose={close}
|
||||
title={titleOf(kind)}
|
||||
width={560}
|
||||
buttons={[
|
||||
{ label: "Preview", role: "extra", disabled: !ready, onClick: () => void submit(true) },
|
||||
{ label: "Refactor", role: "default", disabled: !ready, onClick: () => void submit(false) },
|
||||
{ label: "Cancel", role: "cancel" },
|
||||
]}
|
||||
footerStart={
|
||||
busy ? (
|
||||
<span className={styles.busy}>
|
||||
<Spinner size={12} />
|
||||
<T>Checking for conflicts…</T>
|
||||
</span>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<div className={styles.body}>
|
||||
{pullUp ? (
|
||||
<label className={styles.field}>
|
||||
<span>{tx`Pull up members of ${info.className} to:`}</span>
|
||||
<Select
|
||||
aria-label="Destination class"
|
||||
value={target}
|
||||
options={info.targets.map((t) => ({
|
||||
value: t.id,
|
||||
label: t.qualifiedName,
|
||||
icon: t.interface ? "interface" : "class",
|
||||
}))}
|
||||
onChange={setTarget}
|
||||
/>
|
||||
</label>
|
||||
) : (
|
||||
<div className={styles.field}>
|
||||
<span>{tx`Push members of ${info.className} down to:`}</span>
|
||||
<span className={styles.targets}>{info.targets.map((t) => t.name).join(", ")}</span>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={styles.table}
|
||||
role="table"
|
||||
aria-label={pullUp ? tx("Members to be pulled up") : tx("Members to be pushed down")}
|
||||
>
|
||||
<div className={styles.head} role="row">
|
||||
<span role="columnheader">
|
||||
<T>Member</T>
|
||||
</span>
|
||||
<span role="columnheader">{pullUp ? <T>Make abstract</T> : <T>Keep abstract</T>}</span>
|
||||
</div>
|
||||
<div className={styles.rows}>
|
||||
{rows.map((row) => {
|
||||
const forced = abstractForced(row, kind, toInterface);
|
||||
return (
|
||||
<div key={row.id} className={styles.row} role="row">
|
||||
<Checkbox
|
||||
className={styles.member}
|
||||
checked={row.checked}
|
||||
onChange={(checked) => update(row.id, { checked })}
|
||||
label={<span className={styles.label}>{row.label}</span>}
|
||||
/>
|
||||
<Checkbox
|
||||
aria-label={pullUp ? "Make abstract" : "Keep abstract"}
|
||||
checked={row.checked && (forced || (row.abstractable && row.makeAbstract))}
|
||||
disabled={!row.checked || forced || !row.abstractable}
|
||||
onChange={(makeAbstract) => update(row.id, { makeAbstract })}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{toInterface && (
|
||||
<span className={styles.hint}>
|
||||
<T>Methods pulled up to an interface are declared abstract there.</T>
|
||||
</span>
|
||||
)}
|
||||
{error && (
|
||||
<span className={styles.error} role="alert">
|
||||
{tx(error)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
* IDEA's refactorings and how language-server code actions map onto them:
|
||||
* `refactor.extract.*` → Extract Method / Introduce Variable…, `refactor.inline*`
|
||||
* → Inline, `refactor.move*` → Move, signature rewrites → Change Signature.
|
||||
* Pull Members Up / Push Members Down come from the Java extension, not code actions.
|
||||
* Pure; used by Refactor This (⌃T) and the Refactor menu actions.
|
||||
*/
|
||||
import type { CodeAction } from "@/bindings";
|
||||
@@ -16,7 +17,9 @@ export type RefactoringId =
|
||||
| "extractConstant"
|
||||
| "extractField"
|
||||
| "extractParameter"
|
||||
| "inline";
|
||||
| "inline"
|
||||
| "pullMembersUp"
|
||||
| "pushMembersDown";
|
||||
|
||||
export interface Refactoring {
|
||||
id: RefactoringId;
|
||||
@@ -94,8 +97,25 @@ export const REFACTORINGS: readonly Refactoring[] = [
|
||||
sel ? "Selected block should represent an expression" : "Caret should be inside an expression in a function",
|
||||
},
|
||||
{ id: "inline", action: "refactor.inline", label: "Inline…", kind: "refactor.inline", reason: () => AT_NAME },
|
||||
{
|
||||
id: "pullMembersUp",
|
||||
action: "refactor.pullMembersUp",
|
||||
label: "Pull Members Up…",
|
||||
kind: null,
|
||||
reason: () => "Caret should be inside a class that has members to pull up",
|
||||
},
|
||||
{
|
||||
id: "pushMembersDown",
|
||||
action: "refactor.pushMembersDown",
|
||||
label: "Push Members Down…",
|
||||
kind: null,
|
||||
reason: () => "Caret should be inside a class that has members to push down",
|
||||
},
|
||||
];
|
||||
|
||||
/** Refactorings of the class hierarchy, run by the app-owned Java extension (`hierarchy.ts`). */
|
||||
export const HIERARCHY_REFACTORINGS: ReadonlySet<RefactoringId> = new Set(["pullMembersUp", "pushMembersDown"]);
|
||||
|
||||
export function refactoring(id: RefactoringId): Refactoring {
|
||||
return REFACTORINGS.find((r) => r.id === id)!;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import type { HierarchyMembers, HierarchyRefactored, WorkspaceEdit } from "@/bindings";
|
||||
|
||||
type Result = { status: "ok"; data: unknown } | { status: "error"; error: { kind: string; message: string } };
|
||||
const ok = (data: unknown): Result => ({ status: "ok", data });
|
||||
const refused = (message: string): Result => ({ status: "error", error: { kind: "invalidInput", message } });
|
||||
|
||||
let members: Result = ok(null);
|
||||
let refactored: Result = ok(null);
|
||||
const lspHierarchyMembers = vi.fn(async () => members);
|
||||
const lspHierarchyRefactor = vi.fn(async () => refactored);
|
||||
const applyWorkspaceEdit = vi.fn(async () => ok(null));
|
||||
vi.mock("@/platform/ipc", async (orig) => {
|
||||
const actual = await orig<typeof import("@/platform/ipc")>();
|
||||
return {
|
||||
...actual,
|
||||
isTauri: () => true,
|
||||
commands: {
|
||||
...actual.commands,
|
||||
lspHierarchyMembers,
|
||||
lspHierarchyRefactor,
|
||||
applyWorkspaceEdit,
|
||||
cancelRequest: vi.fn(async () => undefined),
|
||||
},
|
||||
};
|
||||
});
|
||||
const showHint = vi.fn();
|
||||
vi.mock("@/editor/lsp/hint", () => ({ showHint }));
|
||||
let answer = "continue";
|
||||
const ask = vi.fn(async () => ({ button: answer }));
|
||||
vi.mock("@/workbench/messages", () => ({ ask }));
|
||||
|
||||
const {
|
||||
abstractForced,
|
||||
choicesOf,
|
||||
conflicts,
|
||||
fatalProblem,
|
||||
initialRows,
|
||||
refactorHierarchy,
|
||||
startHierarchyRefactoring,
|
||||
useHierarchyDialog,
|
||||
} = await import("./hierarchy");
|
||||
const { useLspPopup, closePopup } = await import("@/editor/lsp/popupStore");
|
||||
const { lspDoc } = await import("@/editor/lsp/lsp");
|
||||
|
||||
const INFO: HierarchyMembers = {
|
||||
className: "Dog",
|
||||
targets: [
|
||||
{ id: "animal", name: "Animal", qualifiedName: "zoo.Animal", interface: false },
|
||||
{ id: "speaker", name: "Speaker", qualifiedName: "zoo.Speaker", interface: true },
|
||||
],
|
||||
members: [
|
||||
{ id: "tail", label: "tail : int", kind: "field", selected: false, abstractable: false },
|
||||
{ id: "bark", label: "bark() : String", kind: "method", selected: true, abstractable: true },
|
||||
],
|
||||
};
|
||||
const at = (line: number) => ({ start: { line, col: 0 }, end: { line, col: 0 } });
|
||||
const EDIT = {
|
||||
files: [
|
||||
{ kind: "edit", path: "/p/Dog.java", edits: [{ range: at(1), new_text: "" }] },
|
||||
{ kind: "edit", path: "/p/Animal.java", edits: [{ range: at(2), new_text: "String bark() {}" }] },
|
||||
],
|
||||
} as WorkspaceEdit;
|
||||
const done = (problems: HierarchyRefactored["problems"], edit: WorkspaceEdit | null = EDIT): Result =>
|
||||
ok({ problems, edit } satisfies HierarchyRefactored);
|
||||
|
||||
let view: EditorView | null = null;
|
||||
function editor(readOnly = false) {
|
||||
view = new EditorView({
|
||||
state: EditorState.create({
|
||||
doc: 'class Dog extends Animal { String bark() { return ""; } }',
|
||||
selection: { anchor: 30 },
|
||||
extensions: [lspDoc.of({ doc: 7, path: "/p/Dog.java", language: "java", uiMode: null, readOnly })],
|
||||
}),
|
||||
});
|
||||
return view;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
view?.destroy();
|
||||
view = null;
|
||||
closePopup();
|
||||
useHierarchyDialog.setState({ request: null });
|
||||
answer = "continue";
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("member choices", () => {
|
||||
it("starts from the member at the caret and sends what is checked", () => {
|
||||
const rows = initialRows(INFO);
|
||||
expect(rows.map((r) => [r.id, r.checked])).toEqual([
|
||||
["tail", false],
|
||||
["bark", true],
|
||||
]);
|
||||
rows[0]!.checked = true;
|
||||
rows[1]!.makeAbstract = true;
|
||||
expect(choicesOf(rows, "pullUp", false)).toEqual([
|
||||
{ id: "tail", makeAbstract: false },
|
||||
{ id: "bark", makeAbstract: true },
|
||||
]);
|
||||
});
|
||||
|
||||
it("declares methods abstract in an interface, and never abstracts a field", () => {
|
||||
const rows = initialRows(INFO).map((r) => ({ ...r, checked: true, makeAbstract: true }));
|
||||
const [field, method] = rows;
|
||||
expect(abstractForced(method!, "pullUp", true)).toBe(true);
|
||||
expect(abstractForced(method!, "pushDown", true)).toBe(false);
|
||||
expect(abstractForced(field!, "pullUp", true)).toBe(false);
|
||||
rows[1]!.makeAbstract = false;
|
||||
expect(choicesOf(rows, "pullUp", true)).toEqual([
|
||||
{ id: "tail", makeAbstract: false },
|
||||
{ id: "bark", makeAbstract: true },
|
||||
]);
|
||||
expect(choicesOf(rows, "pushDown", false)[1]).toEqual({ id: "bark", makeAbstract: false });
|
||||
});
|
||||
|
||||
it("separates fatal problems from the ones the user may accept", () => {
|
||||
const problems = [
|
||||
{ severity: "info", message: "i" },
|
||||
{ severity: "warning", message: "w" },
|
||||
{ severity: "error", message: "e" },
|
||||
] as const;
|
||||
expect(fatalProblem([...problems])).toBeNull();
|
||||
expect(conflicts([...problems])).toEqual(["w", "e"]);
|
||||
expect(fatalProblem([...problems, { severity: "fatal", message: "f" }])).toBe("f");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Pull Members Up / Push Members Down", () => {
|
||||
it("opens the dialog with the server's members, at the caret", async () => {
|
||||
members = ok(INFO);
|
||||
const v = editor();
|
||||
await startHierarchyRefactoring(v, "pullUp");
|
||||
expect(lspHierarchyMembers).toHaveBeenCalledWith(expect.any(Number), 7, { line: 0, col: 30 }, "pullUp");
|
||||
const request = useHierarchyDialog.getState().request;
|
||||
expect(request?.info.className).toBe("Dog");
|
||||
expect(request?.kind).toBe("pullUp");
|
||||
});
|
||||
|
||||
it("says why when the class can't be refactored, or the file is read-only", async () => {
|
||||
members = refused("Type has no super types.");
|
||||
await startHierarchyRefactoring(editor(), "pullUp");
|
||||
expect(showHint).toHaveBeenCalledWith(expect.anything(), "Cannot perform refactoring. Type has no super types");
|
||||
expect(useHierarchyDialog.getState().request).toBeNull();
|
||||
view!.destroy();
|
||||
await startHierarchyRefactoring(editor(true), "pushDown");
|
||||
expect(showHint).toHaveBeenLastCalledWith(expect.anything(), "Cannot perform refactoring. The file is read-only");
|
||||
expect(lspHierarchyMembers).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("applies a clean edit at once", async () => {
|
||||
members = ok(INFO);
|
||||
refactored = done([{ severity: "info", message: "fine" }]);
|
||||
await startHierarchyRefactoring(editor(), "pullUp");
|
||||
const request = useHierarchyDialog.getState().request!;
|
||||
const choices = [{ id: "bark", makeAbstract: false }];
|
||||
expect(await refactorHierarchy(request, "animal", choices, false)).toEqual({ status: "done" });
|
||||
expect(lspHierarchyRefactor).toHaveBeenCalledWith(
|
||||
expect.any(Number),
|
||||
7,
|
||||
{ line: 0, col: 30 },
|
||||
"pullUp",
|
||||
"animal",
|
||||
choices,
|
||||
);
|
||||
expect(ask).not.toHaveBeenCalled();
|
||||
expect(applyWorkspaceEdit).toHaveBeenCalledWith(EDIT);
|
||||
});
|
||||
|
||||
it("asks about conflicts first; Cancel changes nothing and keeps the dialog", async () => {
|
||||
members = ok(INFO);
|
||||
refactored = done([{ severity: "error", message: "Field 'tail' is not accessible from type 'Animal'" }]);
|
||||
await startHierarchyRefactoring(editor(), "pullUp");
|
||||
const request = useHierarchyDialog.getState().request!;
|
||||
answer = "cancel";
|
||||
expect(await refactorHierarchy(request, "animal", [{ id: "bark", makeAbstract: false }], false)).toEqual({
|
||||
status: "stay",
|
||||
});
|
||||
expect(ask).toHaveBeenCalledWith(expect.objectContaining({ title: "Problems Detected" }));
|
||||
expect(applyWorkspaceEdit).not.toHaveBeenCalled();
|
||||
answer = "continue";
|
||||
expect((await refactorHierarchy(request, "animal", [{ id: "bark", makeAbstract: false }], false)).status).toBe(
|
||||
"done",
|
||||
);
|
||||
expect(applyWorkspaceEdit).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("keeps the dialog with the reason when nothing can be done", async () => {
|
||||
members = ok(INFO);
|
||||
await startHierarchyRefactoring(editor(), "pushDown");
|
||||
const request = useHierarchyDialog.getState().request!;
|
||||
refactored = done([{ severity: "fatal", message: "No subclasses" }], null);
|
||||
expect(await refactorHierarchy(request, null, [{ id: "bark", makeAbstract: true }], false)).toEqual({
|
||||
status: "stay",
|
||||
error: "No subclasses",
|
||||
});
|
||||
refactored = refused("The class changed; open the dialog again");
|
||||
expect(await refactorHierarchy(request, null, [{ id: "bark", makeAbstract: true }], false)).toEqual({
|
||||
status: "stay",
|
||||
error: "The class changed; open the dialog again",
|
||||
});
|
||||
expect(applyWorkspaceEdit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("refuses a request whose position an edit has moved", async () => {
|
||||
members = ok(INFO);
|
||||
const v = editor();
|
||||
await startHierarchyRefactoring(v, "pullUp");
|
||||
v.dispatch({ changes: { from: 0, insert: "// moved\n" } });
|
||||
const request = useHierarchyDialog.getState().request!;
|
||||
expect(await refactorHierarchy(request, "animal", [{ id: "bark", makeAbstract: false }], false)).toEqual({
|
||||
status: "stay",
|
||||
error: "The file changed; open the dialog again",
|
||||
});
|
||||
expect(lspHierarchyRefactor).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("previews the edit before applying it when asked", async () => {
|
||||
members = ok(INFO);
|
||||
refactored = done([]);
|
||||
await startHierarchyRefactoring(editor(), "pullUp");
|
||||
const request = useHierarchyDialog.getState().request!;
|
||||
expect((await refactorHierarchy(request, "animal", [{ id: "bark", makeAbstract: false }], true)).status).toBe(
|
||||
"done",
|
||||
);
|
||||
const popup = useLspPopup.getState().popup;
|
||||
expect(popup?.kind).toBe("preview");
|
||||
if (popup?.kind !== "preview") return;
|
||||
expect(popup.title).toBe("Pull Members Up from 'Dog'");
|
||||
expect(popup.summary.files).toBe(2);
|
||||
expect(applyWorkspaceEdit).not.toHaveBeenCalled();
|
||||
popup.onConfirm();
|
||||
await vi.waitFor(() => expect(applyWorkspaceEdit).toHaveBeenCalledWith(EDIT));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* Pull Members Up / Push Members Down, IDEA's class-hierarchy refactorings. The
|
||||
* language server (the app-owned JDT extension) lists the class's members and
|
||||
* destinations, checks the chosen move and computes the edit; the dialog
|
||||
* (`HierarchyDialog.tsx`) picks the members, problems are confirmed like IDEA's
|
||||
* "Problems Detected", and the edit is applied as one change (or previewed first).
|
||||
*/
|
||||
import type { Text } from "@codemirror/state";
|
||||
import type { EditorView } from "@codemirror/view";
|
||||
import { create } from "zustand";
|
||||
import type {
|
||||
HierarchyChoice,
|
||||
HierarchyMemberKind,
|
||||
HierarchyMembers,
|
||||
HierarchyRefactoring,
|
||||
Pos,
|
||||
RefactoringProblem,
|
||||
WorkspaceEdit,
|
||||
} from "@/bindings";
|
||||
import { commands, isCancelled, nextRequestId, unavailableText } from "@/platform/ipc";
|
||||
import { tx } from "@/platform/i18n";
|
||||
import { whenAllSynced, whenSynced } from "@/editor/api";
|
||||
import { ask } from "@/workbench/messages";
|
||||
import { docOf, isUnavailable, lspRequest, reportLspError } from "@/editor/lsp/lsp";
|
||||
import { offsetToPos } from "@/editor/lsp/positions";
|
||||
import { showHint } from "@/editor/lsp/hint";
|
||||
import { openPopup } from "@/editor/lsp/popupStore";
|
||||
import { summarizeWorkspaceEdit } from "@/editor/lsp/workspaceEdit";
|
||||
import { cannotPerform } from "./catalog";
|
||||
|
||||
export interface MemberRow {
|
||||
id: string;
|
||||
label: string;
|
||||
kind: HierarchyMemberKind;
|
||||
checked: boolean;
|
||||
/** Pull up: declare abstract in the destination. Push down: keep an abstract declaration. */
|
||||
makeAbstract: boolean;
|
||||
abstractable: boolean;
|
||||
}
|
||||
|
||||
export function initialRows(info: HierarchyMembers): MemberRow[] {
|
||||
return info.members.map((m) => ({
|
||||
id: m.id,
|
||||
label: m.label,
|
||||
kind: m.kind,
|
||||
checked: m.selected,
|
||||
makeAbstract: false,
|
||||
abstractable: m.abstractable,
|
||||
}));
|
||||
}
|
||||
|
||||
/** A method pulled up to an interface is only declared there (JDT, like IDEA, never moves its body). */
|
||||
export function abstractForced(row: MemberRow, kind: HierarchyRefactoring, toInterface: boolean): boolean {
|
||||
return kind === "pullUp" && toInterface && row.kind === "method";
|
||||
}
|
||||
|
||||
export function choicesOf(
|
||||
rows: readonly MemberRow[],
|
||||
kind: HierarchyRefactoring,
|
||||
toInterface: boolean,
|
||||
): HierarchyChoice[] {
|
||||
return rows
|
||||
.filter((r) => r.checked)
|
||||
.map((r) => ({
|
||||
id: r.id,
|
||||
makeAbstract: abstractForced(r, kind, toInterface) || (r.abstractable && r.makeAbstract),
|
||||
}));
|
||||
}
|
||||
|
||||
/** The first fatal problem: nothing can be done. */
|
||||
export function fatalProblem(problems: readonly RefactoringProblem[]): string | null {
|
||||
return problems.find((p) => p.severity === "fatal")?.message ?? null;
|
||||
}
|
||||
|
||||
/** Problems the user confirms before the change is made (IDEA lists errors and warnings). */
|
||||
export function conflicts(problems: readonly RefactoringProblem[]): string[] {
|
||||
return problems.filter((p) => p.severity === "error" || p.severity === "warning").map((p) => p.message);
|
||||
}
|
||||
|
||||
export function titleOf(kind: HierarchyRefactoring): string {
|
||||
return kind === "pullUp" ? "Pull Members Up" : "Push Members Down";
|
||||
}
|
||||
|
||||
export interface HierarchyRequest {
|
||||
seq: number;
|
||||
view: EditorView;
|
||||
kind: HierarchyRefactoring;
|
||||
doc: number;
|
||||
pos: Pos;
|
||||
/** The document `pos` refers to. */
|
||||
text: Text;
|
||||
info: HierarchyMembers;
|
||||
}
|
||||
|
||||
export const useHierarchyDialog = create<{ request: HierarchyRequest | null }>(() => ({ request: null }));
|
||||
let seq = 0;
|
||||
let running: AbortController | null = null;
|
||||
|
||||
function errorText(e: unknown): string {
|
||||
if (isUnavailable(e)) return unavailableText(e, "No language server available");
|
||||
return e instanceof Error ? e.message : String(e);
|
||||
}
|
||||
|
||||
/** Pull Members Up… / Push Members Down…: asks the server what can move, then opens the dialog. */
|
||||
export async function startHierarchyRefactoring(view: EditorView, kind: HierarchyRefactoring): Promise<void> {
|
||||
const ctx = docOf(view);
|
||||
if (!ctx) return;
|
||||
if (ctx.readOnly) {
|
||||
showHint(view, cannotPerform("The file is read-only"));
|
||||
return;
|
||||
}
|
||||
running?.abort();
|
||||
const run = (running = new AbortController());
|
||||
await whenSynced(view);
|
||||
const state = view.state;
|
||||
const pos = offsetToPos(state.doc, state.selection.main.head);
|
||||
const request = nextRequestId();
|
||||
const abort = () => void commands.cancelRequest(request);
|
||||
run.signal.addEventListener("abort", abort);
|
||||
try {
|
||||
const info = await lspRequest(() => commands.lspHierarchyMembers(request, ctx.doc, pos, kind));
|
||||
if (run.signal.aborted || view.state.doc !== state.doc) return;
|
||||
useHierarchyDialog.setState({ request: { seq: ++seq, view, kind, doc: ctx.doc, pos, text: state.doc, info } });
|
||||
} catch (e) {
|
||||
if (isCancelled(e) || run.signal.aborted) return;
|
||||
showHint(view, cannotPerform(errorText(e)));
|
||||
} finally {
|
||||
run.signal.removeEventListener("abort", abort);
|
||||
if (running === run) running = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function applyEdit(view: EditorView, edit: WorkspaceEdit) {
|
||||
try {
|
||||
await whenAllSynced();
|
||||
await lspRequest(() => commands.applyWorkspaceEdit(edit));
|
||||
} catch (e) {
|
||||
reportLspError(view, e, true);
|
||||
}
|
||||
}
|
||||
|
||||
/** IDEA's "Problems Detected": true to go on anyway. */
|
||||
async function confirmConflicts(messages: readonly string[]): Promise<boolean> {
|
||||
const { button } = await ask({
|
||||
title: "Problems Detected",
|
||||
icon: "warning",
|
||||
body: messages.join("\n"),
|
||||
buttons: [
|
||||
{ id: "continue", label: "Continue", primary: true },
|
||||
{ id: "cancel", label: "Cancel" },
|
||||
],
|
||||
});
|
||||
return button === "continue";
|
||||
}
|
||||
|
||||
export type RefactorOutcome =
|
||||
/** The edit was applied or handed to the preview: close the dialog. */
|
||||
| { status: "done" }
|
||||
/** Stay in the dialog (the user cancelled at the problems); `error` says why nothing can be done. */
|
||||
| { status: "stay"; error?: string };
|
||||
|
||||
/**
|
||||
* Checks the chosen move, confirms its problems and applies the edit, or opens the
|
||||
* preview with `preview`. The dialog stays open until the change is made.
|
||||
*/
|
||||
export async function refactorHierarchy(
|
||||
request: HierarchyRequest,
|
||||
target: string | null,
|
||||
choices: HierarchyChoice[],
|
||||
preview: boolean,
|
||||
): Promise<RefactorOutcome> {
|
||||
const { view, kind } = request;
|
||||
// The position names the class: after an edit it may name another one.
|
||||
if (view.state.doc !== request.text) return { status: "stay", error: "The file changed; open the dialog again" };
|
||||
let refactored;
|
||||
try {
|
||||
await whenSynced(view);
|
||||
refactored = await lspRequest(() =>
|
||||
commands.lspHierarchyRefactor(nextRequestId(), request.doc, request.pos, kind, target, choices),
|
||||
);
|
||||
} catch (e) {
|
||||
if (isCancelled(e)) return { status: "stay" };
|
||||
return { status: "stay", error: errorText(e) };
|
||||
}
|
||||
const fatal = fatalProblem(refactored.problems);
|
||||
if (fatal || !refactored.edit) return { status: "stay", error: fatal ?? "Nothing to change" };
|
||||
const found = conflicts(refactored.problems);
|
||||
if (found.length && !(await confirmConflicts(found))) return { status: "stay" };
|
||||
const edit = refactored.edit;
|
||||
const summary = summarizeWorkspaceEdit(edit);
|
||||
if (!summary.files) return { status: "stay", error: "Nothing to change" };
|
||||
if (preview) {
|
||||
openPopup({
|
||||
kind: "preview",
|
||||
title:
|
||||
kind === "pullUp"
|
||||
? tx`Pull Members Up from '${request.info.className}'`
|
||||
: tx`Push Members Down from '${request.info.className}'`,
|
||||
summary,
|
||||
confirmLabel: "Do Refactor",
|
||||
onConfirm: () => void applyEdit(view, edit),
|
||||
});
|
||||
} else {
|
||||
await applyEdit(view, edit);
|
||||
}
|
||||
return { status: "done" };
|
||||
}
|
||||
@@ -1,2 +1,4 @@
|
||||
export { refactorThis, runRefactoring, showNothingHere } from "./refactorThis";
|
||||
export { askUsagesDetected, fileUsages, safeDelete, viewUsages } from "./safeDelete";
|
||||
export { startHierarchyRefactoring } from "./hierarchy";
|
||||
export { HierarchyDialog } from "./HierarchyDialog";
|
||||
@@ -64,6 +64,8 @@ vi.mock("./introduce", () => ({
|
||||
}));
|
||||
const safeDelete = vi.fn();
|
||||
vi.mock("./safeDelete", () => ({ safeDelete }));
|
||||
const startHierarchyRefactoring = vi.fn();
|
||||
vi.mock("./hierarchy", () => ({ startHierarchyRefactoring }));
|
||||
|
||||
const { classifyAction, kindAdvertised, cannotPerform } = await import("./catalog");
|
||||
const { refactorThis, runRefactoring } = await import("./refactorThis");
|
||||
@@ -88,13 +90,19 @@ const answer =
|
||||
ok({ actions, advertisedKinds } satisfies CodeActionQuery);
|
||||
|
||||
let view: EditorView | null = null;
|
||||
function editor(text = "fn main() { let total = price * 2; }") {
|
||||
function editor(text = "fn main() { let total = price * 2; }", language = "rust") {
|
||||
view = new EditorView({
|
||||
state: EditorState.create({
|
||||
doc: text,
|
||||
selection: { anchor: text.indexOf("price") + 1 },
|
||||
extensions: [
|
||||
lspDoc.of({ doc: 1, path: "/p/a.rs", language: "rust", uiMode: null, readOnly: false }),
|
||||
lspDoc.of({
|
||||
doc: 1,
|
||||
path: `/p/a.${language === "java" ? "java" : "rs"}`,
|
||||
language,
|
||||
uiMode: null,
|
||||
readOnly: false,
|
||||
}),
|
||||
inplaceRenameExtension(),
|
||||
],
|
||||
}),
|
||||
@@ -179,6 +187,24 @@ describe("Refactor This", () => {
|
||||
expect(lspCodeActionQuery.mock.calls[0]![3]).toEqual(["refactor"]);
|
||||
});
|
||||
|
||||
it("offers Pull Members Up / Push Members Down in Java files, after IDEA's other refactorings", async () => {
|
||||
query = answer([action("Convert to switch expression", "refactor.rewrite")]);
|
||||
const v = editor("class Dog extends Animal { int price; }", "java");
|
||||
await refactorThis(v, vi.fn());
|
||||
expect(labels()).toEqual(["Pull Members Up…", "Push Members Down…", "Convert to switch expression"]);
|
||||
const items = (useLspPopup.getState().popup as { items: { label: string; run: () => void }[] }).items;
|
||||
items.find((i) => i.label === "Push Members Down…")!.run();
|
||||
expect(startHierarchyRefactoring).toHaveBeenCalledWith(v, "pushDown");
|
||||
closePopup();
|
||||
v.destroy();
|
||||
// Other languages have no server for them; nor does Java while its server is down.
|
||||
await refactorThis(editor(), vi.fn());
|
||||
expect(labels()).not.toContain("Pull Members Up…");
|
||||
query = async () => unavailable;
|
||||
await refactorThis(editor("class Dog {}", "java"), vi.fn());
|
||||
expect(labels()).toEqual(["Nothing here"]);
|
||||
});
|
||||
|
||||
it("runs the mapped server action, Rename and Safe Delete", async () => {
|
||||
prepare = ok({ start: { line: 0, col: 0 }, end: { line: 0, col: 1 } });
|
||||
query = answer([action("Inline variable", "refactor.inline")]);
|
||||
|
||||
@@ -20,6 +20,7 @@ import { caretAnchor, openPopup, type ListPopupItem } from "@/editor/lsp/popupSt
|
||||
import { applyCodeAction } from "@/editor/lsp/intentions";
|
||||
import { planLocalRename } from "@/editor/lsp/syntaxFallback";
|
||||
import {
|
||||
HIERARCHY_REFACTORINGS,
|
||||
REFACTORINGS,
|
||||
cannotPerform,
|
||||
classifyAction,
|
||||
@@ -38,6 +39,10 @@ import {
|
||||
introducesName,
|
||||
} from "./serverIntroduce";
|
||||
import { safeDelete } from "./safeDelete";
|
||||
import { startHierarchyRefactoring } from "./hierarchy";
|
||||
|
||||
/** Languages whose server runs the class-hierarchy refactorings (the app-owned JDT extension). */
|
||||
const HIERARCHY_LANGUAGES = new Set(["java"]);
|
||||
|
||||
/** Outcome of the server query: its answer, or why there is none. */
|
||||
export type ServerAnswer =
|
||||
@@ -214,8 +219,12 @@ async function expressionAtCaret(view: EditorView, signal: AbortSignal): Promise
|
||||
export function refactorThisItems(
|
||||
view: EditorView,
|
||||
answer: ServerAnswer,
|
||||
facts: { symbol: boolean; expression: boolean },
|
||||
run: { rename: (view: EditorView) => unknown; safeDelete: (view: EditorView) => unknown },
|
||||
facts: { symbol: boolean; expression: boolean; hierarchy?: boolean },
|
||||
run: {
|
||||
rename: (view: EditorView) => unknown;
|
||||
safeDelete: (view: EditorView) => unknown;
|
||||
hierarchy?: (view: EditorView, kind: "pullUp" | "pushDown") => unknown;
|
||||
},
|
||||
): ListPopupItem[] {
|
||||
const { mapped, other } = groupActions(answer.status === "ok" ? answer.query.actions : []);
|
||||
const serverUsable = answer.status === "ok";
|
||||
@@ -231,6 +240,8 @@ export function refactorThisItems(
|
||||
else if (actions.length) go = () => runActions(view, r.label.replace(/…$/, ""), actions, r.id);
|
||||
else if (r.id === "rename" && facts.symbol) go = () => run.rename(view);
|
||||
else if (r.id === "safeDelete" && facts.symbol && !docOf(view)?.readOnly) go = () => run.safeDelete(view);
|
||||
else if (HIERARCHY_REFACTORINGS.has(r.id) && facts.hierarchy && serverUsable && run.hierarchy)
|
||||
go = () => run.hierarchy!(view, r.id === "pullMembersUp" ? "pullUp" : "pushDown");
|
||||
else if (INTRODUCE[r.id] && facts.expression && !serverUsable)
|
||||
go = () => introduceFallback(view, INTRODUCE[r.id]!, answer.status === "notReady" ? "notReady" : "error");
|
||||
if (!go) continue;
|
||||
@@ -266,7 +277,13 @@ export async function refactorThis(view: EditorView, rename: (view: EditorView)
|
||||
symbolAtCaret(view, answer.status !== "notReady"),
|
||||
expressionAtCaret(view, run.signal),
|
||||
]);
|
||||
items = refactorThisItems(view, answer, { symbol, expression }, { rename, safeDelete });
|
||||
const hierarchy = HIERARCHY_LANGUAGES.has(ctx.language) && !ctx.readOnly;
|
||||
items = refactorThisItems(
|
||||
view,
|
||||
answer,
|
||||
{ symbol, expression, hierarchy },
|
||||
{ rename, safeDelete, hierarchy: startHierarchyRefactoring },
|
||||
);
|
||||
} catch (e) {
|
||||
if (isCancelled(e)) return;
|
||||
throw e;
|
||||
|
||||
@@ -228,6 +228,8 @@ export const IDEA_ACTIONS: Readonly<Record<string, string>> = {
|
||||
Inline: "refactor.inline",
|
||||
Move: "refactor.move",
|
||||
SafeDelete: "refactor.safeDelete",
|
||||
MembersPullUp: "refactor.pullMembersUp",
|
||||
MemberPushDown: "refactor.pushMembersDown",
|
||||
Run: "run.run",
|
||||
Debug: "run.debug",
|
||||
RunClass: "run.runContext",
|
||||
|
||||
@@ -170,6 +170,8 @@ export const intellijMacos: Record<string, string[]> = {
|
||||
"refactor.inline": ["Meta+Alt+N"],
|
||||
"refactor.move": ["F6"],
|
||||
"refactor.safeDelete": ["Meta+Delete"],
|
||||
"refactor.pullMembersUp": [],
|
||||
"refactor.pushMembersDown": [],
|
||||
|
||||
// File / editor tabs
|
||||
"file.save": ["Meta+S"],
|
||||
|
||||
@@ -171,6 +171,8 @@ export const intellijWindows: Record<string, string[]> = {
|
||||
"refactor.inline": ["Ctrl+Alt+N"],
|
||||
"refactor.move": ["F6"],
|
||||
"refactor.safeDelete": ["Alt+Delete"],
|
||||
"refactor.pullMembersUp": [],
|
||||
"refactor.pushMembersDown": [],
|
||||
|
||||
// File / editor tabs
|
||||
"file.save": ["Ctrl+S"],
|
||||
|
||||
@@ -172,6 +172,8 @@ export const vscodeMacos: Record<string, string[]> = {
|
||||
"refactor.inline": [],
|
||||
"refactor.move": [],
|
||||
"refactor.safeDelete": [],
|
||||
"refactor.pullMembersUp": [],
|
||||
"refactor.pushMembersDown": [],
|
||||
|
||||
// File / editor tabs
|
||||
"file.save": ["Meta+S"],
|
||||
|
||||
@@ -242,6 +242,8 @@
|
||||
"menu.refactor.extractVariable": "Introduce Variable…",
|
||||
"menu.refactor.inline": "Inline…",
|
||||
"menu.refactor.move": "Move…",
|
||||
"menu.refactor.pullMembersUp": "Pull Members Up…",
|
||||
"menu.refactor.pushMembersDown": "Push Members Down…",
|
||||
"menu.refactor.rename": "Rename…",
|
||||
"menu.refactor.safeDelete": "Safe Delete…",
|
||||
"menu.refactor.thisMenu": "Refactor This…",
|
||||
|
||||
@@ -241,6 +241,8 @@
|
||||
"menu.refactor.extractVariable": "Introduire une variable…",
|
||||
"menu.refactor.inline": "Intégrer…",
|
||||
"menu.refactor.move": "Déplacer…",
|
||||
"menu.refactor.pullMembersUp": "Remonter les membres…",
|
||||
"menu.refactor.pushMembersDown": "Descendre les membres…",
|
||||
"menu.refactor.rename": "Renommer…",
|
||||
"menu.refactor.safeDelete": "Suppression sûre…",
|
||||
"menu.refactor.thisMenu": "Refactoriser ceci…",
|
||||
|
||||
@@ -242,6 +242,8 @@
|
||||
"menu.refactor.extractVariable": "변수 도입…",
|
||||
"menu.refactor.inline": "인라인…",
|
||||
"menu.refactor.move": "이동…",
|
||||
"menu.refactor.pullMembersUp": "멤버 끌어올리기…",
|
||||
"menu.refactor.pushMembersDown": "멤버 끌어내리기…",
|
||||
"menu.refactor.rename": "이름 변경…",
|
||||
"menu.refactor.safeDelete": "안전 삭제…",
|
||||
"menu.refactor.thisMenu": "이 항목 리팩터링…",
|
||||
|
||||
@@ -5784,5 +5784,26 @@
|
||||
"Weekly Sonnet limit": "Limite hebdomadaire Sonnet",
|
||||
"Marketplace index URL (https://…/index.json or a folder path)": "URL de l'index de la marketplace (https://…/index.json ou un chemin de dossier)",
|
||||
"Editor Providers": "Fournisseurs d'éditeurs",
|
||||
"Syntax errors": "Erreurs de syntaxe"
|
||||
"Syntax errors": "Erreurs de syntaxe",
|
||||
"Pull Members Up…": "Remonter les membres…",
|
||||
"Push Members Down…": "Descendre les membres…",
|
||||
"Pull Members Up": "Remonter les membres",
|
||||
"Push Members Down": "Descendre les membres",
|
||||
"Caret should be inside a class that has members to pull up": "Le curseur doit se trouver dans une classe ayant des membres à remonter",
|
||||
"Caret should be inside a class that has members to push down": "Le curseur doit se trouver dans une classe ayant des membres à descendre",
|
||||
"Problems Detected": "Problèmes détectés",
|
||||
"Nothing to change": "Rien à modifier",
|
||||
"Pull Members Up from '{name}'": "Remonter les membres de '{name}'",
|
||||
"Push Members Down from '{name}'": "Descendre les membres de '{name}'",
|
||||
"Pull up members of {name} to:": "Remonter les membres de {name} vers :",
|
||||
"Push members of {name} down to:": "Descendre les membres de {name} vers :",
|
||||
"Checking for conflicts…": "Recherche de conflits…",
|
||||
"Destination class": "Classe de destination",
|
||||
"Members to be pulled up": "Membres à remonter",
|
||||
"Members to be pushed down": "Membres à descendre",
|
||||
"Member": "Membre",
|
||||
"Make abstract": "Rendre abstrait",
|
||||
"Keep abstract": "Garder abstrait",
|
||||
"Methods pulled up to an interface are declared abstract there.": "Les méthodes remontées dans une interface y sont déclarées abstraites.",
|
||||
"The file changed; open the dialog again": "Le fichier a changé ; rouvrez la boîte de dialogue"
|
||||
}
|
||||
@@ -5830,5 +5830,26 @@
|
||||
"Could not start Compose project {project}": "Compose 프로젝트 {project}을(를) 시작할 수 없습니다",
|
||||
"Could not stop Compose project {project}": "Compose 프로젝트 {project}을(를) 중지할 수 없습니다",
|
||||
"Could not restart Compose project {project}": "Compose 프로젝트 {project}을(를) 다시 시작할 수 없습니다",
|
||||
"Could not take down Compose project {project}": "Compose 프로젝트 {project}을(를) 중지 및 제거할 수 없습니다"
|
||||
"Could not take down Compose project {project}": "Compose 프로젝트 {project}을(를) 중지 및 제거할 수 없습니다",
|
||||
"Pull Members Up…": "멤버 끌어올리기…",
|
||||
"Push Members Down…": "멤버 끌어내리기…",
|
||||
"Pull Members Up": "멤버 끌어올리기",
|
||||
"Push Members Down": "멤버 끌어내리기",
|
||||
"Caret should be inside a class that has members to pull up": "캐럿은 끌어올릴 멤버가 있는 클래스 안에 있어야 합니다",
|
||||
"Caret should be inside a class that has members to push down": "캐럿은 끌어내릴 멤버가 있는 클래스 안에 있어야 합니다",
|
||||
"Problems Detected": "문제 감지됨",
|
||||
"Nothing to change": "변경할 내용이 없습니다",
|
||||
"Pull Members Up from '{name}'": "'{name}'에서 멤버 끌어올리기",
|
||||
"Push Members Down from '{name}'": "'{name}'에서 멤버 끌어내리기",
|
||||
"Pull up members of {name} to:": "{name}의 멤버를 끌어올릴 위치:",
|
||||
"Push members of {name} down to:": "{name}의 멤버를 끌어내릴 위치:",
|
||||
"Checking for conflicts…": "충돌 확인 중…",
|
||||
"Destination class": "대상 클래스",
|
||||
"Members to be pulled up": "끌어올릴 멤버",
|
||||
"Members to be pushed down": "끌어내릴 멤버",
|
||||
"Member": "멤버",
|
||||
"Make abstract": "추상으로 만들기",
|
||||
"Keep abstract": "추상 선언 유지",
|
||||
"Methods pulled up to an interface are declared abstract there.": "인터페이스로 끌어올린 메서드는 그곳에 추상 메서드로 선언됩니다.",
|
||||
"The file changed; open the dialog again": "파일이 변경되었습니다. 대화상자를 다시 여세요"
|
||||
}
|
||||
@@ -6025,5 +6025,26 @@
|
||||
"Back to Changed Files": "返回更改文件列表",
|
||||
"Default (the module's JDK)": "默认(模块的 JDK)",
|
||||
"Use classpath of module": "使用模块的类路径",
|
||||
"Find by main class": "按主类查找"
|
||||
"Find by main class": "按主类查找",
|
||||
"Pull Members Up…": "上拉成员…",
|
||||
"Push Members Down…": "下推成员…",
|
||||
"Pull Members Up": "上拉成员",
|
||||
"Push Members Down": "下推成员",
|
||||
"Caret should be inside a class that has members to pull up": "光标应位于具有可上拉成员的类中",
|
||||
"Caret should be inside a class that has members to push down": "光标应位于具有可下推成员的类中",
|
||||
"Problems Detected": "检测到问题",
|
||||
"Nothing to change": "没有需要更改的内容",
|
||||
"Pull Members Up from '{name}'": "从 '{name}' 上拉成员",
|
||||
"Push Members Down from '{name}'": "从 '{name}' 下推成员",
|
||||
"Pull up members of {name} to:": "将 {name} 的成员上拉到:",
|
||||
"Push members of {name} down to:": "将 {name} 的成员下推到:",
|
||||
"Checking for conflicts…": "正在检查冲突…",
|
||||
"Destination class": "目标类",
|
||||
"Members to be pulled up": "要上拉的成员",
|
||||
"Members to be pushed down": "要下推的成员",
|
||||
"Member": "成员",
|
||||
"Make abstract": "设为抽象",
|
||||
"Keep abstract": "保留抽象声明",
|
||||
"Methods pulled up to an interface are declared abstract there.": "上拉到接口的方法将在接口中声明为抽象方法。",
|
||||
"The file changed; open the dialog again": "文件已更改,请重新打开对话框"
|
||||
}
|
||||
@@ -242,6 +242,8 @@
|
||||
"menu.refactor.extractVariable": "引入变量…",
|
||||
"menu.refactor.inline": "内联…",
|
||||
"menu.refactor.move": "移动…",
|
||||
"menu.refactor.pullMembersUp": "上拉成员…",
|
||||
"menu.refactor.pushMembersDown": "下推成员…",
|
||||
"menu.refactor.rename": "重命名…",
|
||||
"menu.refactor.safeDelete": "安全删除…",
|
||||
"menu.refactor.thisMenu": "重构此项…",
|
||||
|
||||
Reference in new issue
Заблокувати користувача