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

Mapping/command to move to beginning/end of blocks, parent blocks? #1

Open
jeetsukumaran opened this issue Feb 15, 2016 · 16 comments
Open

Comments

@jeetsukumaran
Copy link

Is there such? If not, would it be possible to request it?

Nice movements would be:

  • end of current block
  • beginning of current block
  • beginning of parent block
  • beginning of next block at higher level

E.g.,
Given:

1:  class A(object):
.
.       
2:      def func1():
.           [main func1 body]
.
3:          if x:
4:              [stuff]
.
.
5:          else:
.               [stuff]             
.
6:          [more main func1 body]
.
.
7:      def func2()
.
.

(1) If the cursor is between line 4 (inclusive) and 5 (exclusive), then would move to the line just before [5].

(2) If the cursor is between line 4 (inclusive) and 5 (exclusive), then would move to [4]

(3) If the cursor is between line 4 (inclusive) and 5 (exclusive), then would move to [3]

(4) If the cursor is between line 4 (inclusive) and 5 (exclusive), then would move to [7]

@tweekmonster
Copy link
Owner

I wanted to add [] and ][, but it's something I struggled to wrap my head around as far as being consistent enough for the user. The trouble I'm having is with something like this:

def hello(obj):
    if True:
        print('Hello, %s' % obj)

On all 3 lines there, you are inside one of those blocks. It becomes unclear to me about how the plugin should decide. Not to mention the 3rd line is the "end" for the def and if lines. It was the main reason I built in support for EasyMotion, tbh.

There are keys that move outward to the parent g[ and inward to a child g], but I'm not sure if those motions are canonical enough to make permanent, and they don't "feel right" to me since they'll stop once they reach the minimum and maximum indent.

@jeetsukumaran
Copy link
Author

I think the issue becomes clearer if we decide whether the plugin is focussed on blocks defined by indents or code blocks defined by indents. The latter is a semantic overlay on the former. I think the fact that your plugin is focussed on the semantically-richer "code block" concept is exactly what makes it really nice, distinguishing it from others out there. I am the author of indentwise, which focusses on blocks defined by indents, with no attempt at semantic interpretation. Your plugin instantly appealed to me more than my own for Python programming precisely because it goes one step further and sees the world in terms of semantically important code blocks rather than just indented blocks.

So, given that, in the example you list above:

  • command to move to the beginning of the current block should result in NO movement if triggered in any of the 3 lines, IMHO
  • command to move to the end of the current block should go to the last line if triggered in any of the three lines (so no movement in line 3, move to line 3 if on line 1 or 2)
  • command to move to the beginning of the parent block should move to the previous line if on line 2 or 3, and no movement if on line 1
  • command to move to the beginning of the next block at a higher level should move to the end of the fragment (line 3)

Note that "move to end of block" is difficult to consistently and robustly define, at least in a way that makes movement predictable and intuitive in braceless syntaxes. In most cases, though, the "beginning of next block at higher level" usually gets you close enough for all practical purposes.

@tweekmonster
Copy link
Owner

I saw your plugin after I had left for dinner and know you have a real appreciation for the goofiness involved here, so your input is definitely appreciated. I actually had no particular focus on indent blocks vs code blocks (but obviously emphasized on the latter). I broke down and made this after I had typed di<esc> for the millionth time in Python and got frustrated about the fact that there are insanely complex Vim plugins out there, but I couldn't find one that did proper Python blocks.

Back on topic: I think what you mentioned above makes more sense to me now. I'll focus more on code blocks and shrug off the impossible scenarios that the user should already understand. Not sure when I'll get around to looking at it next since my day is pretty filled tomorrow.

Do you have suggestions on what the default motion keys should be? I did want to implement ]m, [m, ]M, and [M in the near future. [[ and ]] is already covered, but I'm not sure what they should be for inner block start and end.

@jeetsukumaran
Copy link
Author

When I first shared "indentwise" on reddit, key-mappings was a pretty involved discussion. Long story short: everyone had a perspective/opinion, most of which made sense, but there was no consensus or even a majority opinion. Basically there were as many schemes as their were programmers. [Though the schemes could be classified into two broad groups, those who preferred a mnemonic-emphasis consistent with other movement keys and those who preferred something easy to type. I fell into the first class, using for, e.g. "]-" to move to the next higher-level indent rather than, e.g. "m,"].

So given all that, I would recommend:

(1) All commands are bound to PLUG mappings.
(2) Either no actual keys are actually mapped or else a set of default/suggested keys are mapped to the PLUG mappings by default and a global option to disable these default mappings are provided (e.g., "let g:braceless_default_key_mappings=0").
(3) For the default/suggested keys: go with whatever works for YOU! That, at least, is one perk of being a plugin author that you should indulge in!

(1) and (2) are considered standard best practices for modern Vim plugin development. With respect to (1), best practices also recommend that you name your plugins with enclosing parenthesis to avoid needing to timeout in the case of names that are a subsets of another. E.g. (from indentwise):

map [- <Plug>(IndentWisePreviousLesserIndent)
map [= <Plug>(IndentWisePreviousEqualIndent)
map [+ <Plug>(IndentWisePreviousGreaterIndent)
map ]- <Plug>(IndentWiseNextLesserIndent)
map ]= <Plug>(IndentWiseNextEqualIndent)
map ]+ <Plug>(IndentWiseNextGreaterIndent)
map [_ <Plug>(IndentWisePreviousAbsoluteIndent)
map ]_ <Plug>(IndentWiseNextAbsoluteIndent)
map [% <Plug>(IndentWiseBlockScopeBoundaryBegin)
map ]% <Plug>(IndentWiseBlockScopeBoundaryEnd)

So you might have, e.g.

<Plug>(BracelessBeginningOfCurrentParentBlock)
<Plug>(BracelessBeginningOfNextHigherLevelBlock)

Bbecause I would like to retain the "m" modifiers for movements that have some sort of "method/function" semantics, my personal preference for the above movements might be something more neutral "[j" and "]k" rather than "[m" and "]m".

But you know what would be deliciously ironic? If "Braceless" actually used braces as the primary prefix keys!! E.g., "{m" and "}m" or "{j" or "}k". Ha!

Either way, though, using the PLUG architecture gives you the flexibility to indulge in whatever works for you while allowing everyone else's idiosyncracies to be supported easily and without hassle as well.

EDIT: remove angle-brackets from PLUG so that they text actually shows up!

@tweekmonster
Copy link
Owner

I'm a little slow to reply to this one because it warrants some thought.

I think I like [j for previous (inclusive) inner start, and ]j for next inner start, ]k for next (inclusive) inner end, and [k for previous inner end. What do you think?

But you know what would be deliciously ironic? If "Braceless" actually used braces as the primary prefix keys!! E.g., "{m" and "}m" or "{j" or "}k". Ha!

I definitely was thinking about this when I started, but didn't want to add a delay to paragraph movement since it's still kind of useful in Python 😆

@jeetsukumaran
Copy link
Author

I think I like [j for previous (inclusive) inner start, and ]j for next inner start, ]k for next (inclusive) inner end, and [k for previous inner end. What do you think?

Due to the j and k being more fundamental movement keys, I think that the direction semantics should be dominated by j and k rather than [ and ]: j should be "previous" and k should be "next", always. So: [j for previous (inclusive) inner start and ]j for previous inner start, and [k for next (inclusive) inner end and ]k for next inner end.

But there is a case to be made for [ and ] having well-established credentials as previous/next movement prefix keys, so it's a toughie! Maybe not so fundamental as j and k, but still ...

If you do go with the [ and ] as indicators of movement direction route, I would suggest not using j and k as the second element in the mapping to avoid confusion. Perhaps have a look at the fold movement keys ("MOVING OVER FOLDS" in help) for inspiration:

                            *[z*
[z      Move to the start of the current open fold.  If already at the
        start, move to the start of the fold that contains it.  If
        there is no containing fold, the command fails.
        When a count is used, repeats the command [count] times.

                            *]z*
]z      Move to the end of the current open fold.  If already at the
        end, move to the end of the fold that contains it.  If there
        is no containing fold, the command fails.
        When a count is used, repeats the command [count] times.

                            *zj*
zj      Move downwards to the start of the next fold.  A closed fold
        is counted as one fold.
        When a count is used, repeats the command [count] times.
        This command can be used after an |operator|.

                            *zk*
zk      Move upwards to the end of the previous fold.  A closed fold
        is counted as one fold.
        When a count is used, repeats the command [count] times.
        This command can be used after an |operator|.

Maybe find a suitable replacement for "z" for Braceless? Maybe ">": "[>", "]>", ">j", ">k"?

@tweekmonster
Copy link
Owner

I forgot to reply to this earlier, sorry.

I think that the direction semantics should be dominated by j and k rather than [ and ]

I was actually thinking this, but I guess I typed it out the wrong way 😓

Maybe find a suitable replacement for "z" for Braceless? Maybe ">": "[>", "]>", ">j", ">k"?

I thought about > and < but it started feeling like I was mapping keys to help you develop carpal tunnel. The one key I was thinking about using as the motion prefix was t since that was only really associated with html tags and got the left hand involved. Maybe that'd be a good enough as a default and let people remap it if they hate it.

@jeetsukumaran
Copy link
Author

There's also SPACE or TABas prefixes, both of which are redundant or not very useful by default in normal mode, and would be appropriate given that the structure is defined by one or both of these characters ;)

But the "key" (ha ha) point you make is this:

let people remap it if they hate it

Yep! Default to what you feel comfortable with + option to suppress or not enable default mappings + everything bound to PLUG's so folks can map to whatever they want if they want = WIN.

tweekmonster added a commit that referenced this issue Feb 22, 2016
@tweekmonster
Copy link
Owner

@jeetsukumaran Sorry for not updating sooner. This took a while to get right. And by "right", I mean "as close to making sense as possible". I went with <Tab> after going through a few other keys to get a feel for them. They all sucked, but <Tab> sucked the least.

What I did was made it so that movement happened by block segments. Here's an image that shows what are considered block segments:

block-segments

It somewhat matches the fold movement keys. <Tab>j positions the cursor at the end of the previous segment, and <Tab>k positions the cursor at the start of the next segment. [<Tab> and ]<Tab> moves the cursor to the start and end of the current segment, but they don't work like [z and ]z when already at the start/end.

I'm going to sit on this for a day or two before merging it into master so I can try using these keys in real work. You should try out the block-motions branch and see what you think.

@jeetsukumaran
Copy link
Author

(1) First, I think that every motion should add the current position to the jumplist so that CTRL-i and CTRL-o work as expected.

(2) I think that TAB k should move to the beginning of the previous segment, rather than the end.

For e.g., consider the following code fragment::

def f1(x, y):                 # : 1  :
    code                      # : 2  :
    code                      # : 3  :
    if y:                     # : 4  :
        code                  # : 5  :
        for y in range(10):   # : 6  :
            code              # : 7  :
            code              # : 8  :
    elif x:                   # : 9  :
        code                  # : 10 :
        code                  # : 11 :
        code                  # : 12 :
        for y in range(10):   # : 13 :
            code              # : 14 :
            code              # : 15 :
    else:                     # : 16 :
        code                  # : 17 :
        code                  # : 18 :
    code                      # : 19 :
    code                      # : 20 :
    code                      # : 21 :
                              # : 22 :
                              # : 23 :
def f2(x, y):                 # : 24 :
    code                      # : 25 :
    code                      # : 26 :
    code                      # : 27 :

Then, if the cursor is on line 10, I can see wanting go up to line 5 or 4, but not so much the end of the previous block. BUT, I confess that this might be just me and if you find it useful by all means you should keep it.

(3) Similarly, I am thrown off by the cursor going to the end of the line: that typically is not a place I find most convenient to begin working on a like (my eyes have to scan "backward" to the place I want to then go, and it seems more natural to go to the beginning of the line and scan forward). But again, this might be just me.

(4) For reference, here are the indentwise motions based on the above block, assuming that the cursor is on line 10:

  • ]+ (move to next line of greater indent) will go to line 14
  • ]- (move to next line of less indent) will go to line 16
  • ]= (move to next line of equal indent, separated by lines of different indent) wil go to line 17
  • [+ (move to previous line of greater indent) will go to line 8
  • [- (move to previous line of less indent) will go to line 9
  • [= (move to previous line of equal indent, separated by lines of different indent) wil go to line 6

But what is missing these movements produced by indentwise is the awareness of code blocks rather than indent-based segments.
So if I am sitting on line 10, and I want to:

  1. go to the beginning of the next if-else subclause (line 16)
  2. go to the beginning of the block that follows the current block, i.e., the beginning of the next block at the parent level (line 19)
  3. go to the end of the current if-else subclause (line 15)
  4. go to the end of of the current if-else block (line 29)
  5. if sitting in a function body, go to the end of the current function; if sitting in a class, go to the end of the current class

Some of the above can be achieved through multiple smart invocations of the dumb indentwise commands, e.g. ]- followed by ]= will achieve (2) above. But it takes some thinking and that ruins the flow, and Vim is all about flow!

@tweekmonster
Copy link
Owner

Is there a vim function for setting updating the jumps or is it a matter of `normal! m`` before the movement?

Yeah, after some real use, the cursor positions after the move are a little too weird for me. I was modeling after the fold movements so I could have some concept to latch on to while working on this because it was getting a little confusing. And, now that you've mentioned jumps, I just noticed that ctrl-I now has a delay because of the <Tab> prefix :goberserk: How about something like discussed earlier (going by your example of line 10):

  • [j jump to beginning of previous segment - line 7
  • ]j jump to the end of previous segment - line 8
  • [k jump to the beginning of next segment - line 14
  • ]k jump to the end of next block - line 15
  • [J jump to the beginning of previous segment that contains this position - line 2
  • ]J jump to the end of the previous segment that contains this position - line 3
  • [K jump to the beginning of the next segment that contains this position - line 19
  • ]K jump to the end of the next segment that contains this position - line 21

I checked and rechecked and there doesn't seem to be anything already using these keys. Can you confirm that? I've only been using Vim seriously for a little over a year now, and very poorly for about a decade. So, I'm far from an expert and might be missing something.

The reason that these don't land on the line that starts the block is because of the way the lines are being searched. In a case like this:

if True:  # Go here
    if True:
        if True:
            pass  # Start here
        pass
    pass  # Go here

It was not awesome jumping one line at a time with two keys when 3k or 2j was more effective. What I wrote prefers to skip block heads that don't have any space between them. I'll probably leave this as is until I or someone else contributes a better concept for block jumping.

I'd rather keep these movements agnostic and not Python specific since I want to extend this plugin into CoffeeScript one day. So, no go on having these being contextual to a block head (re: your comment about def and class). I am going to be adding Python specifics though, but most likely reimplementations of what's in python-mode.

In any case, I'm going to be merging what I have now into master to fix a regression in indentation (updated movement code is needed to fix it), but it's going to be disabled by default.

@jeetsukumaran
Copy link
Author

The key to having the current position recorded in the jumplist is to use movements that record the current position in the jumplist. Tautalogical, I know, but what I mean is this ...

If you decide you want to move 3 lines up to line 35, you could either do 3j or 35gg or35G. The first move will NOT result in the current position being recorded in the jumplist, while the latter two will. Unfortunately, the programmatic move, cursor(...) also does NOT result in the the current position being recorded in the jump list. So what you have to do is after deciding that you want to go to a particular line and you want this move to be part of the jumplist so that CTRL-O will return you here, instead of using cursor(..) or j/k, you have to do something like:

execute "normal! " . target_line . "G"

or

if preserve_col_pos
    execute "normal! " . target_line . "G" . current_column . "|"
else 
    execute "normal! " . target_line . "G^"
endif

Conversely, you use :keepjumps if a command/movement you are executing should NOT modify the jumplist when it normally does.

@tweekmonster
Copy link
Owner

Thanks for the pointers about the jumps. I've been using it everywhere it makes sense. Observations of my own: You can move the cursor all you want, but to record the jump correctly, you have to restore the original position before execute "normal ! ...".

The segment movements have been more difficult than I expected. I implemented [j, [k, ]j, and ]k as described above, but not the rest yet. I'm taking a break from it for now and will revisit in the future. I intentionally made the new script very verbose to make it somewhat easier to understand. So, feel free to take a stab at it if you have the urge.

@tweekmonster tweekmonster reopened this Mar 5, 2016
@tweekmonster
Copy link
Owner

Oops, I forgot I was optimistic and had the commit message close this.

@jeetsukumaran
Copy link
Author

Thanks! Will give it a spin.

tweekmonster added a commit that referenced this issue Mar 5, 2016
@tweekmonster
Copy link
Owner

So, I lied about taking a break from this. I added [J and [K for moving to segments with lower indent levels and ]J and ]K for moving to segments with higher indent levels. Not going to merge into master until I get a feel for it. So far, the only bad thing is when moving to a segment of a higher indent level that's completely outside of the root block.

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