tinymist_world/
entry.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
use std::path::{Path, PathBuf};
use std::sync::LazyLock;

use serde::{Deserialize, Serialize};
use tinymist_std::{error::prelude::*, ImmutPath};
use tinymist_vfs::{WorkspaceResolution, WorkspaceResolver};
use typst::diag::SourceResult;
use typst::syntax::{FileId, VirtualPath};

pub trait EntryReader {
    fn entry_state(&self) -> EntryState;

    fn main_id(&self) -> Option<FileId> {
        self.entry_state().main()
    }
}

pub trait EntryManager: EntryReader {
    fn mutate_entry(&mut self, state: EntryState) -> SourceResult<EntryState>;
}

#[derive(Debug, Clone, Hash, PartialEq, Eq, Default)]
pub struct EntryState {
    /// Path to the root directory of compilation.
    /// The world forbids direct access to files outside this directory.
    root: Option<ImmutPath>,
    /// Identifier of the main file in the workspace
    main: Option<FileId>,
}

pub static DETACHED_ENTRY: LazyLock<FileId> =
    LazyLock::new(|| FileId::new(None, VirtualPath::new(Path::new("/__detached.typ"))));

pub static MEMORY_MAIN_ENTRY: LazyLock<FileId> =
    LazyLock::new(|| FileId::new(None, VirtualPath::new(Path::new("/__main__.typ"))));

impl EntryState {
    /// Create an entry state with no workspace root and no main file.
    pub fn new_detached() -> Self {
        Self {
            root: None,
            main: None,
        }
    }

    /// Create an entry state with a workspace root and no main file.
    pub fn new_workspace(root: ImmutPath) -> Self {
        Self::new_rooted(root, None)
    }

    /// Create an entry state without permission to access the file system.
    pub fn new_rootless(main: VirtualPath) -> Self {
        Self {
            root: None,
            main: Some(FileId::new(None, main)),
        }
    }

    /// Create an entry state with a workspace root and an main file.
    pub fn new_rooted_by_id(root: ImmutPath, main: FileId) -> Self {
        Self::new_rooted(root, Some(main.vpath().clone()))
    }

    /// Create an entry state with a workspace root and an optional main file.
    pub fn new_rooted(root: ImmutPath, main: Option<VirtualPath>) -> Self {
        let main = main.map(|main| WorkspaceResolver::workspace_file(Some(&root), main));
        Self {
            root: Some(root),
            main,
        }
    }

    /// Create an entry state with only a main file given.
    pub fn new_rooted_by_parent(entry: ImmutPath) -> Option<Self> {
        let root = entry.parent().map(ImmutPath::from);
        let main =
            WorkspaceResolver::workspace_file(root.as_ref(), VirtualPath::new(entry.file_name()?));

        Some(Self {
            root,
            main: Some(main),
        })
    }

    pub fn main(&self) -> Option<FileId> {
        self.main
    }

    pub fn root(&self) -> Option<ImmutPath> {
        self.root.clone()
    }

    pub fn workspace_root(&self) -> Option<ImmutPath> {
        if let Some(main) = self.main {
            match WorkspaceResolver::resolve(main).ok()? {
                WorkspaceResolution::Workspace(id) | WorkspaceResolution::UntitledRooted(id) => {
                    Some(id.path().clone())
                }
                WorkspaceResolution::Rootless => None,
                WorkspaceResolution::Package => self.root.clone(),
            }
        } else {
            self.root.clone()
        }
    }

    pub fn select_in_workspace(&self, path: &Path) -> EntryState {
        let id = WorkspaceResolver::workspace_file(self.root.as_ref(), VirtualPath::new(path));

        Self {
            root: self.root.clone(),
            main: Some(id),
        }
    }

    pub fn try_select_path_in_workspace(&self, path: &Path) -> Result<Option<EntryState>> {
        Ok(match self.workspace_root() {
            Some(root) => match path.strip_prefix(&root) {
                Ok(path) => Some(EntryState::new_rooted(
                    root.clone(),
                    Some(VirtualPath::new(path)),
                )),
                Err(err) => {
                    return Err(
                        error_once!("entry file is not in workspace", err: err, entry: path.display(), root: root.display()),
                    )
                }
            },
            None => EntryState::new_rooted_by_parent(path.into()),
        })
    }

    pub fn is_detached(&self) -> bool {
        self.root.is_none() && self.main.is_none()
    }

    pub fn is_inactive(&self) -> bool {
        self.main.is_none()
    }

    pub fn is_in_package(&self) -> bool {
        self.main.is_some_and(WorkspaceResolver::is_package_file)
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum EntryOpts {
    Workspace {
        /// Path to the root directory of compilation.
        /// The world forbids direct access to files outside this directory.
        root: PathBuf,
        /// Relative path to the main file in the workspace.
        main: Option<PathBuf>,
    },
    RootByParent {
        /// Path to the entry file of compilation.
        entry: PathBuf,
    },
    Detached,
}

impl Default for EntryOpts {
    fn default() -> Self {
        Self::Detached
    }
}

impl EntryOpts {
    pub fn new_detached() -> Self {
        Self::Detached
    }

    pub fn new_workspace(root: PathBuf) -> Self {
        Self::Workspace { root, main: None }
    }

    pub fn new_rooted(root: PathBuf, main: Option<PathBuf>) -> Self {
        Self::Workspace { root, main }
    }

    pub fn new_rootless(entry: PathBuf) -> Option<Self> {
        if entry.is_relative() {
            return None;
        }

        Some(Self::RootByParent {
            entry: entry.clone(),
        })
    }
}

impl TryFrom<EntryOpts> for EntryState {
    type Error = tinymist_std::Error;

    fn try_from(value: EntryOpts) -> Result<Self, Self::Error> {
        match value {
            EntryOpts::Workspace { root, main: entry } => Ok(EntryState::new_rooted(
                root.as_path().into(),
                entry.map(VirtualPath::new),
            )),
            EntryOpts::RootByParent { entry } => {
                if entry.is_relative() {
                    return Err(error_once!("entry path must be absolute", path: entry.display()));
                }

                // todo: is there path that has no parent?
                EntryState::new_rooted_by_parent(entry.as_path().into())
                    .ok_or_else(|| error_once!("entry path is invalid", path: entry.display()))
            }
            EntryOpts::Detached => Ok(EntryState::new_detached()),
        }
    }
}