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

108 lines
4.2 KiB
TypeScript

/**
* Gutter ▶ icons in Dockerfiles (`FROM` → run / build the image) and Compose
* files (`services:` → up all, each service → up it). As in IDEA they start
* temporary Dockerfile / Docker Compose run configurations through `run.runConfig`.
*/
import { RangeSet, StateEffect, StateField, type EditorState, type Extension } from "@codemirror/state";
import { EditorView, GutterMarker, ViewPlugin, gutter, type ViewUpdate } from "@codemirror/view";
import type { EditorDocContext } from "@/editor/api";
import { executeAction } from "@/platform/actions";
import { useProject } from "@/platform/project";
import { iconMarkup, showContextMenu, type MenuItem } from "@/ui";
import { fileTargets, isComposeFile, isDockerfile, targetTitle, type DockerTarget } from "./compose";
import { gutterActions } from "./runTypes";
import styles from "./Services.module.css";
interface Placed {
target: DockerTarget;
pos: number;
}
const setTargets = StateEffect.define<Placed[]>();
const targetsField = StateField.define<Placed[]>({
create: () => [],
update(value, tr) {
for (const e of tr.effects) if (e.is(setTargets)) return e.value;
if (!tr.docChanged) return value;
return value.map((p) => ({ ...p, pos: tr.state.doc.lineAt(tr.changes.mapPos(p.pos, -1)).from }));
},
});
function place(state: EditorState, path: string): Placed[] {
return fileTargets(path, state.doc.toString())
.filter((t) => t.line < state.doc.lines)
.map((target) => ({ target, pos: state.doc.line(target.line + 1).from }));
}
function menu(target: DockerTarget, path: string): MenuItem[] {
const root = useProject.getState().project?.root ?? null;
return gutterActions(target, path, root).map(({ title, config }, i) => ({
label: title,
icon: i === 0 ? "run" : "build-project",
run: () => void executeAction("run.runConfig", { config, temporary: true }),
}));
}
class Marker extends GutterMarker {
constructor(readonly target: DockerTarget, readonly path: string) {
super();
}
override eq(other: GutterMarker): boolean {
return other instanceof Marker && other.path === this.path && targetTitle(other.target, other.path) === targetTitle(this.target, this.path);
}
override toDOM(): Node {
const el = document.createElement("div");
el.className = styles.marker ?? "";
el.title = targetTitle(this.target, this.path);
el.setAttribute("role", "button");
el.setAttribute("aria-label", el.title);
el.innerHTML = iconMarkup(this.target.kind === "composeAll" ? "gutter-run-all" : "gutter-run", 14) ?? "";
return el;
}
}
export function dockerGutter(ctx: EditorDocContext): Extension {
const path = ctx.path;
if (ctx.virtual || !(isDockerfile(path) || isComposeFile(path))) return [];
const detector = ViewPlugin.fromClass(
class {
timer: ReturnType<typeof setTimeout> | undefined;
constructor(readonly view: EditorView) {
this.schedule(0);
}
update(u: ViewUpdate) {
if (u.docChanged) this.schedule(300);
}
schedule(delay: number) {
clearTimeout(this.timer);
this.timer = setTimeout(() => this.view.dispatch({ effects: setTargets.of(place(this.view.state, path)) }), delay);
}
destroy() {
clearTimeout(this.timer);
}
},
);
return [
targetsField,
detector,
gutter({
// Shares the run gutter's class so "Show run icons" hides these too.
class: `${styles.gutter} cm-openidea-gutter-run`,
markers: (view) => RangeSet.of(view.state.field(targetsField).map((p) => new Marker(p.target, path).range(p.pos)), true),
initialSpacer: () => new Marker({ kind: "composeAll", line: 0 }, path),
domEventHandlers: {
mousedown: (view, block, event) => {
const placed = view.state.field(targetsField).find((p) => p.pos === block.from);
if (!placed || (event as MouseEvent).button !== 0) return false;
const el = (event.target as HTMLElement).closest?.(`.${styles.marker}`) ?? (event.target as HTMLElement);
const rect = el.getBoundingClientRect();
event.preventDefault();
showContextMenu({ x: rect.left, y: rect.bottom + 2 }, menu(placed.target, path));
return true;
},
},
}),
];
}