Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extension completions #49

Draft
wants to merge 11 commits into
base: master
Choose a base branch
from
20 changes: 16 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions lsp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
166 changes: 109 additions & 57 deletions lsp/src/handle.rs
Original file line number Diff line number Diff line change
@@ -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<Text>,
}

#[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 {
Expand All @@ -61,41 +34,84 @@ pub enum HtmxResult {
// ignore snakeCase
#[allow(non_snake_case)]
fn handle_didChange(noti: Notification) -> Option<HtmxResult> {
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::<DidChangeTextDocument>(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(&params.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<HtmxResult> {
debug!("handle_didOpen params {:?}", noti.params);
let text_document_changes = match serde_json::from_value::<TextDocumentOpen>(noti.params) {
Ok(p) => p.text_document,
let text_doc_open = match cast_notif::<DidOpenTextDocument>(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
}
Expand All @@ -116,8 +132,22 @@ fn handle_completion(req: Request) -> Option<HtmxResult> {
..
}) => {
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;
}
Expand All @@ -129,7 +159,7 @@ fn handle_completion(req: Request) -> Option<HtmxResult> {
);

Some(HtmxResult::AttributeCompletion(HtmxAttributeCompletion {
items: items.to_vec(),
items,
id: req.id,
}))
}
Expand Down Expand Up @@ -186,10 +216,23 @@ pub fn handle_other(msg: Message) -> Option<HtmxResult> {
None
}

fn cast_notif<R>(notif: Notification) -> anyhow::Result<R::Params>
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();
Expand All @@ -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]
Expand Down
15 changes: 15 additions & 0 deletions lsp/src/htmx/attributes/class-tools/classes.md
Original file line number Diff line number Diff line change
@@ -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.

<div hx-ext="class-tools">
<div classes="add foo"/> <!-- adds the class "foo" after 100ms -->
<div class="bar" classes="remove bar:1s"/> <!-- removes the class "bar" after 1s -->
<div class="bar" classes="remove bar:1s, add foo:1s"/> <!-- removes the class "bar" after 1s
then adds the class "foo" 1s after that -->
<div class="bar" classes="remove bar:1s & add foo:1s"/> <!-- removes the class "bar" and adds
class "foo" after 1s -->
<div classes="toggle foo:1s"/> <!-- toggles the class "foo" every 1s -->
</div>
15 changes: 15 additions & 0 deletions lsp/src/htmx/attributes/class-tools/data-classes.md
Original file line number Diff line number Diff line change
@@ -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.

<div hx-ext="class-tools">
<div data-classes="add foo"/> <!-- adds the class "foo" after 100ms -->
<div class="bar" data-classes="remove bar:1s"/> <!-- removes the class "bar" after 1s -->
<div class="bar" data-classes="remove bar:1s, add foo:1s"/> <!-- removes the class "bar" after 1s
then adds the class "foo" 1s after that -->
<div class="bar" data-classes="remove bar:1s & add foo:1s"/> <!-- removes the class "bar" and adds
class "foo" after 1s -->
<div data-classes="toggle foo:1s"/> <!-- toggles the class "foo" every 1s -->
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Looks up a handlebars <script> tag by ID for the template content

Usage:

<div hx-ext="client-side-templates">
<button hx-get="/some_json"
handlebars-template="my-handlebars-template">
Handle with handlebars
</button>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Look up a mustache template by ID for the template content which is rendered for each element in the array

Example for an API that returns an array:

<div hx-ext="client-side-templates">
<button hx-get="https://jsonplaceholder.typicode.com/users"
hx-swap="innerHTML"
hx-target="#content"
mustache-array-template="foo">
Click Me
</button>

<p id="content">Start</p>

<template id="foo">
{{#data}}
<p> {{name}} at {{email}} is with {{company.name}}</p>
{{/data}}
</template>
</div>
18 changes: 18 additions & 0 deletions lsp/src/htmx/attributes/client-side-templates/mustache-template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Look up a mustache template by ID for the template content

Example:

<div hx-ext="client-side-templates">
<button hx-get="https://jsonplaceholder.typicode.com/todos/1"
hx-swap="innerHTML"
hx-target="#content"
mustache-template="foo">
Click Me
</button>

<p id="content">Start</p>

<template id="foo">
<p> {% raw %}{{userID}}{% endraw %} and {% raw %}{{id}}{% endraw %} and {% raw %}{{title}}{% endraw %} and {% raw %}{{completed}}{% endraw %}</p>
</template>
</div>
10 changes: 10 additions & 0 deletions lsp/src/htmx/attributes/client-side-templates/nunjucks-template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Resolves the template by name via `nunjucks.render()

Usage:

<div hx-ext="client-side-templates">
<button hx-get="/some_json"
nunjucks-template="my-nunjucks-template">
Handle with nunjucks
</button>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Looks up an XSLT <script> tab by ID for the template content

Example:
<div hx-ext="client-side-templates">
<button hx-get="/some_xml"
xslt-template="my-xslt-template">
Handle with XSLT
</button>
</div>
Loading
Loading