A Vim plugin that provides text objects and motions for Python classes, methods, functions, and doc strings.
Clone or download

README.md

Pythonsense

Description

Pythonsense is a Vim plugin that provides text objects and motions for Python classes, methods, functions, and doc strings.

Python Text Objects

  • "ac" : Outer class text object. This includes the entire class, including the header (class name declaration) and decorators, the class body, as well as a blank line if this is given after the class definition.
  • "ic" : Inner class text object. This is the class body only, thus excluding the class name declaration line and any blank lines after the class definition.
  • "af" : Outer function text object. This includes the entire function, including the header (function name declaration) and decorators, the function body, as well as a blank line if this is given after the function definition.
  • "if" : Inner function text object. This is the function body only, thus excluding the function name declaration line and any blank lines after the function definition.
  • "ad" : Outer docstring text object.
  • "id" : Inner docstring text object.

The differences between inner and outer class and method/function text objects are illustrated by the following:

class OneRing(object):             -----------------------------+
                                   --------------------+        |
    def __init__(self):                                |        |
        print("One ring to ...")                       |        |
                                                       |        |
    def rule_them_all(self):                           |        |
        self.find_them()                               |        |
                                                       |        |
    def find_them(self):           ------------+       |        |
        a = [3, 7, 9, 1]           ----+       |       |        |
        self.bring_them(a)             |- `if` |- `af` |- `ic`  | - `ac`
        self.bind_them("darkness") ----+       |       |        |
                                   ------------+       |        |
    def bring_them_all(self, a):                       |        |
        self.bind_them(a, "#000")                      |        |
                                                       |        |
    def bind_them(self, a, c):                         |        |
        print("shadows lie.")      --------------------+        |
                                   -----------------------------+

Python Object Motions

  • "]]" : Move (forward) to the beginning of the next Python class.

  • "][" : Move (forward) to the end of the current Python class.

  • "[[" : Move (backward) to beginning of the current Python class (or beginning of the previous Python class if not currently in a class or already at the beginning of a class).

  • "[]" : Move (backward) to end of the previous Python class.

  • "]m" : Move (forward) to the beginning of the next Python method or function.

  • "]M" : Move (forward) to the end of the current Python method or function.

  • "[m" : Move (backward) to the beginning of the current Python method or function (or to the beginning of the previous method or function if not currently in a method/function or already at the beginning of a method/function).

  • "[M" : Move (backward) to the end of the previous Python method or function.

Note that in Vim 8.0 or later, motions with these key-bindings are provided by default. While in the most simplest of cases they converge with "Pythonsense" in operation, they result in very different behavior in more complex cases. This difference (discussed in detail below) basically comes down to "Pythonsense" providing semantically aware and contextual class-wise and method- or functionwise motions, while the stock Vim 8+ provides non-semantically aware indent-level based motions. If you prefer the stock Vim 8.0+ motions to the ones overridden by "Pythonsense", then, as described in the section on suppressing the default key mappings, just specify the following in your "~/.vimrc":

let g:is_pythonsense_suppress_motion_keymaps = 1

If you want both the stock Vim 8.0+ motions, then you can activate alternate mappings to the Pythonsense semantic motions that do not hide the the stock Vim 8.0+ motions by specifying the following in your "~/.vimrc":

let g:is_pythonsense_alternate_motion_keymaps = 1

See below for more details on these key mappings.

Python Location Information

  • "g:" : print (echo) current semantic location (e.g. ""(class:)Foo > (def:)bar"")

Installation

Manually (Example)

If you are using Vim 8.0 and above, I recommend you take advantage of the 'package' feature and do something like this:

$ cd ~/.vim
$ mkdir -p pack/import/start
$ cd pack/import/start && git clone git://github.com/jeetsukumaran/vim-pythonsense.git

You do not have to name the mid-level directory "import"; it can be anything else that makes sense to you for this plugin (e.g., "bundle", "jeetsukumaran", "python", etc.). For more information, see the documentation on "packages".

If you are using an older Vim version and do not want to upgrade, then you would be much, much, much better served by using a plugin manager like Vim-Plug (described below). However, if you are stuck with an old version of Vim and really do not or cannot use a plugin manager, then will need to manually copy the files to the corresponding locations in your home directory:

  • "vim-pythonsense/after/ftplugin/python/pythonsense.vim" to: "~/.vim/after/ftplugin/python/pythonsense.vim"
  • "vim-pythonsense/autoload/pythonsense.vim" to: "~/.vim/autoload/pythonsense.vim"
  • "vim-pythonsense/ftplugin/python/pythonsense.vim" to: "~/.vim/ftplugin/python/pythonsense.vim"

