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#[derive(Debug, Clone, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub struct SpanInfo {
20 pub sources: Vec<DataSource>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26#[serde(rename_all = "camelCase")]
27pub struct AnnotatedContent {
28 pub content: String,
30 pub span_kind: String,
32 pub spans: Vec<i32>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38#[serde(rename_all = "camelCase")]
39pub struct DocumentFontInfo {
40 pub name: String,
43 pub style: FontStyle,
45 pub weight: FontWeight,
47 pub stretch: FontStretch,
49 pub postscript_name: Option<String>,
51 pub family: Option<String>,
53 pub full_name: Option<String>,
55 pub fixed_family: Option<String>,
57 pub source: Option<u32>,
59 pub index: Option<u32>,
61 pub uses_scale: Option<u32>,
65 pub uses: Option<AnnotatedContent>,
68 pub first_occur_file: Option<String>,
71 pub first_occur_line: Option<u32>,
74 pub first_occur_column: Option<u32>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81#[serde(rename_all = "camelCase")]
82pub struct DocumentMetricsResponse {
83 pub span_info: SpanInfo,
85 pub font_info: Vec<DocumentFontInfo>,
87}
88
89#[derive(Debug, Clone)]
93pub struct DocumentMetricsRequest {
94 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}