Files
openidea/src/features/services/compose.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

82 lines
3.2 KiB
TypeScript

/**
* Run targets of Dockerfiles (`FROM` lines → build image) and Compose files
* (`services:` → up all, each service → up that service), detected per line like
* the run gutter does (the configurations they start are in `runTypes.ts`).
*/
export type DockerTarget =
| { kind: "build"; line: number; stage: string | null; last: boolean }
| { kind: "composeAll"; line: number }
| { kind: "composeService"; line: number; service: string };
const base = (path: string) => path.slice(path.lastIndexOf("/") + 1);
const dir = (path: string) => path.slice(0, Math.max(0, path.lastIndexOf("/"))) || "/";
export function isDockerfile(path: string): boolean {
const name = base(path);
return /^(Dockerfile|Containerfile)(\.[\w.-]+)?$/i.test(name) || /\.(dockerfile|containerfile)$/i.test(name);
}
export function isComposeFile(path: string): boolean {
return /^(docker-)?compose(\.[\w-]+)?\.ya?ml$/i.test(base(path));
}
/** Every `FROM` line; a named stage (`AS build`) is built with `--target`, the last one whole. */
export function dockerfileTargets(text: string): DockerTarget[] {
const out: Array<Extract<DockerTarget, { kind: "build" }>> = [];
text.split("\n").forEach((line, i) => {
const m = /^\s*FROM\s+(?:--\S+\s+)*\S+(?:\s+AS\s+([\w.-]+))?\s*$/i.exec(line.replace(/\r$/, ""));
if (m) out.push({ kind: "build", line: i, stage: m[1] ?? null, last: false });
});
if (out.length) out[out.length - 1]!.last = true;
return out;
}
const KEY = /^(\s*)("([^"]+)"|'([^']+)'|[A-Za-z0-9._-]+)\s*:\s*(#.*)?$/;
/** The top-level `services:` key and each service directly below it. */
export function composeTargets(text: string): DockerTarget[] {
const lines = text.split("\n").map((l) => l.replace(/\r$/, ""));
const out: DockerTarget[] = [];
const start = lines.findIndex((l) => /^services\s*:\s*(#.*)?$/.test(l));
if (start < 0) return out;
out.push({ kind: "composeAll", line: start });
let indent: number | null = null;
for (let i = start + 1; i < lines.length; i++) {
const line = lines[i]!;
if (!line.trim() || line.trimStart().startsWith("#")) continue;
const lead = line.length - line.trimStart().length;
if (lead === 0) break; // next top-level key
indent ??= lead;
if (lead !== indent) continue;
const m = KEY.exec(line);
if (m) out.push({ kind: "composeService", line: i, service: m[3] ?? m[4] ?? m[2]! });
}
return out;
}
export function fileTargets(path: string, text: string): DockerTarget[] {
if (isDockerfile(path)) return dockerfileTargets(text);
if (isComposeFile(path)) return composeTargets(text);
return [];
}
/** An image tag from the build context's folder name (`My App` → `my-app:latest`). */
export function imageTag(path: string): string {
const folder = base(dir(path))
.toLowerCase()
.replace(/[^a-z0-9._-]+/g, "-")
.replace(/^[-._]+|[-._]+$/g, "");
return `${folder || "image"}:latest`;
}
export function targetTitle(t: DockerTarget, path: string): string {
switch (t.kind) {
case "build":
return t.stage && !t.last ? `Build Stage '${t.stage}'` : `Run '${base(path)}'`;
case "composeAll":
return "Compose Up (All Services)";
case "composeService":
return `Compose Up '${t.service}'`;
}
}