mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-11-25 13:23:44 +00:00
feat: add status bar to showing words count, also for compiling status (#158)
* feat: add status bar to showing words count, also for compilng status * dev: add configuration for compile status * fix: let focus state correct * dev: improve hint
This commit is contained in:
parent
454127e354
commit
6722b2501f
16 changed files with 633 additions and 47 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -3734,6 +3734,7 @@ dependencies = [
|
||||||
"typst-ts-svg-exporter",
|
"typst-ts-svg-exporter",
|
||||||
"typstfmt_lib",
|
"typstfmt_lib",
|
||||||
"typstyle",
|
"typstyle",
|
||||||
|
"unicode-script",
|
||||||
"vergen",
|
"vergen",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,7 @@ mod polymorphic {
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum ExportKind {
|
pub enum ExportKind {
|
||||||
Pdf,
|
Pdf,
|
||||||
|
WordCount,
|
||||||
Svg { page: PageSelection },
|
Svg { page: PageSelection },
|
||||||
Png { page: PageSelection },
|
Png { page: PageSelection },
|
||||||
}
|
}
|
||||||
|
|
@ -136,6 +137,7 @@ mod polymorphic {
|
||||||
pub fn extension(&self) -> &str {
|
pub fn extension(&self) -> &str {
|
||||||
match self {
|
match self {
|
||||||
Self::Pdf => "pdf",
|
Self::Pdf => "pdf",
|
||||||
|
Self::WordCount => "txt",
|
||||||
Self::Svg { .. } => "svg",
|
Self::Svg { .. } => "svg",
|
||||||
Self::Png { .. } => "png",
|
Self::Png { .. } => "png",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ crossbeam-channel.workspace = true
|
||||||
lsp-types.workspace = true
|
lsp-types.workspace = true
|
||||||
dhat = { version = "0.3.3", optional = true }
|
dhat = { version = "0.3.3", optional = true }
|
||||||
chrono = { version = "0.4" }
|
chrono = { version = "0.4" }
|
||||||
|
unicode-script = "0.5"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["cli", "preview"]
|
default = ["cli", "preview"]
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ pub mod render;
|
||||||
pub mod typ_client;
|
pub mod typ_client;
|
||||||
pub mod typ_server;
|
pub mod typ_server;
|
||||||
|
|
||||||
|
use tinymist_query::ExportKind;
|
||||||
use tokio::sync::{broadcast, watch};
|
use tokio::sync::{broadcast, watch};
|
||||||
use typst::util::Deferred;
|
use typst::util::Deferred;
|
||||||
use typst_ts_compiler::{
|
use typst_ts_compiler::{
|
||||||
|
|
@ -23,7 +24,7 @@ use self::{
|
||||||
use crate::{
|
use crate::{
|
||||||
compiler::CompileServer,
|
compiler::CompileServer,
|
||||||
world::{ImmutDict, LspWorld, LspWorldBuilder},
|
world::{ImmutDict, LspWorld, LspWorldBuilder},
|
||||||
TypstLanguageServer,
|
ExportMode, TypstLanguageServer,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use formatting::{FormattingConfig, FormattingRequest};
|
pub use formatting::{FormattingConfig, FormattingRequest};
|
||||||
|
|
@ -33,26 +34,46 @@ type CompileDriverInner = CompileDriverImpl<LspWorld>;
|
||||||
impl CompileServer {
|
impl CompileServer {
|
||||||
pub fn server(
|
pub fn server(
|
||||||
&self,
|
&self,
|
||||||
diag_group: String,
|
editor_group: String,
|
||||||
entry: EntryState,
|
entry: EntryState,
|
||||||
inputs: ImmutDict,
|
inputs: ImmutDict,
|
||||||
) -> CompileClientActor {
|
) -> CompileClientActor {
|
||||||
let (doc_tx, doc_rx) = watch::channel(None);
|
let (doc_tx, doc_rx) = watch::channel(None);
|
||||||
let (render_tx, _) = broadcast::channel(10);
|
let (render_tx, _) = broadcast::channel(10);
|
||||||
|
|
||||||
// Run the Export actor before preparing cluster to avoid loss of events
|
let config = ExportConfig {
|
||||||
|
substitute_pattern: self.config.output_path.clone(),
|
||||||
|
entry: entry.clone(),
|
||||||
|
mode: self.config.export_pdf,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run Export actors before preparing cluster to avoid loss of events
|
||||||
self.handle.spawn(
|
self.handle.spawn(
|
||||||
ExportActor::new(
|
ExportActor::new(
|
||||||
|
editor_group.clone(),
|
||||||
doc_rx.clone(),
|
doc_rx.clone(),
|
||||||
|
self.diag_tx.clone(),
|
||||||
render_tx.subscribe(),
|
render_tx.subscribe(),
|
||||||
ExportConfig {
|
config.clone(),
|
||||||
substitute_pattern: self.config.output_path.clone(),
|
ExportKind::Pdf,
|
||||||
entry: entry.clone(),
|
|
||||||
mode: self.config.export_pdf,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
.run(),
|
.run(),
|
||||||
);
|
);
|
||||||
|
if self.config.notify_compile_status {
|
||||||
|
let mut config = config;
|
||||||
|
config.mode = ExportMode::OnType;
|
||||||
|
self.handle.spawn(
|
||||||
|
ExportActor::new(
|
||||||
|
editor_group.clone(),
|
||||||
|
doc_rx.clone(),
|
||||||
|
self.diag_tx.clone(),
|
||||||
|
render_tx.subscribe(),
|
||||||
|
config,
|
||||||
|
ExportKind::WordCount,
|
||||||
|
)
|
||||||
|
.run(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Take all dirty files in memory as the initial snapshot
|
// Take all dirty files in memory as the initial snapshot
|
||||||
let snapshot = FileChangeSet::default();
|
let snapshot = FileChangeSet::default();
|
||||||
|
|
@ -63,14 +84,14 @@ impl CompileServer {
|
||||||
let handler = CompileHandler {
|
let handler = CompileHandler {
|
||||||
#[cfg(feature = "preview")]
|
#[cfg(feature = "preview")]
|
||||||
inner: std::sync::Arc::new(parking_lot::Mutex::new(None)),
|
inner: std::sync::Arc::new(parking_lot::Mutex::new(None)),
|
||||||
diag_group: diag_group.clone(),
|
diag_group: editor_group.clone(),
|
||||||
doc_tx,
|
doc_tx,
|
||||||
render_tx: render_tx.clone(),
|
render_tx: render_tx.clone(),
|
||||||
diag_tx: self.diag_tx.clone(),
|
editor_tx: self.diag_tx.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let position_encoding = self.const_config().position_encoding;
|
let position_encoding = self.const_config().position_encoding;
|
||||||
let diag_group = diag_group.clone();
|
let diag_group = editor_group.clone();
|
||||||
let entry = entry.clone();
|
let entry = entry.clone();
|
||||||
let font_resolver = self.font.clone();
|
let font_resolver = self.font.clone();
|
||||||
move || {
|
move || {
|
||||||
|
|
@ -104,7 +125,7 @@ impl CompileServer {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
CompileClientActor::new(diag_group, self.config.clone(), entry, inner, render_tx)
|
CompileClientActor::new(editor_group, self.config.clone(), entry, inner, render_tx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,37 +7,76 @@ use lsp_types::Url;
|
||||||
use tinymist_query::{DiagnosticsMap, LspDiagnostic};
|
use tinymist_query::{DiagnosticsMap, LspDiagnostic};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
use crate::{LspHost, TypstLanguageServer};
|
use crate::{tools::word_count::WordsCount, LspHost, TypstLanguageServer};
|
||||||
|
|
||||||
pub struct CompileClusterActor {
|
pub enum CompileClusterRequest {
|
||||||
|
Diag(String, Option<DiagnosticsMap>),
|
||||||
|
Status(String, TinymistCompileStatusEnum),
|
||||||
|
WordCount(String, Option<WordsCount>),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EditorActor {
|
||||||
pub host: LspHost<TypstLanguageServer>,
|
pub host: LspHost<TypstLanguageServer>,
|
||||||
pub diag_rx: mpsc::UnboundedReceiver<(String, Option<DiagnosticsMap>)>,
|
pub diag_rx: mpsc::UnboundedReceiver<CompileClusterRequest>,
|
||||||
|
|
||||||
pub diagnostics: HashMap<Url, HashMap<String, Vec<LspDiagnostic>>>,
|
pub diagnostics: HashMap<Url, HashMap<String, Vec<LspDiagnostic>>>,
|
||||||
pub affect_map: HashMap<String, Vec<Url>>,
|
pub affect_map: HashMap<String, Vec<Url>>,
|
||||||
pub published_primary: bool,
|
pub published_primary: bool,
|
||||||
|
pub notify_compile_status: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CompileClusterActor {
|
impl EditorActor {
|
||||||
pub async fn run(mut self) {
|
pub async fn run(mut self) {
|
||||||
|
let mut compile_status = TinymistCompileStatusEnum::Compiling;
|
||||||
|
let mut words_count = None;
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
e = self.diag_rx.recv() => {
|
e = self.diag_rx.recv() => {
|
||||||
let Some((group, diagnostics)) = e else {
|
match e {
|
||||||
break;
|
Some(CompileClusterRequest::Diag(group, diagnostics)) => {
|
||||||
};
|
info!("received diagnostics from {}: diag({:?})", group, diagnostics.as_ref().map(|e| e.len()));
|
||||||
info!("received diagnostics from {}: diag({:?})", group, diagnostics.as_ref().map(|e| e.len()));
|
|
||||||
|
|
||||||
let with_primary = (self.affect_map.len() <= 1 && self.affect_map.contains_key("primary")) && group == "primary";
|
let with_primary = (self.affect_map.len() <= 1 && self.affect_map.contains_key("primary")) && group == "primary";
|
||||||
|
|
||||||
self.publish(group, diagnostics, with_primary).await;
|
self.publish(group, diagnostics, with_primary).await;
|
||||||
|
|
||||||
// Check with primary again after publish
|
// Check with primary again after publish
|
||||||
let again_with_primary = self.affect_map.len() == 1 && self.affect_map.contains_key("primary");
|
let again_with_primary = self.affect_map.len() == 1 && self.affect_map.contains_key("primary");
|
||||||
|
|
||||||
if !with_primary && self.published_primary != again_with_primary {
|
if !with_primary && self.published_primary != again_with_primary {
|
||||||
self.flush_primary_diagnostics(again_with_primary).await;
|
self.flush_primary_diagnostics(again_with_primary).await;
|
||||||
self.published_primary = again_with_primary;
|
self.published_primary = again_with_primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(CompileClusterRequest::Status(group, status)) => {
|
||||||
|
log::debug!("received status request");
|
||||||
|
if self.notify_compile_status {
|
||||||
|
if group != "primary" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
compile_status = status;
|
||||||
|
self.host.send_notification::<TinymistCompileStatus>(TinymistCompileStatus {
|
||||||
|
status: compile_status.clone(),
|
||||||
|
words_count: words_count.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(CompileClusterRequest::WordCount(group, wc)) => {
|
||||||
|
log::debug!("received word count request");
|
||||||
|
if self.notify_compile_status {
|
||||||
|
if group != "primary" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
words_count = wc;
|
||||||
|
self.host.send_notification::<TinymistCompileStatus>(TinymistCompileStatus {
|
||||||
|
status: compile_status.clone(),
|
||||||
|
words_count: words_count.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -136,3 +175,24 @@ impl CompileClusterActor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Notification
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub enum TinymistCompileStatusEnum {
|
||||||
|
Compiling,
|
||||||
|
CompileSuccess,
|
||||||
|
CompileError,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct TinymistCompileStatus {
|
||||||
|
pub status: TinymistCompileStatusEnum,
|
||||||
|
#[serde(rename = "wordsCount")]
|
||||||
|
pub words_count: Option<WordsCount>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl lsp_types::notification::Notification for TinymistCompileStatus {
|
||||||
|
type Params = TinymistCompileStatus;
|
||||||
|
const METHOD: &'static str = "tinymist/compileStatus";
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,14 @@ use parking_lot::Mutex;
|
||||||
use tinymist_query::{ExportKind, PageSelection};
|
use tinymist_query::{ExportKind, PageSelection};
|
||||||
use tokio::sync::{
|
use tokio::sync::{
|
||||||
broadcast::{self, error::RecvError},
|
broadcast::{self, error::RecvError},
|
||||||
oneshot, watch,
|
mpsc, oneshot, watch,
|
||||||
};
|
};
|
||||||
use typst::{foundations::Smart, layout::Frame};
|
use typst::{foundations::Smart, layout::Frame};
|
||||||
use typst_ts_core::{config::compiler::EntryState, path::PathClean, ImmutPath, TypstDocument};
|
use typst_ts_core::{config::compiler::EntryState, path::PathClean, ImmutPath, TypstDocument};
|
||||||
|
|
||||||
use crate::ExportMode;
|
use crate::{tools::word_count, ExportMode};
|
||||||
|
|
||||||
|
use super::cluster::CompileClusterRequest;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct OneshotRendering {
|
pub struct OneshotRendering {
|
||||||
|
|
@ -48,6 +50,8 @@ pub struct ExportConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ExportActor {
|
pub struct ExportActor {
|
||||||
|
group: String,
|
||||||
|
editor_tx: mpsc::UnboundedSender<CompileClusterRequest>,
|
||||||
render_rx: broadcast::Receiver<RenderActorRequest>,
|
render_rx: broadcast::Receiver<RenderActorRequest>,
|
||||||
document: watch::Receiver<Option<Arc<TypstDocument>>>,
|
document: watch::Receiver<Option<Arc<TypstDocument>>>,
|
||||||
|
|
||||||
|
|
@ -59,17 +63,22 @@ pub struct ExportActor {
|
||||||
|
|
||||||
impl ExportActor {
|
impl ExportActor {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
group: String,
|
||||||
document: watch::Receiver<Option<Arc<TypstDocument>>>,
|
document: watch::Receiver<Option<Arc<TypstDocument>>>,
|
||||||
|
editor_tx: mpsc::UnboundedSender<CompileClusterRequest>,
|
||||||
render_rx: broadcast::Receiver<RenderActorRequest>,
|
render_rx: broadcast::Receiver<RenderActorRequest>,
|
||||||
config: ExportConfig,
|
config: ExportConfig,
|
||||||
|
kind: ExportKind,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
group,
|
||||||
|
editor_tx,
|
||||||
render_rx,
|
render_rx,
|
||||||
document,
|
document,
|
||||||
substitute_pattern: config.substitute_pattern,
|
substitute_pattern: config.substitute_pattern,
|
||||||
entry: config.entry,
|
entry: config.entry,
|
||||||
mode: config.mode,
|
mode: config.mode,
|
||||||
kind: ExportKind::Pdf,
|
kind,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -282,6 +291,15 @@ impl ExportActor {
|
||||||
.encode_png()
|
.encode_png()
|
||||||
.map_err(|err| anyhow::anyhow!("failed to encode PNG ({err})"))?
|
.map_err(|err| anyhow::anyhow!("failed to encode PNG ({err})"))?
|
||||||
}
|
}
|
||||||
|
ExportKind::WordCount => {
|
||||||
|
let wc = word_count::word_count(doc);
|
||||||
|
log::debug!("word count: {wc:?}");
|
||||||
|
let _ = self.editor_tx.send(CompileClusterRequest::WordCount(
|
||||||
|
self.group.clone(),
|
||||||
|
Some(wc),
|
||||||
|
));
|
||||||
|
return Ok(PathBuf::new());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
std::fs::write(&to, data)
|
std::fs::write(&to, data)
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,10 @@ use typst_ts_core::{
|
||||||
Error, ImmutPath, TypstFont,
|
Error, ImmutPath, TypstFont,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::typ_server::CompileClient as TsCompileClient;
|
use super::{
|
||||||
|
cluster::{CompileClusterRequest, TinymistCompileStatusEnum},
|
||||||
|
typ_server::CompileClient as TsCompileClient,
|
||||||
|
};
|
||||||
use super::{render::ExportConfig, typ_server::CompileServerActor};
|
use super::{render::ExportConfig, typ_server::CompileServerActor};
|
||||||
use crate::world::LspWorld;
|
use crate::world::LspWorld;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -72,7 +75,7 @@ type CompileDriverInner = CompileDriverImpl<LspWorld>;
|
||||||
type CompileService = CompileServerActor<CompileDriver>;
|
type CompileService = CompileServerActor<CompileDriver>;
|
||||||
type CompileClient = TsCompileClient<CompileService>;
|
type CompileClient = TsCompileClient<CompileService>;
|
||||||
|
|
||||||
type DiagnosticsSender = mpsc::UnboundedSender<(String, Option<DiagnosticsMap>)>;
|
type EditorSender = mpsc::UnboundedSender<CompileClusterRequest>;
|
||||||
|
|
||||||
pub struct CompileHandler {
|
pub struct CompileHandler {
|
||||||
pub(super) diag_group: String,
|
pub(super) diag_group: String,
|
||||||
|
|
@ -82,7 +85,7 @@ pub struct CompileHandler {
|
||||||
|
|
||||||
pub(super) doc_tx: watch::Sender<Option<Arc<TypstDocument>>>,
|
pub(super) doc_tx: watch::Sender<Option<Arc<TypstDocument>>>,
|
||||||
pub(super) render_tx: broadcast::Sender<RenderActorRequest>,
|
pub(super) render_tx: broadcast::Sender<RenderActorRequest>,
|
||||||
pub(super) diag_tx: DiagnosticsSender,
|
pub(super) editor_tx: EditorSender,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CompilationHandle for CompileHandler {
|
impl CompilationHandle for CompileHandler {
|
||||||
|
|
@ -103,6 +106,17 @@ impl CompilationHandle for CompileHandler {
|
||||||
let _ = self.render_tx.send(RenderActorRequest::OnTyped);
|
let _ = self.render_tx.send(RenderActorRequest::OnTyped);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.editor_tx
|
||||||
|
.send(CompileClusterRequest::Status(
|
||||||
|
self.diag_group.clone(),
|
||||||
|
if res.is_ok() {
|
||||||
|
TinymistCompileStatusEnum::CompileSuccess
|
||||||
|
} else {
|
||||||
|
TinymistCompileStatusEnum::CompileError
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
#[cfg(feature = "preview")]
|
#[cfg(feature = "preview")]
|
||||||
{
|
{
|
||||||
let inner = self.inner.lock();
|
let inner = self.inner.lock();
|
||||||
|
|
@ -115,7 +129,10 @@ impl CompilationHandle for CompileHandler {
|
||||||
|
|
||||||
impl CompileHandler {
|
impl CompileHandler {
|
||||||
fn push_diagnostics(&mut self, diagnostics: Option<DiagnosticsMap>) {
|
fn push_diagnostics(&mut self, diagnostics: Option<DiagnosticsMap>) {
|
||||||
let err = self.diag_tx.send((self.diag_group.clone(), diagnostics));
|
let err = self.editor_tx.send(CompileClusterRequest::Diag(
|
||||||
|
self.diag_group.clone(),
|
||||||
|
diagnostics,
|
||||||
|
));
|
||||||
if let Err(err) = err {
|
if let Err(err) = err {
|
||||||
error!("failed to send diagnostics: {:#}", err);
|
error!("failed to send diagnostics: {:#}", err);
|
||||||
}
|
}
|
||||||
|
|
@ -141,6 +158,13 @@ impl CompileMiddleware for CompileDriver {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wrap_compile(&mut self, env: &mut CompileEnv) -> SourceResult<Arc<typst::model::Document>> {
|
fn wrap_compile(&mut self, env: &mut CompileEnv) -> SourceResult<Arc<typst::model::Document>> {
|
||||||
|
self.handler
|
||||||
|
.editor_tx
|
||||||
|
.send(CompileClusterRequest::Status(
|
||||||
|
self.handler.diag_group.clone(),
|
||||||
|
TinymistCompileStatusEnum::Compiling,
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
self.handler.status(CompileStatus::Compiling);
|
self.handler.status(CompileStatus::Compiling);
|
||||||
match self.inner_mut().compile(env) {
|
match self.inner_mut().compile(env) {
|
||||||
Ok(doc) => {
|
Ok(doc) => {
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,13 @@ use lsp_types::{notification::Notification as _, ExecuteCommandParams};
|
||||||
use paste::paste;
|
use paste::paste;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{Map, Value as JsonValue};
|
use serde_json::{Map, Value as JsonValue};
|
||||||
use tinymist_query::{DiagnosticsMap, ExportKind, PageSelection};
|
use tinymist_query::{ExportKind, PageSelection};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use typst::util::Deferred;
|
use typst::util::Deferred;
|
||||||
use typst_ts_core::ImmutPath;
|
use typst_ts_core::ImmutPath;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actor::{render::ExportConfig, typ_client::CompileClientActor},
|
actor::{cluster::CompileClusterRequest, render::ExportConfig, typ_client::CompileClientActor},
|
||||||
compiler_init::{CompileConfig, CompilerConstConfig},
|
compiler_init::{CompileConfig, CompilerConstConfig},
|
||||||
harness::InitializedLspDriver,
|
harness::InitializedLspDriver,
|
||||||
internal_error, invalid_params, method_not_found, run_query,
|
internal_error, invalid_params, method_not_found, run_query,
|
||||||
|
|
@ -72,7 +72,7 @@ pub struct CompileServerArgs {
|
||||||
pub client: LspHost<CompileServer>,
|
pub client: LspHost<CompileServer>,
|
||||||
pub compile_config: CompileConfig,
|
pub compile_config: CompileConfig,
|
||||||
pub const_config: CompilerConstConfig,
|
pub const_config: CompilerConstConfig,
|
||||||
pub diag_tx: mpsc::UnboundedSender<(String, Option<DiagnosticsMap>)>,
|
pub diag_tx: mpsc::UnboundedSender<CompileClusterRequest>,
|
||||||
pub font: Deferred<SharedFontResolver>,
|
pub font: Deferred<SharedFontResolver>,
|
||||||
pub handle: tokio::runtime::Handle,
|
pub handle: tokio::runtime::Handle,
|
||||||
}
|
}
|
||||||
|
|
@ -109,7 +109,7 @@ pub struct CompileServer {
|
||||||
/// Source synchronized with client
|
/// Source synchronized with client
|
||||||
pub memory_changes: HashMap<Arc<Path>, MemoryFileMeta>,
|
pub memory_changes: HashMap<Arc<Path>, MemoryFileMeta>,
|
||||||
/// The diagnostics sender to send diagnostics to `crate::actor::cluster`.
|
/// The diagnostics sender to send diagnostics to `crate::actor::cluster`.
|
||||||
pub diag_tx: mpsc::UnboundedSender<(String, Option<DiagnosticsMap>)>,
|
pub diag_tx: mpsc::UnboundedSender<CompileClusterRequest>,
|
||||||
/// The compiler actor.
|
/// The compiler actor.
|
||||||
pub compiler: Option<CompileClientActor>,
|
pub compiler: Option<CompileClientActor>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use comemo::Prehashed;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::{Map, Value as JsonValue};
|
use serde_json::{Map, Value as JsonValue};
|
||||||
use tinymist_query::{DiagnosticsMap, PositionEncoding};
|
use tinymist_query::PositionEncoding;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use typst::foundations::IntoValue;
|
use typst::foundations::IntoValue;
|
||||||
use typst::syntax::FileId;
|
use typst::syntax::FileId;
|
||||||
|
|
@ -17,6 +17,7 @@ use typst::util::Deferred;
|
||||||
use typst_ts_core::config::compiler::EntryState;
|
use typst_ts_core::config::compiler::EntryState;
|
||||||
use typst_ts_core::{ImmutPath, TypstDict};
|
use typst_ts_core::{ImmutPath, TypstDict};
|
||||||
|
|
||||||
|
use crate::actor::cluster::CompileClusterRequest;
|
||||||
use crate::compiler::{CompileServer, CompileServerArgs};
|
use crate::compiler::{CompileServer, CompileServerArgs};
|
||||||
use crate::harness::LspDriver;
|
use crate::harness::LspDriver;
|
||||||
use crate::world::{ImmutDict, SharedFontResolver};
|
use crate::world::{ImmutDict, SharedFontResolver};
|
||||||
|
|
@ -93,6 +94,8 @@ pub struct CompileConfig {
|
||||||
pub export_pdf: ExportMode,
|
pub export_pdf: ExportMode,
|
||||||
/// Specifies the root path of the project manually.
|
/// Specifies the root path of the project manually.
|
||||||
pub root_path: Option<PathBuf>,
|
pub root_path: Option<PathBuf>,
|
||||||
|
/// Specifies the root path of the project manually.
|
||||||
|
pub notify_compile_status: bool,
|
||||||
/// Typst extra arguments.
|
/// Typst extra arguments.
|
||||||
pub typst_extra_args: Option<CompileExtraOpts>,
|
pub typst_extra_args: Option<CompileExtraOpts>,
|
||||||
pub has_default_entry_path: bool,
|
pub has_default_entry_path: bool,
|
||||||
|
|
@ -144,6 +147,14 @@ impl CompileConfig {
|
||||||
self.root_path = None;
|
self.root_path = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let compile_status = update.get("compileStatus").and_then(|x| x.as_str());
|
||||||
|
if let Some(word_count) = compile_status {
|
||||||
|
if !matches!(word_count, "enable" | "disable") {
|
||||||
|
bail!("compileStatus must be either 'enable' or 'disable'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.notify_compile_status = compile_status.map_or(false, |e| e != "disable");
|
||||||
|
|
||||||
'parse_extra_args: {
|
'parse_extra_args: {
|
||||||
if let Some(typst_extra_args) = update.get("typstExtraArgs") {
|
if let Some(typst_extra_args) = update.get("typstExtraArgs") {
|
||||||
let typst_args: Vec<String> = match serde_json::from_value(typst_extra_args.clone())
|
let typst_args: Vec<String> = match serde_json::from_value(typst_extra_args.clone())
|
||||||
|
|
@ -330,7 +341,7 @@ impl Default for CompilerConstConfig {
|
||||||
pub struct CompileInit {
|
pub struct CompileInit {
|
||||||
pub handle: tokio::runtime::Handle,
|
pub handle: tokio::runtime::Handle,
|
||||||
pub font: CompileFontOpts,
|
pub font: CompileFontOpts,
|
||||||
pub diag_tx: mpsc::UnboundedSender<(String, Option<DiagnosticsMap>)>,
|
pub diag_tx: mpsc::UnboundedSender<CompileClusterRequest>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{Map, Value as JsonValue};
|
use serde_json::{Map, Value as JsonValue};
|
||||||
use tinymist_query::{
|
use tinymist_query::{
|
||||||
get_semantic_tokens_options, get_semantic_tokens_registration,
|
get_semantic_tokens_options, get_semantic_tokens_registration,
|
||||||
get_semantic_tokens_unregistration, DiagnosticsMap, ExportKind, PageSelection,
|
get_semantic_tokens_unregistration, ExportKind, PageSelection, SemanticTokenContext,
|
||||||
SemanticTokenContext,
|
|
||||||
};
|
};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use typst::diag::StrResult;
|
use typst::diag::StrResult;
|
||||||
|
|
@ -33,6 +32,7 @@ use typst_ts_core::path::PathClean;
|
||||||
use typst_ts_core::{error::prelude::*, ImmutPath};
|
use typst_ts_core::{error::prelude::*, ImmutPath};
|
||||||
|
|
||||||
use super::lsp_init::*;
|
use super::lsp_init::*;
|
||||||
|
use crate::actor::cluster::CompileClusterRequest;
|
||||||
use crate::actor::typ_client::CompileClientActor;
|
use crate::actor::typ_client::CompileClientActor;
|
||||||
use crate::actor::{FormattingConfig, FormattingRequest};
|
use crate::actor::{FormattingConfig, FormattingRequest};
|
||||||
use crate::compiler::{CompileServer, CompileServerArgs};
|
use crate::compiler::{CompileServer, CompileServerArgs};
|
||||||
|
|
@ -152,7 +152,7 @@ fn as_path_pos(inp: TextDocumentPositionParams) -> (PathBuf, Position) {
|
||||||
pub struct TypstLanguageServerArgs {
|
pub struct TypstLanguageServerArgs {
|
||||||
pub client: LspHost<TypstLanguageServer>,
|
pub client: LspHost<TypstLanguageServer>,
|
||||||
pub const_config: ConstConfig,
|
pub const_config: ConstConfig,
|
||||||
pub diag_tx: mpsc::UnboundedSender<(String, Option<DiagnosticsMap>)>,
|
pub diag_tx: mpsc::UnboundedSender<CompileClusterRequest>,
|
||||||
pub font: Deferred<SharedFontResolver>,
|
pub font: Deferred<SharedFontResolver>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ use typst::util::Deferred;
|
||||||
use typst_ts_core::error::prelude::*;
|
use typst_ts_core::error::prelude::*;
|
||||||
use typst_ts_core::ImmutPath;
|
use typst_ts_core::ImmutPath;
|
||||||
|
|
||||||
use crate::actor::cluster::CompileClusterActor;
|
use crate::actor::cluster::EditorActor;
|
||||||
use crate::compiler_init::CompileConfig;
|
use crate::compiler_init::CompileConfig;
|
||||||
use crate::harness::LspHost;
|
use crate::harness::LspHost;
|
||||||
use crate::world::{ImmutDict, SharedFontResolver};
|
use crate::world::{ImmutDict, SharedFontResolver};
|
||||||
|
|
@ -88,11 +88,14 @@ const CONFIG_ITEMS: &[&str] = &[
|
||||||
"semanticTokens",
|
"semanticTokens",
|
||||||
"formatterMode",
|
"formatterMode",
|
||||||
"typstExtraArgs",
|
"typstExtraArgs",
|
||||||
|
"compileStatus",
|
||||||
];
|
];
|
||||||
|
|
||||||
/// The user configuration read from the editor.
|
/// The user configuration read from the editor.
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
/// Specifies the root path of the project manually.
|
||||||
|
pub notify_compile_status: bool,
|
||||||
/// The compile configurations
|
/// The compile configurations
|
||||||
pub compile: CompileConfig,
|
pub compile: CompileConfig,
|
||||||
/// Dynamic configuration for semantic tokens.
|
/// Dynamic configuration for semantic tokens.
|
||||||
|
|
@ -357,12 +360,13 @@ impl Init {
|
||||||
|
|
||||||
service.run_format_thread();
|
service.run_format_thread();
|
||||||
|
|
||||||
let cluster_actor = CompileClusterActor {
|
let cluster_actor = EditorActor {
|
||||||
host: self.host.clone(),
|
host: self.host.clone(),
|
||||||
diag_rx,
|
diag_rx,
|
||||||
diagnostics: HashMap::new(),
|
diagnostics: HashMap::new(),
|
||||||
affect_map: HashMap::new(),
|
affect_map: HashMap::new(),
|
||||||
published_primary: false,
|
published_primary: false,
|
||||||
|
notify_compile_status: service.config.compile.notify_compile_status,
|
||||||
};
|
};
|
||||||
|
|
||||||
let fallback = service.config.compile.determine_default_entry_path();
|
let fallback = service.config.compile.determine_default_entry_path();
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
pub mod package;
|
pub mod package;
|
||||||
pub mod preview;
|
pub mod preview;
|
||||||
|
pub mod word_count;
|
||||||
|
|
|
||||||
302
crates/tinymist/src/tools/word_count.rs
Normal file
302
crates/tinymist/src/tools/word_count.rs
Normal file
|
|
@ -0,0 +1,302 @@
|
||||||
|
use std::io::{self, Write};
|
||||||
|
use std::ops::Range;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use typst::{model::Document, syntax::Span, text::TextItem};
|
||||||
|
use typst_ts_core::{debug_loc::SourceSpanOffset, exporter_utils::map_err};
|
||||||
|
use unicode_script::{Script, UnicodeScript};
|
||||||
|
|
||||||
|
/// Words count for a document.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct WordsCount {
|
||||||
|
/// Number of words.
|
||||||
|
pub words: usize,
|
||||||
|
/// Number of characters.
|
||||||
|
pub chars: usize,
|
||||||
|
/// Number of spaces.
|
||||||
|
/// Multiple consecutive spaces are counted as one.
|
||||||
|
pub spaces: usize,
|
||||||
|
/// Number of CJK characters.
|
||||||
|
#[serde(rename = "cjkChars")]
|
||||||
|
pub cjk_chars: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Count words in a document.
|
||||||
|
pub fn word_count(doc: &Document) -> WordsCount {
|
||||||
|
// the mapping is still not use, so we prevent the warning here
|
||||||
|
let _ = TextContent::map_back_spans;
|
||||||
|
|
||||||
|
let mut words = 0;
|
||||||
|
let mut chars = 0;
|
||||||
|
let mut cjk_chars = 0;
|
||||||
|
let mut spaces = 0;
|
||||||
|
|
||||||
|
// First, get text representation of the document.
|
||||||
|
let w = TextExporter::default();
|
||||||
|
let content = w.collect(doc).unwrap();
|
||||||
|
|
||||||
|
/// A automaton to count words.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum CountState {
|
||||||
|
/// Waiting for a word. (Default state)
|
||||||
|
InSpace,
|
||||||
|
/// At a word.
|
||||||
|
InNonCJK,
|
||||||
|
/// At a CJK character.
|
||||||
|
InCJK,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_cjk(c: char) -> bool {
|
||||||
|
matches!(
|
||||||
|
c.script(),
|
||||||
|
Script::Han | Script::Hiragana | Script::Katakana | Script::Hangul
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut state = CountState::InSpace;
|
||||||
|
for c in content.chars() {
|
||||||
|
chars += 1;
|
||||||
|
|
||||||
|
if c.is_whitespace() {
|
||||||
|
if state != CountState::InSpace {
|
||||||
|
spaces += 1;
|
||||||
|
}
|
||||||
|
state = CountState::InSpace;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check unicode script to see if it's a CJK character.
|
||||||
|
if is_cjk(c) {
|
||||||
|
words += 1;
|
||||||
|
cjk_chars += 1;
|
||||||
|
|
||||||
|
state = CountState::InCJK;
|
||||||
|
} else {
|
||||||
|
if state != CountState::InNonCJK {
|
||||||
|
words += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
state = CountState::InNonCJK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WordsCount {
|
||||||
|
words,
|
||||||
|
chars,
|
||||||
|
spaces,
|
||||||
|
cjk_chars,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Export text content from a document.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct TextExporter {}
|
||||||
|
|
||||||
|
impl TextExporter {
|
||||||
|
pub fn collect(&self, output: &Document) -> typst::diag::SourceResult<String> {
|
||||||
|
let w = std::io::BufWriter::new(Vec::new());
|
||||||
|
|
||||||
|
let mut d = TextExportWorker { w };
|
||||||
|
d.doc(output).map_err(map_err)?;
|
||||||
|
|
||||||
|
d.w.flush().unwrap();
|
||||||
|
Ok(String::from_utf8(d.w.into_inner().unwrap()).unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TextExportWorker {
|
||||||
|
w: std::io::BufWriter<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextExportWorker {
|
||||||
|
fn doc(&mut self, doc: &Document) -> io::Result<()> {
|
||||||
|
for page in doc.pages.iter() {
|
||||||
|
self.frame(&page.frame)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn frame(&mut self, doc: &typst::layout::Frame) -> io::Result<()> {
|
||||||
|
for (_, item) in doc.items() {
|
||||||
|
self.item(item)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn item(&mut self, item: &typst::layout::FrameItem) -> io::Result<()> {
|
||||||
|
use typst::introspection::Meta::*;
|
||||||
|
use typst::layout::FrameItem::*;
|
||||||
|
match item {
|
||||||
|
Group(g) => self.frame(&g.frame),
|
||||||
|
Text(t) => {
|
||||||
|
write!(self.w, "{}", t.text.as_str())
|
||||||
|
}
|
||||||
|
// Meta(ContentHint(c), _) => f.write_char(*c),
|
||||||
|
Meta(Link(..), _) | Shape(..) | Image(..) => self.w.write_all(b"object"),
|
||||||
|
Meta(Elem(..) | Hide, _) => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given a text range, map it back to the original document.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MappedSpan {
|
||||||
|
/// The start span.
|
||||||
|
pub span: SourceSpanOffset,
|
||||||
|
/// The end span.
|
||||||
|
pub span_end: Option<SourceSpanOffset>,
|
||||||
|
/// Whether a text range is completely covered by [`Self::span`] and
|
||||||
|
/// [`Self::span_end`].
|
||||||
|
pub completed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Annotated content for a font.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TextContent {
|
||||||
|
/// A string of the content for slicing.
|
||||||
|
pub content: String,
|
||||||
|
/// annotating document.
|
||||||
|
pub doc: Arc<Document>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextContent {
|
||||||
|
/// Map text ranges (with byte offsets) back to the original document in
|
||||||
|
/// batch.
|
||||||
|
pub fn map_back_spans(
|
||||||
|
&self,
|
||||||
|
mut spans: Vec<std::ops::Range<usize>>,
|
||||||
|
) -> Vec<Option<MappedSpan>> {
|
||||||
|
// Sort for scanning
|
||||||
|
spans.sort_by_key(|r| r.start);
|
||||||
|
|
||||||
|
// Scan the document recursively to map back the spans.
|
||||||
|
let mut mapper = SpanMapper::default();
|
||||||
|
mapper.doc(&self.doc);
|
||||||
|
|
||||||
|
// Align result with the input to prevent bad scanning.
|
||||||
|
let mut offsets = mapper.span_offset;
|
||||||
|
while spans.len() < offsets.len() {
|
||||||
|
offsets.pop();
|
||||||
|
}
|
||||||
|
while spans.len() > offsets.len() {
|
||||||
|
offsets.push(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
offsets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
struct SpanMapper {
|
||||||
|
offset: usize,
|
||||||
|
spans_to_map: Vec<std::ops::Range<usize>>,
|
||||||
|
span_offset: Vec<Option<MappedSpan>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SpanMapper {
|
||||||
|
fn doc(&mut self, doc: &Document) {
|
||||||
|
for page in doc.pages.iter() {
|
||||||
|
self.frame(&page.frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn frame(&mut self, doc: &typst::layout::Frame) {
|
||||||
|
for (_, item) in doc.items() {
|
||||||
|
self.item(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn item(&mut self, item: &typst::layout::FrameItem) {
|
||||||
|
use typst::introspection::Meta::*;
|
||||||
|
use typst::layout::FrameItem::*;
|
||||||
|
match item {
|
||||||
|
Group(g) => self.frame(&g.frame),
|
||||||
|
Text(t) => {
|
||||||
|
self.check(t.text.as_str(), Some(t));
|
||||||
|
}
|
||||||
|
Meta(Link(..), _) | Shape(..) | Image(..) => {
|
||||||
|
self.check("object", None);
|
||||||
|
}
|
||||||
|
Meta(Elem(..) | Hide, _) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check(&mut self, text: &str, src: Option<&TextItem>) {
|
||||||
|
if let Some(src) = src {
|
||||||
|
self.do_check(src);
|
||||||
|
}
|
||||||
|
self.offset += text.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_check(&mut self, text: &TextItem) -> Option<()> {
|
||||||
|
let beg = self.offset;
|
||||||
|
let end = beg + text.text.len();
|
||||||
|
loop {
|
||||||
|
let so = self.span_offset.len();
|
||||||
|
let to_check = self.spans_to_map.get(so)?;
|
||||||
|
if to_check.start >= end {
|
||||||
|
return Some(());
|
||||||
|
}
|
||||||
|
if to_check.end <= beg {
|
||||||
|
self.span_offset.push(None);
|
||||||
|
log::info!("span out of range {to_check:?}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// todo: don't swallow the span
|
||||||
|
if to_check.start < beg {
|
||||||
|
self.span_offset.push(None);
|
||||||
|
log::info!("span skipped {to_check:?}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("span checking {to_check:?} in {text:?}");
|
||||||
|
let inner = to_check.start - beg;
|
||||||
|
self.span_offset
|
||||||
|
.push(self.check_text_inner(text, inner..inner + (to_check.len())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_text_inner(&self, text: &TextItem, rng: std::ops::Range<usize>) -> Option<MappedSpan> {
|
||||||
|
let glyphs = text
|
||||||
|
.glyphs
|
||||||
|
.iter()
|
||||||
|
.filter(|g| rng.contains(&g.range().start));
|
||||||
|
let mut min_span: Option<(Range<usize>, (Span, u16))> = None;
|
||||||
|
let mut max_span: Option<(Range<usize>, (Span, u16))> = None;
|
||||||
|
let mut found = vec![];
|
||||||
|
for glyph in glyphs {
|
||||||
|
found.push(glyph.range());
|
||||||
|
if let Some((mii, s)) = min_span.as_ref() {
|
||||||
|
if glyph.range().start < mii.start && !s.0.is_detached() {
|
||||||
|
// min_span = Some(glyph.range());
|
||||||
|
min_span = Some((glyph.range(), glyph.span));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// min_span = Some(glyph.range());
|
||||||
|
min_span = Some((glyph.range(), glyph.span));
|
||||||
|
}
|
||||||
|
if let Some((mai, s)) = max_span.as_ref() {
|
||||||
|
if glyph.range().end > mai.end && !s.0.is_detached() {
|
||||||
|
// max_span = Some(glyph.range());
|
||||||
|
max_span = Some((glyph.range(), glyph.span));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// max_span = Some(glyph.range());
|
||||||
|
max_span = Some((glyph.range(), glyph.span));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
found.sort_by(|a, b| a.start.cmp(&b.start).then_with(|| a.end.cmp(&b.end)));
|
||||||
|
let completed = !found.is_empty()
|
||||||
|
&& found[0].start <= rng.start
|
||||||
|
&& found[found.len() - 1].end >= rng.end;
|
||||||
|
let span = min_span?.1 .0;
|
||||||
|
let span_end = max_span.map(|m| m.1 .0);
|
||||||
|
Some(MappedSpan {
|
||||||
|
span: SourceSpanOffset::from(span),
|
||||||
|
span_end: span_end.map(SourceSpanOffset::from),
|
||||||
|
completed,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -91,6 +91,16 @@
|
||||||
],
|
],
|
||||||
"default": null
|
"default": null
|
||||||
},
|
},
|
||||||
|
"tinymist.compileStatus": {
|
||||||
|
"title": "Show/Report compilation status",
|
||||||
|
"description": "In VSCode, enable compile status meaning that the extension will show the compilation status in the status bar. Since neovim and helix don't have a such feature, it is disabled by default at the language server lebel.",
|
||||||
|
"type": "string",
|
||||||
|
"default": "enable",
|
||||||
|
"enum": [
|
||||||
|
"enable",
|
||||||
|
"disable"
|
||||||
|
]
|
||||||
|
},
|
||||||
"tinymist.typstExtraArgs": {
|
"tinymist.typstExtraArgs": {
|
||||||
"title": "Specifies the arguments for Typst as same as typst-cli",
|
"title": "Specifies the arguments for Typst as same as typst-cli",
|
||||||
"description": "You can pass any arguments as you like, and we will try to follow behaviors of the **same version** of typst-cli. Note: the arguments may be overridden by other settings. For example, `--font-path` will be overridden by `tinymist.fontPaths`.",
|
"description": "You can pass any arguments as you like, and we will try to follow behaviors of the **same version** of typst-cli. Note: the arguments may be overridden by other settings. For example, `--font-path` will be overridden by `tinymist.fontPaths`.",
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import {
|
||||||
commands,
|
commands,
|
||||||
ViewColumn,
|
ViewColumn,
|
||||||
Uri,
|
Uri,
|
||||||
WorkspaceConfiguration,
|
|
||||||
TextEditor,
|
TextEditor,
|
||||||
ExtensionMode,
|
ExtensionMode,
|
||||||
} from "vscode";
|
} from "vscode";
|
||||||
|
|
@ -20,6 +19,7 @@ import {
|
||||||
} from "vscode-languageclient/node";
|
} from "vscode-languageclient/node";
|
||||||
import vscodeVariables from "vscode-variables";
|
import vscodeVariables from "vscode-variables";
|
||||||
import { activateEditorTool, getUserPackageData } from "./editor-tools";
|
import { activateEditorTool, getUserPackageData } from "./editor-tools";
|
||||||
|
import { triggerStatusBar, wordCountItemProcess } from "./ui-extends";
|
||||||
|
|
||||||
let client: LanguageClient | undefined = undefined;
|
let client: LanguageClient | undefined = undefined;
|
||||||
|
|
||||||
|
|
@ -94,6 +94,10 @@ async function startClient(context: ExtensionContext): Promise<void> {
|
||||||
clientOptions
|
clientOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
|
client.onNotification("tinymist/compileStatus", (params) => {
|
||||||
|
wordCountItemProcess(params);
|
||||||
|
});
|
||||||
|
|
||||||
window.onDidChangeActiveTextEditor((editor: TextEditor | undefined) => {
|
window.onDidChangeActiveTextEditor((editor: TextEditor | undefined) => {
|
||||||
if (editor?.document.isUntitled) {
|
if (editor?.document.isUntitled) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -171,8 +175,29 @@ async function startClient(context: ExtensionContext): Promise<void> {
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
commands.registerCommand("tinymist.traceCurrentFile", () => commandShowTrace(context))
|
commands.registerCommand("tinymist.traceCurrentFile", () => commandShowTrace(context))
|
||||||
);
|
);
|
||||||
|
context.subscriptions.push(
|
||||||
|
commands.registerCommand("tinymist.showLog", () => {
|
||||||
|
if (client) {
|
||||||
|
client.outputChannel.show();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return client.start();
|
await client.start();
|
||||||
|
|
||||||
|
// Find first document to focus
|
||||||
|
const editor = window.activeTextEditor;
|
||||||
|
if (editor?.document.languageId === "typst" && editor.document.uri.fsPath) {
|
||||||
|
commandActivateDoc(editor.document.uri.fsPath);
|
||||||
|
} else {
|
||||||
|
window.visibleTextEditors.forEach((editor) => {
|
||||||
|
if (editor.document.languageId === "typst" && editor.document.uri.fsPath) {
|
||||||
|
commandActivateDoc(editor.document.uri.fsPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deactivate(): Promise<void> | undefined {
|
export function deactivate(): Promise<void> | undefined {
|
||||||
|
|
@ -492,7 +517,17 @@ async function commandInitTemplate(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let focusingFile: string | undefined = undefined;
|
||||||
|
export function getFocusingFile() {
|
||||||
|
return focusingFile;
|
||||||
|
}
|
||||||
|
|
||||||
async function commandActivateDoc(fsPath: string | undefined): Promise<void> {
|
async function commandActivateDoc(fsPath: string | undefined): Promise<void> {
|
||||||
|
// console.log("focus main", fsPath, new Error().stack);
|
||||||
|
focusingFile = fsPath;
|
||||||
|
if (!fsPath) {
|
||||||
|
triggerStatusBar();
|
||||||
|
}
|
||||||
await client?.sendRequest("workspace/executeCommand", {
|
await client?.sendRequest("workspace/executeCommand", {
|
||||||
command: "tinymist.focusMain",
|
command: "tinymist.focusMain",
|
||||||
arguments: [fsPath],
|
arguments: [fsPath],
|
||||||
|
|
|
||||||
96
editors/vscode/src/ui-extends.ts
Normal file
96
editors/vscode/src/ui-extends.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
import * as vscode from "vscode";
|
||||||
|
import { getFocusingFile } from "./extension";
|
||||||
|
|
||||||
|
let statusBarItem: vscode.StatusBarItem;
|
||||||
|
|
||||||
|
function initWordCountItem() {
|
||||||
|
statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 1);
|
||||||
|
statusBarItem.name = "Tinymist Status";
|
||||||
|
statusBarItem.command = "tinymist.showLog";
|
||||||
|
return statusBarItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
let words = 0;
|
||||||
|
let chars = 0;
|
||||||
|
let spaces = 0;
|
||||||
|
let cjkChars = 0;
|
||||||
|
|
||||||
|
interface WordsCount {
|
||||||
|
words: number;
|
||||||
|
chars: number;
|
||||||
|
spaces: number;
|
||||||
|
cjkChars: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TinymistStatus {
|
||||||
|
status: "compiling" | "compileSuccess" | "compileError";
|
||||||
|
wordsCount: WordsCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const triggerStatusBar = () => {
|
||||||
|
if (getFocusingFile()) {
|
||||||
|
statusBarItem.show();
|
||||||
|
} else {
|
||||||
|
statusBarItem.hide();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function wordCountItemProcess(event: TinymistStatus) {
|
||||||
|
statusBarItem = statusBarItem || initWordCountItem();
|
||||||
|
|
||||||
|
const updateTooltip = () => {
|
||||||
|
statusBarItem.tooltip = `${words} ${plural("Word", words)}
|
||||||
|
${chars} ${plural("Character", chars)}
|
||||||
|
${spaces} ${plural("Space", spaces)}
|
||||||
|
${cjkChars} CJK ${plural("Character", cjkChars)}
|
||||||
|
[Click to show logs]`;
|
||||||
|
};
|
||||||
|
|
||||||
|
words = event.wordsCount?.words || 0;
|
||||||
|
chars = event.wordsCount?.chars || 0;
|
||||||
|
spaces = event.wordsCount?.spaces || 0;
|
||||||
|
cjkChars = event.wordsCount?.cjkChars || 0;
|
||||||
|
|
||||||
|
const style: string = "errorStatus";
|
||||||
|
if (statusBarItem) {
|
||||||
|
if (event.status === "compiling") {
|
||||||
|
if (style === "compact") {
|
||||||
|
statusBarItem.text = "$(sync~spin)";
|
||||||
|
} else if (style === "errorStatus") {
|
||||||
|
statusBarItem.text = `$(sync~spin) ${words} ${plural("Word", words)}`;
|
||||||
|
}
|
||||||
|
statusBarItem.backgroundColor = new vscode.ThemeColor(
|
||||||
|
"statusBarItem.prominentBackground"
|
||||||
|
);
|
||||||
|
updateTooltip();
|
||||||
|
triggerStatusBar();
|
||||||
|
} else if (event.status === "compileSuccess") {
|
||||||
|
if (style === "compact") {
|
||||||
|
statusBarItem.text = "$(typst-guy)";
|
||||||
|
} else if (style === "errorStatus") {
|
||||||
|
statusBarItem.text = `$(sync) ${words} ${plural("Word", words)}`;
|
||||||
|
}
|
||||||
|
statusBarItem.backgroundColor = new vscode.ThemeColor(
|
||||||
|
"statusBarItem.prominentBackground"
|
||||||
|
);
|
||||||
|
updateTooltip();
|
||||||
|
triggerStatusBar();
|
||||||
|
} else if (event.status === "compileError") {
|
||||||
|
if (style === "compact") {
|
||||||
|
statusBarItem.text = "$(typst-guy)";
|
||||||
|
} else if (style === "errorStatus") {
|
||||||
|
statusBarItem.text = `$(sync) ${words} ${plural("Word", words)}`;
|
||||||
|
}
|
||||||
|
statusBarItem.backgroundColor = new vscode.ThemeColor("statusBarItem.errorBackground");
|
||||||
|
updateTooltip();
|
||||||
|
triggerStatusBar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function plural(w: string, words: number): string {
|
||||||
|
if (words <= 1) {
|
||||||
|
return w;
|
||||||
|
} else {
|
||||||
|
return w + "s";
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue