дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
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
Цей коміт міститься в:
1 джерело
294198a727
коміт
1e00ea5ee2
426 files changed
+85241
-2886
No files matched your search
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача