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

+39
Переглянути файл
@@ -108,3 +108,42 @@
.link:hover {
color: var(--link-hover);
}
.links {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
/* Manage Coverage Reports */
.reports {
height: 260px;
margin: 12px;
border: 1px solid var(--border);
display: flex;
flex-direction: column;
}
.reportRow {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
flex: 1;
white-space: nowrap;
}
.reportDetail {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
color: var(--fg-subtle);
}
.reportTime {
flex: none;
font-size: var(--fs-small);
color: var(--fg-muted);
}
.reportsEmpty {
padding: 12px;
color: var(--fg-muted);
}
+19 -10
Переглянути файл
@@ -35,13 +35,17 @@ export function CoverageHeaderActions() {
const visible = useCoverage((s) => s.visible);
const hasSuite = useCoverage((s) => !!s.activeId);
return (
<IconButton
icon={visible ? "close" : "eye"}
size="small"
title={visible ? "Hide Coverage" : "Show Coverage"}
disabled={!hasSuite}
onClick={() => setCoverageVisible(!visible)}
/>
<>
<IconButton icon="folder-open" size="small" action="coverage.importReport" />
<IconButton icon="list" size="small" action="coverage.manageReports" />
<IconButton
icon={visible ? "close" : "eye"}
size="small"
title={visible ? "Hide Coverage" : "Show Coverage"}
disabled={!hasSuite}
onClick={() => setCoverageVisible(!visible)}
/>
</>
);
}
@@ -87,9 +91,14 @@ export function CoverageView() {
<span>
<T>Run a configuration with coverage to see which lines its tests execute.</T>
</span>
<button type="button" className={styles.link} onClick={() => void executeAction("run.coverage")}>
<T>Run with Coverage</T>
</button>
<span className={styles.links}>
<button type="button" className={styles.link} onClick={() => void executeAction("run.coverage")}>
<T>Run with Coverage</T>
</button>
<button type="button" className={styles.link} onClick={() => void executeAction("coverage.importReport")}>
<T>Import a report collected in CI from disk</T>
</button>
</span>
</EmptyState>
);
}
+21 -1
Переглянути файл
@@ -3,7 +3,7 @@ import { EditorState } from "@codemirror/state";
import type { CoverageSuite, FileCoverage, RunConfig } from "@/platform/ipc";
import { coverageMarkers } from "./gutter";
import { coverageBadges, coverageSupports, coverageTree, formatPercent, lineState, sortTree } from "./model";
import { MAX_SUITES, addSuite, closeSuite, resetCoverage, setCoverageVisible, shownFileCoverage, useCoverage } from "./store";
import { MAX_SUITES, addSuite, closeSuite, reportEntries, resetCoverage, setCoverageVisible, shownFileCoverage, useCoverage } from "./store";
function cfg(command: string, args: string[], extra: Partial<RunConfig> = {}): RunConfig {
return { id: "c", name: "c", kind: "shell", command, args, cwd: null, env: {}, detected: true, isTest: false, debugAdapter: null, ...extra };
@@ -115,4 +115,24 @@ describe("gutter and suites", () => {
expect(useCoverage.getState().activeId).toBe(s.suites[1]!.id);
resetCoverage();
});
it("replaces a re-imported report and lists remembered imports in Manage Coverage Reports", () => {
addSuite("tests", suite([file("/p/a.rs", 1, 2)]));
const first = addSuite("lcov.info", suite([file("/p/a.rs", 1, 2)]), "/ci/lcov.info");
const again = addSuite("lcov.info", suite([file("/p/a.rs", 2, 2)]), "/ci/lcov.info");
const { suites } = useCoverage.getState();
expect(suites.map((s) => s.id)).toEqual([again.id, suites[1]!.id]);
expect(suites.some((s) => s.id === first.id)).toBe(false);
expect(shownFileCoverage("/p/a.rs")?.linesCovered).toBe(2);
const entries = reportEntries(suites, [
{ path: "/ci/lcov.info", name: "lcov.info", importedAt: 1 },
{ path: "/ci/jacoco.xml", name: "jacoco.xml", importedAt: 2 },
]);
expect(entries.map((e) => [e.name, e.detail, !!e.suite])).toEqual([
["lcov.info", "/ci/lcov.info", true],
["tests", "t", true],
["jacoco.xml", "/ci/jacoco.xml", false],
]);
resetCoverage();
});
});
+23 -1
Переглянути файл
@@ -6,10 +6,11 @@
import { registerEditorContribution } from "@/editor/api";
import { executeAction, registerActions } from "@/platform/actions";
import { useProject } from "@/platform/project";
import { registerRunExecutor, registerToolWindow, toolWindowService } from "@/workbench/contributions";
import { registerOverlay, registerRunExecutor, registerToolWindow, toolWindowService } from "@/workbench/contributions";
import { CoverageHeaderActions, CoverageTabs, CoverageView } from "./CoverageView";
import { coverageExecutor } from "./executor";
import { coverageGutter } from "./gutter";
import { ManageReportsOverlay, importReport, openManageReports } from "./reports";
import { resetCoverage, setCoverageVisible, syncCoverageViews, useCoverage } from "./store";
export function activate(): () => void {
@@ -28,6 +29,7 @@ export function activate(): () => void {
}),
registerEditorContribution({ id: "coverage.gutter", order: 65, extensions: coverageGutter }),
syncCoverageViews(),
registerOverlay({ id: "coverage.manageReports", component: ManageReportsOverlay }),
registerActions([
{
id: "run.coverage",
@@ -49,6 +51,26 @@ export function activate(): () => void {
keywords: ["coverage"],
run: ({ arg }) => executeAction("run.withExecutor", { executor: "coverage", context: arg ?? true }).then(() => undefined),
},
{
id: "coverage.importReport",
title: "Import a Coverage Report…",
description: "Show a report collected elsewhere (CI): lcov, JaCoCo XML or a Go cover profile; argument: the file path",
category: "Run",
icon: "folder-open",
when: "projectOpen",
keywords: ["coverage", "lcov", "jacoco", "ci", "import"],
run: ({ arg }) => importReport(typeof arg === "string" ? arg : undefined).then(() => undefined),
},
{
id: "coverage.manageReports",
title: "Manage Coverage Reports…",
description: "Choose the shown coverage suite, delete suites and imported reports",
category: "Run",
icon: "list",
when: "projectOpen",
keywords: ["coverage", "suites", "show coverage data"],
run: () => openManageReports(),
},
{
id: "coverage.hide",
title: "Hide Coverage",
+165
Переглянути файл
@@ -0,0 +1,165 @@
/**
* Coverage reports collected elsewhere: "Import a report collected in CI from
* disk" (lcov, JaCoCo XML, Go cover profile) and "Manage Coverage Reports"
* (choose the shown suite, delete suites and remembered imports).
*/
import { useMemo, useState } from "react";
import { create } from "zustand";
import { call, commands, isTauri } from "@/platform/ipc";
import { basename } from "@/platform/project";
import { Dialog, Icon, List, T } from "@/ui";
import { notifications, toolWindowService } from "@/workbench/contributions";
import { formatPercent } from "./model";
import {
addSuite,
closeSuite,
forgetImport,
importedReports,
rememberImport,
reportEntries,
selectSuite,
useCoverage,
type ReportEntry,
type Suite,
} from "./store";
import styles from "./Coverage.module.css";
const errorText = (e: unknown) => (e instanceof Error ? e.message : String(e));
async function pickReport(): Promise<string | null> {
const { open } = await import("@tauri-apps/plugin-dialog");
const file = await open({
multiple: false,
directory: false,
title: "Import Coverage Report",
filters: [
{
name: "Coverage reports (lcov, JaCoCo XML, Go cover profile)",
extensions: ["info", "lcov", "xml", "out", "txt", "cov", "coverprofile"],
},
{ name: "All files", extensions: ["*"] },
],
});
return typeof file === "string" ? file : null;
}
/** Imports `path` (asks for a file without one) and shows it like a run's suite. */
export async function importReport(path?: string): Promise<Suite | null> {
if (!isTauri()) {
notifications().notify({ group: "run", level: "warning", title: "Importing coverage needs the desktop app" });
return null;
}
const file = path ?? (await pickReport());
if (!file) return null;
try {
const data = await call(commands.coverageImport(file));
const suite = addSuite(basename(file), data, file);
rememberImport({ path: file, name: suite.name, importedAt: suite.createdAt });
try {
toolWindowService().show("coverage");
} catch {
/* no workbench (tests) */
}
const [covered, total] = data.files.reduce((a, f) => [a[0]! + f.linesCovered, a[1]! + f.linesTotal], [0, 0]);
notifications().notify({
group: "run",
level: "info",
key: "coverage-import",
title: `Imported coverage '${suite.name}': ${formatPercent(covered!, total!)} lines`,
body: data.files.length ? undefined : "The report covers no files of this project.",
});
return suite;
} catch (e) {
notifications().notify({
group: "run",
level: "error",
key: "coverage-import",
title: `Cannot import ${basename(file)}`,
body: errorText(e),
});
return null;
}
}
const useManageDialog = create<{ open: boolean }>(() => ({ open: false }));
export const openManageReports = () => useManageDialog.setState({ open: true });
export function ManageReportsOverlay() {
const open = useManageDialog((s) => s.open);
return open ? <ManageReportsDialog onClose={() => useManageDialog.setState({ open: false })} /> : null;
}
function ManageReportsDialog({ onClose }: { onClose: () => void }) {
const suites = useCoverage((s) => s.suites);
const activeId = useCoverage((s) => s.activeId);
const [imported, setImported] = useState(() => importedReports());
const entries = useMemo(() => reportEntries(suites, imported), [suites, imported]);
const [active, setActive] = useState(() =>
Math.max(
0,
entries.findIndex((e) => e.suite?.id === activeId),
),
);
const current: ReportEntry | undefined = entries[Math.min(active, entries.length - 1)];
const show = async (entry: ReportEntry | undefined) => {
if (!entry) return;
if (entry.suite) selectSuite(entry.suite.id);
else if (entry.file && !(await importReport(entry.file))) return;
onClose();
};
const remove = (entry: ReportEntry | undefined) => {
if (!entry) return;
if (entry.suite) closeSuite(entry.suite.id);
if (entry.file) {
forgetImport(entry.file);
setImported(importedReports());
}
};
const importMore = async () => {
if (await importReport()) setImported(importedReports());
};
return (
<Dialog
open
title="Coverage Reports"
onClose={onClose}
width={560}
buttons={[
{ label: "Import…", role: "extra", onClick: () => void importMore() },
{ label: "Delete", role: "extra", disabled: !current, onClick: () => remove(current) },
{ label: "Show Selected", role: "default", disabled: !current, onClick: () => void show(current) },
{ label: "Cancel", role: "cancel" },
]}
>
<div className={styles.reports}>
<List<ReportEntry>
aria-label="Coverage reports"
items={entries}
getKey={(e) => e.key}
activeIndex={active}
onActiveChange={setActive}
onActivate={(e) => void show(e)}
renderItem={(e) => (
<span className={styles.reportRow}>
<Icon name={e.file ? "folder-open" : "run-coverage"} size={16} className={styles.icon} />
<span className={styles.label}>{e.name}</span>
<span className={styles.reportDetail}>{e.detail}</span>
<span className={styles.reportTime}>
{e.suite?.id === activeId ? <T>shown</T> : e.suite ? null : <T>not loaded</T>}{" "}
{new Date(e.time).toLocaleString()}
</span>
</span>
)}
empty={
<div className={styles.reportsEmpty}>
<T>No coverage reports. Run with coverage or import a report.</T>
</div>
}
/>
</div>
</Dialog>
);
}
+83 -3
Переглянути файл
@@ -14,9 +14,11 @@ export const MAX_SUITES = 5;
export interface Suite {
id: string;
/** Run configuration name. */
/** Run configuration name, or the imported report's file name. */
name: string;
createdAt: number;
/** The report file of an imported suite ("Import a report collected in CI"). */
file?: string;
data: CoverageSuite;
byPath: Map<string, FileCoverage>;
}
@@ -43,15 +45,21 @@ export function shownFileCoverage(path: string, s: CoverageState = useCoverage.g
return activeSuite(s)?.byPath.get(path) ?? null;
}
export function addSuite(name: string, data: CoverageSuite): Suite {
export function addSuite(name: string, data: CoverageSuite, file?: string): Suite {
const suite: Suite = {
id: `suite${++seq}`,
name,
createdAt: Date.now(),
file,
data,
byPath: new Map(data.files.map((f) => [f.path, f])),
};
useCoverage.setState((s) => ({ suites: [suite, ...s.suites].slice(0, MAX_SUITES), activeId: suite.id, visible: true }));
useCoverage.setState((s) => ({
// A re-imported file replaces its earlier suite.
suites: [suite, ...s.suites.filter((x) => !file || x.file !== file)].slice(0, MAX_SUITES),
activeId: suite.id,
visible: true,
}));
return suite;
}
@@ -97,3 +105,75 @@ export function syncCoverageViews(): () => void {
setFileDecorations("coverage", new Map());
};
}
// ---------------------------------------------------------------------------
// Imported reports, remembered per project so "Manage Coverage Reports" lists
// them after a restart.
export interface ImportedReport {
path: string;
name: string;
importedAt: number;
}
const IMPORTED_KEY = "openidea.coverage.imported.v1:";
const MAX_IMPORTED = 20;
export function importedReports(root: string | null = useProject.getState().project?.root ?? null): ImportedReport[] {
if (!root) return [];
try {
const v: unknown = JSON.parse(globalThis.localStorage?.getItem(IMPORTED_KEY + root) ?? "[]");
if (!Array.isArray(v)) return [];
return v.filter(
(r): r is ImportedReport =>
!!r && typeof r.path === "string" && typeof r.name === "string" && typeof r.importedAt === "number",
);
} catch {
return [];
}
}
function saveImported(root: string, reports: ImportedReport[]) {
try {
globalThis.localStorage?.setItem(IMPORTED_KEY + root, JSON.stringify(reports.slice(0, MAX_IMPORTED)));
} catch {
/* storage unavailable */
}
}
export function rememberImport(report: ImportedReport) {
const root = useProject.getState().project?.root;
if (root) saveImported(root, [report, ...importedReports(root).filter((r) => r.path !== report.path)]);
}
export function forgetImport(path: string) {
const root = useProject.getState().project?.root;
if (root) saveImported(root, importedReports(root).filter((r) => r.path !== path));
}
/** A row of "Manage Coverage Reports": a loaded suite, or an imported report not loaded now. */
export interface ReportEntry {
key: string;
name: string;
/** Tool or file shown beside the name. */
detail: string;
time: number;
suite: Suite | null;
file: string | null;
}
export function reportEntries(suites: readonly Suite[], imported: readonly ImportedReport[]): ReportEntry[] {
const out: ReportEntry[] = suites.map((s) => ({
key: s.id,
name: s.name,
detail: s.file ?? s.data.runner,
time: s.createdAt,
suite: s,
file: s.file ?? null,
}));
for (const r of imported) {
if (suites.some((s) => s.file === r.path)) continue;
out.push({ key: `file:${r.path}`, name: r.name, detail: r.path, time: r.importedAt, suite: null, file: r.path });
}
return out;
}