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

Basic matchit support #15

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

AndrewRadev
Copy link

Matchit is a built-in plugin that allows the % key to jump between language constructs. This PR adds support for jumping between if, elif and else:

if one:
    print("foo")
elif two:
    print("bar")
else:
    print("baz")

It's pretty basic -- there are lots of other constructs that this could be useful for. But it's at least a start. Here's what the Ruby support uses: https://github.com/vim-ruby/vim-ruby/blob/4788a08433c3c90e131fc7d110d82577e1234a86/ftplugin/ruby.vim#L21-L41

@tpict
Copy link
Owner

tpict commented Feb 23, 2021

Hey, thanks for the PR! Is there any way of getting matchit to only jump between keywords at the same indentation level? At the moment this seems to slip up on nested if statements–I'm not after perfection here but it would be nice to iron out that wrinkle.

@AndrewRadev
Copy link
Author

AndrewRadev commented Feb 24, 2021

I agree, but I can't seem to reproduce the issue -- I tried a couple of if-clauses nested within the if, the else, etc, but they all seem to correctly respect nesting. Here's one example that works for me:

if one:
    if three:
        print("foo 1")
    elif four:
        print("foo 2")
    else:
        print("foo 3")
elif two:
    print("bar")
else:
    print("baz")

With the cursor on the outer if, tapping % keeps jumping between the outer if/else clauses, and it does the same for the inner one. Does this example work for you? If not, maybe there's some configuration difference messing things up. I tried setting the buffer-local variables manually in a vim --clean, and it still seems to work. I ran a vim --clean test.py and executed the following commands:

:packadd matchit
:let b:match_words = '\<if\>:\<elif\>:\<else\>'
:let b:match_skip = 'R:^\s*'

If this particular example does work for you as well, could you give me one that doesn't that I can test with?

@tpict
Copy link
Owner

tpict commented Apr 22, 2021

Hi @AndrewRadev, sorry about the delay in getting back to you. Yes, that example does work on my end. If you remove the else clause on lines 6-7, then the behavior becomes a little unexpected:

  • hitting % on the first if does nothing
  • hitting % on any other conditional keyword cycles between all the remaining ones, even at different indentation levels

Is it possible to have matchit handle these fall-through cases?

@AndrewRadev
Copy link
Author

AndrewRadev commented Apr 23, 2021

Hm, you're right, that's definitely a problem. Unfortunately, I can't figure out a solution. For starters, there's no way to tell matchit to match the same whitespase in all cases. Putting backreferences is supported, but they're mechanically replaced. So, for instance, \(\s*\)if:\1elsif is going to get translated to \(\s*\)if:\(s*\)elsif. Which is not what we want.

I did come up with this: 😀

autocmd CursorMoved <buffer> let b:match_words = s:BuildMatchWords()

function! s:BuildMatchWords()
  if indent('.') > 0
    let start_pattern = '\%(^'.repeat(' ', indent('.')).'\)\@<='
  else
    let start_pattern = '^'
  endif

  return join([
        \ start_pattern.'if\>',
        \ start_pattern.'elif\>',
        \ start_pattern.'else\>',
        \ ], ':')
endfunction

Which essentially updates b:match_words with a new pattern on every cursor move. Probably overengineered, and it still doesn't quite work. Nesting is certainly one problem, but even with the dynamically changing pattern, this is a fundamental problem:

if three:
    print("foo 1")
elif four:
    print("foo 2")

The matchit plugin expects either 2 or 3 components -- beginning + end, or beginning + middle + end. Unfortunately, while ruby has a literal end that can be used as the terminating pattern, python doesn't. The end was the else: in my initial idea, but actually, an elsif could also be an ending to an if-clause match. Using elsif\|else doesn't work either, because then it stops going forward at the first match.


The bottom-line is that I guess this would only be a useful tool if you have if-clauses with elses to terminate. I still plan to keep it in my own config (I don't actually use python very often, admittedly), but I can't say whether that's good enough in practice to be in the core ftplugin. It might be better to do nothing at all than to provide a partial solution -- your call. I'd understand if you preferred to close the PR instead. Naturally, if you have any other ideas to try, I'd be happy to experiment, but I guess at this point I can't see this completely working on a conceptual level.

@Konfekt
Copy link

Konfekt commented Mar 19, 2024

Since there are no end markers in Python, defining a pair in matchit itself always falls short, but the author provided https://github.com/vim-scripts/python_match.vim which lacks the pair def and return/yield, but otherwise works well.

@Konfekt
Copy link

Konfekt commented Dec 1, 2024

@tpict How about including the matchit support by the authoer of matchit, @benjifisher ?

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

Successfully merging this pull request may close these issues.

3 participants