Files
openidea/src/features/analysis/scope.ts
l0ng-ai 1e00ea5ee2 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
2026-10-25 21:24:00 +08:00

99 lines
3.5 KiB
TypeScript

/**
* IDEA's "Specify Scope" choices for the analyses (Inspect Code, Locate
* Duplicates, Search Structurally), resolved to a backend `SearchScope`.
*/
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" | "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> = {
project: "Whole project",
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. */
export function currentFile(): string | null {
const ctx = activeEditor()?.ctx;
return ctx && !ctx.virtual ? ctx.path : null;
}
export function dirname(path: string): string {
const i = path.lastIndexOf("/");
return i > 0 ? path.slice(0, i) : "/";
}
/** Initial scope: the current file's directory is offered for `directory`. */
export function defaultScope(kind: ScopeKind = "project"): ScopeChoice {
const file = currentFile();
const root = useProject.getState().project?.root ?? "";
return { kind, directory: file ? dirname(file) : root };
}
/** Paths of files with uncommitted changes (from Git status). */
export async function changedFiles(): Promise<string[]> {
if (!isTauri()) return [];
const status = await call(commands.vcsStatus());
return status.filter((s) => s.unstaged !== "deleted" && s.staged !== "deleted").map((s) => s.path);
}
/** A backend scope, or a message why there is none. */
export async function resolveScope(choice: ScopeChoice): Promise<SearchScope | string> {
switch (choice.kind) {
case "project":
return { kind: "project" };
case "directory":
return choice.directory.trim() ? { kind: "directory", path: choice.directory.trim() } : "Choose a directory";
case "file": {
const file = currentFile();
return file ? { kind: "files", paths: [file] } : "No file is open";
}
case "changed": {
let paths: string[];
try {
paths = await changedFiles();
} catch (e) {
return e instanceof Error ? e.message : String(e);
}
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";
}
}
}
/** "Project", "Directory src/app", "Current file a.rs", "3 uncommitted files". */
export function describeScope(choice: ScopeChoice, scope: SearchScope): string {
const root = useProject.getState().project?.root ?? "";
const rel = (p: string) => (root && p.startsWith(`${root}/`) ? p.slice(root.length + 1) : p);
switch (choice.kind) {
case "project":
return "Project";
case "directory":
return `Directory ${rel(choice.directory)}`;
case "file":
return scope.kind === "files" ? `File ${rel(scope.paths[0] ?? "")}` : "Current file";
case "changed":
return scope.kind === "files"
? `${scope.paths.length} uncommitted file${scope.paths.length === 1 ? "" : "s"}`
: "Uncommitted files";
case "custom":
return `Scope '${choice.custom ?? ""}'`;
}
}