drym-org / symex.el

An intuitive way to edit Lisp symbolic expressions ("symexes") structurally in Emacs
Other
271 stars 22 forks source link

Operating on regions (Visual mode) #53

Open chip2n opened 2 years ago

chip2n commented 2 years ago

One feature I'm missing since switching from lispy is the way you can operate on regions, in particular the way you can mark the current S-expression and then grow/shrink the region.

https://github.com/abo-abo/lispy#operating-on-regions

Is this in scope for the project?

countvajhula commented 2 years ago

Definitely, and other users have brought this up before as well. This falls under properly supporting Evil's "Visual mode" which is a Vim-like modal way to use regions.

For now, you could use commands like c/x/y along with quantifiers (e.g. 3c) to account for some of these cases, and C/D/Y in other cases. But I agree that proper support for visual mode / regions would be valuable and can't always be expressed this way (not to mention visual feedback is nicer than relying on counting correctly!).

I seem to recall I did initially try to add support for it a long time ago, but at the time I wasn't familiar with evil and wasn't able to get keybindings to be recognized in Visual state. Were I to attempt it now, I would try something like this:

  1. Define a new minor mode keymap for use in visual mode (similar to symex-editing-mode), where the motions (e.g. hjkl) move to the end of each expression instead of the start (as it usually does in Symex mode).
  2. Modify the evil keybinding helper utilities to use the usual symex-editing-mode map for Normal and Operator state, but use the new visual-specific keymap for Visual state (probably would need to add a separate visual-specific helper there).
  3. For motions that could move to a deeper level of nesting (e.g. f, b or k (i.e. the default k, unless you've remapped the up/down orientation)), we should probably make them no-ops in the Visual state keymap, i.e. they should have no effect.
  4. For motions that move to a shallower level of nesting (i.e. causing the start of the region to be inside the start of the current expression), the region should automatically expand to start at the beginning of the current expression to keep it structurally consistent.
  5. Once the above are working purely for selection, the visual state keymap could be expanded to include any desirable operations like cdy. Since they are just operating on regions at this point, we can use standard utilities for this. That is, instead of doing symex-delete or symex-change they would just do evil-yank, evil-change and evil-delete (or even the equivalent Emacs functions like delete-region, if those would be more appropriate or convenient). Other useful commands like HL to shift the region around could also be added, but these would probably require writing custom functions like symex-shift-region-forward (or we could potentially use evil-delete (or delete-region), followed by an appropriate motion, and then evil-paste), so they could be added as a followup and needn't be part of the initial effort.

Now in visual state we can already use o to switch to the "other" side of the region (you could try this in Evil's Normal state). As you pointed out in Lispy's docs, this is a useful feature to be able to expand/shrink the region in either direction. In order to support this, we would now need to modify the motion functions we wrote in step (1) to condition on the side of the region on which the cursor currently is. That is, if the cursor is at the end, then do the behavior we have already defined, otherwise if it is at the start, then we move to the start of each expression (similar to Symex's current way) instead of the end.

Assuming all that works out, this would be quite a nice addition.

chip2n commented 2 years ago

Thank you for the detailed response! I might take a stab at this and open a pull request if I have something that seems interesting. Here's some of my initial thoughts:

Regarding point 3, I think I'd expect the region to follow the point even when moving to deeper levels of nesting. So if I press j followed by k, I end up with the original region (like how symex usually lets you "revert" to the previous position by using the opposite keys). Or, to put it another way, the marked region is always around the current expression until the user grows it to extend it beyond that.

Growing/shrinking the region could perhaps just use the default slurp/barf bindings in the visual mode? In that case, we don't need to consider which side of the expression the cursor is on.

countvajhula commented 2 years ago

Ah, I think I understand now. We are describing two different paradigms here 🙂

The first is essentially a "point-free" paradigm, where we don't have a notion of the point/cursor position and it is entirely in relation to the current region as a whole.

The second is a motion-oriented one where the point guides the boundaries of the region.

To illustrate the difference, if we wanted to go from this selection:

(a b c [d] e f g)

to this one:

(a [b c d e f] g)

it would take 4 steps in the pointfree way, and 5 steps in the motion-oriented way (including the o to move point to the other end). I do agree that the former is even more convenient in this case than the numbers would seem to indicate.

On the other hand, to go from:

(a b c (d e f (g h i (j k [l]))))

to this:

(a b c [(d e f (g h i (j k l)))])

With the barf/slurp (or "emit/capture" as they are called in symex) bindings, I assume this would do more than just go back and forward at the same level? That is, it would probably expand to larger and larger expressions when it hits a boundary at the present level? Then it would take 11 steps to reach the (d ...) expression.

With hjkl motion-oriented regions, it would take 3 steps -- just "out, out, out".

My feeling is that the motion-oriented way is more general than the pointfree way, and it should be possible to implement the latter on top of it as a thin wrapper. That way, we could support both simple motions like hjkl as well as emit/capture to "do what I mean" in many common cases, and could even use both paradigms at the same time for maximum flexibility since there is no conflict between them.

Re: point 3, yeah I think you're right, and we should support entering expressions as long as we are able to avoid selecting things that are structurally invalid like (a [b (c] d) e). To go over some example cases to help in deciding what should happen:

(a [(b c)] d)

could yield

(a ([b] c) d)

regardless of point location, while

(a [|(b c) (d e)])

(note point on the left end) could yield:

(a ([b] c) (d e))

and

(a [(b c) (d e)|])

(point on the right end) could yield:

(a (b c) ([d] e))

and that may avoid any weird cases of non-structural selection.

chip2n commented 2 years ago

Yep, it sounds like we're on the same page! I'll try to set aside some time this week to look closer at it :+1:

devcarbon-com commented 1 year ago

Does this need to be tied to evil's visual mode? Could we just use region, simular to meow?

countvajhula commented 1 year ago

@devcarbon-com You may be right that it would be better to use regions directly. The design discussed above does seem to introduce the idea of a "motion" in the Evil sense, but I'm not sure if it sufficiently fits into Evil's UI paradigm where it would be much easier to implement in evil vs doing it directly using regions. And of course, doing it with regions directly avoids increasing the dependency on evil. If we used regions directly though, we'd still need to ensure that it is "modal" in some way, so that e.g. pressing y after selecting some code copies it, and c changes it, instead of inserting y and c as it naively would with regions.

markgdawson commented 11 months ago

@chip2n, have you done any work on this? I'm considering giving this a go if I find time in the near future.

chip2n commented 11 months ago

@chip2n, have you done any work on this? I'm considering giving this a go if I find time in the near future.

I never got the time to do this unfortunately, so please go ahead! I think it would be a great addition to this package.