tinymist_query/
code_context.rs

1use 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/// A query to get the mode at a specific position in a text document.
16#[derive(Debug, Clone, Deserialize)]
17#[serde(tag = "kind", rename_all = "camelCase")]
18pub enum InteractCodeContextQuery {
19    /// Get the mode at a specific position in a text document.
20    ModeAt {
21        /// The position inside the text document.
22        position: LspPosition,
23    },
24    /// Get the style at a specific position in a text document.
25    StyleAt {
26        /// The position inside the text document.
27        position: LspPosition,
28        /// Style to query
29        style: Vec<String>,
30    },
31}
32
33/// A response to a `InteractCodeContextQuery`.
34#[derive(Debug, Clone, Serialize, Deserialize)]
35#[serde(tag = "kind", rename_all = "camelCase")]
36pub enum InteractCodeContextResponse {
37    /// Get the mode at a specific position in a text document.
38    ModeAt {
39        /// The mode at the requested position.
40        mode: InterpretMode,
41    },
42    /// Get the style at a specific position in a text document.
43    StyleAt {
44        /// The style at the requested position.
45        style: Vec<Option<JsonValue>>,
46    },
47}
48
49/// A request to get the code context of a text document.
50#[derive(Debug, Clone, Deserialize)]
51#[serde(tag = "kind")]
52pub struct InteractCodeContextRequest {
53    /// The path to the text document.
54    pub path: PathBuf,
55    /// The queries to execute.
56    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        // Smart special cases that is definitely at markup
151        if pos == 0 || pos >= source.text().len() {
152            return Some(InterpretMode::Markup);
153        }
154
155        // Get mode
156        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}