feat: render examples in docs (#772)

* feat: render examples in docs

* fix: create `hover-images` on `startHover`

* dev: update snapshot
This commit is contained in:
Myriad-Dreamin 2024-11-08 15:55:47 +08:00 committed by GitHub
parent d02fa18617
commit d75fd7e74e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 334 additions and 74 deletions

View file

@ -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<typlite::scopes::Scopes<Value>> {
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);

View file

@ -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<Arc<dyn PeriscopeProvider + Send + Sync>>,
/// The semantic token context.

View file

@ -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<Mutex<FileId>> =
LazyLock::new(|| Mutex::new(FileId::new(None, VirtualPath::new("__tinymist_docs__.typ"))));
pub(crate) fn convert_docs(world: &LspWorld, content: &str) -> StrResult<EcoString> {
static DOCS_LIB: LazyLock<Arc<Scopes<Value>>> = LazyLock::new(lib);
pub(crate) fn convert_docs(ctx: &SharedContext, content: &str) -> StrResult<EcoString> {
static DOCS_LIB: LazyLock<Arc<Scopes<Value>>> =
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<EcoStri
let conv = typlite::Typlite::new(Arc::new(w))
.with_library(DOCS_LIB.clone())
.with_color_theme(ctx.analysis.color_theme)
.annotate_elements(true)
.convert()
.map_err(|e| eco_format!("failed to convert to markdown: {e}"))?;
Ok(conv.replace("```example", "```typ"))
}
pub(super) fn lib() -> Arc<Scopes<Value>> {
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<Value> {
let body = get_pos_named!(args, body: Content).0;
let body = body.trim();
let ticks = body.chars().take_while(|t| *t == '`').collect::<String>();
let body = &body[ticks.len()..];
let body = eco_format!("{ticks}typ{body}");
Ok(Value::Content(body))
}

View file

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

View file

@ -107,7 +107,7 @@ struct DocsChecker<'a> {
impl<'a> DocsChecker<'a> {
pub fn check_func_docs(mut self, docs: String) -> Option<DocString> {
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<DocString> {
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<DocString> {
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 {

View file

@ -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<dyn PeriscopeProvider + Send + Sync>

View file

@ -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<PeriscopeArgs>,
/// Typst extra arguments.
pub typst_extra_args: Option<CompileExtraOpts>,
/// The preferred theme for the document.
pub preferred_theme: Option<String>,
/// The preferred color theme for the document.
pub color_theme: Option<String>,
/// 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(),

View file

@ -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<T, Err = Error> = std::result::Result<T, Err>;
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<ColorTheme>,
}
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<ColorTheme>,
gfm: bool,
do_annotate: bool,
scopes: Arc<Scopes<Value>>,
@ -290,37 +318,84 @@ impl TypliteWorker {
Ok(Value::Content(s))
}
fn render(&mut self, node: &SyntaxNode, inline: bool) -> Result<Value> {
let dark = self.render_inner(node, true)?;
let light = self.render_inner(node, false)?;
if inline {
Ok(Value::Content(eco_format!(
r#"<picture><source media="(prefers-color-scheme: dark)" srcset="data:image/svg+xml;base64,{dark}"><img style="vertical-align: -0.35em" alt="typst-block" src="data:image/svg+xml;base64,{light}"/></picture>"#
)))
} else {
Ok(Value::Content(eco_format!(
r#"<p align="center"><picture><source media="(prefers-color-scheme: dark)" srcset="data:image/svg+xml;base64,{dark}"><img alt="typst-block" src="data:image/svg+xml;base64,{light}"/></picture></p>"#
)))
}
pub fn render(&mut self, node: &SyntaxNode, inline: bool) -> Result<Value> {
let code = node.clone().into_text();
self.render_code(&code, false, "center", "", inline)
}
fn render_inner(&mut self, node: &SyntaxNode, is_dark: bool) -> Result<String> {
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<Value> {
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#"<p align="{align}">"#);
}
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#"<img{inline_attrs} alt="typst-block" src="data:image/svg+xml;base64,{data}" {extra_attrs}/>"#,
);
}
None => {
let _ = write!(
content,
r#"<picture><source media="(prefers-color-scheme: dark)" srcset="data:image/svg+xml;base64,{dark}"><img{inline_attrs} alt="typst-block" src="data:image/svg+xml;base64,{light}" {extra_attrs}/></picture>"#,
dark = render(ColorTheme::Dark)?,
light = render(ColorTheme::Light)?
);
}
}
if !inline {
content.push_str("</p>");
}
Ok(Value::Content(content))
}
fn render_inner(&mut self, code: &str, is_markup: bool, theme: ColorTheme) -> Result<String> {
static DARK_THEME_INPUT: LazyLock<Arc<LazyHash<Dict>>> = 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;

View file

@ -4,6 +4,9 @@ use super::*;
use ecow::eco_format;
use value::*;
mod docstring;
pub use docstring::docstring_lib;
pub fn library() -> Scopes<Value> {
let mut scopes = Scopes::new();
scopes.define("link", link as RawFunc);

View file

@ -0,0 +1,58 @@
use super::*;
pub fn docstring_lib() -> Scopes<Value> {
let mut scopes = library();
scopes.define("example", example as RawFunc);
scopes
}
/// Evaluate a `example`.
pub fn example(mut args: Args) -> Result<Value> {
let body = get_pos_named!(args, body: &SyntaxNode);
let body = body
.cast::<ast::Raw>()
.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.
// <https://github.com/typst/typst/blob/070e3144b33e9a9e9839c138df2b0a13dde7abc7/docs/src/html.rs#L355>
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))
}

View file

@ -95,7 +95,7 @@ $
$
"###), @r###"
<p align="center"><picture><source media="(prefers-color-scheme: dark)" srcset="data:image-hash/svg+xml;base64,redacted"><img alt="typst-block" src="data:image-hash/svg+xml;base64,redacted"/></picture></p>
<p align="center"><picture><source media="(prefers-color-scheme: dark)" srcset="data:image-hash/svg+xml;base64,redacted"><img alt="typst-block" src="data:image-hash/svg+xml;base64,redacted" /></picture></p>
"###);
}

View file

@ -6,14 +6,14 @@ fn test_math_equation() {
$integral x dif x$
"###), @r###"
<picture><source media="(prefers-color-scheme: dark)" srcset="data:image-hash/svg+xml;base64,redacted"><img style="vertical-align: -0.35em" alt="typst-block" src="data:image-hash/svg+xml;base64,redacted"/></picture>
<picture><source media="(prefers-color-scheme: dark)" srcset="data:image-hash/svg+xml;base64,redacted"><img style="vertical-align: -0.35em" alt="typst-block" src="data:image-hash/svg+xml;base64,redacted" /></picture>
"###);
insta::assert_snapshot!(conv(r###"
$ integral x dif x $
"###), @r###"
<p align="center"><picture><source media="(prefers-color-scheme: dark)" srcset="data:image-hash/svg+xml;base64,redacted"><img alt="typst-block" src="data:image-hash/svg+xml;base64,redacted"/></picture></p>
<p align="center"><picture><source media="(prefers-color-scheme: dark)" srcset="data:image-hash/svg+xml;base64,redacted"><img alt="typst-block" src="data:image-hash/svg+xml;base64,redacted" /></picture></p>
"###);
}

View file

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

View file

@ -6,7 +6,7 @@ export function loadTinymistConfig() {
let config: Record<string, any> = 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)) {

View file

@ -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<void> {
try {
@ -76,6 +77,7 @@ export async function doActivate(context: ExtensionContext): Promise<void> {
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<string, any>) {
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<string, any>) {
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;
},
},

View file

@ -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<HoverStorageHandler>;
}
export interface HoverStorageHandler {
baseUri(): vscode.Uri | undefined;
storeImage(image: string): string;
finish(): Promise<void>;
}
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<void>[] = [];
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;
}
}

View file

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