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 {
root: Option<ImmutPath>,
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 {
pub fn new_detached() -> Self {
Self {
root: None,
main: None,
}
}
pub fn new_workspace(root: ImmutPath) -> Self {
Self::new_rooted(root, None)
}
pub fn new_rootless(main: VirtualPath) -> Self {
Self {
root: None,
main: Some(FileId::new(None, main)),
}
}
pub fn new_rooted_by_id(root: ImmutPath, main: FileId) -> Self {
Self::new_rooted(root, Some(main.vpath().clone()))
}
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,
}
}
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 {
root: PathBuf,
main: Option<PathBuf>,
},
RootByParent {
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()));
}
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()),
}
}
}