tinymist_query/
document_metrics.rs

1use std::sync::Arc;
2use std::{collections::HashMap, path::PathBuf};
3
4use serde::{Deserialize, Serialize};
5use tinymist_std::typst::TypstDocument;
6use tinymist_world::debug_loc::DataSource;
7use typst::text::{Font, FontStretch, FontStyle, FontWeight};
8use typst::{
9    layout::{Frame, FrameItem},
10    syntax::Span,
11    text::TextItem,
12};
13
14use crate::prelude::*;
15
16/// Span information for some content.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub struct SpanInfo {
20    /// The sources that are used in the span information.
21    pub sources: Vec<DataSource>,
22}
23
24/// Annotated content for a font.
25#[derive(Debug, Clone, Serialize, Deserialize)]
26#[serde(rename_all = "camelCase")]
27pub struct AnnotatedContent {
28    /// A string of the content for slicing.
29    pub content: String,
30    /// The kind of the span encoding.
31    pub span_kind: String,
32    /// Encoded spans.
33    pub spans: Vec<i32>,
34}
35
36/// Information about a font.
37#[derive(Debug, Clone, Serialize, Deserialize)]
38#[serde(rename_all = "camelCase")]
39pub struct DocumentFontInfo {
40    /// The display name of the font, which is computed by this crate and
41    /// unnecessary from any fields of the font file.
42    pub name: String,
43    /// The style of the font.
44    pub style: FontStyle,
45    /// The weight of the font.
46    pub weight: FontWeight,
47    /// The stretch of the font.
48    pub stretch: FontStretch,
49    /// The PostScript name of the font.
50    pub postscript_name: Option<String>,
51    /// The Family in font file.
52    pub family: Option<String>,
53    /// The Full Name in font file.
54    pub full_name: Option<String>,
55    /// The Fixed Family used by Typst.
56    pub fixed_family: Option<String>,
57    /// The source of the font.
58    pub source: Option<u32>,
59    /// The index of the font in the source.
60    pub index: Option<u32>,
61    /// The annotated content length of the font.
62    /// If it is None, the uses is not calculated.
63    /// Otherwise, it is the length of the uses.
64    pub uses_scale: Option<u32>,
65    /// The annotated content of the font.
66    /// If it is not None, the uses_scale must be provided.
67    pub uses: Option<AnnotatedContent>,
68    /// The source Typst file of the locatable text element
69    /// in which the font first occurs.
70    pub first_occur_file: Option<String>,
71    /// The line number of the locatable text element
72    /// in which the font first occurs.
73    pub first_occur_line: Option<u32>,
74    /// The column number of the locatable text element
75    /// in which the font first occurs.
76    pub first_occur_column: Option<u32>,
77}
78
79/// The response to a DocumentMetricsRequest.
80#[derive(Debug, Clone, Serialize, Deserialize)]
81#[serde(rename_all = "camelCase")]
82pub struct DocumentMetricsResponse {
83    /// File span information.
84    pub span_info: SpanInfo,
85    /// Font information.
86    pub font_info: Vec<DocumentFontInfo>,
87}
88
89/// A request to compute DocumentMetrics for a document.
90///
91/// This is not part of the LSP protocol.
92#[derive(Debug, Clone)]
93pub struct DocumentMetricsRequest {
94    /// The path of the document to compute DocumentMetricss.
95    pub path: PathBuf,
96}
97
98impl StatefulRequest for DocumentMetricsRequest {
99    type Response = DocumentMetricsResponse;
100
101    fn request(self, ctx: &mut LocalContext, graph: LspComputeGraph) -> Option<Self::Response> {
102        let doc = graph.snap.success_doc.as_ref()?;
103
104        let mut worker = DocumentMetricsWorker {
105            ctx,
106            span_info: Default::default(),
107            span_info2: Default::default(),
108            font_info: Default::default(),
109        };
110
111        worker.work(doc)?;
112
113        let font_info = worker.compute()?;
114        let span_info = SpanInfo {
115            sources: worker.span_info2,
116        };
117        Some(DocumentMetricsResponse {
118            span_info,
119            font_info,
120        })
121    }
122}
123
124#[derive(Default)]
125struct FontInfoValue {
126    uses: u32,
127    first_occur_file: Option<String>,
128    first_occur_line: Option<u32>,
129    first_occur_column: Option<u32>,
130}
131
132struct DocumentMetricsWorker<'a> {
133    ctx: &'a mut LocalContext,
134    span_info: HashMap<Arc<DataSource>, u32>,
135    span_info2: Vec<DataSource>,
136    font_info: HashMap<Font, FontInfoValue>,
137}
138
139impl DocumentMetricsWorker<'_> {
140    fn work(&mut self, doc: &TypstDocument) -> Option<()> {
141        match doc {
142            TypstDocument::Paged(paged_doc) => {
143                for page in &paged_doc.pages {
144                    self.work_frame(&page.frame)?;
145                }
146
147                Some(())
148            }
149            _ => None,
150        }
151    }
152
153    fn work_frame(&mut self, frame: &Frame) -> Option<()> {
154        for (_, elem) in frame.items() {
155            self.work_elem(elem)?;
156        }
157
158        Some(())
159    }
160
161    fn work_elem(&mut self, elem: &FrameItem) -> Option<()> {
162        match elem {
163            FrameItem::Text(text) => self.work_text(text),
164            FrameItem::Group(frame) => self.work_frame(&frame.frame),
165            FrameItem::Shape(..)
166            | FrameItem::Image(..)
167            | FrameItem::Tag(..)
168            | FrameItem::Link(..) => Some(()),
169        }
170    }
171
172    fn work_text(&mut self, text: &TextItem) -> Option<()> {
173        let font_key = text.font.clone();
174        let glyph_len = text.glyphs.len();
175
176        let has_source_info = if let Some(font_info) = self.font_info.get(&font_key) {
177            font_info.first_occur_file.is_some()
178        } else {
179            false
180        };
181
182        if !has_source_info && glyph_len > 0 {
183            let (span, span_offset) = text.glyphs[0].span;
184
185            if let Some((filepath, line, column)) = self.source_code_file_line(span, span_offset) {
186                let uses = self.font_info.get(&font_key).map_or(0, |info| info.uses);
187                self.font_info.insert(
188                    font_key.clone(),
189                    FontInfoValue {
190                        uses,
191                        first_occur_file: Some(filepath),
192                        first_occur_line: Some(line),
193                        first_occur_column: Some(column),
194                    },
195                );
196            }
197        }
198
199        let font_info_value = self.font_info.entry(font_key).or_default();
200        font_info_value.uses = font_info_value.uses.checked_add(glyph_len as u32)?;
201
202        Some(())
203    }
204
205    fn source_code_file_line(&self, span: Span, span_offset: u16) -> Option<(String, u32, u32)> {
206        let world = self.ctx.world();
207        let file_id = span.id()?;
208        let source = world.source(file_id).ok()?;
209        let range = source.range(span)?;
210        let byte_index = range.start + usize::from(span_offset);
211        let byte_index = byte_index.min(range.end - 1);
212        let line = source.byte_to_line(byte_index)?;
213        let column = source.byte_to_column(byte_index)?;
214
215        let filepath = self.ctx.path_for_id(file_id).ok()?;
216        let filepath_str = filepath.as_path().display().to_string();
217
218        Some((filepath_str, line as u32 + 1, column as u32 + 1))
219    }
220
221    fn internal_source(&mut self, source: Arc<DataSource>) -> u32 {
222        if let Some(&id) = self.span_info.get(source.as_ref()) {
223            return id;
224        }
225        let id = self.span_info2.len() as u32;
226        self.span_info2.push(source.as_ref().clone());
227        self.span_info.insert(source, id);
228        id
229    }
230
231    fn compute(&mut self) -> Option<Vec<DocumentFontInfo>> {
232        use ttf_parser::name_id::*;
233        let font_info = std::mem::take(&mut self.font_info)
234            .into_iter()
235            .map(|(font, font_info_value)| {
236                let extra = self.ctx.font_info(font.clone());
237                let info = &font.info();
238                DocumentFontInfo {
239                    name: info.family.clone(),
240                    style: info.variant.style,
241                    weight: info.variant.weight,
242                    stretch: info.variant.stretch,
243                    postscript_name: font.find_name(POST_SCRIPT_NAME),
244                    full_name: font.find_name(FULL_NAME),
245                    family: font.find_name(FAMILY),
246                    fixed_family: Some(info.family.clone()),
247                    source: extra.map(|source| self.internal_source(source)),
248                    index: Some(font.index()),
249                    uses_scale: Some(font_info_value.uses),
250                    uses: None,
251                    first_occur_file: font_info_value.first_occur_file,
252                    first_occur_line: font_info_value.first_occur_line,
253                    first_occur_column: font_info_value.first_occur_column,
254                }
255            })
256            .collect();
257
258        Some(font_info)
259    }
260}