htmx_lsp2/
query_helper.rs

1use std::collections::HashMap;
2
3use tree_sitter::{Node, Point, Query, QueryCursor};
4
5use crate::{
6    htmx_tags::{get_tag, get_tags, Tag},
7    init_hx::LangType,
8    position::{CaptureDetails, Position, PositionDefinition, QueryType},
9    queries::{
10        HX_ANY_HTML, HX_GO_TAGS, HX_HTML, HX_JS_TAGS, HX_NAME, HX_PYTHON_TAGS, HX_RUST_TAGS,
11        HX_VALUE,
12    },
13};
14
15/// Container for all queries. This struct can be cloned and used in other threads.
16/// It doesn't contain state about previous results of query.
17pub struct Queries {
18    /// Check `HTMLQueries` for more info.
19    pub html: HTMLQueries,
20    /// JavaScript/TypeScript query. TreeSitter query for both languages is same.
21    pub javascript: Query,
22    /// Backend tags query. Can be in Python, Rust, Go.
23    pub backend: Query,
24}
25
26impl Clone for Queries {
27    fn clone(&self) -> Self {
28        Self::default()
29    }
30}
31
32impl Default for Queries {
33    fn default() -> Self {
34        Self {
35            html: HTMLQueries::default(),
36            javascript: Query::new(tree_sitter_javascript::language(), HX_JS_TAGS).unwrap(),
37            backend: Query::new(tree_sitter_rust::language(), HX_RUST_TAGS).unwrap(),
38        }
39    }
40}
41
42impl Queries {
43    /// Get query.
44    pub fn get(&self, query: HtmxQuery) -> &Query {
45        match query {
46            HtmxQuery::Html(html) => self.html.get(html),
47            HtmxQuery::JavaScript => &self.javascript,
48            HtmxQuery::Backend => &self.backend,
49        }
50    }
51
52    /// Default backend language is Rust. Change at the beginning to other.
53    pub fn change_backend(&mut self, lang: &str) -> Option<()> {
54        let lang = match lang {
55            "python" => Some((tree_sitter_python::language(), HX_PYTHON_TAGS)),
56            "go" => Some((tree_sitter_go::language(), HX_GO_TAGS)),
57            _ => None,
58        };
59        if let Some(lang) = lang {
60            self.backend = Query::new(lang.0, lang.1).unwrap();
61        }
62        None
63    }
64}
65
66/// HTMLQueries has three queries:
67/// * lsp `HX_HTML`
68/// * name `HX_NAME`
69/// * value `HX_VALUE`   
70pub struct HTMLQueries {
71    lsp: Query,
72    name: Query,
73    value: Query,
74}
75
76impl Default for HTMLQueries {
77    fn default() -> Self {
78        let lsp = Query::new(tree_sitter_html::language(), HX_HTML).unwrap();
79        let name = Query::new(tree_sitter_html::language(), HX_NAME).unwrap();
80        let value = Query::new(tree_sitter_html::language(), HX_VALUE).unwrap();
81        Self { lsp, name, value }
82    }
83}
84
85impl Clone for HTMLQueries {
86    fn clone(&self) -> Self {
87        Self::default()
88    }
89}
90
91impl HTMLQueries {
92    pub fn get(&self, query: HTMLQuery) -> &Query {
93        match query {
94            HTMLQuery::Lsp => &self.lsp,
95            HTMLQuery::Name => &self.name,
96            HTMLQuery::Value => &self.value,
97        }
98    }
99
100    /// Generate new query, for some random, non-htmx attribute.
101    pub fn get_by_attribute_name(name: &str) -> Query {
102        Query::new(
103            tree_sitter_html::language(),
104            &HX_ANY_HTML.replace("NAME", name),
105        )
106        .unwrap()
107    }
108}
109
110/// HTML can have multiple query types.
111pub enum HTMLQuery {
112    Lsp,
113    Name,
114    Value,
115}
116
117/// HtmxQuery
118pub enum HtmxQuery {
119    Html(HTMLQuery),
120    JavaScript,
121    Backend,
122}
123
124impl TryFrom<LangType> for HtmxQuery {
125    type Error = ();
126
127    fn try_from(value: LangType) -> Result<Self, Self::Error> {
128        match value {
129            LangType::Template => Err(()),
130            LangType::JavaScript => Ok(HtmxQuery::JavaScript),
131            LangType::Backend => Ok(HtmxQuery::Backend),
132        }
133    }
134}
135
136/// Capture all query results. No duplicates, except when searching for hx_comment.
137pub fn query_props(
138    node: Node<'_>,
139    source: &str,
140    trigger_point: Point,
141    query: &Query,
142    all: bool,
143) -> HashMap<String, CaptureDetails> {
144    let mut cursor_qry = QueryCursor::new();
145    let capture_names = query.capture_names();
146    let matches = cursor_qry.matches(query, node, source.as_bytes());
147
148    let mut cnt = 0;
149    matches
150        .into_iter()
151        .flat_map(|m| {
152            m.captures
153                .iter()
154                .filter(|capture| all || capture.node.start_position() <= trigger_point)
155        })
156        .fold(HashMap::new(), |mut acc, capture| {
157            let key = capture_names[capture.index as usize].to_owned();
158            let value = if let Ok(capture_value) = capture.node.utf8_text(source.as_bytes()) {
159                capture_value.to_owned()
160            } else {
161                "".to_owned()
162            };
163            if key == "hx_comment" {
164                cnt += 1;
165            }
166            let key = {
167                if all {
168                    format!("{}{cnt}", key)
169                } else {
170                    key
171                }
172            };
173
174            acc.insert(
175                key,
176                CaptureDetails {
177                    value,
178                    end_position: capture.node.end_position(),
179                    start_position: capture.node.start_position(),
180                },
181            );
182
183            acc
184        })
185}
186
187/// Query only attribute name. Can be used in testing.
188pub fn query_name(
189    element: Node<'_>,
190    source: &str,
191    trigger_point: Point,
192    query_type: &QueryType,
193    query: &Query,
194) -> Option<Position> {
195    let props = query_props(element, source, trigger_point, query, false);
196    let attr_name = props.get("attr_name")?;
197    if let Some(unfinished_tag) = props.get("unfinished_tag") {
198        if query_type == &QueryType::Hover {
199            let complete_match = props.get("complete_match");
200            if complete_match.is_some() && trigger_point <= attr_name.end_position {
201                return Some(Position::AttributeName(attr_name.value.to_string()));
202            }
203            return None;
204        } else if query_type == &QueryType::Completion
205            && trigger_point > unfinished_tag.end_position
206        {
207            return Some(Position::AttributeName(String::from("--")));
208        } else if let Some(_capture) = props.get("equal_error") {
209            if query_type == &QueryType::Completion {
210                return None;
211            }
212        }
213    }
214
215    Some(Position::AttributeName(attr_name.value.to_string()))
216}
217
218/// Query for attribute values. Can be used for testing.
219pub fn query_value(
220    element: Node<'_>,
221    source: &str,
222    trigger_point: Point,
223    query_type: &QueryType,
224    query: &Query,
225) -> Option<Position> {
226    let props = query_props(element, source, trigger_point, query, false);
227
228    let attr_name = props.get("attr_name")?;
229    let mut value = String::new();
230    let mut definition = None;
231    let hovered_name = trigger_point < attr_name.end_position && query_type == &QueryType::Hover;
232    if hovered_name {
233        return Some(Position::AttributeName(attr_name.value.to_string()));
234    } else if props.get("open_quote_error").is_some() || props.get("empty_attribute").is_some() {
235        if query_type == &QueryType::Completion {
236            if let Some(quoted) = props.get("quoted_attr_value") {
237                if trigger_point >= quoted.end_position {
238                    return None;
239                }
240            }
241        }
242        return Some(Position::AttributeValue {
243            name: attr_name.value.to_owned(),
244            value: "".to_string(),
245            definition: None,
246        });
247    }
248
249    if let Some(error_char) = props.get("error_char") {
250        if error_char.value == "=" {
251            return None;
252        }
253    };
254
255    if let Some(capture) = props.get("non_empty_attribute") {
256        if trigger_point >= capture.end_position {
257            return None;
258        }
259        if query_type == &QueryType::Hover || query_type == &QueryType::Definition {
260            let mut start = 0;
261            let _ = props.get("attr_value").is_some_and(|s| {
262                value = s.value.to_string();
263                start = s.start_position.column;
264                true
265            });
266            if query_type == &QueryType::Definition {
267                //
268                definition = Some(PositionDefinition::new(start, trigger_point));
269            }
270        }
271    }
272
273    Some(Position::AttributeValue {
274        name: attr_name.value.to_owned(),
275        value,
276        definition,
277    })
278}
279
280/// Query for htmx tags on backend/javascript.
281pub fn query_tag(
282    element: Node<'_>,
283    source: &str,
284    trigger_point: Point,
285    _query_type: &QueryType,
286    query: &Query,
287    full: bool,
288) -> Vec<Tag> {
289    let comments = query_props(element, source, trigger_point, query, full);
290    let mut tags = vec![];
291    for comment in comments {
292        if let Some(mut tag) = get_tag(&comment.1.value) {
293            tag.start.row = comment.1.start_position.row;
294            tag.end.row = comment.1.start_position.row;
295            tags.push(tag);
296        }
297    }
298    tags
299}
300
301/// Capture all tags(if any is found) that matches `tag_name` parameter.
302#[allow(clippy::too_many_arguments)]
303pub fn query_htmx_lsp(
304    element: Node<'_>,
305    source: &str,
306    trigger_point: Point,
307    _query_type: &QueryType,
308    query: &Query,
309    tag_name: &str,
310    references: &mut Vec<Tag>,
311    file: usize,
312) {
313    let lsp_names = query_props(element, source, trigger_point, query, true);
314    for capture in lsp_names {
315        if capture.0.starts_with("attr_value") {
316            let value = capture.1.value;
317            let tags = get_tags(
318                &value,
319                capture.1.start_position.column,
320                capture.1.start_position.row,
321            );
322            if let Some(tags) = tags {
323                let tag = tags.iter().find(|item| item.name == tag_name);
324                if let Some(tag) = tag {
325                    let mut tag = tag.clone();
326                    tag.file = file;
327                    references.push(tag);
328                }
329            }
330            // let position = PositionDefinition::new(capture.1.start_position.row, capture.1.start_position.column);
331            //
332        }
333    }
334}
335
336/// `HX_HTML`
337pub fn find_hx_lsp(
338    element: Node<'_>,
339    source: String,
340    trigger_point: Point,
341    query: &Query,
342) -> Option<CaptureDetails> {
343    let props = query_props(element, &source, trigger_point, query, false);
344    if props.get("attr_name").is_some() {
345        let value = props.get("attr_value")?;
346        return Some(value.clone());
347    }
348    None
349}