tinymist_query/
code_context.rs1use serde::{Deserialize, Serialize};
2use tinymist_analysis::analyze_expr;
3use tinymist_world::ShadowApi;
4use typst::{
5 foundations::{Bytes, IntoValue, StyleChain},
6 text::TextElem,
7};
8use typst_shim::syntax::LinkedNodeExt;
9
10use crate::{
11 prelude::*,
12 syntax::{InterpretMode, interpret_mode_at},
13};
14
15#[derive(Debug, Clone, Deserialize)]
17#[serde(tag = "kind", rename_all = "camelCase")]
18pub enum InteractCodeContextQuery {
19 ModeAt {
21 position: LspPosition,
23 },
24 StyleAt {
26 position: LspPosition,
28 style: Vec<String>,
30 },
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35#[serde(tag = "kind", rename_all = "camelCase")]
36pub enum InteractCodeContextResponse {
37 ModeAt {
39 mode: InterpretMode,
41 },
42 StyleAt {
44 style: Vec<Option<JsonValue>>,
46 },
47}
48
49#[derive(Debug, Clone, Deserialize)]
51#[serde(tag = "kind")]
52pub struct InteractCodeContextRequest {
53 pub path: PathBuf,
55 pub query: Vec<Option<InteractCodeContextQuery>>,
57}
58
59impl SemanticRequest for InteractCodeContextRequest {
60 type Response = Vec<Option<InteractCodeContextResponse>>;
61
62 fn request(self, ctx: &mut LocalContext) -> Option<Self::Response> {
63 let mut responses = Vec::new();
64
65 let source = ctx.source_by_path(&self.path).ok()?;
66
67 for query in self.query {
68 responses.push(query.and_then(|query| match query {
69 InteractCodeContextQuery::ModeAt { position } => {
70 let cursor = ctx.to_typst_pos(position, &source)?;
71 let mode = Self::mode_at(&source, cursor)?;
72 Some(InteractCodeContextResponse::ModeAt { mode })
73 }
74 InteractCodeContextQuery::StyleAt { position, style } => {
75 let mut world = ctx.world().clone();
76 log::info!(
77 "style at position {position:?} . {style:?} when main is {:?}",
78 world.main()
79 );
80 let cursor = ctx.to_typst_pos(position, &source)?;
81 let root = LinkedNode::new(source.root());
82 let mut leaf = root.leaf_at_compat(cursor)?;
83 log::info!("style at leaf {leaf:?} . {style:?}");
84
85 if !matches!(leaf.kind(), SyntaxKind::Text | SyntaxKind::MathText) {
86 return None;
87 }
88
89 if matches!(leaf.parent_kind(), Some(SyntaxKind::Raw)) {
90 leaf = leaf.parent()?.clone();
91 }
92
93 let mode = Self::mode_at(&source, cursor);
94 if !matches!(
95 mode,
96 Some(InterpretMode::Code | InterpretMode::Markup | InterpretMode::Math)
97 ) {
98 leaf = leaf.parent()?.clone();
99 }
100 let mut mapped_source = source.clone();
101 let (with, offset) = match mode {
102 Some(InterpretMode::Code) => ("context text.font", 8),
103 _ => ("#context text.font", 10),
104 };
105 let start = leaf.range().start;
106 mapped_source.edit(leaf.range(), with);
107
108 let _ = world.map_shadow_by_id(
109 mapped_source.id(),
110 Bytes::new(mapped_source.text().as_bytes().to_vec()),
111 );
112 world.take_db();
113
114 let root = LinkedNode::new(mapped_source.root());
115 let leaf = root.leaf_at_compat(start + offset)?;
116
117 log::info!("style at new_leaf {leaf:?} . {style:?}");
118
119 let mut cursor_styles = analyze_expr(&world, &leaf)
120 .iter()
121 .filter_map(|s| s.1.clone())
122 .collect::<Vec<_>>();
123 cursor_styles.sort_by_key(|x| x.as_slice().len());
124 log::info!("style at styles {cursor_styles:?} . {style:?}");
125 let cursor_style = cursor_styles.into_iter().next_back().unwrap_or_default();
126 let cursor_style = StyleChain::new(&cursor_style);
127
128 log::info!("style at style {cursor_style:?} . {style:?}");
129
130 let style = style
131 .iter()
132 .map(|style| Self::style_at(cursor_style, style))
133 .collect();
134 let _ = world.map_shadow_by_id(
135 mapped_source.id(),
136 Bytes::new(source.text().as_bytes().to_vec()),
137 );
138
139 Some(InteractCodeContextResponse::StyleAt { style })
140 }
141 }));
142 }
143
144 Some(responses)
145 }
146}
147
148impl InteractCodeContextRequest {
149 fn mode_at(source: &Source, pos: usize) -> Option<InterpretMode> {
150 if pos == 0 || pos >= source.text().len() {
152 return Some(InterpretMode::Markup);
153 }
154
155 let root = LinkedNode::new(source.root());
157 Some(interpret_mode_at(root.leaf_at_compat(pos).as_ref()))
158 }
159
160 fn style_at(cursor_style: StyleChain, style: &str) -> Option<JsonValue> {
161 match style {
162 "text.font" => {
163 let font = cursor_style.get_cloned(TextElem::font).into_value();
164 serde_json::to_value(font).ok()
165 }
166 _ => None,
167 }
168 }
169}