Файли
openidea/src/platform/projectRoots.ts
T
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

168 рядки
5.8 KiB
TypeScript

/**
* Source / test / resource / excluded roots of the open project: detected by
* the backend from build files (`project_roots`, refreshed on
* `projectRootsChanged`) and merged here with the Project Structure settings,
* so marking a folder shows at once.
*/
import { useMemo } from "react";
import { create } from "zustand";
import { call, commands, isTauri, onIdeEvent } from "./ipc";
import {
effectiveRoots,
isTestPath,
rootFor,
useProjectStructure,
validateProjectStructure,
type RootFolder,
} from "./languageSettings";
import { useProject } from "./project";
interface RootsState {
/** Project the roots belong to. */
root: string | null;
detected: RootFolder[];
}
export const useProjectRoots = create<RootsState>(() => ({ root: null, detected: [] }));
let generation = 0;
/** Re-reads the detected roots of the open project. */
export async function loadProjectRoots(): Promise<void> {
const root = useProject.getState().project?.root ?? null;
const gen = ++generation;
if (!root || !isTauri()) {
useProjectRoots.setState({ root, detected: [] });
return;
}
try {
const roots = await call(commands.projectRoots());
if (gen !== generation) return;
useProjectRoots.setState({
root,
detected: roots.detected.map((r) => ({ path: r.path, kind: r.kind, detectedFrom: r.detectedFrom ?? null })),
});
} catch {
if (gen === generation) useProjectRoots.setState({ root, detected: [] });
}
}
let users = 0;
let dispose: (() => void) | null = null;
/** Keeps the roots in sync with the open project (reference-counted; returns a disposer). */
export function initProjectRoots(): () => void {
if (users++ === 0) {
void loadProjectRoots();
const unsubProject = useProject.subscribe((s, prev) => {
if (s.project?.root !== prev.project?.root) void loadProjectRoots();
});
const unsubEvent = onIdeEvent("projectRootsChanged", () => void loadProjectRoots());
dispose = () => {
unsubProject();
unsubEvent();
};
}
let done = false;
return () => {
if (done) return;
done = true;
if (--users === 0) {
dispose?.();
dispose = null;
}
};
}
function currentDetected(): RootFolder[] {
const s = useProjectRoots.getState();
return s.root === (useProject.getState().project?.root ?? null) ? s.detected : [];
}
/** Effective roots right now (non-reactive). */
export function currentRoots(): RootFolder[] {
return effectiveRoots(currentDetected(), validateProjectStructure(useProject.getState().settings?.projectStructure));
}
/** Detected roots of the open project right now (non-reactive). */
export function currentDetectedRoots(): RootFolder[] {
return currentDetected();
}
/** Effective roots (reactive). */
export function useEffectiveRoots(): RootFolder[] {
const structure = useProjectStructure();
const projectRoot = useProject((s) => s.project?.root ?? null);
const detected = useProjectRoots((s) => (s.root === projectRoot ? s.detected : null));
return useMemo(() => effectiveRoots(detected ?? [], structure), [detected, structure]);
}
/** Detected roots (reactive). */
export function useDetectedRoots(): RootFolder[] {
const projectRoot = useProject((s) => s.project?.root ?? null);
const detected = useProjectRoots((s) => (s.root === projectRoot ? s.detected : null));
return detected ?? EMPTY;
}
const EMPTY: RootFolder[] = [];
/** Project-relative path of an absolute `path`, or null outside the project. */
export function projectRelative(path: string): string | null {
const root = useProject.getState().project?.root;
if (!root) return null;
if (path === root) return "";
return path.startsWith(root + "/") ? path.slice(root.length + 1) : null;
}
/** The innermost root containing the absolute `path`. */
export function rootOfPath(path: string, roots: readonly RootFolder[] = currentRoots()): RootFolder | null {
const rel = projectRelative(path);
return rel === null ? null : rootFor(roots, rel);
}
/** Whether the absolute `path` is a test file (test root or test-file name). */
export function isTestFile(path: string, roots: readonly RootFolder[] = currentRoots()): boolean {
const rel = projectRelative(path);
return rel !== null && isTestPath(roots, rel);
}
/** IDEA's named scopes over the roots. */
export type ProjectFileSet = "all" | "production" | "test";
/** Whether the absolute `path` belongs to a named project scope ("Project Production Files" …). */
export function inFileSet(set: ProjectFileSet, path: string, roots: readonly RootFolder[] = currentRoots()): boolean {
const rel = projectRelative(path);
if (rel === null) return false;
const root = rootFor(roots, rel);
if (root?.kind === "excluded") return false;
if (set === "all") return true;
return isTestPath(roots, rel) === (set === "test");
}
/** A module: the project itself or a folder with a build file roots were detected from. */
export interface ProjectModule {
name: string;
/** Project-relative, `/`-separated; `""` is the project root. */
path: string;
}
/** Modules of the project, root first, then by path. */
export function projectModules(detected: readonly RootFolder[], projectName: string): ProjectModule[] {
const dirs = new Set([""]);
for (const r of detected) {
if (!r.detectedFrom) continue;
const i = r.detectedFrom.lastIndexOf("/");
dirs.add(i < 0 ? "" : r.detectedFrom.slice(0, i));
}
return [...dirs]
.sort((a, b) => (a === "" ? -1 : b === "" ? 1 : a < b ? -1 : a > b ? 1 : 0))
.map((path) => ({ path, name: path ? path.slice(path.lastIndexOf("/") + 1) : projectName }));
}
/** Modules of the open project (reactive). */
export function useProjectModules(): ProjectModule[] {
const detected = useDetectedRoots();
const name = useProject((s) => s.project?.name ?? "");
return useMemo(() => projectModules(detected, name), [detected, name]);
}