Файли
openidea/src/features/terminal/embedded.ts
T
胡翀 55f42d2771 fix(editor): match IDEA font weights in the light scheme (#35)
Root cause: the editor had no font weight settings, and the WebView rendering of JetBrains Mono looks lighter than IDEA (#32).
Fix (IDEA's Settings › Editor › Font › Typography Settings): configurable Main font weight and Bold font weight (defaults Regular 400 / Bold 700) via --fw-code / --fw-code-bold; colour-scheme bold/regular overrides still win; settings preview updates live. Replaces the Light-only 450/550 hard-coding.
Siblings covered: sticky lines, diff/merge, run console (ANSI bold), Find in Files, completion/parameter info/docs, database editor, debugger value popup, matched braces, terminal with "Use editor font".
Verified: scoped vitest, ide-core tests, in-app on macOS (settings UI, editor weight follows the setting end to end).

Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
2026-10-27 10:41:56 +08:00

113 рядки
4.0 KiB
TypeScript

/**
* Terminal widgets embedded in other views (the `embeddedTerminal` service):
* an xterm.js instance with the terminal's font, theme and links, whose input
* and size go to the owner instead of a terminal PTY (a Run console emulating
* a terminal feeds its process).
*/
import { Terminal, type IDisposable } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import "@xterm/xterm/css/xterm.css";
import type { EmbeddedTerminal, EmbeddedTerminalOptions } from "@/workbench/contributions";
import { useProject } from "@/platform/project";
import { workflowSettings } from "@/platform/workflowSettings";
import { findTerminalLinks } from "./links";
import { fontFor, openExternal, openFileLink } from "./session";
import { readTerminalTheme, remeasureWhenFontsLoad } from "./theme";
const live = new Set<Terminal>();
/** Follows a theme change (called with the terminal sessions' own update). */
export function applyThemeToEmbedded() {
const theme = readTerminalTheme();
for (const t of live) t.options.theme = theme;
}
export function createEmbeddedTerminal(host: HTMLElement, opts: EmbeddedTerminalOptions): EmbeddedTerminal {
const settings = workflowSettings().terminal;
const font = fontFor(settings);
const term = new Terminal({
fontFamily: font.family,
fontSize: font.size,
lineHeight: font.lineHeight,
letterSpacing: font.letterSpacing,
fontWeight: font.weight,
fontWeightBold: font.boldWeight,
cursorBlink: settings.cursorBlink,
cursorWidth: 2,
cursorInactiveStyle: "outline",
allowProposedApi: true,
scrollback: 10_000,
macOptionIsMeta: settings.optionAsMeta,
macOptionClickForcesSelection: true,
drawBoldTextInBrightColors: false,
minimumContrastRatio: 1,
smoothScrollDuration: 0,
convertEol: false,
theme: readTerminalTheme(),
});
const fit = new FitAddon();
term.loadAddon(fit);
live.add(term);
const disposables: IDisposable[] = [
term.onData((d) => opts.onData(d)),
term.onBinary((d) => opts.onData(d)),
term.onResize(({ cols, rows }) => opts.onResize(cols, rows)),
term.registerLinkProvider({
provideLinks(y, callback) {
const line = term.buffer.active.getLine(y - 1);
const found = line ? findTerminalLinks(line.translateToString(true)) : [];
if (!found.length) return callback(undefined);
callback(
found.map((l) => ({
text: l.text,
range: { start: { x: l.start + 1, y }, end: { x: l.end, y } },
decorations: { underline: true, pointerCursor: true },
activate: (event: MouseEvent) => {
event.preventDefault();
if (l.kind === "url") void openExternal(l.url);
else void openFileLink(l, opts.cwd ?? useProject.getState().project?.root ?? "");
},
})),
);
},
}),
];
term.open(host);
if (opts["aria-label"]) term.textarea?.setAttribute("aria-label", opts["aria-label"]);
let raf = 0;
const doFit = () => {
raf = 0;
if (!host.isConnected || host.offsetWidth === 0 || host.offsetHeight === 0) return;
const dims = fit.proposeDimensions();
if (!dims || !Number.isFinite(dims.cols) || !Number.isFinite(dims.rows)) return;
if (dims.cols !== term.cols || dims.rows !== term.rows) term.resize(dims.cols, dims.rows);
};
const scheduleFit = () => {
if (!raf) raf = requestAnimationFrame(doFit);
};
const observer = new ResizeObserver(scheduleFit);
observer.observe(host);
scheduleFit();
remeasureWhenFontsLoad(term, font, () => live.has(term), scheduleFit);
// The owner learns the initial size even when it matches xterm's default.
queueMicrotask(() => opts.onResize(term.cols, term.rows));
return {
write: (data) => term.write(data),
clear: () => term.reset(),
focus: () => term.focus(),
fit: () => {
cancelAnimationFrame(raf);
doFit();
},
dispose: () => {
cancelAnimationFrame(raf);
observer.disconnect();
for (const d of disposables) d.dispose();
live.delete(term);
term.dispose();
},
};
}