mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-12-23 08:47:50 +00:00
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:
parent
a325c6f6c8
commit
89c178295a
12 changed files with 216 additions and 49 deletions
|
|
@ -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();
|
||||
|
|
@ -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::*;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,15 @@
|
|||
# Tinymist Server Configuration
|
||||
|
||||
## `projectResolution`
|
||||
|
||||
This configuration specifies the way to resolved projects.
|
||||
|
||||
- **Type**: `string`
|
||||
- **Enum**:
|
||||
- `singleFile`: Manage typst documents like what we did in Markdown. Each single file is an individual document and no project resolution is needed.
|
||||
- `lockDatabase`: 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.
|
||||
- **Default**: `"singleFile"`
|
||||
|
||||
## `outputPath`
|
||||
|
||||
The path pattern to store Typst artifacts, you can use `$root` or `$dir` or `$name` to do magic configuration, e.g. `$dir/$name` (default) and `$root/target/$dir/$name`.
|
||||
|
|
|
|||
|
|
@ -276,6 +276,19 @@
|
|||
"type": "object",
|
||||
"title": "Tinymist Typst LSP",
|
||||
"properties": {
|
||||
"tinymist.projectResolution": {
|
||||
"type": "string",
|
||||
"default": "singleFile",
|
||||
"markdownDescription": "This configuration specifies the way to resolved projects.",
|
||||
"enum": [
|
||||
"singleFile",
|
||||
"lockDatabase"
|
||||
],
|
||||
"enumDescriptions": [
|
||||
"Manage typst documents like what we did in Markdown. Each single file is an individual document and no project resolution is needed.",
|
||||
"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."
|
||||
]
|
||||
},
|
||||
"tinymist.outputPath": {
|
||||
"title": "Output path",
|
||||
"description": "The path pattern to store Typst artifacts, you can use `$root` or `$dir` or `$name` to do magic configuration, e.g. `$dir/$name` (default) and `$root/target/$dir/$name`.",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue