mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-11-25 05:22:52 +00:00
Merge 64e32c79d2 into 06c2240caa
This commit is contained in:
commit
f673f7c580
8 changed files with 445 additions and 148 deletions
|
|
@ -1,23 +1,18 @@
|
|||
use std::ops::Deref;
|
||||
|
||||
use comemo::Track;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tinymist_analysis::analyze_expr;
|
||||
use tinymist_project::{DiagnosticFormat, PathPattern};
|
||||
use tinymist_project::PathPattern;
|
||||
use tinymist_std::error::prelude::*;
|
||||
use tinymist_world::vfs::WorkspaceResolver;
|
||||
use tinymist_world::{EntryReader, EntryState, ShadowApi, diag::print_diagnostics_to_string};
|
||||
use typst::diag::{At, SourceResult};
|
||||
use typst::foundations::{Args, Dict, NativeFunc, eco_format};
|
||||
use typst::syntax::Span;
|
||||
use typst::utils::LazyHash;
|
||||
use tinymist_world::{EntryReader, ShadowApi};
|
||||
use typst::foundations::{Dict, eco_format};
|
||||
use typst::{
|
||||
foundations::{Bytes, IntoValue, StyleChain},
|
||||
text::TextElem,
|
||||
};
|
||||
use typst_shim::eval::{Eval, Vm};
|
||||
use typst_shim::syntax::LinkedNodeExt;
|
||||
|
||||
use crate::hook::HookScript;
|
||||
use crate::{
|
||||
prelude::*,
|
||||
syntax::{InterpretMode, interpret_mode_at},
|
||||
|
|
@ -112,7 +107,7 @@ impl SemanticRequest for InteractCodeContextRequest {
|
|||
for query in self.query {
|
||||
responses.push(query.and_then(|query| match query {
|
||||
InteractCodeContextQuery::PathAt { code, inputs: base } => {
|
||||
let res = eval_path_expr(ctx, &code, base)?;
|
||||
let res = eval_path(ctx, &code, base)?;
|
||||
Some(InteractCodeContextResponse::PathAt(res))
|
||||
}
|
||||
InteractCodeContextQuery::ModeAt { position } => {
|
||||
|
|
@ -217,65 +212,16 @@ impl InteractCodeContextRequest {
|
|||
}
|
||||
}
|
||||
|
||||
fn eval_path_expr(
|
||||
fn eval_path(
|
||||
ctx: &mut LocalContext,
|
||||
code: &str,
|
||||
inputs: Dict,
|
||||
) -> Option<QueryResult<serde_json::Value>> {
|
||||
let entry = ctx.world().entry_state();
|
||||
let path = if code.starts_with("{") && code.ends_with("}") {
|
||||
let id = entry
|
||||
.select_in_workspace(Path::new("/__path__.typ"))
|
||||
.main()?;
|
||||
|
||||
let inputs = make_sys(&entry, ctx.world().inputs(), inputs);
|
||||
let (inputs, root, dir, name) = match inputs {
|
||||
Some(EvalSysCtx {
|
||||
inputs,
|
||||
root,
|
||||
dir,
|
||||
name,
|
||||
}) => (Some(inputs), Some(root), dir, Some(name)),
|
||||
None => (None, None, None, None),
|
||||
};
|
||||
|
||||
let mut world = ctx.world().task(tinymist_world::TaskInputs {
|
||||
entry: None,
|
||||
inputs,
|
||||
});
|
||||
// todo: bad performance
|
||||
world.take_db();
|
||||
let _ = world.map_shadow_by_id(id, Bytes::from_string(code.to_owned()));
|
||||
|
||||
tinymist_analysis::upstream::with_vm((&world as &dyn World).track(), |vm| {
|
||||
define_val(vm, "join", Value::Func(join::data().into()));
|
||||
for (key, value) in [("root", root), ("dir", dir), ("name", name)] {
|
||||
if let Some(value) = value {
|
||||
define_val(vm, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
let mut expr = typst::syntax::parse_code(code);
|
||||
let span = Span::from_range(id, 0..code.len());
|
||||
expr.synthesize(span);
|
||||
|
||||
let expr = match expr.cast::<ast::Code>() {
|
||||
Some(v) => v,
|
||||
None => bail!(
|
||||
"code is not a valid code expression: kind={:?}",
|
||||
expr.kind()
|
||||
),
|
||||
};
|
||||
match expr.eval(vm) {
|
||||
Ok(value) => serde_json::to_value(value).context_ut("failed to serialize path"),
|
||||
Err(e) => {
|
||||
let res =
|
||||
print_diagnostics_to_string(&world, e.iter(), DiagnosticFormat::Human);
|
||||
let err = res.unwrap_or_else(|e| e);
|
||||
bail!("failed to evaluate path expression: {err}")
|
||||
}
|
||||
}
|
||||
})
|
||||
crate::hook::eval_script(ctx.world(), HookScript::Code(code), inputs, &entry).and_then(
|
||||
|(_, value)| serde_json::to_value(value).context_ut("failed to serialize path"),
|
||||
)
|
||||
} else {
|
||||
PathPattern::new(code)
|
||||
.substitute(&entry)
|
||||
|
|
@ -287,85 +233,6 @@ fn eval_path_expr(
|
|||
Some(path.into())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
struct EvalSysCtx {
|
||||
inputs: Arc<LazyHash<Dict>>,
|
||||
root: Value,
|
||||
dir: Option<Value>,
|
||||
name: Value,
|
||||
}
|
||||
|
||||
#[comemo::memoize]
|
||||
fn make_sys(entry: &EntryState, base: Arc<LazyHash<Dict>>, inputs: Dict) -> Option<EvalSysCtx> {
|
||||
let root = entry.root();
|
||||
let main = entry.main();
|
||||
|
||||
log::debug!("Check path {main:?} and root {root:?}");
|
||||
|
||||
let (root, main) = root.zip(main)?;
|
||||
|
||||
// Files in packages are not exported
|
||||
if WorkspaceResolver::is_package_file(main) {
|
||||
return None;
|
||||
}
|
||||
// Files without a path are not exported
|
||||
let path = main.vpath().resolve(&root)?;
|
||||
|
||||
// todo: handle untitled path
|
||||
if path.strip_prefix("/untitled").is_ok() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let path = path.strip_prefix(&root).ok()?;
|
||||
let dir = path.parent();
|
||||
let file_name = path.file_name().unwrap_or_default();
|
||||
|
||||
let root = Value::Str(root.to_string_lossy().into());
|
||||
|
||||
let dir = dir.map(|d| Value::Str(d.to_string_lossy().into()));
|
||||
|
||||
let name = file_name.to_string_lossy();
|
||||
let name = name.as_ref().strip_suffix(".typ").unwrap_or(name.as_ref());
|
||||
let name = Value::Str(name.into());
|
||||
|
||||
let mut dict = base.as_ref().deref().clone();
|
||||
for (key, value) in inputs {
|
||||
dict.insert(key, value);
|
||||
}
|
||||
dict.insert("root".into(), root.clone());
|
||||
if let Some(dir) = &dir {
|
||||
dict.insert("dir".into(), dir.clone());
|
||||
}
|
||||
dict.insert("name".into(), name.clone());
|
||||
|
||||
Some(EvalSysCtx {
|
||||
inputs: Arc::new(LazyHash::new(dict)),
|
||||
root,
|
||||
dir,
|
||||
name,
|
||||
})
|
||||
}
|
||||
|
||||
fn define_val(vm: &mut Vm, name: &str, value: Value) {
|
||||
let ident = SyntaxNode::leaf(SyntaxKind::Ident, name);
|
||||
vm.define(ident.cast::<ast::Ident>().unwrap(), value);
|
||||
}
|
||||
|
||||
#[typst_macros::func(title = "Join function")]
|
||||
fn join(args: &mut Args) -> SourceResult<Value> {
|
||||
let pos = args.take().to_pos();
|
||||
let mut res = PathBuf::new();
|
||||
for arg in pos {
|
||||
match arg {
|
||||
Value::Str(s) => res.push(s.as_str()),
|
||||
_ => {
|
||||
return Err(eco_format!("join argument is not a string: {arg:?}")).at(args.span);
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(Value::Str(res.to_string_lossy().into()))
|
||||
}
|
||||
|
||||
/// A result of a query.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
|
|
|
|||
149
crates/tinymist-query/src/hook/export.rs
Normal file
149
crates/tinymist-query/src/hook/export.rs
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context;
|
||||
use ecow::eco_format;
|
||||
use tinymist_project::{EntryReader, ExportTask, LspWorld};
|
||||
use tinymist_std::error::prelude::*;
|
||||
use typst::{
|
||||
diag::{At, SourceResult, StrResult},
|
||||
foundations::{Dict, Func, Str, Value},
|
||||
syntax::Span,
|
||||
};
|
||||
|
||||
use crate::hook::HookScript;
|
||||
|
||||
/// The state desc of an export script.
|
||||
pub enum ExportState {
|
||||
/// A debounce state.
|
||||
Debounce {
|
||||
/// The world to run the script in.
|
||||
world: LspWorld,
|
||||
/// The inner function to debounce.
|
||||
inner: Func,
|
||||
/// The duration to debounce.
|
||||
duration: Duration,
|
||||
/// The time the state was last checked.
|
||||
checked: tinymist_std::time::Time,
|
||||
},
|
||||
/// A finished state.
|
||||
Finished {
|
||||
/// The tasks to run.
|
||||
task: Vec<ExportTask>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Runs an export script.
|
||||
pub fn run_export_script(world: &LspWorld, code: &str, inputs: Dict) -> Result<ExportState> {
|
||||
let result = super::eval_script(world, HookScript::Code(code), inputs, &world.entry_state())?;
|
||||
check_script_res(result)
|
||||
}
|
||||
|
||||
/// Determines the export of a state.
|
||||
pub fn determine_export(state: ExportState) -> Result<ExportState> {
|
||||
match state {
|
||||
ExportState::Debounce {
|
||||
world,
|
||||
inner,
|
||||
duration,
|
||||
checked,
|
||||
} => {
|
||||
let now = tinymist_std::time::now();
|
||||
if now
|
||||
.duration_since(checked)
|
||||
.context("failed to get duration since last checked")?
|
||||
< duration
|
||||
{
|
||||
Ok(ExportState::Debounce {
|
||||
world,
|
||||
inner,
|
||||
duration,
|
||||
checked,
|
||||
})
|
||||
} else {
|
||||
check_script_res(super::eval_script(
|
||||
&world,
|
||||
HookScript::Callback(inner),
|
||||
Dict::default(),
|
||||
&world.entry_state(),
|
||||
)?)
|
||||
}
|
||||
}
|
||||
ExportState::Finished { task } => Ok(ExportState::Finished { task }),
|
||||
}
|
||||
}
|
||||
|
||||
fn check_script_res((world, res): (LspWorld, Value)) -> Result<ExportState> {
|
||||
match res {
|
||||
Value::Dict(d) => {
|
||||
let kind = match d.get("kind") {
|
||||
Ok(Value::Str(kind)) => kind,
|
||||
_ => bail!("expected result.kind to be a string"),
|
||||
};
|
||||
Ok(match kind.as_str() {
|
||||
"debounce" => {
|
||||
let inner = match d.get("inner") {
|
||||
Ok(Value::Func(func)) => func.clone(),
|
||||
_ => bail!("expected result.inner to be a function"),
|
||||
};
|
||||
let duration = match d.get("duration") {
|
||||
Ok(Value::Int(duration)) => Duration::from_millis((*duration) as u64),
|
||||
_ => bail!("expected result.duration to be a duration"),
|
||||
};
|
||||
ExportState::Debounce {
|
||||
world,
|
||||
inner,
|
||||
duration,
|
||||
checked: tinymist_std::time::now(),
|
||||
}
|
||||
}
|
||||
_ => bail!("expected result.kind to be 'debounce'"),
|
||||
})
|
||||
}
|
||||
_ => bail!("expected result to be a dictionary"),
|
||||
}
|
||||
}
|
||||
|
||||
#[typst_macros::func(title = "debounce function")]
|
||||
pub(crate) fn debounce(span: Span, duration: Str, inner: Func) -> SourceResult<Dict> {
|
||||
let duration = parse_time(duration.as_str()).at(span)?;
|
||||
let mut res = Dict::default();
|
||||
|
||||
res.insert("inner".into(), Value::Func(inner.clone()));
|
||||
res.insert("kind".into(), Value::Str("debounce".into()));
|
||||
res.insert("duration".into(), Value::Int(duration.as_millis() as i64));
|
||||
|
||||
// let global = engine.world.library().global.scope();
|
||||
// let sys = global.get("sys").unwrap().read().scope().unwrap();
|
||||
// let inputs = sys.get("inputs").unwrap().read().clone();
|
||||
// let last = match inputs {
|
||||
// Value::Dict(dict) => dict.get("x-last").at(span)?.clone(),
|
||||
// _ => return Err(eco_format!("expected sys.inputs to be a
|
||||
// dict")).at(span), };
|
||||
// let last_duration = match last {
|
||||
// Value::Str(stamp) => Duration::from_millis(
|
||||
// stamp
|
||||
// .as_str()
|
||||
// .parse::<u64>()
|
||||
// .map_err(|e| eco_format!("expected sys.inputs.x-last to be a int,
|
||||
// but {e}")) .at(span)?,
|
||||
// ),
|
||||
// _ => return Err(eco_format!("expected sys.inputs.x-last to be a
|
||||
// int")).at(span), };
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn parse_time(spec: &str) -> StrResult<Duration> {
|
||||
let (digits, unit) = if let Some(digits) = spec.strip_suffix("ms") {
|
||||
(digits, 1u64)
|
||||
} else if let Some(digits) = spec.strip_suffix("s") {
|
||||
(digits, 1000u64)
|
||||
} else {
|
||||
return Err("expected time spec like `5s` or `5ms`".into());
|
||||
};
|
||||
|
||||
let digits = digits
|
||||
.parse::<u64>()
|
||||
.map_err(|e| eco_format!("expected time spec like `5s` or `5ms`, but {e}"))?;
|
||||
Ok(Duration::from_millis(digits * unit))
|
||||
}
|
||||
186
crates/tinymist-query/src/hook/mod.rs
Normal file
186
crates/tinymist-query/src/hook/mod.rs
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
//! Runs hook scripts for the server.
|
||||
|
||||
mod export;
|
||||
|
||||
use std::ops::Deref;
|
||||
|
||||
pub use export::*;
|
||||
use tinymist_world::diag::print_diagnostics_to_string;
|
||||
use tinymist_world::vfs::WorkspaceResolver;
|
||||
|
||||
use crate::prelude::*;
|
||||
use comemo::Track;
|
||||
use tinymist_project::LspWorld;
|
||||
use tinymist_std::error::prelude::*;
|
||||
use tinymist_world::{DiagnosticFormat, EntryState, ShadowApi};
|
||||
use typst::World;
|
||||
use typst::diag::{At, SourceResult};
|
||||
use typst::foundations::{Args, Bytes, Context, Dict, Func, NativeFunc, eco_format};
|
||||
use typst::syntax::Span;
|
||||
use typst::syntax::SyntaxKind;
|
||||
use typst::syntax::SyntaxNode;
|
||||
use typst::syntax::ast;
|
||||
use typst::utils::LazyHash;
|
||||
use typst_shim::eval::{Eval, Vm};
|
||||
|
||||
/// The hook script.
|
||||
pub enum HookScript<'a> {
|
||||
/// A code script.
|
||||
Code(&'a str),
|
||||
/// A function callback.
|
||||
Callback(Func),
|
||||
}
|
||||
|
||||
/// Evaluates a hook script.
|
||||
pub fn eval_script(
|
||||
world: &LspWorld,
|
||||
code: HookScript,
|
||||
inputs: Dict,
|
||||
entry: &EntryState,
|
||||
) -> Result<(LspWorld, Value)> {
|
||||
let id = entry
|
||||
.select_in_workspace(Path::new("/__script__.typ"))
|
||||
.main()
|
||||
.expect("cannot create script file id");
|
||||
|
||||
let inputs = make_sys(entry, world.inputs(), inputs);
|
||||
let (inputs, root, dir, name) = match inputs {
|
||||
Some(EvalSysCtx {
|
||||
inputs,
|
||||
root,
|
||||
dir,
|
||||
name,
|
||||
}) => (Some(inputs), Some(root), dir, Some(name)),
|
||||
None => (None, None, None, None),
|
||||
};
|
||||
|
||||
let mut world = world.task(tinymist_world::TaskInputs {
|
||||
entry: None,
|
||||
inputs,
|
||||
});
|
||||
if let HookScript::Code(code) = &code {
|
||||
// todo: bad performance
|
||||
world.take_db();
|
||||
let _ = world.map_shadow_by_id(id, Bytes::from_string((*code).to_owned()));
|
||||
}
|
||||
|
||||
let res = tinymist_analysis::upstream::with_vm((&world as &dyn World).track(), |vm| {
|
||||
define_val(vm, "join", Value::Func(join::data().into()));
|
||||
define_val(vm, "debounce", Value::Func(debounce::data().into()));
|
||||
for (key, value) in [("root", root), ("dir", dir), ("name", name)] {
|
||||
if let Some(value) = value {
|
||||
define_val(vm, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
let res = match code {
|
||||
HookScript::Code(code) => {
|
||||
let mut expr = typst::syntax::parse_code(code);
|
||||
let span = Span::from_range(id, 0..code.len());
|
||||
expr.synthesize(span);
|
||||
|
||||
let expr = match expr.cast::<ast::Code>() {
|
||||
Some(v) => v,
|
||||
None => bail!(
|
||||
"code is not a valid code expression: kind={:?}",
|
||||
expr.kind()
|
||||
),
|
||||
};
|
||||
expr.eval(vm)
|
||||
}
|
||||
HookScript::Callback(callback) => callback.call(
|
||||
&mut vm.engine,
|
||||
Context::default().track(),
|
||||
Vec::<Value>::default(),
|
||||
),
|
||||
};
|
||||
match res {
|
||||
Ok(value) => Ok(value),
|
||||
Err(e) => {
|
||||
let res = print_diagnostics_to_string(&world, e.iter(), DiagnosticFormat::Human);
|
||||
let err = res.unwrap_or_else(|e| e);
|
||||
bail!("failed to evaluate expression: {err}")
|
||||
}
|
||||
}
|
||||
})?;
|
||||
Ok((world, res))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Hash)]
|
||||
struct EvalSysCtx {
|
||||
inputs: Arc<LazyHash<Dict>>,
|
||||
root: Value,
|
||||
dir: Option<Value>,
|
||||
name: Value,
|
||||
}
|
||||
|
||||
#[comemo::memoize]
|
||||
fn make_sys(entry: &EntryState, base: Arc<LazyHash<Dict>>, inputs: Dict) -> Option<EvalSysCtx> {
|
||||
let root = entry.root();
|
||||
let main = entry.main();
|
||||
|
||||
log::debug!("Check path {main:?} and root {root:?}");
|
||||
|
||||
let (root, main) = root.zip(main)?;
|
||||
|
||||
// Files in packages are not exported
|
||||
if WorkspaceResolver::is_package_file(main) {
|
||||
return None;
|
||||
}
|
||||
// Files without a path are not exported
|
||||
let path = main.vpath().resolve(&root)?;
|
||||
|
||||
// todo: handle untitled path
|
||||
if path.strip_prefix("/untitled").is_ok() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let path = path.strip_prefix(&root).ok()?;
|
||||
let dir = path.parent();
|
||||
let file_name = path.file_name().unwrap_or_default();
|
||||
|
||||
let root = Value::Str(root.to_string_lossy().into());
|
||||
|
||||
let dir = dir.map(|d| Value::Str(d.to_string_lossy().into()));
|
||||
|
||||
let name = file_name.to_string_lossy();
|
||||
let name = name.as_ref().strip_suffix(".typ").unwrap_or(name.as_ref());
|
||||
let name = Value::Str(name.into());
|
||||
|
||||
let mut dict = base.as_ref().deref().clone();
|
||||
for (key, value) in inputs {
|
||||
dict.insert(key, value);
|
||||
}
|
||||
dict.insert("root".into(), root.clone());
|
||||
if let Some(dir) = &dir {
|
||||
dict.insert("dir".into(), dir.clone());
|
||||
}
|
||||
dict.insert("name".into(), name.clone());
|
||||
|
||||
Some(EvalSysCtx {
|
||||
inputs: Arc::new(LazyHash::new(dict)),
|
||||
root,
|
||||
dir,
|
||||
name,
|
||||
})
|
||||
}
|
||||
|
||||
fn define_val(vm: &mut Vm, name: &str, value: Value) {
|
||||
let ident = SyntaxNode::leaf(SyntaxKind::Ident, name);
|
||||
vm.define(ident.cast::<ast::Ident>().unwrap(), value);
|
||||
}
|
||||
|
||||
#[typst_macros::func(title = "Join function")]
|
||||
fn join(args: &mut Args) -> SourceResult<Value> {
|
||||
let pos = args.take().to_pos();
|
||||
let mut res = PathBuf::new();
|
||||
for arg in pos {
|
||||
match arg {
|
||||
Value::Str(s) => res.push(s.as_str()),
|
||||
_ => {
|
||||
return Err(eco_format!("join argument is not a string: {arg:?}")).at(args.span);
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(Value::Str(res.to_string_lossy().into()))
|
||||
}
|
||||
|
|
@ -44,6 +44,7 @@ pub use workspace_label::*;
|
|||
|
||||
pub mod analysis;
|
||||
pub mod docs;
|
||||
pub mod hook;
|
||||
pub mod index;
|
||||
pub mod package;
|
||||
pub mod syntax;
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ pub enum ProjectTask {
|
|||
ExportTeX(ExportTeXTask),
|
||||
/// An export Text task.
|
||||
ExportText(ExportTextTask),
|
||||
/// An query task.
|
||||
/// A query task.
|
||||
Query(QueryTask),
|
||||
// todo: compatibility
|
||||
// An export task of another type.
|
||||
|
|
|
|||
|
|
@ -445,6 +445,89 @@ impl ProjectPreviewState {
|
|||
}
|
||||
}
|
||||
|
||||
struct CompileHooks {
|
||||
pub(crate) export: ExportHook,
|
||||
pub(crate) preview: PreviewHook,
|
||||
pub(crate) diag: DiagHook,
|
||||
pub(crate) word_count: WordCountHook,
|
||||
}
|
||||
|
||||
impl CompileHooks {
|
||||
pub fn needs_comile(&self, snap: &LspCompileSnapshot) -> bool {
|
||||
self.export.needs_comile(snap)
|
||||
|| self.preview.needs_comile(snap)
|
||||
|| self.diag.needs_comile(snap)
|
||||
|| self.word_count.needs_comile(snap)
|
||||
}
|
||||
}
|
||||
|
||||
struct ExportHook {
|
||||
pub(crate) task: crate::task::ExportTask,
|
||||
}
|
||||
|
||||
impl ExportHook {
|
||||
fn needs_comile(&self, snap: &LspCompileSnapshot) -> bool {
|
||||
let s = snap.signal;
|
||||
|
||||
let config = self.task.factory.task();
|
||||
let when = config.task.when().unwrap_or(&TaskWhen::Never);
|
||||
needs_compile(snap, when)
|
||||
}
|
||||
}
|
||||
|
||||
struct PreviewHook {
|
||||
#[cfg(feature = "preview")]
|
||||
pub(crate) preview: ProjectPreviewState,
|
||||
pub(crate) when: TaskWhen,
|
||||
}
|
||||
|
||||
impl PreviewHook {
|
||||
fn needs_comile(&self, snap: &LspCompileSnapshot) -> bool {
|
||||
needs_compile(snap, &self.when)
|
||||
}
|
||||
}
|
||||
|
||||
struct DiagHook {
|
||||
pub(crate) editor_tx: EditorSender,
|
||||
/// When to trigger the diag.
|
||||
pub(crate) when: TaskWhen,
|
||||
/// When to trigger the lint.
|
||||
pub(crate) lint: TaskWhen,
|
||||
}
|
||||
|
||||
impl DiagHook {
|
||||
fn needs_comile(&self, snap: &LspCompileSnapshot) -> bool {
|
||||
needs_compile(snap, &self.when) || needs_compile(snap, &self.lint)
|
||||
}
|
||||
}
|
||||
|
||||
struct WordCountHook {
|
||||
count_words: bool,
|
||||
}
|
||||
|
||||
impl WordCountHook {
|
||||
fn needs_comile(&self, snap: &LspCompileSnapshot) -> bool {
|
||||
let when = if self.count_words {
|
||||
&TaskWhen::OnSave
|
||||
} else {
|
||||
&TaskWhen::Never
|
||||
};
|
||||
needs_compile(snap, when)
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_compile(snap: &LspCompileSnapshot, when: &TaskWhen) -> bool {
|
||||
let s = snap.signal;
|
||||
match when {
|
||||
TaskWhen::Never => false,
|
||||
TaskWhen::Script => s.by_entry_update,
|
||||
TaskWhen::OnType => s.by_mem_events,
|
||||
TaskWhen::OnSave => s.by_fs_events,
|
||||
// todo: respect doc
|
||||
TaskWhen::OnDocumentHasTitle => s.by_fs_events, // && doc.info().title.is_some(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The implementation of the compile handler.
|
||||
pub struct CompileHandlerImpl {
|
||||
/// The analysis data.
|
||||
|
|
@ -659,6 +742,9 @@ impl CompileHandler<LspCompilerFeat, ProjectInsStateExt> for CompileHandlerImpl
|
|||
|
||||
s.ext.pending_reasons = CompileSignal::default();
|
||||
s.ext.emitted_reasons = reason;
|
||||
|
||||
// todo: reason here.
|
||||
|
||||
let Some(compile_fn) = s.may_compile(&c.handler) else {
|
||||
continue;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ The following example demonstrates how to customize the paste behavior when past
|
|||
|
||||
Specifically, three script hooks will be supported:
|
||||
- Hook on Paste: customize the paste behavior when pasting resources into the editing typst document.
|
||||
- Hook on Watch: customize the watch behavior when a file change is detected in the workspaces.
|
||||
- Hook on Export: customize the export behavior when a file change is detected in the workspaces.
|
||||
- Hook on Generating Code Actions and Lenses: adding additional code actions by typst scripting.
|
||||
|
||||
= Customizing Paste Behavior
|
||||
|
|
@ -109,11 +109,9 @@ If the result is a string, it will be treated as the `dir` field, i.e. `{ dir: <
|
|||
|
||||
More fields will be supported in the future. If you have any suggestions, please feel free to open an issue.
|
||||
|
||||
= Customizing Watch Behavior (Experimental)
|
||||
= Customizing Export Behavior (Experimental)
|
||||
|
||||
*Note: this is not implemented yet in current version.*
|
||||
|
||||
You could configure `tinymist.onWatch` to customize the watch behavior. It will be executed when a file change is detected in the workspace.
|
||||
You could configure `tinymist.onExport` to customize the export behavior. It will be executed when a file change is detected in the workspace.
|
||||
|
||||
For example, debouncing time:
|
||||
|
||||
|
|
@ -151,6 +149,8 @@ async function pdfWithGhostScript() {
|
|||
|
||||
Hint: you could create your own vscode extension to define such custom commands.
|
||||
|
||||
`tinymist.exportPdf` will be ignored if this configuration item is set.
|
||||
|
||||
= Providing Package-Specific Code Actions (Experimental)
|
||||
|
||||
*Note: this is not implemented yet in current version.*
|
||||
|
|
|
|||
|
|
@ -1303,6 +1303,14 @@ zh = "自定义粘贴行为的脚本"
|
|||
en = "The script to be executed when pasting resources into the editing typst document. If the script code starts with `{` and ends with `}`, it will be evaluated as a typst code expression, e.g. `$root/x/$dir/$name` evaluated as `/path/to/root/x/dir/main`, otherwise it will be evaluated as a path pattern, e.g. `{ join(root, \"x\", dir, if name.ends-with(\".png\") (\"imgs\"), name) }` evaluated as `/path/to/root/x/dir/imgs/main`. The extra valid definitions are `root`, `dir`, `name`, and `join`. To learn more about the paste script, please visit [Script Hooks](https://myriad-dreamin.github.io/tinymist/feature/script-hook.html). Hint: you could import `@local` packages in the paste script. Note: restarting the editor is required to change this setting."
|
||||
zh = "将资源粘贴到正在编辑的 typst 文档时要执行的脚本。如果脚本代码以 `{` 开头并以 `}` 结尾,那么依 typst 代码表达式语法解释脚本,否则依路径模板解释脚本。路径模板例如 `$root/x/$dir/$name` 求解为 `/path/to/root/x/dir/main`;typst 代码表达式例如 `{ join(root, \"x\", dir, if name.ends-with(\".png\") (\"imgs\"), name) }` 求解为 `/path/to/root/x/dir/imgs/main`。额外的有效定义有 `root`、`dir`、`name` 和 `join`。要了解有关粘贴脚本的更多信息,请访问 [Script Hooks](https://myriad-dreamin.github.io/tinymist/feature/script-hook.html)。提示:您可以在粘贴脚本中导入 `@local` 包。注意:更改此设置需要重新启动编辑器。"
|
||||
|
||||
[extension.tinymist.config.tinymist.onExport.title]
|
||||
en = "The Script to Customize Export Behavior"
|
||||
zh = "自定义导出行为的脚本"
|
||||
|
||||
[extension.tinymist.config.tinymist.onExport.desc]
|
||||
en = "The script to be executed when perform export on change of typst document. `tinymist.exportPdf` will be ignored if this configuration item is set."
|
||||
zh = "当修改 Typst 文档时用于确定导出行为的脚本。此配置不为空时,将忽略`tinymist.exportPdf`。"
|
||||
|
||||
[extension.tinymist.config.tinymist.renderDocs.title]
|
||||
en = "(Experimental) Render Docs"
|
||||
zh = "(实验性)渲染文档"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue