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

Feat: When an hx-ext tag is used in the current or a parent element enable autocompletion based on it #14

Open
itepastra opened this issue Aug 28, 2023 · 13 comments

Comments

@itepastra
Copy link
Contributor

itepastra commented Aug 28, 2023

Some extensions, like websockets make use of some extra tags. When the extension is active (I.e. this element or a parent element has the hx-ext=extension attribute) we could also include the extension tags and attributes

an example:

<div hx-ext="ws">
    <div ws- #autocomplete to ws-send for example
</div>
<div ws- #does not autocomplete

I get it if this isn't added too soon, the parsing has to allow it first.

@itepastra itepastra changed the title When an hx-ext tag is used in the current or a parent element enable autocompletion based on it Feat: When an hx-ext tag is used in the current or a parent element enable autocompletion based on it Aug 28, 2023
@itepastra
Copy link
Contributor Author

itepastra commented Aug 28, 2023

a query like this matches all tags where an htmx extension is active, up to 12 levels deep since tree sitter query language does not support arbitrary depth patterns yet. tree-sitter/tree-sitter#880

(
	(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 )))))))))))))
            ]
        )
    ) @elem
(#match? @hxext "hx-ext")
)

This might be useful for implementing conditional auto completion

@cristianoliveira
Copy link
Contributor

I believe this might be fixed by #16 but to be fair, I don't think people code with incomplete tags everywhere 😅

@cristianoliveira
Copy link
Contributor

Ahh sorry, understood the issue incorrectly, my PR doesn't solve that :(

@cristianoliveira
Copy link
Contributor

cristianoliveira commented Aug 29, 2023

If I got it right once the extension is enabled all children start to be able to use the new attrs, right?

Since tree-sitter query has that limitation, maybe querying all elements isn't the way to go, but find at which point of the tree the ws is initialized and once a suggestion is triggered we check if the trigger_point is before or after the extension initialization. There is an example of node filtering by position in the PR I shared if you want to give it try :)

EDIT:
here https://github.com/ThePrimeagen/htmx-lsp/pull/16/files#diff-e6fdca6d3502145d2fbe769b58c7289ffb7860079ba6f75ce14fb89c1aac57eaR83

@itepastra
Copy link
Contributor Author

itepastra commented Aug 29, 2023

If I got it right once the extension is enabled all children start to be able to use the new attrs, right?

yes, that is the behaviour of most extensions in HTMX and I think it would be nice if the lsp supports it.

Since tree-sitter query has that limitation, maybe querying all elements isn't the way to go, but find at which point of the tree the ws is initialized and once a suggestion is triggered we check if the trigger_point is before or after the extension initialization.

Yeah, that might be a better way to do it, I was thinking, maybe it can be useful.

I might try to make a prototype tomorrow, since I have hockey tonight.

@itepastra
Copy link
Contributor Author

I have no clue how to do this. If someone wants to have a try, feel free. I can add the documentation and completions if there is a start to add to.

@WillLillis
Copy link
Contributor

WillLillis commented Jan 28, 2024

I've played around a bit with implementing this. I wasn't able to completely finish, but I hope this might be helpful to build off of :)

On every completion request, the basic process I followed is:

  1. Create an empty hashset to store the 0 or more extensions the current cursor position is inside of
  2. Issue @itepastra's tree-sitter query on the tree for the document in question
  3. For each resulting match, check if the the cursor overlaps with either of the two @tag captures. If so, add the associated @extension capture text to the hashset
  4. Collect the hashset into a Vec and return it. If the Vec is non-empty, the caller (assuming this is hx_completion() from lsp/src/htmx/mod.rs) can then add the extension-related completion items to the returned slice.

I believe this is blocked by tree-sitter/tree-sitter#2847. I've run into this issue trying to implement a somewhat similar feature on another LSP written in Rust using the tree-sitter bindings.

I haven't had time to rigorously test anything yet, but here's the gist of the code I wrote to try to accomplish this:

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 a (potentially empty) Vec 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) -> Vec<String> {
    static QUERY_HTMX_EXT: Lazy<Query> = 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 )))))))))))))
            ]
        )
    ) @elem
