cursorless-dev / cursorless

Don't let the cursor slow you down
https://www.cursorless.org/
MIT License
1.13k stars 78 forks source link

Stable snippets #443

Open pokey opened 2 years ago

pokey commented 2 years ago

The problem

The tab stops in VSCode's snippet interface can be a bit fragile, especially when combining them with cursorless commands. If you're not careful, vscode will decide that you're no longer in a snippet and drop all the tab stops.

In addition, VSCode snippets have no way of indicating how a snippet hole should expand as text is inserted and the code around it changes. Should it expand in both directions? Should it expand to match a particular syntactic scope (eg the syntax node corresponding to the condition of an if statement)?

Having an extremely stable version of snippets where the tab stops don't go away even if you switch to a different file could also enable some interesting workflows where you set up a snippet and then navigate from file to file adding things to a snippet body that is still in the background

We'd like an experience which is similar to the way that holes work in agda.

Implementation

References

We should take a look at https://github.com/ethan-leba/tree-edit

wolfmanstout commented 2 years ago

This is a really interesting idea -- I came here after watching your YouTube video. I have a few clarification questions on the implementation. You mentioned that these would be so stable that they don't go away if you navigate to another file. Do you also intend that these remain active if you navigate to another place in the same file? That sounds harder, because at least the cursor location within an editor is saved when switching editors. Similarly, how about closing and reopening a file? Are you expecting to only have one snippet active at time? How about nested snippets? These all sound really difficult to support, but they would be very powerful: essentially promoting snippets to first-class entities alongside tree-sitter nodes. I completely understand if this is way out of scope -- I'm just trying to better understand what you have in mind. Maybe this makes more sense if only applied to a subset of snippets which are intended to mirror tree-sitter node types.

When cursorless inserts a snippet, instead of using the built-in vscode snippet engine, it will insert the text of the snippet and then use decorations to indicate the different holes in the snippet

Is the idea that you would use the existing hat decorations, except on the empty holes instead of on characters? Or did you have something else in mind?

I was thinking that the natural way to refer to these holes is via modifiers named after the snippet structure. So if my cursor is anywhere within an "if" snippet with a "condition" placeholder, then I can refer to "snip condition" just as I can refer to "funk" if I'm anywhere within a function. I could also refer to "snip if" (or maybe just "snip") to select the entire snippet. This is maybe a confusing example because "condition" exists today as a scope, but hopefully it's clear how this would generalize to user-defined snippets.

Add marks corresponding to all the different holes of a snippet, eg "snip condition", etc. The marks could potentially be active even if the editor is not visible

Maybe your response to my earlier question will answer this: what do you mean by "add marks"? Do you mean a whole new mark type, or a decoration? Your example "snip condition" sounds more like a modifier, similar to what I described above, but I'm probably misunderstanding.

Thanks for your patience here -- still new to Cursorless but your videos (especially the internals walkthrough) have been super helpful to building my mental model.

pokey commented 2 years ago

Do you also intend that these remain active if you navigate to another place in the same file? That sounds harder, because at least the cursor location within an editor is saved when switching editors. Similarly, how about closing and reopening a file?

I do, and it's actually not any harder. The snippet placeholders will just be represented by a location, so where your cursor happens to be at any given moment isn't actually relevant

Even if you close and reopen a workspace, you should be ok, because we can store the locations in workspace state (scroll down to workspaceState)

Are you expecting to only have one snippet active at time?

That's a super good question. The answer is "no", but I hadn't actually thought too hard about that case. If you have multiple snippets active that have different placeholder names, it should be ok. If you have multiple snippets active which have the same placeholder names (eg the same snippet), I think probably the best solution would be to have a stack, and by default the placeholder marks refer to the top level of the stack, but you can use some prefix to refer to other open copies of the same snippet, such as ordinals, so eg "second snip condition"

We'd want to do something to make sure you don't leave a snippet open accidentally 🤔

How about nested snippets?

Yep! Shouldn't be any extra work to support nested snippets once we support multiple snippets

These all sound really difficult to support, but they would be very powerful: essentially promoting snippets to first-class entities alongside tree-sitter nodes.

💯

I completely understand if this is way out of scope -- I'm just trying to better understand what you have in mind.

Not out of scope at all! And really helpful to make me clarify 😊

Maybe this makes more sense if only applied to a subset of snippets which are intended to mirror tree-sitter node types.

I think we'd support it no matter what mechanism is used to keep the snippet hole range up to date, but obviously I'd imagine that mirroring to a tree-sitter node will be a common case (tho keep in mind we likely won't mirror directly to a tree-sitter node type, but rather to a cursorless scope type which corresponds to a pattern matcher.

Is the idea that you would use the existing hat decorations, except on the empty holes instead of on characters? Or did you have something else in mind?

No, basically we'd render something that looks like placeholder text you'd see in an HTML form, using the name of the snippet hole as the placeholder text

I was thinking that the natural way to refer to these holes is via modifiers named after the snippet structure. So if my cursor is anywhere within an "if" snippet with a "condition" placeholder, then I can refer to "snip condition" just as I can refer to "funk" if I'm anywhere within a function. I could also refer to "snip if" (or maybe just "snip") to select the entire snippet. This is maybe a confusing example because "condition" exists today as a scope, but hopefully it's clear how this would generalize to user-defined snippets.

Yes that's what I was thinking as well, but I wouldn't require your cursor to be inside the "if" snippet; you could use that mark from anywhere, including another file / if the original file was hidden or closed

Add marks corresponding to all the different holes of a snippet, eg "snip condition", etc. The marks could potentially be active even if the editor is not visible

Maybe your response to my earlier question will answer this: what do you mean by "add marks"? Do you mean a whole new mark type, or a decoration? Your example "snip condition" sounds more like a modifier, similar to what I described above, but I'm probably misunderstanding.

It's a whole new mark type

Thanks for your patience here -- still new to Cursorless but your videos (especially the internals walkthrough) have been super helpful to building my mental model.

Not at all! Questions always very much appreciated. Hope these answers help

wolfmanstout commented 2 years ago

Very interesting, thank you for clarifying all that. The idea of a new mark for snippet holes now makes a lot of sense with your example of placeholder text in an HTML form.

I now see that I had quite a different mental model for how multiple snippets in a file might be supported, which I think may be interesting to discuss. I was assuming that you would drop the distinction of an active vs. inactive snippet altogether, and simply pattern match against whatever is in the file to find snippets. For example, if a user "chucks" a condition of an if statement, you could imagine that the snippet placeholder appears for that condition. Users could potentially "bring" structure from one part of a file to another, but have it provide placeholders to change the details. This is also why I was assuming that a user might refer to a snippet relative to their cursor location, or to a mark -- like they do with existing scopes. This model would avoid the whole problem of whether a user left a snippet open accidentally. This would be much more difficult to implement, e.g. because multiple different snippets may overlap in arbitrary ways with a single piece of code. That's why I was thinking that this supercharged functionality might only be applied to a set of snippets maintained by Cursorless. My motivation is that it seems pretty intuitive to me that the same conceptual entities that are currently used for destruction of code can also be used for construction (using your terminology). On the other hand, to support ideas like snippets that span multiple files, I can see why the concept of an "active snippet" is very useful. Perhaps these could coexist, and what I'm describing wouldn't be surfaced as snippets but rather as more construction-focused functionality added to existing predefined scopes.

pokey commented 2 years ago

I think it's worth hashing this one out over a jitsi / discord

What you're describing sounds a lot like what cursorless already supports today. For example, I can already say "change condition" from anywhere in an if statement to clear the condition and move my cursor there, or "copy funk name" from anywhere in a function to copy the function name

I do think it's worth exploring how we formalise the connection between the above (which are modifiers applied to the current selection(s)), and the snippet placeholders described in this issue (which are marks)