дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
Opening a folder of independent Gradle builds selected only the first build. Link Gradle Projects… (pick a build's settings or build file) and Unlink on a build's root let an ordinary project window manage several independent builds without changing their scripts. The build model holds a list of Gradle builds; each keeps its own wrapper, working directory, environment, task routing and sync cache, keyed by the owning build directory. The links persist in the project's Gradle settings (linkedProjects); unlinking leaves files and saved run configurations alone. Existing includeBuild support is unchanged, and an explicitly linked build takes precedence over a composite that also includes it. Workspace windows keep Link/Unlink disabled. Co-authored-by: gudcks0305 <[email protected]>
229 рядки
9.0 KiB
TypeScript
229 рядки
9.0 KiB
TypeScript
/** Project view: the "project" tool window, its file operations and actions. */
|
||
import { onEditorEvent, openEditorPaths } from "@/editor/api";
|
||
import { commands, isTauri, onIdeEvent } from "@/platform/ipc";
|
||
import { useBuildModel } from "@/platform/buildModel";
|
||
import { validateProjectStructure, type RootFolder } from "@/platform/languageSettings";
|
||
import { useProject } from "@/platform/project";
|
||
import { initProjectRoots, useProjectRoots } from "@/platform/projectRoots";
|
||
import { folderIconName } from "@/ui";
|
||
import {
|
||
localChangedPaths,
|
||
provideProjectViewSelection,
|
||
registerOverlay,
|
||
registerProjectViewRoot,
|
||
registerToolWindow,
|
||
registerUndoHandler,
|
||
useLocalChanges,
|
||
useProjectViewRoots,
|
||
} from "@/workbench/contributions";
|
||
import { registerProjectActions } from "./actions";
|
||
import { gradleModuleGroups } from "./moduleGroups";
|
||
import { FileRefactorDialogHost } from "./FileRefactorDialogs";
|
||
import { NewPopupHost } from "./NewPopup";
|
||
import { RefactoringPreviewHost } from "./RefactoringPreview";
|
||
import { ProjectHeaderActions, useProjectHeaderMenu, useProjectTitleMenu } from "./HeaderActions";
|
||
import { loadHomeDir } from "./home";
|
||
import { selectedNodes, undoLastFileOperation } from "./operations";
|
||
import { ProjectView } from "./ProjectView";
|
||
import {
|
||
applyFsChange,
|
||
dropOutlines,
|
||
initProjectTree,
|
||
reloadAll,
|
||
reloadLoadedDirs,
|
||
setChangedFiles,
|
||
setOpenFiles,
|
||
setViewMode,
|
||
useProjectTree,
|
||
} from "./store";
|
||
|
||
export function activate(): () => void {
|
||
loadHomeDir();
|
||
let unityTimer: ReturnType<typeof setTimeout> | undefined;
|
||
let unityGeneration = 0;
|
||
const sync = () => {
|
||
const p = useProject.getState().project;
|
||
const s = useProjectTree.getState();
|
||
if ((p?.root ?? null) !== s.root || p?.workspace?.projects !== (s.members ?? undefined))
|
||
initProjectTree(p?.root ?? null, p?.name ?? "", p?.workspace?.projects ?? null, p?.unity ?? null);
|
||
else if (p) {
|
||
useProjectTree.setState({ name: p.name, unity: p.unity ?? null });
|
||
if (p.unity && !s.unity && s.mode === "project") setViewMode("unity");
|
||
if (!p.unity && s.mode === "unity") setViewMode("project");
|
||
}
|
||
};
|
||
sync();
|
||
const unsubProject = useProject.subscribe((s, prev) => {
|
||
if (s.project?.root !== prev.project?.root) {
|
||
unityGeneration++;
|
||
clearTimeout(unityTimer);
|
||
}
|
||
if (s.project !== prev.project) sync();
|
||
});
|
||
// Unity regenerates these files while the IDE stays open. Refresh only metadata,
|
||
// retaining the current settings and ignoring replies from a previous project.
|
||
const unsubUnity = onIdeEvent("fsChanged", (e) => {
|
||
const project = useProject.getState().project;
|
||
if (!project || !isTauri()) return;
|
||
const relevant = [...e.created, ...e.modified, ...e.removed].some((p) => {
|
||
if (!p.startsWith(project.root + "/")) return false;
|
||
const rel = p.slice(project.root.length + 1);
|
||
return (
|
||
/^(Assets|Packages|ProjectSettings)(\/ProjectVersion\.txt)?$/.test(rel) ||
|
||
/^[^/]+\.(csproj|sln|slnx)$/i.test(rel)
|
||
);
|
||
});
|
||
if (!relevant) return;
|
||
const generation = ++unityGeneration;
|
||
clearTimeout(unityTimer);
|
||
unityTimer = setTimeout(async () => {
|
||
try {
|
||
const fresh = await commands.projectCurrent();
|
||
const current = useProject.getState().project;
|
||
if (generation !== unityGeneration || current?.root !== project.root || fresh?.root !== project.root) return;
|
||
useProject.setState({ project: { ...current, unity: fresh.unity } });
|
||
} catch {
|
||
// A project may have closed while the metadata request was in flight.
|
||
}
|
||
}, 150);
|
||
});
|
||
// Excluded folders / exclude globs change which entries are `ignored`.
|
||
const unsubExcludes = useProject.subscribe((s, prev) => {
|
||
const ex = (x: typeof s.settings) =>
|
||
JSON.stringify([
|
||
x?.files?.exclude,
|
||
x?.projectStructure?.excluded,
|
||
x?.projectStructure?.unmarked,
|
||
x?.projectStructure?.excludeFiles,
|
||
]);
|
||
if (s.settings !== prev.settings && prev.settings && ex(s.settings) !== ex(prev.settings)) void reloadAll();
|
||
});
|
||
// …and so do excluded roots detected from build files (`target`, `node_modules`, …).
|
||
const disposeRoots = initProjectRoots();
|
||
const detectedExcluded = (s: { root: string | null; detected: readonly RootFolder[] }) =>
|
||
JSON.stringify([s.root, s.detected.filter((r) => r.kind === "excluded").map((r) => r.path)]);
|
||
const unsubRoots = useProjectRoots.subscribe((s, prev) => {
|
||
if (s.root && s.root === prev.root && detectedExcluded(s) !== detectedExcluded(prev)) void reloadAll();
|
||
});
|
||
|
||
// Project Structure content roots after the project folder, and IDEA's module groups of
|
||
// a Gradle build with modules outside it (as of the last sync) before it.
|
||
let contentRoots: Array<() => void> = [];
|
||
let shownRoots = "";
|
||
const syncContentRoots = () => {
|
||
const { project, settings } = useProject.getState();
|
||
const own = project && !project.workspace;
|
||
const paths = own ? validateProjectStructure(settings?.projectStructure).contentRoots : [];
|
||
// The Build tool window's model (one load per build change), not a load of our own.
|
||
const build = useBuildModel.getState();
|
||
const gradleBuilds = build.root === project?.root ? (build.model?.gradle ?? []) : [];
|
||
const groups =
|
||
own && project
|
||
? gradleBuilds.flatMap((g) =>
|
||
gradleModuleGroups(g.projects, g.projects.find((p) => p.path === ":")?.name ?? "", project.root, g.dir),
|
||
)
|
||
: [];
|
||
const key = JSON.stringify([paths, groups]);
|
||
if (key === shownRoots) return;
|
||
shownRoots = key;
|
||
contentRoots.forEach((d) => d());
|
||
contentRoots = paths.map((path, i) =>
|
||
registerProjectViewRoot({
|
||
id: `contentRoot:${path}`,
|
||
label: path.slice(path.lastIndexOf("/") + 1) || path,
|
||
detail: path,
|
||
icon: folderIconName("module"),
|
||
path,
|
||
order: -100 + i,
|
||
}),
|
||
);
|
||
groups.forEach((g, i) =>
|
||
contentRoots.push(registerProjectViewRoot({ ...g, id: g.id!, order: -200 + i, beforeProject: true })),
|
||
);
|
||
};
|
||
syncContentRoots();
|
||
const unsubGradleRoots = useBuildModel.subscribe((s, prev) => {
|
||
if (s.model !== prev.model || s.root !== prev.root) syncContentRoots();
|
||
});
|
||
const unsubContentRoots = useProject.subscribe((s, prev) => {
|
||
if (s.settings !== prev.settings || s.project !== prev.project) syncContentRoots();
|
||
});
|
||
|
||
// Contributed roots (Scratches) are part of the derived model: re-render.
|
||
const unsubExtraRoots = useProjectViewRoots.subscribe(() =>
|
||
useProjectTree.setState((s) => ({ listings: new Map(s.listings) })),
|
||
);
|
||
|
||
// Open Files / All Changed Files panes: editor tabs and VCS changes, via the editor API and decorations.
|
||
let openTimer: ReturnType<typeof setTimeout> | undefined;
|
||
const syncOpenFiles = () => {
|
||
clearTimeout(openTimer);
|
||
// Tabs update after the document events.
|
||
openTimer = setTimeout(() => setOpenFiles(openEditorPaths()), 0);
|
||
};
|
||
syncOpenFiles();
|
||
const unsubEditor = onEditorEvent((e) => {
|
||
if (e.type === "docOpened" || e.type === "docClosed" || e.type === "activeEditorChanged") syncOpenFiles();
|
||
if (e.type === "docSaved") dropOutlines([e.path]);
|
||
});
|
||
// "All Changed Files": change-list files still on disk (IDEA leaves out unversioned, ignored and deleted ones).
|
||
const syncChanged = () => setChangedFiles(localChangedPaths(useLocalChanges.getState().lists, null, true));
|
||
syncChanged();
|
||
const unsubDecorations = useLocalChanges.subscribe((s, prev) => {
|
||
if (s.lists !== prev.lists) syncChanged();
|
||
});
|
||
|
||
const disposers = [
|
||
() => {
|
||
clearTimeout(unityTimer);
|
||
unityGeneration++;
|
||
unsubUnity();
|
||
},
|
||
() => clearTimeout(openTimer),
|
||
unsubEditor,
|
||
unsubDecorations,
|
||
unsubProject,
|
||
unsubExtraRoots,
|
||
unsubExcludes,
|
||
unsubContentRoots,
|
||
unsubGradleRoots,
|
||
() => contentRoots.forEach((d) => d()),
|
||
disposeRoots,
|
||
unsubRoots,
|
||
registerToolWindow({
|
||
id: "project",
|
||
title: "Project",
|
||
icon: "toolwindow-project",
|
||
anchor: "left",
|
||
order: 0,
|
||
component: ProjectView,
|
||
headerActions: ProjectHeaderActions,
|
||
headerMenu: useProjectHeaderMenu,
|
||
titleMenu: useProjectTitleMenu,
|
||
}),
|
||
registerOverlay({ id: "project.fileRefactor", component: FileRefactorDialogHost }),
|
||
registerOverlay({ id: "project.refactoringPreview", component: RefactoringPreviewHost }),
|
||
registerOverlay({ id: "project.newPopup", component: NewPopupHost }),
|
||
registerProjectActions(),
|
||
// Edit › Undo while the Project view has focus.
|
||
registerUndoHandler(
|
||
(command, focused) =>
|
||
command === "undo" &&
|
||
focused.getAttribute("role") === "tree" &&
|
||
!!focused.closest("[data-project-view]") &&
|
||
undoLastFileOperation(),
|
||
),
|
||
provideProjectViewSelection(() =>
|
||
selectedNodes()
|
||
.filter((n) => !n.group)
|
||
.map((n) => n.path),
|
||
),
|
||
onIdeEvent("fsChanged", (e) => void applyFsChange(e)),
|
||
onIdeEvent("excludedDirsChanged", (e) => void reloadLoadedDirs(e.dirs)),
|
||
];
|
||
return () => {
|
||
disposers.reverse().forEach((d) => d());
|
||
initProjectTree(null);
|
||
};
|
||
}
|