(#match? @hxext "hx-ext")
)"#,
        )
        .unwrap()
    });

    let mut ext_tags: HashSet<String> = 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;

   // get the document contents and its corresponding tree-sitter tree from the store
    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 matches = cursor.matches(
                &QUERY_HTMX_EXT,
                curr_tree.root_node(),
                entry.doc.get_content(None).as_bytes(),
            );
            for match_ in matches {
                let caps = match_.captures;
                let extension = caps[1]
                    .node
                    .utf8_text(entry.doc.get_content(None).as_bytes())
                    .unwrap();

                // skip @hxext and @extension, grab both @tag's if they're there
                for cap in caps.iter().skip(2).take(2) {
                    let cap_start = cap.node.range().start_point;
                    let cap_end = cap.node.range().end_point;
                    // if the cursor is current at a tag inside the extension's scope,
                    // we need to add that extension's tags and attributes
                    if cursor_matches!(cursor_line, cursor_char, cap_start, cap_end) {
                        ext_tags.insert(extension.to_string());
                    }
                }
            }
        }
    }

    ext_tags.into_iter().collect()
}

My work can be found in the htmx-extensions branch of my fork. I've built off of the tree-sitter work from #43, but the overall idea is the same with the project's current state.

@RafaelZasas
Copy link

Just a thought- instead of searching the tree to find extensions present, would it be possible to allow for users to enter extensions that they frequently use?
This could default to displaying all native suggestions, plus extension suggestions from the extensions you opted into.

I understand it is not ideal in cases where you are working in lots of different htmx projects with different extensions in each.
I assume (perhaps incorrectly) that most people are only using a small number of extensions, and wouldn't mind to have those suggestions displayed.

@itepastra
Copy link
Contributor Author

Having the option to force the lsp to display auto completion for specific extensions can be very useful when using composable templates.

I've finished my last project (an animated wallpaper) so after I get nixos working decently I might have a stab at this feature.

@WillLillis
Copy link
Contributor

I spent some more time with this today and I have a skeleton in place for it to work decently well. In my own testing, the program is able to identify which extension tag(s) the cursor is in scope with. The basic flow is similar to the one detailed in my last comment in this issue, but I was able to clean things up a bit. After reparsing the given file's tree, it then issues the query for extension tags. If a match is found and the current cursor position overlaps with a @tag node, the current extension's name (the text is from the @hext capture) is added to a hashset to return. Because the completions haven't been added yet, I can only show that it's working through the logs, but regardless here's some quick demos:

Adding suggestions while inside the hx-ext="ws" tag (note the "Adding completes for ws" log):

extension_suggestion

Not adding suggestions while outside the hx-ext="ws" tag (There is no "Adding completes for ws" log because the extension is out of scope):

extension_no_suggestion

I can add the documentation and completions if there is a start to add to.

@itepastra Do you still want to do this, or would you rather work on your own implementation? I'm happy to open up a draft PR to add to if you think that's the right next step. I built this off of the pending work in #43, but if that doesn't get merged I'm happy to refactor around it. All the code can be found in the extensions branch of my fork. I had a lot of fun putting this together, really hope this helps! :)

@itepastra
Copy link
Contributor Author

I have had (and still have) many things on my mind but I like the progress. Will try to have a look soon(ish).

@itepastra
Copy link
Contributor Author

I am unsure how to handle https://htmx.org/extensions/response-targets/, I don't think adding every statuscode as a completion is a good idea but maybe some common ones?

@itepastra itepastra mentioned this issue Mar 5, 2024
5 tasks
@WillLillis
Copy link
Contributor

I am unsure how to handle https://htmx.org/extensions/response-targets/, I don't think adding every statuscode as a completion is a good idea but maybe some common ones?

Would adding the common codes from Wikipedia, Mozilla, or a similar list be sufficient? I imagine all the markdown files could be roughly the same besides some brief docs on the particular response code. For example, maybe the hx-target-404 entry could be something roughly like:

This extension allows you to specify different target elements to be swapped when the 404 HTTP response code is received.

[404 Not Found](https://en.wikipedia.org/wiki/HTTP_404): 
The requested resource could not be found but may be available in the future. Subsequent requests by the client are permissible.

Example:

<div hx-ext="response-targets">
    <div id="response-div"></div>
    <button hx-post="/register"
            hx-target="#response-div"
            hx-target-5*="#serious-errors"
            hx-target-404="#not-found">
        Register!
    </button>
    <div id="serious-errors"></div>
    <div id="not-found"></div>
</div>

I'd be happy to work through the main list to create a markdown file for each common code like the example above if that would be helpful. :)

Also regarding this extension, how should the program support wildcards? (e.g. 4xx or 40x)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants