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
Цей коміт міститься в:
l0ng-ai committed 2026-10-25 21:24:00 +08:00
1 джерело 294198a727
коміт 1e00ea5ee2
426 files changed
+85241 -2886

No files matched your search

+85 -4
Переглянути файл
@@ -8,12 +8,15 @@ import {
type StructuralFile,
type StructuralLanguage,
} from "@/platform/ipc";
import { basename } from "@/platform/project";
import { Checkbox, Dialog, Input, Select, Spinner, T } from "@/ui";
import { executeAction } from "@/platform/actions";
import { activeProfileId, listProfiles, validateInspections, type ProfileId } from "@/platform/inspections";
import { basename, useProject } from "@/platform/project";
import { Button, Checkbox, Dialog, Input, Select, Spinner, T } from "@/ui";
import { ScopeFields, openDialog } from "./dialogs";
import { locateDuplicates, loadDuplicateOptions } from "./duplicates";
import { runInspection } from "./inspect";
import { defaultScope, type ScopeChoice } from "./scope";
import { runCodeCleanup, type CleanupOptions } from "./cleanup";
import { currentFile, defaultScope, type ScopeChoice } from "./scope";
import {
applyStructuralReplace,
buildReplaceEdit,
@@ -28,9 +31,21 @@ import styles from "./Analysis.module.css";
function InspectCodeDialog({ onClose }: { onClose: () => void }) {
const [scope, setScope] = useState<ScopeChoice>(() => defaultScope());
const [profile, setProfile] = useState<ProfileId>(activeProfileId);
const hasProject = useProject((s) => !!s.project);
const settings = useProject((s) => s.settings);
const profiles = useMemo(
() =>
listProfiles(validateInspections(settings?.inspections), hasProject).map((p) => ({
value: p.id,
label: p.label,
detail: p.level === "project" ? "Project" : "IDE",
})),
[hasProject, settings],
);
const ok = () => {
onClose();
void runInspection(scope);
void runInspection(scope, profile);
};
return (
<Dialog
@@ -45,6 +60,24 @@ function InspectCodeDialog({ onClose }: { onClose: () => void }) {
>
<div className={styles.form}>
<ScopeFields name="inspect-scope" value={scope} onChange={setScope} />
<div className={styles.fieldRow}>
<span>
<T>Inspection profile:</T>
</span>
<Select
className={styles.grow}
aria-label="Inspection profile"
value={profile}
options={profiles}
onChange={setProfile}
/>
<Button
aria-label="Configure inspections"
onClick={() => void executeAction("file.settings", "editor.inspections")}
>
…
</Button>
</div>
<div className={styles.hint}>
<T>Files are checked by their language servers; files without one are skipped.</T>
</div>
@@ -53,6 +86,53 @@ function InspectCodeDialog({ onClose }: { onClose: () => void }) {
);
}
const CLEANUP_SCOPES = ["project", "directory", "file", "changed"] as const;
function CodeCleanupDialog({ onClose }: { onClose: () => void }) {
const [scope, setScope] = useState<ScopeChoice>(() => defaultScope(currentFile() ? "file" : "project"));
const [options, setOptions] = useState<CleanupOptions>({ fixAll: true, optimizeImports: true, reformat: true });
const any = options.fixAll || options.optimizeImports || options.reformat;
const ok = () => {
if (!any) return;
onClose();
void runCodeCleanup(scope, options);
};
return (
<Dialog
open
title="Run Code Cleanup"
onClose={onClose}
width={440}
buttons={[
{ label: "OK", role: "default", disabled: !any, onClick: ok },
{ label: "Cancel", role: "cancel" },
]}
>
<div className={styles.form}>
<ScopeFields name="cleanup-scope" value={scope} onChange={setScope} kinds={CLEANUP_SCOPES} />
<Checkbox
checked={options.fixAll}
onChange={(fixAll) => setOptions({ ...options, fixAll })}
label="Apply the language servers' automatic fixes"
/>
<Checkbox
checked={options.optimizeImports}
onChange={(optimizeImports) => setOptions({ ...options, optimizeImports })}
label="Optimize imports"
/>
<Checkbox
checked={options.reformat}
onChange={(reformat) => setOptions({ ...options, reformat })}
label="Reformat code"
/>
<div className={styles.hint}>
<T>Files are cleaned up by their language servers and saved; files without one are skipped.</T>
</div>
</div>
</Dialog>
);
}
function DuplicatesDialog({ onClose }: { onClose: () => void }) {
const [scope, setScope] = useState<ScopeChoice>(() => defaultScope());
const [options, setOptions] = useState<DuplicateOptions>(loadDuplicateOptions);
@@ -303,6 +383,7 @@ function ReplacePreviewDialog({ files, onClose }: { files: StructuralFile[]; onC
export const showInspectCodeDialog = () => openDialog((close) => <InspectCodeDialog onClose={close} />);
export const showDuplicatesDialog = () => openDialog((close) => <DuplicatesDialog onClose={close} />);
export const showCodeCleanupDialog = () => openDialog((close) => <CodeCleanupDialog onClose={close} />);
export const showStructuralDialog = (replace: boolean) =>
openDialog((close) => <StructuralDialog replace={replace} onClose={close} />);
export const showReplacePreview = (files: StructuralFile[]) =>
+8
Переглянути файл
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import type { Diagnostic, StructuralFile } from "@/platform/ipc";
import { useCustomScopes } from "@/platform/search";
import { fragmentLines } from "./duplicates";
import { groupInspection, kindOf } from "./inspect";
import { buildInspectionNodes } from "./InspectionView";
@@ -149,6 +150,13 @@ describe("analysis scopes and fragments", () => {
expect(await resolveScope({ kind: "directory", directory: "" })).toBe("Choose a directory");
expect(await resolveScope({ kind: "file", directory: "" })).toBe("No file is open");
expect(await resolveScope({ kind: "changed", directory: "" })).toBe("There are no uncommitted files");
expect(await resolveScope({ kind: "custom", directory: "" })).toBe("Choose a scope");
useCustomScopes.setState({ root: null, scopes: [{ name: "Src", pattern: "file:src//*" }] });
expect(await resolveScope({ kind: "custom", directory: "", custom: "Src" })).toEqual({
kind: "pattern",
pattern: "file:src//*",
});
useCustomScopes.setState({ root: null, scopes: [] });
});
it("numbers fragment lines from the fragment's first line", () => {
+136
Переглянути файл
@@ -0,0 +1,136 @@
/**
* Code › Code Cleanup…: the language servers' fix-all, organize imports and
* reformat over a scope, one file after another (opened, cleaned, saved; tabs
* opened only for the cleanup are closed again). Cancellable from the
* Background Tasks popup.
*/
import { executeAction } from "@/platform/actions";
import { call, commands, isTauri, type FileEntry } from "@/platform/ipc";
import { useProject } from "@/platform/project";
import { maybeEditorService, notifications } from "@/workbench/contributions";
import { confirm } from "@/workbench/messages";
import { removeTask, upsertTask } from "@/workbench/tasks";
import { changedFiles, currentFile, type ScopeChoice } from "./scope";
export interface CleanupOptions {
fixAll: boolean;
optimizeImports: boolean;
reformat: boolean;
}
/** Above this many files the user confirms first. */
const CONFIRM_ABOVE = 100;
const extOf = (path: string) => {
const name = path.slice(path.lastIndexOf("/") + 1);
const i = name.lastIndexOf(".");
return i > 0 ? name.slice(i + 1).toLowerCase() : "";
};
/** Extensions of languages that have a language server (the only files a cleanup can change). */
async function serverExtensions(): Promise<Set<string>> {
const out = new Set<string>();
for (const l of await commands.languages())
if (l.languageServers?.length) for (const e of l.extensions) out.add(e.replace(/^\./, "").toLowerCase());
return out;
}
/** Files under `dir`, skipping ignored, excluded and hidden entries and symlinks. */
export async function walkFiles(
dir: string,
readDir: (path: string) => Promise<FileEntry[]>,
accept: (path: string) => boolean,
limit = 5000,
): Promise<string[]> {
const out: string[] = [];
const queue = [dir];
while (queue.length && out.length < limit) {
let entries: FileEntry[];
try {
entries = await readDir(queue.shift()!);
} catch {
continue;
}
for (const e of entries) {
if (e.ignored || e.excluded || e.hidden || e.kind === "symlink") continue;
if (e.kind === "dir") queue.push(e.path);
else if (accept(e.path)) out.push(e.path);
}
}
return out.sort();
}
/** The files of `choice` a cleanup applies to, or a message why there are none. */
export async function cleanupFiles(choice: ScopeChoice): Promise<string[] | string> {
const root = useProject.getState().project?.root;
if (!root) return "No project is open";
const exts = await serverExtensions();
const accept = (p: string) => exts.has(extOf(p));
const readDir = (p: string) => call(commands.fsReadDir(p));
let files: string[];
switch (choice.kind) {
case "file": {
const f = currentFile();
files = f ? [f] : [];
break;
}
case "changed":
files = (await changedFiles()).filter(accept);
break;
case "directory":
files = choice.directory.trim() ? await walkFiles(choice.directory.trim(), readDir, accept) : [];
break;
default:
files = await walkFiles(root, readDir, accept);
}
return files.length ? files : "No files with a language server in this scope";
}
let running: { cancelled: boolean } | null = null;
export async function runCodeCleanup(choice: ScopeChoice, options: CleanupOptions): Promise<void> {
if (!isTauri() || running) return;
const files = await cleanupFiles(choice);
if (typeof files === "string") {
notifications().notify({ group: "analysis", level: "warning", title: "Code Cleanup", body: files, key: "code.cleanup" });
return;
}
if (
files.length > CONFIRM_ABOVE &&
!(await confirm({ title: "Code Cleanup", message: `Clean up ${files.length} files?`, ok: "Clean Up" }))
)
return;
const editors = maybeEditorService();
const wasOpen = new Set(editors?.openPaths?.() ?? []);
const run: { cancelled: boolean } = { cancelled: false };
running = run;
const id = "code.cleanup";
let done = 0;
try {
for (const path of files) {
if (run.cancelled) break;
upsertTask({
id,
title: "Code Cleanup",
message: path.slice(path.lastIndexOf("/") + 1),
fraction: done / files.length,
done: false,
cancel: () => (run.cancelled = true),
});
await executeAction("code.cleanupFile", { path, ...options });
await editors?.saveAll();
if (!wasOpen.has(path)) editors?.closePath?.(path);
done++;
}
} finally {
running = null;
upsertTask({ id, title: "Code Cleanup", message: null, fraction: 1, done: true });
if (run.cancelled) removeTask(id);
}
notifications().notify({
group: "analysis",
level: "info",
title: run.cancelled ? `Code Cleanup cancelled after ${done} of ${files.length} files` : `Code Cleanup processed ${done} files`,
key: "code.cleanup",
});
}
+29 -4
Переглянути файл
@@ -1,8 +1,10 @@
/** Dialog stack of the analysis feature and the shared "Specify Scope" fields. */
import { Fragment, type ReactNode } from "react";
import { create } from "zustand";
import { executeAction } from "@/platform/actions";
import { isTauri } from "@/platform/ipc";
import { Button, Input, T } from "@/ui";
import { EDIT_SCOPES_ACTION, useCustomScopeList } from "@/platform/search";
import { Button, IconButton, Input, Select, T } from "@/ui";
import { SCOPE_LABELS, currentFile, type ScopeChoice, type ScopeKind } from "./scope";
import styles from "./Analysis.module.css";
@@ -42,32 +44,38 @@ async function browseDirectory(initial: string): Promise<string | null> {
return typeof picked === "string" ? picked : null;
}
const KINDS: ScopeKind[] = ["project", "directory", "file", "changed"];
const KINDS: ScopeKind[] = ["project", "directory", "file", "changed", "custom"];
/** IDEA's "Specify Scope" radio group. */
export function ScopeFields({
value,
onChange,
name,
kinds = KINDS,
}: {
value: ScopeChoice;
onChange: (v: ScopeChoice) => void;
name: string;
/** The choices offered (default: all). */
kinds?: readonly ScopeKind[];
}) {
const file = currentFile();
const scopes = useCustomScopeList();
return (
<fieldset className={styles.scope}>
<legend className={styles.fieldLabel}>
<T>Scope</T>
</legend>
{KINDS.map((kind) => (
{kinds.map((kind) => (
<label key={kind} className={styles.radioRow}>
<input
type="radio"
name={name}
checked={value.kind === kind}
disabled={kind === "file" && !file}
onChange={() => onChange({ ...value, kind })}
onChange={() =>
onChange({ ...value, kind, custom: kind === "custom" ? (value.custom ?? scopes[0]?.name) : value.custom })
}
/>
<span>{kind === "file" && file ? `File '${file.split("/").pop()}'` : SCOPE_LABELS[kind]}</span>
</label>
@@ -87,6 +95,23 @@ export function ScopeFields({
</Button>
</div>
)}
{value.kind === "custom" && (
<div className={styles.dirRow}>
<Select<string>
className={styles.grow}
value={value.custom ?? null}
options={scopes.map((s) => ({ value: s.name, label: s.name, detail: s.pattern }))}
placeholder="No custom scopes"
onChange={(custom) => onChange({ ...value, custom })}
aria-label="Custom scope"
/>
<IconButton
icon="more-horizontal"
title="Edit Scopes…"
onClick={() => void executeAction(EDIT_SCOPES_ACTION, value.custom ? { select: value.custom } : undefined)}
/>
</div>
)}
</fieldset>
);
}
+9 -1
Переглянути файл
@@ -5,7 +5,7 @@
import { registerActions } from "@/platform/actions";
import { useProject } from "@/platform/project";
import { registerOverlay, registerToolWindow } from "@/workbench/contributions";
import { showDuplicatesDialog, showInspectCodeDialog, showStructuralDialog } from "./AnalysisDialogs";
import { showCodeCleanupDialog, showDuplicatesDialog, showInspectCodeDialog, showStructuralDialog } from "./AnalysisDialogs";
import { AnalysisDialogHost } from "./dialogs";
import { resetDuplicates } from "./duplicates";
import { DuplicatesHeaderActions, DuplicatesView } from "./DuplicatesView";
@@ -51,6 +51,14 @@ export function activate(): () => void {
keywords: ["inspection", "analyze", "diagnostics", "problems"],
run: () => void showInspectCodeDialog(),
},
{
id: "code.cleanup",
title: "Code Cleanup…",
category: "Code",
when: "projectOpen",
keywords: ["fix all", "cleanup", "organize imports", "reformat"],
run: () => void showCodeCleanupDialog(),
},
{
id: "analyze.locateDuplicates",
title: "Locate Duplicates…",
+28 -6
Переглянути файл
@@ -14,6 +14,14 @@ import {
type InspectSummary,
type RequestId,
} from "@/platform/ipc";
import {
activeProfileId,
applyProfileMixed,
noteDiagnosticCodes,
producerOf,
profileOverrides,
type ProfileId,
} from "@/platform/inspections";
import { notifications, toolWindowService } from "@/workbench/contributions";
import { describeScope, resolveScope, type ScopeChoice } from "./scope";
@@ -99,6 +107,8 @@ interface InspectState {
error: string | null;
/** Scope of the last run (Rerun). */
last: ScopeChoice | null;
/** Inspection profile of the last run. */
profile: ProfileId | null;
/** Problems excluded from the results (⌫), by `problemId`. */
excluded: ReadonlySet<string>;
}
@@ -112,6 +122,7 @@ const initialInspection = (): InspectState => ({
scopeLabel: null,
error: null,
last: null,
profile: null,
excluded: new Set(),
});
@@ -144,7 +155,8 @@ export function resetInspection() {
useInspection.setState(initialInspection());
}
export async function runInspection(choice: ScopeChoice): Promise<void> {
/** Inspect Code over `choice` with `profileId` (default: the profile in effect). */
export async function runInspection(choice: ScopeChoice, profileId: ProfileId = activeProfileId()): Promise<void> {
const scope = await resolveScope(choice);
if (typeof scope === "string") {
notifications().notify({ group: "analysis", level: "warning", title: "Inspect Code", body: scope });
@@ -162,8 +174,10 @@ export async function runInspection(choice: ScopeChoice): Promise<void> {
error: null,
scopeLabel: describeScope(choice, scope),
last: choice,
profile: profileId,
excluded: new Set(),
});
const profile = profileOverrides(profileId);
toolWindowService().show("inspection");
if (!isTauri()) {
useInspection.setState({ running: false, error: "Inspect Code needs the desktop app" });
@@ -182,15 +196,23 @@ export async function runInspection(choice: ScopeChoice): Promise<void> {
case "started":
useInspection.setState({ total: ev.total });
break;
case "file":
if (ev.file.diagnostics.length) found.set(ev.file.path, ev.file);
case "file": {
const diagnostics = applyProfileMixed(profile, ev.file.diagnostics);
for (const d of ev.file.diagnostics) if (d.code) noteDiagnosticCodes(producerOf(d), [d]);
if (diagnostics.length) found.set(ev.file.path, { ...ev.file, diagnostics });
useInspection.setState({ done: ev.done });
flush ??= setTimeout(publish, 150);
break;
}
case "done":
if (flush) clearTimeout(flush);
inflight = null;
useInspection.setState({ running: false, files: new Map(found), summary: ev.summary });
useInspection.setState({
running: false,
files: new Map(found),
// Problems the profile kept, not all the servers reported.
summary: { ...ev.summary, problems: [...found.values()].reduce((n, f) => n + f.diagnostics.length, 0) },
});
break;
}
};
@@ -205,6 +227,6 @@ export async function runInspection(choice: ScopeChoice): Promise<void> {
}
export function rerunInspection() {
const last = useInspection.getState().last;
if (last) void runInspection(last);
const { last, profile } = useInspection.getState();
if (last) void runInspection(last, profile ?? undefined);
}
+11 -1
Переглянути файл
@@ -5,13 +5,16 @@
import { activeEditor } from "@/editor/api";
import { call, commands, isTauri, type SearchScope } from "@/platform/ipc";
import { useProject } from "@/platform/project";
import { findCustomScope } from "@/platform/search";
export type ScopeKind = "project" | "directory" | "file" | "changed";
export type ScopeKind = "project" | "directory" | "file" | "changed" | "custom";
export interface ScopeChoice {
kind: ScopeKind;
/** Absolute directory for `directory`. */
directory: string;
/** Name of the custom scope for `custom` (`@/platform/search`). */
custom?: string;
}
export const SCOPE_LABELS: Record<ScopeKind, string> = {
@@ -19,6 +22,7 @@ export const SCOPE_LABELS: Record<ScopeKind, string> = {
directory: "Module or directory",
file: "Current file",
changed: "Uncommitted files",
custom: "Custom scope",
};
/** The file of the active editor, if it is a real file. */
@@ -66,6 +70,10 @@ export async function resolveScope(choice: ScopeChoice): Promise<SearchScope | s
}
return paths.length ? { kind: "files", paths } : "There are no uncommitted files";
}
case "custom": {
const scope = choice.custom ? findCustomScope(choice.custom) : null;
return scope ? { kind: "pattern", pattern: scope.pattern } : "Choose a scope";
}
}
}
@@ -84,5 +92,7 @@ export function describeScope(choice: ScopeChoice, scope: SearchScope): string {
return scope.kind === "files"
? `${scope.paths.length} uncommitted file${scope.paths.length === 1 ? "" : "s"}`
: "Uncommitted files";
case "custom":
return `Scope '${choice.custom ?? ""}'`;
}
}