дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +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
@@ -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[]) =>
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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…",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 ?? ""}'`;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача