feat: fix show pdf and add export pdf code lens

This commit is contained in:
Myriad-Dreamin 2024-03-11 13:22:55 +08:00
parent b508843fc0
commit e714ab462f
10 changed files with 218 additions and 55 deletions

View file

@ -0,0 +1,41 @@
use lsp_types::Command;
use crate::prelude::*;
#[derive(Debug, Clone)]
pub struct CodeLensRequest {
pub path: PathBuf,
}
impl CodeLensRequest {
pub fn request(
self,
world: &TypstSystemWorld,
position_encoding: PositionEncoding,
) -> Option<Vec<CodeLens>> {
let source = get_suitable_source_in_workspace(world, &self.path).ok()?;
let doc_start = typst_to_lsp::range(0..0, &source, position_encoding);
let mut res = vec![];
let run_code_lens_cmd = |title: &str, args: Vec<JsonValue>| Command {
title: title.to_string(),
command: "tinymist.runCodeLens".to_string(),
arguments: Some(args),
};
let doc_lens = |title: &str, args: Vec<JsonValue>| CodeLens {
range: doc_start,
command: Some(run_code_lens_cmd(title, args)),
data: None,
};
res.push(doc_lens("Preview", vec!["preview".into()]));
res.push(doc_lens("Preview in ..", vec!["preview-in".into()]));
res.push(doc_lens("Export PDF", vec!["export-pdf".into()]));
res.push(doc_lens("Export ..", vec!["export-as".into()]));
Some(res)
}
}

View file

@ -31,6 +31,8 @@ pub(crate) mod prepare_rename;
pub use prepare_rename::*;
pub(crate) mod rename;
pub use rename::*;
pub(crate) mod code_lens;
pub use code_lens::*;
pub mod lsp_typst_boundary;
pub use lsp_typst_boundary::*;
@ -41,6 +43,11 @@ mod polymorphic {
use super::prelude::*;
use super::*;
#[derive(Debug, Clone)]
pub struct OnExportRequest {
pub path: PathBuf,
}
#[derive(Debug, Clone)]
pub struct OnSaveExportRequest {
pub path: PathBuf,
@ -56,10 +63,12 @@ mod polymorphic {
#[derive(Debug, Clone)]
pub enum CompilerQueryRequest {
OnExport(OnExportRequest),
OnSaveExport(OnSaveExportRequest),
Hover(HoverRequest),
GotoDefinition(GotoDefinitionRequest),
InlayHint(InlayHintRequest),
CodeLens(CodeLensRequest),
Completion(CompletionRequest),
SignatureHelp(SignatureHelpRequest),
Rename(RenameRequest),
@ -76,10 +85,12 @@ mod polymorphic {
pub fn fold_feature(&self) -> FoldRequestFeature {
use FoldRequestFeature::*;
match self {
CompilerQueryRequest::OnExport(..) => Mergable,
CompilerQueryRequest::OnSaveExport(..) => Mergable,
CompilerQueryRequest::Hover(..) => PinnedFirst,
CompilerQueryRequest::GotoDefinition(..) => PinnedFirst,
CompilerQueryRequest::InlayHint(..) => Unique,
CompilerQueryRequest::CodeLens(..) => Unique,
CompilerQueryRequest::Completion(..) => Mergable,
CompilerQueryRequest::SignatureHelp(..) => PinnedFirst,
CompilerQueryRequest::Rename(..) => Mergable,
@ -95,10 +106,12 @@ mod polymorphic {
pub fn associated_path(&self) -> Option<&Path> {
Some(match self {
CompilerQueryRequest::OnExport(..) => return None,
CompilerQueryRequest::OnSaveExport(req) => &req.path,
CompilerQueryRequest::Hover(req) => &req.path,
CompilerQueryRequest::GotoDefinition(req) => &req.path,
CompilerQueryRequest::InlayHint(req) => &req.path,
CompilerQueryRequest::CodeLens(req) => &req.path,
CompilerQueryRequest::Completion(req) => &req.path,
CompilerQueryRequest::SignatureHelp(req) => &req.path,
CompilerQueryRequest::Rename(req) => &req.path,
@ -115,10 +128,12 @@ mod polymorphic {
#[derive(Debug, Clone)]
pub enum CompilerQueryResponse {
OnExport(Option<PathBuf>),
OnSaveExport(()),
Hover(Option<Hover>),
GotoDefinition(Option<GotoDefinitionResponse>),
InlayHint(Option<Vec<InlayHint>>),
CodeLens(Option<Vec<CodeLens>>),
Completion(Option<CompletionResponse>),
SignatureHelp(Option<SignatureHelp>),
PrepareRename(Option<PrepareRenameResponse>),

View file

@ -9,12 +9,14 @@ pub use comemo::{Track, Tracked};
pub use itertools::{Format, Itertools};
pub use log::{error, trace};
pub use lsp_types::{
CompletionResponse, DiagnosticRelatedInformation, DocumentSymbol, DocumentSymbolResponse,
Documentation, FoldingRange, GotoDefinitionResponse, Hover, InlayHint, Location as LspLocation,
MarkupContent, MarkupKind, Position as LspPosition, PrepareRenameResponse, SelectionRange,
SemanticTokens, SemanticTokensDelta, SemanticTokensFullDeltaResult, SemanticTokensResult,
SignatureHelp, SignatureInformation, SymbolInformation, Url, WorkspaceEdit,
CodeLens, CompletionResponse, DiagnosticRelatedInformation, DocumentSymbol,
DocumentSymbolResponse, Documentation, FoldingRange, GotoDefinitionResponse, Hover, InlayHint,
Location as LspLocation, MarkupContent, MarkupKind, Position as LspPosition,
PrepareRenameResponse, SelectionRange, SemanticTokens, SemanticTokensDelta,
SemanticTokensFullDeltaResult, SemanticTokensResult, SignatureHelp, SignatureInformation,
SymbolInformation, Url, WorkspaceEdit,
};
pub use serde_json::Value as JsonValue;
pub use typst::diag::{EcoString, FileError, FileResult, Tracepoint};
pub use typst::foundations::{Func, ParamInfo, Value};
pub use typst::syntax::{

View file

@ -32,17 +32,14 @@ use typst_ts_compiler::service::{
WorkspaceProvider, WorldExporter,
};
use crate::task::BorrowTask;
#[derive(Debug, Clone)]
pub struct VersionedDocument {
pub version: usize,
pub document: Arc<TypstDocument>,
}
/// A task that can be sent to the context (compiler thread)
///
/// The internal function will be dereferenced and called on the context.
type BorrowTask<Ctx> = Box<dyn FnOnce(&mut Ctx) + Send + 'static>;
/// Interrupts for external sources
enum ExternalInterrupt<Ctx> {
/// Interrupted by task.

View file

@ -7,9 +7,10 @@ use std::{
use anyhow::Context;
use log::{error, info};
use parking_lot::Mutex;
use tokio::sync::{
broadcast::{self, error::RecvError},
watch,
oneshot, watch,
};
use typst::foundations::Smart;
use typst_ts_core::{path::PathClean, ImmutPath, TypstDocument};
@ -19,6 +20,8 @@ use crate::ExportPdfMode;
#[derive(Debug, Clone)]
pub enum RenderActorRequest {
OnTyped,
// todo: bad arch...
DoExport(Arc<Mutex<Option<oneshot::Sender<Option<PathBuf>>>>>),
OnSaved(PathBuf),
ChangeExportPath(PdfPathVars),
ChangeConfig(PdfExportConfig),
@ -94,7 +97,20 @@ impl PdfExportActor {
self.path = cfg.path;
}
_ => {
self.check_mode_and_export(req).await;
let sender = match &req {
RenderActorRequest::DoExport(sender) => Some(sender.clone()),
_ => None,
};
let resp = self.check_mode_and_export(req).await;
if let Some(sender) = sender {
let Some(sender) = sender.lock().take() else {
error!("PdfRenderActor: sender is None");
continue;
};
if let Err(e) = sender.send(resp) {
error!("PdfRenderActor: failed to send response: {err:?}", err = e);
}
}
}
}
}
@ -102,14 +118,15 @@ impl PdfExportActor {
}
}
async fn check_mode_and_export(&self, req: RenderActorRequest) {
async fn check_mode_and_export(&self, req: RenderActorRequest) -> Option<PathBuf> {
let Some(document) = self.document.borrow().clone() else {
info!("PdfRenderActor: document is not ready");
return;
return None;
};
let eq_mode = match req {
RenderActorRequest::OnTyped => ExportPdfMode::OnType,
RenderActorRequest::DoExport(..) => ExportPdfMode::OnSave,
RenderActorRequest::OnSaved(..) => ExportPdfMode::OnSave,
_ => unreachable!(),
};
@ -119,11 +136,17 @@ impl PdfExportActor {
self.path, self.substitute_pattern
);
if let Some(path) = self.path.as_ref() {
if (get_mode(self.mode) == eq_mode) || validate_document(&req, self.mode, &document) {
let Err(err) = self.export_pdf(&document, path).await else {
return;
};
let should_do = matches!(req, RenderActorRequest::DoExport(..));
let should_do = should_do || get_mode(self.mode) == eq_mode;
let should_do = should_do || validate_document(&req, self.mode, &document);
if should_do {
return match self.export_pdf(&document, path).await {
Ok(pdf) => Some(pdf),
Err(err) => {
error!("PdfRenderActor: failed to export PDF: {err}", err = err);
None
}
};
}
}
@ -150,9 +173,11 @@ impl PdfExportActor {
false
}
None
}
async fn export_pdf(&self, doc: &TypstDocument, path: &Path) -> anyhow::Result<()> {
async fn export_pdf(&self, doc: &TypstDocument, path: &Path) -> anyhow::Result<PathBuf> {
let Some(to) = substitute_path(&self.substitute_pattern, &self.root, path) else {
return Err(anyhow::anyhow!("failed to substitute path"));
};
@ -176,10 +201,10 @@ impl PdfExportActor {
// todo: timestamp world.now()
let data = typst_pdf::pdf(doc, Smart::Auto, None);
std::fs::write(to, data).context("failed to export PDF")?;
std::fs::write(&to, data).context("failed to export PDF")?;
info!("PDF export complete");
Ok(())
Ok(to)
}
}

View file

@ -10,9 +10,9 @@ use log::{debug, error, info, trace, warn};
use parking_lot::Mutex;
use tinymist_query::{
CompilerQueryRequest, CompilerQueryResponse, DiagnosticsMap, FoldRequestFeature,
OnSaveExportRequest, PositionEncoding,
OnExportRequest, OnSaveExportRequest, PositionEncoding,
};
use tokio::sync::{broadcast, mpsc, watch};
use tokio::sync::{broadcast, mpsc, oneshot, watch};
use typst::{
diag::{SourceDiagnostic, SourceResult},
layout::Position,
@ -579,6 +579,9 @@ impl CompileActor {
assert!(query.fold_feature() != FoldRequestFeature::ContextFreeUnique);
match query {
CompilerQueryRequest::OnExport(OnExportRequest { path }) => {
Ok(CompilerQueryResponse::OnExport(self.on_export(path)?))
}
CompilerQueryRequest::OnSaveExport(OnSaveExportRequest { path }) => {
self.on_save_export(path)?;
Ok(CompilerQueryResponse::OnSaveExport(()))
@ -586,6 +589,7 @@ impl CompileActor {
Hover(req) => query_state!(self, Hover, req),
GotoDefinition(req) => query_world!(self, GotoDefinition, req),
InlayHint(req) => query_world!(self, InlayHint, req),
CodeLens(req) => query_world!(self, CodeLens, req),
Completion(req) => query_state!(self, Completion, req),
SignatureHelp(req) => query_world!(self, SignatureHelp, req),
Rename(req) => query_world!(self, Rename, req),
@ -599,6 +603,40 @@ impl CompileActor {
}
}
pub fn sync_render<Ret: Send + 'static>(&self, f: oneshot::Receiver<Ret>) -> ZResult<Ret> {
// get current async handle
if let Ok(e) = tokio::runtime::Handle::try_current() {
// todo: remove blocking
return std::thread::spawn(move || {
e.block_on(f)
.map_err(map_string_err("failed to sync_render"))
})
.join()
.unwrap();
}
f.blocking_recv()
.map_err(map_string_err("failed to recv from sync_render"))
}
fn on_export(&self, path: PathBuf) -> anyhow::Result<Option<PathBuf>> {
info!("CompileActor: on export: {}", path.display());
let (tx, rx) = oneshot::channel();
let task = Arc::new(Mutex::new(Some(tx)));
self.render_tx
.send(RenderActorRequest::DoExport(task))
.map_err(map_string_err("failed to send to sync_render"))?;
let res: Option<PathBuf> = self.sync_render(rx)?;
info!("CompileActor: on export end: {path:?} as {res:?}");
Ok(res)
}
fn on_save_export(&self, path: PathBuf) -> anyhow::Result<()> {
info!("CompileActor: on save export: {}", path.display());
let _ = self.render_tx.send(RenderActorRequest::OnSaved(path));

View file

@ -585,6 +585,9 @@ impl Init {
}),
document_formatting_provider,
inlay_hint_provider: Some(OneOf::Left(true)),
code_lens_provider: Some(CodeLensOptions {
resolve_provider: Some(false),
}),
..Default::default()
},
..Default::default()

View file

@ -30,6 +30,7 @@
mod actor;
pub mod init;
mod query;
mod task;
pub mod transport;
use core::fmt;
@ -210,7 +211,7 @@ type LspResult<Res> = Result<Res, ResponseError>;
type LspMethod<Res> = fn(srv: &mut TypstLanguageServer, args: JsonValue) -> LspResult<Res>;
type LspHandler<Req, Res> = fn(srv: &mut TypstLanguageServer, args: Req) -> LspResult<Res>;
type ExecuteCmdMap = HashMap<&'static str, LspHandler<Vec<JsonValue>, ()>>;
type ExecuteCmdMap = HashMap<&'static str, LspHandler<Vec<JsonValue>, JsonValue>>;
type NotifyCmdMap = HashMap<&'static str, LspMethod<()>>;
type RegularCmdMap = HashMap<&'static str, LspMethod<JsonValue>>;
@ -344,11 +345,12 @@ impl TypstLanguageServer {
request_fn!(SemanticTokensFullRequest, Self::semantic_tokens_full),
request_fn!(SemanticTokensFullDeltaRequest, Self::semantic_tokens_full_delta),
request_fn!(DocumentSymbolRequest, Self::document_symbol),
request_fn!(InlayHintRequest, Self::inlay_hint),
// Sync for low latency
request_fn!(SelectionRangeRequest, Self::selection_range),
// latency insensitive
request_fn!(InlayHintRequest, Self::inlay_hint),
request_fn!(HoverRequest, Self::hover),
request_fn!(CodeLensRequest, Self::code_lens),
request_fn!(FoldingRangeRequest, Self::folding_range),
request_fn!(SignatureHelpRequest, Self::signature_help),
request_fn!(PrepareRenameRequest, Self::prepare_rename),
@ -615,13 +617,13 @@ impl TypstLanguageServer {
($key: expr, Self::$method: ident) => {
(
$key,
exec_fn!(LspHandler<Vec<JsonValue>, ()>, Self::$method, inputs),
exec_fn!(LspHandler<Vec<JsonValue>, JsonValue>, Self::$method, inputs),
)
};
}
ExecuteCmdMap::from_iter([
redirected_command!("tinymist.doPdfExport", Self::export_pdf),
redirected_command!("tinymist.exportPdf", Self::export_pdf),
redirected_command!("tinymist.doClearCache", Self::clear_cache),
redirected_command!("tinymist.doPinMain", Self::pin_main),
redirected_command!("tinymist.doActivateDoc", Self::activate_doc),
@ -640,8 +642,7 @@ impl TypstLanguageServer {
return Err(method_not_found());
};
handler(self, arguments)?;
Ok(Some(JsonValue::Null))
Ok(Some(handler(self, arguments)?))
}
/// Export the current document as a PDF file. The client is responsible for
@ -649,7 +650,7 @@ impl TypstLanguageServer {
///
/// # Errors
/// Errors if a provided file URI is not a valid file URI.
pub fn export_pdf(&self, arguments: Vec<JsonValue>) -> LspResult<()> {
pub fn export_pdf(&self, arguments: Vec<JsonValue>) -> LspResult<JsonValue> {
if arguments.is_empty() {
return Err(invalid_params("Missing file URI argument"));
}
@ -662,25 +663,26 @@ impl TypstLanguageServer {
.to_file_path()
.map_err(|_| invalid_params("URI is not a file URI"))?;
let _ = run_query!(self.OnSaveExport(path));
let res = run_query!(self.OnExport(path))?;
let res = serde_json::to_value(res).map_err(|_| internal_error("Cannot serialize path"))?;
Ok(())
Ok(res)
}
/// Clear all cached resources.
///
/// # Errors
/// Errors if the cache could not be cleared.
pub fn clear_cache(&self, _arguments: Vec<JsonValue>) -> LspResult<()> {
pub fn clear_cache(&self, _arguments: Vec<JsonValue>) -> LspResult<JsonValue> {
comemo::evict(0);
Ok(())
Ok(JsonValue::Null)
}
/// Pin main file to some path.
///
/// # Errors
/// Errors if a provided file URI is not a valid file URI.
pub fn pin_main(&mut self, arguments: Vec<JsonValue>) -> LspResult<()> {
pub fn pin_main(&mut self, arguments: Vec<JsonValue>) -> LspResult<JsonValue> {
let Some(file_uri) = arguments.first().and_then(|v| v.as_str()) else {
return Err(invalid_params("Missing file path as the first argument"));
};
@ -728,7 +730,7 @@ impl TypstLanguageServer {
})?;
info!("main file pinned: {main_url:?}", main_url = file_uri);
Ok(())
Ok(JsonValue::Null)
}
/// Change the actived document.
@ -736,7 +738,7 @@ impl TypstLanguageServer {
/// # Errors
/// Errors if a provided file URI is not a valid path string.
/// Errors if the document could not be activated.
pub fn activate_doc(&self, arguments: Vec<JsonValue>) -> LspResult<()> {
pub fn activate_doc(&self, arguments: Vec<JsonValue>) -> LspResult<JsonValue> {
let Some(path) = arguments.first() else {
return Err(invalid_params("Missing path argument"));
};
@ -762,7 +764,7 @@ impl TypstLanguageServer {
// })?;
info!("active document set: {path:?}", path = path);
Ok(())
Ok(JsonValue::Null)
}
}
@ -928,6 +930,11 @@ impl TypstLanguageServer {
run_query!(self.InlayHint(path, range))
}
fn code_lens(&self, params: CodeLensParams) -> LspResult<Option<Vec<CodeLens>>> {
let path = as_path(params.text_document);
run_query!(self.CodeLens(path))
}
fn completion(&self, params: CompletionParams) -> LspResult<Option<CompletionResponse>> {
let (path, position) = as_path_pos(params.text_document_position);
let explicit = params

View file

@ -0,0 +1,4 @@
/// A task that can be sent to the context (compiler/render thread)
///
/// The internal function will be dereferenced and called on the context.
pub type BorrowTask<Ctx> = Box<dyn FnOnce(&mut Ctx) + Send + 'static>;

View file

@ -84,6 +84,9 @@ async function startClient(context: ExtensionContext): Promise<void> {
);
context.subscriptions.push(commands.registerCommand("tinymist.showPdf", commandShowPdf));
context.subscriptions.push(commands.registerCommand("tinymist.clearCache", commandClearCache));
context.subscriptions.push(
commands.registerCommand("tinymist.runCodeLens", commandRunCodeLens)
);
return client.start();
}
@ -149,7 +152,7 @@ function validateServer(path: string): { valid: true } | { valid: false; message
}
}
async function commandExportCurrentPdf(): Promise<void> {
async function commandExportCurrentPdf(): Promise<string | undefined> {
const activeEditor = window.activeTextEditor;
if (activeEditor === undefined) {
return;
@ -157,10 +160,15 @@ async function commandExportCurrentPdf(): Promise<void> {
const uri = activeEditor.document.uri.toString();
await client?.sendRequest("workspace/executeCommand", {
command: "tinymist.doPdfExport",
const res = await client?.sendRequest<string | null>("workspace/executeCommand", {
command: "tinymist.exportPdf",
arguments: [uri],
});
console.log("export pdf", res);
if (res === null) {
return undefined;
}
return res;
}
/**
@ -173,22 +181,19 @@ async function commandShowPdf(): Promise<void> {
return;
}
// todo: this is wrong
const uri = activeEditor.document.uri;
// change the file extension to `.pdf` as we want to open the pdf file
// and not the currently opened `.typ` file.
const n = uri.toString().lastIndexOf(".");
const pdf_uri = Uri.parse(uri.toString().slice(0, n) + ".pdf");
try {
await workspace.fs.stat(pdf_uri);
} catch {
// only create pdf if it does not exist yet
await commandExportCurrentPdf();
} finally {
// here we can be sure that the pdf exists
await commands.executeCommand("vscode.open", pdf_uri, ViewColumn.Beside);
const pdfPath = await commandExportCurrentPdf();
if (pdfPath === undefined) {
// show error message
await window.showErrorMessage("Failed to create PDF");
return;
}
const pdfUri = Uri.file(pdfPath);
// here we can be sure that the pdf exists
await commands.executeCommand("vscode.open", pdfUri, ViewColumn.Beside);
}
async function commandClearCache(): Promise<void> {
@ -233,3 +238,29 @@ async function commandActivateDoc(editor: TextEditor | undefined): Promise<void>
arguments: [editor?.document.uri.fsPath],
});
}
async function commandRunCodeLens(...args: string[]): Promise<void> {
console.log("run code lens", args);
if (args.length === 0) {
return;
}
switch (args[0]) {
case "preview": {
break;
}
case "preview-in": {
break;
}
case "export-pdf": {
await commandShowPdf();
break;
}
case "export-as": {
break;
}
default: {
console.error("unknown code lens command", args[0]);
}
}
}