mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-08-04 10:18:16 +00:00
feat: profile and visualize coverage of the current document (#1490)
* feat: draft * feat: run coverage command in vscode * feat: create and move location crate * feat: run and visualize coverage * feat: l10n
This commit is contained in:
parent
99900b2c76
commit
c96ea6d77f
37 changed files with 1668 additions and 471 deletions
19
Cargo.lock
generated
19
Cargo.lock
generated
|
@ -4027,6 +4027,7 @@ dependencies = [
|
|||
"temp-env",
|
||||
"tinymist-assets 0.13.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tinymist-core",
|
||||
"tinymist-debug",
|
||||
"tinymist-l10n",
|
||||
"tinymist-project",
|
||||
"tinymist-query",
|
||||
|
@ -4060,6 +4061,7 @@ version = "0.13.2"
|
|||
dependencies = [
|
||||
"ecow",
|
||||
"insta",
|
||||
"log",
|
||||
"lsp-types",
|
||||
"regex",
|
||||
"serde",
|
||||
|
@ -4098,6 +4100,23 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinymist-debug"
|
||||
version = "0.13.8"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"comemo",
|
||||
"insta",
|
||||
"parking_lot",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tinymist-analysis",
|
||||
"tinymist-std",
|
||||
"tinymist-world",
|
||||
"typst",
|
||||
"typst-library",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinymist-derive"
|
||||
version = "0.13.2"
|
||||
|
|
|
@ -134,6 +134,7 @@ reflexo-vec2svg = { version = "=0.5.5-rc7" }
|
|||
|
||||
typst = "0.13.0"
|
||||
typst-html = "0.13.0"
|
||||
typst-library = "0.13.0"
|
||||
typst-timing = "0.13.0"
|
||||
typst-svg = "0.13.0"
|
||||
typst-render = "0.13.0"
|
||||
|
@ -186,6 +187,7 @@ tinymist-analysis = { path = "./crates/tinymist-analysis/", version = "0.13.2" }
|
|||
typst-shim = { path = "./crates/typst-shim", version = "0.13.2" }
|
||||
|
||||
tinymist-core = { path = "./crates/tinymist-core/", version = "0.13.8", default-features = false }
|
||||
tinymist-debug = { path = "./crates/tinymist-debug/", version = "0.13.8" }
|
||||
tinymist = { path = "./crates/tinymist/", version = "0.13.8" }
|
||||
tinymist-l10n = { path = "./crates/tinymist-l10n/", version = "0.13.8" }
|
||||
tinymist-query = { path = "./crates/tinymist-query/", version = "0.13.8" }
|
||||
|
@ -258,6 +260,7 @@ extend-exclude = ["/.git", "fixtures"]
|
|||
#
|
||||
# A regular build MUST use `tag` or `rev` to specify the version of the patched crate to ensure stability.
|
||||
typst = { git = "https://github.com/Myriad-Dreamin/typst.git", tag = "tinymist/v0.13.2" }
|
||||
typst-library = { git = "https://github.com/Myriad-Dreamin/typst.git", tag = "tinymist/v0.13.2" }
|
||||
typst-html = { git = "https://github.com/Myriad-Dreamin/typst.git", tag = "tinymist/v0.13.2" }
|
||||
typst-timing = { git = "https://github.com/Myriad-Dreamin/typst.git", tag = "tinymist/v0.13.2" }
|
||||
typst-svg = { git = "https://github.com/Myriad-Dreamin/typst.git", tag = "tinymist/v0.13.2" }
|
||||
|
|
|
@ -19,6 +19,7 @@ serde.workspace = true
|
|||
strum.workspace = true
|
||||
toml.workspace = true
|
||||
typst.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
insta.workspace = true
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
//! Tinymist Analysis
|
||||
|
||||
pub mod debug_loc;
|
||||
pub mod location;
|
||||
mod prelude;
|
||||
pub mod syntax;
|
||||
|
||||
|
|
364
crates/tinymist-analysis/src/location.rs
Normal file
364
crates/tinymist-analysis/src/location.rs
Normal file
|
@ -0,0 +1,364 @@
|
|||
//! Conversions between Typst and LSP locations
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::ops::Range;
|
||||
|
||||
use typst::syntax::Source;
|
||||
|
||||
/// An LSP Position encoded by [`PositionEncoding`].
|
||||
type LspPosition = lsp_types::Position;
|
||||
/// An LSP range encoded by [`PositionEncoding`].
|
||||
type LspRange = lsp_types::Range;
|
||||
|
||||
/// What counts as "1 character" for string indexing. We should always prefer
|
||||
/// UTF-8, but support UTF-16 as long as it is standard. For more background on
|
||||
/// encodings and LSP, try ["The bottom emoji breaks rust-analyzer"](https://fasterthanli.me/articles/the-bottom-emoji-breaks-rust-analyzer),
|
||||
/// a well-written article on the topic.
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default)]
|
||||
pub enum PositionEncoding {
|
||||
/// "1 character" means "1 UTF-16 code unit"
|
||||
///
|
||||
/// This is the only required encoding for LSPs to support, but it's not a
|
||||
/// natural one (unless you're working in JS). Prefer UTF-8, and refer
|
||||
/// to the article linked in the `PositionEncoding` docs for more
|
||||
/// background.
|
||||
#[default]
|
||||
Utf16,
|
||||
/// "1 character" means "1 byte"
|
||||
Utf8,
|
||||
}
|
||||
|
||||
impl From<PositionEncoding> for lsp_types::PositionEncodingKind {
|
||||
fn from(position_encoding: PositionEncoding) -> Self {
|
||||
match position_encoding {
|
||||
PositionEncoding::Utf16 => Self::UTF16,
|
||||
PositionEncoding::Utf8 => Self::UTF8,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an LSP position to a Typst position.
|
||||
pub fn to_typst_position(
|
||||
lsp_position: LspPosition,
|
||||
lsp_position_encoding: PositionEncoding,
|
||||
typst_source: &Source,
|
||||
) -> Option<usize> {
|
||||
let lines = typst_source.len_lines() as u32;
|
||||
|
||||
'bound_checking: {
|
||||
let should_warning = match lsp_position.line.cmp(&lines) {
|
||||
Ordering::Greater => true,
|
||||
Ordering::Equal => lsp_position.character > 0,
|
||||
Ordering::Less if lsp_position.line + 1 == lines => {
|
||||
let last_line_offset = typst_source.line_to_byte(lines as usize - 1)?;
|
||||
let last_line_chars = &typst_source.text()[last_line_offset..];
|
||||
let len = match lsp_position_encoding {
|
||||
PositionEncoding::Utf8 => last_line_chars.len(),
|
||||
PositionEncoding::Utf16 => {
|
||||
last_line_chars.chars().map(char::len_utf16).sum::<usize>()
|
||||
}
|
||||
};
|
||||
|
||||
match lsp_position.character.cmp(&(len as u32)) {
|
||||
Ordering::Less => break 'bound_checking,
|
||||
Ordering::Greater => true,
|
||||
Ordering::Equal => false,
|
||||
}
|
||||
}
|
||||
Ordering::Less => break 'bound_checking,
|
||||
};
|
||||
|
||||
if should_warning {
|
||||
log::warn!(
|
||||
"LSP position is out of bounds: {:?}, while only {:?} lines and {:?} characters at the end.",
|
||||
lsp_position, typst_source.len_lines(), typst_source.line_to_range(typst_source.len_lines() - 1),
|
||||
);
|
||||
}
|
||||
|
||||
return Some(typst_source.len_bytes());
|
||||
}
|
||||
|
||||
match lsp_position_encoding {
|
||||
PositionEncoding::Utf8 => {
|
||||
let line_index = lsp_position.line as usize;
|
||||
let column_index = lsp_position.character as usize;
|
||||
typst_source.line_column_to_byte(line_index, column_index)
|
||||
}
|
||||
PositionEncoding::Utf16 => {
|
||||
// We have a line number and a UTF-16 offset into that line. We want a byte
|
||||
// offset into the file.
|
||||
//
|
||||
// Typst's `Source` provides several UTF-16 methods:
|
||||
// - `len_utf16` for the length of the file
|
||||
// - `byte_to_utf16` to convert a byte offset from the start of the file to a
|
||||
// UTF-16 offset from the start of the file
|
||||
// - `utf16_to_byte` to do the opposite of `byte_to_utf16`
|
||||
//
|
||||
// Unfortunately, none of these address our needs well, so we do some math
|
||||
// instead. This is not the fastest possible implementation, but
|
||||
// it's the most reasonable without access to the internal state
|
||||
// of `Source`.
|
||||
|
||||
// TODO: Typst's `Source` could easily provide an implementation of the method
|
||||
// we need here. Submit a PR against `typst` to add it, then
|
||||
// update this if/when merged.
|
||||
|
||||
let line_index = lsp_position.line as usize;
|
||||
let utf16_offset_in_line = lsp_position.character as usize;
|
||||
|
||||
let byte_line_offset = typst_source.line_to_byte(line_index)?;
|
||||
let utf16_line_offset = typst_source.byte_to_utf16(byte_line_offset)?;
|
||||
let utf16_offset = utf16_line_offset + utf16_offset_in_line;
|
||||
|
||||
typst_source.utf16_to_byte(utf16_offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a Typst position to an LSP position.
|
||||
pub fn to_lsp_position(
|
||||
typst_offset: usize,
|
||||
lsp_position_encoding: PositionEncoding,
|
||||
typst_source: &Source,
|
||||
) -> LspPosition {
|
||||
if typst_offset > typst_source.len_bytes() {
|
||||
return LspPosition::new(typst_source.len_lines() as u32, 0);
|
||||
}
|
||||
|
||||
let line_index = typst_source.byte_to_line(typst_offset).unwrap();
|
||||
let column_index = typst_source.byte_to_column(typst_offset).unwrap();
|
||||
|
||||
let lsp_line = line_index as u32;
|
||||
let lsp_column = match lsp_position_encoding {
|
||||
PositionEncoding::Utf8 => column_index as u32,
|
||||
PositionEncoding::Utf16 => {
|
||||
// See the implementation of `position_to_offset` for discussion
|
||||
// relevant to this function.
|
||||
|
||||
// TODO: Typst's `Source` could easily provide an implementation of the method
|
||||
// we need here. Submit a PR to `typst` to add it, then update
|
||||
// this if/when merged.
|
||||
|
||||
let utf16_offset = typst_source.byte_to_utf16(typst_offset).unwrap();
|
||||
|
||||
let byte_line_offset = typst_source.line_to_byte(line_index).unwrap();
|
||||
let utf16_line_offset = typst_source.byte_to_utf16(byte_line_offset).unwrap();
|
||||
|
||||
let utf16_column_offset = utf16_offset - utf16_line_offset;
|
||||
utf16_column_offset as u32
|
||||
}
|
||||
};
|
||||
|
||||
LspPosition::new(lsp_line, lsp_column)
|
||||
}
|
||||
|
||||
/// Convert an LSP range to a Typst range.
|
||||
pub fn to_typst_range(
|
||||
lsp_range: LspRange,
|
||||
lsp_position_encoding: PositionEncoding,
|
||||
source: &Source,
|
||||
) -> Option<Range<usize>> {
|
||||
let lsp_start = lsp_range.start;
|
||||
let typst_start = to_typst_position(lsp_start, lsp_position_encoding, source)?;
|
||||
|
||||
let lsp_end = lsp_range.end;
|
||||
let typst_end = to_typst_position(lsp_end, lsp_position_encoding, source)?;
|
||||
|
||||
Some(Range {
|
||||
start: typst_start,
|
||||
end: typst_end,
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert a Typst range to an LSP range.
|
||||
pub fn to_lsp_range(
|
||||
typst_range: Range<usize>,
|
||||
typst_source: &Source,
|
||||
lsp_position_encoding: PositionEncoding,
|
||||
) -> LspRange {
|
||||
let typst_start = typst_range.start;
|
||||
let lsp_start = to_lsp_position(typst_start, lsp_position_encoding, typst_source);
|
||||
|
||||
let typst_end = typst_range.end;
|
||||
let lsp_end = to_lsp_position(typst_end, lsp_position_encoding, typst_source);
|
||||
|
||||
LspRange::new(lsp_start, lsp_end)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use lsp_types::Position;
|
||||
|
||||
use super::*;
|
||||
|
||||
const ENCODING_TEST_STRING: &str = "test 🥺 test";
|
||||
|
||||
#[test]
|
||||
fn issue_14_invalid_range() {
|
||||
let source = Source::detached("#set page(height: 2cm)");
|
||||
let rng = LspRange {
|
||||
start: LspPosition {
|
||||
line: 0,
|
||||
character: 22,
|
||||
},
|
||||
// EOF
|
||||
end: LspPosition {
|
||||
line: 1,
|
||||
character: 0,
|
||||
},
|
||||
};
|
||||
let res = to_typst_range(rng, PositionEncoding::Utf16, &source).unwrap();
|
||||
assert_eq!(res, 22..22);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_14_invalid_range_2() {
|
||||
let source = Source::detached(
|
||||
r"#let f(a) = {
|
||||
a
|
||||
}
|
||||
",
|
||||
);
|
||||
let rng = LspRange {
|
||||
start: LspPosition {
|
||||
line: 2,
|
||||
character: 1,
|
||||
},
|
||||
// EOF
|
||||
end: LspPosition {
|
||||
line: 3,
|
||||
character: 0,
|
||||
},
|
||||
};
|
||||
let res = to_typst_range(rng, PositionEncoding::Utf16, &source).unwrap();
|
||||
assert_eq!(res, 19..source.len_bytes());
|
||||
// EOF
|
||||
let rng = LspRange {
|
||||
start: LspPosition {
|
||||
line: 3,
|
||||
character: 1,
|
||||
},
|
||||
end: LspPosition {
|
||||
line: 4,
|
||||
character: 0,
|
||||
},
|
||||
};
|
||||
let res = to_typst_range(rng, PositionEncoding::Utf16, &source).unwrap();
|
||||
assert_eq!(res, source.len_bytes()..source.len_bytes());
|
||||
|
||||
for line in 0..=5 {
|
||||
for character in 0..2 {
|
||||
let off = to_typst_position(
|
||||
Position { line, character },
|
||||
PositionEncoding::Utf16,
|
||||
&source,
|
||||
);
|
||||
assert!(off.is_some(), "line: {line}, character: {character}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overflow_offset_to_position() {
|
||||
let source = Source::detached("test");
|
||||
|
||||
let offset = source.len_bytes();
|
||||
let position = to_lsp_position(offset, PositionEncoding::Utf16, &source);
|
||||
assert_eq!(
|
||||
position,
|
||||
LspPosition {
|
||||
line: 0,
|
||||
character: 4
|
||||
}
|
||||
);
|
||||
|
||||
let offset = source.len_bytes() + 1;
|
||||
let position = to_lsp_position(offset, PositionEncoding::Utf16, &source);
|
||||
assert_eq!(
|
||||
position,
|
||||
LspPosition {
|
||||
line: 1,
|
||||
character: 0
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn utf16_position_to_utf8_offset() {
|
||||
let source = Source::detached(ENCODING_TEST_STRING);
|
||||
|
||||
let start = LspPosition {
|
||||
line: 0,
|
||||
character: 0,
|
||||
};
|
||||
let emoji = LspPosition {
|
||||
line: 0,
|
||||
character: 5,
|
||||
};
|
||||
let post_emoji = LspPosition {
|
||||
line: 0,
|
||||
character: 7,
|
||||
};
|
||||
let end = LspPosition {
|
||||
line: 0,
|
||||
character: 12,
|
||||
};
|
||||
|
||||
let start_offset = to_typst_position(start, PositionEncoding::Utf16, &source).unwrap();
|
||||
let start_actual = 0;
|
||||
|
||||
let emoji_offset = to_typst_position(emoji, PositionEncoding::Utf16, &source).unwrap();
|
||||
let emoji_actual = 5;
|
||||
|
||||
let post_emoji_offset =
|
||||
to_typst_position(post_emoji, PositionEncoding::Utf16, &source).unwrap();
|
||||
let post_emoji_actual = 9;
|
||||
|
||||
let end_offset = to_typst_position(end, PositionEncoding::Utf16, &source).unwrap();
|
||||
let end_actual = 14;
|
||||
|
||||
assert_eq!(start_offset, start_actual);
|
||||
assert_eq!(emoji_offset, emoji_actual);
|
||||
assert_eq!(post_emoji_offset, post_emoji_actual);
|
||||
assert_eq!(end_offset, end_actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn utf8_offset_to_utf16_position() {
|
||||
let source = Source::detached(ENCODING_TEST_STRING);
|
||||
|
||||
let start = 0;
|
||||
let emoji = 5;
|
||||
let post_emoji = 9;
|
||||
let end = 14;
|
||||
|
||||
let start_position = LspPosition {
|
||||
line: 0,
|
||||
character: 0,
|
||||
};
|
||||
let start_actual = to_lsp_position(start, PositionEncoding::Utf16, &source);
|
||||
|
||||
let emoji_position = LspPosition {
|
||||
line: 0,
|
||||
character: 5,
|
||||
};
|
||||
let emoji_actual = to_lsp_position(emoji, PositionEncoding::Utf16, &source);
|
||||
|
||||
let post_emoji_position = LspPosition {
|
||||
line: 0,
|
||||
character: 7,
|
||||
};
|
||||
let post_emoji_actual = to_lsp_position(post_emoji, PositionEncoding::Utf16, &source);
|
||||
|
||||
let end_position = LspPosition {
|
||||
line: 0,
|
||||
character: 12,
|
||||
};
|
||||
let end_actual = to_lsp_position(end, PositionEncoding::Utf16, &source);
|
||||
|
||||
assert_eq!(start_position, start_actual);
|
||||
assert_eq!(emoji_position, emoji_actual);
|
||||
assert_eq!(post_emoji_position, post_emoji_actual);
|
||||
assert_eq!(end_position, end_actual);
|
||||
}
|
||||
}
|
30
crates/tinymist-debug/Cargo.toml
Normal file
30
crates/tinymist-debug/Cargo.toml
Normal file
|
@ -0,0 +1,30 @@
|
|||
[package]
|
||||
name = "tinymist-debug"
|
||||
description = "Tinymist debug support for Typst."
|
||||
categories = ["compilers"]
|
||||
keywords = ["api", "language", "typst"]
|
||||
authors.workspace = true
|
||||
version.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
typst-library.workspace = true
|
||||
typst.workspace = true
|
||||
tinymist-std.workspace = true
|
||||
tinymist-analysis.workspace = true
|
||||
tinymist-world = { workspace = true, features = ["system"] }
|
||||
parking_lot.workspace = true
|
||||
comemo.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
base64.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
insta.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
367
crates/tinymist-debug/src/cov.rs
Normal file
367
crates/tinymist-debug/src/cov.rs
Normal file
|
@ -0,0 +1,367 @@
|
|||
//! Tinymist coverage support for Typst.
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use tinymist_std::hash::FxHashMap;
|
||||
use tinymist_world::vfs::FileId;
|
||||
use typst::diag::FileResult;
|
||||
use typst::foundations::func;
|
||||
use typst::syntax::ast::AstNode;
|
||||
use typst::syntax::{ast, Source, Span, SyntaxNode};
|
||||
|
||||
use crate::instrument::Instrumenter;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CoverageInstrumenter {
|
||||
/// The coverage map.
|
||||
pub map: Mutex<FxHashMap<FileId, Arc<InstrumentMeta>>>,
|
||||
}
|
||||
|
||||
impl Instrumenter for CoverageInstrumenter {
|
||||
fn instrument(&self, _source: Source) -> FileResult<Source> {
|
||||
let (new, meta) = instrument_coverage(_source)?;
|
||||
let region = CovRegion {
|
||||
hits: Arc::new(Mutex::new(vec![0; meta.meta.len()])),
|
||||
};
|
||||
|
||||
let mut map = self.map.lock();
|
||||
map.insert(new.id(), meta);
|
||||
|
||||
let mut cov_map = COVERAGE_MAP.lock();
|
||||
cov_map.regions.insert(new.id(), region);
|
||||
|
||||
Ok(new)
|
||||
}
|
||||
}
|
||||
|
||||
/// The coverage map.
|
||||
#[derive(Default)]
|
||||
pub struct CoverageMap {
|
||||
last_hit: Option<(FileId, CovRegion)>,
|
||||
/// The coverage map.
|
||||
pub regions: FxHashMap<FileId, CovRegion>,
|
||||
}
|
||||
|
||||
/// The coverage region
|
||||
#[derive(Default, Clone)]
|
||||
pub struct CovRegion {
|
||||
/// The hits
|
||||
pub hits: Arc<Mutex<Vec<u8>>>,
|
||||
}
|
||||
|
||||
pub static COVERAGE_LOCK: LazyLock<Mutex<()>> = LazyLock::new(Mutex::default);
|
||||
pub static COVERAGE_MAP: LazyLock<Mutex<CoverageMap>> = LazyLock::new(Mutex::default);
|
||||
|
||||
#[func(name = "__cov_pc", title = "Coverage function")]
|
||||
pub fn __cov_pc(span: Span, pc: i64) {
|
||||
let Some(fid) = span.id() else {
|
||||
return;
|
||||
};
|
||||
let mut map = COVERAGE_MAP.lock();
|
||||
if let Some(last_hit) = map.last_hit.as_ref() {
|
||||
if last_hit.0 == fid {
|
||||
let mut hits = last_hit.1.hits.lock();
|
||||
let c = &mut hits[pc as usize];
|
||||
*c = c.saturating_add(1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let region = map.regions.entry(fid).or_default();
|
||||
{
|
||||
let mut hits = region.hits.lock();
|
||||
let c = &mut hits[pc as usize];
|
||||
*c = c.saturating_add(1);
|
||||
}
|
||||
map.last_hit = Some((fid, region.clone()));
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Kind {
|
||||
OpenBrace,
|
||||
CloseBrace,
|
||||
Functor,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct InstrumentMeta {
|
||||
pub meta: Vec<(Span, Kind)>,
|
||||
}
|
||||
|
||||
#[comemo::memoize]
|
||||
fn instrument_coverage(source: Source) -> FileResult<(Source, Arc<InstrumentMeta>)> {
|
||||
let node = source.root();
|
||||
let mut worker = InstrumentWorker {
|
||||
meta: InstrumentMeta::default(),
|
||||
instrumented: String::new(),
|
||||
};
|
||||
|
||||
worker.visit_node(node);
|
||||
let new_source: Source = Source::new(source.id(), worker.instrumented);
|
||||
|
||||
Ok((new_source, Arc::new(worker.meta)))
|
||||
}
|
||||
|
||||
struct InstrumentWorker {
|
||||
meta: InstrumentMeta,
|
||||
instrumented: String,
|
||||
}
|
||||
|
||||
impl InstrumentWorker {
|
||||
fn instrument_block_child(&mut self, container: &SyntaxNode, b1: Span, b2: Span) {
|
||||
for child in container.children() {
|
||||
if b1 == child.span() || b2 == child.span() {
|
||||
self.instrument_block(child);
|
||||
} else {
|
||||
self.visit_node(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_node(&mut self, node: &SyntaxNode) {
|
||||
if let Some(expr) = node.cast::<ast::Expr>() {
|
||||
match expr {
|
||||
ast::Expr::Code(..) => {
|
||||
self.instrument_block(node);
|
||||
return;
|
||||
}
|
||||
ast::Expr::While(while_expr) => {
|
||||
self.instrument_block_child(node, while_expr.body().span(), Span::detached());
|
||||
return;
|
||||
}
|
||||
ast::Expr::For(for_expr) => {
|
||||
self.instrument_block_child(node, for_expr.body().span(), Span::detached());
|
||||
return;
|
||||
}
|
||||
ast::Expr::Conditional(cond_expr) => {
|
||||
self.instrument_block_child(
|
||||
node,
|
||||
cond_expr.if_body().span(),
|
||||
cond_expr.else_body().unwrap_or_default().span(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
ast::Expr::Closure(closure) => {
|
||||
self.instrument_block_child(node, closure.body().span(), Span::detached());
|
||||
return;
|
||||
}
|
||||
ast::Expr::Show(show_rule) => {
|
||||
let transform = show_rule.transform().to_untyped().span();
|
||||
|
||||
for child in node.children() {
|
||||
if transform == child.span() {
|
||||
self.instrument_functor(child);
|
||||
} else {
|
||||
self.visit_node(child);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
ast::Expr::Text(..)
|
||||
| ast::Expr::Space(..)
|
||||
| ast::Expr::Linebreak(..)
|
||||
| ast::Expr::Parbreak(..)
|
||||
| ast::Expr::Escape(..)
|
||||
| ast::Expr::Shorthand(..)
|
||||
| ast::Expr::SmartQuote(..)
|
||||
| ast::Expr::Strong(..)
|
||||
| ast::Expr::Emph(..)
|
||||
| ast::Expr::Raw(..)
|
||||
| ast::Expr::Link(..)
|
||||
| ast::Expr::Label(..)
|
||||
| ast::Expr::Ref(..)
|
||||
| ast::Expr::Heading(..)
|
||||
| ast::Expr::List(..)
|
||||
| ast::Expr::Enum(..)
|
||||
| ast::Expr::Term(..)
|
||||
| ast::Expr::Equation(..)
|
||||
| ast::Expr::Math(..)
|
||||
| ast::Expr::MathText(..)
|
||||
| ast::Expr::MathIdent(..)
|
||||
| ast::Expr::MathShorthand(..)
|
||||
| ast::Expr::MathAlignPoint(..)
|
||||
| ast::Expr::MathDelimited(..)
|
||||
| ast::Expr::MathAttach(..)
|
||||
| ast::Expr::MathPrimes(..)
|
||||
| ast::Expr::MathFrac(..)
|
||||
| ast::Expr::MathRoot(..)
|
||||
| ast::Expr::Ident(..)
|
||||
| ast::Expr::None(..)
|
||||
| ast::Expr::Auto(..)
|
||||
| ast::Expr::Bool(..)
|
||||
| ast::Expr::Int(..)
|
||||
| ast::Expr::Float(..)
|
||||
| ast::Expr::Numeric(..)
|
||||
| ast::Expr::Str(..)
|
||||
| ast::Expr::Content(..)
|
||||
| ast::Expr::Parenthesized(..)
|
||||
| ast::Expr::Array(..)
|
||||
| ast::Expr::Dict(..)
|
||||
| ast::Expr::Unary(..)
|
||||
| ast::Expr::Binary(..)
|
||||
| ast::Expr::FieldAccess(..)
|
||||
| ast::Expr::FuncCall(..)
|
||||
| ast::Expr::Let(..)
|
||||
| ast::Expr::DestructAssign(..)
|
||||
| ast::Expr::Set(..)
|
||||
| ast::Expr::Contextual(..)
|
||||
| ast::Expr::Import(..)
|
||||
| ast::Expr::Include(..)
|
||||
| ast::Expr::Break(..)
|
||||
| ast::Expr::Continue(..)
|
||||
| ast::Expr::Return(..) => {}
|
||||
}
|
||||
}
|
||||
|
||||
self.visit_node_fallback(node);
|
||||
}
|
||||
|
||||
fn visit_node_fallback(&mut self, node: &SyntaxNode) {
|
||||
let txt = node.text();
|
||||
if !txt.is_empty() {
|
||||
self.instrumented.push_str(txt);
|
||||
}
|
||||
|
||||
for child in node.children() {
|
||||
self.visit_node(child);
|
||||
}
|
||||
}
|
||||
|
||||
fn make_cov(&mut self, span: Span, kind: Kind) {
|
||||
let it = self.meta.meta.len();
|
||||
self.meta.meta.push((span, kind));
|
||||
self.instrumented.push_str("__cov_pc(");
|
||||
self.instrumented.push_str(&it.to_string());
|
||||
self.instrumented.push_str(");\n");
|
||||
}
|
||||
|
||||
fn instrument_block(&mut self, child: &SyntaxNode) {
|
||||
self.instrumented.push_str("{\n");
|
||||
let (first, last) = {
|
||||
let mut children = child.children();
|
||||
let first = children
|
||||
.next()
|
||||
.map(|s| s.span())
|
||||
.unwrap_or_else(Span::detached);
|
||||
let last = children
|
||||
.last()
|
||||
.map(|s| s.span())
|
||||
.unwrap_or_else(Span::detached);
|
||||
|
||||
(first, last)
|
||||
};
|
||||
self.make_cov(first, Kind::OpenBrace);
|
||||
self.visit_node_fallback(child);
|
||||
self.instrumented.push('\n');
|
||||
self.make_cov(last, Kind::CloseBrace);
|
||||
self.instrumented.push_str("}\n");
|
||||
}
|
||||
|
||||
fn instrument_functor(&mut self, child: &SyntaxNode) {
|
||||
self.instrumented.push_str("{\nlet __cov_functor = ");
|
||||
let s = child.span();
|
||||
self.visit_node_fallback(child);
|
||||
self.instrumented.push_str("\n__it => {");
|
||||
self.make_cov(s, Kind::Functor);
|
||||
self.instrumented.push_str("__cov_functor(__it); } }\n");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn instr(input: &str) -> String {
|
||||
let source = Source::detached(input);
|
||||
let (new, _meta) = instrument_coverage(source).unwrap();
|
||||
new.text().to_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_physica_vector() {
|
||||
let instrumented = instr(include_str!("fixtures/instr_coverage/physica_vector.typ"));
|
||||
insta::assert_snapshot!(instrumented, @r#"
|
||||
// A show rule, should be used like:
|
||||
// #show: super-plus-as-dagger
|
||||
// U^+U = U U^+ = I
|
||||
// or in scope:
|
||||
// #[
|
||||
// #show: super-plus-as-dagger
|
||||
// U^+U = U U^+ = I
|
||||
// ]
|
||||
#let super-plus-as-dagger(document) = {
|
||||
__cov_pc(0);
|
||||
{
|
||||
show math.attach: {
|
||||
let __cov_functor = elem => {
|
||||
__cov_pc(1);
|
||||
{
|
||||
if __eligible(elem.base) and elem.at("t", default: none) == [+] {
|
||||
__cov_pc(2);
|
||||
{
|
||||
$attach(elem.base, t: dagger, b: elem.at("b", default: #none))$
|
||||
}
|
||||
__cov_pc(3);
|
||||
}
|
||||
else {
|
||||
__cov_pc(4);
|
||||
{
|
||||
elem
|
||||
}
|
||||
__cov_pc(5);
|
||||
}
|
||||
|
||||
}
|
||||
__cov_pc(6);
|
||||
}
|
||||
|
||||
__it => {__cov_pc(7);
|
||||
__cov_functor(__it); } }
|
||||
|
||||
|
||||
document
|
||||
}
|
||||
__cov_pc(8);
|
||||
}
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_playground() {
|
||||
let instrumented = instr(include_str!("fixtures/instr_coverage/playground.typ"));
|
||||
insta::assert_snapshot!(instrumented, @"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_instrument_coverage() {
|
||||
let source = Source::detached("#let a = 1;");
|
||||
let (new, _meta) = instrument_coverage(source).unwrap();
|
||||
insta::assert_snapshot!(new.text(), @"#let a = 1;");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_instrument_coverage_nested() {
|
||||
let source = Source::detached("#let a = {1};");
|
||||
let (new, _meta) = instrument_coverage(source).unwrap();
|
||||
insta::assert_snapshot!(new.text(), @r"
|
||||
#let a = {
|
||||
__cov_pc(0);
|
||||
{1}
|
||||
__cov_pc(1);
|
||||
}
|
||||
;
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_instrument_coverage_functor() {
|
||||
let source = Source::detached("#show: main");
|
||||
let (new, _meta) = instrument_coverage(source).unwrap();
|
||||
insta::assert_snapshot!(new.text(), @r"
|
||||
#show: {
|
||||
let __cov_functor = main
|
||||
__it => {__cov_pc(0);
|
||||
__cov_functor(__it); } }
|
||||
");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
|
||||
// A show rule, should be used like:
|
||||
// #show: super-plus-as-dagger
|
||||
// U^+U = U U^+ = I
|
||||
// or in scope:
|
||||
// #[
|
||||
// #show: super-plus-as-dagger
|
||||
// U^+U = U U^+ = I
|
||||
// ]
|
||||
#let super-plus-as-dagger(document) = {
|
||||
show math.attach: elem => {
|
||||
if __eligible(elem.base) and elem.at("t", default: none) == [+] {
|
||||
$attach(elem.base, t: dagger, b: elem.at("b", default: #none))$
|
||||
} else {
|
||||
elem
|
||||
}
|
||||
}
|
||||
|
||||
document
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
|
72
crates/tinymist-debug/src/instrument.rs
Normal file
72
crates/tinymist-debug/src/instrument.rs
Normal file
|
@ -0,0 +1,72 @@
|
|||
//! Tinymist instrument support for Typst.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use tinymist_std::hash::FxHashMap;
|
||||
use tinymist_world::vfs::PathResolution;
|
||||
use tinymist_world::SourceWorld;
|
||||
use tinymist_world::{vfs::FileId, CompilerFeat, CompilerWorld};
|
||||
use typst::diag::FileResult;
|
||||
use typst::foundations::{Bytes, Datetime};
|
||||
use typst::syntax::Source;
|
||||
use typst::text::{Font, FontBook};
|
||||
use typst::utils::LazyHash;
|
||||
use typst::Library;
|
||||
|
||||
pub trait Instrumenter: Send + Sync {
|
||||
fn instrument(&self, source: Source) -> FileResult<Source>;
|
||||
}
|
||||
|
||||
pub struct InstrumentWorld<'a, F: CompilerFeat, I> {
|
||||
pub base: &'a CompilerWorld<F>,
|
||||
pub library: Arc<LazyHash<Library>>,
|
||||
pub instr: I,
|
||||
pub instrumented: Mutex<FxHashMap<FileId, FileResult<Source>>>,
|
||||
}
|
||||
|
||||
impl<F: CompilerFeat, I: Instrumenter> typst::World for InstrumentWorld<'_, F, I>
|
||||
where
|
||||
I:,
|
||||
{
|
||||
fn library(&self) -> &LazyHash<Library> {
|
||||
&self.library
|
||||
}
|
||||
|
||||
fn book(&self) -> &LazyHash<FontBook> {
|
||||
self.base.book()
|
||||
}
|
||||
|
||||
fn main(&self) -> FileId {
|
||||
self.base.main()
|
||||
}
|
||||
|
||||
fn source(&self, id: FileId) -> FileResult<Source> {
|
||||
let mut instrumented = self.instrumented.lock();
|
||||
if let Some(source) = instrumented.get(&id) {
|
||||
return source.clone();
|
||||
}
|
||||
|
||||
let source = self.base.source(id).and_then(|s| self.instr.instrument(s));
|
||||
instrumented.insert(id, source.clone());
|
||||
source
|
||||
}
|
||||
|
||||
fn file(&self, id: FileId) -> FileResult<Bytes> {
|
||||
self.base.file(id)
|
||||
}
|
||||
|
||||
fn font(&self, index: usize) -> Option<Font> {
|
||||
self.base.font(index)
|
||||
}
|
||||
|
||||
fn today(&self, offset: Option<i64>) -> Option<Datetime> {
|
||||
self.base.today(offset)
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: CompilerFeat, I: Instrumenter> SourceWorld for InstrumentWorld<'_, F, I> {
|
||||
fn path_for_id(&self, id: FileId) -> FileResult<PathResolution> {
|
||||
self.base.path_for_id(id)
|
||||
}
|
||||
}
|
145
crates/tinymist-debug/src/lib.rs
Normal file
145
crates/tinymist-debug/src/lib.rs
Normal file
|
@ -0,0 +1,145 @@
|
|||
//! Tinymist coverage support for Typst.
|
||||
|
||||
mod cov;
|
||||
mod instrument;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::ops::DerefMut;
|
||||
use std::sync::Arc;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use tinymist_analysis::location::PositionEncoding;
|
||||
use tinymist_std::debug_loc::LspRange;
|
||||
use tinymist_std::{error::prelude::*, hash::FxHashMap};
|
||||
use tinymist_world::package::PackageSpec;
|
||||
use tinymist_world::{print_diagnostics, CompilerFeat, CompilerWorld};
|
||||
use typst::diag::EcoString;
|
||||
use typst::syntax::package::PackageVersion;
|
||||
use typst::syntax::FileId;
|
||||
use typst::utils::LazyHash;
|
||||
use typst::{Library, World, WorldExt};
|
||||
|
||||
use cov::*;
|
||||
use instrument::InstrumentWorld;
|
||||
|
||||
/// The coverage result.
|
||||
pub struct CoverageResult {
|
||||
/// The coverage meta.
|
||||
pub meta: FxHashMap<FileId, Arc<InstrumentMeta>>,
|
||||
/// The coverage map.
|
||||
pub regions: FxHashMap<FileId, CovRegion>,
|
||||
}
|
||||
|
||||
impl CoverageResult {
|
||||
/// Converts the coverage result to JSON.
|
||||
pub fn to_json<F: CompilerFeat>(&self, w: &CompilerWorld<F>) -> serde_json::Value {
|
||||
let lsp_position_encoding = PositionEncoding::Utf16;
|
||||
|
||||
let mut result = VscodeCoverage::new();
|
||||
|
||||
for (file_id, region) in &self.regions {
|
||||
let file_path = w
|
||||
.path_for_id(*file_id)
|
||||
.unwrap()
|
||||
.as_path()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
let mut details = vec![];
|
||||
|
||||
let meta = self.meta.get(file_id).unwrap();
|
||||
|
||||
let Ok(typst_source) = w.source(*file_id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let hits = region.hits.lock();
|
||||
for (idx, (span, _kind)) in meta.meta.iter().enumerate() {
|
||||
let Some(typst_range) = w.range(*span) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let rng = tinymist_analysis::location::to_lsp_range(
|
||||
typst_range,
|
||||
&typst_source,
|
||||
lsp_position_encoding,
|
||||
);
|
||||
|
||||
details.push(VscodeFileCoverageDetail {
|
||||
executed: hits[idx] > 0,
|
||||
location: rng,
|
||||
});
|
||||
}
|
||||
|
||||
result.insert(file_path, details);
|
||||
}
|
||||
|
||||
serde_json::to_value(result).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Collects the coverage of a single execution.
|
||||
pub fn collect_coverage<D: typst::Document, F: CompilerFeat>(
|
||||
base: &CompilerWorld<F>,
|
||||
) -> Result<CoverageResult> {
|
||||
let instr = InstrumentWorld {
|
||||
base,
|
||||
library: instrument_library(&base.library),
|
||||
instr: CoverageInstrumenter::default(),
|
||||
instrumented: Mutex::new(FxHashMap::default()),
|
||||
};
|
||||
|
||||
let _cov_lock = cov::COVERAGE_LOCK.lock();
|
||||
|
||||
if let Err(e) = typst::compile::<D>(&instr).output {
|
||||
print_diagnostics(&instr, e.iter(), tinymist_world::DiagnosticFormat::Human)
|
||||
.context_ut("failed to print diagnostics")?;
|
||||
bail!("");
|
||||
}
|
||||
|
||||
let meta = std::mem::take(instr.instr.map.lock().deref_mut());
|
||||
let CoverageMap { regions, .. } = std::mem::take(cov::COVERAGE_MAP.lock().deref_mut());
|
||||
|
||||
Ok(CoverageResult { meta, regions })
|
||||
}
|
||||
|
||||
#[comemo::memoize]
|
||||
fn instrument_library(library: &Arc<LazyHash<Library>>) -> Arc<LazyHash<Library>> {
|
||||
let mut library = library.as_ref().clone();
|
||||
|
||||
library.global.scope_mut().define_func::<__cov_pc>();
|
||||
Arc::new(library)
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct PackageSpecCmp<'a> {
|
||||
/// The namespace the package lives in.
|
||||
pub namespace: &'a EcoString,
|
||||
/// The name of the package within its namespace.
|
||||
pub name: &'a EcoString,
|
||||
/// The package's version.
|
||||
pub version: &'a PackageVersion,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a PackageSpec> for PackageSpecCmp<'a> {
|
||||
fn from(spec: &'a PackageSpec) -> Self {
|
||||
Self {
|
||||
namespace: &spec.namespace,
|
||||
name: &spec.name,
|
||||
version: &spec.version,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The coverage result in the format of the VSCode coverage data.
|
||||
pub type VscodeCoverage = HashMap<String, Vec<VscodeFileCoverageDetail>>;
|
||||
|
||||
/// Converts the coverage result to the VSCode coverage data.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct VscodeFileCoverageDetail {
|
||||
/// Whether the location is being executed
|
||||
pub executed: bool,
|
||||
/// The location of the coverage.
|
||||
pub location: LspRange,
|
||||
}
|
|
@ -1,10 +1,7 @@
|
|||
//! Conversions between Typst and LSP types and representations
|
||||
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use tinymist_std::path::PathClean;
|
||||
use tinymist_world::vfs::PathResolution;
|
||||
use typst::syntax::Source;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
|
@ -13,32 +10,7 @@ pub type LspPosition = lsp_types::Position;
|
|||
/// An LSP range encoded by [`PositionEncoding`].
|
||||
pub type LspRange = lsp_types::Range;
|
||||
|
||||
/// What counts as "1 character" for string indexing. We should always prefer
|
||||
/// UTF-8, but support UTF-16 as long as it is standard. For more background on
|
||||
/// encodings and LSP, try ["The bottom emoji breaks rust-analyzer"](https://fasterthanli.me/articles/the-bottom-emoji-breaks-rust-analyzer),
|
||||
/// a well-written article on the topic.
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default)]
|
||||
pub enum PositionEncoding {
|
||||
/// "1 character" means "1 UTF-16 code unit"
|
||||
///
|
||||
/// This is the only required encoding for LSPs to support, but it's not a
|
||||
/// natural one (unless you're working in JS). Prefer UTF-8, and refer
|
||||
/// to the article linked in the `PositionEncoding` docs for more
|
||||
/// background.
|
||||
#[default]
|
||||
Utf16,
|
||||
/// "1 character" means "1 byte"
|
||||
Utf8,
|
||||
}
|
||||
|
||||
impl From<PositionEncoding> for lsp_types::PositionEncodingKind {
|
||||
fn from(position_encoding: PositionEncoding) -> Self {
|
||||
match position_encoding {
|
||||
PositionEncoding::Utf16 => Self::UTF16,
|
||||
PositionEncoding::Utf8 => Self::UTF8,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub use tinymist_analysis::location::*;
|
||||
|
||||
const UNTITLED_ROOT: &str = "/untitled";
|
||||
static EMPTY_URL: LazyLock<Url> = LazyLock::new(|| Url::parse("file://").unwrap());
|
||||
|
@ -104,158 +76,8 @@ pub fn url_to_path(uri: Url) -> PathBuf {
|
|||
uri.to_file_path().unwrap()
|
||||
}
|
||||
|
||||
/// Convert an LSP position to a Typst position.
|
||||
pub fn to_typst_position(
|
||||
lsp_position: LspPosition,
|
||||
lsp_position_encoding: PositionEncoding,
|
||||
typst_source: &Source,
|
||||
) -> Option<usize> {
|
||||
let lines = typst_source.len_lines() as u32;
|
||||
|
||||
'bound_checking: {
|
||||
let should_warning = match lsp_position.line.cmp(&lines) {
|
||||
Ordering::Greater => true,
|
||||
Ordering::Equal => lsp_position.character > 0,
|
||||
Ordering::Less if lsp_position.line + 1 == lines => {
|
||||
let last_line_offset = typst_source.line_to_byte(lines as usize - 1)?;
|
||||
let last_line_chars = &typst_source.text()[last_line_offset..];
|
||||
let len = match lsp_position_encoding {
|
||||
PositionEncoding::Utf8 => last_line_chars.len(),
|
||||
PositionEncoding::Utf16 => {
|
||||
last_line_chars.chars().map(char::len_utf16).sum::<usize>()
|
||||
}
|
||||
};
|
||||
|
||||
match lsp_position.character.cmp(&(len as u32)) {
|
||||
Ordering::Less => break 'bound_checking,
|
||||
Ordering::Greater => true,
|
||||
Ordering::Equal => false,
|
||||
}
|
||||
}
|
||||
Ordering::Less => break 'bound_checking,
|
||||
};
|
||||
|
||||
if should_warning {
|
||||
log::warn!(
|
||||
"LSP position is out of bounds: {:?}, while only {:?} lines and {:?} characters at the end.",
|
||||
lsp_position, typst_source.len_lines(), typst_source.line_to_range(typst_source.len_lines() - 1),
|
||||
);
|
||||
}
|
||||
|
||||
return Some(typst_source.len_bytes());
|
||||
}
|
||||
|
||||
match lsp_position_encoding {
|
||||
PositionEncoding::Utf8 => {
|
||||
let line_index = lsp_position.line as usize;
|
||||
let column_index = lsp_position.character as usize;
|
||||
typst_source.line_column_to_byte(line_index, column_index)
|
||||
}
|
||||
PositionEncoding::Utf16 => {
|
||||
// We have a line number and a UTF-16 offset into that line. We want a byte
|
||||
// offset into the file.
|
||||
//
|
||||
// Typst's `Source` provides several UTF-16 methods:
|
||||
// - `len_utf16` for the length of the file
|
||||
// - `byte_to_utf16` to convert a byte offset from the start of the file to a
|
||||
// UTF-16 offset from the start of the file
|
||||
// - `utf16_to_byte` to do the opposite of `byte_to_utf16`
|
||||
//
|
||||
// Unfortunately, none of these address our needs well, so we do some math
|
||||
// instead. This is not the fastest possible implementation, but
|
||||
// it's the most reasonable without access to the internal state
|
||||
// of `Source`.
|
||||
|
||||
// TODO: Typst's `Source` could easily provide an implementation of the method
|
||||
// we need here. Submit a PR against `typst` to add it, then
|
||||
// update this if/when merged.
|
||||
|
||||
let line_index = lsp_position.line as usize;
|
||||
let utf16_offset_in_line = lsp_position.character as usize;
|
||||
|
||||
let byte_line_offset = typst_source.line_to_byte(line_index)?;
|
||||
let utf16_line_offset = typst_source.byte_to_utf16(byte_line_offset)?;
|
||||
let utf16_offset = utf16_line_offset + utf16_offset_in_line;
|
||||
|
||||
typst_source.utf16_to_byte(utf16_offset)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a Typst position to an LSP position.
|
||||
pub fn to_lsp_position(
|
||||
typst_offset: usize,
|
||||
lsp_position_encoding: PositionEncoding,
|
||||
typst_source: &Source,
|
||||
) -> LspPosition {
|
||||
if typst_offset > typst_source.len_bytes() {
|
||||
return LspPosition::new(typst_source.len_lines() as u32, 0);
|
||||
}
|
||||
|
||||
let line_index = typst_source.byte_to_line(typst_offset).unwrap();
|
||||
let column_index = typst_source.byte_to_column(typst_offset).unwrap();
|
||||
|
||||
let lsp_line = line_index as u32;
|
||||
let lsp_column = match lsp_position_encoding {
|
||||
PositionEncoding::Utf8 => column_index as u32,
|
||||
PositionEncoding::Utf16 => {
|
||||
// See the implementation of `position_to_offset` for discussion
|
||||
// relevant to this function.
|
||||
|
||||
// TODO: Typst's `Source` could easily provide an implementation of the method
|
||||
// we need here. Submit a PR to `typst` to add it, then update
|
||||
// this if/when merged.
|
||||
|
||||
let utf16_offset = typst_source.byte_to_utf16(typst_offset).unwrap();
|
||||
|
||||
let byte_line_offset = typst_source.line_to_byte(line_index).unwrap();
|
||||
let utf16_line_offset = typst_source.byte_to_utf16(byte_line_offset).unwrap();
|
||||
|
||||
let utf16_column_offset = utf16_offset - utf16_line_offset;
|
||||
utf16_column_offset as u32
|
||||
}
|
||||
};
|
||||
|
||||
LspPosition::new(lsp_line, lsp_column)
|
||||
}
|
||||
|
||||
/// Convert an LSP range to a Typst range.
|
||||
pub fn to_typst_range(
|
||||
lsp_range: LspRange,
|
||||
lsp_position_encoding: PositionEncoding,
|
||||
source: &Source,
|
||||
) -> Option<Range<usize>> {
|
||||
let lsp_start = lsp_range.start;
|
||||
let typst_start = to_typst_position(lsp_start, lsp_position_encoding, source)?;
|
||||
|
||||
let lsp_end = lsp_range.end;
|
||||
let typst_end = to_typst_position(lsp_end, lsp_position_encoding, source)?;
|
||||
|
||||
Some(Range {
|
||||
start: typst_start,
|
||||
end: typst_end,
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert a Typst range to an LSP range.
|
||||
pub fn to_lsp_range(
|
||||
typst_range: Range<usize>,
|
||||
typst_source: &Source,
|
||||
lsp_position_encoding: PositionEncoding,
|
||||
) -> LspRange {
|
||||
let typst_start = typst_range.start;
|
||||
let lsp_start = to_lsp_position(typst_start, lsp_position_encoding, typst_source);
|
||||
|
||||
let typst_end = typst_range.end;
|
||||
let lsp_end = to_lsp_position(typst_end, lsp_position_encoding, typst_source);
|
||||
|
||||
LspRange::new(lsp_start, lsp_end)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use lsp_types::Position;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
|
@ -279,175 +101,4 @@ mod test {
|
|||
let uri2 = path_to_url(&path).unwrap();
|
||||
assert_eq!(EMPTY_URL.clone(), uri2);
|
||||
}
|
||||
|
||||
const ENCODING_TEST_STRING: &str = "test 🥺 test";
|
||||
|
||||
#[test]
|
||||
fn issue_14_invalid_range() {
|
||||
let source = Source::detached("#set page(height: 2cm)");
|
||||
let rng = LspRange {
|
||||
start: LspPosition {
|
||||
line: 0,
|
||||
character: 22,
|
||||
},
|
||||
// EOF
|
||||
end: LspPosition {
|
||||
line: 1,
|
||||
character: 0,
|
||||
},
|
||||
};
|
||||
let res = to_typst_range(rng, PositionEncoding::Utf16, &source).unwrap();
|
||||
assert_eq!(res, 22..22);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_14_invalid_range_2() {
|
||||
let source = Source::detached(
|
||||
r"#let f(a) = {
|
||||
a
|
||||
}
|
||||
",
|
||||
);
|
||||
let rng = LspRange {
|
||||
start: LspPosition {
|
||||
line: 2,
|
||||
character: 1,
|
||||
},
|
||||
// EOF
|
||||
end: LspPosition {
|
||||
line: 3,
|
||||
character: 0,
|
||||
},
|
||||
};
|
||||
let res = to_typst_range(rng, PositionEncoding::Utf16, &source).unwrap();
|
||||
assert_eq!(res, 19..source.len_bytes());
|
||||
// EOF
|
||||
let rng = LspRange {
|
||||
start: LspPosition {
|
||||
line: 3,
|
||||
character: 1,
|
||||
},
|
||||
end: LspPosition {
|
||||
line: 4,
|
||||
character: 0,
|
||||
},
|
||||
};
|
||||
let res = to_typst_range(rng, PositionEncoding::Utf16, &source).unwrap();
|
||||
assert_eq!(res, source.len_bytes()..source.len_bytes());
|
||||
|
||||
for line in 0..=5 {
|
||||
for character in 0..2 {
|
||||
let off = to_typst_position(
|
||||
Position { line, character },
|
||||
PositionEncoding::Utf16,
|
||||
&source,
|
||||
);
|
||||
assert!(off.is_some(), "line: {line}, character: {character}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overflow_offset_to_position() {
|
||||
let source = Source::detached("test");
|
||||
|
||||
let offset = source.len_bytes();
|
||||
let position = to_lsp_position(offset, PositionEncoding::Utf16, &source);
|
||||
assert_eq!(
|
||||
position,
|
||||
LspPosition {
|
||||
line: 0,
|
||||
character: 4
|
||||
}
|
||||
);
|
||||
|
||||
let offset = source.len_bytes() + 1;
|
||||
let position = to_lsp_position(offset, PositionEncoding::Utf16, &source);
|
||||
assert_eq!(
|
||||
position,
|
||||
LspPosition {
|
||||
line: 1,
|
||||
character: 0
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn utf16_position_to_utf8_offset() {
|
||||
let source = Source::detached(ENCODING_TEST_STRING);
|
||||
|
||||
let start = LspPosition {
|
||||
line: 0,
|
||||
character: 0,
|
||||
};
|
||||
let emoji = LspPosition {
|
||||
line: 0,
|
||||
character: 5,
|
||||
};
|
||||
let post_emoji = LspPosition {
|
||||
line: 0,
|
||||
character: 7,
|
||||
};
|
||||
let end = LspPosition {
|
||||
line: 0,
|
||||
character: 12,
|
||||
};
|
||||
|
||||
let start_offset = to_typst_position(start, PositionEncoding::Utf16, &source).unwrap();
|
||||
let start_actual = 0;
|
||||
|
||||
let emoji_offset = to_typst_position(emoji, PositionEncoding::Utf16, &source).unwrap();
|
||||
let emoji_actual = 5;
|
||||
|
||||
let post_emoji_offset =
|
||||
to_typst_position(post_emoji, PositionEncoding::Utf16, &source).unwrap();
|
||||
let post_emoji_actual = 9;
|
||||
|
||||
let end_offset = to_typst_position(end, PositionEncoding::Utf16, &source).unwrap();
|
||||
let end_actual = 14;
|
||||
|
||||
assert_eq!(start_offset, start_actual);
|
||||
assert_eq!(emoji_offset, emoji_actual);
|
||||
assert_eq!(post_emoji_offset, post_emoji_actual);
|
||||
assert_eq!(end_offset, end_actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn utf8_offset_to_utf16_position() {
|
||||
let source = Source::detached(ENCODING_TEST_STRING);
|
||||
|
||||
let start = 0;
|
||||
let emoji = 5;
|
||||
let post_emoji = 9;
|
||||
let end = 14;
|
||||
|
||||
let start_position = LspPosition {
|
||||
line: 0,
|
||||
character: 0,
|
||||
};
|
||||
let start_actual = to_lsp_position(start, PositionEncoding::Utf16, &source);
|
||||
|
||||
let emoji_position = LspPosition {
|
||||
line: 0,
|
||||
character: 5,
|
||||
};
|
||||
let emoji_actual = to_lsp_position(emoji, PositionEncoding::Utf16, &source);
|
||||
|
||||
let post_emoji_position = LspPosition {
|
||||
line: 0,
|
||||
character: 7,
|
||||
};
|
||||
let post_emoji_actual = to_lsp_position(post_emoji, PositionEncoding::Utf16, &source);
|
||||
|
||||
let end_position = LspPosition {
|
||||
line: 0,
|
||||
character: 12,
|
||||
};
|
||||
let end_actual = to_lsp_position(end, PositionEncoding::Utf16, &source);
|
||||
|
||||
assert_eq!(start_position, start_actual);
|
||||
assert_eq!(emoji_position, emoji_actual);
|
||||
assert_eq!(post_emoji_position, post_emoji_actual);
|
||||
assert_eq!(end_position, end_actual);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -188,6 +188,14 @@ impl Error {
|
|||
pub fn arguments(&self) -> &[(&'static str, String)] {
|
||||
self.err.args.as_deref().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns the diagnostics attach to the error.
|
||||
pub fn diagnostics(&self) -> Option<&[SourceDiagnostic]> {
|
||||
match &self.err.kind {
|
||||
ErrKind::RawDiag(diag) => Some(diag),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
|
|
|
@ -28,7 +28,7 @@ pub use tinymist_vfs as vfs;
|
|||
#[cfg(feature = "system")]
|
||||
pub mod system;
|
||||
#[cfg(feature = "system")]
|
||||
pub use system::{SystemCompilerFeat, TypstSystemUniverse, TypstSystemWorld};
|
||||
pub use system::{print_diagnostics, SystemCompilerFeat, TypstSystemUniverse, TypstSystemWorld};
|
||||
|
||||
/// Run the compiler in the browser environment.
|
||||
#[cfg(feature = "browser")]
|
||||
|
@ -146,6 +146,14 @@ pub trait CompilerFeat: Send + Sync + 'static {
|
|||
type Registry: PackageRegistry + Send + Sync + Sized;
|
||||
}
|
||||
|
||||
/// Which format to use for diagnostics.
|
||||
#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub enum DiagnosticFormat {
|
||||
#[default]
|
||||
Human,
|
||||
Short,
|
||||
}
|
||||
|
||||
pub mod build_info {
|
||||
/// The version of the reflexo-world crate.
|
||||
pub static VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
|
|
@ -12,6 +12,9 @@ use crate::{
|
|||
EntryState,
|
||||
};
|
||||
|
||||
mod diag;
|
||||
pub use diag::*;
|
||||
|
||||
/// type trait of [`TypstSystemWorld`].
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct SystemCompilerFeat;
|
||||
|
|
82
crates/tinymist-world/src/system/diag.rs
Normal file
82
crates/tinymist-world/src/system/diag.rs
Normal file
|
@ -0,0 +1,82 @@
|
|||
use std::io::IsTerminal;
|
||||
|
||||
use codespan_reporting::{
|
||||
diagnostic::{Diagnostic, Label},
|
||||
term::{
|
||||
self,
|
||||
termcolor::{ColorChoice, StandardStream},
|
||||
},
|
||||
};
|
||||
use tinymist_std::Result;
|
||||
use tinymist_vfs::FileId;
|
||||
use typst::diag::{eco_format, Severity, SourceDiagnostic};
|
||||
use typst::syntax::Span;
|
||||
use typst::{World, WorldExt};
|
||||
|
||||
use crate::{DiagnosticFormat, SourceWorld};
|
||||
|
||||
/// Get stderr with color support if desirable.
|
||||
fn color_stream() -> StandardStream {
|
||||
StandardStream::stderr(if std::io::stderr().is_terminal() {
|
||||
ColorChoice::Auto
|
||||
} else {
|
||||
ColorChoice::Never
|
||||
})
|
||||
}
|
||||
|
||||
/// Print diagnostic messages to the terminal.
|
||||
pub fn print_diagnostics<'d, 'files, W: SourceWorld>(
|
||||
world: &'files W,
|
||||
errors: impl Iterator<Item = &'d SourceDiagnostic>,
|
||||
diagnostic_format: DiagnosticFormat,
|
||||
) -> Result<(), codespan_reporting::files::Error> {
|
||||
let world = world.for_codespan_reporting();
|
||||
|
||||
let mut w = match diagnostic_format {
|
||||
DiagnosticFormat::Human => color_stream(),
|
||||
DiagnosticFormat::Short => StandardStream::stderr(ColorChoice::Never),
|
||||
};
|
||||
|
||||
let mut config = term::Config {
|
||||
tab_width: 2,
|
||||
..Default::default()
|
||||
};
|
||||
if diagnostic_format == DiagnosticFormat::Short {
|
||||
config.display_style = term::DisplayStyle::Short;
|
||||
}
|
||||
|
||||
for diagnostic in errors {
|
||||
let diag = match diagnostic.severity {
|
||||
Severity::Error => Diagnostic::error(),
|
||||
Severity::Warning => Diagnostic::warning(),
|
||||
}
|
||||
.with_message(diagnostic.message.clone())
|
||||
.with_notes(
|
||||
diagnostic
|
||||
.hints
|
||||
.iter()
|
||||
.map(|e| (eco_format!("hint: {e}")).into())
|
||||
.collect(),
|
||||
)
|
||||
.with_labels(label(world.world, diagnostic.span).into_iter().collect());
|
||||
|
||||
term::emit(&mut w, &config, &world, &diag)?;
|
||||
|
||||
// Stacktrace-like helper diagnostics.
|
||||
for point in &diagnostic.trace {
|
||||
let message = point.v.to_string();
|
||||
let help = Diagnostic::help()
|
||||
.with_message(message)
|
||||
.with_labels(label(world.world, point.span).into_iter().collect());
|
||||
|
||||
term::emit(&mut w, &config, &world, &help)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create a label for a span.
|
||||
fn label<W: World>(world: &W, span: Span) -> Option<Label<FileId>> {
|
||||
Some(Label::primary(span.id()?, world.range(span)?))
|
||||
}
|
|
@ -547,25 +547,6 @@ impl<F: CompilerFeat> CompilerWorld<F> {
|
|||
))
|
||||
}
|
||||
|
||||
/// Lookup a source file by id.
|
||||
#[track_caller]
|
||||
fn lookup(&self, id: FileId) -> Source {
|
||||
self.source(id)
|
||||
.expect("file id does not point to any source file")
|
||||
}
|
||||
|
||||
fn map_source_or_default<T>(
|
||||
&self,
|
||||
id: FileId,
|
||||
default_v: T,
|
||||
f: impl FnOnce(Source) -> CodespanResult<T>,
|
||||
) -> CodespanResult<T> {
|
||||
match World::source(self, id).ok() {
|
||||
Some(source) => f(source),
|
||||
None => Ok(default_v),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn revision(&self) -> NonZeroUsize {
|
||||
self.revision
|
||||
}
|
||||
|
@ -750,6 +731,105 @@ impl<F: CompilerFeat> WorldDeps for CompilerWorld<F> {
|
|||
}
|
||||
}
|
||||
|
||||
pub trait SourceWorld: World {
|
||||
fn path_for_id(&self, id: FileId) -> Result<PathResolution, FileError>;
|
||||
fn lookup(&self, id: FileId) -> Source {
|
||||
self.source(id)
|
||||
.expect("file id does not point to any source file")
|
||||
}
|
||||
fn map_source_or_default<T>(
|
||||
&self,
|
||||
id: FileId,
|
||||
default_v: T,
|
||||
f: impl FnOnce(Source) -> CodespanResult<T>,
|
||||
) -> CodespanResult<T> {
|
||||
match self.source(id).ok() {
|
||||
Some(source) => f(source),
|
||||
None => Ok(default_v),
|
||||
}
|
||||
}
|
||||
|
||||
fn for_codespan_reporting(&self) -> CodeSpanReportWorld<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
CodeSpanReportWorld { world: self }
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: CompilerFeat> SourceWorld for CompilerWorld<F> {
|
||||
/// Resolve the real path for a file id.
|
||||
fn path_for_id(&self, id: FileId) -> Result<PathResolution, FileError> {
|
||||
self.path_for_id(id)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CodeSpanReportWorld<'a, T> {
|
||||
pub world: &'a T,
|
||||
}
|
||||
|
||||
impl<'a, T: SourceWorld> codespan_reporting::files::Files<'a> for CodeSpanReportWorld<'a, T> {
|
||||
/// A unique identifier for files in the file provider. This will be used
|
||||
/// for rendering `diagnostic::Label`s in the corresponding source files.
|
||||
type FileId = FileId;
|
||||
|
||||
/// The user-facing name of a file, to be displayed in diagnostics.
|
||||
type Name = String;
|
||||
|
||||
/// The source code of a file.
|
||||
type Source = Source;
|
||||
|
||||
/// The user-facing name of a file.
|
||||
fn name(&'a self, id: FileId) -> CodespanResult<Self::Name> {
|
||||
Ok(match self.world.path_for_id(id) {
|
||||
Ok(path) => path.as_path().display().to_string(),
|
||||
Err(_) => format!("{id:?}"),
|
||||
})
|
||||
}
|
||||
|
||||
/// The source code of a file.
|
||||
fn source(&'a self, id: FileId) -> CodespanResult<Self::Source> {
|
||||
Ok(self.world.lookup(id))
|
||||
}
|
||||
|
||||
/// See [`codespan_reporting::files::Files::line_index`].
|
||||
fn line_index(&'a self, id: FileId, given: usize) -> CodespanResult<usize> {
|
||||
let source = self.world.lookup(id);
|
||||
source
|
||||
.byte_to_line(given)
|
||||
.ok_or_else(|| CodespanError::IndexTooLarge {
|
||||
given,
|
||||
max: source.len_bytes(),
|
||||
})
|
||||
}
|
||||
|
||||
/// See [`codespan_reporting::files::Files::column_number`].
|
||||
fn column_number(&'a self, id: FileId, _: usize, given: usize) -> CodespanResult<usize> {
|
||||
let source = self.world.lookup(id);
|
||||
source.byte_to_column(given).ok_or_else(|| {
|
||||
let max = source.len_bytes();
|
||||
if given <= max {
|
||||
CodespanError::InvalidCharBoundary { given }
|
||||
} else {
|
||||
CodespanError::IndexTooLarge { given, max }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// See [`codespan_reporting::files::Files::line_range`].
|
||||
fn line_range(&'a self, id: FileId, given: usize) -> CodespanResult<std::ops::Range<usize>> {
|
||||
self.world.map_source_or_default(id, 0..0, |source| {
|
||||
source
|
||||
.line_to_range(given)
|
||||
.ok_or_else(|| CodespanError::LineTooLarge {
|
||||
given,
|
||||
max: source.len_lines(),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// todo: remove me
|
||||
impl<'a, F: CompilerFeat> codespan_reporting::files::Files<'a> for CompilerWorld<F> {
|
||||
/// A unique identifier for files in the file provider. This will be used
|
||||
/// for rendering `diagnostic::Label`s in the corresponding source files.
|
||||
|
@ -763,51 +843,27 @@ impl<'a, F: CompilerFeat> codespan_reporting::files::Files<'a> for CompilerWorld
|
|||
|
||||
/// The user-facing name of a file.
|
||||
fn name(&'a self, id: FileId) -> CodespanResult<Self::Name> {
|
||||
Ok(match self.path_for_id(id) {
|
||||
Ok(path) => path.as_path().display().to_string(),
|
||||
Err(_) => format!("{id:?}"),
|
||||
})
|
||||
self.for_codespan_reporting().name(id)
|
||||
}
|
||||
|
||||
/// The source code of a file.
|
||||
fn source(&'a self, id: FileId) -> CodespanResult<Self::Source> {
|
||||
Ok(self.lookup(id))
|
||||
self.for_codespan_reporting().source(id)
|
||||
}
|
||||
|
||||
/// See [`codespan_reporting::files::Files::line_index`].
|
||||
fn line_index(&'a self, id: FileId, given: usize) -> CodespanResult<usize> {
|
||||
let source = self.lookup(id);
|
||||
source
|
||||
.byte_to_line(given)
|
||||
.ok_or_else(|| CodespanError::IndexTooLarge {
|
||||
given,
|
||||
max: source.len_bytes(),
|
||||
})
|
||||
self.for_codespan_reporting().line_index(id, given)
|
||||
}
|
||||
|
||||
/// See [`codespan_reporting::files::Files::column_number`].
|
||||
fn column_number(&'a self, id: FileId, _: usize, given: usize) -> CodespanResult<usize> {
|
||||
let source = self.lookup(id);
|
||||
source.byte_to_column(given).ok_or_else(|| {
|
||||
let max = source.len_bytes();
|
||||
if given <= max {
|
||||
CodespanError::InvalidCharBoundary { given }
|
||||
} else {
|
||||
CodespanError::IndexTooLarge { given, max }
|
||||
}
|
||||
})
|
||||
self.for_codespan_reporting().column_number(id, 0, given)
|
||||
}
|
||||
|
||||
/// See [`codespan_reporting::files::Files::line_range`].
|
||||
fn line_range(&'a self, id: FileId, given: usize) -> CodespanResult<std::ops::Range<usize>> {
|
||||
self.map_source_or_default(id, 0..0, |source| {
|
||||
source
|
||||
.line_to_range(given)
|
||||
.ok_or_else(|| CodespanError::LineTooLarge {
|
||||
given,
|
||||
max: source.len_lines(),
|
||||
})
|
||||
})
|
||||
self.for_codespan_reporting().line_range(id, given)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -79,6 +79,7 @@ typst-shim.workspace = true
|
|||
typst-preview = { workspace = true, optional = true }
|
||||
typst-ansi-hl.workspace = true
|
||||
tinymist-task.workspace = true
|
||||
tinymist-debug.workspace = true
|
||||
typstfmt.workspace = true
|
||||
typstyle-core.workspace = true
|
||||
unicode-script.workspace = true
|
||||
|
|
|
@ -31,6 +31,9 @@ pub enum Commands {
|
|||
#[cfg(feature = "preview")]
|
||||
Preview(tinymist::tool::preview::PreviewCliArgs),
|
||||
|
||||
/// Execute a document and collect coverage
|
||||
#[clap(hide(true))] // still in development
|
||||
Cov(CompileOnceArgs),
|
||||
/// Runs compile command like `typst-cli compile`
|
||||
Compile(CompileArgs),
|
||||
/// Generates build script for compilation
|
||||
|
|
|
@ -18,7 +18,9 @@ use serde_json::Value as JsonValue;
|
|||
use sync_lsp::internal_error;
|
||||
use sync_lsp::transport::{with_stdio_transport, MirrorArgs};
|
||||
use sync_lsp::{LspBuilder, LspClientRoot, LspResult};
|
||||
use tinymist::tool::project::{compile_main, generate_script_main, project_main, task_main};
|
||||
use tinymist::tool::project::{
|
||||
compile_main, coverage_main, generate_script_main, project_main, task_main,
|
||||
};
|
||||
use tinymist::world::TaskInputs;
|
||||
use tinymist::{CompileConfig, Config, RegularInit, ServerState, SuperInit, UserActionTask};
|
||||
use tinymist_core::LONG_VERSION;
|
||||
|
@ -90,6 +92,7 @@ fn main() -> Result<()> {
|
|||
|
||||
match args.command.unwrap_or_default() {
|
||||
Commands::Completion(args) => completion(args),
|
||||
Commands::Cov(args) => coverage_main(args),
|
||||
Commands::Compile(args) => RUNTIMES.tokio_runtime.block_on(compile_main(args)),
|
||||
Commands::GenerateScript(args) => generate_script_main(args),
|
||||
Commands::Query(query_cmds) => query_main(query_cmds),
|
||||
|
|
|
@ -7,6 +7,7 @@ use std::{
|
|||
|
||||
use clap_complete::Shell;
|
||||
use reflexo::{path::unix_slash, ImmutPath};
|
||||
use reflexo_typst::{diag::print_diagnostics, DiagnosticFormat};
|
||||
use tinymist_std::{bail, error::prelude::*};
|
||||
|
||||
use crate::{project::*, task::ExportTask};
|
||||
|
@ -105,6 +106,41 @@ impl LockFileExt for LockFile {
|
|||
}
|
||||
}
|
||||
|
||||
/// Runs project compilation(s)
|
||||
pub fn coverage_main(args: CompileOnceArgs) -> Result<()> {
|
||||
// Prepares for the compilation
|
||||
let universe = args.resolve()?;
|
||||
let world = universe.snapshot();
|
||||
|
||||
let result = Ok(()).and_then(|_| -> Result<()> {
|
||||
let res =
|
||||
tinymist_debug::collect_coverage::<tinymist_std::typst::TypstPagedDocument, _>(&world)?;
|
||||
let cov_path = Path::new("target/coverage.json");
|
||||
let res = serde_json::to_string(&res.to_json(&world)).context("coverage")?;
|
||||
|
||||
std::fs::create_dir_all(cov_path.parent().context("parent")?).context("create coverage")?;
|
||||
std::fs::write(cov_path, res).context("write coverage")?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
print_diag_or_error(&world, result)
|
||||
}
|
||||
|
||||
fn print_diag_or_error(world: &LspWorld, result: Result<()>) -> Result<()> {
|
||||
if let Err(e) = result {
|
||||
if let Some(diagnostics) = e.diagnostics() {
|
||||
print_diagnostics(world, diagnostics.iter(), DiagnosticFormat::Human)
|
||||
.context_ut("print diagnostics")?;
|
||||
bail!("");
|
||||
}
|
||||
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runs project compilation(s)
|
||||
pub async fn compile_main(args: CompileArgs) -> Result<()> {
|
||||
// Identifies the input and output
|
||||
|
|
|
@ -1062,6 +1062,11 @@
|
|||
"title": "%extension.tinymist.command.tinymist.profileCurrentFile%",
|
||||
"category": "Typst"
|
||||
},
|
||||
{
|
||||
"command": "tinymist.profileCurrentFileCoverage",
|
||||
"title": "%extension.tinymist.command.tinymist.profileCurrentFileCoverage%",
|
||||
"category": "Typst"
|
||||
},
|
||||
{
|
||||
"command": "tinymist.syncLabel",
|
||||
"title": "%extension.tinymist.command.tinymist.syncLabel%",
|
||||
|
|
111
editors/vscode/src/context.ts
Normal file
111
editors/vscode/src/context.ts
Normal file
|
@ -0,0 +1,111 @@
|
|||
import * as vscode from "vscode";
|
||||
|
||||
/**
|
||||
* The active editor owning *typst language document* to track.
|
||||
*/
|
||||
let activeEditor: vscode.TextEditor | undefined;
|
||||
|
||||
export class IContext {
|
||||
subscriptions: vscode.Disposable[];
|
||||
|
||||
fileLevelCodelens: ICommand[];
|
||||
|
||||
constructor(public context: vscode.ExtensionContext) {
|
||||
this.subscriptions = context.subscriptions;
|
||||
this.fileLevelCodelens = [];
|
||||
|
||||
// Tracks the active editor owning *typst language document*.
|
||||
context.subscriptions.push(
|
||||
vscode.window.onDidChangeActiveTextEditor((editor: vscode.TextEditor | undefined) => {
|
||||
const langId = editor?.document.languageId;
|
||||
if (langId === "typst") {
|
||||
activeEditor = editor;
|
||||
} else if (editor === undefined || activeEditor?.document.isClosed) {
|
||||
activeEditor = undefined;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// todo: remove me
|
||||
static currentActiveEditor(): vscode.TextEditor | undefined {
|
||||
return activeEditor;
|
||||
}
|
||||
|
||||
registerFileLevelCommand(command: IFileLevelCommand) {
|
||||
this.fileLevelCodelens.push(command);
|
||||
this.subscriptions.push(
|
||||
vscode.commands.registerCommand(command.command, (...args: unknown[]) =>
|
||||
command.execute(this.provideEditor({}), ...args),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
provideEditor(context: FileLevelContext): FileLevelContext {
|
||||
context.currentEditor = activeEditor || vscode.window.activeTextEditor;
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
getCwd(context: any) {
|
||||
if (context.cwd) {
|
||||
return context.cwd;
|
||||
}
|
||||
|
||||
return this.getRootForUri(context.currentEditor?.document?.uri as vscode.Uri);
|
||||
}
|
||||
|
||||
getRootForUri(uri?: vscode.Uri) {
|
||||
const enclosedRoot = uri && vscode.workspace.getWorkspaceFolder(uri);
|
||||
if (enclosedRoot) {
|
||||
return enclosedRoot.uri.fsPath;
|
||||
}
|
||||
|
||||
if (uri) {
|
||||
return vscode.Uri.joinPath(uri, "..").fsPath;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
isValidEditor(currentEditor: vscode.TextEditor | undefined): currentEditor is vscode.TextEditor {
|
||||
if (!currentEditor) {
|
||||
vscode.window.showWarningMessage("No editor found for command.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// todo: provide it correctly.
|
||||
tinymistExec?: ICommand<ExecContext, Promise<ExecResult | undefined>>;
|
||||
|
||||
showErrorMessage(message: string) {
|
||||
vscode.window.showErrorMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
export interface FileLevelContext {
|
||||
currentEditor?: vscode.TextEditor;
|
||||
}
|
||||
|
||||
export interface ICommand<T = unknown, R = any> {
|
||||
command: string;
|
||||
execute(context: T, ...args: unknown[]): R;
|
||||
}
|
||||
|
||||
export type IFileLevelCommand = ICommand<FileLevelContext>;
|
||||
|
||||
export interface ExecContext extends FileLevelContext {
|
||||
isTTY?: boolean;
|
||||
stdout?: (data: Buffer) => void;
|
||||
stderr?: (data: Buffer) => void;
|
||||
killer?: vscode.EventEmitter<void>;
|
||||
}
|
||||
|
||||
export interface ExecResult {
|
||||
stdout: Buffer;
|
||||
stderr: Buffer;
|
||||
code: number;
|
||||
signal?: any;
|
||||
}
|
|
@ -7,6 +7,8 @@ import { extensionState } from "./state";
|
|||
|
||||
import { previewPreload } from "./features/preview";
|
||||
import { onEnterHandler } from "./lsp.on-enter";
|
||||
import { ExecContext, ExecResult, ICommand, IContext } from "./context";
|
||||
import { spawn } from "cross-spawn";
|
||||
|
||||
/**
|
||||
* The condition
|
||||
|
@ -15,7 +17,7 @@ type FeatureCondition = boolean;
|
|||
/**
|
||||
* The initialization vector
|
||||
*/
|
||||
type ActivationVector = (context: ExtensionContext) => void;
|
||||
type ActivationVector = (context: IContext) => void;
|
||||
/**
|
||||
* The initialization vector
|
||||
*/
|
||||
|
@ -88,6 +90,7 @@ export async function tinymistActivate(
|
|||
): Promise<void> {
|
||||
const { activateTable, config } = trait;
|
||||
tinymist.context = context;
|
||||
const contextExt = new IContext(context);
|
||||
|
||||
// Sets a global context key to indicate that the extension is activated
|
||||
vscode.commands.executeCommand("setContext", "ext.tinymistActivated", true);
|
||||
|
@ -101,7 +104,15 @@ export async function tinymistActivate(
|
|||
|
||||
// Initializes language client
|
||||
if (extensionState.features.lsp) {
|
||||
tinymist.initClient(config);
|
||||
const executable = tinymist.probeEnvPath("tinymist.serverPath", config.serverPath);
|
||||
config.probedServerPath = executable;
|
||||
// todo: guide installation?
|
||||
|
||||
if (config.probedServerPath) {
|
||||
tinymist.initClient(config);
|
||||
}
|
||||
|
||||
contextExt.tinymistExec = makeExecCommand(contextExt, executable);
|
||||
}
|
||||
// Register Shared commands
|
||||
context.subscriptions.push(
|
||||
|
@ -115,7 +126,7 @@ export async function tinymistActivate(
|
|||
// Activates platform-dependent features
|
||||
for (const [condition, activate] of activateTable()) {
|
||||
if (condition) {
|
||||
activate(context);
|
||||
activate(contextExt);
|
||||
}
|
||||
}
|
||||
// Starts language client
|
||||
|
@ -146,3 +157,62 @@ export async function tinymistDeactivate(
|
|||
await tinymist.stop();
|
||||
tinymist.context = undefined!;
|
||||
}
|
||||
|
||||
function makeExecCommand(
|
||||
context: IContext,
|
||||
executable?: string,
|
||||
): ICommand<ExecContext, Promise<ExecResult | undefined>> {
|
||||
return {
|
||||
command: "tinymist.executeCli",
|
||||
execute: async (ctx, cliArgs: string[]) => {
|
||||
if (!executable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cwd = context.getCwd(ctx);
|
||||
const proc = spawn(executable, cliArgs, {
|
||||
env: {
|
||||
...process.env,
|
||||
RUST_BACKTRACE: "1",
|
||||
},
|
||||
cwd,
|
||||
});
|
||||
|
||||
if (ctx.killer) {
|
||||
ctx.killer.event(() => {
|
||||
proc.kill();
|
||||
});
|
||||
}
|
||||
|
||||
const capturedStdout: Buffer[] = [];
|
||||
const capturedStderr: Buffer[] = [];
|
||||
|
||||
proc.stdout.on("data", (data: Buffer) => {
|
||||
if (ctx.stdout) {
|
||||
ctx.stdout(data);
|
||||
} else {
|
||||
capturedStdout.push(data);
|
||||
}
|
||||
});
|
||||
proc.stderr.on("data", (data: Buffer) => {
|
||||
if (ctx.stderr) {
|
||||
ctx.stderr(data);
|
||||
} else {
|
||||
capturedStderr.push(data);
|
||||
}
|
||||
});
|
||||
|
||||
return new Promise<ExecResult>((resolve, reject) => {
|
||||
proc.on("error", reject);
|
||||
proc.on("exit", (code: any, signal) => {
|
||||
resolve({
|
||||
stdout: Buffer.concat(capturedStdout),
|
||||
stderr: Buffer.concat(capturedStderr),
|
||||
code: code || 0,
|
||||
signal,
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -27,8 +27,10 @@ import { labelFeatureActivate } from "./features/label";
|
|||
import { packageFeatureActivate } from "./features/package";
|
||||
import { toolFeatureActivate } from "./features/tool";
|
||||
import { copyAndPasteActivate, dragAndDropActivate } from "./features/drop-paste";
|
||||
import { testingFeatureActivate } from "./features/testing";
|
||||
import { FeatureEntry, tinymistActivate, tinymistDeactivate } from "./extension.shared";
|
||||
import { LanguageClient } from "vscode-languageclient/node";
|
||||
import { IContext } from "./context";
|
||||
|
||||
LanguageState.Client = LanguageClient;
|
||||
|
||||
|
@ -39,6 +41,7 @@ const systemActivateTable = (): FeatureEntry[] => [
|
|||
[extensionState.features.dragAndDrop, dragAndDropActivate],
|
||||
[extensionState.features.copyAndPaste, copyAndPasteActivate],
|
||||
[extensionState.features.task, taskActivate],
|
||||
[extensionState.features.testing, testingFeatureActivate],
|
||||
[extensionState.features.devKit, devKitFeatureActivate],
|
||||
[extensionState.features.preview, previewActivateInTinymist, previewDeactivate],
|
||||
[extensionState.features.language, languageActivate],
|
||||
|
@ -62,7 +65,7 @@ export async function deactivate(): Promise<void> {
|
|||
});
|
||||
}
|
||||
|
||||
function previewActivateInTinymist(context: ExtensionContext) {
|
||||
function previewActivateInTinymist(context: IContext) {
|
||||
const typstPreviewExtension = vscode.extensions.getExtension("mgt19937.typst-preview");
|
||||
if (typstPreviewExtension) {
|
||||
void vscode.window.showWarningMessage(
|
||||
|
@ -75,10 +78,10 @@ function previewActivateInTinymist(context: ExtensionContext) {
|
|||
|
||||
// Runs Integrated preview extension
|
||||
previewSetIsTinymist();
|
||||
previewActivate(context, false);
|
||||
previewActivate(context.context, false);
|
||||
}
|
||||
|
||||
async function languageActivate(context: ExtensionContext) {
|
||||
async function languageActivate(context: IContext) {
|
||||
const client = tinymist.client;
|
||||
if (!client) {
|
||||
console.warn("activating language feature without starting the tinymist language server");
|
||||
|
@ -190,7 +193,7 @@ async function languageActivate(context: ExtensionContext) {
|
|||
const initTemplateCommand =
|
||||
(inPlace: boolean) =>
|
||||
(...args: string[]) =>
|
||||
initTemplate(context, inPlace, ...args);
|
||||
initTemplate(context.context, inPlace, ...args);
|
||||
|
||||
// prettier-ignore
|
||||
context.subscriptions.push(
|
||||
|
@ -220,7 +223,7 @@ async function languageActivate(context: ExtensionContext) {
|
|||
commands.registerCommand("tinymist.triggerSuggestAndParameterHints", triggerSuggestAndParameterHints),
|
||||
);
|
||||
// context.subscriptions.push
|
||||
const provider = new SymbolViewProvider(context);
|
||||
const provider = new SymbolViewProvider(context.context);
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerWebviewViewProvider(SymbolViewProvider.Name, provider),
|
||||
);
|
||||
|
|
|
@ -18,6 +18,7 @@ export async function activate(context: ExtensionContext): Promise<void> {
|
|||
dragAndDrop: false,
|
||||
copyAndPaste: false,
|
||||
onEnter: false,
|
||||
testing: false,
|
||||
preview: false,
|
||||
language: false,
|
||||
renderDocs: false,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import * as vscode from "vscode";
|
||||
import { IContext } from "../context";
|
||||
|
||||
export function devKitFeatureActivate(context: vscode.ExtensionContext) {
|
||||
export function devKitFeatureActivate(context: IContext) {
|
||||
vscode.commands.executeCommand("setContext", "ext.tinymistDevKit", true);
|
||||
context.subscriptions.push({
|
||||
dispose: () => {
|
||||
|
|
|
@ -15,14 +15,15 @@ import {
|
|||
typstPasteUriEditKind,
|
||||
Schemes,
|
||||
} from "./drop-paste.def";
|
||||
import { IContext } from "../context";
|
||||
|
||||
export function dragAndDropActivate(context: vscode.ExtensionContext) {
|
||||
export function dragAndDropActivate(context: IContext) {
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerDocumentDropEditProvider(typstDocumentSelector, new DropProvider()),
|
||||
);
|
||||
}
|
||||
|
||||
export function copyAndPasteActivate(context: vscode.ExtensionContext) {
|
||||
export function copyAndPasteActivate(context: IContext) {
|
||||
const providedEditKinds = [
|
||||
typstPasteLinkEditKind,
|
||||
typstPasteUriEditKind,
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import * as vscode from "vscode";
|
||||
import { tinymist } from "../lsp";
|
||||
import { IContext } from "../context";
|
||||
|
||||
export function labelFeatureActivate(context: vscode.ExtensionContext) {
|
||||
export function labelFeatureActivate(context: IContext) {
|
||||
const labelViewProvider = new LabelViewProvider();
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerTreeDataProvider("tinymist.label-view", labelViewProvider),
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import * as vscode from "vscode";
|
||||
import { PackageInfo, SymbolInfo, tinymist } from "../lsp";
|
||||
import { editorTool } from "./tool";
|
||||
import { IContext } from "../context";
|
||||
|
||||
export function packageFeatureActivate(context: vscode.ExtensionContext) {
|
||||
export function packageFeatureActivate(context: IContext) {
|
||||
const packageView = new PackageViewProvider();
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerTreeDataProvider("tinymist.package-view", packageView),
|
||||
|
@ -17,7 +18,7 @@ export function packageFeatureActivate(context: vscode.ExtensionContext) {
|
|||
|
||||
const content = await vscode.commands.executeCommand<string>("markdown.api.render", docs);
|
||||
|
||||
await editorTool(context, "docs", { pkg, content });
|
||||
await editorTool(context.context, "docs", { pkg, content });
|
||||
} catch (e) {
|
||||
console.error("show package docs error", e);
|
||||
vscode.window.showErrorMessage(`Failed to show package documentation: ${e}`);
|
||||
|
|
|
@ -25,15 +25,12 @@ import {
|
|||
tinymist,
|
||||
} from "../lsp";
|
||||
import { l10nMsg } from "../l10n";
|
||||
import { IContext } from "../context";
|
||||
|
||||
/**
|
||||
* The launch preview implementation which depends on `isCompat` of previewActivate.
|
||||
*/
|
||||
let launchImpl: typeof launchPreviewLsp;
|
||||
/**
|
||||
* The active editor owning *typst language document* to track.
|
||||
*/
|
||||
let activeEditor: vscode.TextEditor | undefined;
|
||||
|
||||
/**
|
||||
* Preload the preview resources to reduce the latency of the first preview.
|
||||
|
@ -88,18 +85,6 @@ export function previewActivate(context: vscode.ExtensionContext, isCompat: bool
|
|||
),
|
||||
);
|
||||
|
||||
// Tracks the active editor owning *typst language document*.
|
||||
context.subscriptions.push(
|
||||
vscode.window.onDidChangeActiveTextEditor((editor: vscode.TextEditor | undefined) => {
|
||||
const langId = editor?.document.languageId;
|
||||
if (langId === "typst") {
|
||||
activeEditor = editor;
|
||||
} else if (editor === undefined || activeEditor?.document.isClosed) {
|
||||
activeEditor = undefined;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const launchBrowsingPreview = launch("webview", "doc", { isBrowsing: true });
|
||||
const launchDevPreview = launch("webview", "doc", { isDev: true });
|
||||
// Registers preview commands, check `package.json` for descriptions.
|
||||
|
@ -169,7 +154,7 @@ export function previewActivate(context: vscode.ExtensionContext, isCompat: bool
|
|||
*/
|
||||
function launch(kind: "browser" | "webview", mode: "doc" | "slide", opts?: LaunchOpts) {
|
||||
return async () => {
|
||||
activeEditor = activeEditor || vscode.window.activeTextEditor;
|
||||
const activeEditor = IContext.currentActiveEditor() || vscode.window.activeTextEditor;
|
||||
if (!activeEditor) {
|
||||
vscode.window.showWarningMessage("No active editor");
|
||||
return;
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import * as vscode from "vscode";
|
||||
import { runExport } from "./tasks.export";
|
||||
import { IContext } from "../context";
|
||||
|
||||
export const TYPST_TASK_SOURCE = "typst";
|
||||
|
||||
export function taskActivate(context: vscode.ExtensionContext) {
|
||||
export function taskActivate(context: IContext) {
|
||||
const provide = (cls: typeof TypstTaskProvider) =>
|
||||
context.subscriptions.push(vscode.tasks.registerTaskProvider(cls.TYPE, new cls(context)));
|
||||
|
||||
|
@ -20,7 +21,7 @@ class TypstTaskProvider implements vscode.TaskProvider {
|
|||
},
|
||||
} as const;
|
||||
|
||||
constructor(private readonly context: vscode.ExtensionContext) {}
|
||||
constructor(private readonly context: IContext) {}
|
||||
|
||||
static has(task: vscode.Task): boolean {
|
||||
return task.definition.type === TypstTaskProvider.TYPE;
|
||||
|
|
168
editors/vscode/src/features/testing.ts
Normal file
168
editors/vscode/src/features/testing.ts
Normal file
|
@ -0,0 +1,168 @@
|
|||
import * as vscode from "vscode";
|
||||
import { IContext } from "../context";
|
||||
import { VirtualConsole } from "../util";
|
||||
import * as fs from "fs";
|
||||
|
||||
export function testingFeatureActivate(context: IContext) {
|
||||
const testController = vscode.tests.createTestController(
|
||||
"tinymist-tests",
|
||||
"Typst Tests (Tinymist)",
|
||||
);
|
||||
|
||||
const profileCoverage = testController.createRunProfile(
|
||||
"tinymist-profile-coverage",
|
||||
vscode.TestRunProfileKind.Coverage,
|
||||
runCoverageTests,
|
||||
);
|
||||
|
||||
context.subscriptions.push(testController, profileCoverage);
|
||||
|
||||
context.registerFileLevelCommand({
|
||||
command: "tinymist.profileCurrentFileCoverage",
|
||||
execute: async (ctx) => {
|
||||
if (!context.isValidEditor(ctx.currentEditor)) {
|
||||
return;
|
||||
}
|
||||
const document = ctx.currentEditor.document;
|
||||
|
||||
const includes = [
|
||||
testController.createTestItem("tinymist-profile", "tinymist-profile", document.uri),
|
||||
];
|
||||
|
||||
const testRunRequest = new vscode.TestRunRequest(
|
||||
includes,
|
||||
undefined,
|
||||
profileCoverage,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
|
||||
const cc = new vscode.CancellationTokenSource();
|
||||
runCoverageTests(testRunRequest, cc.token);
|
||||
},
|
||||
});
|
||||
|
||||
async function runCoverageTests(request: vscode.TestRunRequest, token: vscode.CancellationToken) {
|
||||
const testRun = testController.createTestRun(request);
|
||||
if (request.include?.length !== 1) {
|
||||
context.showErrorMessage("Invalid tinymist test run request");
|
||||
return;
|
||||
}
|
||||
|
||||
const item = request.include[0];
|
||||
const uri = item.uri;
|
||||
if (!uri) {
|
||||
context.showErrorMessage("Invalid tinymist test item uri");
|
||||
return;
|
||||
}
|
||||
testRun.started(item);
|
||||
|
||||
const failed = (msg: string) => {
|
||||
testRun.failed(item, new vscode.TestMessage(msg));
|
||||
testRun.end();
|
||||
};
|
||||
|
||||
const kind = request.profile?.kind;
|
||||
|
||||
const executable = context.tinymistExec;
|
||||
if (!executable) {
|
||||
failed("tinymist executable not found");
|
||||
testRun.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const vc = new VirtualConsole();
|
||||
const killer = new vscode.EventEmitter<void>();
|
||||
|
||||
const terminal = vscode.window.createTerminal({
|
||||
name: "document Coverage",
|
||||
pty: {
|
||||
onDidWrite: vc.writeEmitter.event,
|
||||
open: () => {},
|
||||
close: () => {
|
||||
killer.fire();
|
||||
},
|
||||
},
|
||||
});
|
||||
terminal.show(true);
|
||||
|
||||
const coverageTask = executable.execute(
|
||||
{
|
||||
killer,
|
||||
isTTY: true,
|
||||
stdout: (data: Buffer) => {
|
||||
vc.write(data.toString("utf8"));
|
||||
},
|
||||
stderr: (data: Buffer) => {
|
||||
vc.write(data.toString("utf8"));
|
||||
},
|
||||
},
|
||||
["cov", uri.fsPath],
|
||||
);
|
||||
|
||||
const detailsFut = coverageTask.then<Map<string, vscode.FileCoverageDetail[]>>((res) => {
|
||||
const details = new Map<string, vscode.FileCoverageDetail[]>();
|
||||
if (!res || res.code !== 0) {
|
||||
return details;
|
||||
}
|
||||
|
||||
const cov_path = "target/coverage.json";
|
||||
if (!fs.existsSync(cov_path)) {
|
||||
return details;
|
||||
}
|
||||
|
||||
const cov = fs.readFileSync(cov_path, "utf8");
|
||||
const cov_json: Record<string, vscode.StatementCoverage[]> = JSON.parse(cov);
|
||||
for (const [k, v] of Object.entries(cov_json)) {
|
||||
details.set(
|
||||
vscode.Uri.file(k).fsPath,
|
||||
v.map((x) => new vscode.StatementCoverage(x.executed, x.location, x.branches)),
|
||||
);
|
||||
}
|
||||
|
||||
return details;
|
||||
});
|
||||
|
||||
const p = request.profile;
|
||||
const requiredCoverage = p && kind === vscode.TestRunProfileKind.Coverage;
|
||||
if (requiredCoverage) {
|
||||
p.loadDetailedCoverage = async (_testRun, fi, token) => {
|
||||
await coverageTask;
|
||||
if (token.isCancellationRequested) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const details = await detailsFut;
|
||||
return details.get(fi.uri.fsPath) || [];
|
||||
};
|
||||
}
|
||||
|
||||
const res = await coverageTask;
|
||||
|
||||
if (!res) {
|
||||
return;
|
||||
}
|
||||
const { code } = res;
|
||||
vc.write(`\nCoverage profiling exited with code ${code}...`);
|
||||
if (code !== 0) {
|
||||
failed(`Coverage profiling exited with code ${code}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (token.isCancellationRequested) {
|
||||
vc.write("\nCoverage profiling cancelled...");
|
||||
return;
|
||||
}
|
||||
|
||||
if (requiredCoverage) {
|
||||
const details = await detailsFut;
|
||||
for (const [k, v] of details) {
|
||||
const uri = vscode.Uri.file(k);
|
||||
testRun.addCoverage(vscode.FileCoverage.fromDetails(uri, v));
|
||||
}
|
||||
}
|
||||
|
||||
testRun.passed(item);
|
||||
testRun.end();
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ import { writeFile } from "fs/promises";
|
|||
import { tinymist } from "../lsp";
|
||||
import { extensionState, ExtensionContext } from "../state";
|
||||
import { activeTypstEditor, base64Encode, loadHTMLFile } from "../util";
|
||||
import { IContext } from "../context";
|
||||
|
||||
const USER_PACKAGE_VERSION = "0.0.1";
|
||||
const FONTS_EXPORT_CONFIGURE_VERSION = "0.0.1";
|
||||
|
@ -47,14 +48,14 @@ const toolDesc: Partial<Record<EditorToolName, ToolDescriptor>> = {
|
|||
},
|
||||
};
|
||||
|
||||
export function toolFeatureActivate(context: vscode.ExtensionContext) {
|
||||
export function toolFeatureActivate(context: IContext) {
|
||||
const toolView = new ToolViewProvider();
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerTreeDataProvider("tinymist.tool-view", toolView),
|
||||
...Object.values(toolDesc).map((desc) =>
|
||||
vscode.commands.registerCommand(desc.command, async () => {
|
||||
await editorTool(context, desc.toolId);
|
||||
await editorTool(context.context, desc.toolId);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -176,7 +176,7 @@ export class LanguageState {
|
|||
const RUST_BACKTRACE = isProdMode ? "1" : "full";
|
||||
|
||||
const run = {
|
||||
command: tinymist.probeEnvPath("tinymist.serverPath", config.serverPath),
|
||||
command: config.probedServerPath,
|
||||
args: ["lsp", ...mirrorFlag],
|
||||
options: { env: Object.assign({}, process.env, { RUST_BACKTRACE }) },
|
||||
};
|
||||
|
|
|
@ -17,6 +17,7 @@ interface ExtensionState {
|
|||
onEnter: boolean;
|
||||
preview: boolean;
|
||||
language: boolean;
|
||||
testing: boolean;
|
||||
renderDocs: boolean;
|
||||
};
|
||||
mut: {
|
||||
|
@ -42,6 +43,7 @@ export const extensionState: ExtensionState = {
|
|||
onEnter: false,
|
||||
preview: false,
|
||||
language: true,
|
||||
testing: true,
|
||||
renderDocs: false,
|
||||
},
|
||||
mut: {
|
||||
|
|
|
@ -648,43 +648,15 @@ zh = "显示字体视图"
|
|||
zh-TW = "顯示字型視圖"
|
||||
|
||||
[extension.tinymist.command.tinymist.profileCurrentFile]
|
||||
en = "Profile and visualize execution of the current Typst file"
|
||||
ar = "تحليل وتصور تنفيذ ملف Typst الحالي"
|
||||
bg = "Профилиране и визуализация на изпълнението на текущия Typst файл"
|
||||
ca = "Perfilar i visualitzar l'execució del fitxer Typst actual"
|
||||
cs = "Profilovat a vizualizovat provedení aktuálního souboru Typst"
|
||||
da = "Profilér og visualiser udførelsen af den aktuelle Typst-fil"
|
||||
de = "Profilieren und Visualisieren der Ausführung der aktuellen Typst-Datei"
|
||||
el = "Προφίλ και απεικόνιση της εκτέλεσης του τρέχοντος αρχείου Typst"
|
||||
es = "Perfilar y visualizar la ejecución del archivo Typst actual"
|
||||
et = "Profiili ja visualiseeri praeguse Typst faili täitmine"
|
||||
eu = "Profilatu eta bistaratu uneko Typst fitxategiaren exekuzioa"
|
||||
fi = "Profiloi ja visualisoi nykyisen Typst-tiedoston suoritus"
|
||||
fr = "Profiler et visualiser l'exécution du fichier Typst actuel"
|
||||
gl = "Perfilar e visualizar a execución do ficheiro Typst actual"
|
||||
he = "פרופיל וחזותי את ביצוע קובץ ה-Typst הנוכחי"
|
||||
hu = "Profilozás és vizualizáció az aktuális Typst fájl végrehajtásához"
|
||||
is = "Prófíl og sjónræna framkvæmd núverandi Typst skrár"
|
||||
it = "Profilare e visualizzare l'esecuzione del file Typst corrente"
|
||||
ja = "現在のTypstファイルの実行をプロファイリングおよび視覚化"
|
||||
la = "Profile et visualiza exsecutionem fasciculi Typst currentis"
|
||||
nb = "Profilér og visualiser utførelsen av den aktuelle Typst-filen"
|
||||
nl = "Profileer en visualiseer de uitvoering van het huidige Typst-bestand"
|
||||
nn = "Profiler og visualiser utføringa av den aktuelle Typst-fila"
|
||||
pl = "Profiluj i wizualizuj wykonanie bieżącego pliku Typst"
|
||||
pt-PT = "Perfilar e visualizar a execução do ficheiro Typst atual"
|
||||
ro = "Profilare și vizualizare a execuției fișierului Typst curent"
|
||||
ru = "Профилирование и визуализация выполнения текущего файла Typst"
|
||||
sl = "Profiliraj in vizualiziraj izvedbo trenutne datoteke Typst"
|
||||
sq = "Profili dhe vizualizo ekzekutimin e skedarit aktual Typst"
|
||||
sr = "Профилиши и визуализуј извршење текућег Typst фајла"
|
||||
sv = "Profilera och visualisera körningen av den aktuella Typst-filen"
|
||||
tr = "Geçerli Typst dosyasının yürütülmesini profille ve görselleştir"
|
||||
uk = "Профілювання та візуалізація виконання поточного файлу Typst"
|
||||
vi = "Phân tích và hiển thị trực quan việc thực thi tệp Typst hiện tại"
|
||||
zh = "对当前 Typst 文件的执行进行分析和可视化"
|
||||
en = "Profile and visualize execution of the current Typst document"
|
||||
zh = "对当前 Typst 文档的执行进行分析和可视化"
|
||||
zh-TW = "對目前 Typst 檔案的執行進行分析和視覺化"
|
||||
|
||||
[extension.tinymist.command.tinymist.profileCurrentFileCoverage]
|
||||
en = "Profile and visualize code coverage of the current Typst document"
|
||||
zh = "对当前 Typst 文档的代码覆盖率进行分析和可视化"
|
||||
zh-TW = "對目前 Typst 檔案的程式碼覆蓋率進行分析和視覺化"
|
||||
|
||||
[extension.tinymist.command.tinymist.syncLabel]
|
||||
en = "Scan workspace and collect all labels again"
|
||||
ar = "فحص مساحة العمل وجمع جميع التسميات مرة أخرى"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue