mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
fix: Close the merge editor on Apply; confirm before terminating a running Run tab
Like IDEA, applying a merge result closes its tab, and closing a Run tab whose process still runs asks before terminating it (with a Settings toggle and "Do not ask again").
This commit is contained in:
1 parent
7df5747418
commit
9da13cc075
13 files changed
+86
-8
No files matched your search
@@ -212,6 +212,8 @@ impl Default for PythonDebuggerSettings {
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct RunSettings {
|
||||
pub confirm_rerun: bool,
|
||||
/// Ask before closing a Run tab whose process is still running.
|
||||
pub confirm_terminate: bool,
|
||||
pub show_on_start: bool,
|
||||
/// Grace period between interrupt and kill when stopping a process.
|
||||
pub termination_timeout_ms: u32,
|
||||
@@ -219,7 +221,7 @@ pub struct RunSettings {
|
||||
|
||||
impl Default for RunSettings {
|
||||
fn default() -> Self {
|
||||
Self { confirm_rerun: true, show_on_start: true, termination_timeout_ms: 3000 }
|
||||
Self { confirm_rerun: true, confirm_terminate: true, show_on_start: true, termination_timeout_ms: 3000 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3522,6 +3522,8 @@ export type RunInfo = {
|
||||
/** Run/Debug configuration behaviour. */
|
||||
export type RunSettings = {
|
||||
confirmRerun?: boolean,
|
||||
/** Ask before closing a Run tab whose process is still running. */
|
||||
confirmTerminate?: boolean,
|
||||
showOnStart?: boolean,
|
||||
/** Grace period between interrupt and kill when stopping a process. */
|
||||
terminationTimeoutMs?: number,
|
||||
|
||||
@@ -65,6 +65,18 @@ describe("EditorController", () => {
|
||||
expect(openFilePaths(useTabs.getState())).toEqual(["/p/a.ts", "/p/b.ts"]);
|
||||
});
|
||||
|
||||
it("closes a tab by its input key", async () => {
|
||||
ctrl = new EditorController(createMemoryBackend());
|
||||
ctrl.start();
|
||||
await ctrl.openFile("/p/a.ts", { focus: false });
|
||||
await ctrl.openFile("/p/b.ts", { focus: false });
|
||||
const key = Object.values(useTabs.getState().panes)
|
||||
.flatMap((p) => p.tabs)
|
||||
.find((t) => t.input.path === "/p/b.ts")!.input.key;
|
||||
ctrl.closeInput(key);
|
||||
expect(openFilePaths(useTabs.getState())).toEqual(["/p/a.ts"]);
|
||||
});
|
||||
|
||||
it("builds IDEA's editor tab menu", async () => {
|
||||
useProject.setState({ project: { name: "p", root: "/p", isGit: true, settings: {} } as never, settings: {} as never });
|
||||
const ids = ["navigate.selectIn", "project.revealInFinder", "vcs.localHistory", "vcs.commit", "project.copyAbsolutePath", "project.rename", "file.settings"];
|
||||
|
||||
@@ -1003,6 +1003,10 @@ export class EditorController implements SlotHost {
|
||||
});
|
||||
}
|
||||
|
||||
closeInput(key: string) {
|
||||
this.apply((s) => closeKey(s, key));
|
||||
}
|
||||
|
||||
activePath(): string | null {
|
||||
return this.activeSlot?.model.path ?? this.activeTabOf()?.input.path ?? null;
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ export function activate(): void | (() => void) {
|
||||
focus: () => ctrl.focus(),
|
||||
openPaths: () => ctrl.openPaths(),
|
||||
closePath: (path) => ctrl.closePath(path),
|
||||
closeInput: (key) => ctrl.closeInput(key),
|
||||
});
|
||||
_provideDocumentAttacher((path) => attachDocument(ctrl.manager, path));
|
||||
_provideCodePreview((path) => previewExtensions(path, ctrl.manager.settings()?.editor));
|
||||
|
||||
@@ -9,7 +9,7 @@ import { runDirectory } from "./navigate";
|
||||
import {
|
||||
acceptsInput,
|
||||
clearConsole,
|
||||
closeSession,
|
||||
closeSessionTab,
|
||||
closeStdin,
|
||||
failedTests,
|
||||
rerunFailed,
|
||||
@@ -217,7 +217,7 @@ export function RunTabs() {
|
||||
items={items}
|
||||
activeId={"session" in current ? current.session.id : current.trace.id}
|
||||
onSelect={(id) => useRun.setState({ activeSession: id })}
|
||||
onClose={(id) => (traces.some((t) => t.id === id) ? closeStackTrace(id) : closeSession(id))}
|
||||
onClose={(id) => (traces.some((t) => t.id === id) ? closeStackTrace(id) : void closeSessionTab(id))}
|
||||
panelIdPrefix="openidea-run"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -36,6 +36,25 @@ export async function confirmRerun(name: string, instances = 1): Promise<boolean
|
||||
return answer !== null;
|
||||
}
|
||||
|
||||
/** IDEA's question before closing a Run tab whose process still runs. Resolves true for Terminate. */
|
||||
export async function confirmTerminate(name: string): Promise<boolean> {
|
||||
const answer = await confirmChecked({
|
||||
title: `Process '${name}' Is Running`,
|
||||
message: `Do you want to terminate the process '${name}'?`,
|
||||
ok: "Terminate",
|
||||
checkbox: "Do not ask again",
|
||||
});
|
||||
if (answer?.checked && isTauri()) {
|
||||
try {
|
||||
await call(commands.settingsSet("global", "run.confirmTerminate", "false"));
|
||||
await loadSettings();
|
||||
} catch {
|
||||
/* keep asking */
|
||||
}
|
||||
}
|
||||
return answer !== null;
|
||||
}
|
||||
|
||||
/** Asks for the value of `$Prompt$`; null when cancelled. */
|
||||
export function promptValue(title: string): Promise<string | null> {
|
||||
return new Promise((resolve) => useRunDialog.setState({ pending: { title, resolve } }));
|
||||
|
||||
@@ -13,10 +13,11 @@ import { ConsoleModel } from "./console";
|
||||
import { ConsoleView, type ConsoleInput } from "./ConsoleView";
|
||||
import { buildRows, mnemonics } from "./ConfigChooser";
|
||||
import { activate } from "./index";
|
||||
import { resetRuns, startRun, useRun, type RunSession } from "./store";
|
||||
import { closeSessionTab, resetRuns, startRun, useRun, type RunSession } from "./store";
|
||||
|
||||
const confirm = vi.hoisted(() => vi.fn(async () => true));
|
||||
vi.mock("./dialogs", () => ({ confirmRerun: confirm, promptValue: vi.fn(), RunDialogsOverlay: () => null }));
|
||||
const confirmTerminate = vi.hoisted(() => vi.fn(async () => true));
|
||||
vi.mock("./dialogs", () => ({ confirmRerun: confirm, confirmTerminate, promptValue: vi.fn(), RunDialogsOverlay: () => null }));
|
||||
|
||||
const config = (id: string, extra: Partial<RunConfig> = {}): RunConfig => ({
|
||||
id,
|
||||
@@ -51,7 +52,7 @@ beforeEach(() => {
|
||||
provideNotificationService({ notify: () => undefined });
|
||||
});
|
||||
|
||||
describe("single-instance runs (IDEA restartRunProfile)", () => {
|
||||
describe("single-instance runs", () => {
|
||||
it("Run on a running configuration asks, then restarts it in its tab", async () => {
|
||||
const app = config("app");
|
||||
useRun.setState({ sessions: [running("run1", app)] });
|
||||
@@ -85,6 +86,26 @@ describe("single-instance runs (IDEA restartRunProfile)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("closing a Run tab", () => {
|
||||
it("asks before terminating a running process and keeps the tab when cancelled", async () => {
|
||||
const app = config("app");
|
||||
useRun.setState({ sessions: [running("run1", app)] });
|
||||
confirmTerminate.mockResolvedValueOnce(false);
|
||||
expect(await closeSessionTab("run1")).toBe(false);
|
||||
expect(confirmTerminate).toHaveBeenCalledWith("app");
|
||||
expect(useRun.getState().sessions).toHaveLength(1);
|
||||
expect(await closeSessionTab("run1")).toBe(true);
|
||||
expect(useRun.getState().sessions).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("closes a finished session without asking", async () => {
|
||||
confirmTerminate.mockClear();
|
||||
useRun.setState({ sessions: [{ ...running("run1", config("app")), status: "finished", exitCode: 0 }] });
|
||||
expect(await closeSessionTab("run1")).toBe(true);
|
||||
expect(confirmTerminate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Stop", () => {
|
||||
let dispose: () => void;
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -13,7 +13,7 @@ import { workflowSettings } from "@/platform/workflowSettings";
|
||||
import { maybeEditorService, notifications, toolWindowService } from "@/workbench/contributions";
|
||||
import { ConsoleModel } from "./console";
|
||||
import { jvmFrameTarget } from "./links";
|
||||
import { confirmRerun } from "./dialogs";
|
||||
import { confirmRerun, confirmTerminate } from "./dialogs";
|
||||
import { createTestParser, frameworkFor, TestResults, withTestReporting, type TestEvent, type TestOutputParser } from "./testing";
|
||||
import { recordTestStates } from "./testing/states";
|
||||
import { failedTestPaths, leafTestPaths, rerunFailedConfig, testsConfig } from "./testing/rerun";
|
||||
@@ -700,6 +700,16 @@ export function clearConsole(sessionId: string) {
|
||||
bump(sessionId);
|
||||
}
|
||||
|
||||
/** Closing a Run tab: a process still running is terminated once the user agrees, as IDEA asks. */
|
||||
export async function closeSessionTab(sessionId: string): Promise<boolean> {
|
||||
const session = getSession(sessionId);
|
||||
if (!session) return false;
|
||||
const running = session.status === "running" || session.status === "starting";
|
||||
if (running && workflowSettings().run.confirmTerminate && !(await confirmTerminate(session.config.name))) return false;
|
||||
closeSession(sessionId);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function closeSession(sessionId: string) {
|
||||
const session = getSession(sessionId);
|
||||
if (!session) return;
|
||||
|
||||
@@ -234,6 +234,7 @@ export const EXECUTION_PAGES: PageSpec[] = [
|
||||
{
|
||||
options: [
|
||||
{ kind: "toggle", key: "run.confirmRerun", label: "Confirm rerun with process termination" },
|
||||
{ kind: "toggle", key: "run.confirmTerminate", label: "Confirm closing a tab with a running process" },
|
||||
{
|
||||
kind: "toggle",
|
||||
key: "run.showOnStart",
|
||||
|
||||
@@ -269,6 +269,8 @@ export function MergeEditor({ input }: { input: EditorInput }) {
|
||||
if (ok !== undefined) {
|
||||
setResolved(true);
|
||||
await refreshNow();
|
||||
// Like IDEA, applying the result closes the merge editor.
|
||||
editorService().closeInput?.(input.key);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ export interface DebuggerSettingsValid {
|
||||
|
||||
export interface RunSettingsValid {
|
||||
confirmRerun: boolean;
|
||||
confirmTerminate: boolean;
|
||||
showOnStart: boolean;
|
||||
terminationTimeoutMs: number;
|
||||
}
|
||||
@@ -211,7 +212,7 @@ export const DEFAULT_WORKFLOW: WorkflowSettings = {
|
||||
skipLibraryCode: true,
|
||||
python: { attachSubprocess: true, gevent: false },
|
||||
},
|
||||
run: { confirmRerun: true, showOnStart: true, terminationTimeoutMs: 3000 },
|
||||
run: { confirmRerun: true, confirmTerminate: true, showOnStart: true, terminationTimeoutMs: 3000 },
|
||||
terminal: {
|
||||
startDirectory: "",
|
||||
env: {},
|
||||
@@ -341,6 +342,7 @@ export function validateRun(raw: unknown): RunSettingsValid {
|
||||
const v = obj(raw);
|
||||
return {
|
||||
confirmRerun: bool(v.confirmRerun, d.confirmRerun),
|
||||
confirmTerminate: bool(v.confirmTerminate, d.confirmTerminate),
|
||||
showOnStart: bool(v.showOnStart, d.showOnStart),
|
||||
terminationTimeoutMs: num(v.terminationTimeoutMs, d.terminationTimeoutMs, WORKFLOW_LIMITS.terminationTimeoutMs, true),
|
||||
};
|
||||
|
||||
@@ -311,6 +311,8 @@ export interface EditorService {
|
||||
openPaths?(): string[];
|
||||
/** Closes every tab showing `path`. */
|
||||
closePath?(path: string): void;
|
||||
/** Closes the tab of the input with this `key` (e.g. a merge editor once applied). */
|
||||
closeInput?(key: string): void;
|
||||
}
|
||||
|
||||
export interface ToolWindowService {
|
||||
|
||||
Reference in new issue
Block a user