noctuid / things.el

Extensions to thingatpt.el
51 stars 0 forks source link

Thing constraints #5

Open noctuid opened 5 years ago

noctuid commented 5 years ago

This would be a generalization of the ideas from #3 and #4. Both would be simple to implement on top of this. There should be a definer for creating "constrained things" that are only valid inside other things (e.g. comma thing inside paren/delimiter thing = argument).

This would require mainly two new macros/functions.

  1. things-with-narrowing - Run body in a narrowed region for the bounding thing. Do nothing when no bounding thing at the point. The tricky part of this will be handling nestable things since we won't want to enter into inner things (e.g. (a, b~, (c, d)|, e) is a valid argument but (a, b~, (c|, d), e) would be wrong). Non-contiguous narrowing is not a builtin Emacs feature, but one way to do this would be to extract to a new buffer, delete all the inner things, and then restore the point correctly in the original buffer. Another way to handle this would be to either check at a higher level or make it an anaphoric macro that provides to check if the current point is at the original nesting level.
  2. things-forward-constrained - While count is positive, continue to go forward. When there is a failure and another bounding thing in the buffer, skip to it with things-next-bounds, and narrow to it. Keep trying while there are more bounding things and a positive count.

It would probably useful to have inner and outer constraints. Outer constraints meaning a thing must be inside of another thing (optionally at the same nesting level). Inner constraints meaning that if a thing is not inside another thing (e.g. comment or string), any regions for those things should be ignored. This would allow for a far more generic implementation of the smart behavior of the pair text objects mentioned in #1.

Another thing to note is that this needs to work for composite things (which can be nestable even if their individual things are non-nestable), so just trying to check if a thing has a things-nestable property (not currently part of the specific) to see if the extra narrowing requirements can be cut out would not work for a list of things.

noctuid commented 5 years ago

Instead of being a definer, I've implemented constraints as a modifier to a thing (like :adjustment) which makes a lot more sense. The current keyword names will probably change in the future since they aren't as intuitive/clear as I would like (e.g. I'll probably make them all either verbs or nouns consistently).

There are currently three types: outer required constraints (:constraint), outer optional constraints (:optional-constraint), and inner constraints (:ignore).

Required outer constraints mean that a thing is only valid if it is inside another thing. This will be useful for implementing argument things #4 (require a comma separator thing to be inside a paren thing; more changes to the separator thing are necessary for this to work correctly).

Optional outer/inner constraints can be used to prevent things from crossing certain boundaries (e.g. a paren must be entirely contained in the same comment or string or not in one at all). This allows for smarter behavior for any type of thing without having to build it in (like I originally did for the regexp pair thing definer). Specifying :optional-constraint 'things-string, for example, would try to get the bounds of a thing after narrowing to a string if there is one at point; on failure, it will try again without the narrowing. Generally, :optional-constraint and :ignore are meant to be combined. :ignore will remove all things in a temporary buffer unless narrowed to that thing. As an example, getting the bounds of '(things-paren :optional-constraint 'things-string :ignore 'things-string) would first try to narrow to a string at the point and get the bounds of a paren thing inside that string. On failure (either because there are no parens or only a single paren in the string or because there is no string at point), it would strip all strings from the temp buffer and then try to get the bounds. This will also work with nestable things (only "inner" things will be stripped if there is a narrowing). Here is the smart paren (#1) example:

(things-define-pair 'things-paren "(" ")")
(things-evil-define smart-paren (things-paren
                                 :optional-constraint (things-aggregated-comment
                                                       things-string)
                                 :ignore (things-aggregated-comment
                                          things-string))
                    :keys "v")

The implementation is slow, but the method seems to work well in basic tried cases (e.g. "(" ")" is not considered a paren thing because the parens are in separate strings).

For simplicity, constraints are handled entirely in things-base-bounds, so anything built on top of it will handle constraints as well (including things-forward, which checks things-base-bounds). This means seeking and avy selection also works as expected. Right now things-seek-op has to implemented to manually confirm a motion moved to a valid position using things-base-bounds; this could be improved in the future.

The main concern is the performance of constraints. Things would be a lot easier if there was a narrow-to-regions. Unfortunately, there is not, and :ignore requires manually implementing discontiguous narrowing and point mapping. The current implementation is both convoluted and extremely inneficient (e.g. remote selection takes about a minute in a small buffer). The idea is hopefully to add tests and then focus on performance while preserving correctness later.

The main issues are as follows:

A potential way to limit the size of the copied buffer is to run the bounds function, strip all things in that region, and then continue to do that while the bounds changes/there are still things to strip inside the new bounds.

It might be worth looking into implementing discontiguous narrowing in core as this would make things a million times easier (no need to make a copy buffer or do point mapping), but that might be pretty difficult (and would make the package dependent on a new version of Emacs)...

noctuid commented 5 years ago

Another potential use case would be to constrain a quote thing to a line. This would be equivalent to the default vim behavior.