Файли
openidea/src/features/run/externalTools.ts
T
l0ng-ai 1e00ea5ee2 feat: Third pass aligning core experience with IntelliJ IDEA
Project & files
- Project view modes (Project Files / Open Files / All Changed Files /
  Scratches); External Libraries for Python, JDK (src.zip), Node, Cargo, Go,
  Maven/Gradle jars; read-only jar/zip browsing
- ⌘N New popup with type search and more file kinds; New Project, Rename
  Project, Reload All from Disk, Export/Import Settings, Power Save Mode
- File type associations, extra content roots and exclude patterns (indexed
  and watched without reopening the project)

Editor & navigation
- Find bar: search scope (comments/strings), Preserve Case, Exclude, multiline,
  history; Find in Files: Module scope, custom pattern scopes, multiline
- Search Everywhere filters, Assign Shortcut, settings results; named bookmark
  lists, file/folder bookmarks; Structure filters and Show Inherited; Recent
  Locations timestamps and breadcrumbs; Code Vision author
- Completion options, editor tab placement/layout, inline completion (LSP 3.18)
- Spell checking (Typo inspection, ide-spell) and Inspections settings with
  custom profiles; Code Style schemes, formatter markers, Do not format

Run, VCS, services, database
- Typed run configurations (Shell, Program, npm, Gradle, Maven, Docker,
  Compound), Modify options, Before launch, env dialog, Current File,
  Emulate terminal (PTY)
- Git log graph/view options, Go to Hash/Branch/Tag, date filter keeps the
  graph; Pull/Merge/Rebase dialogs, Manage Remotes, Worktrees
- Generic Services host (Docker connections/registries, run sessions); TODO
  scope/changes tabs with changelists; Coverage report import
- Data sources: wire-compatible vendors, URL import, SSL certs, Options,
  Schemas and Advanced tabs, color and comment

Workbench
- Run Anything, Customize Main Toolbar, named tool window layouts, menu
  completeness, Code Cleanup, macros, "Don't Show Again", editor banners
- Chinese translations for the new UI

Claude-Session: https://claude.ai/code/session_01SaPPsVPuk6ZTNsoUCNCbMv
2026-10-25 21:24:00 +08:00

182 рядки
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();
};
}