Files
openidea/src/platform/project.ts
T

126 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** 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;
}