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

[Question] dynamic nodes - create nodes at later position/use nesting #328

Closed
atticus-sullivan opened this issue Mar 7, 2022 · 19 comments
Closed

Comments

@atticus-sullivan
Copy link
Contributor

Hi,
I just got into reading the docs/manual and while porting some of my snippets, I stumbled over this.

Is is possible to use dynamic nodes for infinite new nodes but for each node, a new node at another position is created. Sounds probably confusing, so let me illustrate this with my current snippet as an example.

So at first, the snippet expanded to
image
now by toggling the choice node, it inserts a node for inserting the first argument
image
after that the user can decide (probably by choice node) there should be an additional argument thus
image
is created.
After finishing the argument list, there should be nodes to change the variable names (self.arg) to which the arguments are assigned to.


I already played around a bit with

local function py_init()
    return
        sn(nil, {
            i(1,"arg"),
            c(2,{
                sn(nil,{
                    t("):")
                }),
                d(nil,py_init)
            }),
            t{"\tself."},
            rep(1),
            t{" = "},
            rep(1)
        })
end

and

s("test", fmt(
    [[def __init__(self, {}]],
    {
        c(1, {t({"):", "\tpass", ""}), d(nil, py_init, {})})
    }
)),

which should give a tokenization like this
image
(wrong order of the arguments, but I'd be able to live with that). But I'm stuck at the choice node in my py_init function, after inserting the first argument I can't switch the choice (otherwise I get back to the 0 argument version) and tab jumps out of the whole snippet (at the end of the snippet and of course switching choice there doesn't work).

Note: the init template is missing the self parameter for valid python and the self.arg text is no insert node yet.


Now I see this issue is quite similar to the latex itemize and the jdocsnip functionality provided in the examples. But I don't get the issue with my snippet (the inability to switch choice).

And I'm interested in if this approach can be improved as having to insert the delimiting text ():\n) after the last argument is unpleasant since it commands how the recursive calls should be nested, and thus the ordering of the arguments).
[The separation of where the nodes are created (here: in the argument list) and where additional new nodes are inserted (here: the function body) without the need to pass the content between these two places on to the last recursive call was the basic idea/question of this issue]

I know this is quite a huge question so take your time and remember you're doing this voluntarily so don't feel in the need to help (even though of course I'd appreciate it)

@L3MON4D3
Copy link
Owner

L3MON4D3 commented Mar 7, 2022

First of all, good job with the images, they nicely illustrate what you mean 👍

The issue with the snippet in its' current form is just that the initial choice for py_init contains no insertNode to stop the cursor at. Adding an i(1) somewhere inside the snippetNode makes it possible to select another choice.

c(2,{
	sn(nil,{
		i(1), t("):")
	}),
	-- or just t("):"), a single textNode is actually detected and stopped at, but this doesn't work for snippetNodes.
	d(nil,py_init)
}),

(This exact problem has come up a few times already, I'll mention it in the Doc).

And I'm interested in if this approach can be improved

Yes! I think this is a really nice application for the DynamicNode with user Input, in this case you'd control not the number of rows in a table, but the number of arguments for the function.

I know this is quite a huge question so take your time and remember you're doing this voluntarily so don't feel in the need to help (even though of course I'd appreciate it)

Thank you :D
But I gotta say, I really like writing snippets (esp. more involved ones), so I even enjoy questions like this :)

@atticus-sullivan
Copy link
Contributor Author

atticus-sullivan commented Mar 7, 2022

First of all, good job with the images, they nicely illustrate what you mean 👍
Thanks (being honest, it took some while. But writing detailed questions and making things as clear as possible often also helps clearing ones own mind and often the question solves just by its own)

Not quite sure why I even used a sn at this place (I guess because I didn't thought of t{"", ""} and earlier there were multiple text nodes at this place).

So this is what I came up with up to now:

local function py_init()
    return
        sn(nil, {
            t(", "),
            i(1,"arg"),
            c(2,{
                t({"):"}),
                d(nil,py_init)
            }),
            t({"", ""}),
            t{"\tself."},
            d(3, function(args,snip,uarg) return i(nil, args[1][1]) end, {1}),
            t{" = "},
            rep(1),
        })
end
s("test", fmt(
    [[def __init__(self{}]],
    {
        c(1, {t({"):", "\tpass", ""}), d(nil, py_init, {})})
    }
)),

Peek 2022-03-07 18-07
(Note about the keys: tab is my jump-key, ctrl+j is my change_choice-key)
Not quite sure if this is robust/working always, but in my tests it worked.

Still I think your hint about DynamicNode with user Input looks promising and I'll check it out (but it's quite some code so I guess this will take time to wrap my head around). [I'll keep this issue open for that time if this is ok with you]

But I gotta say, I really like writing snippets (esp. more involved ones), so I even enjoy questions like this :)
And we love your plugin 👍 ^^

So thanks for the (fast) help

@L3MON4D3
Copy link
Owner

L3MON4D3 commented Mar 7, 2022

Nice, that looks pretty good already 👍

I'll keep this issue open for that time if this is ok with you

Yup, please do, I don't mind relevant issues being open longer

And we love your plugin 👍 ^^

Hah, thank you :D

@atticus-sullivan
Copy link
Contributor Author

Few notes

  • local node_util = require("luasnip.nodes.util") was missing for me
  • making dynamic_node_external_update local results in _G.dynamic_node_external_update being nil (as it isn't global)

And maybe a small note that C-t will increase the row count while C-g decreases it would be nice (especially for users who simply want to use the function)

in this case you'd control not the number of rows in a table, but the number of arguments for the function.
Hm, I see but I'm not quite sure how to combine this with the possibility of creating an infinite argument list.

One possibility maybe would be using a big input node holding the whole argument list. This wouldn't require external updating as far as I thought it through (except if one wants restore nodes I guess).

The issue I have with this is that this way if jumping back to the input node with the argument list, the whole argument list is marked (in select mode) and I don't know how to only edit a portion of it (if e.g. only the name of one argument should be changed or one argument should be removed). (This is why I aimed at having different input nodes for the different arguments in the first place)

@L3MON4D3
Copy link
Owner

L3MON4D3 commented Mar 9, 2022

Few notes

  • local node_util = require("luasnip.nodes.util") was missing for me
  • making dynamic_node_external_update local results in _G.dynamic_node_external_update being nil (as it isn't global)

And maybe a small note that C-t will increase the row count while C-g decreases it would be nice (especially for users who simply want to use the function)

Thank you, good points👍

in this case you'd control not the number of rows in a table, but the number of arguments for the function.
Hm, I see but I'm not quite sure how to combine this with the possibility of creating an infinite argument list.

I don't really get what you mean with the infinite argument list, I hope I'm not misunderstanding something :D
My idea was to adapt the dynamicNode function of the table-snippet to generate the arguments+assignments below that instead of that table. This would make it possible to adjust the number of args via <c-t/g> (removing from the middle sounds hard though, pretty sure that's not easily doable. Maybe with choiceNodes, but that'd end up being rather ugly I think)

One possibility maybe would be using a big input node holding the whole argument list. This wouldn't require external updating as far as I thought it through (except if one wants restore nodes I guess).

The issue I have with this is that this way if jumping back to the input node with the argument list, the whole argument list is marked (in select mode) and I don't know how to only edit a portion of it (if e.g. only the name of one argument should be changed or one argument should be removed). (This is why I aimed at having different input nodes for the different arguments in the first place)

Thats how the Javadoc-snippet works, and true, it's not really optimal.

@atticus-sullivan
Copy link
Contributor Author

I don't really get what you mean with the infinite argument list, I hope I'm not misunderstanding something :D

Started so well with good explaining... Well by infinite I mean something like the snippet for item where you can add new items (here arguments) as long as you want (just like the first try permitted).

removing from the middle sounds hard though, pretty sure that's not easily doable. Maybe with choiceNodes, but that'd end up being rather ugly I think

Hm yes that's not easy but I think is won't either way since removing nodes from the middle is almost impossible (unless moving every node coming after the one to be removed forward, and how should this be triggerd? (inserting new key bindings all over isn't really the way to go I think))

This would make it possible to adjust the number of args

Ah now I understand what you meant. How would you get the names of the arguments this way? Passing the root dynamic node of the subtree containing the argument list? (as I'm doing it below)

I played around a bit and came up with

local function py_init2()
	return
		c(1, {
			t(""),
			sn(1, {
				t(", "),
				i(1),
				d(2, py_init2)
			})
		})
end

local function to_init_assign(args)
	local tab = {}
	local a = args[1][1]
	if #(a) == 0 then
		table.insert(tab, t({"", "\tpass"}))
	else
		local cnt = 1
		for e in string.gmatch(a, " ?([^,]*) ?") do
			if #e > 0 then
				table.insert(tab, t({"","\tself."}))
				table.insert(tab, r(cnt, tostring(cnt), i(nil,e)))
				table.insert(tab, t(" = "))
				table.insert(tab, t(e))
				cnt = cnt+1
			end
		end
	end
	return
		sn(nil, tab)
end
s("pyinit", fmt(
	[[def __init__(self{}):{}]],
	{
		d(1, py_init2),
		d(2, to_init_assign, {1})
	})),

This is still using choice nodes to add elements to the argument list (no additional keybinding) and with restore nodes, manual overwritten arguments in the assignment body do not change (the self.xxx = yy part).
Peek 2022-03-09 23-19

(This didn't came into my mind as in the beginning I didn't thought I'd be able to access the complete text of a subtree (thought it would be always the text that directly is contained in the corresponding node))

@L3MON4D3
Copy link
Owner

L3MON4D3 commented Mar 10, 2022

Oh, nice, that looks pretty much perfect 👍

Just for the record, here's what I had in mind with external_update_dynamic_node:

local pyinit = function(_, parent)
	-- this could also be outside the dynamicNode.
	local nodes = {t"def __init__(self"}

	-- snip.argc is controlled via c-t/c-g
	local argc = parent.argc
	-- snip.argc is not set on the first call.
	if not argc then
		parent.argc = 1
		argc = 1
	end

	-- store jump_indx separately and increase for each insertNode.
	local jump_indx = 1
	-- generate args
	for _ = 1, argc do
		vim.list_extend(nodes, {t", ", i(jump_indx, "arg"..jump_indx)})
		jump_indx = jump_indx + 1
	end

	nodes[#nodes + 1] = t{")", ""}

	-- generate assignments
	for j = 1, argc do
		vim.list_extend(nodes, {
			t"\t self.",
			i(jump_indx, "arg"..j),
			t" = ",
			-- repeat argj
			rep(j),
			t{"", ""}})
		jump_indx = jump_indx + 1
	end

	-- remove last linebreak.
	nodes[#nodes] = nil

	return sn(nil, nodes)
end

...

s("pyinit", d(1, pyinit, {},
	function(parent) parent.argc = parent.argc + 1 end,
	-- Don't drop below one arg. If 0 args should be accepted too, pyinit needs
	-- to be extended so it has at least one insertNode from which the
	-- dynamicNode may be updated via c-t/c-g.
	function(parent) parent.argc = math.max(parent.argc - 1, 1) end ))
1646898950.mp4

(Do note that this code will break the next time luasnip is updated, I'll push some breaking changes concerning the dynamicNode-user_args soon. d(1, pyinit, {}, {user_args = {fn1, fn2}} will work then)

@L3MON4D3
Copy link
Owner

L3MON4D3 commented Mar 10, 2022

One more idea: using vim.ui.input the number of args can also be set directly (eg. without "scrolling"):

s("pyinit", d(1, pyinit, {},
	function(parent)
		vim.ui.input(
			{prompt = "Number of args: "},
			function(argc)
				parent.argc = math.max(argc, 1)
		end)
	end
))

@atticus-sullivan
Copy link
Contributor Author

What do you mean with scrolling?

@L3MON4D3
Copy link
Owner

Ah, that was not very precise 😅
Instead of in/decrementing argc by one, with that function it can just be set to the desired value right away.

@atticus-sullivan
Copy link
Contributor Author

Ah yes right. So now we've got four solutions

  1. Doing recursion -> wrong ordering, but has some nice properties in holding/restoring the argument names on updates in the argumentlist
  2. By choice nodes and splitting the argumentlist string manually -> no additional keybindings, but need to be concerned with the choice nodes
  3. Setup like with the latex tables -> no choice nodes, but additional keybinding
  4. Directly setting the number of arguments via an input window -> no choice nodes and no additional keybindings as I understand

Before doing the list I thought that 2. would be nice, but now after thinking about it again maybe 4. is the nicest one ^^

@L3MON4D3
Copy link
Owner

L3MON4D3 commented Mar 10, 2022

Ah, the way it's set up now you'd have to use one key for 4, the function that queries for argc is only called via <c-t>. But if you don't need to adjust the number of args after expanding the snippet, you can also use a regTrig-snippet:

s({trig="pyinit(%d+)", regTrig=true}, d(1, pyinit, {}))

...
-- in pyinit:
local argc = tonumber(parent.captures[1])
...

and then trigger it via pyinit3, for example.

@atticus-sullivan
Copy link
Contributor Author

the function that queries for argc is only called via <c-t>

Ah right, I missed the point that the ui function is the argument to the pyinit function.

@atticus-sullivan
Copy link
Contributor Author

So I think for me this is solved (I was only looking for one solution (and then understanding the latex table snippet), now I've got four xD). Thanks a lot for explaining and suggesting various ways to solve this 👍
Do you think we should summarize the solutions we've found (actually code+gif are already present distributed in the comments of this issue)?

@L3MON4D3
Copy link
Owner

So I think for me this is solved (I was only looking for one solution (and then understanding the latex table snippet), now I've got four xD). Thanks a lot for explaining and suggesting various ways to solve this 👍

😆
You're welcome :D

Do you think we should summarize the solutions we've found (actually code+gif are already present distributed in the comments of this issue)?

The solutions definitely shouldn't be left buried in some issue :D
Would you add whichever snippet you end up using to the wiki ("Cool Snippets" xD) and maybe link this issue there, mentioning that there are multiple solutions?

@atticus-sullivan
Copy link
Contributor Author

atticus-sullivan commented Mar 10, 2022

Yes, of course. Good idea (EDIT: I'll do that ^^)

@L3MON4D3
Copy link
Owner

Nice, thank you 😄

@atticus-sullivan
Copy link
Contributor Author

Done. If there is something with the section (too bad documented or something else) just let me know. And again thanks for helping 👍

@L3MON4D3
Copy link
Owner

No problem, thanks for extending the wiki :D

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

2 participants