pathogen.vim

$ cd ~/.vim/bundle
$ git clone git://github.com/jeetsukumaran/vim-pythonsense.git

Vim-Plug

Add the line below into your "~/.vimrc":

Plug 'jeetsukumaran/vim-pythonsense'

and run:

:PlugInstall

More information on setting up Vim-Plug to manage your plugins can be found here.

Vundle

Add the line below into your "~/.vimrc":

Vundle 'jeetsukumaran/vim-pythonsense'

or run:

:VundleInstall jeetsukumaran/vim-pythonsense

Customization

Changing the Key Mappings

If you are unhappy with the default key-mappings you can define your own which will individually override the default ones. However, instead of doing so in your "/.vimrc", you need to do so in a file located in your "/.vim/ftplugin/python/" directory, so that this key mappings are only enabled when editing a Python file. Furthermore, you should make sure that you limit the key mapping to the "<buffer>" scope. For example, to override yet replicate the default mappings you would define, the following in "~/.vim/ftplugin/python/pythonsense-custom.vim":

map <buffer> ac <Plug>(PythonsenseOuterClassTextObject)
map <buffer> ic <Plug>(PythonsenseInnerClassTextObject)
map <buffer> af <Plug>(PythonsenseOuterFunctionTextObject)
map <buffer> if <Plug>(PythonsenseInnerFunctionTextObject)
map <buffer> ad <Plug>(PythonsenseOuterDocStringTextObject)
map <buffer> id <Plug>(PythonsenseInnerDocStringTextObject)

map <buffer> ]] <Plug>(PythonsenseStartOfNextPythonClass)
map <buffer> ][ <Plug>(PythonsenseEndOfPythonClass)
map <buffer> [[ <Plug>(PythonsenseStartOfPythonClass)
map <buffer> [] <Plug>(PythonsenseEndOfPreviousPythonClass)
map <buffer> ]m <Plug>(PythonsenseStartOfNextPythonFunction)
map <buffer> ]M <Plug>(PythonsenseEndOfPythonFunction)
map <buffer> [m <Plug>(PythonsenseStartOfPythonFunction)
map <buffer> [M <Plug>(PythonsenseEndOfPreviousPythonFunction)

map <buffer> g: <Plug>(PythonsensePyWhere)

Note that you do not need to specify all the key mappings if you just want to customize a few. Simply provide your own key mapping to each of the "<Plug>" mappings you want to override, and these will be respected, while any "<Plug>" maps that are not so explicitly bound will be assigned to the default key maps.

Suppressing the Key Mappings

If you want to suppress some of the key mappings entirely (i.e., without providing your own to override the functionality), you can specify one or more of the following in your "~/.vimrc":

let g:is_pythonsense_suppress_object_keymaps = 1
let g:is_pythonsense_suppress_motion_keymaps = 1
let g:is_pythonsense_suppress_location_keymaps = 1

to selectively suppress just the text object, motion, or information key mapping sets by respectively.

You can also suppress all default key mappings by specifying the following in your "~/.vimrc":

let g:is_pythonsense_suppress_keymaps = 1

Activating Alternative Motion Key Mappings

As discussed above, "Pythonsense" overrides some native Vim 8.0+ motion key mappings, replacing the indent-based non-semantic ones with semantically-aware ones. If you wish to have access to both types of motions, i.e., the stock Vim 8.0+ non-semantic indent-based block motions as well as the "Pythonsense" semantically aware class/method/function motions, then you can specify

let g:is_pythonsense_alternate_motion_keymaps = 1

in your "~/.vimrc".

Then (unless you specify you want all or motion key mappings suppressed entirely), the "Pythonsense" semantic class/method/function motions will be bound to the alternate keys below:

  • "]k" : Move (forward) to the beginning of the next Python class.

  • "]K" : Move (forward) to the end of the current Python class.

  • "[k" : Move (backward) to beginning of the current Python class (or beginning of the previous Python class if not currently in a class or already at the beginning of a class).

  • "[K" : Move (backward) to end of the previous Python class.

  • "]f" : Move (forward) to the beginning of the next Python method or function.

  • "]F" : Move (forward) to the end of the current Python method or function.

  • "[f" : Move (backward) to the beginning of the current Python method or function (or to the beginning of the previous method or function if not currently in a method/function or already at the beginning of a method/function).

  • "[F" : Move (backward) to the end of the previous Python method or function.

Similar Projects

Most notable is the vim-textobj-python plugin. Pythonsense distinguishes itself from this plugin in the following ways:

  • Stand-alone and lightweight: does not rely on vim-textobj-user.
  • Also includes a docstring text object (originally from here).
  • Crucially (for me!) selecting an outer class, method, or function also selects the blank line following. Without this, I find I always have to (a) add a new blank line after I have, e.g. pasted the class/method/function in its new location, and (b) delete the extra blank like in the old class/method/function location. With Pythonsense, this is taken care of automatically and the cleaner approach (for me!) not only saves time and key-strokes, but the oh-so-valuable headspace taskload. For a discussion of this issue, see here, and, in particular, here.
  • Allows for successive invocations of text object selectors to select enclosing classes/functions.
  • Handles nested classes/functions better (though with some failing corner cases).
  • Seems to handle functions/classes with multiline arguments better.
  • Handles folds better (opens them automatically if needed).
  • Of course, also provides a docstring text object as well as the semantic location information helper ("g:").

More Information on Text Objects

If you are reading all this and wondering what is a text object or why are text objects such a big deal, then the short answer is that text objects are like using a socket and ratchet as opposed to a monkey wrench: as long as you can find a fit, you cannot beat the efficiency, speed, and precision (as well as elegance and pure pleasure). For more details, you could look at some of the following:

Acknowledgements

  • The seed/inspiration (and basic logic) for the code for Python class and function text objects came from Nat Williams' "pythontextobj.vim". From a practical usage standpoint, principal differences are:
    • The semantics of outer ("a") vs inner ("i") function/class. In the original, both "a" and "i" include the class/function definition line, but "i" extends to include the space after the class/function definition. I felt that "i" should exclude the class/function declaration heading line and the space after the definition (i.e., just the body of the class/function), while "a" should include both the class/function declaration heading line and a space after the definition.
    • Handles nested cases better (or at all): multiple invocations of "ic", "ac", "if", "af", etc. grab successively enclosing classes/functions. The seed/inspiration for the logic for this, in turn, came from Michael Smith's "vim-indent-object".
  • Code for the docstring text objects taken from the pastebin shared by gfixler.

Appendices

Stock Vim vs Pythonsense Motions

The stock Vim 8.0 "class" motions ("]]", "[[", etc.), find blocks that begin at the first column, regardless of whether or not these are class or function blocks, while its method/function motions ("[m", "]m", etc.) find all blocks at any indent regardless of whether or not these are class or function blocks. In contrast, "Pythonsense" class motions work on finding all and only class definitions, regardless of their indent level, while its method/function motions work on finding all and only method/function definitions, regardless of their indent level. The stock Vim 8.0 motions can thus be seen as non-semantically aware motions that target indent levels rather than Python classes/methods/functions, while the "Pythonsense" motions, on the other hand, can been seen as semantically aware motions that target Python classes/methods/functions rather than indent levels. In a simple structured file (without even bare functions), e.g.,

class Foo1(object):
    def __init__(self):
        pass
    def bar(self):
        pass
    def baz(self):
        pass
class Foo2(object):
    def __init__(self):
        pass
    def bar(self):
        pass
    def baz(self):
        pass

both the stock Vim and "Pythonsense" motions work the same.

However, in a file like the following:


class Foo1(object):
    def __init__(self):
        pass
    def bar(self):
        pass
    def baz(self):
        pass

class Foo2(object):
    class Foo2Exception1(Exception):
        def __init__(self):
            pass
    class Foo2Exception2(Exception):
        def __init__(self):
            pass
    def __init__(self):
        pass
    def bar(self):
        pass
    def baz(self):
        pass

def fn1(a):
    pass

def fn2(a):
    pass

def fn3(a):
    pass

the differences in behavior show up:

  • With respect to the "]]"/"[["/etc. motions, the stock Vim 8.0+ implementation will visit both top-level class and function definitions freely and miss the nested classes, while the "Pythonsense" implementation will visit just the class definitions exclusively and comprehensively (i.e., no functions and also include the nested classes in the visits).
  • With respect to the "[m"/"]m"/etc. motions, the stock Vim 8.0+ implementation will visit all class, method, and function definitions in the file, whereas the "Pythonsense" implementation will visit just the method and function definitions exclusively (i.e., no classes).

Copyright, License, and Warranty

Copyright (c) 2018 Jeet Sukumaran. All rights reserved.

This work is licensed under the terms of the (expat) MIT license. See the file "LICENSE" (or https://opensource.org/licenses/MIT) for specific terms and details.