This commit is contained in:
QuadnucYard 2025-12-20 14:40:37 +09:00 committed by GitHub
commit b2406b4d2d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 286 additions and 3 deletions

View file

@ -17,3 +17,9 @@ let sys;
```typc
<module sys>
```
======
[Show Full Value](command:tinymist.showFullValue)

View file

@ -9,3 +9,9 @@ Range: 0:20:0:23
```typc
rgb("#ff4136")
```
======
[Show Full Value](command:tinymist.showFullValue)

View file

@ -17,3 +17,9 @@ let sys;
```typc
<module sys>
```
======
[Show Full Value](command:tinymist.showFullValue)

View file

@ -17,3 +17,9 @@ let sys;
```typc
<module sys>
```
======
[Show Full Value](command:tinymist.showFullValue)

View file

@ -9,3 +9,9 @@ Range: 2:23:2:27
```typc
"A"
```
======
[Show Full Value](command:tinymist.showFullValue)

View file

@ -15,3 +15,10 @@ Range: 2:24:2:31
## The Module (Alias)
======
[Show Full Value](command:tinymist.showFullValue)

View file

@ -15,3 +15,9 @@ This star imports line
```typc
none
```
======
[Show Full Value](command:tinymist.showFullValue)

View file

@ -15,3 +15,10 @@ Range: 2:24:2:30
## The Module
======
[Show Full Value](command:tinymist.showFullValue)

View file

@ -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<Self::Response> {
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::<ast::Expr>() {
ancestor = ancestor.parent()?;
}
let expr = ancestor.cast::<ast::Expr>()?;
if !expr.hash() && !matches!(expr, ast::Expr::MathIdent(_)) {
return None;
}
Some(ancestor)
}
fn format_values(world: &dyn World, expr: &LinkedNode) -> Option<String> {
struct Piece<'a> {
value: &'a Value,
count: usize,
}
let values = analyze_expr(world, expr);
let mut pieces: Vec<Piece<'_>> = 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_::<SIZE_LIMIT>(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)
}

View file

@ -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(())
}

View file

@ -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<Vec<SelectionRange>>),
/// The response to the interact code context request.
InteractCodeContext(Option<Vec<Option<InteractCodeContextResponse>>>),
ShowFullValue(Option<String>),
/// The response to the on enter request.
OnEnter(Option<Vec<TextEdit>>),

View file

@ -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<LspRange>,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ExportValueOpts {
position: Option<LspPosition>,
}
/// 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<JsonValue>) -> 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<T>(
&mut self,
path: PathBuf,

View file

@ -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!(),
};

View file

@ -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)

View file

@ -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%",

View file

@ -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<vscode.Uri>();
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<string> {
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<vscode.Uri> {
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<void> {
const activeEditor = window.activeTextEditor;
if (activeEditor === undefined) {

View file

@ -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<T extends keyof ResourceRoutes>(path: T, ...args: any[]) {
return tinymist.executeCommand<ResourceRoutes[T]>("tinymist.getResources", [path, ...args]);

View file

@ -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: عرض السجل"