Файли
openidea/src/workbench/inputDialogs.tsx
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

168 рядки
5.2 KiB
TypeScript

/**
* Promise-based input dialogs for shell and feature actions (IDEA's
* `Messages.showInputDialog` and a simple chooser):
*
* const name = await promptInput({ title: "Save Layout", label: "Name:" });
* const picked = await chooseItem({ title: "Delete Layout", items: names });
*
* Rendered by `InputDialogsHost` (in `OverlayHost`).
*/
import { useState, type ReactNode } from "react";
import { create } from "zustand";
import { t } from "@/platform/i18n";
import { Dialog, Input, List, T } from "@/ui";
import styles from "./inputDialogs.module.css";
export interface PromptInputOptions {
title: string;
label?: string;
initial?: string;
placeholder?: string;
okLabel?: string;
/** An error message for `value`, or null when valid (default: must not be blank). */
validate?: (value: string) => string | null;
/** Extra content under the field (hints). */
hint?: ReactNode;
}
export interface ChooseItemOptions {
title: string;
items: readonly string[];
/** Shown when `items` is empty. */
empty?: string;
}
type Request =
| { kind: "prompt"; id: number; options: PromptInputOptions; resolve: (v: string | null) => void }
| { kind: "choose"; id: number; options: ChooseItemOptions; resolve: (v: string | null) => void };
const useInputDialogs = create<{ stack: Request[] }>(() => ({ stack: [] }));
let seq = 0;
function open(make: (id: number, finish: (v: string | null) => void) => Request): Promise<string | null> {
return new Promise((resolve) => {
const id = ++seq;
const finish = (v: string | null) => {
useInputDialogs.setState((s) => ({ stack: s.stack.filter((r) => r.id !== id) }));
resolve(v);
};
useInputDialogs.setState((s) => ({ stack: [...s.stack, make(id, finish)] }));
});
}
/** A text field dialog; resolves with the trimmed value, or null when cancelled. */
export function promptInput(options: PromptInputOptions): Promise<string | null> {
return open((id, resolve) => ({ kind: "prompt", id, options, resolve }));
}
/** A list to pick one entry from; resolves with it, or null when cancelled. */
export function chooseItem(options: ChooseItemOptions): Promise<string | null> {
return open((id, resolve) => ({ kind: "choose", id, options, resolve }));
}
export function InputDialogsHost() {
const stack = useInputDialogs((s) => s.stack);
return (
<>
{stack.map((r) =>
r.kind === "prompt" ? (
<PromptDialog key={r.id} options={r.options} done={r.resolve} />
) : (
<ChooseDialog key={r.id} options={r.options} done={r.resolve} />
),
)}
</>
);
}
const notBlank = (v: string) => (v.trim() ? null : "Value must not be empty");
function PromptDialog({ options, done }: { options: PromptInputOptions; done: (v: string | null) => void }) {
const [value, setValue] = useState(options.initial ?? "");
const [touched, setTouched] = useState(false);
const error = (options.validate ?? notBlank)(value);
const submit = () => {
setTouched(true);
if (!error) done(value.trim());
};
return (
<Dialog
open
title={options.title}
onClose={() => done(null)}
width={420}
buttons={[
{ label: options.okLabel ?? "OK", role: "default", disabled: !!error, onClick: submit },
{ label: t("common.cancel"), role: "cancel" },
]}
>
<form
className={styles.form}
onSubmit={(e) => {
e.preventDefault();
submit();
}}
>
{options.label && (
<label className={styles.label}>
<T>{options.label}</T>
</label>
)}
<Input
data-autofocus
aria-label={options.label ?? options.title}
value={value}
placeholder={options.placeholder}
invalid={touched && !!error}
onChange={(e) => {
setTouched(true);
setValue(e.target.value);
}}
/>
{touched && error && (
<div className={styles.error}>
<T>{error}</T>
</div>
)}
{options.hint && <div className={styles.hint}>{options.hint}</div>}
</form>
</Dialog>
);
}
function ChooseDialog({ options, done }: { options: ChooseItemOptions; done: (v: string | null) => void }) {
const [active, setActive] = useState(0);
const pick = options.items[active];
return (
<Dialog
open
title={options.title}
onClose={() => done(null)}
width={360}
buttons={[
{ label: "OK", role: "default", disabled: pick === undefined, onClick: () => pick !== undefined && done(pick) },
{ label: t("common.cancel"), role: "cancel" },
]}
>
<div className={styles.form}>
<List
items={options.items}
getKey={(item) => item}
activeIndex={active}
onActiveChange={setActive}
onActivate={(item) => done(item)}
rowHeight={24}
className={styles.list}
aria-label={options.title}
empty={
<div className={styles.hint}>
<T>{options.empty ?? "Nothing to choose from"}</T>
</div>
}
renderItem={(item, state) => <div className={state.active ? styles.activeRow : styles.row}>{item}</div>}
/>
</div>
</Dialog>
);
}