bps/vim-textobj-python

``af`` should include blank line after function (if any), ``ac`` should include blank line after class (if any)

jeetsukumaran opened this issue · 6 comments

Consider the following fragment:

def f1():     # :1
    code      # :2
    code      # :3
    code      # :4
              # :5
              # :6
def f2():     # :7
    code      # :8
    code      # :9
    code      # :10

If on line 3, the af text object spans lines 1 through 4, inclusive.
I think the text object should span lines 1 through 5, inclusive, unless line 5 is non-blank (i.e., there is not space between the functions).

This is for consistency with the way Vim's other text objects behave. Most of Vim's other text objects also include the subsequent line/space when in a mode.
E.g., in the above example, vap will select lines 1 though 5.
The same principle holds with words, e.g. in the sentence "the quick brown fox", if the cursor is on "quick", while viw will select "quick", but vaw will select "quick_", i.e. "quick" and the subsequent space.

Of course, if there is NO blank line following the function/class, then the object should be restricted as currently (otherwise it will include elements of the next function/class etc)

bps commented

Not sure I agree that this is consistent with other text objects. I think the ap and aw objects are geared towards prose, and their behavior makes sense. But other built-in objects that aren't geared towards prose do not select surrounding whitespace. Consider:

int foo() {
    bar();
    bar();
    bar();
    bar();
    bar();
}



int baz() {
    quux();
    quux();
    quux();
    quux();
    quux();
}

va} within the function blocks selects only the blocks (including the braces) and no surrounding whitespace. Likewise:

<h1>
    text
    text
    text
    text
    text
</h1>



<h1>
    text
    text
    text
    text
    text
    text
</h1>

vat selects the entire tag but no surrounding whitespace.

That said, selecting the whitespace (trailing if it exists, leading if not) might be the nicer behavior. I don't know that the a} and at behavior applies here since both blocks and tags don't necessarily need to be linewise. I can be convinced, but I'm not sure the consistency argument fits.

Good point, regarding consistency ... it seems Vim itself is inconsistent! Though I guess a case can be made that in brace/tag/other token delimited text objects, the tokens themselves serve as the delimiters, so the selection of the token in the a mode is equivalent to the selection of whitespace in cases where the whitespace serves as delimiters (words, etc.).

Here is the ergonomic argument. I most often use af when I want to, delete, yank/paste, or simply clone a function or class. In all these cases, I actually want the subsequent blank line. In its current behavior, say I want to move a function I have to:

(1) daf to delete the function
(2) move to the location where I want to put the function
(3) p to put/paste the function
(4) o<ESC> to add a blank line after the function to separate it from the next function
(5) go back to the original function location
(6) delete the double blank line

The above can be made a little easier by judicious use of the "black hole" register to delete the double blank lines, or, what I typically do, visually select the function (vaf), extend the visual selection by a line (j) and then move/paste as usual. BUT it seems so much more "natural" to me to include the blank line following the function, and this reduces the above workflow to:

(1) daf to delete a function
(2) move to new location
(3) p to put/paste

bps commented

That all makes sense to me.

I can think of a couple cases offhand where this behavior gets tricky:

class Foo(object):
    def __init__(self):
        self._bar = "bar"

    def bar(self):
        print self._bar


class Bar(object):
    def __init__(self):
        self._bar = "bar"


class Quux(object):
    def __init__(self):
        self._bar = "bar"

    def bar(self):
        print self._bar
  • If I do vaf in Foo.bar, I don't want the trailing whitespace to be selected. But I probably want the leading whitespace selected?
  • If I do vac in the same place, I do want the trailing whitespace to be selected.
  • If I do vaf in Bar.__init__, I don't want any whitespace selected.

Hmmm. I guess, for me, in all the three cases you note, it is more natural/intuitive to have a single trailing whitespace line selected, i.e. both in the case of Foo.bar as well as Bar.__init__. But I think that is because I do not maintain the convention of 2 spaces following the last function in a class, or no space preceding the first function: I use a single blank line to separate all these entities. So the fact that Foo.bar is the last function of the class makes no difference: if I want to move/delete it, I want to move/delete the (single) blank line after it as well.

Perhaps an accommodation might be to make this trailing blank line selection an user-configurable option? e.g. "let g:textobj_python_including_trailing_blank_line = 0/1"?

bps commented

You still get tripped up doing daf in Bar.__init__ with one delimiting blank line:

class Bar(object):
    def __init__(self):
        self._bar = "bar"

class Quux(object):
    def __init__(self):
        self._bar = "bar"

    def bar(self):
        print self._bar

would become:

class Bar(object):
class Quux(object):
    def __init__(self):
        self._bar = "bar"

    def bar(self):
        print self._bar

I would rather maintain the whitespace between the classes. But maybe that's not a problem since you're going to have to do something in there regardless to make things parse.

I'd rather not introduce a configuration option — I think in general the blank line selection sounds like the right thing to do. I just want to understand the edge cases.

Ahhh, yes. You are correct. Yep, the uniform application of the "include-trailing-blank-line" rule would indeed result in this. But perhaps (a) the fact that is a relatively rare(-r) use case and, more importantly, (b) the fact that the rule is rigorously consistent regardless of context and thus this behavior is highly predictable may offset the annoyance level somewhat?

And, as you say, something must go in there, so you are either going to go into insert mode anyway so that the extra line is just one more keystroke or you are going to put/paste something and hopefully/probably that will a trailing blank line?