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

[WIP] Treesitter-postfix Support #980

Merged
merged 4 commits into from
Sep 21, 2023

Conversation

TwIStOy
Copy link
Contributor

@TwIStOy TwIStOy commented Aug 14, 2023

No description provided.

@L3MON4D3
Copy link
Owner

That was fast, awesome!
I have to admit, I'm not all that up-to-date when it comes to treesitter, so a few comments, or example-usages would be nice for me to understand what exactly I'm looking at :D

@L3MON4D3
Copy link
Owner

Q: would it be possible to also work with queries?
As far as I can see, this always expects a single node (I don't reallly understand the +,-,/ - operators yet, admittedly), but supporting full queries seems very cool, since we could then also match a specific sequence of nodes.
We could also pass captures through, as env-variables, so the snippet can easily extract parts of the prefix.

@TwIStOy
Copy link
Contributor Author

TwIStOy commented Aug 16, 2023

Please take a look. Introduce two parameters:

  • reparseBuffer: Try to re-parse the whole buffer after removing matched trigger. Because sometimes the trigger will be a part of TSNode, but that's not what we want.
  • matchTSNode: function or string. function, Match a tsnode from given LanguageTree. string, the expected capture name.

@L3MON4D3
Copy link
Owner

Hey, I'll be a bit slower in communicating, I'm rather busy this week, and the next, just FYI 😅

Have you considered accepting the query in the postfix-constructor, and then parse it viavim.treesitter.query.parse(), instead of using the runtimepath-mechanism? Seems a bit more ergonomic, since the snippet and the query may be connected pretty closely.

Also, I don't understand the need for reparseBuffer, could you explain when/why that is necessary?

@TwIStOy
Copy link
Contributor Author

TwIStOy commented Aug 17, 2023

Of course, but i'm thinking of if i should introduce a new parameter for query text.
And, the new parameter reparseBuffer is used to mark that we should re-parse the whole buffer after removing matched trigger.

This idea is from when I'm trying to implement a snippet "fn" which should expand to a lambda definition [&]() {<body>} in function body or argument list and should expand to auto <name>(<args>) {} in other condition.

In this sample code:

#include <compare>

struct Test {
  int foo;
  int bar;
};

int main(int argc, const char* argv) {
  xxxxx.test();
}

Its language tree should be:

preproc_include [0, 0] - [1, 0]
  path: system_lib_string [0, 9] - [0, 18]
struct_specifier [2, 0] - [5, 1]
  name: type_identifier [2, 7] - [2, 11]
  body: field_declaration_list [2, 12] - [5, 1]
    field_declaration [3, 2] - [3, 10]
      type: primitive_type [3, 2] - [3, 5]
      declarator: field_identifier [3, 6] - [3, 9]
    field_declaration [4, 2] - [4, 10]
      type: primitive_type [4, 2] - [4, 5]
      declarator: field_identifier [4, 6] - [4, 9]
function_definition [7, 0] - [9, 1]
  type: primitive_type [7, 0] - [7, 3]
  declarator: function_declarator [7, 4] - [7, 36]
    declarator: identifier [7, 4] - [7, 8]
    parameters: parameter_list [7, 8] - [7, 36]
      parameter_declaration [7, 9] - [7, 17]
        type: primitive_type [7, 9] - [7, 12]
        declarator: identifier [7, 13] - [7, 17]
      parameter_declaration [7, 19] - [7, 35]
        type_qualifier [7, 19] - [7, 24]
        type: primitive_type [7, 25] - [7, 29]
        declarator: pointer_declarator [7, 29] - [7, 35]
          declarator: identifier [7, 31] - [7, 35]
  body: compound_statement [7, 37] - [9, 1]
    expression_statement [8, 2] - [8, 15]
      call_expression [8, 2] - [8, 14]
        function: field_expression [8, 2] - [8, 12]
          argument: identifier [8, 2] - [8, 7]
          field: field_identifier [8, 8] - [8, 12]
        arguments: argument_list [8, 12] - [8, 14]

After input the trigger "fn", it becomes:

#include <compare>

struct Test {
  int foo;
  int bar;
};

fn

int main(int argc, const char* argv) {
  xxxxx.test();
}

The tree becomes,

preproc_include [0, 0] - [1, 0]
  path: system_lib_string [0, 9] - [0, 18]
struct_specifier [2, 0] - [5, 1]
  name: type_identifier [2, 7] - [2, 11]
  body: field_declaration_list [2, 12] - [5, 1]
    field_declaration [3, 2] - [3, 10]
      type: primitive_type [3, 2] - [3, 5]
      declarator: field_identifier [3, 6] - [3, 9]
    field_declaration [4, 2] - [4, 10]
      type: primitive_type [4, 2] - [4, 5]
      declarator: field_identifier [4, 6] - [4, 9]
function_definition [7, 0] - [11, 1]
  type: type_identifier [7, 0] - [7, 2]
  ERROR [9, 0] - [9, 3]
    identifier [9, 0] - [9, 3]
  declarator: function_declarator [9, 4] - [9, 36]
    declarator: identifier [9, 4] - [9, 8]
    parameters: parameter_list [9, 8] - [9, 36]
      parameter_declaration [9, 9] - [9, 17]
        type: primitive_type [9, 9] - [9, 12]
        declarator: identifier [9, 13] - [9, 17]
      parameter_declaration [9, 19] - [9, 35]
        type_qualifier [9, 19] - [9, 24]
        type: primitive_type [9, 25] - [9, 29]
        declarator: pointer_declarator [9, 29] - [9, 35]
          declarator: identifier [9, 31] - [9, 35]
  body: compound_statement [9, 37] - [11, 1]
    expression_statement [10, 2] - [10, 15]
      call_expression [10, 2] - [10, 14]
        function: field_expression [10, 2] - [10, 12]
          argument: identifier [10, 2] - [10, 7]
          field: field_identifier [10, 8] - [10, 12]
        arguments: argument_list [10, 12] - [10, 14]

Now, the matched trigger "fn" now is a part of function_definition. But it should not be. So, my solution is in these cases, re-parse the whole buffer after removing matched trigger. This could make the buffer more "correct".

Here's my snippet using this feature:
https://github.com/TwIStOy/dotvim/blob/master/lua/ht/snippets/cpp/snippets/statements.lua

@L3MON4D3
Copy link
Owner

Ahh of course, very cool!
Have you tried doing this on the live-buffer, eg.

  • remove trigger
  • parse (incrementally)
  • re-insert trigger so snip_expand can do its thing

I like the current solution because it doesn't touch the buffer at all, but maybe this dirtier approach is faster than creating that giant string and parsing it

Of course, but i'm thinking of if i should introduce a new parameter for query text.

Wouldn't that just be an alternative to matchTSNode?

@TwIStOy
Copy link
Contributor Author

TwIStOy commented Aug 17, 2023

i haven't tried that way. i'm not sure if the treesitter's incremental parse will be triggered in this situation. will test it tomorrow.

i'm thinking of allow matchTSNode to be a table including query text, capture names and query group. this also allows users to customize the query group.

maybe like this,

---@class LuaSnip.extra.MatchTSNodeOpts
---@field capture string|string[]
---@field query_group string?
---@field query_text string?

@L3MON4D3
Copy link
Owner

I think as long as parse is called, it should work 🤞

What would we need capture and query_group for, in that context?
As far as I understand, we'd always want the entire query-match to be the POSTFIX_MATCH (otherwise we'd remove a subset of the matched query in front of the trigger, which seems unintuitive), and then we would provide additional captures defined in the query to the user as env-variables.

@TwIStOy
Copy link
Contributor Author

TwIStOy commented Aug 18, 2023

I think as long as parse is called, it should work 🤞

It works. I will update my implementation.

What would we need capture and query_group for, in that context? As far as I understand, we'd always want the entire query-match to be the POSTFIX_MATCH (otherwise we'd remove a subset of the matched query in front of the trigger, which seems unintuitive), and then we would provide additional captures defined in the query to the user as env-variables.

The user should specify the interest in which capture in query_group. Different query-groups can have different captures with the same name.

@L3MON4D3
Copy link
Owner

It works. I will update my implementation.

Should we allow all cases, eg. no reparsing, reparse using the live-buffer, or reparse using the string? Using the live buffer is probably a sane default, but I could see it causing issues, and exposing the option doesn't seem to cause much more complexity (at least as far as I can tell, if you think otherwise, we should leave it out)

The user should specify the interest in which capture in query_group. Different query-groups can have different captures with the same name.

Sorry, I still don't understand, can't we just pass through all captures to the snippet-env?
Could you show an example for capture and query_group?

@TwIStOy
Copy link
Contributor Author

TwIStOy commented Aug 18, 2023

yeah, i will change the option to nil|boolean|"complete".

  • nil: no re-parse
  • boolean: true, update the live buffer, only re-parse the modified part. false, no re-parae
  • "complete": a complete re-parse from the copied buffer

ah, the word capture in this case means a capture's name like "@parameter.inner", query_name means the name of capture set, like "textobject".
i think if we query all captures from all query _groups could be show, and most of these results are useless.

@L3MON4D3
Copy link
Owner

yeah, i will change the option to nil|boolean|"complete"

What about just string, and have the options reparse="no"|"live"|"copy"?

Okay I think we were talking about different things: My suggestion was to have an api that could be called like

local s = ts_postfix({
    query = [[
        (assignment_expression
          left: (identifier) @lhs
          right: (call_expression) @rhs)
    ]],
    query_lang = "cpp",
    trig = ".foo"
})

Where the user would then have env-variables:

  • TS_POSTFIX_FULL: the whole assignment-expression
  • TS_POSTFIX_lhs: whatever is captured as @lhs
  • TS_POSTFIX_rhs: whatever is captured as @rhs

But you also have a point, re-using the queries provided by nvim-treesitter is probably a good idea, since there might be some unnecessary effort in having to come up with the correct queries, if they are already available.

@TwIStOy
Copy link
Contributor Author

TwIStOy commented Aug 18, 2023

yeah, just a string is more clearly.

oh, i forget the provided query text case. what about give all captures if the query text is provided and only these interest captures if the captures name and query group is provided?

@L3MON4D3
Copy link
Owner

