Файли
openidea/src/features/analysis/inspect.ts
T
l0ng-ai e1eb12c0c2 feat: Second pass aligning interactions with IntelliJ IDEA
Result tool windows
- Next/Previous Occurrence (⌥⌘↓/↑) across Find, Usages, Problems, TODO,
  Inspection and Hierarchy, with an "N of M" status hint
- Find: reuse the unpinned tab, Pin Tab, source preview, group by file or
  directory, exclude moves to the next result, Rerun
- Usages: tabs with pinning, exclude and preview
- Problems, TODO, Inspection: no autoscroll by default, severity-first sorting,
  context menu with quick fixes and copy description
- Trees: F4/⌘↓ jump to source; speed search Home/End and arrow handling

Find in Files, bookmarks, navigation
- Replace mode: Enter replaces the selection, ⇧⌥Enter replaces all, multi-select
- Pin Window toggle, highlighted read-only preview, search/replace history (⌥↓),
  Alt mnemonics, search context filter (comments / string literals)
- Mnemonic bookmarks (⌥F3, ⌃⇧digit, ⌃digit), ⌘F3 bookmarks popup, descriptions,
  ⌘-click gutter toggles a bookmark
- Select In popup (⌥F1); Recent Locations double-click and Delete;
  Search Everywhere remembers the query per tab

Code insight and refactoring
- Extract/Introduce continue with in-place rename and name suggestions;
  expression and occurrence choosers with live highlighting
- Safe Delete confirms first; numbered Refactor This
- Quick Documentation history, in-popup links and a Documentation tool window;
  Quick Definition switches between targets and shows the whole declaration
- Goto chooser lists symbol names and can pin to the Find window
- Parameter Info Tab/⇧Tab, intention preview, Reformat dialog and summary,
  inlay hint ⌘-click and context menu
- Live templates: Enter moves to the next field, two-step Esc, ⌘J as a lookup,
  language surrounders with mnemonics
- File Structure popup as a filterable tree; Show Usages filter and preview;
  call hierarchy jumps to call sites

Editor
- Paste re-indents each line by default; Move Statement moves whole statements
  in and out of blocks and re-indents
- Type over closing quotes of existing strings; ⌘D/⌘⌫ cover folded regions;
  ⌥⌫ deletes the preceding word
- ⌃G / ⌃⌘G whole-word rules and wrap hint; double-modifier caret cloning
- Visual-column block selection, nested Collapse All, recursive folding,
  fold selection toggle, Go to Matching Brace, gutter drag selects lines,
  sticky line and error stripe clicks, Insert toggles overwrite mode

Run, VCS, workbench
- Gutter icons show the last test result; Modify Run Configuration; test tree
  hides passed tests, context menu and assertion diff
- Stack frames resolve by fully qualified class name; context run chooser
- Inline Smart Step Into
- Git Log multi-select filters, "me" and date range; favourite branches,
  Fetch and tags in the branch popup; stash Keep index and Unstash options
- Terminal tab titles follow the shell; Distraction Free persists; gear menu;
  sticky balloons move to the notifications tool window after a while
2026-10-25 09:34:36 +08:00

211 рядки
6.6 KiB
TypeScript

