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:
Myriad-Dreamin 2024-04-05 13:16:24 +08:00 committed by GitHub
parent 454127e354
commit 6722b2501f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 633 additions and 47 deletions

1
Cargo.lock generated
View file

@ -3734,6 +3734,7 @@ dependencies = [
"typst-ts-svg-exporter",
"typstfmt_lib",
"typstyle",
"unicode-script",
"vergen",
"walkdir",
]

View file

@ -128,6 +128,7 @@ mod polymorphic {
#[derive(Debug, Clone)]
pub enum ExportKind {
Pdf,
WordCount,
Svg { page: PageSelection },
Png { page: PageSelection },
}
@ -136,6 +137,7 @@ mod polymorphic {
pub fn extension(&self) -> &str {
match self {
Self::Pdf => "pdf",
Self::WordCount => "txt",
Self::Svg { .. } => "svg",
Self::Png { .. } => "png",
}

View file

@ -61,6 +61,7 @@ crossbeam-channel.workspace = true
lsp-types.workspace = true
dhat = { version = "0.3.3", optional = true }
chrono = { version = "0.4" }
unicode-script = "0.5"
[features]
default = ["cli", "preview"]

View file

@ -6,6 +6,7 @@ pub mod render;
pub mod typ_client;
pub mod typ_server;
use tinymist_query::ExportKind;
use tokio::sync::{broadcast, watch};
use typst::util::Deferred;
use typst_ts_compiler::{
@ -23,7 +24,7 @@ use self::{
use crate::{
compiler::CompileServer,
world::{ImmutDict, LspWorld, LspWorldBuilder},
TypstLanguageServer,
ExportMode, TypstLanguageServer,
};
pub use formatting::{FormattingConfig, FormattingRequest};
@ -33,26 +34,46 @@ type CompileDriverInner = CompileDriverImpl<LspWorld>;
impl CompileServer {
pub fn server(
&self,
diag_group: String,
editor_group: String,
entry: EntryState,
inputs: ImmutDict,
) -> CompileClientActor {
let (doc_tx, doc_rx) = watch::channel(None);
let (render_tx, _) = broadcast::channel(10);
// Run the Export actor before preparing cluster to avoid loss of events
self.handle.spawn(
ExportActor::new(
doc_rx.clone(),
render_tx.subscribe(),
ExportConfig {
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(
ExportActor::new(
editor_group.clone(),
doc_rx.clone(),
self.diag_tx.clone(),
render_tx.subscribe(),
config.clone(),
ExportKind::Pdf,
)
.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
let snapshot = FileChangeSet::default();
@ -63,14 +84,14 @@ impl CompileServer {
let handler = CompileHandler {
#[cfg(feature = "preview")]
inner: std::sync::Arc::new(parking_lot::Mutex::new(None)),
diag_group: diag_group.clone(),
diag_group: editor_group.clone(),
doc_tx,
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 diag_group = diag_group.clone();
let diag_group = editor_group.clone();
let entry = entry.clone();
let font_resolver = self.font.clone();
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)
}
}

View file

@ -7,25 +7,33 @@ use lsp_types::Url;
use tinymist_query::{DiagnosticsMap, LspDiagnostic};
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 diag_rx: mpsc::UnboundedReceiver<(String, Option<DiagnosticsMap>)>,
pub diag_rx: mpsc::UnboundedReceiver<CompileClusterRequest>,
pub diagnostics: HashMap<Url, HashMap<String, Vec<LspDiagnostic>>>,
pub affect_map: HashMap<String, Vec<Url>>,
pub published_primary: bool,
pub notify_compile_status: bool,
}
impl CompileClusterActor {
impl EditorActor {
pub async fn run(mut self) {
let mut compile_status = TinymistCompileStatusEnum::Compiling;
let mut words_count = None;
loop {
tokio::select! {
e = self.diag_rx.recv() => {
let Some((group, diagnostics)) = e else {
break;
};
match e {
Some(CompileClusterRequest::Diag(group, diagnostics)) => {
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";
@ -40,6 +48,37 @@ impl CompileClusterActor {
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";
}

View file

@ -12,12 +12,14 @@ use parking_lot::Mutex;
use tinymist_query::{ExportKind, PageSelection};
use tokio::sync::{
broadcast::{self, error::RecvError},
oneshot, watch,
mpsc, oneshot, watch,
};
use typst::{foundations::Smart, layout::Frame};
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)]
pub struct OneshotRendering {
@ -48,6 +50,8 @@ pub struct ExportConfig {
}
pub struct ExportActor {
group: String,
editor_tx: mpsc::UnboundedSender<CompileClusterRequest>,
render_rx: broadcast::Receiver<RenderActorRequest>,
document: watch::Receiver<Option<Arc<TypstDocument>>>,
@ -59,17 +63,22 @@ pub struct ExportActor {
impl ExportActor {
pub fn new(
group: String,
document: watch::Receiver<Option<Arc<TypstDocument>>>,
editor_tx: mpsc::UnboundedSender<CompileClusterRequest>,
render_rx: broadcast::Receiver<RenderActorRequest>,
config: ExportConfig,
kind: ExportKind,
) -> Self {
Self {
group,
editor_tx,
render_rx,
document,
substitute_pattern: config.substitute_pattern,
entry: config.entry,
mode: config.mode,
kind: ExportKind::Pdf,
kind,
}
}
@ -282,6 +291,15 @@ impl ExportActor {
.encode_png()
.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)

View file

@ -55,7 +55,10 @@ use typst_ts_core::{
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 crate::world::LspWorld;
use crate::{
@ -72,7 +75,7 @@ type CompileDriverInner = CompileDriverImpl<LspWorld>;
type CompileService = CompileServerActor<CompileDriver>;
type CompileClient = TsCompileClient<CompileService>;
type DiagnosticsSender = mpsc::UnboundedSender<(String, Option<DiagnosticsMap>)>;
type EditorSender = mpsc::UnboundedSender<CompileClusterRequest>;
pub struct CompileHandler {
pub(super) diag_group: String,
@ -82,7 +85,7 @@ pub struct CompileHandler {
pub(super) doc_tx: watch::Sender<Option<Arc<TypstDocument>>>,
pub(super) render_tx: broadcast::Sender<RenderActorRequest>,
pub(super) diag_tx: DiagnosticsSender,
pub(super) editor_tx: EditorSender,
}
impl CompilationHandle for CompileHandler {
@ -103,6 +106,17 @@ impl CompilationHandle for CompileHandler {
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")]
{
let inner = self.inner.lock();
@ -115,7 +129,10 @@ impl CompilationHandle for CompileHandler {
impl CompileHandler {
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 {
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>> {
self.handler
.editor_tx
.send(CompileClusterRequest::Status(
self.handler.diag_group.clone(),
TinymistCompileStatusEnum::Compiling,
))
.unwrap();
self.handler.status(CompileStatus::Compiling);
match self.inner_mut().compile(env) {
Ok(doc) => {

View file

@ -8,13 +8,13 @@ use lsp_types::{notification::Notification as _, ExecuteCommandParams};
use paste::paste;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value as JsonValue};
use tinymist_query::{DiagnosticsMap, ExportKind, PageSelection};
use tinymist_query::{ExportKind, PageSelection};
use tokio::sync::mpsc;
use typst::util::Deferred;
use typst_ts_core::ImmutPath;
use crate::{
actor::{render::ExportConfig, typ_client::CompileClientActor},
actor::{cluster::CompileClusterRequest, render::ExportConfig, typ_client::CompileClientActor},
compiler_init::{CompileConfig, CompilerConstConfig},
harness::InitializedLspDriver,
internal_error, invalid_params, method_not_found, run_query,
@ -72,7 +72,7 @@ pub struct CompileServerArgs {
pub client: LspHost<CompileServer>,
pub compile_config: CompileConfig,
pub const_config: CompilerConstConfig,
pub diag_tx: mpsc::UnboundedSender<(String, Option<DiagnosticsMap>)>,
pub diag_tx: mpsc::UnboundedSender<CompileClusterRequest>,
pub font: Deferred<SharedFontResolver>,
pub handle: tokio::runtime::Handle,
}
@ -109,7 +109,7 @@ pub struct CompileServer {
/// Source synchronized with client
pub memory_changes: HashMap<Arc<Path>, MemoryFileMeta>,
/// 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.
pub compiler: Option<CompileClientActor>,
}

View file

@ -8,7 +8,7 @@ use comemo::Prehashed;
use once_cell::sync::Lazy;
use serde::Deserialize;
use serde_json::{Map, Value as JsonValue};
use tinymist_query::{DiagnosticsMap, PositionEncoding};
use tinymist_query::PositionEncoding;
use tokio::sync::mpsc;
use typst::foundations::IntoValue;
use typst::syntax::FileId;
@ -17,6 +17,7 @@ use typst::util::Deferred;
use typst_ts_core::config::compiler::EntryState;
use typst_ts_core::{ImmutPath, TypstDict};
use crate::actor::cluster::CompileClusterRequest;
use crate::compiler::{CompileServer, CompileServerArgs};
use crate::harness::LspDriver;
use crate::world::{ImmutDict, SharedFontResolver};
@ -93,6 +94,8 @@ pub struct CompileConfig {
pub export_pdf: ExportMode,
/// Specifies the root path of the project manually.
pub root_path: Option<PathBuf>,
/// Specifies the root path of the project manually.
pub notify_compile_status: bool,
/// Typst extra arguments.
pub typst_extra_args: Option<CompileExtraOpts>,
pub has_default_entry_path: bool,
@ -144,6 +147,14 @@ impl CompileConfig {
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: {
if let Some(typst_extra_args) = update.get("typstExtraArgs") {
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 handle: tokio::runtime::Handle,
pub font: CompileFontOpts,
pub diag_tx: mpsc::UnboundedSender<(String, Option<DiagnosticsMap>)>,
pub diag_tx: mpsc::UnboundedSender<CompileClusterRequest>,
}
#[derive(Debug, Deserialize)]

View file

@ -21,8 +21,7 @@ use serde::{Deserialize, Serialize};
use serde_json::{Map, Value as JsonValue};
use tinymist_query::{
get_semantic_tokens_options, get_semantic_tokens_registration,
get_semantic_tokens_unregistration, DiagnosticsMap, ExportKind, PageSelection,
SemanticTokenContext,
get_semantic_tokens_unregistration, ExportKind, PageSelection, SemanticTokenContext,
};
use tokio::sync::mpsc;
use typst::diag::StrResult;
@ -33,6 +32,7 @@ use typst_ts_core::path::PathClean;
use typst_ts_core::{error::prelude::*, ImmutPath};
use super::lsp_init::*;
use crate::actor::cluster::CompileClusterRequest;
use crate::actor::typ_client::CompileClientActor;
use crate::actor::{FormattingConfig, FormattingRequest};
use crate::compiler::{CompileServer, CompileServerArgs};
@ -152,7 +152,7 @@ fn as_path_pos(inp: TextDocumentPositionParams) -> (PathBuf, Position) {
pub struct TypstLanguageServerArgs {
pub client: LspHost<TypstLanguageServer>,
pub const_config: ConstConfig,
pub diag_tx: mpsc::UnboundedSender<(String, Option<DiagnosticsMap>)>,
pub diag_tx: mpsc::UnboundedSender<CompileClusterRequest>,
pub font: Deferred<SharedFontResolver>,
}

View file

@ -13,7 +13,7 @@ use typst::util::Deferred;
use typst_ts_core::error::prelude::*;
use typst_ts_core::ImmutPath;
use crate::actor::cluster::CompileClusterActor;
use crate::actor::cluster::EditorActor;
use crate::compiler_init::CompileConfig;
use crate::harness::LspHost;
use crate::world::{ImmutDict, SharedFontResolver};
@ -88,11 +88,14 @@ const CONFIG_ITEMS: &[&str] = &[
"semanticTokens",
"formatterMode",
"typstExtraArgs",
"compileStatus",
];
/// The user configuration read from the editor.
#[derive(Debug, Default, Clone)]
pub struct Config {
/// Specifies the root path of the project manually.
pub notify_compile_status: bool,
/// The compile configurations
pub compile: CompileConfig,
/// Dynamic configuration for semantic tokens.
@ -357,12 +360,13 @@ impl Init {
service.run_format_thread();
let cluster_actor = CompileClusterActor {
let cluster_actor = EditorActor {
host: self.host.clone(),
diag_rx,
diagnostics: HashMap::new(),
affect_map: HashMap::new(),
published_primary: false,
notify_compile_status: service.config.compile.notify_compile_status,
};
let fallback = service.config.compile.determine_default_entry_path();

View file

@ -1,2 +1,3 @@
pub mod package;
pub mod preview;
pub mod word_count;

View 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,
})
}
}

View file

@ -91,6 +91,16 @@
],
"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": {
"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`.",

View file

@ -5,7 +5,6 @@ import {
commands,
ViewColumn,
Uri,
WorkspaceConfiguration,
TextEditor,
ExtensionMode,
} from "vscode";
@ -20,6 +19,7 @@ import {
} from "vscode-languageclient/node";
import vscodeVariables from "vscode-variables";
import { activateEditorTool, getUserPackageData } from "./editor-tools";
import { triggerStatusBar, wordCountItemProcess } from "./ui-extends";
let client: LanguageClient | undefined = undefined;
@ -94,6 +94,10 @@ async function startClient(context: ExtensionContext): Promise<void> {
clientOptions
);
client.onNotification("tinymist/compileStatus", (params) => {
wordCountItemProcess(params);
});
window.onDidChangeActiveTextEditor((editor: TextEditor | undefined) => {
if (editor?.document.isUntitled) {
return;
@ -171,8 +175,29 @@ async function startClient(context: ExtensionContext): Promise<void> {
context.subscriptions.push(
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 {
@ -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> {
// console.log("focus main", fsPath, new Error().stack);
focusingFile = fsPath;
if (!fsPath) {
triggerStatusBar();
}
await client?.sendRequest("workspace/executeCommand", {
command: "tinymist.focusMain",
arguments: [fsPath],

View 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";
}
}