dvzubarev / evil-ts-obj

Set of text objects, edit and movement commands for evil

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Treesit Text Objects and edit/movement commands

This package is in the development stage and it requires latest builds of Emacs 30.0.50, since it is based on the thing machinery implemented in the the latest Emacs version.

The goal of this package is not to create text objects for every possible code structure. Instead, the package provides universal text objects that can be used on code structures of the same type. For example, there are no distinct text objects for loops and conditionals, but there is one that you can use on any compound structures like functions, loops, conditionals etc. One can add its own text objects, see customization section. Edit/movement commands heavily rely on defined text objects. For example, extract/inject commands will search for compound text objects to operate on them. However, behavior of edit commands can be changed to some extent via rules.

Key features:

  • Few text objects, but they allow to operate on many code structures (examples)
  • Set of movement commands
  • Set of edit commands/operators (examples)
  • Support of remote text objects using avy (examples)
  • Expand region by repeating selection of the same text object
  • Easy customization of existing language settings

Installation

Emacs and tree-sitter installation

Firstly, install tree-sitter library. You have to use fairly recent version that is not released yet (specifically the version after 6d1904c221d15d2fcbe0b590ff0a3f96c692429f).

git clone https://github.com/tree-sitter/tree-sitter.git
cd tree-sitter
git checkout 660481dbf71413eba5a928b0b0ab8da50c1109e0
make
sudo make install
sudo ldconfig

Install Emacs 30.0.50 from sources.

git clone --no-tags --single-branch --filter=blob:none  https://github.com/mirrors/emacs.git emacs-30
cd emacs-30
git checkout b37676642679a9ec3cd3b995cd14a4af3567cf80
./configure --with-tree-sitter --add-flags-to-your-liking
make
sudo make install

More information about installing tree-sitter library and Emacs from sources see in this post.

Package installation

An example using straight package manager:

(use-package evil-ts-obj
  :straight (evil-ts-obj :type git :host github :repo "dvzubarev/evil-ts-obj")
  :config
  (add-hook 'python-ts-mode-hook #'evil-ts-obj-mode))

doom emacs:

;packages.el
(package! evil-ts-obj :recipe (:host github :repo "dvzubarev/evil-ts-obj"))

(use-package! evil-ts-obj
  :defer t
  :config
  (add-hook 'python-ts-mode-hook #'evil-ts-obj-mode))

If you don’t use straight or doom, you have to clone repository, generate autoloads and optionally compile the project. One way to do it is to use Eldev.

git clone https://github.com/dvzubarev/evil-ts-obj
cd evil-ts-obj
eldev build
eldev compile

After that you can load this package with use-package.

(use-package evil-ts-obj
  :load-path "/path/to/evil-ts-obj/evil-ts-obj/lisp"
  :init
  (load "/path/to/evil-ts-obj/lisp/evil-ts-obj-autoloads.el")
  :defer t
  :config
  (add-hook 'python-ts-mode-hook #'evil-ts-obj-mode))

You also have to compile grammars for languages that you want to use this library with. You can use third-party packages like treesit-auto or install grammars manually.

Package dependencies

  • evil
  • avy
  • better-jumper (optional)
  • eldev (for development)

Supported languages

If you want to enable this package for any supported language, add evil-ts-obj-mode to its hook.

(add-hook 'python-ts-mode-hook #'evil-ts-obj-mode)
(add-hook 'nix-mode-hook #'evil-ts-obj-mode)
  • python
  • bash
  • c++
  • nix
  • yaml

Usage

Defined things

There are a number of things defined in this library:

ThingDescriptionKey
compoundCode structures that may enclose
multiple statements/expressions (function, loops, conditionals etc.)
e
statementSimple statements/expressions, boolean expressions, RHS, etc.s
parameterParameters/arguments of a function, items of a list/mapping/tuplesa

For more information about treesit things see description of treesit-thing-settings variable.

Thing modifiers

Modifiers are used to alter the bounds of a thing, for example, by including next separator or a whitespace. Modifiers behavior depend on the specific thing. The implemented modifiers are:

ModifierDescriptionKey
outerMay extend thing bounds to the next or previous siblinga
innerMay shrink thing boundsi
upperSelect a thing under point and also all its previous siblingsu
UPPERThe same as upper + extends to the next siblingU
lowerSelect a thing under point and also all its next siblingso
LOWERThe same as lower + extends to the previous siblingO

Text objects

See examples in the file.

Combination of modifiers with things produces set of text objects, that you can use with any evil operator (e.g. yie, doa).

  • (outer, compound) - thing without changes
  • (inner, compound) - selects only nested statements
  • (outer, statement) - bounds may be extended to the next sibling, if known separator is adjacent to the current thing. If the current thing is the last statement and there is known separator before it, bounds are extended to the previous sibling.
  • (inner, statement) - thing without changes.
  • (outer, param) - bounds are extended to the next sibling or to the closing parenthesis. If this is the last parameter, bounds are extended to the previous sibling or to the opening parenthesis.
  • (inner, param) - thing without changes.
  • (upper, _) - given the thing under point; its left bound is determined by the furthest previous sibling.
  • (UPPER, _) - the same as upper, but right bound is extended to the next sibling.
  • (lower, _) - given the thing under point; right bound is determined by the furthest next sibling
  • (LOWER, _) - the same as lower, but left bound is extended to the previous sibling

The described behavior may differ from language to language. It is just common conventions that one should try to follow, when creating settings for a language.

Special text objects

Last text object

There is special text object that stores last modified range (evil-ts-obj-last-text-obj). It may be useful in combination with edit operators. For example, after executing extract operator extracted text may be accessed via last text object.

Movement

Commands, listed below, use a group of things, defined in the evil-ts-obj-conf-nav-things variable. This variable is set for every language and for most languages it equals to (or param statement compound). It means, the commands move point to the nearest thing from this list, firstly searching for parameters, then statements and so on.

CommandDescriptionKey
beginning-ofMove to the beginning of the current thing.
When the point is already at the beginning, move to the beginning of the parent thing.
M-a
end-ofMove to the end of the current thing.
When the point is already at the end, move to the end of the parent thing.
M-e
next-thingMove to the next thingM-f
previous-thingMove to the previous thingM-b
same-next-thingDetect current thing at point and
move to the next thing of the same type
C-M-f
same-previous-thingDetect current thing at point and
move to the previous thing of the same type
C-M-b
next-sibling-thingMove to the next sibling thing.
If no sibling exists, move to the next sibling of the parent
M-n
previous-sibling-thingMove to the previous sibling thing
If no sibling exists, move to the parent
M-p
same-next-sibling-thingDetect current thing at point and
move to the next sibling thing of this type
C-M-n
same-previous-sibling-thingDetect current thing at point and
move to the previous sibling thing of this type
C-M-p
next-largest-thingMove to the next thing that starts after the end of the current thing
previous-largest-thingMove to the previous thing that ends before the start of the current thing
same-next-largest-thingDetect current thing at point and
perform next-largest-thing on this thing
same-previous-largest-thingDetect current thing at point and
perform previous-largest-thing on this thing

There are also movement commands for each thing. They are bound to its own prefixes by default.

CommandPrefix key
beginning-of(
end-of)
next-thing]
previous-thing[
next-sibling-thing}
previous-sibling-thing{

Evil operators

There are set of operators for editing text objects. There are DWIM commands that do not require user inputs, operators that expect one text object and operators that should be provided with two text objects in a row. Nearly all DWIM commands have some use for numeric argument. See examples in the file.

drag-{down,up}
Commands: evil-ts-obj-drag-down, evil-ts-obj-drag-up

Swap a current text object with the previous/next sibling. When numeric argument is provided, swap current text object with the Nth sibling.

Default keys: M-j, M-k

raise
Dwim command: evil-ts-obj-raise-dwim

Replace parent text object with the current text object. When numeric argument is set replace Nth parent.

Default key: M-r

Operator: evil-ts-obj-raise

Replace parent text object with the content from provided text object.

Default keys: zr

clone-{before,after}
Commands: evil-ts-obj-clone-before-dwim, evil-ts-obj-clone-after-dwim

Clone current text object at point and paste it before/after the current one.

Default keys: M-C, M-c

extract-{down,up}
Also known as move-{right,left} in lispy.

Dwim commands: evil-ts-obj-extract-down-dwim, evil-ts-obj-extract-up-dwim

Teleport current text object after/before parent text object. When numeric argument is set select Nth parent.

Default keys: M-l, M-h

Operators: evil-ts-obj-extract-down, evil-ts-obj-extract-up

Teleport provided text object after/before parent text object.

Default keys: ze, zE

inject-{down,up}
Also known as lispy-{down,up}-slurp in lispy.

Dwim Commands: evil-ts-obj-inject-down-dwim, evil-ts-obj-inject-up-dwim

Teleport current text object inside next/previous text object. Usually inner compounds are used as place for injection. When numeric argument is set select N-1th child of next/previous text object.

Default keys: M-s, M-S

Operators: evil-ts-obj-inject-down, evil-ts-obj-inject-up

Teleport provided text object inside next/previous text object.

slurp
Command: evil-ts-obj-slurp

Extend current compound with sibling statements COUNT times. Count is provided via numeric argument. When point is inside the compound or at the end of compound slurp lower statements. If point is at the beginning slurp upper statements.

Default key: M->

barf
Command: evil-ts-obj-barf

Shrink current compound extracting inner statements COUNT times. Count is provided via numeric argument. When point is inside the compound or at the end of the compound barf bottommost statements. If point is at the beginning barf topmost statements.

Default key: M-<

low-level operators
These operators expect that user provide two text objects in a row (inspired by evil-exchange).

Operators:

  • evil-ts-obj-swap (zx)
  • evil-ts-obj-replace (zR)
  • evil-ts-obj-clone-after (zc)
  • evil-ts-obj-teleport-after (zt)
  • evil-ts-obj-clone-before (zC)
  • evil-ts-obj-teleport-before (zT)

Selecting overlapping things

The common case is when multiple things start at the same position. It can lead to ambiguity, especially if the things are of the same type, or one uses movement commands that selecting next/previous things based on the current thing at point. For example:

- item1
- item2

The first hyphen symbol is the beginning of the list (compound thing) and a list item (param thing).

if v is not None and v != 0:

Here the first v is the start of the two statement things: whole condition and the first condition (v is not None).

When there are multiple overlapping things, the current thing at point depends on the point position. If the position is before any thing (on the same line), the largest thing is selected, which starts after the position. If the position is after any thing (on the same line), the largest thing is selected, which ends before the position. If the position is inside of any thing, then the smallest enclosing thing is returned.

Expand region

When using evil visual selection you can expand current selection, when using the same text object multiple times. For example, pressing vaeae will select the whole function.

def func():
    while True:
        |pass

Avy integration

By default, avy prefix keybinding is z. Avy allows you to jump to any thing that is currently visible on the screen (try zie). Moreover you can apply any evil operator to the remote thing (e.g. dzae). It also works across multiple windows.

See examples in the file.

Custom avy actions

You can press a special key before selecting avy candidates, to perform some predefined action on it. Those actions are implemented in this package:

DescriptionKey
paste-afterPaste selected thing behind the point.
Special care is taken to adapt indentation of the inserted thing.
p
paste-beforePaste selected thing before the cursor position.P
teleport-afterCut selected thing and paste it behind the point.m
teleport-beforeCut selected thing and paste it before the cursor position.M
deleteDelete selected thingx
yankYank selected thingy

See examples in the file.

For someone who finds it inconvenient to press action key before selection, there exist standalone functions for starting paste action. Check evil-ts-obj-avy-compound-outer-paste-after, evil-ts-obj-avy-compound-outer-teleport-after commands and also corresponding keymaps: evil-ts-obj-avy-inner-paste-map, evil-ts-obj-avy-outer-paste-map.

Customization

Changing various keybindings

(use-package! evil-ts-obj
  :init

  (setopt evil-ts-obj-enabled-keybindings '(generic-navigation text-objects))
  (setopt evil-ts-obj-navigation-keys-prefix
        '((beginning-of . nil)
          (end-of . nil)
          (previous . "(")
          (next . ")")
          (previous-largest . "{")
          (next-largest . "}")))

  (setopt evil-ts-obj-avy-key-prefix "C-j")

  (setopt evil-ts-obj-compound-thing-key "d")
  :config

  (evil-define-key 'normal 'evil-ts-obj-mode
    (kbd "M-a") nil
    (kbd "C-M-a") #'evil-ts-obj-beginning-of-thing))

Add things and text objects

For example, you want to add its own thing for a function in python.

Basic configuration:

(use-package! evil-ts-obj
  :config

  (add-to-list 'evil-ts-obj-python-things '(func "function_definition"))

  (evil-ts-obj-define-text-obj func outer)
  (keymap-set evil-ts-obj-outer-text-objects-map "F" #'evil-ts-obj-func-outer)
  ;to setup everything at once
  ;(evil-ts-obj-setup-all-text-objects func "F")

  ;; bind all movement commands to key f
  (evil-ts-obj-setup-all-movement func "f")

  ;optional modifiers of a new thing
  (setq evil-ts-obj-python-ext-func #'my-python-ext))

(defun my-python-ext (spec node)
  (pcase spec
    ;; create inner text object
    ((pmap (:thing 'func) (:mod 'inner))
     (evil-ts-obj-python-extract-compound-inner node))
    ;jump on the function name for every movement command
    ((pmap (:thing 'func) (:act 'nav))
     (when-let ((name-node (treesit-node-child-by-field-name node "name")))
       (list (treesit-node-start name-node)
             (treesit-node-end node))))
    (_
     (evil-ts-obj-python-ext spec node))))

Add a new language

See basic example for python or yaml settings to get started. Check out other setting files that are in a repository for a little bit more advanced configurations.

Development

For running tests you have to install Eldev. When running tests one should pass the path to the directory containing grammars for all testing languages.

eldev -S '(setq treesit-extra-load-path (list "/path/to/tree-sitter/grammars"))' test

Alternatives and Inspirations

There are packages that are more mature and support more languages:

The main difference of this package is heavy usage of builtin things machinery. Introducing extra layer of abstraction some things are getting easier to do. Although this package is not as powerful as others that directly manipulate the syntax tree, it is still possible to implement some useful editing commands. Also language setup is quite easy in basic case: one need to list nodes designated for things and write a function for extracting inner range from the compound thing. After that most commands will work. Compared to evil-textobj-tree-sitter using things should be more efficient, since it does not require to execute bunch of queries against the whole buffer, whenever you use text objects.

Remote text objects were inspired by the great packages targets.el, things.el.

About

Set of text objects, edit and movement commands for evil

License:GNU General Public License v3.0


Languages

Language:Emacs Lisp 100.0%