1use crate::config::{read_config, validate_config, HtmxConfig};
2use crate::htmx_tags::Tag;
3use crate::query_helper::Queries;
4use crate::to_input_edit::ToInputEdit;
5use std::collections::HashMap;
6
7use std::path::Path;
8use std::sync::{Arc, Mutex, RwLock};
9
10use dashmap::DashMap;
11use ropey::Rope;
12
13use serde_json::Value;
14use tower_lsp::jsonrpc::Result;
15use tower_lsp::lsp_types::request::{GotoImplementationParams, GotoImplementationResponse};
16use tower_lsp::lsp_types::{
17 CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionParams,
18 CodeActionProviderCapability, CodeActionResponse, Command, CompletionContext, CompletionItem,
19 CompletionItemKind, CompletionOptions, CompletionParams, CompletionResponse,
20 CompletionTriggerKind, Diagnostic, DidChangeTextDocumentParams, DidCloseTextDocumentParams,
21 DidOpenTextDocumentParams, DidSaveTextDocumentParams, Documentation, ExecuteCommandOptions,
22 ExecuteCommandParams, GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents,
23 HoverParams, HoverProviderCapability, ImplementationProviderCapability, InitializedParams,
24 Location, MarkupContent, MarkupKind, MessageType, OneOf, ReferenceParams, ServerCapabilities,
25 TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions,
26 TextDocumentSyncSaveOptions, Url,
27};
28use tower_lsp::lsp_types::{InitializeParams, ServerInfo};
29use tower_lsp::{lsp_types::InitializeResult, Client, LanguageServer};
30
31use crate::htmx_tree_sitter::LspFiles;
32use crate::init_hx::{init_hx_tags, init_hx_values, HxCompletion, LangType, LangTypes};
33use crate::position::{get_position_from_lsp_completion, Position, QueryType};
34
35pub struct BackendHtmx {
37 pub client: Client,
38 pub document_map: DashMap<String, Rope>,
40 pub hx_attributes: Vec<HxCompletion>,
42 pub hx_attribute_values: HashMap<String, Vec<HxCompletion>>,
44 pub can_complete: RwLock<bool>,
48 pub htmx_config: RwLock<HtmxConfig>,
50 pub lsp_files: Arc<Mutex<LspFiles>>,
53 pub queries: Arc<Mutex<Queries>>,
55}
56
57impl BackendHtmx {
58 pub fn new(client: Client) -> Self {
60 Self {
61 client,
62 document_map: DashMap::new(),
63 hx_attributes: init_hx_tags(),
64 hx_attribute_values: init_hx_values(),
65 can_complete: RwLock::new(false),
66 htmx_config: RwLock::new(HtmxConfig::default()),
67 lsp_files: Arc::new(Mutex::new(LspFiles::default())),
68 queries: Arc::new(Mutex::new(Queries::default())),
69 }
70 }
71
72 fn after_open(&self, params: ServerTextDocumentItem) {
74 let rope = ropey::Rope::from_str(¶ms.text);
75 self.document_map
76 .insert(params.uri.to_string(), rope.clone());
77 }
78
79 async fn publish_tag_diagnostics(&self, diagnostics: Vec<Tag>, file: Option<String>) {
86 let mut hm: HashMap<String, Vec<Diagnostic>> = HashMap::new();
87 let len = diagnostics.len();
88 self.lsp_files
89 .lock()
90 .ok()
91 .and_then(|lsp_files| -> Option<()> {
92 lsp_files.publish_tag_diagnostics(diagnostics, &mut hm);
93 None
94 });
95 for (url, diagnostics) in hm {
96 if let Ok(uri) = Url::parse(&url) {
97 self.client
98 .publish_diagnostics(uri, diagnostics, None)
99 .await;
100 }
101 }
102 if let Some(uri) = file {
103 if len == 0 {
104 let uri = Url::parse(&uri).unwrap();
105 self.client.publish_diagnostics(uri, vec![], None).await;
106 }
107 }
108 }
109
110 fn check_definition(&self, position: Option<Position>) -> Option<GotoDefinitionResponse> {
112 let mut def = None;
113 let _ = position.is_some_and(|position| {
114 if let Position::AttributeValue {
115 name,
116 value,
117 definition,
118 } = position
119 {
120 if &name == "hx-lsp" {
121 self.lsp_files.lock().ok().and_then(|lsp_files| {
122 lsp_files.goto_definition_response(definition, &value, &mut def)
123 });
124 }
125 }
126 true
127 });
128
129 def
130 }
131}
132
133#[tower_lsp::async_trait]
134impl LanguageServer for BackendHtmx {
135 async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
136 let mut definition_provider = None;
137 let mut references_provider = None;
138 let mut code_action_provider = None;
139 let mut implementation_provider = None;
140 let mut execute_command_provider = None;
141
142 if let Some(client_info) = params.client_info {
143 if client_info.name == "helix" {
144 if let Ok(mut can_complete) = self.can_complete.write() {
145 *can_complete = true;
146 }
147 }
148 }
149 match validate_config(params.initialization_options) {
150 Some(htmx_config) => {
151 self.htmx_config
152 .try_write()
153 .ok()
154 .and_then(|mut config| -> Option<()> {
155 definition_provider = Some(OneOf::Left(true));
156 references_provider = Some(OneOf::Left(true));
157 code_action_provider = Some(CodeActionProviderCapability::Simple(true));
158 implementation_provider =
159 Some(ImplementationProviderCapability::Simple(true));
160 execute_command_provider = Some(ExecuteCommandOptions {
161 commands: vec!["reset_tags".to_string()],
162 ..Default::default()
163 });
164 *config = htmx_config;
165 None
166 });
167 }
168 None => {
169 self.client
170 .log_message(MessageType::INFO, "Config not found")
171 .await;
172 }
173 }
174
175 Ok(InitializeResult {
176 capabilities: ServerCapabilities {
177 text_document_sync: Some(TextDocumentSyncCapability::Options(
178 TextDocumentSyncOptions {
179 change: Some(TextDocumentSyncKind::INCREMENTAL),
180 will_save: Some(true),
181 save: Some(TextDocumentSyncSaveOptions::Supported(true)),
182 ..Default::default()
183 },
184 )),
185 completion_provider: Some(CompletionOptions {
186 resolve_provider: Some(false),
187 trigger_characters: Some(vec![
188 "-".to_string(),
189 "\"".to_string(),
190 " ".to_string(),
191 ]),
192 all_commit_characters: None,
193 work_done_progress_options: Default::default(),
194 completion_item: None,
195 }),
196 hover_provider: Some(HoverProviderCapability::Simple(true)),
197 definition_provider,
198 references_provider,
199 code_action_provider,
200 implementation_provider,
201 execute_command_provider,
202 ..ServerCapabilities::default()
203 },
204 server_info: Some(ServerInfo {
205 name: String::from("htmx-lsp"),
206 version: Some(String::from("0.1.3")),
207 }),
208 offset_encoding: None,
209 })
210 }
211
212 async fn initialized(&self, _params: InitializedParams) {
213 self.client
214 .log_message(MessageType::INFO, "initialized!")
215 .await;
216
217 match read_config(
218 &self.htmx_config,
219 &self.lsp_files,
220 &self.queries,
221 &self.document_map,
222 ) {
223 Ok(diagnostics) => {
224 self.publish_tag_diagnostics(diagnostics, None).await;
225 }
226 Err(err) => {
227 let _ = self
228 .htmx_config
229 .write()
230 .ok()
231 .and_then(|mut config| -> Option<()> {
232 config.is_valid = false;
233 None
234 });
235 let msg = err.to_string();
236 self.client.log_message(MessageType::INFO, msg).await;
237 }
238 };
239 }
240
241 async fn did_open(&self, params: DidOpenTextDocumentParams) {
242 let _temp_uri = params.text_document.uri.clone();
243 self.after_open(ServerTextDocumentItem {
244 uri: params.text_document.uri,
245 text: params.text_document.text,
246 });
247 }
248
249 async fn did_close(&self, _: DidCloseTextDocumentParams) {}
250
251 async fn did_save(&self, params: DidSaveTextDocumentParams) {
252 let uri = params.text_document.uri.to_string();
253 let _path = Path::new(&uri);
254 let mut diags = vec![];
255 if let Ok(lsp_files) = self.lsp_files.lock() {
256 if let Some(diagnostics) = lsp_files.saved(
257 &uri,
258 &mut diags,
259 &self.htmx_config,
260 &self.document_map,
261 &self.queries,
262 ) {
263 diags = diagnostics;
264 }
265 }
266 self.publish_tag_diagnostics(diags, Some(uri)).await;
267 }
268
269 async fn did_change(&self, params: DidChangeTextDocumentParams) {
333 let uri = ¶ms.text_document.uri.to_string();
334 let rope = self.document_map.get_mut(uri);
335 let lang_types = self
336 .htmx_config
337 .read()
338 .ok()
339 .and_then(|lang| lang.file_ext(Path::new(uri)));
340 if lang_types.is_none() {
341 return;
342 }
343 let lang_types = lang_types.unwrap();
344 if let Some(mut rope) = rope {
345 for change in params.content_changes {
346 if let Some(range) = &change.range {
347 let input_edit = rope.to_input_edit(*range, &change.text);
348 let start = rope.to_byte(range.start);
349 let end = rope.to_byte(range.end);
350 if start <= end {
351 rope.remove(start..end);
352 } else {
353 rope.remove(end..start);
354 }
355 if !change.text.is_empty() {
356 rope.insert(start, &change.text);
357 }
358 let mut w = FileWriter::default();
359 let _ = rope.write_to(&mut w);
360 self.lsp_files
361 .lock()
362 .ok()
363 .and_then(|lsp_files| match lang_types {
364 LangTypes::One(lang) => {
365 lsp_files.input_edit(uri, w.content, input_edit, lang)
366 }
367 LangTypes::Two { first, second } => {
368 lsp_files.input_edit(uri, w.content.to_string(), input_edit, first);
369 lsp_files.input_edit(uri, w.content, input_edit, second)
370 }
371 });
372 } else {
373 let new_rope = Rope::from_str(&change.text);
374 *rope = new_rope;
375
376 let mut w = FileWriter::default();
377 let _ = rope.write_to(&mut w);
378
379 self.lsp_files
380 .lock()
381 .ok()
382 .and_then(|lsp_files| match lang_types {
383 LangTypes::One(lang) => lsp_files.add_tree(
384 lsp_files.get_index(uri)?,
385 lang,
386 &w.content,
387 None,
388 ),
389 LangTypes::Two { first, second } => {
390 let index = lsp_files.get_index(uri)?;
391 lsp_files.add_tree(index, first, &w.content, None);
392 lsp_files.add_tree(index, second, &w.content, None)
393 }
394 });
395 }
396 }
397 }
398 }
399
400 async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
401 let can_complete = {
402 matches!(
403 params.context,
404 Some(CompletionContext {
405 trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
406 ..
407 }) | Some(CompletionContext {
408 trigger_kind: CompletionTriggerKind::INVOKED,
409 ..
410 })
411 )
412 };
413 if !can_complete {
414 let can_complete = self.can_complete.read().is_ok_and(|d| *d);
415 if !can_complete {
416 return Ok(None);
417 }
418 }
419
420 let uri = ¶ms.text_document_position.text_document.uri;
421 match uri.to_file_path().unwrap().extension().is_some_and(|ext| {
422 self.htmx_config.read().is_ok_and(|config| {
423 if !config.is_valid {
424 return false;
425 }
426 return ext.to_str().unwrap() != config.template_ext;
427 })
428 }) {
429 true => return Ok(None),
430 false => (),
431 }
432 let result = self.queries.lock().ok().and_then(|queries| {
433 get_position_from_lsp_completion(
434 ¶ms.text_document_position,
435 &self.document_map,
436 uri.to_string(),
437 QueryType::Completion,
438 &self.lsp_files,
439 &queries.html,
440 )
441 });
442
443 if let Some(result) = result {
444 match result {
445 Position::AttributeName(name) => {
446 if name.starts_with("hx-") {
447 let completions = self.hx_attributes.clone();
448 let mut ret = Vec::with_capacity(completions.len());
449 for item in completions {
450 ret.push(CompletionItem {
451 label: item.name.to_string(),
452 kind: Some(CompletionItemKind::TEXT),
453 documentation: Some(Documentation::MarkupContent(MarkupContent {
454 kind: MarkupKind::Markdown,
455 value: item.desc.to_string(),
456 })),
457 ..Default::default()
458 });
459 }
460 return Ok(Some(CompletionResponse::Array(ret)));
461 }
462 }
463 Position::AttributeValue { name, .. } => {
464 if let Some(completions) = self.hx_attribute_values.get(&name) {
465 let mut ret = Vec::with_capacity(completions.len());
466 for item in completions {
467 ret.push(CompletionItem {
468 label: item.name.to_string(),
469 detail: Some(item.desc.to_string()),
470 kind: Some(CompletionItemKind::TEXT),
471 ..Default::default()
472 });
473 }
474 return Ok(Some(CompletionResponse::Array(ret)));
475 }
476 return Ok(None);
477 }
478 }
479 }
480 Ok(None)
481 }
482
483 async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
484 let uri = ¶ms.text_document_position_params.text_document.uri;
485 let result = self.queries.lock().ok().and_then(|queries| {
486 get_position_from_lsp_completion(
487 ¶ms.text_document_position_params,
488 &self.document_map,
489 uri.to_string(),
490 QueryType::Hover,
491 &self.lsp_files,
492 &queries.html,
493 )
494 });
495
496 if let Some(result) = result {
497 match result {
498 Position::AttributeName(name) => {
499 if let Some(res) = self
500 .hx_attributes
501 .iter()
502 .find(|x| x.name == name.replace("hx-", ""))
503 .cloned()
504 {
505 let markup_content = MarkupContent {
506 kind: MarkupKind::Markdown,
507 value: res.desc,
508 };
509 let hover_contents = HoverContents::Markup(markup_content);
510 let hover = Hover {
511 contents: hover_contents,
512 range: None,
513 };
514 return Ok(Some(hover));
515 }
516 }
517 Position::AttributeValue { name, value, .. } => {
518 if let Some(res) = self.hx_attribute_values.get(&name) {
519 if let Some(res) = res.iter().find(|x| x.name == value).cloned() {
520 let markup_content = MarkupContent {
521 kind: MarkupKind::Markdown,
522 value: res.desc,
523 };
524 let hover_contents = HoverContents::Markup(markup_content);
525 let hover = Hover {
526 contents: hover_contents,
527 range: None,
528 };
529 return Ok(Some(hover));
530 }
531 }
532 }
533 }
534 }
535
536 Ok(None)
537 }
538
539 async fn goto_definition(
540 &self,
541 params: GotoDefinitionParams,
542 ) -> Result<Option<GotoDefinitionResponse>> {
543 let res = self.lsp_files.lock().ok().and_then(|lsp_files| {
544 self.queries.lock().ok().and_then(|queries| {
545 let position = lsp_files.goto_definition(
546 params,
547 &self.htmx_config,
548 &self.document_map,
549 &queries.html,
550 );
551 drop(queries);
552 drop(lsp_files);
553 self.check_definition(position)
554 })
555 });
556 Ok(res)
557 }
558
559 async fn references(&self, params: ReferenceParams) -> Result<Option<Vec<Location>>> {
560 let mut locations = None;
561 let mut lang_type = LangType::Template;
562 if let Ok(config) = self.htmx_config.read() {
563 if !config.is_valid {
564 return Ok(locations);
565 }
566 let ext = config.file_ext(Path::new(
567 ¶ms.text_document_position.text_document.uri.as_str(),
568 ));
569 if let Some(lang_types) = ext {
570 let lang_type2 = lang_types.get();
571 if lang_type2 == LangType::Template {
572 return Ok(locations);
573 } else {
574 lang_type = lang_type2;
575 }
576 }
577 }
592 locations = self.lsp_files.lock().ok().and_then(|lsp_files| {
593 self.queries.lock().ok().and_then(|queries| {
594 lsp_files.references(params, &queries, &self.document_map, lang_type)
595 })
596 });
597 Ok(locations)
598 }
599
600 async fn goto_implementation(
601 &self,
602 params: GotoImplementationParams,
603 ) -> Result<Option<GotoImplementationResponse>> {
604 let mut res = None;
605 if let Ok(config) = self.htmx_config.read() {
606 if !config.is_valid {
607 return Ok(res);
608 }
609 res = self.lsp_files.lock().ok().and_then(|lsp_files| {
610 self.queries.lock().ok().and_then(|queries| {
611 let lang_types = config.file_ext(Path::new(
612 params
613 .text_document_position_params
614 .text_document
615 .uri
616 .as_str(),
617 ))?;
618 lsp_files.goto_implementation(params, &queries, &self.document_map, lang_types)
619 })
620 });
621 }
622 Ok(res)
623 }
624
625 async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {
626 let mut res = None;
627 if let Ok(config) = self.htmx_config.read() {
628 if !config.is_valid {
629 return Ok(None);
630 }
631 }
632 let position = self.lsp_files.lock().ok().and_then(|lsp_files| {
633 self.queries.lock().ok().and_then(|queries| {
634 lsp_files.code_action(params, &self.htmx_config, &queries.html, &self.document_map)
635 })
636 });
637 if position.is_some() {
638 res = Some(code_actions());
639 }
640
641 Ok(res)
642 }
643
644 async fn execute_command(&self, params: ExecuteCommandParams) -> Result<Option<Value>> {
645 let command = params.command;
646 if command == "reset_tags" {
647 let diags = read_config(
648 &self.htmx_config,
649 &self.lsp_files,
650 &self.queries,
651 &self.document_map,
652 );
653 if let Ok(diags) = diags {
654 self.publish_tag_diagnostics(diags, None).await;
655 }
656 }
657 Ok(None)
658 }
659
660 async fn shutdown(&self) -> Result<()> {
661 Ok(())
662 }
663}
664
665pub fn code_actions() -> Vec<CodeActionOrCommand> {
667 let mut commands = vec![];
668 let command = ("Reset tags", "reset_tags");
669 commands.push(CodeActionOrCommand::CodeAction(CodeAction {
670 title: command.0.to_string(),
671 kind: Some(CodeActionKind::EMPTY),
672 command: Some(Command::new(
673 command.1.to_string(),
674 command.1.to_string(),
675 None,
676 )),
677 ..Default::default()
678 }));
679 commands
680}
681
682pub struct ServerTextDocumentItem {
684 pub uri: Url,
685 pub text: String,
686}
687
688#[derive(Default, Debug)]
690pub struct FileWriter {
691 pub content: String,
692}
693
694impl std::io::Write for FileWriter {
695 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
696 if let Ok(b) = std::str::from_utf8(buf) {
697 self.content.push_str(b);
698 }
699 Ok(buf.len())
700 }
701
702 fn flush(&mut self) -> std::io::Result<()> {
703 Ok(())
704 }
705}