дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
fix(terminal): configure ConPTY compatibility on Windows (#2180)
Цей коміт міститься в:
1 джерело
047cac209b
коміт
337bf4b1ca
13 files changed
+163
-3
No files matched your search
@@ -22,4 +22,4 @@ tempfile.workspace = true
|
||||
libc = "0.2"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { version = "0.61", features = ["Win32_Globalization", "Win32_Security", "Win32_System_Registry", "Win32_Foundation", "Win32_System_Diagnostics_ToolHelp", "Win32_System_Threading", "Win32_System_IO"] }
|
||||
windows-sys = { version = "0.61", features = ["Wdk_System_SystemServices", "Win32_Foundation", "Win32_Globalization", "Win32_Security", "Win32_System_Diagnostics_ToolHelp", "Win32_System_IO", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_Threading"] }
|
||||
@@ -16,6 +16,7 @@ pub mod ipc_path;
|
||||
pub mod java_runtime;
|
||||
pub mod jsonc;
|
||||
pub mod output_encoding;
|
||||
pub mod platform;
|
||||
pub mod process;
|
||||
pub mod project_dir;
|
||||
pub mod project_roots;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/// The Windows OS build used by ConPTY; unavailable on other platforms or on query failure.
|
||||
#[cfg(windows)]
|
||||
pub fn windows_build_number() -> Option<u32> {
|
||||
use windows_sys::Wdk::System::SystemServices::RtlGetVersion;
|
||||
use windows_sys::Win32::System::SystemInformation::OSVERSIONINFOW;
|
||||
|
||||
let mut info =
|
||||
OSVERSIONINFOW { dwOSVersionInfoSize: std::mem::size_of::<OSVERSIONINFOW>() as u32, ..Default::default() };
|
||||
// SAFETY: `info` is initialized and declares the size of the writable structure.
|
||||
let status = unsafe { RtlGetVersion(&mut info) };
|
||||
(status >= 0 && info.dwBuildNumber > 0).then_some(info.dwBuildNumber)
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub fn windows_build_number() -> Option<u32> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn windows_build_is_platform_specific() {
|
||||
let build = super::windows_build_number();
|
||||
#[cfg(windows)]
|
||||
assert!(build.is_some_and(|build| build > 0));
|
||||
#[cfg(not(windows))]
|
||||
assert_eq!(build, None);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ use tauri::{Manager, State, Window};
|
||||
pub struct AppInfo {
|
||||
pub version: String,
|
||||
pub platform: String,
|
||||
pub windows_build_number: Option<u32>,
|
||||
pub config_dir: ide_core::IpcPath,
|
||||
/// Linux on a Wayland display: GTK draws the frameless windows' shadow and resize edges.
|
||||
pub wayland: bool,
|
||||
@@ -28,6 +29,7 @@ pub fn app_info(state: State<'_, AppState>) -> AppInfo {
|
||||
AppInfo {
|
||||
version: env!("CARGO_PKG_VERSION").into(),
|
||||
platform: std::env::consts::OS.into(),
|
||||
windows_build_number: ide_core::platform::windows_build_number(),
|
||||
config_dir: state.config_dir.clone().into(),
|
||||
#[cfg(target_os = "linux")]
|
||||
wayland: crate::linux_window_state::is_wayland(),
|
||||
|
||||
@@ -1861,6 +1861,7 @@ export type AndroidDevice = {
|
||||
export type AppInfo = {
|
||||
version: string,
|
||||
platform: string,
|
||||
windowsBuildNumber: number | null,
|
||||
configDir: IpcPath,
|
||||
/** Linux on a Wayland display: GTK draws the frameless windows' shadow and resize edges. */
|
||||
wayland: boolean,
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { loadPlatformInfo, usePlatform } from "./platformInfo";
|
||||
|
||||
const appInfo = vi.hoisted(() => vi.fn());
|
||||
vi.mock("@/platform/ipc", () => ({ isTauri: () => true, commands: { appInfo } }));
|
||||
|
||||
const initialPlatform = usePlatform.getState();
|
||||
afterEach(() => usePlatform.setState(initialPlatform));
|
||||
|
||||
it.each([
|
||||
["windows", 19045],
|
||||
["windows", 22631],
|
||||
["macos", null],
|
||||
["linux", null],
|
||||
] as const)("loads %s OS build %s separately from the app version", async (platform, windowsBuildNumber) => {
|
||||
appInfo.mockResolvedValue({ platform, windowsBuildNumber, version: "0.1.39", wayland: false });
|
||||
await loadPlatformInfo();
|
||||
expect(usePlatform.getState()).toMatchObject({ os: platform, windowsBuildNumber, version: "0.1.39" });
|
||||
});
|
||||
|
||||
it("keeps platform defaults when the query fails", async () => {
|
||||
appInfo.mockRejectedValue(new Error("unavailable"));
|
||||
await loadPlatformInfo();
|
||||
expect(usePlatform.getState()).toEqual(initialPlatform);
|
||||
});
|
||||
@@ -5,6 +5,7 @@ interface PlatformInfo {
|
||||
/** `macos` | `windows` | `linux` | `web`. */
|
||||
os: string;
|
||||
version: string | null;
|
||||
windowsBuildNumber: number | null;
|
||||
/** Native traffic lights overlay the title bar (Tauri, macOS, Overlay style). */
|
||||
overlayTitleBar: boolean;
|
||||
/** Frameless window: the title bar draws the main menu and caption buttons (Tauri, Windows and Linux). */
|
||||
@@ -28,6 +29,7 @@ export const usePlatform = create<PlatformInfo>(() => {
|
||||
return {
|
||||
os,
|
||||
version: null,
|
||||
windowsBuildNumber: null,
|
||||
overlayTitleBar: isTauri() && os === "macos",
|
||||
framelessTitleBar: isTauri() && (os === "windows" || os === "linux"),
|
||||
wayland: false,
|
||||
@@ -41,6 +43,7 @@ export async function loadPlatformInfo() {
|
||||
usePlatform.setState({
|
||||
os: info.platform,
|
||||
version: info.version,
|
||||
windowsBuildNumber: info.windowsBuildNumber,
|
||||
overlayTitleBar: info.platform === "macos",
|
||||
framelessTitleBar: info.platform === "windows" || info.platform === "linux",
|
||||
wayland: info.wayland,
|
||||
|
||||
Reference in new issue
Заблокувати користувача