diff --git a/Cargo.lock b/Cargo.lock index 8900162..2dcee0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -278,8 +278,10 @@ dependencies = [ "htmx-lsp-util", "log", "lsp-server", + "lsp-textdocument", "lsp-types", "maplit", + "once_cell", "phf", "serde", "serde_json", @@ -368,11 +370,21 @@ dependencies = [ "serde_json", ] +[[package]] +name = "lsp-textdocument" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62dcaf776a57a63c3baafa3ab0ae25943049865c862980522a5112b1fd849503" +dependencies = [ + "lsp-types", + "serde_json", +] + [[package]] name = "lsp-types" -version = "0.94.0" +version = "0.94.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b63735a13a1f9cd4f4835223d828ed9c2e35c8c5e61837774399f558b6a1237" +checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1" dependencies = [ "bitflags 1.3.2", "serde", @@ -413,9 +425,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "parking_lot" diff --git a/lsp/Cargo.toml b/lsp/Cargo.toml index e16324c..56bfb4f 100644 --- a/lsp/Cargo.toml +++ b/lsp/Cargo.toml @@ -20,3 +20,5 @@ tree-sitter.workspace = true tree-sitter-html.workspace = true maplit = "1.0.2" phf = { version = "0.11.2", features = ["macros"] } +lsp-textdocument = "0.3.2" +once_cell = "1.19.0" diff --git a/lsp/src/handle.rs b/lsp/src/handle.rs index be04e74..8d9f238 100644 --- a/lsp/src/handle.rs +++ b/lsp/src/handle.rs @@ -1,42 +1,15 @@ use crate::{ htmx::{hx_completion, hx_hover, HxCompletion}, - text_store::TEXT_STORE, + text_store::{DocInfo, DOCUMENT_STORE}, + tree_sitter::text_doc_change_to_ts_edit, }; use log::{debug, error, warn}; use lsp_server::{Message, Notification, Request, RequestId}; -use lsp_types::{CompletionContext, CompletionParams, CompletionTriggerKind, HoverParams}; - -#[derive(serde::Deserialize, Debug)] -struct Text { - text: String, -} - -#[derive(serde::Deserialize, Debug)] -struct TextDocumentLocation { - uri: String, -} - -#[derive(serde::Deserialize, Debug)] -struct TextDocumentChanges { - #[serde(rename = "textDocument")] - text_document: TextDocumentLocation, - - #[serde(rename = "contentChanges")] - content_changes: Vec, -} - -#[derive(serde::Deserialize, Debug)] -struct TextDocumentOpened { - uri: String, - - text: String, -} - -#[derive(serde::Deserialize, Debug)] -struct TextDocumentOpen { - #[serde(rename = "textDocument")] - text_document: TextDocumentOpened, -} +use lsp_textdocument::FullTextDocument; +use lsp_types::{ + notification::{DidChangeTextDocument, DidOpenTextDocument}, + CompletionContext, CompletionParams, CompletionTriggerKind, HoverParams, +}; #[derive(Debug)] pub struct HtmxAttributeCompletion { @@ -61,41 +34,84 @@ pub enum HtmxResult { // ignore snakeCase #[allow(non_snake_case)] fn handle_didChange(noti: Notification) -> Option { - let text_document_changes: TextDocumentChanges = serde_json::from_value(noti.params).ok()?; - let uri = text_document_changes.text_document.uri; - let text = text_document_changes.content_changes[0].text.to_string(); - - if text_document_changes.content_changes.len() > 1 { - error!("more than one content change, please be wary"); + match cast_notif::(noti) { + Ok(params) => { + match DOCUMENT_STORE + .get() + .expect("text store not initialized") + .lock() + .expect("text store mutex poisoned") + .get_mut(params.text_document.uri.as_str()) + { + Some(entry) => { + entry + .doc + .update(¶ms.content_changes, params.text_document.version); + + if let Some(ref mut curr_tree) = entry.tree { + for edit in params.content_changes.iter() { + match text_doc_change_to_ts_edit(edit, &entry.doc) { + Ok(edit) => { + curr_tree.edit(&edit); + } + Err(e) => { + error!("handle_didChange Bad edit info, failed to edit tree -- Error: {e}"); + } + } + } + } else { + error!( + "handle_didChange tree for {} is None", + params.text_document.uri.as_str() + ); + } + } + None => { + error!( + "handle_didChange No corresponding doc for supplied edits -- {}", + params.text_document.uri.as_str() + ); + } + } + } + Err(e) => { + error!("Failed the deserialize DidChangeTextDocument params -- Error {e}"); + } } - TEXT_STORE - .get() - .expect("text store not initialized") - .lock() - .expect("text store mutex poisoned") - .insert(uri, text); - None } #[allow(non_snake_case)] fn handle_didOpen(noti: Notification) -> Option { debug!("handle_didOpen params {:?}", noti.params); - let text_document_changes = match serde_json::from_value::(noti.params) { - Ok(p) => p.text_document, + let text_doc_open = match cast_notif::(noti) { + Ok(params) => params, Err(err) => { error!("handle_didOpen parsing params error : {:?}", err); return None; } }; - TEXT_STORE + let doc = FullTextDocument::new( + text_doc_open.text_document.language_id, + text_doc_open.text_document.version, + text_doc_open.text_document.text, + ); + let mut parser = ::tree_sitter::Parser::new(); + parser + .set_language(tree_sitter_html::language()) + .expect("Failed to load HTML grammar"); + let tree = parser.parse(doc.get_content(None), None); + + let doc = DocInfo { doc, parser, tree }; + + DOCUMENT_STORE .get() .expect("text store not initialized") .lock() .expect("text store mutex poisoned") - .insert(text_document_changes.uri, text_document_changes.text); + .insert(text_doc_open.text_document.uri.to_string(), doc); None } @@ -116,8 +132,22 @@ fn handle_completion(req: Request) -> Option { .. }) => { let items = match hx_completion(completion.text_document_position) { - Some(items) => items, - None => { + (Some(items), Some(ext_items)) => { + let mut temp = items.to_vec(); + for ext_item in ext_items { + temp.append(&mut ext_item.to_vec()); + } + temp + } + (Some(items), None) => items.to_vec(), + (None, Some(ext_items)) => { + let mut temp = Vec::new(); + for ext_item in ext_items { + temp.append(&mut ext_item.to_vec()); + } + temp + } + (None, None) => { error!("EMPTY RESULTS OF COMPLETION"); return None; } @@ -129,7 +159,7 @@ fn handle_completion(req: Request) -> Option { ); Some(HtmxResult::AttributeCompletion(HtmxAttributeCompletion { - items: items.to_vec(), + items, id: req.id, })) } @@ -186,10 +216,23 @@ pub fn handle_other(msg: Message) -> Option { None } +fn cast_notif(notif: Notification) -> anyhow::Result +where + R: lsp_types::notification::Notification, + R::Params: serde::de::DeserializeOwned, +{ + match notif.extract(R::METHOD) { + Ok(value) => Ok(value), + Err(e) => Err(anyhow::anyhow!( + "cast_notif Failed to extract params -- Error: {e}" + )), + } +} + #[cfg(test)] mod tests { use super::{handle_request, HtmxResult, Request}; - use crate::text_store::{init_text_store, TEXT_STORE}; + use crate::text_store::{init_text_store, DocInfo, DOCUMENT_STORE}; use std::sync::Once; static SETUP: Once = Once::new(); @@ -198,12 +241,21 @@ mod tests { init_text_store(); }); - TEXT_STORE + let doc = + lsp_textdocument::FullTextDocument::new("html".to_string(), 0, content.to_string()); + let mut parser = ::tree_sitter::Parser::new(); + parser + .set_language(tree_sitter_html::language()) + .expect("Failed to load HTML grammar"); + let tree = parser.parse(doc.get_content(None), None); + let doc_info = DocInfo { doc, parser, tree }; + + DOCUMENT_STORE .get() .expect("text store not initialized") .lock() .expect("text store mutex poisoned") - .insert(file.to_string(), content.to_string()); + .insert(file.to_string(), doc_info); } #[test] diff --git a/lsp/src/htmx/attributes/class-tools/classes.md b/lsp/src/htmx/attributes/class-tools/classes.md new file mode 100644 index 0000000..ade7f4e --- /dev/null +++ b/lsp/src/htmx/attributes/class-tools/classes.md @@ -0,0 +1,15 @@ +A classes attribute value consists of “runs”, which are separated by an & character. All class operations within a given run will be applied sequentially, with the delay specified. + +Within a run, a , character separates distinct class operations. + +A class operation is an operation name add, remove, or toggle, followed by a CSS class name, optionally followed by a colon : and a time delay. + +
+
+
+
+
+
+
diff --git a/lsp/src/htmx/attributes/class-tools/data-classes.md b/lsp/src/htmx/attributes/class-tools/data-classes.md new file mode 100644 index 0000000..dceddb3 --- /dev/null +++ b/lsp/src/htmx/attributes/class-tools/data-classes.md @@ -0,0 +1,15 @@ +A data-classes attribute value consists of “runs”, which are separated by an & character. All class operations within a given run will be applied sequentially, with the delay specified. + +Within a run, a , character separates distinct class operations. + +A class operation is an operation name add, remove, or toggle, followed by a CSS class name, optionally followed by a colon : and a time delay. + +
+
+
+
+
+
+
diff --git a/lsp/src/htmx/attributes/client-side-templates/handlebars-template.md b/lsp/src/htmx/attributes/client-side-templates/handlebars-template.md new file mode 100644 index 0000000..6dc0a9f --- /dev/null +++ b/lsp/src/htmx/attributes/client-side-templates/handlebars-template.md @@ -0,0 +1,10 @@ +Looks up a handlebars + + + +If htmx receives a request containing this new head tag: + + + + + + + + +Then the following operations will occur: + + will be left alone + will be removed from the head + will be added to the head + will be removed from the head + will be left alone + will be added to the head + +The final head element will look like this: + + + + + + + diff --git a/lsp/src/htmx/attributes/include-vals/include-vals.md b/lsp/src/htmx/attributes/include-vals/include-vals.md new file mode 100644 index 0000000..17c0b6f --- /dev/null +++ b/lsp/src/htmx/attributes/include-vals/include-vals.md @@ -0,0 +1,8 @@ + The value of this attribute is one or more name/value pairs, which will be evaluated as the fields in a javascript object literal. + +Usage: +
+
+ Will Include Additional Values +
+
diff --git a/lsp/src/htmx/attributes/loading-states/data-loading-aria-busy.md b/lsp/src/htmx/attributes/loading-states/data-loading-aria-busy.md new file mode 100644 index 0000000..5b257e7 --- /dev/null +++ b/lsp/src/htmx/attributes/loading-states/data-loading-aria-busy.md @@ -0,0 +1,4 @@ +Adds `aria-busy="true"` to the element for the duration of the request + +Example: + diff --git a/lsp/src/htmx/attributes/loading-states/data-loading-class-remove.md b/lsp/src/htmx/attributes/loading-states/data-loading-class-remove.md new file mode 100644 index 0000000..87ab830 --- /dev/null +++ b/lsp/src/htmx/attributes/loading-states/data-loading-class-remove.md @@ -0,0 +1,6 @@ +Removes, then adds back, CSS classes from the element while loading. + +Example: +
+ ... +
diff --git a/lsp/src/htmx/attributes/loading-states/data-loading-class.md b/lsp/src/htmx/attributes/loading-states/data-loading-class.md new file mode 100644 index 0000000..bf62bf5 --- /dev/null +++ b/lsp/src/htmx/attributes/loading-states/data-loading-class.md @@ -0,0 +1,5 @@ +Adds, then removes, CSS classes to the element while loading + +
+ ... +
diff --git a/lsp/src/htmx/attributes/loading-states/data-loading-delay.md b/lsp/src/htmx/attributes/loading-states/data-loading-delay.md new file mode 100644 index 0000000..15d1d19 --- /dev/null +++ b/lsp/src/htmx/attributes/loading-states/data-loading-delay.md @@ -0,0 +1,7 @@ +This attribute ensures that the loading state changes are applied only after 200ms if the request is not finished. The default delay can be modified through the attribute value and expressed in milliseconds: + +Example: + + +Note: +You can place the `data-loading-delay` attribute directly on the element you want to disable, or in any parent element. diff --git a/lsp/src/htmx/attributes/loading-states/data-loading-disable.md b/lsp/src/htmx/attributes/loading-states/data-loading-disable.md new file mode 100644 index 0000000..73d58cf --- /dev/null +++ b/lsp/src/htmx/attributes/loading-states/data-loading-disable.md @@ -0,0 +1,4 @@ +Disables an element for the duration of the request. + +Example: + diff --git a/lsp/src/htmx/attributes/loading-states/data-loading-path.md b/lsp/src/htmx/attributes/loading-states/data-loading-path.md new file mode 100644 index 0000000..51df68d --- /dev/null +++ b/lsp/src/htmx/attributes/loading-states/data-loading-path.md @@ -0,0 +1,11 @@ +Allows filtering the processing of loading states only for specific requests based on the request path. + +
+ +
+ +You can place the data-loading-path attribute directly on the loading state element, or in any parent element. + +
+ +
diff --git a/lsp/src/htmx/attributes/loading-states/data-loading-states.md b/lsp/src/htmx/attributes/loading-states/data-loading-states.md new file mode 100644 index 0000000..2af0454 --- /dev/null +++ b/lsp/src/htmx/attributes/loading-states/data-loading-states.md @@ -0,0 +1,16 @@ +This attribute is optional and it allows defining a scope for the loading states so only elements within that scope are processed. + +Example: +
+
+
loading
+
+ +
+
+
loading
+
+ +
+
loading
+
diff --git a/lsp/src/htmx/attributes/loading-states/data-loading-target.md b/lsp/src/htmx/attributes/loading-states/data-loading-target.md new file mode 100644 index 0000000..2434f6e --- /dev/null +++ b/lsp/src/htmx/attributes/loading-states/data-loading-target.md @@ -0,0 +1,13 @@ +Allows setting a different target to apply the loading states. The attribute value can be any valid CSS selector. The example below disables the submit button and shows the loading state when the form is submitted. + +Example: + +
+ + + +
+ + diff --git a/lsp/src/htmx/attributes/loading-states/data-loading.md b/lsp/src/htmx/attributes/loading-states/data-loading.md new file mode 100644 index 0000000..ca993db --- /dev/null +++ b/lsp/src/htmx/attributes/loading-states/data-loading.md @@ -0,0 +1,9 @@ +Shows the element. The default style is inline-block, but it’s possible to use any display style by specifying it in the attribute value. + +Examples: + +
loading
+ +
loading
+ +
loading
diff --git a/lsp/src/htmx/attributes/path-deps/path-deps.md b/lsp/src/htmx/attributes/path-deps/path-deps.md new file mode 100644 index 0000000..1cc9791 --- /dev/null +++ b/lsp/src/htmx/attributes/path-deps/path-deps.md @@ -0,0 +1,21 @@ +Set the path dependency of the current div. +Use together with hx-trigger="path-deps". + +Usage: + +
+ ... +
+ +This div will fire a GET request to /example when any other element issues a mutating request (that is, a non-GET request like a POST) to /foo/bar or any sub-paths of that path. + +You can use a * to match any path component: + +
+ ... +
+ diff --git a/lsp/src/htmx/attributes/preload/preload-images.md b/lsp/src/htmx/attributes/preload/preload-images.md new file mode 100644 index 0000000..7c8f911 --- /dev/null +++ b/lsp/src/htmx/attributes/preload/preload-images.md @@ -0,0 +1,8 @@ +After an HTML page (or page fragment) is preloaded, also preload the linked image resources. + + + +NOTE: +This does not load images from or run Javascript or CSS content. diff --git a/lsp/src/htmx/attributes/preload/preload.md b/lsp/src/htmx/attributes/preload/preload.md new file mode 100644 index 0000000..e9b6ecb --- /dev/null +++ b/lsp/src/htmx/attributes/preload/preload.md @@ -0,0 +1,27 @@ +The element with this attribute will start preloading the data at the location as soon as the `mousedown` event begins. + +Example: + + +

What Works

+ WILL BE requested using a standard XMLHttpRequest() and default options (below) + + +

What WILL NOT WORK

+ WILL NOT be preloaded because it does not have an explicit "preload" attribute + WILL NOT be preloaded because it is an HX-POST transaction. + + +If other preload triggers need to be used this can be done using +preload="event-name" with some default events + +Examples: +This will be preloaded when the user begins to click. +This will be preloaded when the user's mouse remains over it for more than 100ms. + + +
+
+ +2xx codes will be handled as normal. However, when the response code is 5xx or 4xx, the response from /register will replace the contents of the div with the id any-errors. diff --git a/lsp/src/htmx/attributes/sse/sse-connect.md b/lsp/src/htmx/attributes/sse/sse-connect.md new file mode 100644 index 0000000..4bca366 --- /dev/null +++ b/lsp/src/htmx/attributes/sse/sse-connect.md @@ -0,0 +1,4 @@ +Subscribe to server-sent-events at the + +Usage: +
diff --git a/lsp/src/htmx/attributes/sse/sse-swap.md b/lsp/src/htmx/attributes/sse/sse-swap.md new file mode 100644 index 0000000..0b5bc56 --- /dev/null +++ b/lsp/src/htmx/attributes/sse/sse-swap.md @@ -0,0 +1,6 @@ +Swap when a message with is sent by the server. + +
+ Contents of this box will be updated in real time + with every SSE message received from the chatroom. +
diff --git a/lsp/src/htmx/attributes/ws/ws-connect.md b/lsp/src/htmx/attributes/ws/ws-connect.md new file mode 100644 index 0000000..4fa03b9 --- /dev/null +++ b/lsp/src/htmx/attributes/ws/ws-connect.md @@ -0,0 +1,11 @@ +Connect to using the websocket protocol + +
+
+
+ ... +
+
+ +
+
diff --git a/lsp/src/htmx/attributes/ws/ws-send.md b/lsp/src/htmx/attributes/ws/ws-send.md new file mode 100644 index 0000000..0720bb1 --- /dev/null +++ b/lsp/src/htmx/attributes/ws/ws-send.md @@ -0,0 +1,11 @@ +Send a message to the nearest websocket based on the trigger value for the element + +
+
+
+ ... +
+
+ +
+
diff --git a/lsp/src/htmx/hx-ext/alpine-morph/hx-swap/morph.md b/lsp/src/htmx/hx-ext/alpine-morph/hx-swap/morph.md new file mode 100644 index 0000000..1333ed7 --- /dev/null +++ b/lsp/src/htmx/hx-ext/alpine-morph/hx-swap/morph.md @@ -0,0 +1 @@ +TODO diff --git a/lsp/src/htmx/hx-ext/class-tools/classes/add.md b/lsp/src/htmx/hx-ext/class-tools/classes/add.md new file mode 100644 index 0000000..1333ed7 --- /dev/null +++ b/lsp/src/htmx/hx-ext/class-tools/classes/add.md @@ -0,0 +1 @@ +TODO diff --git a/lsp/src/htmx/hx-ext/class-tools/classes/remove.md b/lsp/src/htmx/hx-ext/class-tools/classes/remove.md new file mode 100644 index 0000000..1333ed7 --- /dev/null +++ b/lsp/src/htmx/hx-ext/class-tools/classes/remove.md @@ -0,0 +1 @@ +TODO diff --git a/lsp/src/htmx/hx-ext/class-tools/classes/toggle.md b/lsp/src/htmx/hx-ext/class-tools/classes/toggle.md new file mode 100644 index 0000000..1333ed7 --- /dev/null +++ b/lsp/src/htmx/hx-ext/class-tools/classes/toggle.md @@ -0,0 +1 @@ +TODO diff --git a/lsp/src/htmx/hx-ext/head-support/hx-head/append.md b/lsp/src/htmx/hx-ext/head-support/hx-head/append.md new file mode 100644 index 0000000..1333ed7 --- /dev/null +++ b/lsp/src/htmx/hx-ext/head-support/hx-head/append.md @@ -0,0 +1 @@ +TODO diff --git a/lsp/src/htmx/hx-ext/head-support/hx-head/merge.md b/lsp/src/htmx/hx-ext/head-support/hx-head/merge.md new file mode 100644 index 0000000..1333ed7 --- /dev/null +++ b/lsp/src/htmx/hx-ext/head-support/hx-head/merge.md @@ -0,0 +1 @@ +TODO diff --git a/lsp/src/htmx/hx-ext/head-support/hx-head/re-eval.md b/lsp/src/htmx/hx-ext/head-support/hx-head/re-eval.md new file mode 100644 index 0000000..1333ed7 --- /dev/null +++ b/lsp/src/htmx/hx-ext/head-support/hx-head/re-eval.md @@ -0,0 +1 @@ +TODO diff --git a/lsp/src/htmx/hx-ext/idiomorph/hx-swap/morph.md b/lsp/src/htmx/hx-ext/idiomorph/hx-swap/morph.md new file mode 100644 index 0000000..1333ed7 --- /dev/null +++ b/lsp/src/htmx/hx-ext/idiomorph/hx-swap/morph.md @@ -0,0 +1 @@ +TODO diff --git a/lsp/src/htmx/hx-ext/idiomorph/hx-swap/morph:innerHTML.md b/lsp/src/htmx/hx-ext/idiomorph/hx-swap/morph:innerHTML.md new file mode 100644 index 0000000..1333ed7 --- /dev/null +++ b/lsp/src/htmx/hx-ext/idiomorph/hx-swap/morph:innerHTML.md @@ -0,0 +1 @@ +TODO diff --git a/lsp/src/htmx/hx-ext/idiomorph/hx-swap/morph:outerHTML.md b/lsp/src/htmx/hx-ext/idiomorph/hx-swap/morph:outerHTML.md new file mode 100644 index 0000000..1333ed7 --- /dev/null +++ b/lsp/src/htmx/hx-ext/idiomorph/hx-swap/morph:outerHTML.md @@ -0,0 +1 @@ +TODO diff --git a/lsp/src/htmx/hx-ext/loading-states/data-loading/block.md b/lsp/src/htmx/hx-ext/loading-states/data-loading/block.md new file mode 100644 index 0000000..1333ed7 --- /dev/null +++ b/lsp/src/htmx/hx-ext/loading-states/data-loading/block.md @@ -0,0 +1 @@ +TODO diff --git a/lsp/src/htmx/hx-ext/loading-states/data-loading/flex.md b/lsp/src/htmx/hx-ext/loading-states/data-loading/flex.md new file mode 100644 index 0000000..1333ed7 --- /dev/null +++ b/lsp/src/htmx/hx-ext/loading-states/data-loading/flex.md @@ -0,0 +1 @@ +TODO diff --git a/lsp/src/htmx/hx-ext/morphdom-swap/hx-swap/morphdom.md b/lsp/src/htmx/hx-ext/morphdom-swap/hx-swap/morphdom.md new file mode 100644 index 0000000..1333ed7 --- /dev/null +++ b/lsp/src/htmx/hx-ext/morphdom-swap/hx-swap/morphdom.md @@ -0,0 +1 @@ +TODO diff --git a/lsp/src/htmx/hx-ext/multi-swap/hx-swap/multi.md b/lsp/src/htmx/hx-ext/multi-swap/hx-swap/multi.md new file mode 100644 index 0000000..1333ed7 --- /dev/null +++ b/lsp/src/htmx/hx-ext/multi-swap/hx-swap/multi.md @@ -0,0 +1 @@ +TODO diff --git a/lsp/src/htmx/hx-ext/path-deps/hx-trigger/path-deps.md b/lsp/src/htmx/hx-ext/path-deps/hx-trigger/path-deps.md new file mode 100644 index 0000000..1333ed7 --- /dev/null +++ b/lsp/src/htmx/hx-ext/path-deps/hx-trigger/path-deps.md @@ -0,0 +1 @@ +TODO diff --git a/lsp/src/htmx/hx-ext/preload/preload-images/false.md b/lsp/src/htmx/hx-ext/preload/preload-images/false.md new file mode 100644 index 0000000..1333ed7 --- /dev/null +++ b/lsp/src/htmx/hx-ext/preload/preload-images/false.md @@ -0,0 +1 @@ +TODO diff --git a/lsp/src/htmx/hx-ext/preload/preload-images/true.md b/lsp/src/htmx/hx-ext/preload/preload-images/true.md new file mode 100644 index 0000000..1333ed7 --- /dev/null +++ b/lsp/src/htmx/hx-ext/preload/preload-images/true.md @@ -0,0 +1 @@ +TODO diff --git a/lsp/src/htmx/hx-ext/preload/preload/mousedown.md b/lsp/src/htmx/hx-ext/preload/preload/mousedown.md new file mode 100644 index 0000000..1333ed7 --- /dev/null +++ b/lsp/src/htmx/hx-ext/preload/preload/mousedown.md @@ -0,0 +1 @@ +TODO diff --git a/lsp/src/htmx/hx-ext/preload/preload/mouseover.md b/lsp/src/htmx/hx-ext/preload/preload/mouseover.md new file mode 100644 index 0000000..1333ed7 --- /dev/null +++ b/lsp/src/htmx/hx-ext/preload/preload/mouseover.md @@ -0,0 +1 @@ +TODO diff --git a/lsp/src/htmx/hx-ext/restored/hx-trigger/restored.md b/lsp/src/htmx/hx-ext/restored/hx-trigger/restored.md new file mode 100644 index 0000000..1333ed7 --- /dev/null +++ b/lsp/src/htmx/hx-ext/restored/hx-trigger/restored.md @@ -0,0 +1 @@ +TODO diff --git a/lsp/src/htmx/hx-ext/sse/hx-trigger/sse:.md b/lsp/src/htmx/hx-ext/sse/hx-trigger/sse:.md new file mode 100644 index 0000000..1333ed7 --- /dev/null +++ b/lsp/src/htmx/hx-ext/sse/hx-trigger/sse:.md @@ -0,0 +1 @@ +TODO diff --git a/lsp/src/htmx/mod.rs b/lsp/src/htmx/mod.rs index 424a80f..8c5b688 100644 --- a/lsp/src/htmx/mod.rs +++ b/lsp/src/htmx/mod.rs @@ -27,15 +27,40 @@ macro_rules! build_completion { }; } -pub fn hx_completion(text_params: TextDocumentPositionParams) -> Option<&'static [HxCompletion]> { - let result = crate::tree_sitter::get_position_from_lsp_completion(text_params.clone())?; +pub fn hx_completion( + text_params: TextDocumentPositionParams, +) -> ( + Option<&'static [HxCompletion]>, + Option>, +) { + let result = crate::tree_sitter::get_position_from_lsp_completion(text_params.clone()); + let ext_result = crate::tree_sitter::get_extension_completes(text_params.clone()); - debug!("result: {:?} params: {:?}", result, text_params); + let comp = match result { + Some(Position::AttributeName(name)) => name.starts_with("hx-").then_some(HX_TAGS), + Some(Position::AttributeValue { name, .. }) => HX_ATTRIBUTE_VALUES.get(&name).copied(), + None => None, + }; - match result { - Position::AttributeName(name) => name.starts_with("hx-").then_some(HX_TAGS), - Position::AttributeValue { name, .. } => HX_ATTRIBUTE_VALUES.get(&name).copied(), - } + let ext_comp = match ext_result { + Some(exts) => { + let mut comps = Vec::new(); + for ext in exts.iter() { + // add matching refs to extension completes to Vec + // once extension completes have been added + comps.push(HX_EXTENSIONS[ext]); + } + Some(comps) + } + None => None, + }; + + debug!( + "result: {:?}, ext_result: {:?}, params: {:?}", + comp, ext_comp, text_params + ); + + (comp, ext_comp) } pub fn hx_hover(text_params: TextDocumentPositionParams) -> Option { @@ -54,6 +79,91 @@ pub fn hx_hover(text_params: TextDocumentPositionParams) -> Option } } +pub static HX_EXTENSIONS: phf::Map<&'static str, &'static [HxCompletion]> = phf::phf_map! { + "class-tools" => build_completion![ + ("classes", "./attributes/class-tools/classes.md"), + ("data-classes", "./attributes/class-tools/data-classes.md") + ] as &[_], + "client-side-templates" => build_completion![ + ("handlebars-template", "./attributes/client-side-templates/handlebars-template.md"), + ("mustache-template", "./attributes/client-side-templates/mustache-template.md"), + ("mustache-array-template", "./attributes/client-side-templates/mustache-array-template.md"), + ("nunjucks-template", "./attributes/client-side-templates/nunjucks-template.md"), + ("xslt-template", "./attributes/client-side-templates/xslt-template.md") + ] as &[_], + "head-support" => build_completion![ + ("hx-head", "./attributes/head-support/hx-head.md") + ] as &[_], + "include-vals" => build_completion![ + ("include-vals", "./attributes/include-vals/include-vals.md") + ] as &[_], + "loading-states" => build_completion![ + ("data-loading", "./attributes/loading-states/data-loading.md"), + ("data-loading-target", "./attributes/loading-states/data-loading-target.md"), + ("data-loading-states", "./attributes/loading-states/data-loading-states.md"), + ("data-loading-path", "./attributes/loading-states/data-loading-path.md"), + ("data-loading-disable", "./attributes/loading-states/data-loading-disable.md"), + ("data-loading-delay", "./attributes/loading-states/data-loading-delay.md"), + ("data-loading-class", "./attributes/loading-states/data-loading-class.md"), + ("data-loading-class-remove", "./attributes/loading-states/data-loading-class-remove.md"), + ("data-loading-aria-busy", "./attributes/loading-states/data-loading-aria-busy.md") + ] as &[_], + "path-deps" => build_completion![ + ("path-deps", "./attributes/path-deps/path-deps.md") + ] as &[_], + "preload" => build_completion![ + ("preload", "./attributes/preload/preload.md"), + ("preload-images", "./attributes/preload/preload-images.md") + ] as &[_], + "remove-me" => build_completion![ + ("remove-me", "./attributes/remove-me/remove-me.md") + ] as &[_], + "response-targets" => build_completion![ + ("hx-target-error", "./attributes/response-targets/hx-target-error.md") + ] as &[_], + "ws" => build_completion![ + ("ws-connect", "./attributes/ws/ws-connect.md"), + ("ws-send", "./attributes/ws/ws-send.md") + ] as &[_], + "sse" => build_completion![ + ("sse-connect", "./attributes/sse/sse-connect.md"), + ("sse-swap", "./attributes/sse/sse-swap.md") + ] as &[_], + "ajax-header" => build_completion![ + + ] as &[_], + "alpine-morph" => build_completion![ + + ] as &[_], + "debug" => build_completion![ + + ] as &[_], + "event-header" => build_completion![ + + ] as &[_], + "json-enc" => build_completion![ + + ] as &[_], + "idiomorph" => build_completion![ + + ] as &[_], + "method-override" => build_completion![ + + ] as &[_], + "morphdom-swap" => build_completion![ + + ] as &[_], + "multi-swap" => build_completion![ + + ] as &[_], + "restored" => build_completion![ + + ] as &[_], + "path-params" => build_completion![ + + ] as &[_], +}; + pub static HX_TAGS: &[HxCompletion] = build_completion!( ("hx-boost", "./attributes/hx-boost.md"), ("hx-delete", "./attributes/hx-delete.md"), diff --git a/lsp/src/lib.rs b/lsp/src/lib.rs index 5cbb405..429c367 100644 --- a/lsp/src/lib.rs +++ b/lsp/src/lib.rs @@ -9,8 +9,8 @@ use htmx::HxCompletion; use log::{debug, error, info, warn}; use lsp_types::{ CompletionItem, CompletionItemKind, CompletionList, HoverContents, InitializeParams, - MarkupContent, ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind, - WorkDoneProgressOptions, + MarkupContent, PositionEncodingKind, ServerCapabilities, TextDocumentSyncCapability, + TextDocumentSyncKind, WorkDoneProgressOptions, }; use lsp_server::{Connection, Message, Response}; @@ -123,7 +123,10 @@ pub fn start_lsp() -> Result<()> { // Run the server and wait for the two threads to end (typically by trigger LSP Exit event). let server_capabilities = serde_json::to_value(ServerCapabilities { - text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)), + position_encoding: Some(PositionEncodingKind::UTF16), // compatability with lsp_textdocument crate + text_document_sync: Some(TextDocumentSyncCapability::Kind( + TextDocumentSyncKind::INCREMENTAL, + )), completion_provider: Some(lsp_types::CompletionOptions { resolve_provider: Some(false), trigger_characters: Some(vec!["-".to_string(), "\"".to_string(), " ".to_string()]), diff --git a/lsp/src/text_store.rs b/lsp/src/text_store.rs index 254e606..69d85e4 100644 --- a/lsp/src/text_store.rs +++ b/lsp/src/text_store.rs @@ -4,39 +4,48 @@ use std::{ sync::{Arc, Mutex, OnceLock}, }; -use lsp_types::{TextDocumentPositionParams, Url}; +use lsp_textdocument::FullTextDocument; +use lsp_types::{Position, Range, TextDocumentPositionParams, Url}; +use tree_sitter::{Parser, Tree}; -type TxtStore = HashMap; +pub struct DocInfo { + pub doc: FullTextDocument, + pub parser: Parser, + pub tree: Option, +} + +type DocStore = HashMap; -pub struct TextStore(TxtStore); +#[derive(Default)] +pub struct DocumentStore(DocStore); -impl Deref for TextStore { - type Target = TxtStore; +impl Deref for DocumentStore { + type Target = DocStore; fn deref(&self) -> &Self::Target { &self.0 } } -impl DerefMut for TextStore { +impl DerefMut for DocumentStore { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } -pub static TEXT_STORE: OnceLock>> = OnceLock::new(); +pub static DOCUMENT_STORE: OnceLock>> = OnceLock::new(); pub fn init_text_store() { - _ = TEXT_STORE.set(Arc::new(Mutex::new(TextStore(HashMap::new())))); + _ = DOCUMENT_STORE.set(Arc::new(Mutex::new(DocumentStore::default()))); } -pub fn get_text_document(uri: &Url) -> Option { - return TEXT_STORE +pub fn get_text_document(uri: &Url, range: Option) -> Option { + return DOCUMENT_STORE .get() .expect("text store not initialized") .lock() .expect("text store mutex poisoned") .get(&uri.to_string()) - .cloned(); + .map(|doc| doc.doc.get_content(range).to_string()); } /// Find the start and end indices of a word inside the given line @@ -67,20 +76,20 @@ fn find_word_at_pos(line: &str, col: usize) -> (usize, usize) { pub fn get_word_from_pos_params(pos_params: &TextDocumentPositionParams) -> anyhow::Result { let uri = &pos_params.text_document.uri; - let line = pos_params.position.line as usize; + let line = pos_params.position.line; let col = pos_params.position.character as usize; - match get_text_document(uri) { - Some(text) => { - let line_conts = match text.lines().nth(line) { - Some(conts) => conts, - None => { - return Err(anyhow::anyhow!( - "get_word_from_pos_params Failed to get word under cursor" - )); - } - }; - let (start, end) = find_word_at_pos(line_conts, col); + let range = Range { + start: Position { line, character: 0 }, + end: Position { + line, + character: u32::MAX, + }, + }; + + match get_text_document(uri, Some(range)) { + Some(line_conts) => { + let (start, end) = find_word_at_pos(&line_conts, col); Ok(String::from(&line_conts[start..end])) } None => Err(anyhow::anyhow!( diff --git a/lsp/src/tree_sitter.rs b/lsp/src/tree_sitter.rs index 645a6b3..61c5b50 100644 --- a/lsp/src/tree_sitter.rs +++ b/lsp/src/tree_sitter.rs @@ -1,11 +1,14 @@ -use crate::tree_sitter_querier::{ - query_attr_keys_for_completion, query_attr_values_for_completion, +use std::collections::HashSet; + +use crate::{ + text_store::DOCUMENT_STORE, + tree_sitter_querier::{query_attr_keys_for_completion, query_attr_values_for_completion}, }; use log::{debug, error}; -use lsp_types::TextDocumentPositionParams; -use tree_sitter::{Node, Parser, Point}; - -use crate::text_store::get_text_document; +use lsp_textdocument::FullTextDocument; +use lsp_types::{TextDocumentContentChangeEvent, TextDocumentPositionParams}; +use once_cell::sync::Lazy; +use tree_sitter::{InputEdit, Node, Point, Query, QueryCursor}; #[derive(Debug, Clone, PartialEq)] pub enum Position { @@ -102,27 +105,158 @@ fn get_position(root: Node<'_>, source: &str, row: usize, column: usize) -> Opti create_attribute(desc, source) } +macro_rules! cursor_matches { + ($cursor_line:expr,$cursor_char:expr,$query_start:expr,$query_end:expr) => {{ + $query_start.row == $cursor_line + && $query_end.row == $cursor_line + && $query_start.column <= $cursor_char + && $query_end.column >= $cursor_char + }}; +} + +/// Returns an Option> of extension tags the provided position is inside of +// Currently limited by tree-sitter's max depth of 12 levels, see https://github.com/tree-sitter/tree-sitter/issues/880 +pub fn get_extension_completes(text_params: TextDocumentPositionParams) -> Option> { + static QUERY_HTMX_EXT: Lazy = Lazy::new(|| { + tree_sitter::Query::new( + tree_sitter_html::language(), + r#"( + (element + (start_tag + (attribute + (attribute_name) @hxext + (quoted_attribute_value + (attribute_value) @extension + ) + ) + (attribute (attribute_name) @tag )? + ) + (element + [ + (_ (attribute (attribute_name) @tag )) + (_ (_ (attribute (attribute_name) @tag ))) + (_ (_ (_ (attribute (attribute_name) @tag )))) + (_ (_ (_ (_ (attribute (attribute_name) @tag ))))) + (_ (_ (_ (_ (_ (attribute (attribute_name) @tag )))))) + (_ (_ (_ (_ (_ (_ (attribute (attribute_name) @tag ))))))) + (_ (_ (_ (_ (_ (_ (_ (attribute (attribute_name) @tag )))))))) + (_ (_ (_ (_ (_ (_ (_ (_ (attribute (attribute_name) @tag ))))))))) + (_ (_ (_ (_ (_ (_ (_ (_ (_ (attribute (attribute_name) @tag )))))))))) + (_ (_ (_ (_ (_ (_ (_ (_ (_ (_ (attribute (attribute_name) @tag ))))))))))) + (_ (_ (_ (_ (_ (_ (_ (_ (_ (_ (_ (attribute (attribute_name) @tag )))))))))))) + (_ (_ (_ (_ (_ (_ (_ (_ (_ (_ (_ (_ (attribute (attribute_name) @tag ))))))))))))) + ] + ) + ) +(#match? @hxext "hx-ext") +) +"#, + ) + .unwrap() + }); + + let mut ext_tags: HashSet = HashSet::new(); + let mut cursor = QueryCursor::new(); + let cursor_line = text_params.position.line as usize; + let cursor_char = text_params.position.character as usize; + + if let Some(entry) = DOCUMENT_STORE + .get() + .expect("text store not initialized") + .lock() + .expect("text store mutex poisoned") + .get_mut(text_params.text_document.uri.as_str()) + { + entry.tree = entry + .parser + .parse(entry.doc.get_content(None), entry.tree.as_ref()); + + if let Some(ref curr_tree) = entry.tree { + let text = entry.doc.get_content(None).as_bytes(); + let matches = cursor.matches(&QUERY_HTMX_EXT, curr_tree.root_node(), text); + + for match_ in matches.into_iter() { + let caps = match_.captures; + if caps.len() < 3 { + continue; + } + + let ext_tag = caps[2].node; + let cap_start = ext_tag.range().start_point; + let cap_end = ext_tag.range().end_point; + if cursor_matches!(cursor_line, cursor_char, cap_start, cap_end) { + if let Ok(ext) = caps[1].node.utf8_text(text) { + debug!("get_extension_completes: Adding completes for {}", ext); + ext_tags.insert(ext.replace('"', "")); + } + } + } + } + } + + if ext_tags.is_empty() { + None + } else { + Some(ext_tags.into_iter().collect()) + } +} + pub fn get_position_from_lsp_completion( text_params: TextDocumentPositionParams, ) -> Option { error!("get_position_from_lsp_completion"); - let text = get_text_document(&text_params.text_document.uri)?; - error!("get_position_from_lsp_completion: text {}", text); let pos = text_params.position; error!("get_position_from_lsp_completion: pos {:?}", pos); - // TODO: Gallons of perf work can be done starting here - let mut parser = Parser::new(); - - parser - .set_language(tree_sitter_html::language()) - .expect("could not load html grammer"); + if let Some(entry) = DOCUMENT_STORE + .get() + .expect("text store not initialized") + .lock() + .expect("text store mutex poisoned") + .get_mut(text_params.text_document.uri.as_str()) + { + let text = entry.doc.get_content(None); + entry.tree = entry.parser.parse(text, entry.tree.as_ref()); + + if let Some(ref curr_tree) = entry.tree { + let trigger_point = Point::new(pos.line as usize, pos.character as usize); + return query_position(curr_tree.root_node(), text, trigger_point); + } + } - let tree = parser.parse(&text, None)?; - let root_node = tree.root_node(); - let trigger_point = Point::new(pos.line as usize, pos.character as usize); + None +} - return query_position(root_node, text.as_str(), trigger_point); +/// Convert an `lsp_types::TextDocumentContentChangeEvent` to a `tree_sitter::InputEdit` +pub fn text_doc_change_to_ts_edit( + change: &TextDocumentContentChangeEvent, + doc: &FullTextDocument, +) -> Result { + let range = change.range.ok_or("Invalid edit range")?; + let start = range.start; + let end = range.end; + + let start_byte = doc.offset_at(start) as usize; + let new_end_byte = start_byte + change.text.len(); + let new_end_pos = doc.position_at(new_end_byte as u32); + + Ok(InputEdit { + start_byte, + old_end_byte: doc.offset_at(end) as usize, + new_end_byte, + start_position: Point { + row: start.line as usize, + column: start.character as usize, + }, + old_end_position: Point { + row: end.line as usize, + column: end.character as usize, + }, + new_end_position: Point { + row: new_end_pos.line as usize, + column: new_end_pos.character as usize, + }, + }) } #[cfg(test)] @@ -154,6 +288,69 @@ mod tests { assert_eq!(matches, Some(Position::AttributeName("hx-".to_string()))); } + #[test] + fn test_it_suggests_normal_when_active() { + let text = r##"
"##; + let tree = prepare_tree(text); + + let matches = query_position(tree.root_node(), text, Point::new(0, 25)); + + assert_eq!(matches, Some(Position::AttributeName("hx-".to_string()))); + } + + #[test] + fn test_it_suggests_extension_when_active() { + let text = r##"
"##; + let tree = prepare_tree(text); + + let matches = query_position(tree.root_node(), text, Point::new(0, 25)); + + assert_eq!(matches, Some(Position::AttributeName("ws-".to_string()))); + } + + #[test] + fn test_it_suggests_extension_preload_when_active() { + let text = r##"
"##; + let tree = prepare_tree(text); + + let matches = query_position(tree.root_node(), text, Point::new(0, 32)); + + assert_eq!( + matches, + Some(Position::AttributeName("preload".to_string())) + ); + } + + #[test] + fn test_it_suggests_extension_starting_with_hx_when_active() { + let text = r##"
"##; + let tree = prepare_tree(text); + + let matches = query_position(tree.root_node(), text, Point::new(0, 35)); + + assert_eq!(matches, Some(Position::AttributeName("hx-".to_string()))); + } + + #[test] + fn test_it_suggest_extension_in_same_element() { + let text = r##"
"##; + let tree = prepare_tree(text); + + let matches = query_position(tree.root_node(), text, Point::new(0, 22)); + + assert_eq!(matches, Some(Position::AttributeName("sse-".to_string()))); + } + + #[test] + fn test_it_does_not_suggest_when_extension_not_active() { + let text = r##"
"##; + let tree = prepare_tree(text); + + let matches = query_position(tree.root_node(), text, Point::new(0, 8)); + + assert_eq!(matches, None) + } + #[test] fn test_it_does_not_suggest_when_quote_not_initiated() { let text = r##"
"##; @@ -290,7 +487,7 @@ mod tests { } #[test] - fn test_it_suggests_attr_names_for_incoplete_quoted_value_in_between_attributes() { + fn test_it_suggests_attr_names_for_incomplete_quoted_value_in_between_attributes() { let text = r##"
"##;