Ahh okay, I think I understand the whole query-business a bit better now:
queries/ft/*.scm contains one giant query (consisting of multiple S-expressions/patterns), and that query can be accessed via vim.treesitter.query.get, and iter_matches will give us each match, where we check if we found the capture the user wants.
AFAICT it's not possible to extract patterns from queries, so if we want to provide this functionality (which would be great, since it allows users to re-use existing semantic definitions, from eg. highlights.scm) there's always the downside that we will iterate lots of unrelated patterns (right?)

Regarding the captures, couldn't we just check if the match contains the desired capture, and then provide, in env_override, all captures of that match?

@TwIStOy
Copy link
Contributor Author

TwIStOy commented Aug 18, 2023

I'm trying to find all matches and setup these in env_override. But I think the user also should select one of them when resolveExpandParams. Without selecting one, we can't know which region should be cleared. So, an additional option selectTSNode has been introduced.

@L3MON4D3
Copy link
Owner

Oh yeah, that's what I meant with the desired capture 👍
I don't quite see what benefit the generality of a function brings here, wouldn't a string for identifying the desired capture be enough?

@TwIStOy
Copy link
Contributor Author

TwIStOy commented Aug 19, 2023

a user provided matchTSNode could be useful because sometimes the matched nodes are not from the captures. also, sometimes we know that the node we want is the k-th parent of the node at cursor, the customize matchTSNode may get a better performance.

a user provided selectTSNode is necessary and can no be a string. because a string can no determine which node is actually wanted. a capture could match many nodes. the desired node can't be selected in this case.

@L3MON4D3
Copy link
Owner

In my mind, the algorithm we should apply (given a query (either as file, or string) and a capture-name capnam) is

  1. iterate all matches of some query
  2. if one capture has the name capnam, and ends just before the trigger, we have found a match => provide all captures of the match to the user (via env), and use the range of the capnam-node for the full match (Until now I thought that we should always require that the full match is in front of the cursor, but some predefined patterns in highlights.scm have their capture not as the root of the pattern, but as a node inside it, so we'd unnecessarily not make those work, which would be stupid I think).
    I think all of this should be possible via Query:iter_matches(), no?

Regarding matching specific node-sequences, I think we should implement that separately, including that in the query-implementation seems messy, or I don't yet see how closely they are related.

because a string can no determine which node is actually wanted. a capture could match many nodes.

I don't think that's a bad thing, for example the cpp-highlights.scm defines many different patterns for constructor, but if a user wants a snippet that expands behind a constructor, just having to pass (something like) {query_name="highlights", query_lang="cpp", capture_name="constructor"} seems extremely desirable

@TwIStOy
Copy link
Contributor Author

TwIStOy commented Aug 19, 2023

Is your question about selectTSNode?
When dealing with a group of TSNode elements rather than just one, it becomes challenging to determine value for clear_region. This is the sole location to modify clear_region. Or the user should modify the buffer manually, it think it is ugly when user should update the buffer by theirselves in maybe pre_expand callback?

@L3MON4D3
Copy link
Owner

No, not really, I was suggesting an alternative api, where selectTSNode can be a simple string, because I can't tell when that fails at uniquely identifying a node in the match of a query.

If I understand the current code correctly, nodes can be filtered first via a query (passed as matchTSNode) and then one final node to act as POASTFIX_MATCH is selected via selectTSNode, right?
I can't quite tell when the entire combination is necessary: either the user needs a query and can define everything in there (I think it can handle almost everything one could implement in selectTSNode, at the least via custom predicates), or they just want to check that the node before the cursor is something specific, and then we don't need to bother with queries. Do you have an example where both are actually necessary?

@TwIStOy
Copy link
Contributor Author

TwIStOy commented Aug 20, 2023

If I understand the current code correctly, nodes can be filtered first via a query (passed as matchTSNode) and then one final node to act as POASTFIX_MATCH is selected via selectTSNode, right?

It's not important if there's a final node to act as POASTFIX_MATCH. I think the only reason that we need to finally find one node is the node's range should to be act as clear_region.

Here's an example:

; extends

[
  (call_expression)
  (identifier)
  (template_function)
  (subscript_expression)
  (field_expression)
  (user_defined_literal)
] @any_expr
  tsp.treesitter_postfix({
    trig = ".be",
    name = "begin..end",
    dscr = "Completes a variable with both begin() and end().",
    matchTSNode = {
      captures = "any_expr",
    },
  }, {
    f(function(_, parent)
      vim.print(parent.env.TREESITTER_MATCHES)
      -- return su.replace_all(
      --   parent.snippet.env.TSNODETEXT_MATCH,
      --   "%s.begin(), %s.end()"
      -- )
    end, {}),
  }),

In this case, it's hard to determine the clear_region except the user provide it.

@L3MON4D3
Copy link
Owner

I can't quite see why this is ambiguous.. can these nodes overlap?
What would the selectTSNode for resolving the ambiguity look like?

@TwIStOy
Copy link
Contributor Author

TwIStOy commented Aug 20, 2023

Some of these node overlap

#include <compare>

struct Test {
  int foo;
  int bar;

};

int main(int argc, const char* argv) {
  test().abasdf.asdfasd
  
}
; extends

[
  (call_expression)
  (identifier)
  (template_function)
  (subscript_expression)
  (field_expression)
  (user_defined_literal)
  (field_identifier)
] @any_expr
preproc_include [0, 0] - [1, 0]
  path: system_lib_string [0, 9] - [0, 18]
struct_specifier [2, 0] - [6, 1]
  name: type_identifier [2, 7] - [2, 11]
  body: field_declaration_list [2, 12] - [6, 1]
    field_declaration [3, 2] - [3, 10]
      type: primitive_type [3, 2] - [3, 5]
      declarator: field_identifier [3, 6] - [3, 9]
    field_declaration [4, 2] - [4, 10]
      type: primitive_type [4, 2] - [4, 5]
      declarator: field_identifier [4, 6] - [4, 9]
declaration [8, 0] - [11, 1]
  type: primitive_type [8, 0] - [8, 3]
  declarator: init_declarator [8, 4] - [11, 1]
    declarator: function_declarator [8, 4] - [8, 36]
      declarator: identifier [8, 4] - [8, 8]
      parameters: parameter_list [8, 8] - [8, 36]
        parameter_declaration [8, 9] - [8, 17]
          type: primitive_type [8, 9] - [8, 12]
          declarator: identifier [8, 13] - [8, 17]
        parameter_declaration [8, 19] - [8, 35]
          type_qualifier [8, 19] - [8, 24]
          type: primitive_type [8, 25] - [8, 29]
          declarator: pointer_declarator [8, 29] - [8, 35]
            declarator: identifier [8, 31] - [8, 35]
    value: initializer_list [8, 37] - [11, 1]
      field_expression [9, 2] - [9, 23]
        argument: field_expression [9, 2] - [9, 15]
          argument: call_expression [9, 2] - [9, 8]
            function: identifier [9, 2] - [9, 6]
            arguments: argument_list [9, 6] - [9, 8]
          field: field_identifier [9, 9] - [9, 15]
        field: field_identifier [9, 16] - [9, 23]

In this case, we got:

{
  any_expr = { {
      start = { 9, 2 },
      text = { "test().abasdf.asdfasd" },
      type = "field_expression"
    }, {
      start = { 9, 16 },
      text = { "asdfasd" },
      type = "field_identifier"
    } }
}

The user should select if they want field_identifier or field_expression?

@L3MON4D3
Copy link
Owner

Okay I see, thanks 👍

Unfortunately this complicates (and makes slower) the simple case of matching a pattern that does not have any ambiguity, where the first match at the trigger is the one we'd like to use.
I think we should provide at least two predefined selectTSNode-functions, "any", which aborts the query:iter_matches-loop as soon as a candidate is found, and "longest", which is probably what most users want here (I guess)

Different question:
When match_node is a user-defined-function, is there any benefit of passing its result through select_matches? Couldn't the user already decide what exact node to use in match_node? My reason for asking this is that IMO we should (at least in the API, so in treesitter_postfix) separate using a query and using a specific function (treating both the same in generate_resolve_expand_param seems good though 👍)

So we should allow options like {query={"cpp", "highlights"}, capture_name="constructor", select_capture="longest"} (if query is a string, it should be interpreted as the query to use) (we could group query_name and query_lang togethercapture_name and select_capture should be grouped together somehow, like longest(capture_name) or any(capture_name)), or, separately {resolve_node=find_topmost_parent("node_name")}
(So, I'd advertise both as separate ways of resolving the node we should use for clear_region, not as somehow interconnected, as that seems a bit complicated)

@TwIStOy
Copy link
Contributor Author

TwIStOy commented Aug 20, 2023

If the user provide a match_tsnode function, I believe that the function can provide the exact TSNode they want. The selectTSNode becomes a new option when I'm implementing captures from query_text or capture_name. :)

Ah, options like {query={"cpp", "highlights"}, capture_name="constructor", select_capture="longest"} seems a good idea.

@TwIStOy
Copy link
Contributor Author

TwIStOy commented Aug 20, 2023

I have updated the implementation. Now the matchTSNode options can be:

  • string: indicates the query_text.
  • string[]: indicates the list of captures.
  • function: user defined matcher
  • LuaSnip.extra.MatchTSNodeOpts

The definition of LuaSnip.extra.MatchTSNodeOpts moved to lua/luasnip/extras/_extra_types.lua.

@L3MON4D3
Copy link
Owner

Hi, I finally have some time to get back into this :)

Could you update the documentation? I think the interface is pretty nice now, and we probably don't have to change it again (I hope :D)

@TwIStOy
Copy link
Contributor Author

TwIStOy commented Sep 4, 2023

DOC has updated. you can take a look.:)

@L3MON4D3
Copy link
Owner

Hey, I decided, instead of trying to convey each of my desired changes via words here, to just implement them.
There are quite a few, what do you think of them?
I don't think I've limited the scope of this feature (ie. it should still be as powerful/flexible as before), but if I did, please point it out.

I have a few more things in mind (eg. make range of nodes accessible in env), but wanted to see what you think before I go ahead and make any more.
It's very possible there are some small mistakes in there, I haven't yet tested it properly

@L3MON4D3
Copy link
Owner

One example:

ls.setup_snip_env()

local ts_post = require("luasnip.extras.treesitter_postfix").treesitter_postfix

ls.add_snippets("all", {
	ts_post({
		matchTSNode = {
			query = [[
				(function_declaration
				  name: (identifier) @fname
				  parameters: (parameters) @params
				  body: (block) @body
				) @prefix
			]],
			query_lang = "lua",
		},
		trig = "var"
	}, fmt([[
		local {} = function{}
			{}
		end
	]], {
		l(l.TS_CAPTURE_fname),
		l(l.TS_CAPTURE_params),
		l(l.TS_CAPTURE_body),
	}))
}, {key = "asdf"})
1694370215.mp4

@L3MON4D3
Copy link
Owner

I looked a bit more into providing node-ranges and other data via env, and there's one tradeoff to make:
We can compute docstrings for snippets, ie. a representation that does not use any data provided at runtime (for example here, the treesitter-captures, but more likely some of the TS_SOMETHING environment-variables).
For this to work, we have to provide some dummy-data for evaluation of dynamic/functionNode, and this is generally done by setting env.TS_SOMETHING to "TS_SOMETHING", which generally leads to descriptive docstrings for a snippet.
Now, the issue here is that this mechanism would not work at all with text-ranges, or we'd have to implement some more flexibility for this dummy-data, but I don't think that is really a good idea yet.

So, we could still provide the data in env, but we'd have to put a big disclaimer in the documentation, warning users of this caveat.
(The workaround to make snippets play nicely with this would be to just detect this docstring-generation (checking some variable for example, local is_static = env.A == "A"), and then behave differently in function/dynamicNode)

@TwIStOy
Copy link
Contributor Author

TwIStOy commented Sep 12, 2023

I think we can just provide data in env. It's hard to compute the doc-strings for these snippets with function/dyanmicNodes even we providing the dummy data.

@L3MON4D3 L3MON4D3 force-pushed the new-extra-ts-snippet branch 3 times, most recently from 7e69578 to a0532ab Compare September 15, 2023 13:29
@L3MON4D3
Copy link
Owner

Okay, so I don't have anything more to add, what do you think about it?
Also, I'm interested: do you have any snippets that use the manual matchTSNode? What functions are generally useful for those? (so what else should be documented :D)

DOC.md Outdated Show resolved Hide resolved
@L3MON4D3 L3MON4D3 force-pushed the new-extra-ts-snippet branch from 0dda26b to 3a03271 Compare September 16, 2023 14:15
DOC.md Outdated Show resolved Hide resolved
@L3MON4D3
Copy link
Owner

I added some tests, and required a minimum-version of nvim-0.9 for requiring treesitter_postfix (some api-functions are not available/have different names earlier, and I feel like the additional work for getting this running under 0.8 is not worth it).

Also switched 0.7 (which we used for running tests on an older version to 0.8 and 0.9, since 0.7 no longer compiled for me. I'll try looking into those issues, and re-including 0.7 tomorrow or so 😅)

@L3MON4D3 L3MON4D3 force-pushed the new-extra-ts-snippet branch from f7f8e8a to 71f844e Compare September 17, 2023 10:04
@L3MON4D3
Copy link
Owner

Alright fixed it, now we have 0.7, 0.9 and master.

@L3MON4D3 L3MON4D3 force-pushed the new-extra-ts-snippet branch 2 times, most recently from 2c14ab6 to e232c3e Compare September 17, 2023 10:44
@L3MON4D3
Copy link
Owner

Alright, so I think this is done, at least as far as queries are concerned..
Could you show some examples of how you use find_topmost_parent or find_nth_parent?

@TwIStOy
Copy link
Contributor Author

TwIStOy commented Sep 19, 2023

Of course. Some postfix I used:

tsp.treesitter_postfix({
  trig = ".be",
  name = "begin..end",
  dscr = "Completes a variable with both begin() and end().",
  wordTrig = false,
  reparseBuffer = nil,
  matchTSNode = tsp.builtin.tsnode_matcher.find_topmost_types({
    "call_expression",
    "identifier",
    "template_function",
    "subscript_expression",
    "field_expression",
    "user_defined_literal",
  }, ".be"),
}, {
  f(function(_, parent)
    return su.replace_all(
      parent.snippet.env.LS_TSMATCH,
      "%s.begin(), %s.end()"
    )
  end, {}),
}),

Other snippets with a simple wrapper:

ts_postfix_any_expr {
  ".mv",
  name = "move(expr)",
  dscr = "Wraps an expression with 'std::move' if it is available",
  nodes = {
    f(function(_, parent)
      return su.replace_all(parent.snippet.env.POSTFIX_MATCH, "std::move(%s)")
    end, {}),
  },
},

ts_postfix_any_expr {
  ".fwd",
  name = "forward(expr)",
  dscr = "Wraps an expression with 'std::forward' if it is available.",
  nodes = {
    f(function(_, parent)
      return su.replace_all(
        parent.snippet.env.POSTFIX_MATCH,
        "std::forward<decltype(%s)>(%s)"
      )
    end, {}),
  },
},

ts_postfix_any_expr {
  ".val",
  name = "declval<expr>()",
  dscr = "Wraps an expression with 'std::declval' if it is available.",
  nodes = {
    f(function(_, parent)
      return su.replace_all(
        parent.snippet.env.POSTFIX_MATCH,
        "std::declval<%s>()"
      )
    end, {}),
  },
},

ts_postfix_any_expr {
  ".uu",
  name = "(void)expr",
  dscr = "Wraps an expression with '(void)expr' to silence unused variable warnings.",
  nodes = {
    f(function(_, parent)
      return su.replace_all(parent.snippet.env.POSTFIX_MATCH, "(void)%s;")
    end, {}),
  },
},

ts_postfix_any_expr {
  ".dt",
  name = "decltype(expr)",
  dscr = "Wraps an expression with 'decltype' to get the type of an expression.",
  nodes = {
    f(function(_, parent)
      return su.replace_all(parent.snippet.env.POSTFIX_MATCH, "decltype(%s)")
    end, {}),
  },
},

ts_postfix_any_expr {
  ".for",
  name = "for (expr)",
  dscr = "Wraps an expression with a range-based 'for' loop.",
  nodes = fmta(
    [[
    for (<typ> <item> : <expr>) {
      <body>
    }
    ]],
    {
      body = i(0),
      expr = f(function(_, parent)
        return parent.snippet.env.POSTFIX_MATCH
      end, {}),
      typ = c(1, {
        t("const auto&"),
        t("auto&&"),
      }),
      item = i(2, "item"),
    }
  ),
},

function()
  local fori_types = vim.deepcopy(ts_postfix.any_expr_types)
  fori_types[#fori_types + 1] = "number_literal"
  return ts_postfix.ts_postfix_maker(fori_types) {
    ".fori",
    name = "for (int i = 0; i < expr; i++)",
    dscr = "Wraps an expression with a 'for' loop.",
    nodes = fmta(
      [[
    for (decltype(<expr>) i = 0; i << <expr>; i++) {
      <body>
    }
    ]],
      {
        body = i(0),
        expr = f(function(_, parent)
          return parent.snippet.env.POSTFIX_MATCH
        end, {}),
      }
    ),
  }
end,

ts_postfix_any_expr {
  ".if",
  name = "if (expr)",
  dscr = "Wraps an expression with an 'if' condition.",
  nodes = fmta(
    [[
    if (<expr>) {
      <body>
    }
    ]],
    {
      body = i(0),
      expr = f(function(_, parent)
        return parent.snippet.env.POSTFIX_MATCH
      end, {}),
    }
  ),
},

ts_postfix_any_expr {
  ".else",
  name = "if (!expr)",
  dscr = "Negates an expression and wraps it with 'if'.",
  nodes = fmta(
    [[
    if (!<expr>) {
      <body>
    }
    ]],
    {
      body = i(0),
      expr = f(function(_, parent)
        return parent.snippet.env.POSTFIX_MATCH
      end, {}),
    }
  ),
},

ts_postfix_any_expr {
  ".sc",
  name = "static_cast<>(expr)",
  dscr = "Wraps an expression with 'static_cast'.",
  nodes = fmt(
    [[
    static_cast<{body}>({expr}){end}
    ]],
    {
      body = i(1),
      expr = f(function(_, parent)
        return parent.snippet.env.POSTFIX_MATCH
      end, {}),
      ["end"] = i(0),
    }
  ),
},

ts_postfix_any_expr {
  ".cast",
  name = "xxx_cast<>(expr)",
  dscr = "Wraps an expression with a cast expression.",
  nodes = fmt(
    [[
    {cast}<{body}>({expr}){end}
    ]],
    {
      cast = c(1, {
        t("static_cast"),
        t("reinterpret_cast"),
        t("dynamic_cast"),
        t("const_cast"),
      }, {
        desc = "Type conversion casts",
      }),
      body = i(2),
      expr = f(function(_, parent)
        return parent.snippet.env.POSTFIX_MATCH
      end, {}),
      ["end"] = i(0),
    }
  ),
},

ts_postfix_ident_only {
  ".fd",
  name = "if (..find)",
  dscr = "Find a member exists in given indent.",
  nodes = fmta(
    [[
    if (auto it = <ident>.find(<key>); it <equal> <ident>.end()) {
      <body>
    }
    ]],
    {
      ident = f(function(_, parent)
        return parent.snippet.env.POSTFIX_MATCH
      end, {}),
      key = i(1, "key"),
      equal = c(2, {
        t("=="),
        t("!="),
      }, {
        desc = "Equals, Not Equals",
      }),
      body = i(0),
    }
  ),
},

@L3MON4D3 L3MON4D3 force-pushed the new-extra-ts-snippet branch from a968ae1 to a83abb2 Compare September 20, 2023 14:25
TwIStOy and others added 2 commits September 20, 2023 16:44
See DOC.md-changes for extensive documentation and examples.
Validate treesitter-postfix on oldest release where it is enabled.
@L3MON4D3 L3MON4D3 force-pushed the new-extra-ts-snippet branch from 333f51b to 5f6929f Compare September 20, 2023 14:45
@L3MON4D3
Copy link
Owner

Alright, I think this is it, sorry for being a bit more demanding on this one 😅
Would you take one last look?

@TwIStOy
Copy link
Contributor Author

TwIStOy commented Sep 21, 2023

Looks great!

@L3MON4D3
Copy link
Owner

Alriight, thank you for your help!

@L3MON4D3 L3MON4D3 merged commit 5bc8c0b into L3MON4D3:master Sep 21, 2023
@TwIStOy TwIStOy deleted the new-extra-ts-snippet branch December 2, 2023 10:45
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

Successfully merging this pull request may close these issues.

2 participants