дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
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
182 рядки
6.4 KiB
TypeScript
182 рядки
6.4 KiB
TypeScript
/**
|
||
* 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();
|
||
};
|
||
}
|