htmx_lsp2/
server.rs

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
35/// BackendHtmx - contains all important parts for htmx-lsp
36pub struct BackendHtmx {
37    pub client: Client,
38    /// Every document is represented as Rope data structure. This lsp support only incremental changes.
39    pub document_map: DashMap<String, Rope>,
40    /// All htmx attributes used for completion and hover.
41    pub hx_attributes: Vec<HxCompletion>,
42    /// All htmx attribute values used for completion and hover.
43    pub hx_attribute_values: HashMap<String, Vec<HxCompletion>>,
44    /// Some clients have no context information about completion request.
45    /// This can help server to suggest htmx values.  
46    /// Client completion starts after trigger character.
47    pub can_complete: RwLock<bool>,
48    /// Configuration for htmx-lsp. Hover and completion can work without it.
49    pub htmx_config: RwLock<HtmxConfig>,
50    /// Main field, responsible for all htmx actions.
51    /// Check `LspFiles` for more information.
52    pub lsp_files: Arc<Mutex<LspFiles>>,
53    /// All tree sitter queries.
54    pub queries: Arc<Mutex<Queries>>,
55}
56
57impl BackendHtmx {
58    /// Initialization of server. `BackendServer` fields are cloneable.
59    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    /// Used after didOpen request.
73    fn after_open(&self, params: ServerTextDocumentItem) {
74        let rope = ropey::Rope::from_str(&params.text);
75        self.document_map
76            .insert(params.uri.to_string(), rope.clone());
77    }
78
79    /// Client notification for `Tag` errors.
80    ///
81    /// Called after:
82    ///  * successful initialization
83    ///  * document save for backend/frontend(tags are saved)
84    ///  * code action  - `reset_tag`.
85    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    /// Go to tag, backend/frontend. This only works when called from template part.
111    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) {
270    //     let uri = &params.text_document.uri.to_string();
271    //     let rope = self.document_map.get_mut(uri);
272    //     let lang_types = self
273    //         .htmx_config
274    //         .read()
275    //         .ok()
276    //         .and_then(|lang| lang.file_ext(Path::new(uri)));
277    //     if lang_types.is_none() {
278    //         return;
279    //     }
280    //     let lang_types = lang_types.unwrap();
281    //     if let Some(mut rope) = rope {
282    //         for change in params.content_changes {
283    //             if let Some(range) = &change.range {
284    //                 let input_edit = range.to_input_edit(&rope);
285    //                 if change.text.is_empty() {
286    //                     self.on_remove(range, &mut rope);
287    //                 } else {
288    //                     self.on_insert(range, &change.text, &mut rope);
289    //                 }
290    //                 let mut w = FileWriter::default();
291    //                 let _ = rope.write_to(&mut w);
292    //                 self.lsp_files
293    //                     .lock()
294    //                     .ok()
295    //                     .and_then(|lsp_files| match lang_types {
296    //                         LangTypes::One(lang) => {
297    //                             lsp_files.input_edit(uri, w.content, input_edit, lang)
298    //                         }
299    //                         LangTypes::Two { first, second } => {
300    //                             lsp_files.input_edit(uri, w.content.to_string(), input_edit, first);
301    //                             lsp_files.input_edit(uri, w.content, input_edit, second)
302    //                         }
303    //                     });
304    //             } else {
305    //                 let new_rope = Rope::from_str(&change.text);
306    //                 *rope = new_rope;
307
308    //                 let mut w = FileWriter::default();
309    //                 let _ = rope.write_to(&mut w);
310
311    //                 self.lsp_files
312    //                     .lock()
313    //                     .ok()
314    //                     .and_then(|lsp_files| match lang_types {
315    //                         LangTypes::One(lang) => lsp_files.add_tree(
316    //                             lsp_files.get_index(uri)?,
317    //                             lang,
318    //                             &w.content,
319    //                             None,
320    //                         ),
321    //                         LangTypes::Two { first, second } => {
322    //                             let index = lsp_files.get_index(uri)?;
323    //                             lsp_files.add_tree(index, first, &w.content, None);
324    //                             lsp_files.add_tree(index, second, &w.content, None)
325    //                         }
326    //                     });
327    //             }
328    //         }
329    //     }
330    // }
331
332    async fn did_change(&self, params: DidChangeTextDocumentParams) {
333        let uri = &params.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 = &params.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                &params.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 = &params.text_document_position_params.text_document.uri;
485        let result = self.queries.lock().ok().and_then(|queries| {
486            get_position_from_lsp_completion(
487                &params.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                &params.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            // match ext
578            //     .and_then(|lang| -> Option<()> {
579            //         lang_type = lang.get();
580            //         if lang_type == LangType::Template {
581            //             None
582            //         } else {
583            //             Some(())
584            //         }
585            //     })
586            //     .is_none()
587            // {
588            //     true => return Ok(locations),
589            //     false => (),
590            // }
591        }
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
665/// Returns available code actions for this language-server.
666pub 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
682/// Used in didOpen request.
683pub struct ServerTextDocumentItem {
684    pub uri: Url,
685    pub text: String,
686}
687
688/// Move content from Rope to this struct.
689#[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}