Файли
openidea/src/features/terminal/index.ts
T
l0ng-ai be49490153 feat(terminal): Align the terminal with IDEA's tabs, menu and settings
- ⌘T (Ctrl+Shift+T on Windows) opens a terminal tab while the terminal
  has focus; elsewhere the key keeps its IDE action.
- The tab bar's ▾ menu lists installed shells under "New Predefined
  Session" (login shell, shell integration) and links to Settings.
- Settings › Tools › Terminal: pick the shell path from the installed
  shells or browse for one, "Move focus to the Editor with Escape",
  fallback font, column width and a link to the Terminal keymap.

Claude-Session: https://claude.ai/code/session_01B65qLEKnkeYTGdbwhLpjCu
2026-10-24 13:02:52 +08:00

205 рядки
7.2 KiB
TypeScript
Неформатований Анотація Історія

Цей файл містить неоднозначні символи Юнікоду
Цей файл містить символи Юнікоду, які можна сплутати з іншими символами. Якщо ви вважаєте, що це зроблено навмисно, можете сміливо ігнорувати це попередження. Щоб показати їх, скористайтеся кнопкою Escape.
/**
* Integrated terminal: xterm.js sessions on backend PTYs in the bottom
* "Terminal" tool window (tabs, splits, links, IDEA-style shortcuts).
*/
import { registerActions } from "@/platform/actions";
import { onIdeEvent } from "@/platform/ipc";
import { useProject } from "@/platform/project";
import { onWorkflowSettingsChange } from "@/platform/workflowSettings";
import { registerToolWindow, toolWindowService } from "@/workbench/contributions";
import { lazyComponent } from "@/ui";
import { installTerminalKeys } from "./keys";
import { loadTerminalRuntime, terminalRuntime } from "./loader";
const WINDOW = "terminal";
function projectRoot(): string | null {
return useProject.getState().project?.root ?? null;
}
function focusActiveSoon() {
// Wait for the tool window and pane to mount before focusing xterm.
requestAnimationFrame(() => requestAnimationFrame(() => terminalRuntime()?.activeSession()?.focus()));
}
function shellName(path: string): string {
return path.split(/[\\/]/).pop()!.replace(/\.exe$/i, "") || path;
}
async function openNewTab(dir?: string) {
const root = projectRoot();
const cwd = dir || root;
if (!cwd) return;
const rt = await loadTerminalRuntime();
if (dir) rt.newTabAt(dir);
else rt.newTab(rt.startDirectory(cwd));
toolWindowService().show(WINDOW, true);
focusActiveSoon();
}
export function activate(): () => void {
const disposers: Array<() => void> = [
registerToolWindow({
id: WINDOW,
title: "Terminal",
icon: "toolwindow-terminal",
anchor: "bottom",
order: 10,
secondary: true,
component: lazyComponent(() => loadTerminalRuntime().then((rt) => rt.TerminalView)),
headerContent: lazyComponent(() => loadTerminalRuntime().then((rt) => rt.TerminalTabs)),
}),
registerActions([
{
id: "terminal.new",
title: "New Terminal Tab",
category: "Terminal",
icon: "plus",
when: "projectOpen",
keywords: ["shell", "console"],
// ⌘T is VCS Update elsewhere; in the terminal it opens a tab, as in IDEA.
keyWhen: "terminalFocus",
run: () => openNewTab(),
},
{
id: "terminal.newPredefinedSession",
title: "New Predefined Session",
description: "Opens a terminal tab running the shell at `arg` (a path from `termAvailableShells`).",
category: "Terminal",
icon: "terminal",
when: "projectOpen",
hidden: true,
run: async ({ arg }) => {
const root = projectRoot();
if (typeof arg !== "string" || !arg || !root) return;
const rt = await loadTerminalRuntime();
rt.newTab(rt.startDirectory(root), shellName(arg), { program: arg, args: [], shell: true });
toolWindowService().show(WINDOW, true);
focusActiveSoon();
},
},
{
id: "terminal.openAt",
title: "Open in Terminal",
category: "Terminal",
icon: "terminal",
when: "projectOpen",
hidden: true,
run: ({ arg }) => openNewTab(typeof arg === "string" && arg ? arg : undefined),
},
{
id: "terminal.runCommand",
title: "Run Command in Terminal",
description: "Opens a terminal tab running `{ program, args, name?, cwd? }` instead of the shell.",
category: "Terminal",
icon: "terminal",
when: "projectOpen",
hidden: true,
run: async ({ arg }) => {
const a = arg as { program?: unknown; args?: unknown; name?: unknown; cwd?: unknown } | undefined;
if (!a || typeof a.program !== "string" || !a.program) return;
const args = Array.isArray(a.args) ? a.args.filter((x): x is string => typeof x === "string") : [];
const cwd = (typeof a.cwd === "string" && a.cwd) || projectRoot();
if (!cwd) return;
const rt = await loadTerminalRuntime();
rt.newTab(cwd, typeof a.name === "string" && a.name ? a.name : undefined, { program: a.program, args });
toolWindowService().show(WINDOW, true);
focusActiveSoon();
},
},
{
id: "terminal.splitRight",
title: "Split Right",
category: "Terminal",
icon: "split",
when: "projectOpen",
run: async () => {
const rt = await loadTerminalRuntime();
if (!rt.useTerminals.getState().activeTab) return openNewTab();
rt.splitActive("horizontal");
focusActiveSoon();
},
},
{
id: "terminal.splitDown",
title: "Split Down",
category: "Terminal",
icon: "split-horizontal",
when: "projectOpen",
run: async () => {
const rt = await loadTerminalRuntime();
if (!rt.useTerminals.getState().activeTab) return openNewTab();
rt.splitActive("vertical");
focusActiveSoon();
},
},
{
id: "terminal.find",
title: "Find in Terminal",
category: "Terminal",
icon: "search",
when: "terminalFocus || activeToolWindow == 'terminal'",
keywords: ["search", "scrollback"],
run: () => {
const rt = terminalRuntime();
if (!rt?.activeSession()) return;
toolWindowService().show(WINDOW);
rt.openTerminalFind();
},
},
{
id: "terminal.clear",
title: "Clear Terminal Buffer",
category: "Terminal",
when: "terminalFocus",
run: () => terminalRuntime()?.activeSession()?.clear(),
},
{
id: "terminal.closeSession",
title: "Close Terminal Session",
category: "Terminal",
when: "terminalFocus",
run: () => {
const rt = terminalRuntime();
const s = rt?.activeSession();
if (!rt || !s) return;
const { tabs } = rt.useTerminals.getState();
const last = tabs.length === 1 && tabs[0]!.panes.length === 1;
rt.closePane(s.key);
if (last) toolWindowService().hide(WINDOW);
else focusActiveSoon();
},
},
]),
installTerminalKeys(),
onWorkflowSettingsChange(
(s) => s.terminal,
() => terminalRuntime()?.applySettingsToAll(),
),
// Font "same as editor" follows Editor › Font.
useProject.subscribe((s, prev) => {
if (s.settings?.editor !== prev.settings?.editor) terminalRuntime()?.applySettingsToAll();
}),
onIdeEvent("terminalExited", (e) => terminalRuntime()?.sessionForTerminalId(e.terminal)?.exited(e.code)),
// Sessions belong to the project; closing / switching it ends them.
useProject.subscribe((s, prev) => {
const rt = terminalRuntime();
if (rt && s.project?.root !== prev.project?.root) {
if (rt.useTerminalFind.getState().open) rt.closeTerminalFind();
rt.closeAll();
}
}),
];
if (typeof MutationObserver !== "undefined" && typeof document !== "undefined") {
const observer = new MutationObserver(() => terminalRuntime()?.applyThemeToAll());
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme", "style"] });
disposers.push(() => observer.disconnect());
}
return () => {
disposers.reverse().forEach((d) => d());
terminalRuntime()?.closeAll();
};
}