дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
126 рядки
4.8 KiB
TypeScript
126 рядки
4.8 KiB
TypeScript
/** The project open in this window. */
|
||
import { confirmProjectTrust } from "./projectTrust";
|
||
import { create } from "zustand";
|
||
import { setContext } from "./context";
|
||
import { call, commands, type ProjectInfo, type Settings } from "./ipc";
|
||
import { isVirtualUri, virtualContainer, virtualName, virtualParent } from "./virtualDocs";
|
||
|
||
interface ProjectState {
|
||
project: ProjectInfo | null;
|
||
/** Effective settings (project-layered when a project is open). */
|
||
settings: Settings | null;
|
||
opening: boolean;
|
||
openingPath: string | null;
|
||
/** LightEdit: this window edits loose files without a project (`project` stays null). */
|
||
lightEdit: boolean;
|
||
}
|
||
|
||
export const useProject = create<ProjectState>(() => ({
|
||
project: null,
|
||
settings: null,
|
||
opening: false,
|
||
openingPath: null,
|
||
lightEdit: false,
|
||
}));
|
||
|
||
useProject.subscribe((s) => setContext("projectOpening", s.openingPath !== null));
|
||
|
||
let leaveGuard: (() => Promise<boolean>) | null = null;
|
||
|
||
/** Installs the check run before this window leaves its project (unsaved changes). */
|
||
export function provideLeaveProjectGuard(guard: () => Promise<boolean>): () => void {
|
||
leaveGuard = guard;
|
||
return () => {
|
||
if (leaveGuard === guard) leaveGuard = null;
|
||
};
|
||
}
|
||
|
||
/** False when the user chose to stay (another project in place, Close Project). */
|
||
export function confirmLeaveProject(): Promise<boolean> {
|
||
const s = useProject.getState();
|
||
return (s.project || s.lightEdit) && leaveGuard ? leaveGuard() : Promise.resolve(true);
|
||
}
|
||
|
||
let startupLaunch: Promise<void> | null = null;
|
||
|
||
/** Installs this window's startup launch (the project it was started or created for, features/app/windows.ts). */
|
||
export function provideStartupLaunch(launch: Promise<void>) {
|
||
startupLaunch = launch;
|
||
}
|
||
|
||
/** Settles once the startup launch has opened its project (if any), so a restore doesn't race it. */
|
||
export function startupLaunchHandled(): Promise<void> {
|
||
return (startupLaunch ?? Promise.resolve()).catch(() => {});
|
||
}
|
||
|
||
export async function openProject(path: string): Promise<ProjectInfo> {
|
||
await confirmProjectTrust(path, "local");
|
||
useProject.setState({ opening: true, openingPath: path });
|
||
try {
|
||
const project = await call(commands.openProject(path));
|
||
useProject.setState({ project, settings: project.settings, lightEdit: false });
|
||
return project;
|
||
} catch (error) {
|
||
// Opening may fail after the backend has already closed the old project.
|
||
if (useProject.getState().project) {
|
||
const project = await commands.projectCurrent().catch(() => null);
|
||
if (project?.root !== useProject.getState().project?.root)
|
||
useProject.setState({ project, ...(project ? { settings: project.settings } : {}), lightEdit: false });
|
||
}
|
||
throw error;
|
||
} finally {
|
||
useProject.setState({ opening: false, openingPath: null });
|
||
}
|
||
}
|
||
|
||
/** Switches this window to LightEdit for the loose file `path` (kept when already in LightEdit). */
|
||
export async function openLightEdit(path: string): Promise<void> {
|
||
const settings = await call(commands.lightEditOpen(path));
|
||
useProject.setState({ project: null, settings, lightEdit: true });
|
||
}
|
||
|
||
export async function closeProject() {
|
||
await commands.closeProject();
|
||
useProject.setState({ project: null, lightEdit: false });
|
||
}
|
||
|
||
export async function loadSettings() {
|
||
useProject.setState({ settings: await commands.settingsGet() });
|
||
}
|
||
|
||
/** Joins and normalises POSIX-style paths returned by the backend. */
|
||
/** Path relative to `root`; a virtual document (library class) shows where it comes from. */
|
||
export function relativePath(root: string, path: string): string {
|
||
if (isVirtualUri(path)) return [virtualContainer(path), virtualName(path)].filter(Boolean).join(" › ");
|
||
return path.startsWith(root + "/") ? path.slice(root.length + 1) : path;
|
||
}
|
||
|
||
/** Project-relative directory of `path`; "" for files directly in `root`. */
|
||
export function relativeDir(root: string, path: string): string {
|
||
if (isVirtualUri(path)) return virtualContainer(path);
|
||
const dir = dirname(path);
|
||
return dir === root ? "" : relativePath(root, dir);
|
||
}
|
||
|
||
export function basename(path: string): string {
|
||
if (isVirtualUri(path)) return virtualName(path);
|
||
const i = path.lastIndexOf("/");
|
||
return i < 0 ? path : path.slice(i + 1);
|
||
}
|
||
|
||
export function dirname(path: string): string {
|
||
if (isVirtualUri(path)) return virtualParent(path);
|
||
const i = path.lastIndexOf("/");
|
||
return i <= 0 ? "/" : path.slice(0, i);
|
||
}
|
||
|
||
/** The user-visible folder containing a file; workspace state is never a content root. */
|
||
export function projectFolder(project: ProjectInfo | null, path: string): { root: string; name: string } | null {
|
||
if (!project) return null;
|
||
if (!project.workspace) return project;
|
||
const root = project.workspace.projects
|
||
.filter((r) => path === r || path.startsWith(r + "/"))
|
||
.sort((a, b) => b.length - a.length)[0];
|
||
return root ? { root, name: basename(root) } : null;
|
||
}
|