feat: initiate lockDatabase project resolution (#1201)

* feat: create a configuration

* docs: edit description

* docs: edit description

* feat: add lock update

* test: make configuration work
This commit is contained in:
Myriad-Dreamin 2025-01-20 12:45:23 +08:00 committed by GitHub
parent a325c6f6c8
commit 89c178295a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 216 additions and 49 deletions

View file

@ -1,11 +1,32 @@
use anyhow::bail;
use serde::{Deserialize, Serialize};
use tinymist_std::ImmutPath;
use tinymist_world::EntryState;
use typst::syntax::VirtualPath;
/// The kind of project resolution.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum ProjectResolutionKind {
/// Manage typst documents like what we did in Markdown. Each single file is
/// an individual document and no project resolution is needed.
/// This is the default behavior.
#[default]
SingleFile,
/// Manage typst documents like what we did in Rust. For each workspace,
/// tinymist tracks your preview and compilation history, and stores the
/// information in a lock file. Tinymist will automatically selects the main
/// file to use according to the lock file. This also allows other tools
/// push preview and export tasks to language server by updating the
/// lock file.
LockDatabase,
}
/// Entry resolver
#[derive(Debug, Default, Clone)]
pub struct EntryResolver {
/// The kind of project resolution.
pub project_resolution: ProjectResolutionKind,
/// Specifies the root path of the project manually.
pub root_path: Option<ImmutPath>,
/// The workspace roots from initialization.
@ -78,6 +99,22 @@ impl EntryResolver {
})
}
pub fn resolve_lock(&self, entry: &EntryState) -> Option<ImmutPath> {
match self.project_resolution {
ProjectResolutionKind::LockDatabase if entry.is_in_package() => {
log::info!("ProjectResolver: no lock for package: {entry:?}");
None
}
ProjectResolutionKind::LockDatabase => {
let root = entry.workspace_root();
log::info!("ProjectResolver: lock for {entry:?} at {root:?}");
root
}
ProjectResolutionKind::SingleFile => None,
}
}
/// Determines the default entry path.
pub fn resolve_default(&self) -> Option<ImmutPath> {
let entry = self.entry.as_ref();

View file

@ -4,6 +4,7 @@
mod args;
mod compiler;
mod entry;
pub mod font;
mod lock;
mod model;
@ -11,6 +12,7 @@ mod watch;
pub mod world;
pub use args::*;
pub use compiler::*;
pub use entry::*;
pub use lock::*;
pub use model::*;
pub use watch::*;

View file

@ -1,6 +1,7 @@
use std::{path::Path, sync::Arc};
use ecow::EcoVec;
use reflexo_typst::ImmutPath;
use tinymist_std::path::unix_slash;
use tinymist_world::EntryReader;
use typst::{diag::EcoString, syntax::FileId};
@ -9,12 +10,11 @@ use crate::model::{Id, ProjectInput, ProjectMaterial, ProjectRoute, ProjectTask,
use crate::LspWorld;
/// Make a new project lock updater.
pub fn update_lock(world: &LspWorld) -> Option<ProjectLockUpdater> {
let root = world.entry_state().workspace_root()?;
Some(ProjectLockUpdater {
pub fn update_lock(root: ImmutPath) -> ProjectLockUpdater {
ProjectLockUpdater {
root,
updates: vec![],
})
}
}
enum LockUpdate {

View file

@ -19,8 +19,6 @@ pub use analysis::{CompletionFeat, LocalContext, LocalContextGuard, LspWorldExt}
pub use completion::PostfixSnippet;
pub use upstream::with_vm;
mod entry;
pub use entry::*;
mod diagnostics;
pub use diagnostics::*;
mod code_action;

View file

@ -137,6 +137,10 @@ impl EntryState {
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)]

View file

@ -14,8 +14,9 @@ use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value as JsonValue};
use strum::IntoEnumIterator;
use task::{FormatUserConfig, FormatterConfig};
use tinymist_project::{EntryResolver, ProjectResolutionKind};
use tinymist_query::analysis::{Modifier, TokenType};
use tinymist_query::{CompletionFeat, EntryResolver, PositionEncoding};
use tinymist_query::{CompletionFeat, PositionEncoding};
use tinymist_render::PeriscopeArgs;
use typst::foundations::IntoValue;
use typst::syntax::FileId;
@ -271,6 +272,7 @@ impl Initializer for SuperInit {
// region Configuration Items
const CONFIG_ITEMS: &[&str] = &[
"tinymist",
"projectResolution",
"outputPath",
"exportPdf",
"rootPath",
@ -292,6 +294,8 @@ const CONFIG_ITEMS: &[&str] = &[
/// The user configuration read from the editor.
#[derive(Debug, Default, Clone)]
pub struct Config {
/// The resolution kind of the project.
pub project_resolution: ProjectResolutionKind,
/// Constant configuration for the server.
pub const_config: ConstConfig,
/// The compile configurations
@ -383,6 +387,7 @@ impl Config {
.ok()
}
assign_config!(project_resolution := "projectResolution"?: ProjectResolutionKind);
assign_config!(semantic_tokens := "semanticTokens"?: SemanticTokensMode);
assign_config!(formatter_mode := "formatterMode"?: FormatterMode);
assign_config!(formatter_print_width := "formatterPrintWidth"?: Option<u32>);
@ -529,6 +534,7 @@ impl CompileConfig {
};
}
let project_resolution = deser_or_default!("projectResolution", ProjectResolutionKind);
self.output_path = deser_or_default!("outputPath", PathPattern);
self.export_pdf = deser_or_default!("exportPdf", ExportMode);
self.notify_status = match try_(|| update.get("compileStatus")?.as_str()) {
@ -597,6 +603,7 @@ impl CompileConfig {
self.font_paths = try_or_default(|| Vec::<_>::deserialize(update.get("fontPaths")?).ok());
self.system_fonts = try_(|| update.get("systemFonts")?.as_bool());
self.entry_resolver.project_resolution = project_resolution;
self.entry_resolver.root_path =
try_(|| Some(Path::new(update.get("rootPath")?.as_str()?).into())).or_else(|| {
self.typst_extra_args

View file

@ -30,7 +30,8 @@ use tinymist::{
};
use tinymist::{world::TaskInputs, world::WorldProvider};
use tinymist_core::LONG_VERSION;
use tinymist_query::{package::PackageInfo, EntryResolver};
use tinymist_project::EntryResolver;
use tinymist_query::package::PackageInfo;
use typst::foundations::IntoValue;
use typst_shim::utils::LazyHash;

View file

@ -25,22 +25,21 @@ use sync_lsp::*;
use task::{
ExportConfig, ExportTask, ExportUserConfig, FormatTask, FormatterConfig, UserActionTask,
};
use tinymist_project::ProjectInsId;
use tinymist_project::{CompileSnapshot, EntryResolver, ProjectInsId};
use tinymist_query::analysis::{Analysis, PeriscopeProvider};
use tinymist_query::{
to_typst_range, CompilerQueryRequest, CompilerQueryResponse, FoldRequestFeature,
OnExportRequest, PositionEncoding, ServerInfoResponse, SyntaxRequest,
to_typst_range, CompilerQueryRequest, CompilerQueryResponse, ExportKind, FoldRequestFeature,
LocalContext, LspWorldExt, OnExportRequest, PageSelection, PositionEncoding,
ServerInfoResponse, SyntaxRequest, VersionedDocument,
};
use tinymist_query::{EntryResolver, PageSelection};
use tinymist_query::{ExportKind, LocalContext, VersionedDocument};
use tinymist_render::PeriscopeRenderer;
use tinymist_std::Error;
use tinymist_std::ImmutPath;
use tinymist_std::{Error, ImmutPath};
use tokio::sync::mpsc;
use typst::layout::Position as TypstPosition;
use typst::{diag::FileResult, syntax::Source};
use crate::project::LspInterrupt;
use crate::project::{update_lock, PROJECT_ROUTE_USER_ACTION_PRIORITY};
use crate::project::{CompileServerOpts, ProjectCompiler};
use crate::stats::CompilerQueryStats;
use crate::world::vfs::{notify::MemoryEvent, FileChangeSet};
@ -1008,12 +1007,36 @@ impl LanguageState {
/// Export the current document.
pub fn on_export(&mut self, req: OnExportRequest) -> QueryFuture {
let OnExportRequest { path, kind, open } = req;
let entry = self.entry_resolver().resolve(Some(path.as_path().into()));
let lock_dir = self.compile_config().entry_resolver.resolve_lock(&entry);
let update_dep = lock_dir.clone().map(|lock_dir| {
|snap: CompileSnapshot<LspCompilerFeat>| async move {
let mut updater = update_lock(lock_dir);
let world = snap.world.clone();
let doc_id = updater.compiled(&world)?;
updater.update_materials(doc_id.clone(), snap.world.depended_files());
updater.route(doc_id, PROJECT_ROUTE_USER_ACTION_PRIORITY);
updater.commit();
Some(())
}
});
let snap = self.snapshot()?;
let entry = self.entry_resolver().resolve(Some(path.as_path().into()));
let export = self.project.export.factory.oneshot(snap, Some(entry), kind);
let task = self.project.export.factory.task();
just_future(async move {
let res = export.await?;
let snap = snap.receive().await?;
let snap = snap.task(TaskInputs {
entry: Some(entry),
..Default::default()
});
let res = task.oneshot(snap.clone(), kind, lock_dir).await?;
if let Some(update_dep) = update_dep {
tokio::spawn(update_dep(snap));
}
// See https://github.com/Myriad-Dreamin/tinymist/issues/837
// Also see https://github.com/Byron/open-rs/issues/105

View file

@ -3,10 +3,14 @@
use std::str::FromStr;
use std::{path::PathBuf, sync::Arc};
use crate::project::{CompiledArtifact, ExportSignal};
use crate::project::{
CompiledArtifact, ExportHtmlTask, ExportMarkdownTask, ExportPdfTask, ExportPngTask,
ExportSignal, ExportTextTask, ProjectTask, TaskWhen,
};
use anyhow::{bail, Context};
use reflexo::ImmutPath;
use reflexo_typst::TypstDatetime;
use tinymist_project::{EntryReader, EntryState, TaskInputs};
use tinymist_project::{CompileSnapshot, EntryReader};
use tinymist_query::{ExportKind, PageSelection};
use tokio::sync::mpsc;
use typlite::Typlite;
@ -20,8 +24,7 @@ use typst_pdf::PdfOptions;
use crate::tool::text::FullTextDigest;
use crate::{
actor::editor::EditorRequest, project::WorldSnapFut, tool::word_count, world::LspCompilerFeat,
ExportMode, PathPattern,
actor::editor::EditorRequest, tool::word_count, world::LspCompilerFeat, ExportMode, PathPattern,
};
use super::*;
@ -63,25 +66,10 @@ impl ExportTask {
}
}
impl SyncTaskFactory<ExportConfig> {
pub fn oneshot(
&self,
snap: WorldSnapFut,
entry: Option<EntryState>,
kind: ExportKind,
) -> impl Future<Output = anyhow::Result<Option<PathBuf>>> {
let export = self.task();
async move {
let snap = snap.receive().await?;
let snap = snap.task(TaskInputs {
entry,
..Default::default()
});
let artifact = snap.compile();
export.do_export(&kind, artifact).await
}
}
pub struct ExportOnceTask<'a> {
pub kind: &'a ExportKind,
pub artifact: CompiledArtifact<LspCompilerFeat>,
pub lock_path: Option<ImmutPath>,
}
#[derive(Clone, Default)]
@ -129,7 +117,14 @@ impl ExportConfig {
let this = self.clone();
let artifact = artifact.clone();
Box::pin(async move {
log_err(this.do_export(&this.kind, artifact).await);
log_err(
this.do_export(ExportOnceTask {
kind: &this.kind,
artifact,
lock_path: None,
})
.await,
);
Some(())
})
})?;
@ -173,16 +168,16 @@ impl ExportConfig {
Some(())
}
async fn do_export(
&self,
kind: &ExportKind,
artifact: CompiledArtifact<LspCompilerFeat>,
) -> anyhow::Result<Option<PathBuf>> {
async fn do_export(&self, task: ExportOnceTask<'_>) -> anyhow::Result<Option<PathBuf>> {
use reflexo_vec2svg::DefaultExportFeature;
use ExportKind::*;
use PageSelection::*;
let CompiledArtifact { snap, doc, .. } = artifact;
let ExportOnceTask {
kind,
artifact: CompiledArtifact { snap, doc, .. },
lock_path: lock_dir,
} = task;
// Prepare the output path.
let entry = snap.world.entry_state();
@ -205,6 +200,68 @@ impl ExportConfig {
}
}
let _: Option<()> = lock_dir.and_then(|lock_dir| {
let mut updater = crate::project::update_lock(lock_dir);
let doc_id = updater.compiled(&snap.world)?;
let task_id = doc_id.clone();
let when = match self.config.mode {
ExportMode::Never => TaskWhen::Never,
ExportMode::OnType => TaskWhen::OnType,
ExportMode::OnSave => TaskWhen::OnSave,
ExportMode::OnDocumentHasTitle => TaskWhen::OnSave,
};
// todo: page transforms
let transforms = vec![];
use tinymist_project::ExportTask as ProjectExportTask;
let export = ProjectExportTask {
document: doc_id,
id: task_id,
when,
transform: transforms,
};
let task = match kind {
Pdf { creation_timestamp } => {
let _ = creation_timestamp;
ProjectTask::ExportPdf(ExportPdfTask {
export,
pdf_standards: Default::default(),
})
}
Html {} => ProjectTask::ExportHtml(ExportHtmlTask { export }),
Markdown {} => ProjectTask::ExportMarkdown(ExportMarkdownTask { export }),
Text {} => ProjectTask::ExportText(ExportTextTask { export }),
Query { .. } => {
// todo: ignoring query task.
return None;
}
Svg { page } => {
// todo: ignoring page selection.
let _ = page;
return None;
}
Png { ppi, fill, page } => {
// todo: ignoring page fill.
let _ = fill;
// todo: ignoring page selection.
let _ = page;
let ppi = ppi.unwrap_or(144.) as f32;
ProjectTask::ExportPng(ExportPngTask { export, ppi })
}
};
updater.task(task);
updater.commit();
Some(())
});
// Prepare the document.
let doc = doc.map_err(|_| anyhow::anyhow!("no document"))?;
@ -327,6 +384,21 @@ impl ExportConfig {
log::info!("RenderActor({kind:?}): export complete");
Ok(Some(to))
}
pub async fn oneshot(
&self,
snap: CompileSnapshot<LspCompilerFeat>,
kind: ExportKind,
lock_path: Option<ImmutPath>,
) -> anyhow::Result<Option<PathBuf>> {
let artifact = snap.compile();
self.do_export(ExportOnceTask {
kind: &kind,
artifact,
lock_path,
})
.await
}
}
fn parse_color(fill: String) -> anyhow::Result<Color> {

View file

@ -34,7 +34,7 @@ impl<T: Clone> SyncTaskFactory<T> {
*w = Arc::new(config);
}
fn task(&self) -> Arc<T> {
pub fn task(&self) -> Arc<T> {
self.0.read().unwrap().clone()
}
}