From c206933bf5093414e15df4720b03d35ed66d0774 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin <35292584+Myriad-Dreamin@users.noreply.github.com> Date: Tue, 11 Nov 2025 02:43:02 +0800 Subject: [PATCH] feat: add `PathAt` code context query (#2232) This is used for custom paste scripts - by pattern: e.g. `$root` - by code: e.g. `{ root }` - on conflict callback: e.g. `{ (dir: root, on-conflict: root + "/" + random() + ".png") }` --- Cargo.lock | 1 + crates/tinymist-query/Cargo.toml | 1 + crates/tinymist-query/src/analysis.rs | 2 +- crates/tinymist-query/src/code_context.rs | 296 +++++++++++++++++- .../fixtures/code_context_path_at/at_root.typ | 17 + .../fixtures/code_context_path_at/in_dir.typ | 1 + .../snaps/test@at_root.typ.snap | 136 ++++++++ .../snaps/test@in_dir.typ.snap | 133 ++++++++ crates/tinymist-query/src/references.rs | 2 +- crates/tinymist-query/src/tests.rs | 53 +++- crates/tinymist-std/src/error.rs | 13 + crates/tinymist-task/src/primitives.rs | 3 +- 12 files changed, 645 insertions(+), 13 deletions(-) create mode 100644 crates/tinymist-query/src/fixtures/code_context_path_at/at_root.typ create mode 100644 crates/tinymist-query/src/fixtures/code_context_path_at/in_dir.typ create mode 100644 crates/tinymist-query/src/fixtures/code_context_path_at/snaps/test@at_root.typ.snap create mode 100644 crates/tinymist-query/src/fixtures/code_context_path_at/snaps/test@in_dir.typ.snap diff --git a/Cargo.lock b/Cargo.lock index 29c74777..4ce0e17e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4745,6 +4745,7 @@ dependencies = [ "typlite", "typst", "typst-assets", + "typst-library", "typst-macros", "typst-shim", "typst-timing 0.14.0", diff --git a/crates/tinymist-query/Cargo.toml b/crates/tinymist-query/Cargo.toml index 66d816d4..5a8a2a1d 100644 --- a/crates/tinymist-query/Cargo.toml +++ b/crates/tinymist-query/Cargo.toml @@ -55,6 +55,7 @@ typst.workspace = true typst-macros.workspace = true typst-shim.workspace = true typst-timing.workspace = true +typst-library.workspace = true unscanny.workspace = true walkdir.workspace = true yaml-rust2.workspace = true diff --git a/crates/tinymist-query/src/analysis.rs b/crates/tinymist-query/src/analysis.rs index 09016d4e..d9cdbb27 100644 --- a/crates/tinymist-query/src/analysis.rs +++ b/crates/tinymist-query/src/analysis.rs @@ -642,7 +642,7 @@ mod lint_tests { let result = crate::diagnostics::DiagWorker::new(ctx).convert_all(result.iter()); let result = result .into_iter() - .map(|(k, v)| (file_path_(&k), v)) + .map(|(k, v)| (file_uri_(&k), v)) .collect::>(); assert_snapshot!(JsonRepr::new_redacted(result, &REDACT_LOC)); }); diff --git a/crates/tinymist-query/src/code_context.rs b/crates/tinymist-query/src/code_context.rs index c0e48d52..6d3dd643 100644 --- a/crates/tinymist-query/src/code_context.rs +++ b/crates/tinymist-query/src/code_context.rs @@ -1,10 +1,21 @@ +use std::ops::Deref; + +use comemo::Track; use serde::{Deserialize, Serialize}; use tinymist_analysis::analyze_expr; -use tinymist_world::ShadowApi; +use tinymist_project::{DiagnosticFormat, 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 typst::{ foundations::{Bytes, IntoValue, StyleChain}, text::TextElem, }; +use typst_shim::eval::{Eval, Vm}; use typst_shim::syntax::LinkedNodeExt; use crate::{ @@ -16,6 +27,38 @@ use crate::{ #[derive(Debug, Clone, Deserialize)] #[serde(tag = "kind", rename_all = "camelCase")] pub enum InteractCodeContextQuery { + /// (Experimental) Evaluate a path expression at a specific position in a + /// text document. + PathAt { + /// Code to evaluate. If the code starts with `{` and ends with `}`, it + /// will be evaluated as a code expression, otherwise it will be + /// evaluated as a path pattern. + /// + /// ## Example + /// + /// evaluate a path pattern, which could use following definitions: + /// + /// ```plain + /// $root/x/$dir/../$name // is evaluated as + /// /path/to/root/x/dir/../main + /// ``` + /// + /// ## Example + /// + /// evaluate a code expression, which could use following definitions: + /// - `root`: the root of the workspace + /// - `dir`: the directory of the current file + /// - `name`: the name of the current file + /// - `join(a, b, ...)`: join the arguments with the path separator + /// + /// ```plain + /// { join(root, "x", dir, "y", name) } // is evaluated as + /// /path/to/root/x/dir/y/main + /// ``` + code: String, + /// The extra `sys.inputs` for the code expression. + inputs: Dict, + }, /// Get the mode at a specific position in a text document. ModeAt { /// The position inside the text document. @@ -34,6 +77,8 @@ pub enum InteractCodeContextQuery { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "camelCase")] pub enum InteractCodeContextResponse { + /// Evaluate a path expression at a specific position in a text document. + PathAt(QueryResult), /// Get the mode at a specific position in a text document. ModeAt { /// The mode at the requested position. @@ -66,6 +111,10 @@ 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)?; + Some(InteractCodeContextResponse::PathAt(res)) + } InteractCodeContextQuery::ModeAt { position } => { let cursor = ctx.to_typst_pos(position, &source)?; let mode = Self::mode_at(&source, cursor)?; @@ -167,3 +216,248 @@ impl InteractCodeContextRequest { } } } + +fn eval_path_expr( + ctx: &mut LocalContext, + code: &str, + inputs: Dict, +) -> Option> { + 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::() { + 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}") + } + } + }) + } else { + PathPattern::new(code) + .substitute(&entry) + .context_ut("failed to substitute path pattern") + .and_then(|path| { + serde_json::to_value(path.deref()).context_ut("failed to serialize path") + }) + }; + Some(path.into()) +} + +#[derive(Debug, Clone, Hash)] +struct EvalSysCtx { + inputs: Arc>, + root: Value, + dir: Option, + name: Value, +} + +#[comemo::memoize] +fn make_sys(entry: &EntryState, base: Arc>, inputs: Dict) -> Option { + 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::().unwrap(), value); +} + +#[typst_macros::func(title = "Join function")] +fn join(args: &mut Args) -> SourceResult { + 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)] +pub enum QueryResult { + /// A successful result. + Success { + /// The value of the result. + value: T, + }, + /// An error result. + Error { + /// The error message. + error: EcoString, + }, +} + +impl QueryResult { + /// Creates a successful result. + pub fn success(value: T) -> Self { + Self::Success { value } + } + + /// Creates an error result. + pub fn error(error: EcoString) -> Self { + Self::Error { error } + } +} + +impl From> for QueryResult { + fn from(value: Result) -> Self { + match value { + Ok(value) => QueryResult::success(value), + Err(error) => QueryResult::error(eco_format!("{error}")), + } + } +} + +#[cfg(test)] +mod tests { + use typst::foundations::dict; + + use super::*; + use crate::tests::*; + + #[test] + fn test() { + snapshot_testing("code_context_path_at", &|ctx, path| { + let patterns = [ + "$root/$dir/$name", + "$root/$name", + "$root/assets", + "$root/assets/$name", + r#"{ join(root, "x", dir, "y", name) }"#, + r#"{ join(root, 1) }"#, + r#"{ join(roo, 1) }"#, + ]; + let inp = [ + dict! { + "x-path-context" => "vscode-paste", + "x-path-input-uri" => "https://huh.io/img.png", + "x-path-input-name" => "img.png", + }, + dict! { + "x-path-context" => "vscode-paste", + "x-path-input-uri" => "https://huh.io/text.md", + "x-path-input-name" => "text.md", + }, + ]; + + let cases = patterns + .iter() + .map(|pat| (*pat, inp[0].clone())) + .chain(inp.iter().map(|inp| { + ( + r#"{ import "/resolve.typ": resolve; resolve(join, root, dir, name) }"#, + inp.clone(), + ) + })); + + let result = cases + .map(|(code, inputs)| { + let request = InteractCodeContextRequest { + path: path.clone(), + query: vec![Some(InteractCodeContextQuery::PathAt { + code: code.to_string(), + inputs: inputs.clone(), + })], + }; + json!({ "code": code, "inputs": inputs, "response": request.request(ctx) }) + }) + .collect::>(); + assert_snapshot!(JsonRepr::new_redacted(result, &REDACT_LOC)); + }); + } +} diff --git a/crates/tinymist-query/src/fixtures/code_context_path_at/at_root.typ b/crates/tinymist-query/src/fixtures/code_context_path_at/at_root.typ new file mode 100644 index 00000000..95c8ab5d --- /dev/null +++ b/crates/tinymist-query/src/fixtures/code_context_path_at/at_root.typ @@ -0,0 +1,17 @@ + +/// path: resolve.typ +#let resolve(join, root, dir, name) = { + let asset-dir = "assets" + if sys.inputs.x-path-input-uri.ends-with(".png") { + return ( + file: join(root, "images", sys.inputs.x-path-input-name), + on-conflict: ```typc + import "/resolve.typ": on-conflict; on-conflict(join, root, dir, name) + ```.text, + ) + } + + join(root, "assets", name) +}; +----- +/// path: x_at_root.typ diff --git a/crates/tinymist-query/src/fixtures/code_context_path_at/in_dir.typ b/crates/tinymist-query/src/fixtures/code_context_path_at/in_dir.typ new file mode 100644 index 00000000..298a395e --- /dev/null +++ b/crates/tinymist-query/src/fixtures/code_context_path_at/in_dir.typ @@ -0,0 +1 @@ +/// path: the_dir/x_in_dir.typ diff --git a/crates/tinymist-query/src/fixtures/code_context_path_at/snaps/test@at_root.typ.snap b/crates/tinymist-query/src/fixtures/code_context_path_at/snaps/test@at_root.typ.snap new file mode 100644 index 00000000..1fbc234c --- /dev/null +++ b/crates/tinymist-query/src/fixtures/code_context_path_at/snaps/test@at_root.typ.snap @@ -0,0 +1,136 @@ +--- +source: crates/tinymist-query/src/code_context.rs +expression: "JsonRepr::new_redacted(result, &REDACT_LOC)" +input_file: crates/tinymist-query/src/fixtures/code_context_path_at/at_root.typ +--- +[ + { + "code": "$root/$dir/$name", + "inputs": { + "x-path-context": "vscode-paste", + "x-path-input-name": "img.png", + "x-path-input-uri": "https://huh.io/img.png" + }, + "response": [ + { + "kind": "pathAt", + "value": "x_at_root" + } + ] + }, + { + "code": "$root/$name", + "inputs": { + "x-path-context": "vscode-paste", + "x-path-input-name": "img.png", + "x-path-input-uri": "https://huh.io/img.png" + }, + "response": [ + { + "kind": "pathAt", + "value": "x_at_root" + } + ] + }, + { + "code": "$root/assets", + "inputs": { + "x-path-context": "vscode-paste", + "x-path-input-name": "img.png", + "x-path-input-uri": "https://huh.io/img.png" + }, + "response": [ + { + "kind": "pathAt", + "value": "assets" + } + ] + }, + { + "code": "$root/assets/$name", + "inputs": { + "x-path-context": "vscode-paste", + "x-path-input-name": "img.png", + "x-path-input-uri": "https://huh.io/img.png" + }, + "response": [ + { + "kind": "pathAt", + "value": "assets/x_at_root" + } + ] + }, + { + "code": "{ join(root, \"x\", dir, \"y\", name) }", + "inputs": { + "x-path-context": "vscode-paste", + "x-path-input-name": "img.png", + "x-path-input-uri": "https://huh.io/img.png" + }, + "response": [ + { + "kind": "pathAt", + "value": "x/y/x_at_root" + } + ] + }, + { + "code": "{ join(root, 1) }", + "inputs": { + "x-path-context": "vscode-paste", + "x-path-input-name": "img.png", + "x-path-input-uri": "https://huh.io/img.png" + }, + "response": [ + { + "error": "crates/tinymist-query/src/code_context.rs:275:21: failed to evaluate path expression: error: join argument is not a string: 1\n ┌─ /__redacted_path__.typ:1:0\n │\n1 │ { join(root, 1) }\n │ ^^^^^^^^^^^^^^^^^\n\n", + "kind": "pathAt" + } + ] + }, + { + "code": "{ join(roo, 1) }", + "inputs": { + "x-path-context": "vscode-paste", + "x-path-input-name": "img.png", + "x-path-input-uri": "https://huh.io/img.png" + }, + "response": [ + { + "error": "crates/tinymist-query/src/code_context.rs:275:21: failed to evaluate path expression: error: unknown variable: roo\n ┌─ /__redacted_path__.typ:1:0\n │\n1 │ { join(roo, 1) }\n │ ^^^^^^^^^^^^^^^^\n\n", + "kind": "pathAt" + } + ] + }, + { + "code": "{ import \"/resolve.typ\": resolve; resolve(join, root, dir, name) }", + "inputs": { + "x-path-context": "vscode-paste", + "x-path-input-name": "img.png", + "x-path-input-uri": "https://huh.io/img.png" + }, + "response": [ + { + "kind": "pathAt", + "value": { + "file": "images/img.png", + "on-conflict": "import \"/resolve.typ\": on-conflict; on-conflict(join, root, dir, name)" + } + } + ] + }, + { + "code": "{ import \"/resolve.typ\": resolve; resolve(join, root, dir, name) }", + "inputs": { + "x-path-context": "vscode-paste", + "x-path-input-name": "text.md", + "x-path-input-uri": "https://huh.io/text.md" + }, + "response": [ + { + "kind": "pathAt", + "value": "assets/x_at_root" + } + ] + } +] diff --git a/crates/tinymist-query/src/fixtures/code_context_path_at/snaps/test@in_dir.typ.snap b/crates/tinymist-query/src/fixtures/code_context_path_at/snaps/test@in_dir.typ.snap new file mode 100644 index 00000000..2a4eeaa1 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/code_context_path_at/snaps/test@in_dir.typ.snap @@ -0,0 +1,133 @@ +--- +source: crates/tinymist-query/src/code_context.rs +expression: "JsonRepr::new_redacted(result, &REDACT_LOC)" +input_file: crates/tinymist-query/src/fixtures/code_context_path_at/in_dir.typ +--- +[ + { + "code": "$root/$dir/$name", + "inputs": { + "x-path-context": "vscode-paste", + "x-path-input-name": "img.png", + "x-path-input-uri": "https://huh.io/img.png" + }, + "response": [ + { + "kind": "pathAt", + "value": "the_dir/x_in_dir" + } + ] + }, + { + "code": "$root/$name", + "inputs": { + "x-path-context": "vscode-paste", + "x-path-input-name": "img.png", + "x-path-input-uri": "https://huh.io/img.png" + }, + "response": [ + { + "kind": "pathAt", + "value": "x_in_dir" + } + ] + }, + { + "code": "$root/assets", + "inputs": { + "x-path-context": "vscode-paste", + "x-path-input-name": "img.png", + "x-path-input-uri": "https://huh.io/img.png" + }, + "response": [ + { + "kind": "pathAt", + "value": "assets" + } + ] + }, + { + "code": "$root/assets/$name", + "inputs": { + "x-path-context": "vscode-paste", + "x-path-input-name": "img.png", + "x-path-input-uri": "https://huh.io/img.png" + }, + "response": [ + { + "kind": "pathAt", + "value": "assets/x_in_dir" + } + ] + }, + { + "code": "{ join(root, \"x\", dir, \"y\", name) }", + "inputs": { + "x-path-context": "vscode-paste", + "x-path-input-name": "img.png", + "x-path-input-uri": "https://huh.io/img.png" + }, + "response": [ + { + "kind": "pathAt", + "value": "x/the_dir/y/x_in_dir" + } + ] + }, + { + "code": "{ join(root, 1) }", + "inputs": { + "x-path-context": "vscode-paste", + "x-path-input-name": "img.png", + "x-path-input-uri": "https://huh.io/img.png" + }, + "response": [ + { + "error": "crates/tinymist-query/src/code_context.rs:275:21: failed to evaluate path expression: error: join argument is not a string: 1\n ┌─ /__redacted_path__.typ:1:0\n │\n1 │ { join(root, 1) }\n │ ^^^^^^^^^^^^^^^^^\n\n", + "kind": "pathAt" + } + ] + }, + { + "code": "{ join(roo, 1) }", + "inputs": { + "x-path-context": "vscode-paste", + "x-path-input-name": "img.png", + "x-path-input-uri": "https://huh.io/img.png" + }, + "response": [ + { + "error": "crates/tinymist-query/src/code_context.rs:275:21: failed to evaluate path expression: error: unknown variable: roo\n ┌─ /__redacted_path__.typ:1:0\n │\n1 │ { join(roo, 1) }\n │ ^^^^^^^^^^^^^^^^\n\n", + "kind": "pathAt" + } + ] + }, + { + "code": "{ import \"/resolve.typ\": resolve; resolve(join, root, dir, name) }", + "inputs": { + "x-path-context": "vscode-paste", + "x-path-input-name": "img.png", + "x-path-input-uri": "https://huh.io/img.png" + }, + "response": [ + { + "error": "crates/tinymist-query/src/code_context.rs:275:21: failed to evaluate path expression: error: file not found (searched at /__redacted_path__.typ)\n ┌─ /__redacted_path__.typ:1:0\n │\n1 │ { import \"/resolve.typ\": resolve; resolve(join, root, dir, name) }\n │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n", + "kind": "pathAt" + } + ] + }, + { + "code": "{ import \"/resolve.typ\": resolve; resolve(join, root, dir, name) }", + "inputs": { + "x-path-context": "vscode-paste", + "x-path-input-name": "text.md", + "x-path-input-uri": "https://huh.io/text.md" + }, + "response": [ + { + "error": "crates/tinymist-query/src/code_context.rs:275:21: failed to evaluate path expression: error: file not found (searched at /__redacted_path__.typ)\n ┌─ /__redacted_path__.typ:1:0\n │\n1 │ { import \"/resolve.typ\": resolve; resolve(join, root, dir, name) }\n │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n", + "kind": "pathAt" + } + ] + } +] diff --git a/crates/tinymist-query/src/references.rs b/crates/tinymist-query/src/references.rs index 50d7e8b7..c26b7146 100644 --- a/crates/tinymist-query/src/references.rs +++ b/crates/tinymist-query/src/references.rs @@ -191,7 +191,7 @@ mod tests { let mut result = result.map(|v| { v.into_iter() .map(|loc| { - let fp = file_path(loc.uri.as_str()); + let fp = file_uri(loc.uri.as_str()); format!( "{fp}@{}:{}:{}:{}", loc.range.start.line, diff --git a/crates/tinymist-query/src/tests.rs b/crates/tinymist-query/src/tests.rs index 3ee10a69..f57fbe92 100644 --- a/crates/tinymist-query/src/tests.rs +++ b/crates/tinymist-query/src/tests.rs @@ -329,6 +329,7 @@ pub static REDACT_LOC: LazyLock = LazyLock::new(|| { RedactFields::from_iter([ "location", "contents", + "file", "uri", "oldUri", "newUri", @@ -419,6 +420,30 @@ impl Redact for RedactFields { for (_, val) in map.iter_mut() { *val = self.redact(val.clone()); } + + if let Some(kind) = map.get("kind") + && matches!(kind.as_str(), Some("pathAt")) + { + if let Some(value) = map.get("value") + && let Value::String(s) = value + { + let v = file_path_(Path::new(s)).into(); + map.insert("value".to_owned(), v); + } + + if let Some(error) = map.get("error") { + let error = error.as_str().unwrap(); + static REG: LazyLock = LazyLock::new(|| { + Regex::new(r#"(/dummy-root/|C:\\dummy-root\\).*?\.typ"#).unwrap() + }); + let error = REG.replace_all(error, "/__redacted_path__.typ").replace( + "crates\\tinymist-query\\src\\code_context.rs", + "crates/tinymist-query/src/code_context.rs", + ); + map.insert("error".to_owned(), Value::String(error)); + } + } + for key in self.0.iter().copied() { let Some(t) = map.remove(key) else { continue; @@ -430,12 +455,18 @@ impl Redact for RedactFields { map.insert( key.to_owned(), Value::Object( - obj.iter().map(|(k, v)| (file_path(k), v.clone())).collect(), + obj.iter().map(|(k, v)| (file_uri(k), v.clone())).collect(), ), ); } + "file" => { + map.insert( + key.to_owned(), + file_path_(Path::new(t.as_str().unwrap())).into(), + ); + } "uri" | "target" | "oldUri" | "newUri" | "targetUri" => { - map.insert(key.to_owned(), file_path(t.as_str().unwrap()).into()); + map.insert(key.to_owned(), file_uri(t.as_str().unwrap()).into()); } "range" | "selectionRange" @@ -465,20 +496,24 @@ impl Redact for RedactFields { } } -pub(crate) fn file_path(uri: &str) -> String { - file_path_(&lsp_types::Url::parse(uri).unwrap()) +pub(crate) fn file_uri(uri: &str) -> String { + file_uri_(&lsp_types::Url::parse(uri).unwrap()) } -pub(crate) fn file_path_(uri: &lsp_types::Url) -> String { +pub(crate) fn file_uri_(uri: &lsp_types::Url) -> String { + let uri = uri.to_file_path().unwrap(); + file_path_(&uri) +} + +pub(crate) fn file_path_(path: &Path) -> String { let root = if cfg!(windows) { PathBuf::from("C:\\dummy-root") } else { PathBuf::from("/dummy-root") }; - let uri = uri.to_file_path().unwrap(); - let abs_path = Path::new(&uri).strip_prefix(root).map(|p| p.to_owned()); - let rel_path = abs_path - .unwrap_or_else(|_| Path::new("-").join(Path::new(&uri).iter().next_back().unwrap())); + let abs_path = path.strip_prefix(root).map(|p| p.to_owned()); + let rel_path = + abs_path.unwrap_or_else(|_| Path::new("-").join(path.iter().next_back().unwrap())); unix_slash(&rel_path) } diff --git a/crates/tinymist-std/src/error.rs b/crates/tinymist-std/src/error.rs index 5dc10869..d4f7254e 100644 --- a/crates/tinymist-std/src/error.rs +++ b/crates/tinymist-std/src/error.rs @@ -395,6 +395,19 @@ impl WithContextUntyped for Result { } } +impl WithContextUntyped for Option { + fn context_ut(self, loc: &'static str) -> Result { + self.ok_or_else(|| Error::new(loc, ErrKind::None, None)) + } + + fn with_context_ut(self, loc: &'static str, f: F) -> Result + where + F: FnOnce() -> Option>, + { + self.ok_or_else(|| Error::new(loc, ErrKind::None, f())) + } +} + /// The error prelude. pub mod prelude { diff --git a/crates/tinymist-task/src/primitives.rs b/crates/tinymist-task/src/primitives.rs index fe19f551..d37d2522 100644 --- a/crates/tinymist-task/src/primitives.rs +++ b/crates/tinymist-task/src/primitives.rs @@ -177,6 +177,7 @@ impl PathPattern { let w = root.to_string_lossy(); let f = file_name.to_string_lossy(); + let f = f.as_ref().strip_suffix(".typ").unwrap_or(f.as_ref()); // replace all $root let mut path = self.0.replace("$root", &w); @@ -184,7 +185,7 @@ impl PathPattern { let d = dir.to_string_lossy(); path = path.replace("$dir", &d); } - path = path.replace("$name", &f); + path = path.replace("$name", f); Some(Path::new(path.as_str()).clean().into()) }