/**
* Inspect Code: batch language-server diagnostics over a scope, grouped
* like IDEA's Inspection Results (severity → inspection → file → problem).
*/
import { Channel } from "@tauri-apps/api/core";
import { create } from "zustand";
import {
call,
commands,
isTauri,
nextRequestId,
type Diagnostic,
type InspectEvent,
type InspectSummary,
type RequestId,
} from "@/platform/ipc";
import { notifications, toolWindowService } from "@/workbench/contributions";
import { describeScope, resolveScope, type ScopeChoice } from "./scope";
export type SeverityKey = "error" | "warning" | "info";
export interface InspectionProblem {
path: string;
relPath: string;
d: Diagnostic;
}
export interface InspectionFileGroup {
path: string;
relPath: string;
problems: InspectionProblem[];
}
export interface InspectionKindGroup {
/** `source` or `source(code)`, e.g. `eslint(no-unused-vars)`. */
kind: string;
count: number;
files: InspectionFileGroup[];
}
export interface InspectionSeverityGroup {
severity: SeverityKey;
count: number;
kinds: InspectionKindGroup[];
}
export interface InspectedFileResult {
path: string;
relPath: string;
diagnostics: Diagnostic[];
}
const SEVERITY_ORDER: SeverityKey[] = ["error", "warning", "info"];
export const severityKey = (d: Diagnostic): SeverityKey => (d.severity === "hint" ? "info" : d.severity);
export function kindOf(d: Diagnostic): string {
const source = d.source || "Language server";
return d.code ? `${source}(${d.code})` : source;
}
/** Groups results by severity, then inspection kind, then file (all sorted). */
export function groupInspection(files: Iterable<InspectedFileResult>): InspectionSeverityGroup[] {
const tree = new Map<SeverityKey, Map<string, Map<string, InspectionFileGroup>>>();
for (const f of files) {
for (const d of f.diagnostics) {
const sev = severityKey(d);
let kinds = tree.get(sev);
if (!kinds) tree.set(sev, (kinds = new Map()));
const kind = kindOf(d);
let byFile = kinds.get(kind);
if (!byFile) kinds.set(kind, (byFile = new Map()));
let group = byFile.get(f.path);
if (!group) byFile.set(f.path, (group = { path: f.path, relPath: f.relPath, problems: [] }));
group.problems.push({ path: f.path, relPath: f.relPath, d });
}
}
const pos = (p: InspectionProblem) => [p.d.range.start.line, p.d.range.start.col] as const;
return SEVERITY_ORDER.filter((s) => tree.has(s)).map((severity) => {
const kinds = [...tree.get(severity)!.entries()]
.map(([kind, byFile]) => {
const groups = [...byFile.values()].sort((a, b) => a.relPath.localeCompare(b.relPath));
for (const g of groups) g.problems.sort((a, b) => pos(a)[0] - pos(b)[0] || pos(a)[1] - pos(b)[1]);
return { kind, count: groups.reduce((n, g) => n + g.problems.length, 0), files: groups };
})
.sort((a, b) => b.count - a.count || a.kind.localeCompare(b.kind));
return { severity, count: kinds.reduce((n, k) => n + k.count, 0), kinds };
});
}
interface InspectState {
running: boolean;
total: number;
done: number;
/** Files with problems, by path. */
files: Map<string, InspectedFileResult>;
summary: InspectSummary | null;
scopeLabel: string | null;
error: string | null;
/** Scope of the last run (Rerun). */
last: ScopeChoice | null;
/** Problems excluded from the results (⌫), by `problemId`. */
excluded: ReadonlySet<string>;
}
const initialInspection = (): InspectState => ({
running: false,
total: 0,
done: 0,
files: new Map(),
summary: null,
scopeLabel: null,
error: null,
last: null,
excluded: new Set(),
});
/** Stable identity of a problem across regroupings. */
export const problemId = (p: InspectionProblem) =>
`${p.path}\u0000${p.d.range.start.line}:${p.d.range.start.col}\u0000${kindOf(p.d)}\u0000${p.d.message}`;
/** Excludes `problems`, or includes them again when all are excluded already. */
export function toggleExcludedProblems(problems: readonly InspectionProblem[]) {
const ids = problems.map(problemId);
const excluded = new Set(useInspection.getState().excluded);
if (ids.every((id) => excluded.has(id))) ids.forEach((id) => excluded.delete(id));
else ids.forEach((id) => excluded.add(id));
useInspection.setState({ excluded });
}
export const useInspection = create<InspectState>(initialInspection);
let inflight: RequestId | null = null;
export function cancelInspection() {
if (inflight === null) return;
void commands.cancelRequest(inflight).catch(() => undefined);
}
/** The window switched projects: stops the run and forgets its results. */
export function resetInspection() {
cancelInspection();
inflight = null;
useInspection.setState(initialInspection());
}
export async function runInspection(choice: ScopeChoice): Promise<void> {
const scope = await resolveScope(choice);
if (typeof scope === "string") {
notifications().notify({ group: "analysis", level: "warning", title: "Inspect Code", body: scope });
return;
}
cancelInspection();
const id = nextRequestId();
inflight = id;
useInspection.setState({
running: true,
total: 0,
done: 0,
files: new Map(),
summary: null,
error: null,
scopeLabel: describeScope(choice, scope),
last: choice,
excluded: new Set(),
});
toolWindowService().show("inspection");
if (!isTauri()) {
useInspection.setState({ running: false, error: "Inspect Code needs the desktop app" });
return;
}
const found = new Map<string, InspectedFileResult>();
let flush: ReturnType<typeof setTimeout> | null = null;
const publish = () => {
flush = null;
if (inflight === id) useInspection.setState({ files: new Map(found) });
};
const channel = new Channel<InspectEvent>();
channel.onmessage = (ev) => {
if (inflight !== id) return;
switch (ev.type) {
case "started":
useInspection.setState({ total: ev.total });
break;
case "file":
if (ev.file.diagnostics.length) found.set(ev.file.path, ev.file);
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 });
break;
}
};
try {
await call(commands.inspectCode(id, scope, channel));
} catch (e) {
if (inflight === id) {
inflight = null;
useInspection.setState({ running: false, error: e instanceof Error ? e.message : String(e) });
}
}
}
export function rerunInspection() {
const last = useInspection.getState().last;
if (last) void runInspection(last);
}