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
Firstly, install tree-sitter library. You have to use fairly recent version (specifically the version after 6d1904c221d15d2fcbe0b590ff0a3f96c692429f).
git clone https://github.com/tree-sitter/tree-sitter.git
cd tree-sitter
git checkout 25c7189180849be27b1e552d27f0488e3bd5900d
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 b22ab99f0a85f73a1aec582f7aba0e6b5101b953
./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.
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.
- evil
- avy
- better-jumper (optional)
- eldev (for development)
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/c++
- rust
- nix
- yaml
There are a number of things defined in this library:
Thing | Description | Key |
---|---|---|
compound | Code structures that may enclose multiple statements/expressions (function, loops, conditionals etc.) | e |
statement | Simple statements/expressions, boolean expressions, RHS, etc. | s |
parameter | Parameters/arguments of a function, items of a list/mapping/tuples | a |
string | Literal strings, raw literal strings, heredocs, etc | q |
For more information about treesit things see description of treesit-thing-settings
variable.
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:
Modifier | Description | Key |
---|---|---|
outer | May extend thing bounds to the next or previous sibling | a |
inner | May shrink thing bounds | i |
upper | Select a thing under point and also all its previous siblings | u |
UPPER | The same as upper + extends to the next sibling | U |
lower | Select a thing under point and also all its next siblings | o |
LOWER | The same as lower + extends to the previous sibling | O |
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.
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.
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.
Command | Description | Key |
---|---|---|
beginning-of | Move 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-of | Move 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-thing | Move to the next thing | M-f |
previous-thing | Move to the previous thing | M-b |
same-next-thing | Detect current thing at point and move to the next thing of the same type | C-M-f |
same-previous-thing | Detect current thing at point and move to the previous thing of the same type | C-M-b |
next-sibling-thing | Move to the next sibling thing. If no sibling exists, move to the next sibling of the parent | M-n |
previous-sibling-thing | Move to the previous sibling thing If no sibling exists, move to the parent | M-p |
same-next-sibling-thing | Detect current thing at point and move to the next sibling thing of this type | C-M-n |
same-previous-sibling-thing | Detect current thing at point and move to the previous sibling thing of this type | C-M-p |
next-largest-thing | Move to the next thing that starts after the end of the current thing | |
previous-largest-thing | Move to the previous thing that ends before the start of the current thing | |
same-next-largest-thing | Detect current thing at point and perform next-largest-thing on this thing | |
same-previous-largest-thing | Detect 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.
Command | Prefix key |
---|---|
beginning-of | ( |
end-of | ) |
next-thing | ] |
previous-thing | [ |
next-sibling-thing | } |
previous-sibling-thing | { |
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. When universal prefix is used comment original text and move point to newly cloned text.
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-<
- convolute
-
Command:
evil-ts-obj-convolute
Swap parent node with the grandparent node for the current text object. When numeric argument is provided select select COUNT siblings of the current text object.
Default key: N/a
- 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
)
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.
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
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.
You can press a special key before selecting avy candidates, to perform some predefined action on it. Those actions are implemented in this package:
Description | Key | |
---|---|---|
paste-after | Paste selected thing behind the point. Special care is taken to adapt indentation of the inserted thing. | p |
paste-before | Paste selected thing before the cursor position. | P |
teleport-after | Cut selected thing and paste it behind the point. | m |
teleport-before | Cut selected thing and paste it before the cursor position. | M |
delete | Delete selected thing | x |
yank | Yank selected thing | y |
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
.
(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))
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))))
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.
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
There are packages that are more mature and support more languages:
- combobulate has many feature. Make sure to check it out!
- tree-edit allows to manipulate syntax tree.
- evil-textobj-tree-sitter supports many more languages and works with the older Emacs versions.
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.