дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
fix(terminal): configure ConPTY compatibility on Windows (#2180)
Цей коміт міститься в:
1 джерело
047cac209b
коміт
337bf4b1ca
13 files changed
+163
-3
No files matched your search
@@ -4,17 +4,22 @@ import { installTerminalKeys } from "./keys";
|
||||
import { installKeyboardDispatcher, useKeymap } from "@/platform/keymap";
|
||||
import { registerAction } from "@/platform/actions";
|
||||
import { usePlatform } from "@/platform/platformInfo";
|
||||
import type { ITerminalOptions } from "@xterm/xterm";
|
||||
|
||||
const terminal = vi.hoisted(() => ({
|
||||
selection: "selected run output",
|
||||
handler: (_e: KeyboardEvent): boolean => true,
|
||||
input: vi.fn(),
|
||||
options: {} as ITerminalOptions,
|
||||
}));
|
||||
vi.mock("@xterm/xterm", () => ({
|
||||
Terminal: class {
|
||||
cols = 80;
|
||||
rows = 24;
|
||||
options = {};
|
||||
constructor(options: ITerminalOptions) {
|
||||
terminal.options = options;
|
||||
}
|
||||
textarea?: HTMLTextAreaElement;
|
||||
hasSelection() {
|
||||
return !!terminal.selection;
|
||||
@@ -56,13 +61,29 @@ vi.mock("./fit", () => ({
|
||||
settledResize: () => ({ resized() {}, cancel() {} }),
|
||||
}));
|
||||
|
||||
beforeEach(() => usePlatform.setState({ os: "windows" }));
|
||||
afterEach(() => usePlatform.setState({ os: "macos" }));
|
||||
beforeEach(() => usePlatform.setState({ os: "windows", windowsBuildNumber: null }));
|
||||
afterEach(() => usePlatform.setState({ os: "macos", windowsBuildNumber: null }));
|
||||
afterEach(() => {
|
||||
document.body.replaceChildren();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["windows", 19045, { backend: "conpty", buildNumber: 19045 }],
|
||||
["windows", 22631, { backend: "conpty", buildNumber: 22631 }],
|
||||
["windows", null, { backend: "conpty", buildNumber: undefined }],
|
||||
["macos", null, undefined],
|
||||
["linux", null, undefined],
|
||||
] as const)("configures embedded terminals for %s build %s", (os, windowsBuildNumber, expected) => {
|
||||
usePlatform.setState({ os, windowsBuildNumber });
|
||||
const term = createEmbeddedTerminal(document.createElement("div"), { onData() {}, onResize() {} });
|
||||
try {
|
||||
expect(terminal.options.windowsPty).toEqual(expected);
|
||||
} finally {
|
||||
term.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it("copies embedded Run terminal selections before xterm can send Ctrl+C", () => {
|
||||
const writeText = vi.fn().mockResolvedValue(undefined);
|
||||
Object.defineProperty(navigator, "clipboard", { configurable: true, value: { writeText } });
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useProject } from "@/platform/project";
|
||||
import { usePlatform } from "@/platform/platformInfo";
|
||||
import { workflowSettings } from "@/platform/workflowSettings";
|
||||
import { fitScheduler, settledResize } from "./fit";
|
||||
import { watchWindowsPty, windowsPty } from "./windowsPty";
|
||||
import { findTerminalLinks } from "./links";
|
||||
import { LINK_HANDLER, fontFor, openExternal, openFileLink } from "./session";
|
||||
import { MIN_CONTRAST_RATIO, readTerminalTheme, remeasureWhenFontsLoad } from "./theme";
|
||||
@@ -33,6 +34,7 @@ export function createEmbeddedTerminal(host: HTMLElement, opts: EmbeddedTerminal
|
||||
const settings = workflowSettings().terminal;
|
||||
const font = fontFor(settings);
|
||||
const term = new Terminal({
|
||||
windowsPty: windowsPty(),
|
||||
linkHandler: LINK_HANDLER,
|
||||
fontFamily: font.family,
|
||||
fontSize: font.size,
|
||||
@@ -76,6 +78,7 @@ export function createEmbeddedTerminal(host: HTMLElement, opts: EmbeddedTerminal
|
||||
opts.onResize(cols, rows);
|
||||
});
|
||||
const disposables: IDisposable[] = [
|
||||
watchWindowsPty(term),
|
||||
term.onData((d) => opts.onData(d)),
|
||||
term.onBinary((d) => opts.onData(d)),
|
||||
term.registerLinkProvider({
|
||||
|
||||
@@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { FIT_SETTLE_MS } from "./fit";
|
||||
import { findTerminalLinks, type TerminalLink } from "./links";
|
||||
import { openFileLink, resolveStartCwd, TerminalSession } from "./session";
|
||||
import { usePlatform } from "@/platform/platformInfo";
|
||||
|
||||
const ipc = vi.hoisted(() => ({
|
||||
call: vi.fn(async (v: unknown) => v),
|
||||
@@ -45,6 +46,37 @@ const fileLink = (text: string): Extract<TerminalLink, { kind: "file" }> => {
|
||||
return l;
|
||||
};
|
||||
|
||||
describe("Windows PTY metadata", () => {
|
||||
const initialPlatform = usePlatform.getState();
|
||||
afterEach(() => usePlatform.setState(initialPlatform));
|
||||
|
||||
it.each([
|
||||
["windows", 19045, { backend: "conpty", buildNumber: 19045 }],
|
||||
["windows", 22631, { backend: "conpty", buildNumber: 22631 }],
|
||||
["windows", null, { backend: "conpty", buildNumber: undefined }],
|
||||
["macos", null, {}],
|
||||
["linux", null, {}],
|
||||
] as const)("configures %s build %s", (os, windowsBuildNumber, expected) => {
|
||||
usePlatform.setState({ os, windowsBuildNumber });
|
||||
const session = new TerminalSession({ key: "pty-options", cwd: "/repo", onChange() {}, onFocus() {} });
|
||||
try {
|
||||
expect(session.term.options.windowsPty).toEqual(expected);
|
||||
} finally {
|
||||
session.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it("updates a restored terminal when platform info arrives and unsubscribes on disposal", () => {
|
||||
usePlatform.setState({ os: "windows", windowsBuildNumber: null });
|
||||
const session = new TerminalSession({ key: "restored", cwd: "/repo", onChange() {}, onFocus() {} });
|
||||
usePlatform.setState({ windowsBuildNumber: 22631 });
|
||||
expect(session.term.options.windowsPty).toEqual({ backend: "conpty", buildNumber: 22631 });
|
||||
session.dispose();
|
||||
usePlatform.setState({ windowsBuildNumber: 19045 });
|
||||
expect(session.term.options.windowsPty).toEqual({ backend: "conpty", buildNumber: 22631 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("openFileLink", () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { call, commands, isTauri, type TerminalId } from "@/platform/ipc";
|
||||
import { tx } from "@/platform/i18n";
|
||||
import { editorService, notifications, registerFontZoomSurface } from "@/workbench/contributions";
|
||||
import { fitScheduler, settledResize } from "./fit";
|
||||
import { watchWindowsPty, windowsPty } from "./windowsPty";
|
||||
import { installTerminalDrop } from "./drop";
|
||||
import { installTerminalPaste, pasteTerminalClipboard } from "./paste";
|
||||
import { ImeCommitGate } from "./imeCommit";
|
||||
@@ -248,6 +249,7 @@ export class TerminalSession {
|
||||
const settings = workflowSettings().terminal;
|
||||
const font = fontFor(settings);
|
||||
this.term = new Terminal({
|
||||
windowsPty: windowsPty(),
|
||||
fontFamily: font.family,
|
||||
fontSize: font.size,
|
||||
lineHeight: font.lineHeight,
|
||||
@@ -275,6 +277,7 @@ export class TerminalSession {
|
||||
this.term.loadAddon(this.searchAddon);
|
||||
|
||||
this.disposables.push(
|
||||
watchWindowsPty(this.term),
|
||||
this.searchAddon.onDidChangeResults((r) => {
|
||||
for (const l of this.searchListeners) l({ index: r.resultIndex, count: r.resultCount });
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { afterEach, expect, it } from "vitest";
|
||||
import { windowsPty } from "./windowsPty";
|
||||
import { usePlatform } from "@/platform/platformInfo";
|
||||
import { useRemote } from "@/platform/remote";
|
||||
|
||||
const initialPlatform = usePlatform.getState();
|
||||
const initialRemote = useRemote.getState();
|
||||
afterEach(() => {
|
||||
useRemote.setState(initialRemote);
|
||||
usePlatform.setState(initialPlatform);
|
||||
});
|
||||
|
||||
it.each(["linux", "windows"])("does not apply the desktop build to a remote %s PTY", (os) => {
|
||||
usePlatform.setState({ os: "windows", windowsBuildNumber: 22631 });
|
||||
useRemote.setState({
|
||||
session: { os, host: "test", root: "/repo", target: { kind: "ssh", config: "test" }, commands: [] },
|
||||
});
|
||||
expect(windowsPty()).toBeUndefined();
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { ITerminalOptions, Terminal } from "@xterm/xterm";
|
||||
import { usePlatform } from "@/platform/platformInfo";
|
||||
import { useRemote } from "@/platform/remote";
|
||||
|
||||
/** Report the local ConPTY backend even when the OS build is unavailable. */
|
||||
export function windowsPty(): ITerminalOptions["windowsPty"] {
|
||||
const { os, windowsBuildNumber } = usePlatform.getState();
|
||||
// Remote PTYs must not inherit the desktop's Windows build or backend.
|
||||
if (useRemote.getState().session) return undefined;
|
||||
return os === "windows" ? { backend: "conpty", buildNumber: windowsBuildNumber ?? undefined } : undefined;
|
||||
}
|
||||
|
||||
/** Restored terminals can be constructed before appInfo answers. */
|
||||
export function watchWindowsPty(term: Terminal) {
|
||||
const unsubscribe = usePlatform.subscribe((next, previous) => {
|
||||
if (next.os !== previous.os || next.windowsBuildNumber !== previous.windowsBuildNumber) {
|
||||
term.options.windowsPty = windowsPty() ?? {};
|
||||
}
|
||||
});
|
||||
return { dispose: unsubscribe };
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача