diff --git a/contrib/typlite/src/main.rs b/contrib/typlite/src/main.rs index 86c7cba6..cf233035 100644 --- a/contrib/typlite/src/main.rs +++ b/contrib/typlite/src/main.rs @@ -1,10 +1,5 @@ #![doc = include_str!("../README.md")] -extern crate clap; -extern crate ecow; -extern crate tinymist_world; -extern crate typlite; - use std::{ path::{Path, PathBuf}, sync::Arc, @@ -60,7 +55,7 @@ fn main() -> typlite::Result<()> { } fn lib() -> Arc> { - let mut scopes = typlite::library::library(); + let mut scopes = typlite::library::docstring_lib(); // todo: how to import this function correctly? scopes.define("cross-link", cross_link as RawFunc); diff --git a/crates/tinymist-query/src/analysis/global.rs b/crates/tinymist-query/src/analysis/global.rs index 9fe2e966..ac37c570 100644 --- a/crates/tinymist-query/src/analysis/global.rs +++ b/crates/tinymist-query/src/analysis/global.rs @@ -36,7 +36,7 @@ use crate::syntax::{ }; use crate::upstream::{tooltip_, Tooltip}; use crate::{ - lsp_to_typst, path_to_url, typst_to_lsp, LspPosition, LspRange, PositionEncoding, + lsp_to_typst, path_to_url, typst_to_lsp, ColorTheme, LspPosition, LspRange, PositionEncoding, SemanticTokenContext, TypstRange, VersionedDocument, }; @@ -45,6 +45,8 @@ use crate::{ pub struct Analysis { /// The position encoding for the workspace. pub position_encoding: PositionEncoding, + /// The editor's color theme. + pub color_theme: ColorTheme, /// The periscope provider. pub periscope: Option>, /// The semantic token context. diff --git a/crates/tinymist-query/src/docs/convert.rs b/crates/tinymist-query/src/docs/convert.rs index 15c951a0..cd00b2e8 100644 --- a/crates/tinymist-query/src/docs/convert.rs +++ b/crates/tinymist-query/src/docs/convert.rs @@ -3,28 +3,30 @@ use std::sync::{Arc, LazyLock}; use ecow::{eco_format, EcoString}; use parking_lot::Mutex; use tinymist_world::base::{EntryState, ShadowApi, TaskInputs}; -use tinymist_world::LspWorld; use typlite::scopes::Scopes; -use typlite::value::{Value, *}; +use typlite::value::Value; use typst::foundations::Bytes; use typst::{ diag::StrResult, syntax::{FileId, VirtualPath}, }; +use crate::analysis::SharedContext; + // Unfortunately, we have only 65536 possible file ids and we cannot revoke // them. So we share a global file id for all docs conversion. static DOCS_CONVERT_ID: LazyLock> = LazyLock::new(|| Mutex::new(FileId::new(None, VirtualPath::new("__tinymist_docs__.typ")))); -pub(crate) fn convert_docs(world: &LspWorld, content: &str) -> StrResult { - static DOCS_LIB: LazyLock>> = LazyLock::new(lib); +pub(crate) fn convert_docs(ctx: &SharedContext, content: &str) -> StrResult { + static DOCS_LIB: LazyLock>> = + LazyLock::new(|| Arc::new(typlite::library::docstring_lib())); let conv_id = DOCS_CONVERT_ID.lock(); let entry = EntryState::new_rootless(conv_id.vpath().as_rooted_path().into()).unwrap(); let entry = entry.select_in_workspace(*conv_id); - let mut w = world.task(TaskInputs { + let mut w = ctx.world.task(TaskInputs { entry: Some(entry), inputs: None, }); @@ -34,29 +36,10 @@ pub(crate) fn convert_docs(world: &LspWorld, content: &str) -> StrResult Arc> { - let mut scopes = typlite::library::library(); - - // todo: how to import this function correctly? - scopes.define("example", example as RawFunc); - - Arc::new(scopes) -} - -/// Evaluate a `example`. -pub fn example(mut args: Args) -> typlite::Result { - let body = get_pos_named!(args, body: Content).0; - let body = body.trim(); - let ticks = body.chars().take_while(|t| *t == '`').collect::(); - let body = &body[ticks.len()..]; - let body = eco_format!("{ticks}typ{body}"); - - Ok(Value::Content(body)) -} diff --git a/crates/tinymist-query/src/lib.rs b/crates/tinymist-query/src/lib.rs index d82f8a15..de3d7743 100644 --- a/crates/tinymist-query/src/lib.rs +++ b/crates/tinymist-query/src/lib.rs @@ -91,6 +91,8 @@ use typst::{model::Document as TypstDocument, syntax::Source}; /// The physical position in a document. pub type FramePosition = typst::layout::Position; +pub use typlite::ColorTheme; + /// A compiled document with an self-incremented logical version. #[derive(Debug, Clone)] pub struct VersionedDocument { diff --git a/crates/tinymist-query/src/syntax/docs.rs b/crates/tinymist-query/src/syntax/docs.rs index b433b120..94a810f6 100644 --- a/crates/tinymist-query/src/syntax/docs.rs +++ b/crates/tinymist-query/src/syntax/docs.rs @@ -107,7 +107,7 @@ struct DocsChecker<'a> { impl<'a> DocsChecker<'a> { pub fn check_func_docs(mut self, docs: String) -> Option { - let converted = convert_docs(&self.ctx.world, &docs).ok()?; + let converted = convert_docs(self.ctx, &docs).ok()?; let converted = identify_func_docs(&converted).ok()?; let module = self.ctx.module_by_str(docs)?; @@ -135,7 +135,7 @@ impl<'a> DocsChecker<'a> { } pub fn check_var_docs(mut self, docs: String) -> Option { - let converted = convert_docs(&self.ctx.world, &docs).ok()?; + let converted = convert_docs(self.ctx, &docs).ok()?; let converted = identify_var_docs(converted).ok()?; let module = self.ctx.module_by_str(docs)?; @@ -152,7 +152,7 @@ impl<'a> DocsChecker<'a> { } pub fn check_module_docs(self, docs: String) -> Option { - let converted = convert_docs(&self.ctx.world, &docs).ok()?; + let converted = convert_docs(self.ctx, &docs).ok()?; let converted = identify_tidy_module_docs(converted).ok()?; Some(DocString { diff --git a/crates/tinymist/src/actor/mod.rs b/crates/tinymist/src/actor/mod.rs index c15122d0..64cd0201 100644 --- a/crates/tinymist/src/actor/mod.rs +++ b/crates/tinymist/src/actor/mod.rs @@ -96,7 +96,6 @@ impl LanguageState { // Create the compile handler for client consuming results. let const_config = self.const_config(); - let position_encoding = const_config.position_encoding; let periscope_args = self.compile_config().periscope_args.clone(); let handle = Arc::new(CompileHandler { #[cfg(feature = "preview")] @@ -107,7 +106,11 @@ impl LanguageState { editor_tx: self.editor_tx.clone(), stats: Default::default(), analysis: Arc::new(Analysis { - position_encoding, + position_encoding: const_config.position_encoding, + color_theme: match self.compile_config().color_theme.as_deref() { + Some("dark") => tinymist_query::ColorTheme::Dark, + _ => tinymist_query::ColorTheme::Light, + }, periscope: periscope_args.map(|args| { let r = TypstPeriscopeProvider(PeriscopeRenderer::new(args)); Arc::new(r) as Arc diff --git a/crates/tinymist/src/init.rs b/crates/tinymist/src/init.rs index 1904c06c..c82bf45d 100644 --- a/crates/tinymist/src/init.rs +++ b/crates/tinymist/src/init.rs @@ -281,7 +281,7 @@ const CONFIG_ITEMS: &[&str] = &[ "systemFonts", "typstExtraArgs", "compileStatus", - "preferredTheme", + "colorTheme", "hoverPeriscope", ]; // endregion Configuration Items @@ -474,8 +474,8 @@ pub struct CompileConfig { pub periscope_args: Option, /// Typst extra arguments. pub typst_extra_args: Option, - /// The preferred theme for the document. - pub preferred_theme: Option, + /// The preferred color theme for the document. + pub color_theme: Option, /// Whether the configuration can have a default entry path. pub has_default_entry_path: bool, /// The inputs for the language server protocol. @@ -508,8 +508,8 @@ impl CompileConfig { Some("disable") | None => false, _ => bail!("compileStatus must be either 'enable' or 'disable'"), }; - self.preferred_theme = try_(|| Some(update.get("preferredTheme")?.as_str()?.to_owned())); - log::info!("preferred theme: {:?} {:?}", self.preferred_theme, update); + self.color_theme = try_(|| Some(update.get("colorTheme")?.as_str()?.to_owned())); + log::info!("color theme: {:?}", self.color_theme); // periscope_args self.periscope_args = match update.get("hoverPeriscope") { @@ -521,7 +521,7 @@ impl CompileConfig { }, }; if let Some(args) = self.periscope_args.as_mut() { - if args.invert_color == "auto" && self.preferred_theme.as_deref() == Some("dark") { + if args.invert_color == "auto" && self.color_theme.as_deref() == Some("dark") { "always".clone_into(&mut args.invert_color); } } @@ -583,7 +583,7 @@ impl CompileConfig { "x-preview".into(), serde_json::to_string(&PreviewInputs { version: 1, - theme: self.preferred_theme.clone().unwrap_or_default(), + theme: self.color_theme.clone().unwrap_or_default(), }) .unwrap() .into_value(), diff --git a/crates/typlite/src/lib.rs b/crates/typlite/src/lib.rs index 123bd7a1..de545952 100644 --- a/crates/typlite/src/lib.rs +++ b/crates/typlite/src/lib.rs @@ -5,15 +5,22 @@ pub mod library; pub mod scopes; pub mod value; -use std::fmt::Write; +use core::fmt; use std::sync::Arc; +use std::{fmt::Write, sync::LazyLock}; pub use error::*; use base64::Engine; use scopes::Scopes; use tinymist_world::{base::ShadowApi, EntryReader, LspWorld}; -use typst::{foundations::Bytes, layout::Abs, World}; +use typst::foundations::IntoValue; +use typst::{ + foundations::{Bytes, Dict}, + layout::Abs, + utils::LazyHash, + World, +}; use value::{Args, Value}; use ecow::{eco_format, EcoString}; @@ -22,11 +29,21 @@ use typst_syntax::{ FileId, Source, SyntaxKind, SyntaxNode, VirtualPath, }; +pub use typst_syntax as syntax; + /// The result type for typlite. pub type Result = std::result::Result; pub use tinymist_world::CompileOnceArgs; +/// A color theme for rendering the content. The valid values can be checked in [color-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/color-scheme). +#[derive(Debug, Default, Clone, Copy)] +pub enum ColorTheme { + #[default] + Light, + Dark, +} + /// Task builder for converting a typst document to Markdown. pub struct Typlite { /// The universe to use for the conversion. @@ -37,6 +54,8 @@ pub struct Typlite { do_annotate: bool, /// Whether to enable GFM (GitHub Flavored Markdown) features. gfm: bool, + /// The preferred color theme + theme: Option, } impl Typlite { @@ -50,6 +69,7 @@ impl Typlite { library: None, do_annotate: false, gfm: false, + theme: None, } } @@ -59,6 +79,12 @@ impl Typlite { self } + /// Set the preferred color theme. + pub fn with_color_theme(mut self, theme: ColorTheme) -> Self { + self.theme = Some(theme); + self + } + /// Annotate the elements for identification. pub fn annotate_elements(mut self, do_annotate: bool) -> Self { self.do_annotate = do_annotate; @@ -82,6 +108,7 @@ impl Typlite { current, gfm: self.gfm, do_annotate: self.do_annotate, + theme: self.theme, list_depth: 0, scopes: self .library @@ -99,6 +126,7 @@ impl Typlite { #[derive(Clone)] pub struct TypliteWorker { current: FileId, + theme: Option, gfm: bool, do_annotate: bool, scopes: Arc>, @@ -290,37 +318,84 @@ impl TypliteWorker { Ok(Value::Content(s)) } - fn render(&mut self, node: &SyntaxNode, inline: bool) -> Result { - let dark = self.render_inner(node, true)?; - let light = self.render_inner(node, false)?; - if inline { - Ok(Value::Content(eco_format!( - r#"typst-block"# - ))) - } else { - Ok(Value::Content(eco_format!( - r#"

typst-block

"# - ))) - } + pub fn render(&mut self, node: &SyntaxNode, inline: bool) -> Result { + let code = node.clone().into_text(); + self.render_code(&code, false, "center", "", inline) } - fn render_inner(&mut self, node: &SyntaxNode, is_dark: bool) -> Result { - let color = if is_dark { - r##"#set text(rgb("#c0caf5"))"## + pub fn render_code( + &mut self, + code: &str, + is_markup: bool, + align: &str, + extra_attrs: &str, + inline: bool, + ) -> Result { + let theme = self.theme; + let mut render = |theme| self.render_inner(code, is_markup, theme); + + let mut content = EcoString::new(); + if !inline { + let _ = write!(content, r#"

"#); + } + + let inline_attrs = if inline { + r#" style="vertical-align: -0.35em""# } else { "" }; - let main = Bytes::from(eco_format!( - r##"#set page(width: auto, height: auto, margin: (y: 0.45em, rest: 0em), fill: rgb("#ffffff00"));{color} -{}"##, - node.clone().into_text() - ).as_bytes().to_owned()); + match theme { + Some(theme) => { + let data = render(theme)?; + let _ = write!( + content, + r#""#, + ); + } + None => { + let _ = write!( + content, + r#""#, + dark = render(ColorTheme::Dark)?, + light = render(ColorTheme::Light)? + ); + } + } + + if !inline { + content.push_str("

"); + } + + Ok(Value::Content(content)) + } + + fn render_inner(&mut self, code: &str, is_markup: bool, theme: ColorTheme) -> Result { + static DARK_THEME_INPUT: LazyLock>> = LazyLock::new(|| { + Arc::new(LazyHash::new(Dict::from_iter(std::iter::once(( + "x-color-theme".into(), + "dark".into_value(), + ))))) + }); + + let code = WrapCode(code, is_markup); + // let inputs = is_dark.then(|| DARK_THEME_INPUT.clone()); + let inputs = match theme { + ColorTheme::Dark => Some(DARK_THEME_INPUT.clone()), + ColorTheme::Light => None, + }; + let code = eco_format!( + r##"#set page(width: auto, height: auto, margin: (y: 0.45em, rest: 0em), fill: none); + #set text(fill: rgb("#c0caf5")) if sys.inputs.at("x-color-theme", default: none) == "dark"; + {code}"## + ); + let main = Bytes::from(code.as_bytes().to_owned()); + // let world = LiteWorld::new(main); let main_id = FileId::new(None, VirtualPath::new("__render__.typ")); let entry = self.world.entry_state().select_in_workspace(main_id); let mut world = self.world.task(tinymist_world::base::TaskInputs { entry: Some(entry), - inputs: None, + inputs, }); world.source_db.take_state(); world.map_shadow_by_id(main_id, main).unwrap(); @@ -341,7 +416,7 @@ impl TypliteWorker { Ok(Value::Content(node.clone().into_text())) } - fn value(res: Value) -> EcoString { + pub fn value(res: Value) -> EcoString { match res { Value::None => EcoString::new(), Value::Content(content) => content, @@ -529,5 +604,24 @@ impl TypliteWorker { } } +struct WrapCode<'a>(&'a str, bool); + +impl<'a> fmt::Display for WrapCode<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let is_markup = self.1; + if is_markup { + f.write_str("#[")?; + } else { + f.write_str("#{")?; + } + f.write_str(self.0)?; + if is_markup { + f.write_str("]") + } else { + f.write_str("}") + } + } +} + #[cfg(test)] mod tests; diff --git a/crates/typlite/src/library.rs b/crates/typlite/src/library.rs index ed745a39..12096139 100644 --- a/crates/typlite/src/library.rs +++ b/crates/typlite/src/library.rs @@ -4,6 +4,9 @@ use super::*; use ecow::eco_format; use value::*; +mod docstring; +pub use docstring::docstring_lib; + pub fn library() -> Scopes { let mut scopes = Scopes::new(); scopes.define("link", link as RawFunc); diff --git a/crates/typlite/src/library/docstring.rs b/crates/typlite/src/library/docstring.rs new file mode 100644 index 00000000..390818cb --- /dev/null +++ b/crates/typlite/src/library/docstring.rs @@ -0,0 +1,58 @@ +use super::*; + +pub fn docstring_lib() -> Scopes { + let mut scopes = library(); + + scopes.define("example", example as RawFunc); + + scopes +} + +/// Evaluate a `example`. +pub fn example(mut args: Args) -> Result { + let body = get_pos_named!(args, body: &SyntaxNode); + let body = body + .cast::() + .ok_or_else(|| format!("expected raw, found {:?}", body.kind()))?; + + let lang = body.lang().map(|l| l.get().as_str()).unwrap_or("typ"); + + // Handle example docs specially. + // + let mut display = String::new(); + let mut compile = String::new(); + for line in body.lines() { + let line = line.get(); + if let Some(suffix) = line.strip_prefix(">>>") { + compile.push_str(suffix); + compile.push('\n'); + } else if let Some(suffix) = line.strip_prefix("<<< ") { + display.push_str(suffix); + display.push('\n'); + } else { + display.push_str(line); + display.push('\n'); + compile.push_str(line); + compile.push('\n'); + } + } + + let mut s = EcoString::new(); + + s.push_str("```"); + s.push_str(lang); + s.push('\n'); + s.push_str(&display); + s.push('\n'); + s.push_str("```"); + s.push('\n'); + + // todo: render examples only if supports HTML + let is_code = lang == "typc"; + let rendered = args + .vm + .render_code(&compile, !is_code, "left", r#"width="500px""#, false)?; + s.push_str(&TypliteWorker::value(rendered)); + + Ok(Value::Content(s)) +} diff --git a/crates/typlite/src/tests.rs b/crates/typlite/src/tests.rs index 9f29c202..29b48943 100644 --- a/crates/typlite/src/tests.rs +++ b/crates/typlite/src/tests.rs @@ -95,7 +95,7 @@ $ $ "###), @r###" -

typst-block

+

typst-block

"###); } diff --git a/crates/typlite/src/tests/rendering.rs b/crates/typlite/src/tests/rendering.rs index 33c97295..529534e8 100644 --- a/crates/typlite/src/tests/rendering.rs +++ b/crates/typlite/src/tests/rendering.rs @@ -6,14 +6,14 @@ fn test_math_equation() { $integral x dif x$ "###), @r###" - typst-block + typst-block "###); insta::assert_snapshot!(conv(r###" $ integral x dif x $ "###), @r###" -

typst-block

+

typst-block

"###); } diff --git a/editors/vscode/package.json b/editors/vscode/package.json index eab32b73..f7671deb 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -430,6 +430,16 @@ "disable" ] }, + "tinymist.renderDocs": { + "title": "(Experimental) Render Docs", + "description": "(Experimental) Whether to render typst elements in (hover) docs. In VS Code, when this feature is enabled, tinymist will store rendered results in the filesystem's temporary storage to show them in the hover content. Note: Please disable this feature if the editor doesn't support/handle image previewing in docs.", + "type": "string", + "default": "enable", + "enum": [ + "enable", + "disable" + ] + }, "tinymist.previewFeature": { "title": "Enable preview features", "description": "Enable or disable preview features of Typst. Note: restarting the editor is required to change this setting.", diff --git a/editors/vscode/src/config.ts b/editors/vscode/src/config.ts index d15a395c..c313b483 100644 --- a/editors/vscode/src/config.ts +++ b/editors/vscode/src/config.ts @@ -6,7 +6,7 @@ export function loadTinymistConfig() { let config: Record = JSON.parse( JSON.stringify(vscode.workspace.getConfiguration("tinymist")), ); - config.preferredTheme = "light"; + config.colorTheme = "light"; const keys = Object.keys(config); let values = keys.map((key) => config[key]); @@ -27,7 +27,7 @@ const STR_VARIABLES = [ "tinymist.outputPath", ]; const STR_ARR_VARIABLES = ["fontPaths", "tinymist.fontPaths"]; -const PREFERRED_THEME = ["preferredTheme", "tinymist.preferredTheme"]; +const COLOR_THEME = ["colorTheme", "tinymist.colorTheme"]; // todo: documentation that, typstExtraArgs won't get variable extended export function substVscodeVarsInConfig( @@ -39,7 +39,7 @@ export function substVscodeVarsInConfig( if (!k) { return value; } - if (PREFERRED_THEME.includes(k)) { + if (COLOR_THEME.includes(k)) { return determineVscodeTheme(); } if (STR_VARIABLES.includes(k)) { diff --git a/editors/vscode/src/extension.ts b/editors/vscode/src/extension.ts index 8710d7d7..0996cf89 100644 --- a/editors/vscode/src/extension.ts +++ b/editors/vscode/src/extension.ts @@ -46,6 +46,7 @@ import { devKitFeatureActivate } from "./features/dev-kit"; import { labelFeatureActivate } from "./features/label"; import { packageFeatureActivate } from "./features/package"; import { dragAndDropActivate } from "./features/drag-and-drop"; +import { HoverDummyStorage, HoverTmpStorage } from "./features/hover-storage"; export async function activate(context: ExtensionContext): Promise { try { @@ -76,6 +77,7 @@ export async function doActivate(context: ExtensionContext): Promise { extensionState.features.devKit = isDevMode || config.devKit === "enable"; extensionState.features.dragAndDrop = config.dragAndDrop === "enable"; extensionState.features.onEnter = !!config.onEnterEvent; + extensionState.features.renderDocs = config.renderDocs === "enable"; // Initializes language client const client = initClient(context, config); setClient(client); @@ -131,6 +133,10 @@ function initClient(context: ExtensionContext, config: Record) { const trustedCommands = { enabledCommands: ["tinymist.openInternal", "tinymist.openExternal"], }; + const hoverStorage = extensionState.features.renderDocs + ? new HoverTmpStorage(context) + : new HoverDummyStorage(); + const clientOptions: LanguageClientOptions = { documentSelector: typstDocumentSelector, initializationOptions: config, @@ -151,12 +157,26 @@ function initClient(context: ExtensionContext, config: Record) { return hover; } + const hoverHandler = await hoverStorage.startHover(); + for (const content of hover.contents) { if (content instanceof vscode.MarkdownString) { content.isTrusted = trustedCommands; content.supportHtml = true; + + if (context.storageUri) { + content.baseUri = Uri.joinPath(context.storageUri, "tmp/"); + } + + // outline all data "data:image/svg+xml;base64," to render huge image correctly + content.value = content.value.replace( + /\"data\:image\/svg\+xml\;base64,([^\"]*)\"/g, + (_, content) => hoverHandler.storeImage(content), + ); } } + + await hoverHandler.finish(); return hover; }, }, diff --git a/editors/vscode/src/features/hover-storage.ts b/editors/vscode/src/features/hover-storage.ts new file mode 100644 index 00000000..928fad3c --- /dev/null +++ b/editors/vscode/src/features/hover-storage.ts @@ -0,0 +1,88 @@ +import * as vscode from "vscode"; +import * as crypto from "crypto"; +import { Uri } from "vscode"; +import { base64Decode } from "../util"; + +export interface HoverStorage { + startHover(): Promise; +} + +export interface HoverStorageHandler { + baseUri(): vscode.Uri | undefined; + storeImage(image: string): string; + finish(): Promise; +} + +export class HoverDummyStorage { + startHover() { + return Promise.resolve(new HoverStorageDummyHandler()); + } +} + +export class HoverTmpStorage { + constructor(readonly context: vscode.ExtensionContext) {} + + async startHover() { + // This is a "workspace wide" storage for temporary hover images + if (this.context.storageUri) { + const tmpImageDir = Uri.joinPath(this.context.storageUri, "tmp/hover-images/"); + try { + const previousEntries = await vscode.workspace.fs.readDirectory(tmpImageDir); + let deleted = 0; + for (const [name, type] of previousEntries) { + if (type === vscode.FileType.File) { + deleted++; + await vscode.workspace.fs.delete(Uri.joinPath(tmpImageDir, name)); + } + } + if (deleted > 0) { + console.log(`Deleted ${deleted} hover images`); + } + } catch {} + try { + await vscode.workspace.fs.createDirectory(tmpImageDir); + return new HoverStorageTmpFsHandler(Uri.joinPath(this.context.storageUri, "tmp/")); + } catch {} + } + + return new HoverStorageDummyHandler(); + } +} + +class HoverStorageTmpFsHandler { + promises: PromiseLike[] = []; + + constructor(readonly _baseUri: vscode.Uri) {} + + baseUri() { + return this._baseUri; + } + + storeImage(content: string) { + const fs = vscode.workspace.fs; + const hash = crypto.createHash("sha256").update(content).digest("hex"); + const tmpImagePath = `./hover-images/${hash}.svg`; + const output = Uri.joinPath(this._baseUri, tmpImagePath); + const outputContent = base64Decode(content); + this.promises.push(fs.writeFile(output, Buffer.from(outputContent, "utf-8"))); + return tmpImagePath; + } + + async finish() { + await Promise.all(this.promises); + } +} + +class HoverStorageDummyHandler { + baseUri() { + return undefined; + } + + storeImage(_content: string) { + return ""; + } + + async finish() { + return; + } +} diff --git a/editors/vscode/src/state.ts b/editors/vscode/src/state.ts index 6b92d57b..c9a7051e 100644 --- a/editors/vscode/src/state.ts +++ b/editors/vscode/src/state.ts @@ -9,6 +9,7 @@ interface ExtensionState { dragAndDrop: boolean; onEnter: boolean; preview: boolean; + renderDocs: boolean; }; mut: { focusingFile: string | undefined; @@ -25,6 +26,7 @@ export const extensionState: ExtensionState = { dragAndDrop: false, onEnter: false, preview: false, + renderDocs: false, }, mut: { focusingFile: undefined,