Файли
openidea/src/features/run/externalTools.ts
T
Jaw 55836e1b49 chore: Format the codebase with oxfmt and check formatting in CI (#135)
Run oxfmt once over the whole repository (formatting only, no logic changes) and add `pnpm format:check` to the frontend CI job so unformatted code no longer builds up. `.oxfmtrc.json` now skips the generated theme docs, test fixtures and code blocks inside Markdown.

Co-authored-by: jaw52 <[email protected]>
2026-10-28 19:35:16 +08:00

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

Цей файл містить неоднозначні символи Юнікоду
Цей файл містить символи Юнікоду, які можна сплутати з іншими символами. Якщо ви вважаєте, що це зроблено навмисно, можете сміливо ігнорувати це попередження. Щоб показати їх, скористайтеся кнопкою Escape.
/**
* Settings › Tools › External Tools: every enabled tool is an action
* (`externalTools.<id>`, category = its group) that expands IDEA macros
* (`$FilePath$`, `$ProjectFileDir$`…) and runs in the Run tool window.
*/
import { activeEditor } from "@/editor/api";
import { executeAction, registerActions, type Action } from "@/platform/actions";
import { splitCommandLine } from "@/platform/commandLine";
import type { RunConfig } from "@/platform/ipc";
import { useProject } from "@/platform/project";
import { onWorkflowSettingsChange, workflowSettings, type ExternalToolValid } from "@/platform/workflowSettings";
import { notifications } from "@/workbench/contributions";
import { promptValue } from "./dialogs";
import { startRun, useRun } from "./store";
export interface MacroContext {
projectRoot: string;
projectName: string;
filePath: string | null;
line: number | null;
column: number | null;
selectedText: string;
}
const MACRO_RE = /\$([A-Za-z]+)\$/g;
function extOf(name: string): string {
const i = name.lastIndexOf(".");
return i > 0 ? name.slice(i + 1) : "";
}
/** Value of one macro, or `undefined` for unknown macros (left as typed). */
export function macroValue(name: string, ctx: MacroContext): string | undefined {
const file = ctx.filePath;
const dir = file ? file.slice(0, Math.max(file.lastIndexOf("/"), 0)) || "/" : "";
const base = file ? file.slice(file.lastIndexOf("/") + 1) : "";
const rel = (p: string) =>
p === ctx.projectRoot ? "" : p.startsWith(ctx.projectRoot + "/") ? p.slice(ctx.projectRoot.length + 1) : p;
switch (name) {
case "FilePath":
return file ?? "";
case "FileDir":
return dir;
case "FileName":
return base;
case "FileNameWithoutExtension": {
const ext = extOf(base);
return ext ? base.slice(0, -ext.length - 1) : base;
}
case "FileExt":
return extOf(base);
case "FileRelativePath":
return file ? rel(file) : "";
case "FileDirRelativeToProjectRoot":
return file ? rel(dir) : "";
case "ProjectFileDir":
return ctx.projectRoot;
case "ProjectName":
return ctx.projectName;
case "LineNumber":
return ctx.line === null ? "" : String(ctx.line);
case "ColumnNumber":
return ctx.column === null ? "" : String(ctx.column);
case "SelectedText":
return ctx.selectedText;
default:
return undefined;
}
}
/** Expands macros in `text`; `$Prompt$` takes `prompt`. */
export function expandMacros(text: string, ctx: MacroContext, prompt = ""): string {
return text.replace(MACRO_RE, (all, name: string) => (name === "Prompt" ? prompt : (macroValue(name, ctx) ?? all)));
}
function currentContext(): MacroContext | null {
const project = useProject.getState().project;
if (!project) return null;
const ed = activeEditor();
let line: number | null = null;
let column: number | null = null;
let selectedText = "";
if (ed) {
const state = ed.view.state;
const sel = state.selection.main;
const l = state.doc.lineAt(sel.head);
line = l.number;
column = sel.head - l.from + 1;
selectedText = state.sliceDoc(sel.from, sel.to);
}
return {
projectRoot: project.root,
projectName: project.name,
filePath: ed?.ctx.path ?? null,
line,
column,
selectedText,
};
}
/** The run configuration a tool expands to (no `$Prompt$` handling). */
export function toolRunConfig(tool: ExternalToolValid, ctx: MacroContext, prompt = ""): RunConfig {
const program = expandMacros(tool.program.trim(), ctx, prompt);
const args = splitCommandLine(tool.arguments).map((a) => expandMacros(a, ctx, prompt));
const cwd = expandMacros(tool.workingDirectory.trim(), ctx, prompt) || ctx.projectRoot;
return {
id: `external:${tool.id}`,
name: tool.name,
kind: "external",
command: program,
args,
cwd,
env: {},
detected: false,
isTest: false,
debugAdapter: null,
};
}
/** The configuration running `tool` now (macros expanded, `$Prompt$` asked); null when cancelled or not possible. */
export async function externalToolConfig(tool: ExternalToolValid): Promise<RunConfig | null> {
const ctx = currentContext();
if (!ctx) return null;
if (!tool.program.trim()) {
notifications().notify({ level: "warning", title: `External tool '${tool.name}' has no program` });
return null;
}
const uses = (m: string) => [tool.program, tool.arguments, tool.workingDirectory].some((t) => t.includes(m));
let prompt = "";
if (uses("$Prompt$")) {
const value = await promptValue(tool.name);
if (value === null) return null;
prompt = value;
}
if (!ctx.filePath && ["$FilePath$", "$FileDir$", "$FileName$", "$FileRelativePath$"].some(uses)) {
notifications().notify({ level: "warning", title: `'${tool.name}' needs an open file` });
return null;
}
return toolRunConfig(tool, ctx, prompt);
}
export async function runExternalTool(tool: ExternalToolValid) {
const config = await externalToolConfig(tool);
if (!config) return;
const session = await startRun(config, undefined, undefined, { show: tool.openConsole });
if (!session || !tool.synchronizeAfter) return;
// "Synchronize files after execution": reload the project from disk once the tool exits.
let done = false;
const check = () => {
if (done) return;
const cur = useRun.getState().sessions.find((x) => x.id === session.id);
if (cur && cur.startedAt === session.startedAt && cur.status !== "finished" && cur.status !== "failed") return;
done = true;
unsub();
if (cur?.startedAt === session.startedAt) void executeAction("project.refresh");
};
const unsub = useRun.subscribe(check);
check();
}
function toolActions(tools: readonly ExternalToolValid[]): Action[] {
return tools
.filter((t) => t.enabled)
.map((t) => ({
id: `externalTools.${t.id}`,
title: t.name,
category: t.group || "External Tools",
icon: "run",
when: "projectOpen",
keywords: ["external tool", "tools", ...(t.description ? [t.description] : [])],
run: () => void runExternalTool(t),
}));
}
/** Keeps one action per enabled tool registered. */
export function registerExternalTools(): () => void {
let dispose = registerActions(toolActions(workflowSettings().externalTools));
const unsub = onWorkflowSettingsChange(
(s) => s.externalTools,
(tools) => {
dispose();
dispose = registerActions(toolActions(tools));
},
);
return () => {
unsub();
dispose();
};
}