1use std::{
2 collections::HashMap,
3 sync::{Arc, Mutex},
4};
5
6use dashmap::DashMap;
7use ropey::Rope;
8use tower_lsp::lsp_types::TextDocumentPositionParams;
9use tree_sitter::{Node, Point};
10
11use crate::{
12 htmx_tree_sitter::LspFiles,
13 init_hx::LangType,
14 query_helper::{query_name, query_value, HTMLQueries, HTMLQuery},
15};
16
17#[derive(PartialEq, Eq)]
20pub enum QueryType {
21 Hover,
22 Completion,
23 Definition,
24}
25
26#[derive(Debug, Clone)]
28pub struct CaptureDetails {
29 pub start_position: Point,
30 pub end_position: Point,
31 pub value: String,
32}
33
34#[derive(Debug, Clone, PartialEq)]
40pub enum Position {
41 AttributeName(String),
42 AttributeValue {
43 name: String,
44 value: String,
45 definition: Option<PositionDefinition>,
46 },
47}
48
49#[derive(Debug, Clone, PartialEq)]
50pub struct PositionDefinition {
51 pub point: Point,
52 pub start: usize,
53}
54
55impl PositionDefinition {
56 pub fn new(start: usize, point: Point) -> Self {
57 Self { point, start }
58 }
59}
60
61pub fn get_position_from_lsp_completion(
63 text_params: &TextDocumentPositionParams,
64 text: &DashMap<String, Rope>,
65 uri: String,
66 query_type: QueryType,
67 lsp_files: &Arc<Mutex<LspFiles>>,
68 query: &HTMLQueries,
69) -> Option<Position> {
70 let text = text.get(&uri)?;
71 let text = text.to_string();
72 let pos = text_params.position;
73
74 if let Ok(lsp_files) = lsp_files.lock() {
75 if let Some(index) = lsp_files.get_index(&uri) {
76 lsp_files.query_position(index, &text, query_type, pos, query)
77 } else if let Some(index) = lsp_files.add_file(String::from(&uri)) {
78 lsp_files.add_tree(index, LangType::Template, &text, None);
79 lsp_files.query_position(index, &text, query_type, pos, query)
80 } else {
81 None
82 }
83 } else {
84 None
85 }
86}
87
88fn find_element_referent_to_current_node(node: Node<'_>) -> Option<Node<'_>> {
89 if node.kind() == "element" || node.kind() == "fragment" {
90 return Some(node);
91 }
92
93 return find_element_referent_to_current_node(node.parent()?);
94}
95
96pub fn query_position(
98 root: Node<'_>,
99 source: &str,
100 trigger_point: Point,
101 query_type: QueryType,
102 query: &HTMLQueries,
103) -> Option<Position> {
104 let closest_node = root.descendant_for_point_range(trigger_point, trigger_point)?;
105 let element = find_element_referent_to_current_node(closest_node)?;
106
107 let name = query_name(
108 element,
109 source,
110 trigger_point,
111 &query_type,
112 query.get(HTMLQuery::Name),
113 );
114 if name.is_some() {
115 return name;
116 }
117 query_value(
118 element,
119 source,
120 trigger_point,
121 &query_type,
122 query.get(HTMLQuery::Value),
123 )
124}
125
126#[allow(dead_code)]
128pub fn dbg_props(props: &HashMap<String, CaptureDetails>) {
129 for i in props {
130 dbg!(i);
131 }
132}
133
134pub fn completion_position(props: HashMap<String, CaptureDetails>) -> Option<Position> {
136 let attr_name = props.get("attr_name")?;
137
138 if let Some(_capture) = props.get("with_attr_name_with_equals_err") {
139 None
140 } else if let Some(_capture) = props.get("with_attr_name_without_value_t") {
141 Some(Position::AttributeName(attr_name.value.to_string()))
142 } else if let Some(_capture) = props.get("with_attr_value_empty") {
143 Some(Position::AttributeValue {
144 name: attr_name.value.to_string(),
145 value: String::new(),
146 definition: None,
147 })
148 } else if let Some(_capture) = props.get("with_attr_value_not_empty") {
149 Some(Position::AttributeValue {
150 name: attr_name.value.to_string(),
151 value: String::new(),
152 definition: None,
153 })
154 } else {
155 props
156 .get("with_error_with_value_t_no_second_quote")
157 .map(|_capture| Position::AttributeValue {
158 name: attr_name.value.to_string(),
159 value: String::new(),
160 definition: None,
161 })
162 }
163}
164
165pub fn hover_position(
167 props: HashMap<String, CaptureDetails>,
168 client_point: Point,
169) -> Option<Position> {
170 let attr_name = props.get("attr_name")?;
171 if let Some(capture) = props.get("with_attr_value_not_empty") {
172 if client_point > capture.end_position {
173 return None;
174 }
175 let attr_value = props.get("attr_value");
176 if let Some(capture) = attr_value {
177 if client_point >= attr_name.end_position {
178 return Some(Position::AttributeValue {
179 name: attr_name.value.to_string(),
180 value: capture.value.to_string(),
181 definition: None,
182 });
183 }
184 }
185 if client_point <= attr_name.end_position {
186 return Some(Position::AttributeName(attr_name.value.to_string()));
187 }
188 None
189 } else if let Some(capture) = props.get("with_attr_value_empty") {
190 if client_point > capture.end_position {
191 return None;
192 }
193 let attr_value = props.get("attr_value");
194 match attr_value {
195 Some(capture) => Some(Position::AttributeValue {
196 name: attr_name.value.to_string(),
197 value: capture.value.to_string(),
198 definition: None,
199 }),
200 None => Some(Position::AttributeName(attr_name.value.to_string())),
201 }
202 } else {
203 None
204 }
205}
206
207#[cfg(test)]
208mod tests1 {
209 use tree_sitter::{Parser, Point};
210
211 use crate::{
212 position::{query_position, Position, QueryType},
213 query_helper::{query_props, HTMLQueries, Queries},
214 };
215
216 fn prepare_tree(text: &str) -> tree_sitter::Tree {
217 let language = tree_sitter_html::language();
218 let mut parser = Parser::new();
219
220 parser
221 .set_language(language)
222 .expect("could not load html grammer");
223
224 parser.parse(text, None).expect("not to fail")
225 }
226
227 fn prepare_python_tree(text: &str) -> tree_sitter::Tree {
228 let language = tree_sitter_python::language();
229 let mut parser = Parser::new();
230
231 parser
232 .set_language(language)
233 .expect("could not load html grammer");
234
235 parser.parse(text, None).expect("not to fail")
236 }
237
238 #[test]
239 fn suggests_attr_names_when_starting_tag() {
240 let text = r##"<div hx- ></div>"##;
241
242 let tree = prepare_tree(text);
243
244 let query = HTMLQueries::default();
245 let matches = query_position(
246 tree.root_node(),
247 text,
248 Point::new(0, 8),
249 QueryType::Completion,
250 &query,
251 );
252 assert_eq!(matches, Some(Position::AttributeName("hx-".to_string())));
253 }
254
255 #[test]
256 fn does_not_suggest_when_quote_not_initiated() {
257 let text = r##"<div hx-swap= ></div>"##;
258
259 let tree = prepare_tree(text);
260
261 let query = HTMLQueries::default();
262 let matches = query_position(
263 tree.root_node(),
264 text,
265 Point::new(0, 13),
266 QueryType::Completion,
267 &query,
268 );
269
270 assert_eq!(matches, None);
271 }
272
273 #[test]
274 fn suggests_attr_values_when_starting_quote_value() {
275 let text = r#"<div hx-swap=" ></div>"#;
276
277 let tree = prepare_tree(text);
278
279 let query = HTMLQueries::default();
280 let matches = query_position(
281 tree.root_node(),
282 text,
283 Point::new(0, 14),
284 QueryType::Completion,
285 &query,
286 );
287
288 assert_eq!(
289 matches,
290 Some(Position::AttributeValue {
291 name: "hx-swap".to_string(),
292 value: "".to_string(),
293 definition: None
294 })
295 );
296 }
297
298 #[test]
299 fn suggests_attr_values_when_open_and_closed_quotes() {
300 let text = r#"<div hx-swap=""></div>"#;
301
302 let tree = prepare_tree(text);
303
304 let query = HTMLQueries::default();
305 let matches = query_position(
306 tree.root_node(),
307 text,
308 Point::new(0, 13),
309 QueryType::Completion,
310 &query,
311 );
312
313 assert_eq!(
314 matches,
315 Some(Position::AttributeValue {
316 name: "hx-swap".to_string(),
317 value: "".to_string(),
318 definition: None
319 })
320 );
321 }
322
323 #[test]
324 fn suggests_attr_values_once_opening_quotes_in_between_tags() {
325 let text = r#"<div id="fa" hx-swap="hx-swap" hx-swap="hx-swap">
326 <span hx-target="
327 <button>Click me</button>
328 </div>
329 "#;
330
331 let tree = prepare_tree(text);
332
333 let query = HTMLQueries::default();
334 let matches = query_position(
335 tree.root_node(),
336 text,
337 Point::new(1, 23),
338 QueryType::Completion,
339 &query,
340 );
341
342 assert_eq!(
343 matches,
344 Some(Position::AttributeValue {
345 name: "hx-target".to_string(),
346 value: "".to_string(),
347 definition: None
348 })
349 );
350 }
351
352 #[test]
353 fn suggests_attr_names_for_incomplete_attr_in_between_tags() {
354 let text = r#"<div id="fa" hx-target="this" hx-swap="hx-swap">
355 <span hx-
356 <button>Click me</button>
357 </div>
358 "#;
359
360 let tree = prepare_tree(text);
361
362 let query = HTMLQueries::default();
363 let matches = query_position(
364 tree.root_node(),
365 text,
366 Point::new(1, 14),
367 QueryType::Completion,
368 &query,
369 );
370
371 assert_eq!(matches, Some(Position::AttributeName("hx-".to_string())));
372 }
373
374 #[test]
375 fn matches_more_than_one_attribute() {
376 let text = r#"<div hx-get="/foo" hx-target="this" hx- ></div>"#;
377
378 let tree = prepare_tree(text);
379
380 let query = HTMLQueries::default();
381 let matches = query_position(
382 tree.root_node(),
383 text,
384 Point::new(0, 39),
385 QueryType::Completion,
386 &query,
387 );
388
389 assert_eq!(matches, Some(Position::AttributeName("hx-".to_string())));
390 }
391
392 #[test]
393 fn suggests_attr_value_when_attr_is_empty_and_in_between_attributes() {
394 let text = r##"<div hx-get="/foo" hx-target="" hx-swap="#swap"></div>
395 "##;
396
397 let tree = prepare_tree(text);
398
399 let query = HTMLQueries::default();
400 let matches = query_position(
401 tree.root_node(),
402 text,
403 Point::new(0, 30),
404 QueryType::Completion,
405 &query,
406 );
407
408 assert_eq!(
409 matches,
410 Some(Position::AttributeValue {
411 name: "hx-target".to_string(),
412 value: "".to_string(),
413 definition: None
414 })
415 );
416 }
417
418 #[test]
419 fn suggests_attr_values_for_incoplete_quoted_attr_when_in_between_attributes() {
420 let text = r##"<div hx-get="/foo" hx-target=" hx-swap="#swap"></div>"##;
421
422 let tree = prepare_tree(text);
423
424 let query = HTMLQueries::default();
425 let matches = query_position(
426 tree.root_node(),
427 text,
428 Point::new(0, 30),
429 QueryType::Completion,
430 &query,
431 );
432
433 assert_eq!(
434 matches,
435 Some(Position::AttributeValue {
436 name: "hx-target".to_string(),
437 value: "".to_string(),
438 definition: None
439 })
440 );
441 }
442
443 #[test]
444 fn suggests_attr_names_for_incoplete_quoted_value_in_between_attributes() {
445 let text = r##"<div hx-get="/foo" hx- hx-swap="#swap"></div>
446 <span class="foo" />"##;
447
448 let tree = prepare_tree(text);
449
450 let query = HTMLQueries::default();
451 let matches = query_position(
452 tree.root_node(),
453 text,
454 Point::new(0, 22),
455 QueryType::Completion,
456 &query,
457 );
458
459 assert_eq!(matches, Some(Position::AttributeName("hx-".to_string())));
460 }
461
462 #[test]
463 fn suggests_attribute_keys_when_half_completeded() {
464 let text = r##"<div hx-get="/foo" hx-t hx-swap="#swap"></div>
465 <span class="foo" />"##;
466
467 let tree = prepare_tree(text);
468
469 let query = HTMLQueries::default();
470 let matches = query_position(
471 tree.root_node(),
472 text,
473 Point::new(0, 23),
474 QueryType::Completion,
475 &query,
476 );
477
478 assert_eq!(matches, Some(Position::AttributeName("hx-t".to_string())));
479 }
480
481 #[test]
482 fn suggests_values_for_already_filled_attributes() {
483 let text = r##"<div hx-get="/foo" hx-target="find " hx-swap="#swap"></div>"##;
484
485 let tree = prepare_tree(text);
486
487 let query = HTMLQueries::default();
488 let matches = query_position(
489 tree.root_node(),
490 text,
491 Point::new(0, 35),
492 QueryType::Hover,
493 &query,
494 );
495
496 assert_eq!(
497 matches,
498 Some(Position::AttributeValue {
499 name: "hx-target".to_string(),
500 value: "find ".to_string(),
501 definition: None
502 })
503 );
504 }
505
506 #[test]
507 fn does_not_suggest_when_cursor_isnt_within_a_htmx_attribute() {
508 let text = r#"<div hx-get="/foo" class="p-4" ></div>"#;
509
510 let tree = prepare_tree(text);
511
512 let query = HTMLQueries::default();
513 let matches = query_position(
514 tree.root_node(),
515 text,
516 Point::new(0, 24),
517 QueryType::Hover,
518 &query,
519 );
520
521 assert_eq!(matches, None);
522 }
523
524 #[test]
525 fn hover_hx_tags() {
526 let cases = [
527 (
528 r#"<div hx-get="/foo" class="p-4" hx-target="closest" ></div>"#,
529 Point::new(0, 37),
530 Some(Position::AttributeName(String::from("hx-target"))),
531 ),
532 (
533 r#"<div hx-get="" class="p-4" hx-target="" ></div>"#,
534 Point::new(0, 9),
535 Some(Position::AttributeName(String::from("hx-get"))),
536 ),
537 (
538 r#"<div hx-get="/foo" hx-target="closest" hx-swap="outerHTML" hx-swap="swap"></div>"#,
539 Point::new(0, 9),
540 Some(Position::AttributeName(String::from("hx-get"))),
541 ),
542 (
543 r#"<a hx-swap="" hx-patch="/route" hx-validate"#,
544 Point::new(0, 40),
545 Some(Position::AttributeName(String::from("hx-validate"))),
546 ),
547 ];
548
549 for case in cases {
550 let text = case.0;
551 let tree = prepare_tree(text);
552 let query = HTMLQueries::default();
553 let matches = query_position(tree.root_node(), text, case.1, QueryType::Hover, &query);
554 assert_eq!(matches, case.2);
555 }
556 }
557
558 #[test]
559 fn unfinished_tag_name() {
560 let cases = [(
561 r#"<a hx-swap class="text-2xl">
562
563</a>
564
565 "#,
566 Point::new(1, 5),
567 QueryType::Completion,
568 )];
569 for case in cases {
570 let text = case.0;
571 let tree = prepare_tree(text);
572 let query = HTMLQueries::default();
573 let matches = query_position(tree.root_node(), text, case.1, case.2, &query);
574 assert_eq!(matches, Some(Position::AttributeName(String::from("--"))));
575 }
576 }
577
578 #[test]
579 fn python_tags() {
580 let case = r#"
581def a():
582 # hx@hello
583 # hx@world
584 print("hello world")
585 # hx@hello_world
586 "#;
587 let tree = prepare_python_tree(case);
588 let trigger_point = Point::new(0, 0);
589 let closest_node = tree.root_node();
590 let mut query = Queries::default();
591 query.change_backend("python");
592 let query = &query.backend;
593 let props = query_props(closest_node, case, trigger_point, query, true);
594 assert_eq!(props.len(), 3);
595 }
596}