1use std::path::{Path, PathBuf};
4use std::sync::LazyLock;
5
6use serde::{Deserialize, Serialize};
7use tinymist_std::{ImmutPath, error::prelude::*};
8use tinymist_vfs::{WorkspaceResolution, WorkspaceResolver};
9use typst::diag::SourceResult;
10use typst::syntax::{FileId, VirtualPath};
11
12pub trait EntryReader {
14 fn entry_state(&self) -> EntryState;
16
17 fn main_id(&self) -> Option<FileId> {
19 self.entry_state().main()
20 }
21}
22
23pub trait EntryManager: EntryReader {
25 fn mutate_entry(&mut self, state: EntryState) -> SourceResult<EntryState>;
27}
28
29#[derive(Debug, Clone, Hash, PartialEq, Eq, Default)]
31pub struct EntryState {
32 root: Option<ImmutPath>,
37 main: Option<FileId>,
41}
42
43pub static DETACHED_ENTRY: LazyLock<FileId> =
45 LazyLock::new(|| FileId::new(None, VirtualPath::new(Path::new("/__detached.typ"))));
46
47pub static MEMORY_MAIN_ENTRY: LazyLock<FileId> =
49 LazyLock::new(|| FileId::new(None, VirtualPath::new(Path::new("/__main__.typ"))));
50
51impl EntryState {
52 pub fn new_detached() -> Self {
54 Self {
55 root: None,
56 main: None,
57 }
58 }
59
60 pub fn new_workspace(root: ImmutPath) -> Self {
62 Self::new_rooted(root, None)
63 }
64
65 pub fn new_rootless(main: VirtualPath) -> Self {
67 Self {
68 root: None,
69 main: Some(FileId::new(None, main)),
70 }
71 }
72
73 pub fn new_rooted_by_id(root: ImmutPath, main: FileId) -> Self {
75 Self::new_rooted(root, Some(main.vpath().clone()))
76 }
77
78 pub fn new_rooted(root: ImmutPath, main: Option<VirtualPath>) -> Self {
80 let main = main.map(|main| WorkspaceResolver::workspace_file(Some(&root), main));
81 Self {
82 root: Some(root),
83 main,
84 }
85 }
86
87 pub fn new_rooted_by_parent(entry: ImmutPath) -> Option<Self> {
89 let root = entry.parent().map(ImmutPath::from);
90 let main =
91 WorkspaceResolver::workspace_file(root.as_ref(), VirtualPath::new(entry.file_name()?));
92
93 Some(Self {
94 root,
95 main: Some(main),
96 })
97 }
98
99 pub fn main(&self) -> Option<FileId> {
101 self.main
102 }
103
104 pub fn root(&self) -> Option<ImmutPath> {
106 self.root.clone()
107 }
108
109 pub fn workspace_root(&self) -> Option<ImmutPath> {
111 if let Some(main) = self.main {
112 match WorkspaceResolver::resolve(main).ok()? {
113 WorkspaceResolution::Workspace(id) | WorkspaceResolution::UntitledRooted(id) => {
114 Some(id.path().clone())
115 }
116 WorkspaceResolution::Rootless => None,
117 WorkspaceResolution::Package => self.root.clone(),
118 }
119 } else {
120 self.root.clone()
121 }
122 }
123
124 pub fn select_in_workspace(&self, path: &Path) -> EntryState {
126 let id = WorkspaceResolver::workspace_file(self.root.as_ref(), VirtualPath::new(path));
127
128 Self {
129 root: self.root.clone(),
130 main: Some(id),
131 }
132 }
133
134 pub fn try_select_path_in_workspace(&self, path: &Path) -> Result<Option<EntryState>> {
136 Ok(match self.workspace_root() {
137 Some(root) => match path.strip_prefix(&root) {
138 Ok(path) => Some(EntryState::new_rooted(
139 root.clone(),
140 Some(VirtualPath::new(path)),
141 )),
142 Err(err) => {
143 return Err(
144 error_once!("entry file is not in workspace", err: err, entry: path.display(), root: root.display()),
145 );
146 }
147 },
148 None => EntryState::new_rooted_by_parent(path.into()),
149 })
150 }
151
152 pub fn is_detached(&self) -> bool {
154 self.root.is_none() && self.main.is_none()
155 }
156
157 pub fn is_inactive(&self) -> bool {
159 self.main.is_none()
160 }
161
162 pub fn is_in_package(&self) -> bool {
164 self.main.is_some_and(WorkspaceResolver::is_package_file)
165 }
166}
167
168#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
170pub enum EntryOpts {
171 Workspace {
173 root: PathBuf,
176 main: Option<PathBuf>,
178 },
179 RootByParent {
181 entry: PathBuf,
183 },
184 Detached,
186}
187
188impl Default for EntryOpts {
189 fn default() -> Self {
190 Self::Detached
191 }
192}
193
194impl EntryOpts {
195 pub fn new_detached() -> Self {
197 Self::Detached
198 }
199
200 pub fn new_workspace(root: PathBuf) -> Self {
202 Self::Workspace { root, main: None }
203 }
204
205 pub fn new_rooted(root: PathBuf, main: Option<PathBuf>) -> Self {
207 Self::Workspace { root, main }
208 }
209
210 pub fn new_rootless(entry: PathBuf) -> Option<Self> {
212 if entry.is_relative() {
213 return None;
214 }
215
216 Some(Self::RootByParent {
217 entry: entry.clone(),
218 })
219 }
220}
221
222impl TryFrom<EntryOpts> for EntryState {
223 type Error = tinymist_std::Error;
224
225 fn try_from(value: EntryOpts) -> Result<Self, Self::Error> {
226 match value {
227 EntryOpts::Workspace { root, main: entry } => Ok(EntryState::new_rooted(
228 root.as_path().into(),
229 entry.map(VirtualPath::new),
230 )),
231 EntryOpts::RootByParent { entry } => {
232 if entry.is_relative() {
233 return Err(error_once!("entry path must be absolute", path: entry.display()));
234 }
235
236 EntryState::new_rooted_by_parent(entry.as_path().into())
238 .ok_or_else(|| error_once!("entry path is invalid", path: entry.display()))
239 }
240 EntryOpts::Detached => Ok(EntryState::new_detached()),
241 }
242 }
243}