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