tinymist_query/
code_context.rs

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