1use std::path::{Path, PathBuf};
2use std::sync::{Arc, LazyLock};
3
4use reflexo::{error::prelude::*, ImmutPath};
5use serde::{Deserialize, Serialize};
6use typst::diag::SourceResult;
7use typst::syntax::{FileId, VirtualPath};
8
9pub trait EntryReader {
10 fn entry_state(&self) -> EntryState;
11
12 fn workspace_root(&self) -> Option<Arc<Path>> {
13 self.entry_state().root().clone()
14 }
15
16 fn main_id(&self) -> Option<FileId> {
17 self.entry_state().main()
18 }
19}
20
21pub trait EntryManager: EntryReader {
22 fn reset(&mut self) -> SourceResult<()> {
23 Ok(())
24 }
25
26 fn mutate_entry(&mut self, state: EntryState) -> SourceResult<EntryState>;
27}
28
29#[derive(Debug, Clone, Hash, PartialEq, Eq, Default)]
30pub struct EntryState {
31 rooted: bool,
35 root: Option<ImmutPath>,
38 main: Option<FileId>,
40}
41
42pub static DETACHED_ENTRY: LazyLock<FileId> =
43 LazyLock::new(|| FileId::new(None, VirtualPath::new(Path::new("/__detached.typ"))));
44
45pub static MEMORY_MAIN_ENTRY: LazyLock<FileId> =
46 LazyLock::new(|| FileId::new(None, VirtualPath::new(Path::new("/__main__.typ"))));
47
48impl EntryState {
49 pub fn new_detached() -> Self {
51 Self {
52 rooted: false,
53 root: None,
54 main: None,
55 }
56 }
57
58 pub fn new_workspace(root: ImmutPath) -> Self {
60 Self::new_rooted(root, None)
61 }
62
63 pub fn new_rooted(root: ImmutPath, main: Option<FileId>) -> Self {
65 Self {
66 rooted: true,
67 root: Some(root),
68 main,
69 }
70 }
71
72 pub fn new_rootless(entry: ImmutPath) -> Option<Self> {
74 Some(Self {
75 rooted: false,
76 root: entry.parent().map(From::from),
77 main: Some(FileId::new(None, VirtualPath::new(entry.file_name()?))),
78 })
79 }
80
81 pub fn main(&self) -> Option<FileId> {
82 self.main
83 }
84
85 pub fn root(&self) -> Option<ImmutPath> {
86 self.root.clone()
87 }
88
89 pub fn workspace_root(&self) -> Option<ImmutPath> {
90 self.rooted.then(|| self.root.clone()).flatten()
91 }
92
93 pub fn select_in_workspace(&self, id: FileId) -> EntryState {
94 Self {
95 rooted: self.rooted,
96 root: self.root.clone(),
97 main: Some(id),
98 }
99 }
100
101 pub fn try_select_path_in_workspace(
102 &self,
103 p: &Path,
104 allow_rootless: bool,
105 ) -> ZResult<Option<EntryState>> {
106 Ok(match self.workspace_root() {
107 Some(root) => match p.strip_prefix(&root) {
108 Ok(p) => Some(EntryState::new_rooted(
109 root.clone(),
110 Some(FileId::new(None, VirtualPath::new(p))),
111 )),
112 Err(e) => {
113 return Err(
114 error_once!("entry file is not in workspace", err: e, entry: p.display(), root: root.display()),
115 )
116 }
117 },
118 None if allow_rootless => EntryState::new_rootless(p.into()),
119 None => None,
120 })
121 }
122
123 pub fn is_detached(&self) -> bool {
124 self.root.is_none() && self.main.is_none()
125 }
126
127 pub fn is_inactive(&self) -> bool {
128 self.main.is_none()
129 }
130}
131
132#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
133pub enum EntryOpts {
134 Workspace {
135 root: PathBuf,
138 entry: Option<PathBuf>,
140 },
141 RootlessEntry {
142 entry: PathBuf,
144 root: Option<PathBuf>,
146 },
147 Detached,
148}
149
150impl Default for EntryOpts {
151 fn default() -> Self {
152 Self::Detached
153 }
154}
155
156impl EntryOpts {
157 pub fn new_detached() -> Self {
158 Self::Detached
159 }
160
161 pub fn new_workspace(root: PathBuf) -> Self {
162 Self::Workspace { root, entry: None }
163 }
164
165 pub fn new_rooted(root: PathBuf, entry: Option<PathBuf>) -> Self {
166 Self::Workspace { root, entry }
167 }
168
169 pub fn new_rootless(entry: PathBuf) -> Option<Self> {
170 if entry.is_relative() {
171 return None;
172 }
173
174 Some(Self::RootlessEntry {
175 entry: entry.clone(),
176 root: entry.parent().map(From::from),
177 })
178 }
179}
180
181impl TryFrom<EntryOpts> for EntryState {
182 type Error = reflexo::Error;
183
184 fn try_from(value: EntryOpts) -> Result<Self, Self::Error> {
185 match value {
186 EntryOpts::Workspace { root, entry } => Ok(EntryState::new_rooted(
187 root.as_path().into(),
188 entry.map(|e| FileId::new(None, VirtualPath::new(e))),
189 )),
190 EntryOpts::RootlessEntry { entry, root } => {
191 if entry.is_relative() {
192 return Err(error_once!("entry path must be absolute", path: entry.display()));
193 }
194
195 let root = root
197 .as_deref()
198 .or_else(|| entry.parent())
199 .ok_or_else(|| error_once!("a root must be determined for EntryOpts::PreparedEntry", path: entry.display()))?;
200
201 let relative_entry = match entry.strip_prefix(root) {
202 Ok(e) => e,
203 Err(_) => {
204 return Err(
205 error_once!("entry path must be inside the root", path: entry.display()),
206 )
207 }
208 };
209
210 Ok(EntryState {
211 rooted: false,
212 root: Some(root.into()),
213 main: Some(FileId::new(None, VirtualPath::new(relative_entry))),
214 })
215 }
216 EntryOpts::Detached => Ok(EntryState::new_detached()),
217 }
218 }
219}