From c2df77dd5e94e8eff0321e90e4d10c66aee51b74 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Tue, 25 Nov 2025 07:13:07 +0800 Subject: [PATCH] feat: customize paste snippet in vscode --- crates/tinymist-query/src/code_context.rs | 159 ++++++++++++++++-- .../snaps/test@at_root.typ.snap | 136 --------------- .../snaps/test@in_dir.typ.snap | 133 --------------- editors/vscode/src/features/drop-paste.ts | 23 ++- editors/vscode/src/test/runTests.ts | 4 +- 5 files changed, 164 insertions(+), 291 deletions(-) delete mode 100644 crates/tinymist-query/src/fixtures/code_context_path_at/snaps/test@at_root.typ.snap delete mode 100644 crates/tinymist-query/src/fixtures/code_context_path_at/snaps/test@in_dir.typ.snap diff --git a/crates/tinymist-query/src/code_context.rs b/crates/tinymist-query/src/code_context.rs index 6d3dd643d..34049bc3a 100644 --- a/crates/tinymist-query/src/code_context.rs +++ b/crates/tinymist-query/src/code_context.rs @@ -112,7 +112,20 @@ 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_expr(ctx, &code, base) + .and_then(|res| { + QueryResult::success(res.map(|mut res| { + res.edits = resolve_edits(ctx, res.edits); + res + })) + }) + .and_then(|res| match serde_json::to_value(res) { + Ok(value) => QueryResult::success(value), + Err(e) => QueryResult::error(eco_format!( + "failed to serialize script result: {e}" + )), + }); + // serde_json::Value Some(InteractCodeContextResponse::PathAt(res)) } InteractCodeContextQuery::ModeAt { position } => { @@ -217,16 +230,29 @@ impl InteractCodeContextRequest { } } +#[derive(Debug, Clone, Serialize, Deserialize)] +struct PathAtOutput { + dir: PathBuf, + edits: Option, +} + +impl PathAtOutput { + fn dir(dir: PathBuf) -> Self { + Self { dir, edits: None } + } +} + fn eval_path_expr( ctx: &mut LocalContext, code: &str, inputs: Dict, -) -> Option> { +) -> QueryResult> { 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 id = match entry.select_in_workspace(Path::new("/__path__.typ")).main() { + Some(id) => id, + None => return QueryResult::error("main file not found".into()), + }; let inputs = make_sys(&entry, ctx.world().inputs(), inputs); let (inputs, root, dir, name) = match inputs { @@ -262,17 +288,28 @@ fn eval_path_expr( let expr = match expr.cast::() { Some(v) => v, None => bail!( - "code is not a valid code expression: kind={:?}", + "script 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"), + Ok(value) => match value { + Value::None => Ok(None), + Value::Str(s) => Ok(Some(PathAtOutput::dir(PathBuf::from(s.as_str())))), + value @ Value::Dict(..) => { + let s = serde_json::to_value(value) + .context_ut("failed to serialize script result")?; + serde_json::from_value::(s) + .context_ut("failed to deserialize script result") + .map(Some) + } + _ => bail!("script result is not a string or dictionary: {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 path expression: {err}") + bail!("failed to evaluate script: {err}") } } }) @@ -280,11 +317,17 @@ fn eval_path_expr( 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") - }) + .map(|dir| Some(PathAtOutput::dir(dir.as_ref().to_owned()))) }; - Some(path.into()) + path.into() +} + +// todo: implement this +fn resolve_edits( + _ctx: &mut LocalContext, + edits: Option, +) -> Option { + edits } #[derive(Debug, Clone, Hash)] @@ -392,6 +435,17 @@ impl QueryResult { pub fn error(error: EcoString) -> Self { Self::Error { error } } + + /// Applies a function to the value of a successful result. + pub fn and_then(self, f: F) -> QueryResult + where + F: FnOnce(T) -> QueryResult, + { + match self { + QueryResult::Success { value } => f(value), + QueryResult::Error { error } => QueryResult::error(error), + } + } } impl From> for QueryResult { @@ -411,7 +465,7 @@ mod tests { use crate::tests::*; #[test] - fn test() { + fn path() { snapshot_testing("code_context_path_at", &|ctx, path| { let patterns = [ "$root/$dir/$name", @@ -460,4 +514,83 @@ mod tests { assert_snapshot!(JsonRepr::new_redacted(result, &REDACT_LOC)); }); } + + #[test] + fn snippet() { + // type ModeEdit = string | { + // kind: "by-mode"; + // math?: string; + // markup?: string; + // code?: string; + // comment?: string; + // string?: string; + // raw?: string; + // rest?: string; + // }; + + // export async function createModeEdit(edit: ModeEdit) { + // const { + // kind, + // math, + // comment, + // markup, + // code, + // string: stringContent, + // raw, + // rest, + // }: Record = edit.newText; + // const newText = kind === "by-mode" ? rest || "" : ""; + + // const res = await vscode.commands.executeCommand< + // [{ mode: "math" | "markup" | "code" | "comment" | "string" | + // "raw" }] >("tinymist.interactCodeContext", { + // textDocument: { + // uri: activeDocument.uri.toString(), + // }, + // query: [ + // { + // kind: "modeAt", + // position: { + // line: selectionStart.line, + // character: selectionStart.character, + // }, + // }, + // ], + // }); + + // const mode = res[0].mode; + + // await editor.edit((editBuilder) => { + // if (mode === "math") { + // // todo: whether to keep stupid + // // if it is before an identifier character, then add a space + // let replaceText = math || newText; + // const range = new vscode.Range( + // selectionStart.with(undefined, selectionStart.character - 1), + // selectionStart, + // ); + // const before = selectionStart.character > 0 ? + // activeDocument.getText(range) : ""; if (before.match(/ + // [\p{XID_Start}\p{XID_Continue}_]/u)) { replaceText = + // ` ${math}`; } + + // editBuilder.replace(selection, replaceText); + // } else if (mode === "markup") { + // editBuilder.replace(selection, markup || newText); + // } else if (mode === "comment") { + // editBuilder.replace(selection, comment || markup || newText); + // } else if (mode === "string") { + // editBuilder.replace(selection, stringContent || raw || + // newText); } else if (mode === "raw") { + // editBuilder.replace(selection, raw || stringContent || + // newText); } else if (mode === "code") { + // editBuilder.replace(selection, code || newText); + // } else { + // editBuilder.replace(selection, newText); + // } + // }); + // } + + todo!() + } } 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 deleted file mode 100644 index 1fbc234c4..000000000 --- a/crates/tinymist-query/src/fixtures/code_context_path_at/snaps/test@at_root.typ.snap +++ /dev/null @@ -1,136 +0,0 @@ ---- -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 deleted file mode 100644 index 2a4eeaa11..000000000 --- a/crates/tinymist-query/src/fixtures/code_context_path_at/snaps/test@in_dir.typ.snap +++ /dev/null @@ -1,133 +0,0 @@ ---- -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/editors/vscode/src/features/drop-paste.ts b/editors/vscode/src/features/drop-paste.ts index ff1ef25d8..1962e4b91 100644 --- a/editors/vscode/src/features/drop-paste.ts +++ b/editors/vscode/src/features/drop-paste.ts @@ -681,14 +681,23 @@ export async function getDesiredNewFilePath( if ("error" in pathAt) { throw new Error(pathAt.error); } - const path = pathAt.value; - if (typeof path !== "string") { - throw new Error( - `expect paste script to return an object { value: string }, got { value: ${typeof path} }`, - ); + let dir = pathAt.value; + if (typeof dir !== "string") { + if (typeof dir !== 'object') { + throw new Error( + `expect paste script to return an object { value: string | object }, got { value: ${JSON.stringify(dir)} }`, + ); + } else { + if (!("dir" in dir)) { + throw new Error( + `expect paste script to return an object { value: { dir: string } }, got { value: ${JSON.stringify(dir)} }`, + ); + } + dir = dir.dir; + } } - const newFileDir = vscode.Uri.file(path); + const newFileDir = vscode.Uri.file(dir); const workspaceFolder = vscode.workspace.getWorkspaceFolder(newFileDir); if (!workspaceFolder) { throw new Error( @@ -768,7 +777,7 @@ export class UriList { constructor( public readonly entries: ReadonlyArray<{ readonly uri: vscode.Uri; readonly str: string }>, - ) {} + ) { } } const externalUriSchemes: ReadonlySet = new Set([ diff --git a/editors/vscode/src/test/runTests.ts b/editors/vscode/src/test/runTests.ts index 5ac194a35..5d2eb3d82 100644 --- a/editors/vscode/src/test/runTests.ts +++ b/editors/vscode/src/test/runTests.ts @@ -40,8 +40,8 @@ async function main() { // Run tests using the minimal supported version and the latest one. for (const version of [minimalVersion, "stable"]) { for (const uri of [ - // path.resolve(extensionDevelopmentPath, "e2e-workspaces/export"), - path.resolve(extensionDevelopmentPath, "e2e-workspaces/book"), + path.resolve(extensionDevelopmentPath, "e2e-workspaces/export"), + // path.resolve(extensionDevelopmentPath, "e2e-workspaces/book"), // undefined, ]) { await runTests({