diff --git a/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_module.typ.snap b/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_module.typ.snap index 1a2bb7b05..118eff02f 100644 --- a/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_module.typ.snap +++ b/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_module.typ.snap @@ -17,3 +17,9 @@ let sys; ```typc ``` + + +====== + + +[Show Full Value](command:tinymist.showFullValue) diff --git a/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_var.typ.snap b/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_var.typ.snap index fb9de0dc2..53584787d 100644 --- a/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_var.typ.snap +++ b/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_var.typ.snap @@ -9,3 +9,9 @@ Range: 0:20:0:23 ```typc rgb("#ff4136") ``` + + +====== + + +[Show Full Value](command:tinymist.showFullValue) diff --git a/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_var2.typ.snap b/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_var2.typ.snap index 32f9116c8..564cf0ac3 100644 --- a/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_var2.typ.snap +++ b/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_var2.typ.snap @@ -17,3 +17,9 @@ let sys; ```typc ``` + + +====== + + +[Show Full Value](command:tinymist.showFullValue) diff --git a/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_var3.typ.snap b/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_var3.typ.snap index 625b29361..e8ef59539 100644 --- a/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_var3.typ.snap +++ b/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_var3.typ.snap @@ -17,3 +17,9 @@ let sys; ```typc ``` + + +====== + + +[Show Full Value](command:tinymist.showFullValue) diff --git a/crates/tinymist-query/src/fixtures/hover/snaps/test@content_field.typ.snap b/crates/tinymist-query/src/fixtures/hover/snaps/test@content_field.typ.snap index 8cc6aee20..c9b8505fe 100644 --- a/crates/tinymist-query/src/fixtures/hover/snaps/test@content_field.typ.snap +++ b/crates/tinymist-query/src/fixtures/hover/snaps/test@content_field.typ.snap @@ -9,3 +9,9 @@ Range: 2:23:2:27 ```typc "A" ``` + + +====== + + +[Show Full Value](command:tinymist.showFullValue) diff --git a/crates/tinymist-query/src/fixtures/hover/snaps/test@module_alias.typ.snap b/crates/tinymist-query/src/fixtures/hover/snaps/test@module_alias.typ.snap index 6173c344a..dfdecb049 100644 --- a/crates/tinymist-query/src/fixtures/hover/snaps/test@module_alias.typ.snap +++ b/crates/tinymist-query/src/fixtures/hover/snaps/test@module_alias.typ.snap @@ -15,3 +15,10 @@ Range: 2:24:2:31 ## The Module (Alias) + + + +====== + + +[Show Full Value](command:tinymist.showFullValue) diff --git a/crates/tinymist-query/src/fixtures/hover/snaps/test@module_star.typ.snap b/crates/tinymist-query/src/fixtures/hover/snaps/test@module_star.typ.snap index 8a3a90b8a..03500e06d 100644 --- a/crates/tinymist-query/src/fixtures/hover/snaps/test@module_star.typ.snap +++ b/crates/tinymist-query/src/fixtures/hover/snaps/test@module_star.typ.snap @@ -15,3 +15,9 @@ This star imports line ```typc none ``` + + +====== + + +[Show Full Value](command:tinymist.showFullValue) diff --git a/crates/tinymist-query/src/fixtures/hover/snaps/test@module_var.typ.snap b/crates/tinymist-query/src/fixtures/hover/snaps/test@module_var.typ.snap index 767a9bd16..522a5db2c 100644 --- a/crates/tinymist-query/src/fixtures/hover/snaps/test@module_var.typ.snap +++ b/crates/tinymist-query/src/fixtures/hover/snaps/test@module_var.typ.snap @@ -15,3 +15,10 @@ Range: 2:24:2:30 ## The Module + + + +====== + + +[Show Full Value](command:tinymist.showFullValue) diff --git a/crates/tinymist-query/src/full_value.rs b/crates/tinymist-query/src/full_value.rs new file mode 100644 index 000000000..cc163bf10 --- /dev/null +++ b/crates/tinymist-query/src/full_value.rs @@ -0,0 +1,89 @@ +use std::fmt::Write; +use tinymist_analysis::{analyze_expr, upstream::truncated_repr_}; +use typst::{engine::Sink, syntax::LinkedNode}; +use typst_shim::syntax::LinkedNodeExt; + +use crate::prelude::*; + +/// A request to show the full tracked value at a specific position. +#[derive(Debug, Clone)] +pub struct ShowFullValueRequest { + /// The source file. + pub path: PathBuf, + /// The cursor position. + pub position: LspPosition, +} + +impl SemanticRequest for ShowFullValueRequest { + type Response = String; + + fn request(self, ctx: &mut LocalContext) -> Option { + let source = ctx.source_by_path(&self.path).ok()?; + let offset = ctx.to_typst_pos(self.position, &source)?; + // the typst's cursor is 1-based, so we need to add 1 to the offset + let cursor = offset + 1; + + let leaf = LinkedNode::new(source.root()).leaf_at_compat(cursor)?; + let expr = get_inspected_expr(&leaf)?; + let content = format_values(&ctx.world(), expr)?; + + Some(content) + } +} + +fn get_inspected_expr<'a>(leaf: &'a LinkedNode<'a>) -> Option<&'a LinkedNode<'a>> { + let mut ancestor = leaf; + while !ancestor.is::() { + ancestor = ancestor.parent()?; + } + + let expr = ancestor.cast::()?; + if !expr.hash() && !matches!(expr, ast::Expr::MathIdent(_)) { + return None; + } + + Some(ancestor) +} + +fn format_values(world: &dyn World, expr: &LinkedNode) -> Option { + struct Piece<'a> { + value: &'a Value, + count: usize, + } + + let values = analyze_expr(world, expr); + + let mut pieces: Vec> = vec![]; + let mut last = None; + for (value, _) in values.iter() { + if last.replace(value).is_some_and(|last| *last == *value) { + pieces.last_mut().unwrap().count += 1; + } else { + pieces.push(Piece { value, count: 1 }); + } + } + + const SIZE_LIMIT: usize = 512 * 1024 * 1024; // 512MB + + let mut buf = String::new(); + let mut limited = false; + for piece in pieces { + let item_repr = truncated_repr_::(piece.value); + if buf.len() + item_repr.len() + 50 > SIZE_LIMIT { + buf.push_str("... (reached size limit)\n"); + limited = true; + break; + } + buf.push('#'); + buf.push_str(&item_repr); + if piece.count > 1 { + write!(buf, " // (x{})", piece.count).unwrap(); + } + buf.push('\n'); + } + if !limited && values.len() == Sink::MAX_VALUES { + buf.push_str("... (reached max values limit)\n"); + } + + Some(buf) +} diff --git a/crates/tinymist-query/src/hover.rs b/crates/tinymist-query/src/hover.rs index 396f09765..1e1053c8d 100644 --- a/crates/tinymist-query/src/hover.rs +++ b/crates/tinymist-query/src/hover.rs @@ -1,7 +1,9 @@ use core::fmt::{self, Write}; +use serde_json::Value as JsonValue; use tinymist_std::typst::TypstDocument; use typst::foundations::repr::separated_list; +use typst::syntax::{LinkedNode, ast}; use typst_shim::syntax::LinkedNodeExt; use crate::analysis::get_link_exprs_in; @@ -108,10 +110,24 @@ impl HoverWorker<'_> { /// Dynamic analysis results fn dynamic_analysis(&mut self) -> Option<()> { let typst_tooltip = self.ctx.tooltip(&self.source, self.cursor)?; + self.value.push(match typst_tooltip { Tooltip::Text(text) => text.to_string(), - Tooltip::Code(code) => format!("### Sampled Values\n```typc\n{code}\n```"), + Tooltip::Code(code) => { + // Add a button to show full values + // todo - we may need only add this button if the code is too long + self.actions.push(CommandLink { + title: Some("Show Full Value".to_string()), + command_or_links: vec![CommandOrLink::Command { + id: "tinymist.showFullValue".to_string(), + args: vec![], + }], + }); + + format!("### Sampled Values\n```typc\n{code}\n```") + } }); + Some(()) } diff --git a/crates/tinymist-query/src/lib.rs b/crates/tinymist-query/src/lib.rs index f7d23d89c..652de2062 100644 --- a/crates/tinymist-query/src/lib.rs +++ b/crates/tinymist-query/src/lib.rs @@ -24,6 +24,7 @@ pub use document_link::*; pub use document_metrics::*; pub use document_symbol::*; pub use folding_range::*; +pub use full_value::*; pub use goto_declaration::*; pub use goto_definition::*; pub use hover::*; @@ -71,6 +72,7 @@ mod document_link; mod document_metrics; mod document_symbol; mod folding_range; +mod full_value; mod goto_declaration; mod goto_definition; mod hover; @@ -263,6 +265,7 @@ mod polymorphic { SelectionRange(SelectionRangeRequest), /// A request to interact with the code context. InteractCodeContext(InteractCodeContextRequest), + ShowFullValue(ShowFullValueRequest), /// A request to get extra text edits on enter. OnEnter(OnEnterRequest), @@ -306,6 +309,7 @@ mod polymorphic { Self::FoldingRange(..) => ContextFreeUnique, Self::SelectionRange(..) => ContextFreeUnique, Self::InteractCodeContext(..) => PinnedFirst, + Self::ShowFullValue(..) => PinnedFirst, Self::OnEnter(..) => ContextFreeUnique, @@ -343,6 +347,7 @@ mod polymorphic { Self::FoldingRange(req) => &req.path, Self::SelectionRange(req) => &req.path, Self::InteractCodeContext(req) => &req.path, + Self::ShowFullValue(..) => return None, // No specific path needed Self::OnEnter(req) => &req.path, @@ -408,6 +413,7 @@ mod polymorphic { SelectionRange(Option>), /// The response to the interact code context request. InteractCodeContext(Option>>), + ShowFullValue(Option), /// The response to the on enter request. OnEnter(Option>), diff --git a/crates/tinymist/src/cmd.rs b/crates/tinymist/src/cmd.rs index 6e87447f7..6939ac170 100644 --- a/crates/tinymist/src/cmd.rs +++ b/crates/tinymist/src/cmd.rs @@ -12,7 +12,7 @@ use serde_json::Value as JsonValue; use task::TraceParams; use tinymist_assets::TYPST_PREVIEW_HTML; use tinymist_query::package::PackageInfo; -use tinymist_query::{LocalContextGuard, LspRange}; +use tinymist_query::{LocalContextGuard, LspPosition, LspRange}; use tinymist_std::error::prelude::*; use typst::syntax::{LinkedNode, Source}; @@ -34,6 +34,12 @@ struct ExportSyntaxRangeOpts { range: Option, } +#[derive(Debug, Clone, Default, Deserialize)] +#[serde(rename_all = "camelCase")] +struct ExportValueOpts { + position: Option, +} + /// Here are implemented the handlers for each command. impl ServerState { /// Export a range of the current document as Ansi highlighted text. @@ -72,6 +78,17 @@ impl ServerState { just_ok(JsonValue::String(output)) } + /// Export the full tracked value at a specific position. + pub fn export_value(&mut self, mut args: Vec) -> AnySchedulableResponse { + let path = get_arg!(args[0] as PathBuf); + let opts = get_arg_or_default!(args[1] as ExportValueOpts); + let position = opts + .position + .ok_or_else(|| internal_error("no position provided"))?; + + run_query!(self.ShowFullValue(path, position)) + } + fn select_range( &mut self, path: PathBuf, diff --git a/crates/tinymist/src/lsp/query.rs b/crates/tinymist/src/lsp/query.rs index 8fe35c4a0..cbbb6ff00 100644 --- a/crates/tinymist/src/lsp/query.rs +++ b/crates/tinymist/src/lsp/query.rs @@ -304,6 +304,7 @@ impl ServerState { Symbol(req) => snap.run_semantic(req, R::Symbol), WorkspaceLabel(req) => snap.run_semantic(req, R::WorkspaceLabel), DocumentMetrics(req) => snap.run_semantic(req, R::DocumentMetrics), + ShowFullValue(req) => snap.run_semantic(req, R::ShowFullValue), _ => unreachable!(), }; diff --git a/crates/tinymist/src/server.rs b/crates/tinymist/src/server.rs index 314bbb17d..75852dadf 100644 --- a/crates/tinymist/src/server.rs +++ b/crates/tinymist/src/server.rs @@ -335,6 +335,7 @@ impl ServerState { .with_command_("tinymist.exportQuery", State::export_query) .with_command("tinymist.exportAnsiHighlight", State::export_ansi_hl) .with_command("tinymist.exportAst", State::export_ast) + .with_command("tinymist.exportValue", State::export_value) .with_command("tinymist.doClearCache", State::clear_cache) .with_command("tinymist.pinMain", State::pin_document) .with_command("tinymist.focusMain", State::focus_document) diff --git a/editors/vscode/package.json b/editors/vscode/package.json index 2963125ae..dedec9fea 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -1217,6 +1217,11 @@ "title": "%extension.tinymist.command.tinymist.viewAst%", "category": "Typst" }, + { + "command": "tinymist.showFullValue", + "title": "%extension.tinymist.command.tinymist.showFullValue%", + "category": "Typst" + }, { "command": "tinymist.showLog", "title": "%extension.tinymist.command.tinymist.showLog%", diff --git a/editors/vscode/src/extension.ts b/editors/vscode/src/extension.ts index 50d09e4cf..2c683467f 100644 --- a/editors/vscode/src/extension.ts +++ b/editors/vscode/src/extension.ts @@ -215,6 +215,7 @@ async function languageActivate(context: IContext) { commands.registerCommand("tinymist.runCodeLens", commandRunCodeLens), commands.registerCommand("tinymist.copyAnsiHighlight", commandCopyAnsiHighlight), commands.registerCommand("tinymist.viewAst", commandViewAst(context)), + commands.registerCommand("tinymist.showFullValue", commandShowFullValue(context)), commands.registerCommand("tinymist.pinMainToCurrent", () => commandPinMain(true)), commands.registerCommand("tinymist.unpinMain", () => commandPinMain(false)), @@ -371,6 +372,98 @@ function commandViewAst(ctx: IContext) { }; } +function commandShowFullValue(ctx: IContext) { + const scheme = "tinymist-full-value"; + const uri = `${scheme}://showFullValue/values.typ`; + + const FullValueDoc = new (class implements vscode.TextDocumentContentProvider { + readonly uri = vscode.Uri.parse(uri); + readonly eventEmitter = new vscode.EventEmitter(); + + constructor() { + vscode.workspace.onDidChangeTextDocument( + this.onDidChangeTextDocument, + this, + ctx.subscriptions, + ); + vscode.window.onDidChangeActiveTextEditor( + this.onDidChangeActiveTextEditor, + this, + ctx.subscriptions, + ); + vscode.window.onDidChangeTextEditorSelection( + this.onDidChangeTextSelection, + this, + ctx.subscriptions, + ); + } + + emitChange() { + this.eventEmitter.fire(this.uri); + } + + private onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) { + if (isTypstDocument(event.document)) { + // commandActivateDoc(event.document); + // We need to order this after language server updates, but there's no API for that. + // Hence, good old sleep(). + setTimeout(() => this.emitChange(), 10); + } + } + + private onDidChangeActiveTextEditor(editor: vscode.TextEditor | undefined) { + if (editor && isTypstDocument(editor.document)) { + // commandActivateDoc(editor.document); + this.emitChange(); + } + } + + private onDidChangeTextSelection(event: vscode.TextEditorSelectionChangeEvent) { + if (isTypstDocument(event.textEditor.document)) { + // commandActivateDoc(event.textEditor.document); + this.emitChange(); + } + } + + async provideTextDocumentContent( + _uri: vscode.Uri, + _ct: vscode.CancellationToken, + ): Promise { + const editor = ctx.currentActiveEditor(); + if (!editor) return "No active editor, change selection to view full value."; + + try { + const res = await tinymist.exportValue(editor.document.uri.fsPath, { + position: (await tinymist.clientPromise).code2ProtocolConverter.asPosition( + editor.selection.active, + ), + }); + + return res || "No value found at this position."; + } catch (error) { + return `Error: ${error}`; + } + } + + get onDidChange(): vscode.Event { + return this.eventEmitter.event; + } + })(); + + ctx.subscriptions.push( + vscode.workspace.registerTextDocumentContentProvider(scheme, FullValueDoc), + ); + + return async () => { + const document = await vscode.workspace.openTextDocument(FullValueDoc.uri); + setTimeout(() => FullValueDoc.emitChange(), 10); + void (await vscode.window.showTextDocument(document, { + viewColumn: vscode.ViewColumn.Two, + preserveFocus: true, + })); + }; +} + async function commandClearCache(): Promise { const activeEditor = window.activeTextEditor; if (activeEditor === undefined) { diff --git a/editors/vscode/src/lsp.ts b/editors/vscode/src/lsp.ts index 487d81f5f..0b3be3acb 100644 --- a/editors/vscode/src/lsp.ts +++ b/editors/vscode/src/lsp.ts @@ -201,7 +201,7 @@ export class LanguageState { }; const trustedCommands = { - enabledCommands: ["tinymist.openInternal", "tinymist.openExternal"], + enabledCommands: ["tinymist.openInternal", "tinymist.openExternal", "tinymist.showFullValue"], }; const hoverStorage = extensionState.features.renderDocs && LanguageState.HoverTmpStorage @@ -349,6 +349,7 @@ export class LanguageState { exportQuery = exportCommand("tinymist.exportQuery"); exportAnsiHighlight = exportStringCommand("tinymist.exportAnsiHighlight"); exportAst = exportStringCommand("tinymist.exportAst"); + exportValue = exportStringCommand("tinymist.exportValue"); getResource(path: T, ...args: any[]) { return tinymist.executeCommand("tinymist.getResources", [path, ...args]); diff --git a/locales/tinymist-vscode.toml b/locales/tinymist-vscode.toml index f234dfbe1..ddc5f142b 100644 --- a/locales/tinymist-vscode.toml +++ b/locales/tinymist-vscode.toml @@ -241,6 +241,10 @@ zh-TW = "複製為 ANSI 代碼" en = "View the AST of the current file" zh = "查看当前文件的 AST" +[extension.tinymist.command.tinymist.showFullValue] +en = "View the full value of the expression under cursor" +zh = "查看光标下表达式的完整值" + [extension.tinymist.command.tinymist.showLog] en = "Tinymist: Show Log" ar = "Tinymist: عرض السجل"