This commit is contained in:
Zane Schaffer 2022-10-11 16:59:29 -07:00 committed by Zane Schaffer
commit 86508c397c
972 changed files with 93146 additions and 0 deletions

8
about.txt Executable file
View File

@ -0,0 +1,8 @@
Hello! I'm Zane ~
Resume
Campaign Specialist at Cox Automotive, 2021-2022
Front End Developer at Zoe Bios Creative, 2020-2021

5
contact.txt Executable file
View File

@ -0,0 +1,5 @@
mail@zane.town
zane @ irc.tilde.chat
zane#9090 @ discord

BIN
cv/ZaneSchafferCV.pdf Executable file

Binary file not shown.

826
cv/index.html Executable file

File diff suppressed because one or more lines are too long

15
dotfiles/.gitconfig Executable file
View File

@ -0,0 +1,15 @@
[user]
name = Zane Schaffer
email = z@zane.town
[core]
editor = vim
excludesFile = '~/.gitignore'
[fetch]
prune = true
[init]
defaultBranch = main
[status]
short = true

34
dotfiles/.tmux.conf Executable file
View File

@ -0,0 +1,34 @@
set -g default-terminal "xterm-256color"
set-option -ga terminal-overrides ",xterm-256color:Tc"
unbind C-b
set -g prefix C-a
bind C-a send-prefix
bind q kill-pane
unbind-key '"'
unbind-key %
unbind-key v
bind v split-window -v
set-window-option -g mode-keys vi
set -g renumber-windows on
set -g status-left ''
set -g status-right '#{now_playing} %I:%M%p '
set -g status-style 'fg=colour15 bg=colour0'
set -g window-status-current-style 'fg=colour7 bold'
set -g window-status-activity-style 'fg=colour2'
set -g window-status-style 'fg=colour15 bg=colour0'
set -g pane-border-style 'fg=colour15'
set -g pane-active-border-style 'fg=colour15'
set -g message-style 'fg=colour15 bg=colour0'
set -g clock-mode-colour 'colour2'
set -g @plugin 'tmux-plugins/tpm'
set -g @plugin 'tmux-plugins/tmux-sensible'
set -g @plugin 'christoomey/vim-tmux-navigator'
set -g @plugin 'spywhere/tmux-now-playing'
run '~/.tmux/plugins/tpm/tpm'

49
dotfiles/.zshrc Executable file
View File

@ -0,0 +1,49 @@
# Exports
export EDITOR=vi
export VISUAL=vi
export PF_INFO="ascii title os host pkgs memory palette"
export PF_COL3=4
export LS_COLORS="$LS_COLORS:ow=1;7;34:st=30;44:su=30;41"
export MANPAGER='nvim +Man!'
export PATH="$PATH:$HOME/nand2tetris/tools"
export PATH="$HOME/.local/bin:$PATH"
export PATH="/opt/local/libexec/gnubin:$PATH"
export KEYTIMEOUT=1
export VI_MODE_SET_CURSOR=true
# Aliases
alias ls="ls -F --color=always --group-directories-first -h"
alias la="ls -F --color=always -Ahs"
alias l="ls -A"
alias ll="la -l"
alias vim=$EDITOR
alias t='python $HOME/repos/t/t.py --task-dir $HOME/tasks --list tasks'
alias wr='curl -fGsS --compressed "wttr.in/98122?1FQnT"'
alias w='curl -fGsS --compressed "wttr.in/98122?format=%C+%f+%p\n"'
setopt interactive_comments
bindkey -v
# Prompt
PS1='%(?.;.%F{red}%B;%b%f) '
# Completion
fpath=(/opt/local/share/zsh/site-functions $fpath)
autoload -Uz compinit
compinit
eval "$(fnm env)"
#[ -f "/Users/zane/.ghcup/env" ] && source "/Users/zane/.ghcup/env" # ghcup-env
source ~/.config/up/up.sh
[ -f "/Users/zane/.ghcup/env" ] && source "/Users/zane/.ghcup/env" # ghcup-env
if [ -z "$TMUX" ]
then
if tmux has-session 2>/dev/null; then
tmux attach-session
else
tmux new-session -s main sandman catgirl tilde
fi
else
fi

60
dotfiles/init.lua Executable file
View File

@ -0,0 +1,60 @@
require('impatient')
-- Don't load stock plugins
vim.g.loaded_gzip = 1
vim.g.loaded_tar = 1
vim.g.loaded_tarPlugin = 1
vim.g.loaded_zip = 1
vim.g.loaded_zipPlugin = 1
vim.g.loaded_getscript = 1
vim.g.loaded_getscriptPlugin = 1
vim.g.loaded_vimball = 1
vim.g.loaded_vimballPlugin = 1
vim.g.loaded_2html_plugin = 1
vim.g.loaded_logiPat = 1
vim.g.loaded_rrhelper = 1
vim.g.loaded_netrwPlugin = 1
vim.g.did_load_filetypes = 1
-- Settings
HOME = os.getenv("HOME")
vim.opt.backspace = "indent,eol,start"
vim.opt.history = 1000
vim.opt.timeout = false
vim.opt.ttimeout = true
vim.opt.ttimeoutlen = 50
vim.opt.scrolloff = 5
vim.opt.clipboard = "unnamed"
vim.opt.backup = false
vim.opt.swapfile = false
vim.opt.laststatus = 3
vim.opt.splitbelow = true
vim.opt.splitright = true
vim.opt.shiftwidth = 2
vim.opt.tabstop = 2
vim.opt.expandtab = true
vim.opt.list = true
vim.opt.listchars= { tab = ' ', nbsp='·',trail ='·' }
vim.opt.incsearch = true
vim.opt.smartcase = true
vim.opt.ignorecase = true
vim.opt.synmaxcol = 200
vim.opt.showmode = false
vim.opt.grepprg="rg --vimgrep --smart-case --hidden"
vim.opt.grepformat="%f:%l:%c:%m"
vim.opt.background = "dark"
vim.g.seoulbones_compat = 1
vim.g.zenwritten_compat = 1
vim.g.tokyobones_compat = 1
vim.opt.termguicolors = true
vim.opt.background = "light"
vim.cmd[[colorscheme seoulbones]]
-- Mappings
vim.api.nvim_set_keymap("t", "jk", "<C-\\><C-n>", {noremap = true})
vim.api.nvim_set_keymap("i", "jk", "<esc>", {noremap = true})
vim.api.nvim_set_keymap("n", "j", "gj", {noremap = false})
vim.api.nvim_set_keymap("n", "k", "gk", {noremap = false})
vim.api.nvim_set_keymap("n", "[q", "<Plug>qf_qf_previous", {noremap = false})
vim.api.nvim_set_keymap("n", "]q", "<Plug>qf_qf_next", {noremap = false})
require("mini.comment").setup()
require("tmux").setup()

View File

@ -0,0 +1,36 @@
name: ci
on: [push, pull_request]
jobs:
linting:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: lunarmodules/luacheck@v0
testing:
strategy:
matrix:
nvim-version: ["v0.6.0", "v0.6.1", "v0.7.0", "nightly"]
# If one versions fails, still run all the other versions
fail-fast: false
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- name: Install Neovim ${{ matrix.nvim-version }}
run: |
mkdir ./neovim
curl -sL https://github.com/neovim/neovim/releases/download/${{ matrix.nvim-version }}/nvim-linux64.tar.gz \
| tar xzf - --strip-components=1 -C ./neovim
./neovim/bin/nvim --version
- name: Install Just
uses: extractions/setup-just@v1
- name: Run tests
run: |
export PATH="./neovim/bin:$PATH"
export VIM="./neovim/share/nvim/runtime"
just test

View File

@ -0,0 +1,3 @@
std = luajit
globals = { "vim", "_" }
exclude_files = { "plenary.nvim" }

View File

@ -0,0 +1,7 @@
all: lint test
test:
nvim --headless --clean -u tests/test_init.vim +Test
lint:
luacheck .

View File

@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

View File

@ -0,0 +1,97 @@
# dirbuf.nvim
A directory buffer for Neovim that lets you edit your filesystem like you edit
text. Inspired by [vim-dirvish] and [vidir].
## Features
* *Intuitive:* Create, copy, delete, and rename files, directories, and more by
editing their lines in the directory buffer. Buffer names are automatically
updated to reflect changes.
* *Minimal:* Works out of the box with no configuration. Default mappings
easily changed.
* *Unobtrusive:* Preserves alternate buffers and navigation history. Switch
between files with `Ctrl-^` (`Ctrl-6`) and jump around your navigation history
with custom `<Plug>` mappings.
* *Safe:* Does not modify the filesystem until you save the buffer. Optionally
request confirmation and dry-run saving.
* *Reliable:* Resolves inter-dependencies in batch renames, including cycles.
* *Polite:* Plays nicely with tree-based file viewers like [nvim-tree.lua],
[fern.vim], and [carbon.nvim].
https://user-images.githubusercontent.com/42009212/162110083-9fd3701f-8ffb-4cf7-9333-d57020a9242e.mp4
## Installation
Requires [Neovim 0.6](https://github.com/neovim/neovim/releases/tag/v0.6.0) or
higher.
* [vim-plug]: `Plug "elihunter173/dirbuf.nvim"`
* [packer.nvim]: `use "elihunter173/dirbuf.nvim"`
### Notes
If you use [`nvim-tree.lua`](https://github.com/kyazdani42/nvim-tree.lua), you
must disable the `:help nvim-tree.update_to_buf_dir` option. Otherwise, Dirbuf
will fail to open directory buffers.
```lua
require("nvim-tree").setup {
update_to_buf_dir = { enable = false }
}
```
## Usage
Run the command `:Dirbuf` to open a directory buffer. Press `-` in any buffer
to open a directory buffer for its parent. Editing a directory will also open
up a directory buffer, overriding Netrw.
Inside a directory buffer, there are the following keybindings:
* `<CR>`: Open the file or directory at the cursor.
* `gh`: Toggle showing hidden files (i.e. dot files).
* `-`: Open parent directory.
See `:help dirbuf.txt` for more info.
## Configuration
Configuration is not necessary for Dirbuf to work. But for those that want to
override the default config, the following options are available with their
default values listed.
```lua
require("dirbuf").setup {
hash_padding = 2,
show_hidden = true,
sort_order = "default",
write_cmd = "DirbufSync",
}
```
Read the [documentation](/doc/dirbuf.txt) for more information (`:help
dirbuf-options`).
## Development
A [Justfile][just] is provided to test and lint the project.
```sh
# Run unit tests
$ just test
# Run luacheck
$ just lint
```
`just test` will automatically download [plenary.nvim]'s test harness and run
the `*_spec.lua` tests in `tests/`.
[carbon.nvim]: https://github.com/SidOfc/carbon.nvim
[fern.vim]: https://github.com/lambdalisue/fern.vim
[just]: https://github.com/casey/just
[nvim-tree.lua]: https://github.com/kyazdani42/nvim-tree.lua
[packer.nvim]: https://github.com/wbthomason/packer.nvim
[plenary.nvim]: https://github.com/nvim-lua/plenary.nvim
[vidir]: https://github.com/trapd00r/vidir
[vim-dirvish]: https://github.com/justinmk/vim-dirvish
[vim-plug]: https://github.com/junegunn/vim-plug

View File

@ -0,0 +1,197 @@
*dirbuf.txt* directory buffer
==============================================================================
OVERVIEW *dirbuf*
Dirbuf provides Neovim with an editable directory buffer. This buffer is a
regular text buffer with some metadata behind the scenes allowing you to
leverage all of Neovim's built-in text editing capabilities to efficiently
manipulate and edit file directories.
To create a new file, add a new line containing the name of the file. To
create an empty directory, add a "/" at the end.
To delete a file or directory, delete its line.
To copy a file or directory, copy its line and give it a new name.
To rename a file or directory, change its name in the directory buffer.
When you save the buffer, Dirbuf applies the necessary filesystem operations
to get the directory into the desired state. It does this by comparing the
snapshot it took of the directory when the buffer was created to the state of
the buffer upon saving. Using the hashes at the end of every line, Dirbuf can
tell what objects are new (i.e. they do not have a hash) and what objects have
changed (i.e. their hash does not match their name).
Because each Dirbuf buffer name is the literal directory path, you can run any
|:!| commands you want and prefix the filenames with |%|. For example, >
:!sed 's/hi/ahoy/g' %/pirate_script.txt -i
Dirbuf is designed to work with built-in Vim concepts as much as possible. Tim
Pope's plugins demonstrate this theme; more plugins should too. Re-use of
concepts multiplies the utility of those concepts; conversely if a plugin does
not reuse a concept, both that concept and the new one are made mutually less
valuable--the sum is less than the parts--because the user must learn or
choose from two slightly different things instead of one augmented system.
==============================================================================
MAPPINGS *dirbuf-mappings*
All mappings are listed with their <Plug> mapping and their default mapping.
If a mapping to the <Plug> version already exists, then the default mapping is
not made.
Global ~
<Plug>(dirbuf_up)
- Opens the current file's directory or the [count]th parent
directory.
Buffer-local (filetype=dirbuf) ~
<Plug>(dirbuf_up)
- Opens the current file's directory or the [count]th parent
directory.
<Plug>(dirbuf_enter)
<CR> Opens file or directory at cursor.
<Plug>(dirbuf_toggle_hide)
gh Toggles whether hidden files (i.e. dot files) are
displayed.
<Plug>(dirbuf_history_forward)
Moves forward [count] times in the directory buffer
history.
<Plug>(dirbuf_history_backward)
Moves backward [count] times in the directory buffer
history.
==============================================================================
COMMANDS *dirbuf-commands*
:Dirbuf [path] *dirbuf-:Dirbuf*
Opens the directory at [path], or its parent if [path] is a file, or the
parent of the current file if [path] is not given.
:DirbufQuit *dirbuf-:DirbufQuit*
Quits and returns to the original file.
:DirbufSync [{flag}] *dirbuf-:DirbufSync*
Saves and refreshes the current directory buffer, syncing its state with
the file system by creating, moving, copying, or deleting entries as
necessary.
Flags: ~
-confirm Before changing the filesystem, print out a list of all
the actions `:DirbufSync` would perform, like `-dry-run`.
Then ask the user to confirm the changes before making
them.
-dry-run Rather than changing the filesystem, print out a list of
all the actions `:DirbufSync` would perform. These are
formatted as Unix-like commands (e.g. `mv 'foo' 'bar'`),
no matter what platform you are on.
==============================================================================
FUNCTIONS *dirbuf-functions*
dirbuf.setup({opts}) *dirbuf.setup()*
Overwrites the default options with the options in the {opts} table.
Example with all the default options: >
require("dirbuf").setup {
hash_padding = 2,
show_hidden = true,
sort_order = "default",
write_cmd = "DirbufSync",
}
dirbuf.enter({cmd}) *dirbuf.enter()*
Performs {cmd} ("edit", "vsplit", "split", "tabedit") on the path
currently under the cursor.
dirbuf.get_cursor_path() *dirbuf.get_cursor_path()*
Returns the absolute path of the filesystem entry under the cursor in the
current directory buffer. If there are any errors parsing the current
line, then this `error()`s with a descriptive error message.
==============================================================================
OPTIONS *dirbuf-options*
|hash_padding| (default: `2`)
Number of characters of padding between the file hashes and the longest
filename. This must be an integer larger than 1.
|show_hidden| (default: `true`)
Whether Dirbuf should display hidden files (i.e. "dot files") by default
when opening new directory buffers. This can be changed locally per-buffer
with the `gh` mapping.
|sort_order| (default: `"default"`)
What order Dirbuf should sort the directory buffer in when it is created
and refreshed.
This must be given as either a `string` or a `function`.
If a `string` is given, then it must have one of the following values.
Values: ~
"default" sort case-insensitively by {fname}
"directories_first" groups files of like {ftype} and then sort within
groups case-insensitively by {fname}
If a `function` is given, it must be a comparison function which takes two
tables {left} and {right}, each describing a filesystem entry, which
returns `true` when {left} should appear before (i.e. above) {right} in
the directory buffer.
Both of the tables {left} and {right} have the following fields.
Fields: ~
{fname} `string` containing the literal, unescaped name of the
filesystem entry without any suffixes (e.g. a directory
example/ would have an fname of "example")
{ftype} `string` describing the type of the filesystem entry, which
must be one of "file", "directory", "link", "fifo", "socket",
"char", or "block"
{path} `string` containing the full path of the filesystem entry
using platform-specific directory separators (i.e. "\" on
Windows and "/" on Linux and MacOS) without a suffix
|write_cmd| (default: `"DirbufSync"`)
What command Dirbuf should execute when the user issues a `:write`.
Examples: ~
"DirbufSync -confirm"
Requests confirmation from the user before syncing the changes
made to the directory buffer.
"" or "echoerr ':write disabled'"
Disables `:write` in directory buffers, forcing users to
explicitly invoke `:DirbufSync`.
==============================================================================
FAQ *dirbuf-faq*
Can I conceal hashes in directory buffers? ~
Dirbuf does not natively support `conceal` on hashes because the author
believes seeing the hashes is important to making Dirbuf's actions
predictable and wants to dissuade new users from hiding the hashes.
However, if you really want to conceal the hashes, you can create a
`after/syntax/dirbuf.vim` file with the following code which modifies the
normal DirbufHash definition to support `conceal`. >
syntax clear DirbufHash
syntax match DirbufHash /^#\x\{8}\t/ms=s-1 conceal cchar=#
setlocal conceallevel=2
setlocal concealcursor=n
If you feel strongly that Dirbuf should natively support `conceal` on hashes,
+1 this issue and I will consider it: >
https://github.com/elihunter173/dirbuf.nvim/issues/23
==============================================================================
CREDITS *dirbuf-credits*
Dirbuf was initially conceived of as a Lua rewrite of the file manager plugin
Dirvish and eventually grew in scope to become an editable directory buffer
similiar to vidir. However, it still owes many of its ideas to Dirvish as well
as much of its literal Vimscript and help documentation.
==============================================================================
vim:tw=78:ts=4:et:ft=help:norl:

View File

@ -0,0 +1,14 @@
dirbuf dirbuf.txt /*dirbuf*
dirbuf-:Dirbuf dirbuf.txt /*dirbuf-:Dirbuf*
dirbuf-:DirbufQuit dirbuf.txt /*dirbuf-:DirbufQuit*
dirbuf-:DirbufSync dirbuf.txt /*dirbuf-:DirbufSync*
dirbuf-commands dirbuf.txt /*dirbuf-commands*
dirbuf-credits dirbuf.txt /*dirbuf-credits*
dirbuf-faq dirbuf.txt /*dirbuf-faq*
dirbuf-functions dirbuf.txt /*dirbuf-functions*
dirbuf-mappings dirbuf.txt /*dirbuf-mappings*
dirbuf-options dirbuf.txt /*dirbuf-options*
dirbuf.enter() dirbuf.txt /*dirbuf.enter()*
dirbuf.get_cursor_path() dirbuf.txt /*dirbuf.get_cursor_path()*
dirbuf.setup() dirbuf.txt /*dirbuf.setup()*
dirbuf.txt dirbuf.txt /*dirbuf.txt*

View File

@ -0,0 +1,14 @@
if !hasmapto('<Plug>(dirbuf_enter)')
nmap <buffer> <cr> <Plug>(dirbuf_enter)
endif
if !hasmapto('<Plug>(dirbuf_toggle_hide)')
nmap <buffer> gh <Plug>(dirbuf_toggle_hide)
endif
if !hasmapto('<Plug>(dirbuf_up)')
nmap <buffer> - <Plug>(dirbuf_up)
endif
augroup dirbuf_local
autocmd! * <buffer>
autocmd BufWriteCmd <buffer> execute v:lua.require('dirbuf.config').get('write_cmd')
augroup END

View File

@ -0,0 +1,383 @@
local api = vim.api
local buffer = require("dirbuf.buffer")
local config = require("dirbuf.config")
local fs = require("dirbuf.fs")
local planner = require("dirbuf.planner")
local M = {}
local CURRENT_BUFFER = 0
local CURRENT_WINDOW = 0
function M.setup(opts)
local errors = config.update(opts)
if #errors == 1 then
api.nvim_err_writeln("dirbuf.setup: " .. errors[1])
elseif #errors > 1 then
api.nvim_err_writeln("dirbuf.setup:")
for _, err in ipairs(errors) do
api.nvim_err_writeln(" " .. err)
end
end
end
-- `normalize_path` takes a `path` entered by the user, potentially containing
-- duplicate path separators, "..", or trailing path separators, and ensures
-- that all duplicate path separators are removed, there is no trailing path
-- separator, and all ".."s are simplified. This does not resolve symlinks.
--
-- This exists to ensure that all paths are displayed in a consistent way and
-- to simplify path manipulation logic.
local function normalize_path(path)
path = vim.fn.simplify(vim.fn.fnamemodify(path, ":p"))
-- On Windows, simplify keeps the path_separator on directories
if path:sub(-1, -1) == fs.path_separator then
path = vim.fn.fnamemodify(path, ":h")
end
return path
end
-- `fill_dirbuf` fills the current buffer with the contents of its
-- corresponding directory. Note that the current buffer must have the name of
-- a valid directory.
--
-- If `on_fname` is set, then the cursor will be put on the line corresponding
-- to `on_fname`.
--
-- Returns: err
local function fill_dirbuf(on_fname)
local dir = api.nvim_buf_get_name(CURRENT_BUFFER)
local err, fs_entries = fs.get_fs_entries(dir, vim.b.dirbuf_show_hidden)
if err ~= nil then
return err
end
-- Before we set lines, we set undolevels to -1 so we delete the history when
-- we set the lines. This prevents people going back to now-invalid hashes
-- and potentially messing up their directory on accident
local buf_lines, fname_line = buffer.write_fs_entries(fs_entries, on_fname)
local undolevels = vim.bo.undolevels
vim.bo.undolevels = -1
api.nvim_buf_set_lines(CURRENT_BUFFER, 0, -1, true, buf_lines)
vim.bo.undolevels = undolevels
vim.b.dirbuf = fs_entries
vim.bo.tabstop = #"#" + buffer.HASH_LEN + config.get("hash_padding")
api.nvim_win_set_cursor(CURRENT_WINDOW, { fname_line or 1, #"#" + buffer.HASH_LEN + #"\t" })
vim.bo.modified = false
return nil
end
function M.init_dirbuf(history, history_index, update_history, from_path)
-- Preserve altbuf
local altbuf = vim.fn.bufnr("#")
local path = normalize_path(vim.fn.expand("%"))
api.nvim_buf_set_name(CURRENT_BUFFER, path)
-- Determine where to place cursor
-- We ignore errors in case the buffer is empty
local _, _, cursor_fname, _ = buffer.parse_line(api.nvim_get_current_line())
-- See if we're coming from a path below this dirbuf.
if from_path ~= nil and vim.startswith(from_path, path) then
-- Make sure we're clipping past the "/" in from_path
local fname_start = #path + 1
if path:sub(-1, -1) ~= fs.path_separator then
fname_start = fname_start + 1
end
local last_path_separator = from_path:find(fs.path_separator, fname_start, true)
if last_path_separator ~= nil then
cursor_fname = from_path:sub(fname_start, last_path_separator - 1)
else
cursor_fname = from_path:sub(fname_start)
end
end
-- Update history
if history == nil then
history = {}
history_index = 0
end
if update_history then
-- Clear old history
while #history > history_index do
table.remove(history)
end
-- We don't add to history if we're just refreshing the dirbuf
if path ~= history[history_index] then
table.insert(history, path)
history_index = history_index + 1
end
end
vim.b.dirbuf_history = history
vim.b.dirbuf_history_index = history_index
-- Set dirbuf options
vim.bo.filetype = "dirbuf"
vim.bo.buftype = "acwrite"
vim.bo.bufhidden = "wipe"
-- Normally unnecessary but sometimes other plugins make things unmodifiable,
-- so we have to do this to prevent running into errors in fill_dirbuf
vim.bo.modifiable = true
-- Set "dirbuf_show_hidden" to default if it is unset
if vim.b.dirbuf_show_hidden == nil then
vim.b.dirbuf_show_hidden = config.get("show_hidden")
end
if altbuf ~= -1 then
vim.fn.setreg("#", altbuf)
end
local err = fill_dirbuf(cursor_fname)
if err ~= nil then
api.nvim_err_writeln(err)
return
end
end
function M.get_cursor_path()
local err, _, fname, _ = buffer.parse_line(api.nvim_get_current_line())
if err ~= nil then
error(err)
end
local dir = normalize_path(vim.fn.expand("%"))
return fs.join_paths(dir, fname)
end
-- If `path` is a file, this returns the absolute path to its parent. Otherwise
-- it returns the absolute path of `path`.
local function directify(path)
if fs.is_directory(path) then
return vim.fn.fnamemodify(path, ":p")
else
return vim.fn.fnamemodify(path, ":h:p")
end
end
function M.open(path)
if path == "" then
path = api.nvim_buf_get_name(CURRENT_BUFFER)
end
path = normalize_path(directify(path))
local from_path = normalize_path(vim.fn.expand("%"))
if from_path == path then
-- If we're not leaving, we want to keep the cursor on the same line
local err, _, fname, _ = buffer.parse_line(api.nvim_get_current_line())
if err ~= nil then
api.nvim_err_writeln("Error placing cursor: " .. err)
return
end
from_path = fs.join_paths(path, fname)
end
local keepalt = ""
if vim.bo.filetype == "dirbuf" then
-- If we're leaving a dirbuf, keep our alternate buffer
keepalt = "keepalt"
end
local history, history_index = vim.b.dirbuf_history, vim.b.dirbuf_history_index
vim.cmd(keepalt .. " noautocmd edit " .. vim.fn.fnameescape(path))
-- Sanity check: If we're not in the file we just edited, something went
-- wrong. This can happen if someone has `:set nohidden confirm`,
-- accidentally opens dirbuf, and hits escape at the save prompt. The edit
-- "fails" without raising an error
if api.nvim_buf_get_name(CURRENT_BUFFER) ~= path then
return
end
M.init_dirbuf(history, history_index, true, from_path)
end
function M.enter(cmd)
if cmd == nil then
cmd = "edit"
end
if vim.bo.filetype ~= "dirbuf" then
api.nvim_err_writeln("Operation only supports 'filetype=dirbuf'")
return
end
local err, _, fname, _ = buffer.parse_line(api.nvim_get_current_line())
if err ~= nil then
api.nvim_err_writeln(err)
return
end
if vim.bo.modified then
api.nvim_err_writeln(string.format("Cannot enter '%s'. Dirbuf must be saved first", fname))
return
end
local dir = normalize_path(vim.fn.expand("%"))
local path = fs.join_paths(dir, fname)
local noautocmd = ""
if fs.is_directory(path) then
noautocmd = "noautocmd"
end
local history, history_index = vim.b.dirbuf_history, vim.b.dirbuf_history_index
vim.cmd("keepalt " .. noautocmd .. " " .. cmd .. " " .. vim.fn.fnameescape(path))
if fs.is_directory(path) then
M.init_dirbuf(history, history_index, true)
end
end
function M.jump_history(n)
if vim.bo.filetype ~= "dirbuf" then
api.nvim_err_writeln("Operation only supports 'filetype=dirbuf'")
return
end
local history, history_index = vim.b.dirbuf_history, vim.b.dirbuf_history_index
local next_index = math.max(1, math.min(#history, history_index + n))
vim.cmd("keepalt noautocmd edit " .. vim.fn.fnameescape(history[next_index]))
M.init_dirbuf(history, next_index, false, history[history_index])
end
function M.quit()
if vim.bo.filetype ~= "dirbuf" then
api.nvim_err_writeln(":DirbufQuit only supports 'filetype=dirbuf'")
return
end
local altbuf = vim.fn.bufnr("#")
if altbuf == -1 or altbuf == api.nvim_get_current_buf() then
vim.cmd("bdelete")
else
api.nvim_set_current_buf(altbuf)
end
end
-- Ensure that the directory has not changed since our last snapshot
local function check_dirbuf(buf)
local dir = api.nvim_buf_get_name(buf)
local err, current_fs_entries = fs.get_fs_entries(dir, vim.b.dirbuf_show_hidden)
if err ~= nil then
return "Error while checking: " .. err
end
if not vim.deep_equal(vim.b.dirbuf, current_fs_entries) then
return "Snapshot out of date with current directory. Run :edit! to refresh"
end
return nil
end
-- print_plan() should only be called from dirbuf.sync()
local function print_plan(plan)
local function fmt_fs_entry(fs_entry)
return vim.fn.shellescape(buffer.display_fs_entry(fs_entry))
end
for _, action in ipairs(plan) do
if action.type == "create" then
if action.fs_entry.ftype == "directory" then
print("mkdir " .. fmt_fs_entry(action.fs_entry))
else
print("touch " .. fmt_fs_entry(action.fs_entry))
end
elseif action.type == "copy" then
print("cp " .. fmt_fs_entry(action.src_fs_entry) .. " " .. fmt_fs_entry(action.dst_fs_entry))
elseif action.type == "delete" then
print("rm " .. fmt_fs_entry(action.fs_entry))
elseif action.type == "move" then
print("mv " .. fmt_fs_entry(action.src_fs_entry) .. " " .. fmt_fs_entry(action.dst_fs_entry))
else
error("Unrecognized action: " .. vim.inspect(action))
end
end
end
-- do_plan() should only be called from dirbuf.sync()
local function do_plan(plan)
local err = planner.execute_plan(plan)
if err ~= nil then
api.nvim_err_writeln("Error making changes: " .. err)
api.nvim_err_writeln("WARNING: Dirbuf in inconsistent state. Run :edit! to refresh")
return
end
-- Leave cursor on the same file
local fname
err, _, fname, _ = buffer.parse_line(api.nvim_get_current_line())
if err ~= nil then
api.nvim_err_writeln(err)
return
end
err = fill_dirbuf(fname)
if err ~= nil then
api.nvim_err_writeln(err)
return
end
end
function M.sync(opt)
if opt == nil then
opt = ""
end
if vim.bo.filetype ~= "dirbuf" then
api.nvim_err_writeln(":DirbufSync only supports 'filetype=dirbuf'")
return
end
if opt ~= "" and opt ~= "-confirm" and opt ~= "-dry-run" then
api.nvim_err_writeln(":DirbufSync unrecognized option: " .. opt)
end
if not vim.bo.modified then
return
end
local err = check_dirbuf(CURRENT_BUFFER)
if err ~= nil then
api.nvim_err_writeln("Cannot save dirbuf: " .. err)
return
end
local dir = api.nvim_buf_get_name(CURRENT_BUFFER)
local lines = api.nvim_buf_get_lines(CURRENT_BUFFER, 0, -1, true)
local changes
err, changes = planner.build_changes(dir, vim.b.dirbuf, lines)
if err ~= nil then
api.nvim_err_writeln(err)
return
end
local plan = planner.determine_plan(changes)
if opt == "-confirm" then
print_plan(plan)
-- We pcall to make Ctrl-C work
local ok, response = pcall(vim.fn.confirm, "Sync changes?", "&Yes\n&No", 2)
if ok and response == 1 then
do_plan(plan)
end
elseif opt == "-dry-run" then
print_plan(plan)
else
do_plan(plan)
end
end
function M.toggle_hide()
if vim.bo.filetype ~= "dirbuf" then
api.nvim_err_writeln("Operation only supports 'filetype=dirbuf'")
return
end
vim.b.dirbuf_show_hidden = not vim.b.dirbuf_show_hidden
-- Leave cursor on the same file
local err, _, fname, _ = buffer.parse_line(api.nvim_get_current_line())
if err ~= nil then
api.nvim_err_writeln(err)
return
end
err = fill_dirbuf(fname)
if err ~= nil then
api.nvim_err_writeln(err)
return
end
end
return M

View File

@ -0,0 +1,203 @@
local fs = require("dirbuf.fs")
local M = {}
M.HASH_LEN = 8
--[[
local record Dirbuf
dir: string
fs_entries: {string: FSEntry}
end
--]]
local function is_suffix(c)
return c == "/" or c == "\\" or c == "@" or c == "|" or c == "=" or c == "%" or c == "#"
end
-- These suffixes are taken from `ls --classify` and zsh's tab completion
local function suffix_to_ftype(suffix)
if suffix == nil then
return "file"
elseif suffix == "/" or suffix == "\\" then
return "directory"
elseif suffix == "@" then
return "link"
elseif suffix == "|" then
return "fifo"
elseif suffix == "=" then
return "socket"
elseif suffix == "%" then
return "char"
elseif suffix == "#" then
return "block"
else
error(
string.format("Unrecognized suffix %s. This should be impossible and is a bug in dirbuf.", vim.inspect(suffix))
)
end
end
local function ftype_to_suffix(ftype)
if ftype == "file" then
return ""
elseif ftype == "directory" then
return fs.path_separator
elseif ftype == "link" then
return "@"
elseif ftype == "fifo" then
return "|"
elseif ftype == "socket" then
return "="
elseif ftype == "char" then
return "%"
elseif ftype == "block" then
return "#"
else
error(string.format("Unrecognized ftype %s. This should be impossible and is a bug in dirbuf", vim.inspect(ftype)))
end
end
-- escaped char -> unescaped
-- We treat "\\" separately to avoid confusion where we duplicate backslashes
-- when trying to programmatically escape characters
local ESCAPE_CHARS = { n = "\n", t = "\t" }
-- Returns err, fname, ftype
local function parse_fname(chars)
local string_builder = {}
local last_suffix = nil
while true do
local c = chars()
if c == nil or c == "\t" then
break
end
if last_suffix ~= nil then
-- This suffix wasn't it :)
table.insert(string_builder, last_suffix)
last_suffix = nil
end
if c == "\\" then
local next_c = chars()
if next_c == nil or next_c == "\t" then
-- `c` was a terminal backslash
last_suffix = "\\"
break
end
-- Convert escape sequence
if next_c == "\\" then
last_suffix = nil
table.insert(string_builder, "\\")
elseif ESCAPE_CHARS[next_c] ~= nil then
last_suffix = nil
table.insert(string_builder, ESCAPE_CHARS[next_c])
else
return string.format("Invalid escape sequence %s", vim.inspect(c .. next_c))
end
elseif is_suffix(c) then
last_suffix = c
else
table.insert(string_builder, c)
end
end
if #string_builder == 0 and last_suffix ~= nil then
table.insert(string_builder, last_suffix)
last_suffix = nil
end
if #string_builder > 0 then
local fname = table.concat(string_builder)
local ftype = suffix_to_ftype(last_suffix)
return nil, fname, ftype
else
return nil, nil, nil
end
end
-- Returns err, hash
local function parse_hash(chars)
local c = chars()
if c == nil then
-- Ended line before hash
return nil, nil
elseif c ~= "#" then
return string.format("Unexpected character %s after fname", vim.inspect(c))
end
local string_builder = {}
for _ = 1, M.HASH_LEN do
c = chars()
if c == nil then
return "Unexpected end of line in hash"
elseif not c:match("%x") then
return string.format("Invalid hash character %s", vim.inspect(c))
else
table.insert(string_builder, c)
end
end
return nil, tonumber(table.concat(string_builder), 16)
end
-- The language of valid dirbuf lines is regular, so normally I would use
-- regex. However, Lua's patterns cannot parse dirbuf lines because of escaping
-- and I want better error messages, so I parse lines by hand.
--
-- Returns err, hash, fname, ftype
function M.parse_line(line)
local chars = line:gmatch(".")
-- We throw away the error because if there's an error in parsing the hash,
-- we treat the whole thing as an fname
local _, hash = parse_hash(chars)
if hash == nil or chars() ~= "\t" then
hash = nil
chars = line:gmatch(".")
end
local err, fname, ftype = parse_fname(chars)
if err ~= nil then
return err
end
-- Ensure that we parsed the whole line
local c = chars()
if c ~= nil then
return string.format("Unexpected character %s after fname", vim.inspect(c))
end
return nil, hash, fname, ftype
end
function M.display_fs_entry(fs_entry)
local escaped = fs_entry.fname:gsub("\\", "\\\\")
for escape_char, unescaped in pairs(ESCAPE_CHARS) do
escaped = escaped:gsub(unescaped, "\\" .. escape_char)
end
return escaped .. ftype_to_suffix(fs_entry.ftype)
end
function M.write_fs_entries(fs_entries, track_fname)
local fname_line = nil
for lnum, fs_entry in ipairs(fs_entries) do
if fs_entry.fname == track_fname then
fname_line = lnum
break
end
end
local buf_lines = {}
for idx, fs_entry in ipairs(fs_entries) do
local hash = string.format("%08x", idx)
local display = M.display_fs_entry(fs_entry)
table.insert(buf_lines, "#" .. hash .. "\t" .. display)
end
return buf_lines, fname_line
end
return M

View File

@ -0,0 +1,103 @@
local M = {}
local function sort_default(left, right)
return left.fname:lower() < right.fname:lower()
end
local function sort_directories_first(left, right)
if left.ftype ~= right.ftype then
return left.ftype < right.ftype
else
return left.fname:lower() < right.fname:lower()
end
end
local CONFIG_SPEC = {
hash_padding = {
default = 2,
check = function(val)
if type(val) ~= "number" or math.floor(val) ~= val or val < 1 then
return "must be integer larger than 1"
end
end,
},
show_hidden = {
default = true,
check = function(val)
if type(val) ~= "boolean" then
return "must be boolean, received " .. type(val)
end
end,
},
sort_order = {
default = sort_default,
check = function(val)
if val == "default" then
return nil, sort_default
elseif val == "directories_first" then
return nil, sort_directories_first
elseif type(val) == "function" then
return nil, val
else
return 'must be one of "default", "directories_first", or function'
end
end,
},
write_cmd = {
default = "DirbufSync",
check = function(val)
if type(val) ~= "string" then
return "must be string, received " .. type(val)
end
end,
},
}
local user_config = {}
function M.update(opts)
local errors = {}
for option_name, spec in pairs(CONFIG_SPEC) do
local val = opts[option_name]
if val == nil then
-- Don't check unset options
user_config[option_name] = nil
else
local err, converted = spec.check(val)
if err ~= nil then
table.insert(errors, string.format("`%s` %s", option_name, err))
elseif converted == nil then
user_config[option_name] = val
else
user_config[option_name] = converted
end
end
end
local unknown_options = {}
for key, _ in pairs(opts) do
if CONFIG_SPEC[key] == nil then
table.insert(unknown_options, "`" .. key .. "`")
end
end
if #unknown_options > 0 then
table.insert(errors, table.concat(unknown_options, ", ") .. " not recognized")
end
return errors
end
function M.get(opt)
-- Ensure we don't typo options
if CONFIG_SPEC[opt] == nil then
error("Unrecognized option: " .. opt)
end
if user_config[opt] == nil then
return CONFIG_SPEC[opt].default
else
return user_config[opt]
end
end
return M

View File

@ -0,0 +1,322 @@
local api = vim.api
local uv = vim.loop
local config = require("dirbuf.config")
local M = {}
M.path_separator = package.config:sub(1, 1)
function M.is_hidden(fname)
return fname:sub(1, 1) == "."
end
function M.join_paths(...)
local string_builder = {}
for _, path in ipairs({ ... }) do
if path:sub(-1, -1) == M.path_separator then
path = path:sub(0, -2)
end
table.insert(string_builder, path)
end
return table.concat(string_builder, M.path_separator)
end
function M.is_directory(path)
return vim.fn.isdirectory(path) == 1
end
-- FTypes are taken from
-- https://github.com/tbastos/luv/blob/2fed9454ebb870548cef1081a1f8a3dd879c1e70/src/fs.c#L420-L430
--[[
local enum FType
"file"
"directory"
"link"
"fifo"
"socket"
"char"
"block"
end
local record FSEntry
fname: string
ftype: FType
path: string
end
--]]
M.FSEntry = {}
local FSEntry = M.FSEntry
function FSEntry.new(fname, parent, ftype)
return { fname = fname, path = M.join_paths(parent, fname), ftype = ftype }
end
function FSEntry.temp(ftype)
local temppath = vim.fn.tempname()
return {
-- XXX: This technically violates fname's assumption that it is alwaies a
-- simple name and not a path
fname = temppath,
path = temppath,
ftype = ftype,
}
end
function M.get_fs_entries(dir, show_hidden)
local fs_entries = {}
local handle, err, _ = uv.fs_scandir(dir)
if handle == nil then
return err
end
while true do
local fname, ftype = uv.fs_scandir_next(handle)
if fname == nil then
break
end
if show_hidden or not M.is_hidden(fname) then
table.insert(fs_entries, FSEntry.new(fname, dir, ftype))
end
end
table.sort(fs_entries, config.get("sort_order"))
return nil, fs_entries
end
M.plan = {}
M.actions = {}
local DEFAULT_FILE_MODE = tonumber("644", 8)
-- Directories have to be executable for you to chdir into them
local DEFAULT_DIR_MODE = tonumber("755", 8)
local function cp(src_path, dst_path, ftype)
if ftype == "directory" then
local ok, err, _ = uv.fs_mkdir(dst_path, DEFAULT_DIR_MODE)
if not ok then
return err
end
local handle = uv.fs_scandir(src_path)
while true do
local next_fname, next_ftype = uv.fs_scandir_next(handle)
if next_fname == nil then
break
end
err = cp(M.join_paths(src_path, next_fname), M.join_paths(dst_path, next_fname), next_ftype)
if err ~= nil then
return err
end
end
return nil
elseif ftype == "link" then
local src_points_to, err, _ = uv.fs_readlink(src_path)
if src_points_to == nil then
return err
end
local ok
ok, err, _ = uv.fs_symlink(src_points_to, dst_path)
if not ok then
return err
end
return nil
else
local ok, err, _ = uv.fs_copyfile(src_path, dst_path)
if not ok then
return err
end
return nil
end
end
local function rm(path, ftype)
if ftype == "directory" then
local handle = uv.fs_scandir(path)
while true do
local next_fname, next_ftype = uv.fs_scandir_next(handle)
if next_fname == nil then
break
end
local err = rm(M.join_paths(path, next_fname), next_ftype)
if err ~= nil then
return err
end
end
local ok, err, _ = uv.fs_rmdir(path)
if not ok then
return err
end
return nil
else
local ok, err, _ = uv.fs_unlink(path)
if not ok then
return err
end
return nil
end
end
local function mv(src_path, dst_path, ftype)
-- FIXME: This is a TOCTOU
if uv.fs_access(dst_path, "W") then
return string.format("'%s' already exists", dst_path)
end
local ok, err, err_type = uv.fs_rename(src_path, dst_path)
if not ok and err_type == "EXDEV" then
err = cp(src_path, dst_path, ftype)
if err ~= nil then
return err
end
err = rm(src_path, ftype)
if err ~= nil then
return err
end
elseif not ok then
return err
end
return nil
end
local function is_child_of(maybe_child, parent)
local exact_match = maybe_child == parent
local child_match = vim.startswith(maybe_child, parent .. M.path_separator)
return exact_match or child_match
end
-- `rename_loaded_buffers` finds all renamed buffers under `old_path` and
-- renames them to be under `new_path`.
local function rename_loaded_buffers(old_path, new_path)
for _, buf in ipairs(api.nvim_list_bufs()) do
if not api.nvim_buf_is_loaded(buf) then
goto continue
end
-- api.nvim_buf_get_name() returns absolute path so no post-processing
local buf_name = api.nvim_buf_get_name(buf)
if is_child_of(buf_name, old_path) then
api.nvim_buf_set_name(buf, new_path .. buf_name:sub(#old_path + 1))
-- We have to :write! normal files to avoid `E13: File exists (add ! to
-- override)` error when manually calling :write
if api.nvim_buf_get_option(buf, "buftype") == "" then
api.nvim_buf_call(buf, function()
vim.cmd("silent! write!")
end)
end
end
::continue::
end
end
-- `delete_loaded_buffers` finds all deleted buffers under `path` and replaces
-- them with their alternate buffer, or a [No Name] buffer if its alternate
-- buffer doesn't exist.
local function delete_loaded_buffers(path)
for _, buf in ipairs(api.nvim_list_bufs()) do
if not api.nvim_buf_is_loaded(buf) then
goto continue
end
-- api.nvim_buf_get_name() returns absolute path so no post-processing
local buf_name = api.nvim_buf_get_name(buf)
if is_child_of(buf_name, path) then
for _, win in ipairs(vim.fn.win_findbuf(buf)) do
api.nvim_win_call(win, function()
local altbuf = vim.fn.bufnr("#")
if api.nvim_buf_is_valid(altbuf) then
api.nvim_win_set_buf(win, altbuf)
else
vim.cmd("enew!")
end
end)
end
api.nvim_buf_delete(buf, { force = true })
end
::continue::
end
end
function M.plan.create(fs_entry)
return { type = "create", fs_entry = fs_entry }
end
function M.actions.create(args)
local fs_entry = args.fs_entry
-- FIXME: This is a TOCTOU
if uv.fs_access(fs_entry.path, "W") then
return string.format("'%s' already exists", fs_entry.ftype, fs_entry.path)
end
if fs_entry.ftype == "file" then
local fd, err = uv.fs_open(fs_entry.path, "w", DEFAULT_FILE_MODE)
if fd == nil then
return err
end
local ok
ok, err = uv.fs_close(fd)
if not ok then
return err
end
elseif fs_entry.ftype == "directory" then
local ok, err = uv.fs_mkdir(fs_entry.path, DEFAULT_DIR_MODE)
if not ok then
return err
end
else
return string.format("Cannot create %s", fs_entry.ftype)
end
return nil
end
function M.plan.copy(src_fs_entry, dst_fs_entry)
return { type = "copy", src_fs_entry = src_fs_entry, dst_fs_entry = dst_fs_entry }
end
function M.actions.copy(args)
local src_fs_entry, dst_fs_entry = args.src_fs_entry, args.dst_fs_entry
-- planner ensures src and dst have same ftype
return cp(src_fs_entry.path, dst_fs_entry.path, src_fs_entry.ftype)
end
function M.plan.delete(fs_entry)
return { type = "delete", fs_entry = fs_entry }
end
function M.actions.delete(args)
local fs_entry = args.fs_entry
local err = rm(fs_entry.path, fs_entry.ftype)
if err ~= nil then
return string.format("Delete %s: %s", fs_entry.path, err)
end
delete_loaded_buffers(fs_entry.path)
return nil
end
function M.plan.move(src_fs_entry, dst_fs_entry)
return { type = "move", src_fs_entry = src_fs_entry, dst_fs_entry = dst_fs_entry }
end
function M.actions.move(args)
local src_fs_entry, dst_fs_entry = args.src_fs_entry, args.dst_fs_entry
-- planner ensures src and dst have same ftype
local err = mv(src_fs_entry.path, dst_fs_entry.path, src_fs_entry.ftype)
if err ~= nil then
return string.format("Move failed for %s -> %s: %s", src_fs_entry.path, dst_fs_entry.path, err)
end
rename_loaded_buffers(src_fs_entry.path, dst_fs_entry.path)
return nil
end
return M

View File

@ -0,0 +1,213 @@
local buffer = require("dirbuf.buffer")
local fs = require("dirbuf.fs")
local FSEntry = fs.FSEntry
local create, copy, delete, move = fs.plan.create, fs.plan.copy, fs.plan.delete, fs.plan.move
local M = {}
--[[
local record Changes
new_files: {FSEntry},
change_map: {string: Change},
}
local record Change
{FSEntry} -- dst_fs_entries
current_fs_entry: FSEntry
stays: bool
progress: Progress
end
local enum Progress
"unhandled"
"handling"
"handled"
end
--]]
-- `build_changes` creates a diff between the snapshotted state of the
-- directory buffer `dirbuf` and the updated state of the directory buffer
-- `lines`.
--
-- TODO: It's kinda gross that I just store `lines` because then I have to deal
-- with parsing here, but I'm not sure of a better way to do it
--
-- Returns: err, changes
function M.build_changes(dir, fs_entries, lines)
local new_files = {}
local change_map = {}
for _, fs_entry in pairs(fs_entries) do
change_map[fs_entry.fname] = {
current_fs_entry = fs_entry,
stays = false,
handled = false,
}
end
-- No duplicate fnames
local used_fnames = {}
for lnum, line in ipairs(lines) do
local err, hash, fname, ftype = buffer.parse_line(line)
if err ~= nil then
return string.format("Line %d: %s", lnum, err)
end
if fname == nil then
goto continue
end
if used_fnames[fname] ~= nil then
return string.format("Line %d: Duplicate name '%s'", lnum, fname)
end
local dst_fs_entry = FSEntry.new(fname, dir, ftype)
if hash == nil then
table.insert(new_files, dst_fs_entry)
else
local current_fs_entry = fs_entries[hash]
if current_fs_entry.ftype ~= dst_fs_entry.ftype then
return string.format("line %d: cannot change %s -> %s", lnum, current_fs_entry.ftype, dst_fs_entry.ftype)
end
if current_fs_entry.fname == dst_fs_entry.fname then
change_map[current_fs_entry.fname].stays = true
else
table.insert(change_map[current_fs_entry.fname], dst_fs_entry)
end
end
used_fnames[dst_fs_entry.fname] = true
::continue::
end
return nil, { change_map = change_map, new_files = new_files }
end
-- TODO: Currently we don't always find the optimal unsticking point
-- Also, sorry this is hard to read...
local function resolve_change(plan, change_map, change)
if change.progress == "handled" then
return
elseif change.progress == "handling" then
error("unhandled cycle detected")
end
change.progress = "handling"
-- If there's a cycle, we need to "unstick" it by moving one file to a
-- temporary location. However, we need to remember to move that temporary
-- file back to where we want after everything else in the cycle has been
-- resolved.
--
-- It's not obvious that we can get away with only returning one action.
-- However, due to our guarantee that the `Changes` we're given only use each
-- `fname` once (i.e. the max in-degree of the graph of filename changes is
-- 1), we know that we can only ever have one cycle from any given starting
-- point.
local post_resolution_action = nil
-- If the file doesn't stay, we prevent an extra copy by moving the file
-- as the last change. We arbitrarily pick the first file to move it after
-- everything
local move_to = nil
local stuck_fs_entry = nil
for _, dst_fs_entry in ipairs(change) do
local dependent_change = change_map[dst_fs_entry.fname]
if dependent_change ~= nil then
if dependent_change.progress == "handling" then
-- We have a cycle, we need to unstick it
if stuck_fs_entry ~= nil then
error("my assumption about `stuck_change` was wrong")
end
-- We handle this later
stuck_fs_entry = dst_fs_entry
goto continue
else
-- We can handle the dependent_change directly
-- Double check that my assumption holds
local rtn = resolve_change(plan, change_map, dependent_change)
if rtn ~= nil and post_resolution_action ~= nil then
error("my assumption about `post_resolution_action` was wrong")
end
post_resolution_action = rtn
end
end
if not change.stays and move_to == nil then
move_to = dst_fs_entry
else
table.insert(plan, copy(change.current_fs_entry, dst_fs_entry))
end
::continue::
end
local gone = false
if move_to ~= nil then
table.insert(plan, move(change.current_fs_entry, move_to))
gone = true
end
if stuck_fs_entry ~= nil then
if move_to ~= nil then
-- We have a safe place to copy from
post_resolution_action = copy(move_to, stuck_fs_entry)
elseif change.stays then
-- We have a safe place to copy from
post_resolution_action = copy(change.current_fs_entry, stuck_fs_entry)
else
-- We have NO safe place to copy from and we don't stay, so move to a
-- temporary and then move again
local temp_fs_entry = FSEntry.temp(change.current_fs_entry.ftype)
table.insert(plan, move(change.current_fs_entry, temp_fs_entry))
post_resolution_action = move(temp_fs_entry, stuck_fs_entry)
gone = true
end
end
-- The file gets deleted and we never moved it, so we have to directly delete
-- it
if not change.stays and not gone then
table.insert(plan, delete(change.current_fs_entry))
end
change.progress = "handled"
return post_resolution_action
end
-- `determine_plan` finds the most efficient sequence of actions necessary to
-- apply the set of validated changes we have `changes`.
--
-- Returns: list of actions as in fs.plan
function M.determine_plan(changes)
local plan = {}
for _, change in pairs(changes.change_map) do
local extra_action = resolve_change(plan, changes.change_map, change)
if extra_action ~= nil then
table.insert(plan, extra_action)
end
end
for _, fs_entry in ipairs(changes.new_files) do
table.insert(plan, create(fs_entry))
end
return plan
end
-- `execute_plan` executes the plan (i.e. sequence of actions) as created by
-- `determine_plan` using the `fs.actions` action handlers.
--
-- Returns: err
function M.execute_plan(plan)
-- TODO: Make this async
for _, action in ipairs(plan) do
local err = fs.actions[action.type](action)
if err ~= nil then
return err
end
end
return nil
end
return M

View File

@ -0,0 +1,37 @@
if exists('g:loaded_dirbuf')
finish
endif
command! -nargs=? -complete=dir Dirbuf lua require'dirbuf'.open(<q-args>)
command! DirbufQuit lua require'dirbuf'.quit()
command! -nargs=? -complete=customlist,s:DirbufSyncOptions DirbufSync lua require'dirbuf'.sync(<q-args>)
function! s:DirbufSyncOptions(arg_lead, cmd_line, cursor_pos)
let options = ['-confirm', '-dry-run']
return filter(options, 'v:val =~ "^'.a:arg_lead.'"')
endfunction
" This (dirbuf_up) mapping was taken from vim-dirvish
noremap <unique> <Plug>(dirbuf_up) <cmd>execute 'Dirbuf %:p'.repeat(':h', v:count1 + isdirectory(expand('%')))<cr>
noremap <unique> <Plug>(dirbuf_enter) <cmd>execute 'lua require"dirbuf".enter()'<cr>
noremap <unique> <Plug>(dirbuf_toggle_hide) <cmd>execute 'lua require"dirbuf".toggle_hide()'<cr>
noremap <unique> <Plug>(dirbuf_history_forward) <cmd>execute 'lua require"dirbuf".jump_history('v:count1')'<cr>
noremap <unique> <Plug>(dirbuf_history_backward) <cmd>execute 'lua require"dirbuf".jump_history(-'v:count1')'<cr>
if mapcheck('-', 'n') ==# '' && !hasmapto('<Plug>(dirbuf_up)', 'n')
nmap - <Plug>(dirbuf_up)
endif
augroup dirbuf
autocmd!
" Makes editing a directory open a dirbuf. We always re-init the dirbuf
autocmd BufEnter * if isdirectory(expand('%')) && !&modified
\ | execute 'lua require"dirbuf".init_dirbuf(vim.b.dirbuf_history, vim.b.dirbuf_history_index, true)'
\ | endif
" Netrw hijacking for vim-plug and &rtp friends
autocmd VimEnter * if exists('#FileExplorer') | execute 'autocmd! FileExplorer *' | endif
augroup END
" Netrw hijacking for packer and packages friends
if exists('#FileExplorer') | execute 'autocmd! FileExplorer *' | endif
let g:loaded_dirbuf = 1

View File

@ -0,0 +1,2 @@
indent_type = "Spaces"
indent_width = 2

View File

@ -0,0 +1,64 @@
" # Regex Breakdown
"
" /^\([^\\\t]\|\\[\\t]\)\+$/
" ^^(a)^^ ^^(b)^^ (c)
" (a): all valid single-characters (i.e. not tabs or escape sequences).
" (b): all valid escape sequences.
" (c): suffix + $ (end of line)
"
" The longest regex is the one highlighted, so the suffix always controls the
" color. We include `me=e-suffix_len` to set the 'match end' to be one before
" the normal 'end' so the suffix doesn't get highlighted.
"
" The suffixes are taken from `ls --classify` and zsh's tab completion.
function! s:SetMatch(group_name, suffix, suffix_len)
execute 'syntax match 'a:group_name.' /\([^\\\t]\|\\[\\nt]\)\+'.a:suffix.'$/me=e-'.a:suffix_len
endfunction
call s:SetMatch('DirbufFile', '', 0)
call s:SetMatch('DirbufDirectory', '[/\\]', 1)
call s:SetMatch('DirbufLink', '@', 1)
call s:SetMatch('DirbufFifo', '|', 1)
call s:SetMatch('DirbufSocket', '=', 1)
call s:SetMatch('DirbufChar', '%', 1)
call s:SetMatch('DirbufBlock', '\\$', 1)
" We include `ms=s-1` to not highlight the tab
syntax match DirbufHash /^#\x\{8}\t/ms=s-1
" /^\(\(The_Regular_Expression\)\@!.\)*$/
" Finds every except for the regular expression
" See: https://vim.fandom.com/wiki/Search_for_lines_not_containing_pattern_and_other_helpful_searches#Searching_with_.2F
syntax match DirbufMalformedLine /^\(\(\_^\(#\x\{8}\t\)\?\([^\\\t]\|\\[\\nt]\)\+\\\?\_$\)\@!.\)*$/
" Highlight each object according to its color in by ls --color=always. This
" fallback system was taken and modified from nvim-tree.lua's colors.lua
function! s:SetColor(group_name, color_num, fallback_group, fallback_color)
if exists('g:terminal_color_'.a:color_num)
let l:color = get(g:, 'terminal_color_'.a:color_num)
execute 'highlight '.a:group_name.' ctermfg='.a:color_num.' gui=bold guifg='.l:color
return
endif
let l:id = v:lua.vim.api.nvim_get_hl_id_by_name(a:fallback_group)
let l:foreground = synIDattr(synIDtrans(id), "fg")
if l:foreground !=# ''
execute 'highlight '.a:group_name.' ctermfg='.a:color_num.' gui=bold guifg='.l:foreground
else
execute 'highlight '.a:group_name.' ctermfg='.a:color_num.' gui=bold guifg='.a:fallback_color
endif
endfunction
highlight link DifbufFile Normal
if exists('g:terminal_color_4')
execute 'highlight DirbufDirectory ctermfg=4 gui=bold guifg='.g:terminal_color_4
else
highlight link DirbufDirectory Directory
endif
call s:SetColor('DirbufLink', 6, 'Conditional', 'Cyan')
call s:SetColor('DirbufFifo', 2, 'Character', 'Green')
call s:SetColor('DirbufSocket', 5, 'Define', 'Purple')
call s:SetColor('DirbufChar', 3, 'PreProc', 'Yellow')
call s:SetColor('DirbufBlock', 3, 'PreProc', 'Yellow')
highlight link DirbufHash Special
highlight link DirbufMalformedLine Error

View File

@ -0,0 +1,220 @@
local buffer = require("dirbuf.buffer")
local fs = require("dirbuf.fs")
local function entry(fname, ftype)
return fs.FSEntry.new(fname, "", ftype or "file")
end
describe("parse_line", function()
local function expect_parse(line, expected)
local err, hash, fname, ftype = buffer.parse_line(line)
if expected.err then
assert.is_not_nil(err)
else
assert.is_nil(err)
end
assert.equal(expected.hash, hash, "hash")
assert.equal(expected.fname, fname, "fname")
assert.equal(expected.ftype, ftype, "ftype")
end
local function test_suffix(expected_ftype, suffix)
it("ftype " .. expected_ftype .. suffix, function()
expect_parse("#0000000a\tfoo" .. suffix, {
err = false,
hash = 10,
fname = "foo",
ftype = expected_ftype,
})
end)
end
test_suffix("file", "")
test_suffix("directory", "/")
test_suffix("directory", "\\")
test_suffix("link", "@")
test_suffix("fifo", "|")
test_suffix("socket", "=")
test_suffix("char", "%")
test_suffix("block", "#")
it("interior @", function()
expect_parse([[#0000000a foo@bar]], {
err = false,
hash = 10,
fname = "foo@bar",
ftype = "file",
})
end)
it("interior /", function()
expect_parse([[#0000000a foo/bar]], {
err = false,
hash = 10,
fname = "foo/bar",
ftype = "file",
})
end)
it("fname is @", function()
expect_parse([[@]], {
err = false,
hash = nil,
fname = "@",
ftype = "file",
})
end)
it("only fname", function()
expect_parse([[foo]], {
err = false,
hash = nil,
fname = "foo",
ftype = "file",
})
end)
it("only hash", function()
expect_parse([[#0000000a]], {
err = false,
hash = nil,
fname = "#0000000a",
ftype = "file",
})
end)
it("spaces", function()
expect_parse([[#0000000a a b c ]], {
err = false,
hash = 10,
fname = " a b c ",
ftype = "file",
})
end)
it("escaped tab", function()
expect_parse([[#0000000a before\tafter]], {
err = false,
hash = 10,
fname = [[before after]],
ftype = "file",
})
end)
it("escaped backslash", function()
expect_parse([[#0000000a before\\after]], {
err = false,
hash = 10,
fname = [[before\after]],
ftype = "file",
})
end)
it("escaped backslash end", function()
expect_parse([[#0000000a foo\\]], {
err = false,
hash = 10,
fname = [[foo\]],
ftype = "file",
})
end)
it("unescaped tab", function()
expect_parse([[#0000000a foo bar]], {
err = true,
hash = nil,
fname = nil,
ftype = nil,
})
end)
it("invalid escape sequence", function()
expect_parse([[#0000000a \y]], {
err = true,
hash = nil,
fname = nil,
ftype = nil,
})
end)
it("short hash", function()
expect_parse([[#0123456 foo]], {
err = true,
hash = nil,
fname = nil,
ftype = nil,
})
end)
it("long hash", function()
expect_parse([[#012345678 foo]], {
err = true,
hash = nil,
fname = nil,
ftype = nil,
})
end)
it("invalid hex character hash", function()
expect_parse([[#0123456z foo]], {
err = true,
hash = nil,
fname = nil,
ftype = nil,
})
end)
it("trailing spaces no hash", function()
expect_parse([[foo ]], {
err = false,
hash = nil,
fname = "foo ",
ftype = "file",
})
end)
it("non-ASCII fname", function()
expect_parse([[#0000000a 文档]], {
err = false,
hash = 10,
fname = "文档",
ftype = "file",
})
end)
end)
describe("write_fs_entries", function()
it("types", function()
local fs_entries = {
entry("file", "file"),
entry("directory", "directory"),
entry("link", "link"),
entry("fifo", "fifo"),
entry("socket", "socket"),
entry("char", "char"),
entry("block", "block"),
}
local buf_lines, _ = buffer.write_fs_entries(fs_entries)
assert.same({
"#00000001 file",
"#00000002 directory/",
"#00000003 link@",
"#00000004 fifo|",
"#00000005 socket=",
"#00000006 char%",
"#00000007 block#",
}, buf_lines)
end)
it("escape characters", function()
local fs_entries = { entry("a\\\t") }
local buf_lines, _ = buffer.write_fs_entries(fs_entries)
assert.same({ [[#00000001 a\\\t]] }, buf_lines)
end)
it("track_fname", function()
local fs_entries = { entry("a"), entry("b"), entry("c") }
local _, fname_line = buffer.write_fs_entries(fs_entries, "b")
assert.equal(2, fname_line)
end)
end)

View File

@ -0,0 +1,37 @@
local config = require("dirbuf.config")
describe("update", function()
it("legal", function()
local errors = config.update({
hash_padding = 3,
show_hidden = false,
sort_order = "directories_first",
})
assert.equal(0, #errors)
assert.equal(3, config.get("hash_padding"))
assert.equal(false, config.get("show_hidden"))
end)
it("illegal", function()
local errors = config.update({
hash_padding = -1,
show_hidden = "foo",
sort_order = {},
unknown = true,
})
assert.equal(4, #errors)
end)
it("set then unset", function()
config.update({ hash_padding = 3 })
assert.equal(3, config.get("hash_padding"))
config.update({})
assert.equal(2, config.get("hash_padding"))
end)
it("unknown option", function()
assert.errors(function()
config.get("unknown")
end)
end)
end)

View File

@ -0,0 +1,108 @@
local api = vim.api
local uv = vim.loop
local function scan_directory(path)
local directory = {}
local handle = assert(uv.fs_scandir(path))
while true do
local fname, ftype = uv.fs_scandir_next(handle)
if fname == nil then
break
end
directory[fname] = ftype
end
return directory
end
local function lines()
return api.nvim_buf_get_lines(0, 0, -1, true)
end
local function expect_lines(expected)
assert.same(expected, lines())
end
local function expect_directory(expected)
local path = vim.api.nvim_buf_get_name(0)
assert.same(expected, scan_directory(path))
end
local function open_dirbuf_of(directory)
local path = assert(uv.fs_mkdtemp("/tmp/dirbuf-XXXXXX"))
for fname, ftype in pairs(directory) do
if ftype == "directory" then
vim.fn.mkdir(path .. "/" .. fname)
elseif ftype == "file" then
vim.fn.writefile({ "file " .. fname }, path .. "/" .. fname)
else
error("unrecognized ftype: " .. ftype)
end
end
vim.cmd("Dirbuf " .. vim.fn.fnameescape(path))
end
-- TODO: Use feed
local function feed(keys)
vim.fn.feedkeys(keys, "x")
end
describe("end-to-end", function()
it("edits", function()
open_dirbuf_of({ a = "file", b = "file", c = "directory" })
expect_lines({ "#00000001\ta", "#00000002\tb", "#00000003\tc/" })
vim.cmd("g/b/d")
vim.cmd("s/c/d/")
api.nvim_put({ "new file" }, "l", "p", true)
api.nvim_put({ "new directory/" }, "l", "p", true)
expect_lines({ "#00000001\ta", "#00000003\td/", "new file", "new directory/" })
expect_directory({ a = "file", b = "file", c = "directory" })
vim.cmd("DirbufSync")
expect_lines({ "#00000001\ta", "#00000002\td/", "#00000003\tnew directory/", "#00000004\tnew file" })
expect_directory({ a = "file", d = "directory", ["new file"] = "file", ["new directory"] = "directory" })
end)
it("escape characters", function()
open_dirbuf_of({ ["\\hello\n\t"] = "file", normal = "file" })
expect_lines({ [[#00000001 \\hello\n\t]], [[#00000002 normal]] })
vim.cmd("s/hello/goodbye/")
vim.cmd("DirbufSync")
expect_lines({ [[#00000001 \\goodbye\n\t]], [[#00000002 normal]] })
end)
it("jump_history()", function()
open_dirbuf_of({ a = "directory", b = "file" })
expect_lines({ [[#00000001 a/]], [[#00000002 b]] })
require("dirbuf").enter()
expect_lines({ "" })
require("dirbuf").jump_history(-1)
expect_lines({ [[#00000001 a/]], [[#00000002 b]] })
require("dirbuf").jump_history(1)
expect_lines({ "" })
end)
it(":DirbufSync -confirm smoke test", function()
open_dirbuf_of({ a = "file", b = "file", c = "file" })
expect_lines({ "#00000001\ta", "#00000002\tb", "#00000003\tc" })
vim.cmd("g/b/d")
expect_lines({ "#00000001\ta", "#00000003\tc" })
expect_directory({ a = "file", b = "file", c = "file" })
vim.cmd("DirbufSync -confirm")
end)
it(":DirbufSync unrecognized option", function()
assert.errors(function()
vim.cmd("Dirbuf")
vim.cmd("DirbufSync -some-fake-option")
end)
vim.cmd("bdelete!")
end)
-- TODO: Figure out how to trigger
-- https://github.com/elihunter173/dirbuf.nvim/issues/48 on commit e004455
pending("works with autochdir", function()
vim.opt.autochdir = true
feed("-")
assert.is_not.same({ "" }, lines())
vim.opt.autochdir = false
end)
end)

View File

@ -0,0 +1,187 @@
local buffer = require("dirbuf.buffer")
local fs = require("dirbuf.fs")
local planner = require("dirbuf.planner")
local function mkplan(before, after)
local fake_fs = {}
local before_fs_entries = {}
for _, line in ipairs(before) do
local err, hash, fname, ftype = buffer.parse_line(line)
assert(err == nil, err)
fake_fs["/" .. fname] = fname
before_fs_entries[hash] = fs.FSEntry.new(fname, "/", ftype)
end
local err, changes = planner.build_changes("/", before_fs_entries, after)
assert(err == nil, err)
local plan = planner.determine_plan(changes)
return fake_fs, plan
end
local function apply_plan(fake_fs, plan)
for _, action in ipairs(plan) do
if action.type == "create" then
fake_fs[action.fs_entry.path] = ""
elseif action.type == "copy" then
fake_fs[action.dst_fs_entry.path] = fake_fs[action.src_fs_entry.path]
elseif action.type == "delete" then
fake_fs[action.fs_entry.path] = nil
elseif action.type == "move" then
fake_fs[action.dst_fs_entry.path] = fake_fs[action.src_fs_entry.path]
fake_fs[action.src_fs_entry.path] = nil
end
end
end
local function opcount(plan, op)
local count = 0
for _, action in ipairs(plan) do
if action.type == op then
count = count + 1
end
end
return count
end
describe("determine_plan", function()
it("no changes", function()
local fake_fs, plan = mkplan({
[[#0000000a a]],
[[#0000000b b]],
}, {
[[#0000000a a]],
[[#0000000b b]],
})
apply_plan(fake_fs, plan)
assert.same({ ["/a"] = "a", ["/b"] = "b" }, fake_fs)
assert.same(0, #plan)
end)
it("reordering", function()
local fake_fs, plan = mkplan({
[[#0000000a a]],
[[#0000000b b]],
}, {
[[#0000000b b]],
[[#0000000a a]],
})
apply_plan(fake_fs, plan)
assert.same({ ["/a"] = "a", ["/b"] = "b" }, fake_fs)
assert.same(0, #plan)
end)
it("rename", function()
local fake_fs, plan = mkplan({
[[#0000000a a]],
[[#0000000b b]],
}, {
[[#0000000a c]],
[[#0000000b b]],
})
apply_plan(fake_fs, plan)
assert.same({ ["/c"] = "a", ["/b"] = "b" }, fake_fs)
assert.same(1, #plan)
end)
it("delete", function()
local fake_fs, plan = mkplan({
[[#0000000a a]],
[[#0000000b b]],
}, {
[[#0000000b b]],
})
apply_plan(fake_fs, plan)
assert.same({ ["/b"] = "b" }, fake_fs)
assert.same(1, #plan)
end)
it("create", function()
local fake_fs, plan = mkplan({
[[#0000000b b]],
}, {
[[a]],
[[#0000000b b]],
})
apply_plan(fake_fs, plan)
assert.same({ ["/a"] = "", ["/b"] = "b" }, fake_fs)
assert.same(1, #plan)
end)
it("copy", function()
local fake_fs, plan = mkplan({
[[#0000000a a]],
[[#0000000b b]],
}, {
[[#0000000a a]],
[[#0000000a c]],
[[#0000000b b]],
})
apply_plan(fake_fs, plan)
assert.same({ ["/a"] = "a", ["/b"] = "b", ["/c"] = "a" }, fake_fs)
assert.same(1, #plan)
end)
it("dependent rename", function()
local fake_fs, plan = mkplan({
[[#0000000a a]],
[[#0000000b b]],
}, {
[[#0000000a b]],
[[#0000000b c]],
})
apply_plan(fake_fs, plan)
assert.same({ ["/b"] = "a", ["/c"] = "b" }, fake_fs)
assert.same(2, #plan)
assert.same(2, opcount(plan, "move"))
end)
it("swap", function()
local fake_fs, plan = mkplan({
[[#0000000a a]],
[[#0000000b b]],
}, {
[[#0000000a b]],
[[#0000000b a]],
})
apply_plan(fake_fs, plan)
assert.same({ ["/a"] = "b", ["/b"] = "a" }, fake_fs)
assert.same(3, #plan)
assert.same(3, opcount(plan, "move"))
end)
-- FIXME: We skip the "efficient breakpoint" efficiency tests because Dirbuf
-- sometimes misses efficient breakpoints. Dirbuf's solutions are always
-- correct but not always optimal.
it("swap with efficient breakpoint", function()
local fake_fs, plan = mkplan({
[[#0000000a a]],
[[#0000000b b]],
}, {
[[#0000000a b]],
[[#0000000b a]],
[[#0000000b c]],
})
apply_plan(fake_fs, plan)
assert.same({ ["/a"] = "b", ["/b"] = "a", ["/c"] = "b" }, fake_fs)
-- assert.same(3, #plan)
-- assert.same(3, opcount(plan, "move"))
end)
it("cycle with efficient breakpoint", function()
local fake_fs, plan = mkplan({
[[#0000000a a]],
[[#0000000b b]],
[[#0000000c c]],
}, {
[[#0000000a b]],
[[#0000000b c]],
[[#0000000b d]],
[[#0000000c a]],
})
apply_plan(fake_fs, plan)
assert.same({ ["/a"] = "c", ["/b"] = "a", ["/c"] = "b", ["/d"] = "b" }, fake_fs)
-- assert.same(4, #plan)
-- assert.same(3, opcount(plan, "move"))
-- assert.same(1, opcount(plan, "copy"))
end)
end)

View File

@ -0,0 +1,8 @@
if !isdirectory('plenary.nvim')
!git clone https://github.com/nvim-lua/plenary.nvim.git plenary.nvim
!git -C plenary.nvim reset --hard 1338bbe8ec6503ca1517059c52364ebf95951458
endif
set runtimepath+=plenary.nvim,.
runtime plugin/plenary.vim
try | runtime plugin/dirbuf.vim | catch | cquit! 173 | endtry
command Test PlenaryBustedDirectory tests/ {minimal_init = 'tests/test_init.vim'}

View File

@ -0,0 +1,319 @@
# filetype.nvim
Easily speed up your neovim startup time!
## What does this do?
This plugin is a replacement for the included `filetype.vim` that is sourced on startup.
The purpose of that file is to create a series of autocommands that set the `filetype` variable
depending on the filename. The issue is that creating autocommands have significant overhead, and
creating [800+ of them](https://github.com/vim/vim/blob/master/runtime/filetype.vim) as `filetype.vim` does is a very inefficient way to get the job done.
As you can see, `filetype.vim` is by far the heaviest nvim runtime file
```diff
13.782 [runtime]
- 9.144 /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/filetype.vim
1.662 /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/matchit.vim
0.459 /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/syntax/synload.vim
0.388 /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/netrwPlugin.vim
0.334 /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/gzip.vim
0.251 /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/rplugin.vim
0.248 /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/syntax/syntax.vim
0.216 /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/tarPlugin.vim
0.205 /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/zipPlugin.vim
0.186 /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/syntax/syncolor.vim
0.173 /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/matchparen.vim
0.123 /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/shada.vim
0.114 /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/tohtml.vim
0.075 /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/man.vim
0.056 /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/ftplugin.vim
0.048 /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/indent.vim
0.039 /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/spellfile.vim
0.038 /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/tutor.vim
0.022 /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/health.vim
```
`filetype.nvim` fixes the issue by only creating a single autocommand that resolves the file type
when a buffer is opened. This method is ~175x faster\*!
## Usage
First, install using your favorite package manager. Using [packer](https://github.com/wbthomason/packer.nvim):
```lua
use("nathom/filetype.nvim")
```
If using a Neovim version earlier than 0.6.0, add the following to `init.lua`
```lua
-- Do not source the default filetype.vim
vim.g.did_load_filetypes = 1
```
That's it! You should now have a much snappier neovim experience!
## Customization
`filetype.nvim` allows you to easily add custom filetypes using the `setup` function. Here's an example:
```lua
-- In init.lua or filetype.nvim's config file
require("filetype").setup({
overrides = {
extensions = {
-- Set the filetype of *.pn files to potion
pn = "potion",
},
literal = {
-- Set the filetype of files named "MyBackupFile" to lua
MyBackupFile = "lua",
},
complex = {
-- Set the filetype of any full filename matching the regex to gitconfig
[".*git/config"] = "gitconfig", -- Included in the plugin
},
-- The same as the ones above except the keys map to functions
function_extensions = {
["cpp"] = function()
vim.bo.filetype = "cpp"
-- Remove annoying indent jumping
vim.bo.cinoptions = vim.bo.cinoptions .. "L0"
end,
["pdf"] = function()
vim.bo.filetype = "pdf"
-- Open in PDF viewer (Skim.app) automatically
vim.fn.jobstart(
"open -a skim " .. '"' .. vim.fn.expand("%") .. '"'
)
end,
},
function_literal = {
Brewfile = function()
vim.cmd("syntax off")
end,
},
function_complex = {
["*.math_notes/%w+"] = function()
vim.cmd("iabbrev $ $$")
end,
},
shebang = {
-- Set the filetype of files with a dash shebang to sh
dash = "sh",
},
},
})
```
The `extensions` and `literal` tables are orders faster than the other ones
because they only require a table lookup. Always try to use these before resorting
to the `complex` tables, which require looping over the entries and running
a regex for each one.
## Performance Comparison
**These were measured using [startuptime.vim](https://github.com/tweekmonster/startuptime.vim)**
### Without `filetype.nvim`
Average startup time (100 rounds): **36.410 ms**
<details>
<summary>Sample log</summary>
```diff
times in msec
clock self+sourced self: sourced script
clock elapsed: other lines
000.008 000.008: --- NVIM STARTING ---
000.827 000.819: locale set
001.304 000.477: inits 1
001.358 000.054: window checked
001.369 000.011: parsing arguments
002.537 001.168: expanding arguments
002.626 000.089: inits 2
002.998 000.372: init highlight
012.731 000.961 000.961: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/vim-gruvbox8/colors/gruvbox8.vim
012.829 009.549 008.588: sourcing /Users/nathan/.config/nvim/init.lua
012.837 000.290: sourcing vimrc file(s)
019.775 000.035 000.035: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/elixir.vim
019.867 000.026 000.026: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/fish.vim
019.949 000.022 000.022: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/gdresource.vim
020.025 000.017 000.017: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/gdscript.vim
020.108 000.018 000.018: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/gomod.vim
020.194 000.029 000.029: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/graphql.vim
020.280 000.029 000.029: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/hcl.vim
020.358 000.021 000.021: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/heex.vim
020.436 000.021 000.021: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/json5.vim
020.517 000.024 000.024: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/julia.vim
020.601 000.028 000.028: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/ledger.vim
020.680 000.022 000.022: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/nix.vim
020.764 000.028 000.028: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/ql.vim
020.851 000.031 000.031: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/query.vim
020.933 000.025 000.025: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/surface.vim
021.127 000.031 000.031: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/teal.vim
021.218 000.025 000.025: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/tlaplus.vim
021.301 000.023 000.023: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/yang.vim
021.382 000.023 000.023: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/zig.vim
- 022.213 009.200 008.722: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/filetype.vim
022.820 000.046 000.046: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/ftplugin.vim
023.350 000.042 000.042: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/indent.vim
025.075 000.180 000.180: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/syntax/syncolor.vim
026.263 001.786 001.606: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/vim-gruvbox8/colors/gruvbox8.vim
026.338 002.204 000.418: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/syntax/synload.vim
026.432 002.447 000.243: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/syntax/syntax.vim
030.711 000.317 000.317: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/gzip.vim
030.810 000.021 000.021: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/health.vim
030.951 000.074 000.074: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/man.vim
032.470 000.187 000.187: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/pack/dist/opt/matchit/plugin/matchit.vim
032.781 001.760 001.573: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/matchit.vim
033.095 000.240 000.240: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/matchparen.vim
033.539 000.364 000.364: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/netrwPlugin.vim
033.873 000.021 000.021: sourcing /Users/nathan/.local/share/nvim/rplugin.vim
033.883 000.251 000.231: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/rplugin.vim
034.065 000.106 000.106: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/shada.vim
034.185 000.036 000.036: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/spellfile.vim
034.472 000.205 000.205: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/tarPlugin.vim
034.664 000.104 000.104: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/tohtml.vim
034.781 000.034 000.034: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/tutor.vim
035.048 000.178 000.178: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/zipPlugin.vim
042.395 000.030 000.030: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/opt/vim-markdown/ftdetect/markdown.vim
042.409 007.066 007.036: sourcing /Users/nathan/.config/nvim/plugin/packer_compiled.lua
043.195 007.867: loading plugins
043.813 000.037 000.037: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/easy-replace.nvim/plugin/easy_replace.vim
044.564 000.032 000.032: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-bqf/plugin/bqf.vim
046.955 001.984 001.984: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/plugin/nvim-treesitter.vim
047.595 000.050 000.050: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/elixir.vim
047.693 000.030 000.030: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/fish.vim
047.851 000.092 000.092: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/gdresource.vim
047.978 000.026 000.026: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/gdscript.vim
048.082 000.026 000.026: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/gomod.vim
048.183 000.031 000.031: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/graphql.vim
048.284 000.031 000.031: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/hcl.vim
048.378 000.024 000.024: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/heex.vim
048.470 000.023 000.023: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/json5.vim
048.562 000.022 000.022: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/julia.vim
048.659 000.027 000.027: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/ledger.vim
048.749 000.021 000.021: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/nix.vim
048.842 000.024 000.024: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/ql.vim
048.943 000.032 000.032: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/query.vim
049.035 000.019 000.019: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/surface.vim
049.115 000.018 000.018: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/teal.vim
049.197 000.017 000.017: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/tlaplus.vim
049.276 000.017 000.017: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/yang.vim
049.390 000.017 000.017: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/zig.vim
049.772 000.047 000.047: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-web-devicons/plugin/nvim-web-devicons.vim
050.319 000.043 000.043: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/plenary.nvim/plugin/plenary.vim
051.424 000.301 000.301: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/vim-rooter/plugin/rooter.vim
051.751 005.565: loading packages
052.307 000.556: loading after plugins
052.316 000.010: inits 3
052.328 000.012: clearing screen
054.268 001.940: opening buffers
054.539 000.271: BufEnter autocommands
- 054.542 000.003: editing files in windows
```
</details>
### With `filetype.nvim`
Average startup time (100 rounds): **26.492 ms**
<details>
<summary>Sample log</summary>
```diff
times in msec
clock self+sourced self: sourced script
clock elapsed: other lines
000.008 000.008: --- NVIM STARTING ---
000.813 000.805: locale set
001.282 000.470: inits 1
001.334 000.052: window checked
001.345 000.011: parsing arguments
002.386 001.041: expanding arguments
002.459 000.073: inits 2
002.859 000.400: init highlight
013.346 001.066 001.066: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/vim-gruvbox8/colors/gruvbox8.vim
013.471 010.343 009.276: sourcing /Users/nathan/.config/nvim/init.lua
013.485 000.283: sourcing vimrc file(s)
+ 013.666 000.025 000.025: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/filetype.vim
014.360 000.057 000.057: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/ftplugin.vim
014.993 000.043 000.043: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/indent.vim
016.715 000.168 000.168: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/syntax/syncolor.vim
017.849 001.667 001.499: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/vim-gruvbox8/colors/gruvbox8.vim
017.932 002.321 000.654: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/syntax/synload.vim
018.025 002.551 000.230: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/syntax/syntax.vim
021.955 000.187 000.187: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/gzip.vim
022.056 000.021 000.021: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/health.vim
022.175 000.047 000.047: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/man.vim
023.777 000.207 000.207: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/pack/dist/opt/matchit/plugin/matchit.vim
024.039 001.791 001.584: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/matchit.vim
024.276 000.164 000.164: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/matchparen.vim
024.668 000.318 000.318: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/netrwPlugin.vim
024.992 000.017 000.017: sourcing /Users/nathan/.local/share/nvim/rplugin.vim
025.001 000.245 000.228: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/rplugin.vim
025.153 000.077 000.077: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/shada.vim
025.270 000.035 000.035: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/spellfile.vim
025.469 000.118 000.118: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/tarPlugin.vim
025.719 000.163 000.163: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/tohtml.vim
025.834 000.031 000.031: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/tutor.vim
026.077 000.169 000.169: sourcing /usr/local/Cellar/neovim/0.5.0/share/nvim/runtime/plugin/zipPlugin.vim
033.400 000.027 000.027: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/opt/vim-markdown/ftdetect/markdown.vim
033.411 007.043 007.016: sourcing /Users/nathan/.config/nvim/plugin/packer_compiled.lua
034.214 007.645: loading plugins
034.853 000.030 000.030: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/easy-replace.nvim/plugin/easy_replace.vim
+ 035.412 000.022 000.022: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/filetype.nvim/plugin/filetype.vim
036.064 000.027 000.027: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-bqf/plugin/bqf.vim
038.325 001.867 001.867: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/plugin/nvim-treesitter.vim
038.937 000.037 000.037: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/elixir.vim
039.039 000.032 000.032: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/fish.vim
039.132 000.023 000.023: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/gdresource.vim
039.284 000.023 000.023: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/gdscript.vim
039.427 000.022 000.022: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/gomod.vim
039.523 000.028 000.028: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/graphql.vim
039.620 000.030 000.030: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/hcl.vim
039.711 000.023 000.023: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/heex.vim
039.800 000.022 000.022: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/json5.vim
039.888 000.021 000.021: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/julia.vim
039.983 000.029 000.029: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/ledger.vim
040.075 000.026 000.026: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/nix.vim
040.169 000.025 000.025: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/ql.vim
040.271 000.035 000.035: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/query.vim
040.362 000.024 000.024: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/surface.vim
040.455 000.027 000.027: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/teal.vim
040.547 000.025 000.025: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/tlaplus.vim
040.638 000.025 000.025: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/yang.vim
040.731 000.027 000.027: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-treesitter/ftdetect/zig.vim
041.143 000.047 000.047: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/nvim-web-devicons/plugin/nvim-web-devicons.vim
041.688 000.042 000.042: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/plenary.nvim/plugin/plenary.vim
042.618 000.203 000.203: sourcing /Users/nathan/.local/share/nvim/site/pack/packer/start/vim-rooter/plugin/rooter.vim
042.980 006.026: loading packages
043.533 000.553: loading after plugins
043.543 000.010: inits 3
043.554 000.011: clearing screen
045.378 001.823: opening buffers
045.676 000.298: BufEnter autocommands
+ 045.679 000.003: editing files in windows
```
</details>
\* The time my machine takes to source the file goes from 9.1 ms to (0.022 + 0.03) ms, which is a 175x speedup.
## Contributions
All contributions are appreciated! But please make sure to follow these guidelines:
- Format your code with stylua, complying with the rules in the `stylua.toml` file
- Document any new functions you write, and update the documentation of functions
you edit if appropriate
- Set the base branch to `dev`

View File

@ -0,0 +1,6 @@
let g:did_load_filetypes = 1
augroup filetypedetect
au!
au BufNewFile,BufRead * lua require('filetype').resolve()
augroup END

View File

@ -0,0 +1,218 @@
-- generate the filetype
local custom_map = nil
-- Lua implementation of the setfiletype builtin function.
-- See :help setf
local function setf(filetype)
if vim.fn.did_filetype() == 0 then
vim.bo.filetype = filetype
end
end
local function set_filetype(name)
if type(name) == "string" then
setf(name)
return true
elseif type(name) == "function" then
local result = name()
if type(result) == "string" then
setf(result)
return true
end
end
return false
end
if vim.g.ft_ignore_pat == nil then
vim.g.ft_ignore_pat = [[\.\(Z\|gz\|bz2\|zip\|tgz\)$]]
end
local ft_ignore_regex = vim.regex(vim.g.ft_ignore_pat)
local function star_set_filetype(name)
if not ft_ignore_regex:match_str(name) then
return set_filetype(name)
end
return false
end
-- Loop through the regex-filetype pairs in the map table
-- and check if absolute_path matches any of them
-- Returns true if the filetype was set
local function try_regex(absolute_path, maps, star_set)
if maps == nil then
return false
end
for regexp, ft in pairs(maps) do
if absolute_path:find(regexp) then
if star_set then
if star_set_filetype(ft) then
return true
end
else
set_filetype(ft)
return true
end
end
end
return false
end
local function try_lookup(query, map)
if query == nil or map == nil then
return false
end
if map[query] ~= nil then
set_filetype(map[query])
return true
end
return false
end
-- Check the first line in the buffer for a shebang
-- If there is one, set the filetype appropriately
local function analyze_shebang()
local fstline = vim.api.nvim_buf_get_lines(0, 0, 1, true)[1]
if fstline then
return fstline:match("#!%s*/usr/bin/env%s+(%S+)")
or fstline:match("#!%s*/%S+/([^ /]+)")
end
return false
end
-- Return the value of map.shebang[s]; that is the value of the field indexed
-- by the value of s in map.shebang. This could be nil.
local function shebang_from_map(s, map)
-- Avoid indexing nil.
if map and map.shebang then
return map.shebang[s]
end
return false
end
local M = {}
function M.setup(opts)
if opts.overrides then
custom_map = opts.overrides
end
end
function M.resolve()
-- Just in case
vim.g.did_load_filetypes = 1
local absolute_path = vim.api.nvim_buf_get_name(0)
if vim.bo.filetype == "bqfpreview" then
absolute_path = vim.fn.expand("<amatch>")
end
if #absolute_path == 0 then
return
end
local filename = absolute_path:match(".*[\\/](.*)")
local ext = filename:match(".+%.(%w+)")
-- Try to match the custom defined filetypes
if custom_map ~= nil then
-- Avoid indexing nil
if try_lookup(ext, custom_map.extensions) then
return
end
if try_lookup(filename, custom_map.literal) then
return
end
if try_lookup(ext, custom_map.function_extensions) then
return
end
if try_lookup(filename, custom_map.function_literal) then
return
end
if try_regex(absolute_path, custom_map.endswith) then
return
end
if try_regex(absolute_path, custom_map.complex) then
return
end
if try_regex(absolute_path, custom_map.function_complex) then
return
end
if try_regex(absolute_path, custom_map.star_sets, true) then
return
end
-- if try_filetype_map(absolute_path, filename, ext, custom_map) then
-- return
-- end
end
local extension_map = require("filetype.mappings.extensions")
if try_lookup(ext, extension_map) then
return
end
local literal_map = require("filetype.mappings.literal")
if try_lookup(filename, literal_map) then
return
end
local function_maps = require("filetype.mappings.function")
if try_lookup(ext, function_maps.extensions) then
return
end
if try_lookup(filename, function_maps.literal) then
return
end
if try_regex(absolute_path, function_maps.complex) then
return
end
local complex_maps = require("filetype.mappings.complex")
if try_regex(absolute_path, complex_maps.endswith) then
return
end
if try_regex(absolute_path, complex_maps.complex) then
return
end
if try_regex(absolute_path, complex_maps.star_sets, true) then
return
end
-- At this point, no filetype has been detected
-- so let's just default to the extension, if it has one
if ext then
set_filetype(ext)
return
end
-- If there is no extension, look for a shebang and set the filetype to
-- that. Look for a shebang override in custom_map first. If there is none,
-- check the default shebangs defined in function_maps. Otherwise, default
-- to setting the filetype to the value of shebang itself.
local shebang = analyze_shebang()
if shebang then
shebang = shebang_from_map(shebang, custom_map)
or function_maps.shebang[shebang]
or shebang
set_filetype(shebang)
local mapped_shebang
if custom_map and custom_map.shebang then
mapped_shebang = custom_map.shebang[shebang]
end
mapped_shebang = mapped_shebang
or function_maps.shebang[shebang]
or shebang
set_filetype(mapped_shebang)
end
end
return M

View File

@ -0,0 +1,181 @@
local M = {}
-- mapping of lua regex to filetype
M.endswith = {
["/%.aptitude/config$"] = "aptconf",
["/%.config/git/config$"] = "gitconfig",
["/%.gnupg/gpg.conf$"] = "gpg",
["/%.gnupg/options$"] = "gpg",
["/%.icewm/menu$"] = "icemenu",
["/%.libao$"] = "libao",
["/%.mplayer/config$"] = "mplayerconf",
["/%.pinforc$"] = "pinfo",
["/%.ssh/config$"] = "sshconfig",
["/boot/grub/grub%.conf$"] = "grub",
["/boot/grub/menu%.lst$"] = "grub",
["/debian/control$"] = "debcontrol",
["/debian/copyright$"] = "debcopyright",
["/etc/DIR_COLORS$"] = "dircolors",
["/etc/a2ps%.cfg$"] = "a2ps",
["/etc/aliases$"] = "mailaliases",
["/etc/apt/sources%.list$"] = "debsources",
["/etc/asound%.conf$"] = "alsaconf",
["/etc/blkid%.tab$"] = "xml",
["/etc/blkid%.tab.old$"] = "xml",
["/etc/cdrdao%.conf$"] = "cdrdaoconf",
["/etc/conf%.modules$"] = "modconf",
["/etc/default/cdrdao$"] = "cdrdaoconf",
["/etc/defaults/cdrdao$"] = "cdrdaoconf",
["/etc/dnsmasq%.conf$"] = "dnsmasq",
["/etc/grub%.conf$"] = "grub",
["/etc/host%.conf$"] = "hostconf",
["/etc/hosts%.allow$"] = "hostsaccess",
["/etc/hosts%.deny$"] = "hostsaccess",
["/etc/libao%.conf$"] = "libao",
["/etc/limits$"] = "limits",
["/etc/login%.access$"] = "loginaccess",
["/etc/login%.defs$"] = "logindefs",
["/etc/mail/aliases$"] = "mailaliases",
["/etc/man%.conf$"] = "manconf",
["/etc/modules$"] = "modconf",
["/etc/modules%.conf$"] = "modconf",
["/etc/nanorc$"] = "nanorc",
["/etc/pacman%.conf$"] = "dosini",
["/etc/pam%.conf$"] = "pamconf",
["/etc/pinforc$"] = "pinfo",
["/etc/protocols$"] = "protocols",
["/etc/sensors%.conf$"] = "sensors",
["/etc/sensors3%.conf$"] = "sensors",
["/etc/serial%.conf$"] = "setserial",
["/etc/services$"] = "services",
["/etc/slp%.conf$"] = "slpconf",
["/etc/slp%.reg$"] = "slpreg",
["/etc/slp%.spi$"] = "slpspi",
["/etc/sudoers$"] = "sudoers",
["/etc/sysctl%.conf$"] = "sysctl",
["/etc/udev/cdsymlinks%.conf$"] = "sh",
["/etc/udev/udev%.conf$"] = "udevconf",
["/etc/updatedb%.conf$"] = "updatedb",
["/etc/xinetd%.conf$"] = "xinetd",
["/etc/yum%.conf$"] = "dosini",
["/etc/zprofile$"] = "zsh",
["/usr/share/alsa/alsa%.conf$"] = "alsaconf",
["Xmodmap$"] = "xmodmap",
["bsd$"] = "bsdl",
["esmtprc$"] = "esmtprc",
["hgrc$"] = "cfg",
["lftp/rc$"] = "lftp",
["lpe$"] = "dracula",
["lvs$"] = "dracula",
}
M.complex = {
["%.tmux.*%.conf"] = "tmux",
[".*%.git/modules/.*/config"] = "gitconfig",
[".*git/config"] = "gitconfig",
[".*/%.config/systemd/user/.*%.d/.*%.conf"] = "systemd",
[".*/%.config/upstart/.*%.conf"] = "upstart",
[".*/%.config/upstart/.*%.override"] = "upstart",
[".*/%.init/.*%.conf"] = "upstart",
[".*/%.init/.*%.override"] = "upstart",
[".*/LiteStep/.*/.*%.rc"] = "litestep",
[".*/etc/.*limits%.conf"] = "limits",
[".*/etc/.*limits%.d/.*%.conf"] = "limits",
[".*/etc/a2ps/.*%.cfg"] = "a2ps",
[".*/etc/apt/sources%.list%.d/.*%.list"] = "debsources",
[".*/etc/httpd/.*%.conf"] = "apache",
[".*/etc/init/.*%.conf"] = "upstart",
[".*/etc/init/.*%.override"] = "upstart",
[".*/etc/initng/.*/.*%.i"] = "initng",
[".*/etc/ssh/ssh_config%.d/.*%.conf"] = "sshconfig",
[".*/etc/ssh/sshd_config%.d/.*%.conf"] = "sshdconfig",
[".*/etc/sysctl%.d/.*%.conf"] = "sysctl",
["/etc/gitconfig"] = "gitconfig",
[".*/etc/systemd/.*%.conf%.d/.*%.conf"] = "systemd",
[".*/etc/systemd/system/.*%.d/.*%.conf"] = "systemd",
[".*/etc/udev/permissions%.d/.*%.permissions"] = "udevperm",
[".*/etc/xdg/menus/.*%.menu"] = "xml",
[".*/usr/.*/gnupg/options%.skel"] = "gpg",
[".*/usr/share/upstart/.*%.conf"] = "upstart",
[".*/usr/share/upstart/.*%.override"] = "upstart",
[".*Eterm/.*%.cfg"] = "eterm",
[".*enlightenment/.*%.cfg"] = "c",
["bzr_log%..*"] = "bzr",
["named.*%.conf"] = "named",
["rndc.*%.conf"] = "named",
["rndc.*%.key"] = "named",
}
-- These require a special set_ft function
M.star_sets = {
[".*/etc/Muttrc%.d/.*"] = [[muttrc]],
[".*/etc/proftpd/.*%.conf.*"] = [[apachestyle]],
[".*/etc/proftpd/conf%..*/.*"] = [[apachestyle]],
["proftpd%.conf.*"] = [[apachestyle]],
["access%.conf.*"] = [[apache]],
["apache%.conf.*"] = [[apache]],
["apache2%.conf.*"] = [[apache]],
["httpd%.conf.*"] = [[apache]],
["srm%.conf.*"] = [[apache]],
[".*/etc/apache2/.*%.conf.*"] = [[apache]],
[".*/etc/apache2/conf%..*/.*"] = [[apache]],
[".*/etc/apache2/mods-.*/.*"] = [[apache]],
[".*/etc/apache2/sites-.*/.*"] = [[apache]],
[".*/etc/httpd/conf%.d/.*%.conf.*"] = [[apache]],
[".*asterisk/.*%.conf.*"] = [[asterisk]],
[".*asterisk.*/.*voicemail%.conf.*"] = [[asteriskvm]],
[".*/named/db%..*"] = [[bindzone]],
[".*/bind/db%..*"] = [[bindzone]],
["cabal%.project%..*"] = [[cabalproject]],
["crontab"] = [[crontab]],
["crontab%..*"] = [[crontab]],
[".*/etc/cron%.d/.*"] = [[crontab]],
[".*/etc/dnsmasq%.d/.*"] = [[dnsmasq]],
["drac%..*"] = [[dracula]],
[".*/%.fvwm/.*"] = [[fvwm]],
[".*/tmp/lltmp.*"] = [[gedcom]],
[".*/%.gitconfig%.d/.*"] = [[gitconfig]],
["/etc/gitconfig%.d/.*"] = [[gitconfig]],
[".*/gitolite-admin/conf/.*"] = [[gitolite]],
["%.gtkrc.*"] = [[gtkrc]],
["gtkrc.*"] = [[gtkrc]],
["Prl.*%..*"] = [[jam]],
["JAM.*%..*"] = [[jam]],
[".*%.properties_??_??_.*"] = [[jproperties]],
["Kconfig%..*"] = [[kconfig]],
["lilo%.conf.*"] = [[lilo]],
[".*/etc/logcheck/.*%.d.*/.*"] = [[logcheck]],
["[mM]akefile.*"] = [[make]],
["mk"] = [[make]],
["mak"] = [[make]],
["dsp"] = [[make]],
["[rR]akefile.*"] = [[ruby]],
["reportbug-.*"] = [[mail]],
[".*/etc/modprobe%..*"] = [[modconf]],
["%.mutt{ng,}rc.*"] = [[muttrc]],
[".*/%.mutt{ng,}/mutt{ng,}rc.*"] = [[muttrc]],
["mutt{ng,}rc.*,Mutt{ng,}rc.*"] = [[muttrc]],
["%.neomuttrc.*"] = [[neomuttrc]],
[".*/%.neomutt/neomuttrc.*"] = [[neomuttrc]],
["neomuttrc.*"] = [[neomuttrc]],
["Neomuttrc.*"] = [[neomuttrc]],
["tmac%..*"] = [[nroff]],
["/etc/hostname%..*"] = [[config]],
[".*/etc/pam%.d/.*"] = [[pamconf]],
["%.reminders.*"] = [[remind]],
["sgml%.catalog.*"] = [[catalog]],
[".*%.vhdl_[0-9].*"] = [[vhdl]],
[".*vimrc.*"] = [[vim]],
["Xresources.*"] = [[xdefaults]],
[".*/app-defaults/.*"] = [[xdefaults]],
[".*/Xresources/.*"] = [[xdefaults]],
[".*xmodmap.*"] = [[xmodmap]],
[".*/etc/xinetd%.d/.*"] = [[xinetd]],
[".*/etc/yum%.repos%.d/.*"] = [[dosini]],
["%.zsh.*"] = [[zsh]],
["%.zlog.*"] = [[zsh]],
["%.zcompdump.*"] = [[zsh]],
["zsh.*"] = [[zsh]],
["zlog.*"] = [[zsh]],
}
return M

View File

@ -0,0 +1,646 @@
return {
[".ch"] = "chill",
["4gh"] = "fgl",
["4gl"] = "fgl",
["8th"] = "8th",
["ACE"] = "lace",
["BUILD"] = "bzl",
["C"] = "cpp",
["DEF"] = "modula2",
["Dockerfile"] = "dockerfile",
["EC"] = "esqlc",
["F"] = "fortran",
["F03"] = "fortran",
["F08"] = "fortran",
["F77"] = "fortran",
["F90"] = "fortran",
["F95"] = "fortran",
["FOR"] = "fortran",
["FPP"] = "fortran",
["FTN"] = "fortran",
["H"] = "cpp",
["INF"] = "inform",
["JAL"] = "jal",
["L"] = "lisp",
["MOD"] = "modula2",
["Rd"] = "rhelp",
["Rmd"] = "rmd",
["Rnw"] = "rnoweb",
["Rrst"] = "rrst",
["Smd"] = "rmd",
["Snw"] = "rnoweb",
["Srst"] = "rrst",
["a65"] = "a65",
["aap"] = "aap",
["abap"] = "abap",
["abc"] = "abc",
["abl"] = "abel",
["ace"] = "lace",
["action"] = "privoxy",
["ada"] = "ada",
["adb"] = "ada",
["ado"] = "stata",
["adoc"] = "asciidoc",
["ads"] = "ada",
["afm"] = "postscr",
["ahk"] = "autohotkey",
["ai"] = "postscr",
["aidl"] = "aidl",
["al"] = "perl",
["aml"] = "aml",
["art"] = "art",
["as"] = "atlas",
["asciidoc"] = "asciidoc",
["asn"] = "asn",
["asn1"] = "asn",
["at"] = "m4",
["atg"] = "coco",
["atl"] = "atlas",
["atom"] = "xml",
["au3"] = "autoit",
["ave"] = "ave",
["awk"] = "awk",
["bat"] = "dosbatch",
["bbl"] = "tex",
["bc"] = "bc",
["bdf"] = "bdf",
["beancount"] = "beancount",
["bi"] = "freebasic",
["bib"] = "bib",
["bl"] = "blank",
["bsdl"] = "bsdl",
["bst"] = "bst",
["bu"] = "yaml",
["builder"] = "ruby",
["c++"] = "cpp",
["cabal"] = "cabal",
["cbl"] = "cobol",
["cdf"] = "skill",
["cdl"] = "cdl",
["cdxml"] = "xml",
["cfc"] = "cf",
["cfg"] = "cfg",
["cfi"] = "cf",
["cfm"] = "cf",
["chf"] = "ch",
["cho"] = "chordpro",
["chopro"] = "chordpro",
["chordpro"] = "chordpro",
["chs"] = "chaskell",
["cjs"] = "javascript",
["cl"] = "lisp",
["clj"] = "clojure",
["cljc"] = "clojure",
["cljs"] = "clojure",
["cljx"] = "clojure",
["clp"] = "jess",
["cm"] = "voscm",
["cmake"] = "cmake",
["cmake.in"] = "cmake",
["cmod"] = "cmod",
["cob"] = "cobol",
["comp"] = "mason",
["con"] = "cterm",
["crd"] = "chordpro",
["crdpro"] = "chordpro",
["crm"] = "crm",
["cs"] = "cs",
["csc"] = "csc",
["csdl"] = "csdl",
["csp"] = "csp",
["csproj"] = "xml",
["csproj.user"] = "xml",
["css"] = "css",
["ctl"] = "vb",
["cu"] = "cuda",
["cuh"] = "cuda",
["cxx"] = "cpp",
["cyn"] = "cynpp",
["dart"] = "dart",
["dat"] = "nastran",
["dcd"] = "dcd",
["dcl"] = "clean",
["def"] = "def",
["desc"] = "desc",
["desktop"] = "desktop",
["diff"] = "diff",
["directory"] = "desktop",
["do"] = "stata",
["dot"] = "dot",
["dpr"] = "pascal",
["drac"] = "dracula",
["drc"] = "dracula",
["ds"] = "datascript",
["dsl"] = "dsl",
["dsm"] = "vb",
["dtd"] = "dtd",
["dts"] = "dts",
["dtsi"] = "dts",
["dtx"] = "tex",
["dylan"] = "dylan",
["ec"] = "esqlc",
["ecd"] = "ecd",
["el"] = "lisp",
["elm"] = "elm",
["eni"] = "cl",
["epp"] = "epuppet",
["eps"] = "postscr",
["epsf"] = "postscr",
["epsi"] = "postscr",
["erb"] = "eruby",
["erl"] = "erlang",
["errsum"] = "hercules",
["es"] = "javascript",
["ev"] = "hercules",
["ex"] = "elixir",
["exp"] = "expect",
["exs"] = "elixir",
["f"] = "fortran",
["f03"] = "fortran",
["f08"] = "fortran",
["f77"] = "fortran",
["f90"] = "fortran",
["f95"] = "fortran",
["factor"] = "factor",
["fal"] = "falcon",
["fan"] = "fan",
["fb"] = "freebasic",
["fdr"] = "csp",
["feature"] = "cucumber",
["fex"] = "focexec",
["fnl"] = "fennel",
["focexec"] = "focexec",
["for"] = "fortran",
["fortran"] = "fortran",
["fpc"] = "fpcmake",
["fpp"] = "fortran",
["frt"] = "reva",
["fs"] = "forth",
["fsl"] = "framescript",
["ft"] = "forth",
["fth"] = "forth",
["ftn"] = "fortran",
["fwt"] = "fan",
["g"] = "pccts",
["gawk"] = "awk",
["gdmo"] = "gdmo",
["ged"] = "gedcom",
["gemspec"] = "ruby",
["go"] = "go",
["gp"] = "gp",
["gpi"] = "gnuplot",
["gpr"] = "ada",
["gql"] = "graphql",
["gradle"] = "groovy",
["graphql"] = "graphql",
["gretl"] = "gretl",
["groovy"] = "groovy",
["gs"] = "grads",
["gsp"] = "gsp",
["gv"] = "dot",
["h32"] = "hex",
["haml"] = "haml",
["hs"] = "haskell",
["hsc"] = "haskell",
["hs-boot"] = "haskell",
["hsig"] = "haskell",
["hb"] = "hb",
["hdl"] = "vhdl",
["hex"] = "hex",
["hgrc"] = "cfg",
["hh"] = "cpp",
["hlp"] = "smcl",
["hog"] = "hog",
["hpp"] = "cpp",
["hrl"] = "erlang",
["hsm"] = "hamster",
["ht"] = "haste",
["htb"] = "httest",
["html.m4"] = "htmlm4",
["htpp"] = "hastepreproc",
["htt"] = "httest",
["hx"] = "haxe",
["hxml"] = "hxml",
["hxx"] = "cpp",
["iba"] = "ibasic",
["ibi"] = "ibasic",
["ice"] = "slice",
["icl"] = "clean",
["icn"] = "icon",
["ih"] = "ppwiz",
["ihlp"] = "smcl",
["ii"] = "initng",
["ijs"] = "j",
["il"] = "skill",
["ils"] = "skill",
["imata"] = "stata",
["imp"] = "b",
["inf"] = "inform",
["ini"] = "dosini",
["inl"] = "cpp",
["ino"] = "arduino",
["intr"] = "dylanintr",
["ipp"] = "cpp",
["ipynb"] = "json",
["isc"] = "monk",
["iss"] = "iss",
["ist"] = "ist",
["it"] = "ppwiz",
["itcl"] = "tcl",
["itk"] = "tcl",
["j73"] = "jovial",
["jacl"] = "tcl",
["jal"] = "jal",
["jav"] = "java",
["java"] = "java",
["javascript"] = "javascript",
["jgr"] = "jgraph",
["jj"] = "javacc",
["jjt"] = "javacc",
["jl"] = "julia",
["jov"] = "jovial",
["jovial"] = "jovial",
["jpl"] = "jam",
["jpr"] = "jam",
["jrexx"] = "rexx",
["js"] = "javascript",
["json"] = "json",
["jsonp"] = "json",
["jsp"] = "jsp",
["jsx"] = "javascriptreact",
["k"] = "kwt",
["kix"] = "kix",
["ks"] = "kscript",
["kt"] = "kotlin",
["ktm"] = "kotlin",
["kts"] = "kotlin",
["kv"] = "kivy",
["latex"] = "tex",
["latte"] = "latte",
["ld"] = "ld",
["ldif"] = "ldif",
["less"] = "less",
["lgt"] = "logtalk",
["lhs"] = "lhaskell",
["lib"] = "cobol",
["lid"] = "dylanlid",
["liquid"] = "liquid",
["lisp"] = "lisp",
["lite"] = "lite",
["ll"] = "lifelines",
["lot"] = "lotos",
["lotos"] = "lotos",
["lou"] = "lout",
["lout"] = "lout",
["lpc"] = "lpc",
["lpr"] = "pascal",
["lsl"] = "lsl",
["lsp"] = "lisp",
["lss"] = "lss",
["lt"] = "lite",
["lte"] = "latte",
["ltx"] = "tex",
["lua"] = "lua",
["lock"] = "toml",
["m2"] = "modula2",
["m4gl"] = "fgl",
["man"] = "nroff",
["map"] = "map",
["mar"] = "vmasm",
["markdown"] = "markdown",
["mas"] = "master",
["mason"] = "mason",
["master"] = "master",
["mat"] = "radiance",
["mata"] = "stata",
["mch"] = "b",
["md"] = "markdown",
["mdown"] = "markdown",
["mdwn"] = "markdown",
["mel"] = "mel",
["mf"] = "mf",
["mgl"] = "mgl",
["mgp"] = "mgp",
["mhtml"] = "mason",
["mi"] = "modula2",
["mib"] = "mib",
["mix"] = "mix",
["mixal"] = "mix",
["mjs"] = "javascript",
["mkd"] = "markdown",
["mkdn"] = "markdown",
["mkii"] = "context",
["mkiv"] = "context",
["mklx"] = "context",
["mkvi"] = "context",
["mkxl"] = "context",
["ml"] = "ocaml",
["ml.cppo"] = "ocaml",
["mli"] = "ocaml",
["mli.cppo"] = "ocaml",
["mlip"] = "ocaml",
["mll"] = "ocaml",
["mlp"] = "ocaml",
["mlt"] = "ocaml",
["mly"] = "ocaml",
["mmp"] = "mmp",
["mo"] = "gdmo",
["moc"] = "cpp",
["mof"] = "msidl",
["mom"] = "nroff",
["monk"] = "monk",
["moo"] = "moo",
["mot"] = "srec",
["mp"] = "mp",
["mpl"] = "maple",
["msc"] = "xmath",
["msf"] = "xmath",
["msql"] = "msql",
["mst"] = "ist",
["mush"] = "mush",
["mv"] = "maple",
["mws"] = "maple",
["my"] = "mib",
["mysql"] = "mysql",
["nanorc"] = "nanorc",
["nb"] = "mma",
["ncf"] = "ncf",
["ninja"] = "ninja",
["nqc"] = "nqc",
["nr"] = "nroff",
["nse"] = "lua",
["nsh"] = "nsis",
["nsi"] = "nsis",
["obj"] = "obj",
["occ"] = "occam",
["odl"] = "msidl",
["opam"] = "opam",
["opam.template"] = "opam",
["or"] = "openroad",
["ora"] = "ora",
["org"] = "org",
["orx"] = "rexx",
["p36"] = "plm",
["p6"] = "raku",
["pac"] = "plm",
["page"] = "mallard",
["papp"] = "papp",
["pas"] = "pascal",
["patch"] = "diff",
["pbtxt"] = "pbtxt",
["pc"] = "proc",
["pcmk"] = "pcmk",
["pdb"] = "prolog",
["pde"] = "arduino",
["pdf"] = "pdf",
["pfa"] = "postscr",
["pike"] = "pike",
["pk"] = "poke",
["pkb"] = "sql",
["pks"] = "sql",
["pl1"] = "pli",
["pld"] = "cupl",
["pli"] = "pli",
["plm"] = "plm",
["plp"] = "plp",
["pls"] = "plsql",
["plsql"] = "plsql",
["plx"] = "perl",
["pm6"] = "raku",
["pml"] = "promela",
["pmod"] = "pike",
["po"] = "po",
["pod"] = "pod",
["pod6"] = "raku",
["pot"] = "po",
["pov"] = "pov",
["ppd"] = "ppd",
["pr"] = "sdl",
["proto"] = "proto",
["ps"] = "postscr",
["ps1"] = "ps1",
["ps1xml"] = "ps1xml",
["psc1"] = "xml",
["psd1"] = "ps1",
["psf"] = "psf",
["psgi"] = "perl",
["psl"] = "psl",
["psm1"] = "ps1",
["pssc"] = "ps1",
["ptl"] = "python",
["pxd"] = "pyrex",
["pxml"] = "papp",
["pxsl"] = "papp",
["py"] = "python",
["pyi"] = "python",
["pyw"] = "python",
["pyx"] = "pyrex",
["qc"] = "c",
["quake"] = "m3quake",
["rad"] = "radiance",
["rake"] = "ruby",
["raku"] = "raku",
["rakudoc"] = "raku",
["rakumod"] = "raku",
["rakutest"] = "raku",
["raml"] = "raml",
["rb"] = "ruby",
["rbs"] = "rbs",
["rbw"] = "ruby",
["rc"] = "rc",
["rch"] = "rc",
["rcp"] = "pilrc",
["rd"] = "rhelp",
["recipe"] = "conaryrecipe",
["ref"] = "b",
["rego"] = "rego",
["rej"] = "diff",
["rem"] = "remind",
["remind"] = "remind",
["res"] = "rescript",
["resi"] = "rescript",
["rex"] = "rexx",
["rexx"] = "rexx",
["rexxj"] = "rexx",
["rhtml"] = "eruby",
["rib"] = "rib",
["rjs"] = "ruby",
["rkt"] = "scheme",
["rmd"] = "rmd",
["rnc"] = "rnc",
["rng"] = "rng",
["rnw"] = "rnoweb",
["rockspec"] = "lua",
["roff"] = "nroff",
["rpl"] = "rpl",
["rq"] = "sparql",
["rrst"] = "rrst",
["rs"] = "rust",
["rss"] = "xml",
["rst"] = "rst",
["rtf"] = "rtf",
["ru"] = "ruby",
["run"] = "ampl",
["rxj"] = "rexx",
["rxml"] = "ruby",
["rxo"] = "rexx",
["s19"] = "srec",
["s28"] = "srec",
["s37"] = "srec",
["s85"] = "sinda",
["sa"] = "sather",
["sas"] = "sas",
["sass"] = "sass",
["sba"] = "vb",
["sbt"] = "sbt",
["sc"] = "scala",
["scala"] = "scala",
["sce"] = "scilab",
["sci"] = "scilab",
["scm"] = "scheme",
["score"] = "slrnsc",
["scpt"] = "applescript",
["scss"] = "scss",
["sd"] = "sd",
["sdc"] = "sdc",
["sdl"] = "sdl",
["sed"] = "sed",
["sexp"] = "sexplib",
["si"] = "cuplsim",
["sieve"] = "sieve",
["sig"] = "lprolog",
["sil"] = "sil",
["sim"] = "simula",
["sin"] = "sinda",
["siv"] = "sieve",
["sl"] = "slang",
["slt"] = "tsalt",
["sol"] = "solidity",
["smcl"] = "smcl",
["smd"] = "rmd",
["smith"] = "smith",
["sml"] = "sml",
["smt"] = "smith",
["sno"] = "snobol4",
["snw"] = "rnoweb",
["sp"] = "spice",
["sparql"] = "sparql",
["spd"] = "spup",
["spdata"] = "spup",
["spec"] = "spec",
["speedup"] = "spup",
["spi"] = "spyce",
["spice"] = "spice",
["spt"] = "snobol4",
["spy"] = "spyce",
["sqi"] = "sqr",
["sqlj"] = "sqlj",
["sqr"] = "sqr",
["srec"] = "srec",
["srst"] = "rrst",
["ss"] = "scheme",
["ssc"] = "monk",
["st"] = "st",
["stp"] = "stp",
["strl"] = "esterel",
["sty"] = "tex",
["sum"] = "hercules",
["sv"] = "systemverilog",
["svelte"] = "svelte",
["svg"] = "svg",
["svh"] = "systemverilog",
["swift"] = "swift",
["swift.gyb"] = "swiftgyb",
["sys"] = "dosbatch",
["t.html"] = "tilde",
["t6"] = "raku",
["tak"] = "tak",
["tcc"] = "cpp",
["tcl"] = "tcl",
["tdf"] = "ahdl",
["testGroup"] = "rexx",
["testUnit"] = "rexx",
["texi"] = "texinfo",
["texinfo"] = "texinfo",
["text"] = "text",
["tf"] = "tf",
["ti"] = "terminfo",
["tk"] = "tcl",
["tlh"] = "cpp",
["tli"] = "tli",
["tmac"] = "nroff",
["tmpl"] = "template",
["toc"] = "cdrtoc",
["toml"] = "toml",
["tpl"] = "smarty",
["tpm"] = "xml",
["tpp"] = "cpp",
["tr"] = "nroff",
["tsc"] = "monk",
["tsx"] = "typescriptreact",
["txi"] = "texinfo",
["tyb"] = "sql",
["tyc"] = "sql",
["typ"] = "sql",
["uc"] = "uc",
["ui"] = "xml",
["uil"] = "uil",
["uit"] = "uil",
["ulpc"] = "lpc",
["v"] = "verilog",
["va"] = "verilogams",
["vams"] = "verilogams",
["vb"] = "vb",
["vba"] = "vim",
["vbe"] = "vhdl",
["vbs"] = "vb",
["vc"] = "hercules",
["vhd"] = "vhdl",
["vhdl"] = "vhdl",
["vho"] = "vhdl",
["vim"] = "vim",
["vr"] = "vera",
["vrh"] = "vera",
["vri"] = "vera",
["vroom"] = "vroom",
["vst"] = "vhdl",
["vue"] = "vue",
["wast"] = "wast",
["wat"] = "wast",
["wbt"] = "winbatch",
["webmanifest"] = "json",
["wiki"] = "flexwiki",
["wm"] = "webmacro",
["wml"] = "wml",
["wpl"] = "xml",
["wrap"] = "dosini",
["wrl"] = "vrml",
["wrm"] = "acedb",
["wsdl"] = "xml",
["wsml"] = "wsml",
["x"] = "rpcgen",
["xht"] = "xhtml",
["xhtml"] = "xhtml",
["xin"] = "omnimark",
["xlf"] = "xml",
["xliff"] = "xml",
["xmi"] = "xml",
["xom"] = "omnimark",
["xq"] = "xquery",
["xql"] = "xquery",
["xqm"] = "xquery",
["xquery"] = "xquery",
["xqy"] = "xquery",
["xs"] = "xs",
["xsd"] = "xsd",
["xsl"] = "xslt",
["xslt"] = "xslt",
["xul"] = "xml",
["yaml"] = "yaml",
["yaws"] = "erlang",
["yml"] = "yaml",
["z8a"] = "z8a",
["zsh"] = "zsh",
["zu"] = "zimbu",
["zut"] = "zimbutempl",
}

View File

@ -0,0 +1,596 @@
local M = {}
local function getlines(i, j)
return table.concat(
vim.api.nvim_buf_get_lines(0, i - 1, j or i, true),
"\n"
)
end
M.extensions = {
["ms"] = function()
vim.cmd([[if !dist#ft#FTnroff() | setf xmath | endif]])
end,
["xpm"] = function()
if getlines(1):find("XPM2") then
return "xpm2"
else
return "xpm"
end
end,
["module"] = function()
if getlines(1):find("%<%?php") then
return "php"
else
return "virata"
end
end,
["pkg"] = function()
if getlines(1):find("%<%?php") then
return "php"
else
return "virata"
end
end,
["hw"] = function()
if getlines(1):find("%<%?php") then
return "php"
else
return "virata"
end
end,
["ts"] = function()
if getlines(1):find("<%?xml") then
return "xml"
else
return "typescript"
end
end,
["ttl"] = function()
if getlines(1):find("^@?(prefix|base)") then
return "stata"
end
end,
["t"] = function()
-- Don't know how to translate this :(
vim.cmd(
[[if !dist#ft#FTnroff() && !dist#ft#FTperl() | setf tads | endif]]
)
end,
["class"] = function()
-- Decimal escape sequence
-- The original was "^\xca\xfe\xba\xbe"
if getlines(1):find("^\x202\x254\x186\x190") then
return "stata"
end
end,
["smi"] = function()
if getlines(1):find("smil") then
return "smil"
else
return "mib"
end
end,
["smil"] = function()
if getlines(1):find("<?%s*xml.*?>") then
return "xml"
else
return "smil"
end
end,
["cls"] = function()
local first_line = getlines(1)
if first_line:find("^%%") then
return "tex"
elseif first_line:sub(1, 1) == "#" and first_line:find("rexx") then
return "rexx"
else
return "st"
end
end,
["install"] = function()
if getlines(1):find("%<%?php") then
return "php"
else
vim.cmd([[call dist#ft#SetFileTypeSH("bash")]])
end
end,
["decl"] = function()
if getlines(1, 3):find("^%<%!SGML") then
return "sgmldecl"
end
end,
["sgm"] = function()
local top_file = getlines(1, 5)
if top_file:find("linuxdoc") then
return "sgmlnx"
elseif
getlines(1):find("%<%!DOCTYPE.*DocBook")
or getlines(2):find("<!DOCTYPE.*DocBook")
then
vim.b.docbk_type = "sgml"
vim.b.docbk_ver = 4
return "docbk"
else
return "sgml"
end
end,
["sgml"] = function()
local top_file = getlines(1, 5)
if top_file:find("linuxdoc") then
return "sgmlnx"
elseif
getlines(1):find("%<%!DOCTYPE.*DocBook")
or getlines(2):find("<!DOCTYPE.*DocBook")
then
vim.b.docbk_type = "sgml"
vim.b.docbk_ver = 4
return "docbk"
else
return "sgml"
end
end,
["reg"] = function()
if
getlines(1):find(
"^REGEDIT[0-9]*%s*$|^Windows Registry Editor Version %d*%.%d*%s*$"
)
then
return "registry"
end
end,
["pm"] = function()
if getlines(1):find("XPM2") then
return "xpm2"
elseif getlines(1):find("XPM") then
return "xpm"
else
return "perl"
end
end,
["me"] = function()
if
vim.fn.expand("<afile>") ~= "read.me"
and vim.fn.expand("<afile>") ~= "click.me"
then
return "nroff"
end
end,
["m4"] = function()
if not vim.fn.expand("<afile>"):find("(html.m4$|fvwm2rc)") then
return "m4"
end
end,
["edn"] = function()
if getlines(1):find("^%s*%(%s*edif") then
return "edif"
else
return "clojure"
end
end,
["rul"] = function()
local top_file = getlines(1, 6)
if top_file:find("InstallShield") then
return "ishd"
else
return "diva"
end
end,
["prg"] = function()
if vim.fn.exists("g:filetype_prg") == 1 then
return vim.g.filetype_prg
else
return "clipper"
end
end,
["cpy"] = function()
if getlines(1):find("^%#%#") then
return "python"
else
return "cobol"
end
end,
-- Complicated functions
["asp"] = function()
if vim.g.filetype_asp ~= nil then
return vim.g.filetype_asp
elseif getlines(1, 3):find("perlscript") then
return "aspperl"
else
return "aspvbs"
end
end,
["asa"] = function()
if vim.g.filetype_asa ~= nil then
return vim.g.filetype_asa
else
return "aspvbs"
end
end,
["cmd"] = function()
if getlines(1):find("^%/%*") then
return "rexx"
else
return "dosbatch"
end
end,
["cc"] = function()
if vim.fn.exists("cynlib_syntax_for_cc") == 1 then
return "cynlib"
else
return "cpp"
end
end,
["cpp"] = function()
if vim.fn.exists("cynlib_syntax_for_cpp") == 1 then
return "cynlib"
else
return "cpp"
end
end,
["inp"] = function()
vim.cmd([[call dist#ft#Check_inp()]])
end,
["asm"] = function()
vim.cmd([[call dist#ft#FTasm()]])
end,
["s"] = function()
vim.cmd([[call dist#ft#FTasm()]])
end,
["S"] = function()
vim.cmd([[call dist#ft#FTasm()]])
end,
["a"] = function()
vim.cmd([[call dist#ft#FTasm()]])
end,
["A"] = function()
vim.cmd([[call dist#ft#FTasm()]])
end,
["mac"] = function()
vim.cmd([[call dist#ft#FTasm()]])
end,
["lst"] = function()
vim.cmd([[call dist#ft#FTasm()]])
end,
["bas"] = function()
vim.cmd([[call dist#ft#FTVB("basic")]])
end,
["btm"] = function()
vim.cmd([[call dist#ft#FTbtm()]])
end,
["db"] = function()
vim.cmd([[call dist#ft#BindzoneCheck('')]])
end,
["c"] = function()
vim.cmd([[call dist#ft#FTlpc()]])
end,
["h"] = function()
vim.cmd([[call dist#ft#FTheader()]])
end,
["ch"] = function()
vim.cmd([[call dist#ft#FTchange()]])
end,
["ent"] = function()
vim.cmd([[call dist#ft#FTent()]])
end,
["ex"] = function()
vim.cmd([[call dist#ft#ExCheck()]])
end,
["eu"] = function()
vim.cmd([[call dist#ft#EuphoriaCheck()]])
end,
["ew"] = function()
vim.cmd([[call dist#ft#EuphoriaCheck()]])
end,
["exu"] = function()
vim.cmd([[call dist#ft#EuphoriaCheck()]])
end,
["exw"] = function()
vim.cmd([[call dist#ft#EuphoriaCheck()]])
end,
["EU"] = function()
vim.cmd([[call dist#ft#EuphoriaCheck()]])
end,
["EW"] = function()
vim.cmd([[call dist#ft#EuphoriaCheck()]])
end,
["EX"] = function()
vim.cmd([[call dist#ft#EuphoriaCheck()]])
end,
["EXU"] = function()
vim.cmd([[call dist#ft#EuphoriaCheck()]])
end,
["EXW"] = function()
vim.cmd([[call dist#ft#EuphoriaCheck()]])
end,
["d"] = function()
vim.cmd([[call dist#ft#DtraceCheck()]])
end,
["com"] = function()
vim.cmd([[call dist#ft#BindzoneCheck('dcl')]])
end,
["e"] = function()
vim.cmd([[call dist#ft#FTe()]])
end,
["E"] = function()
vim.cmd([[call dist#ft#FTe()]])
end,
["html"] = function()
vim.cmd([[call dist#ft#FThtml()]])
end,
["htm"] = function()
vim.cmd([[call dist#ft#FThtml()]])
end,
["shtml"] = function()
vim.cmd([[call dist#ft#FThtml()]])
end,
["stm"] = function()
vim.cmd([[call dist#ft#FThtml()]])
end,
["idl"] = function()
vim.cmd([[call dist#ft#FTidl()]])
end,
["pro"] = function()
vim.cmd([[call dist#ft#ProtoCheck('idlang')]])
end,
["m"] = function()
vim.cmd([[call dist#ft#FTm()]])
end,
["mms"] = function()
vim.cmd([[call dist#ft#FTmms()]])
end,
["*.mm"] = function()
vim.cmd([[call dist#ft#FTmm()]])
end,
["pp"] = function()
vim.cmd([[call dist#ft#FTpp()]])
end,
["pl"] = function()
vim.cmd([[call dist#ft#FTpl()]])
end,
["PL"] = function()
vim.cmd([[call dist#ft#FTpl()]])
end,
["inc"] = function()
vim.cmd([[call dist#ft#FTinc()]])
end,
["w"] = function()
vim.cmd([[call dist#ft#FTprogress_cweb()]])
end,
["i"] = function()
vim.cmd([[call dist#ft#FTprogress_asm()]])
end,
["p"] = function()
vim.cmd([[call dist#ft#FTprogress_pascal()]])
end,
["r"] = function()
vim.cmd([[call dist#ft#FTr()]])
end,
["R"] = function()
vim.cmd([[call dist#ft#FTr()]])
end,
["mc"] = function()
vim.cmd([[call dist#ft#McSetf()]])
end,
["ebuild"] = function()
vim.cmd([[call dist#ft#SetFileTypeSH("bash")]])
end,
["bash"] = function()
vim.cmd([[call dist#ft#SetFileTypeSH("bash")]])
end,
["eclass"] = function()
vim.cmd([[call dist#ft#SetFileTypeSH("bash")]])
end,
["ksh"] = function()
vim.cmd([[call dist#ft#SetFileTypeSH("ksh")]])
end,
["etc/profile"] = function()
vim.cmd([[call dist#ft#SetFileTypeSH(getline(1))]])
end,
["sh"] = function()
vim.cmd([[call dist#ft#SetFileTypeSH(getline(1))]])
end,
["env"] = function()
vim.cmd([[call dist#ft#SetFileTypeSH(getline(1))]])
end,
["tcsh"] = function()
vim.cmd([[call dist#ft#SetFileTypeShell("tcsh")]])
end,
["csh"] = function()
vim.cmd([[call dist#ft#CSH()]])
end,
["rules"] = function()
vim.cmd([[call dist#ft#FTRules()]])
end,
["sql"] = function()
vim.cmd([[call dist#ft#SQL()]])
end,
["tex"] = function()
vim.cmd([[call dist#ft#FTtex()]])
end,
["frm"] = function()
vim.cmd([[call dist#ft#FTVB("form")]])
end,
["xml"] = function()
vim.cmd([[call dist#ft#FTxml()]])
end,
["y"] = function()
vim.cmd([[call dist#ft#FTy()]])
end,
["dtml"] = function()
vim.cmd([[call dist#ft#FThtml()]])
end,
["pt"] = function()
vim.cmd([[call dist#ft#FThtml()]])
end,
["cpt"] = function()
vim.cmd([[call dist#ft#FThtml()]])
end,
["zsql"] = function()
vim.cmd([[call dist#ft#SQL()]])
end,
}
M.literal = {
["xorg.conf-4"] = function()
vim.b.xf86conf_xfree86_version = 4
return "xf86conf"
end,
["xorg.conf"] = function()
vim.b.xf86conf_xfree86_version = 4
return "xf86conf"
end,
["XF86Config"] = function()
if getlines(1):find("XConfigurator") then
vim.b.xf86conf_xfree86_version = 3
end
return "xf86conf"
end,
["INDEX"] = function()
if
getlines(1):find(
"^%s*(distribution|installed_software|root|bundle|product)%s*$"
)
then
return "psf"
end
end,
["INFO"] = function()
if
getlines(1):find(
"^%s*(distribution|installed_software|root|bundle|product)%s*$"
)
then
return "psf"
end
end,
["control"] = function()
if getlines(1):find("^Source%:") then
return "debcontrol"
end
end,
["NEWS"] = function()
if getlines(1):find("%; urgency%=") then
return "debchangelog"
end
end,
["indent.pro"] = function()
vim.cmd([[call dist#ft#ProtoCheck('indent')]])
end,
[".bashrc"] = function()
vim.cmd([[call dist#ft#SetFileTypeSH("bash")]])
end,
["bashrc"] = function()
vim.cmd([[call dist#ft#SetFileTypeSH("bash")]])
end,
["bash.bashrc"] = function()
vim.cmd([[call dist#ft#SetFileTypeSH("bash")]])
end,
["PKGBUILD"] = function()
vim.cmd([[call dist#ft#SetFileTypeSH("bash")]])
end,
["APKBUILD"] = function()
vim.cmd([[call dist#ft#SetFileTypeSH("bash")]])
end,
[".kshrc"] = function()
vim.cmd([[call dist#ft#SetFileTypeSH("ksh")]])
end,
[".profile"] = function()
vim.cmd([[call dist#ft#SetFileTypeSH(getline(1))]])
end,
[".tcshrc"] = function()
vim.cmd([[call dist#ft#SetFileTypeShell("tcsh")]])
end,
["tcsh.tcshrc"] = function()
vim.cmd([[call dist#ft#SetFileTypeShell("tcsh")]])
end,
["tcsh.login"] = function()
vim.cmd([[call dist#ft#SetFileTypeShell("tcsh")]])
end,
[".login"] = function()
vim.cmd([[call dist#ft#CSH()]])
end,
[".cshrc"] = function()
vim.cmd([[call dist#ft#CSH()]])
end,
["csh.cshrc"] = function()
vim.cmd([[call dist#ft#CSH()]])
end,
["csh.login"] = function()
vim.cmd([[call dist#ft#CSH()]])
end,
["csh.logout"] = function()
vim.cmd([[call dist#ft#CSH()]])
end,
[".alias"] = function()
vim.cmd([[call dist#ft#CSH()]])
end,
[".d"] = function()
vim.cmd([[call dist#ft#SetFileTypeSH("bash")]])
end,
}
M.complex = {
[".*/xorg%.conf%.d/.*%.conf"] = function()
vim.b.xf86conf_xfree86_version = 4
return "xf86conf"
end,
[".*printcap"] = function()
vim.b.ptcap_type = "print"
return "ptcap"
end,
[".*termcap"] = function()
vim.b.ptcap_type = "term"
return "ptcap"
end,
["[cC]hange[lL]og"] = function()
if getlines(1):find("%; urgency%=") then
return "debchangelog"
else
return "changelog"
end
end,
["%.bashrc.*"] = function()
vim.cmd([[call dist#ft#SetFileTypeSH("bash")]])
end,
["%.bash[_-]profile"] = function()
vim.cmd([[call dist#ft#SetFileTypeSH("bash")]])
end,
["%.bash[_-]logout"] = function()
vim.cmd([[call dist#ft#SetFileTypeSH("bash")]])
end,
["%.bash[_-]aliases"] = function()
vim.cmd([[call dist#ft#SetFileTypeSH("bash")]])
end,
["%.bash%-fc[_-]"] = function()
vim.cmd([[call dist#ft#SetFileTypeSH("bash")]])
end,
["PKGBUILD.*"] = function()
vim.cmd([[call dist#ft#SetFileTypeSH("bash")]])
end,
["APKBUILD.*"] = function()
vim.cmd([[call dist#ft#SetFileTypeSH("bash")]])
end,
["%.kshrc.*"] = function()
vim.cmd([[call dist#ft#SetFileTypeSH("ksh")]])
end,
["%.profile.*"] = function()
vim.cmd([[call dist#ft#SetFileTypeSH(getline(1))]])
end,
["%.tcshrc.*"] = function()
vim.cmd([[call dist#ft#SetFileTypeShell("tcsh")]])
end,
["%.login.*"] = function()
vim.cmd([[call dist#ft#CSH()]])
end,
["%.cshrc.*"] = function()
vim.cmd([[call dist#ft#CSH()]])
end,
}
M.shebang = {
["bash"] = "sh",
["node"] = "javascript",
["python3"] = "python",
}
return M

View File

@ -0,0 +1,164 @@
return {
[".a2psrc"] = "a2ps",
[".asoundrc"] = "alsaconf",
[".babelrc"] = "json",
[".cdrdao"] = "cdrdaoconf",
[".cvsrc"] = "cvsrc",
[".dictrc"] = "dictconf",
[".dir_colors"] = "dircolors",
[".dircolors"] = "dircolors",
[".editorconfig"] = "dosini",
[".emacs"] = "lisp",
[".eslintrc"] = "json",
[".exrc"] = "vim",
[".fetchmailrc"] = "fetchmail",
[".firebaserc"] = "json",
[".gdbinit"] = "gdb",
[".gitconfig"] = "gitconfig",
[".gitmodules"] = "gitconfig",
[".gnashpluginrc"] = "gnash",
[".gnashrc"] = "gnash",
[".gprc"] = "gp",
[".gtkrc"] = "gtkrc",
[".htaccess"] = "apache",
[".indent.pro"] = "indent",
[".inputrc"] = "readline",
[".irbrc"] = "ruby",
[".lftprc"] = "lftp",
[".mailcap"] = "mailcap",
[".mrxvtrc"] = "mrxvtrc",
[".netrc"] = "netrc",
[".npmrc"] = "dosini",
[".ocamlinit"] = "ocaml",
[".pam_environment"] = "pamenv",
[".pinerc"] = "pine",
[".pinercex"] = "pine",
[".povrayrc"] = "povini",
[".prettierrc"] = "json",
[".procmail"] = "procmail",
[".procmailrc"] = "procmail",
[".pythonrc"] = "python",
[".pythonstartup"] = "python",
[".ratpoisonrc"] = "ratpoison",
[".reminders"] = "remind",
[".Rprofile"] = "r",
[".sawfishrc"] = "lisp",
[".sbclrc"] = "lisp",
[".screenrc"] = "screen",
[".slrnrc"] = "slrnrc",
[".stylelintrc"] = "json",
[".tfrc"] = "tf",
[".tidyrc"] = "tidy",
[".viminfo"] = "viminfo",
[".wgetrc"] = "wget",
[".wvdialrc"] = "wvdial",
[".zcompdump"] = "zsh",
[".zfbfmarks"] = "zsh",
[".zlogin"] = "zsh",
[".zlogout"] = "zsh",
[".zprofile"] = "zsh",
[".zshenv"] = "zsh",
[".zshrc"] = "zsh",
["Appfile"] = "ruby",
["Brewfile"] = "ruby",
["BUILD"] = "bzl",
["CMakeLists.txt"] = "cmake",
["COMMIT_EDITMSG"] = "gitcommit",
["Containerfile"] = "dockerfile",
["Dockerfile"] = "dockerfile",
["Fastfile"] = "ruby",
["Gemfile"] = "ruby",
["Kconfig"] = "kconfig",
["Kconfig.debug"] = "kconfig",
["MERGE_MSG"] = "gitcommit",
["Neomuttrc"] = "neomuttrc",
["Pipfile"] = "config",
["Pipfile.lock"] = "json",
["Podfile"] = "ruby",
["Puppetfile"] = "ruby",
["README"] = "text",
["SConstruct"] = "python",
["TAG_EDITMSG"] = "gitcommit",
["_exrc"] = "vim",
["_viminfo"] = "viminfo",
["a2psrc"] = "a2ps",
["apt.conf"] = "aptconf",
["auto.master"] = "conf",
["build.xml"] = "ant",
["cabal.config"] = "cabalconfig",
["cabal.project"] = "cabalproject",
["calendar"] = "calendar",
["catalog"] = "catalog",
["cfengine.conf"] = "cfengine",
[".clang-format"] = "yaml",
["_clang-format"] = "yaml",
["cm3.cfg"] = "m3quake",
["configure.ac"] = "config",
["configure.in"] = "config",
["denyhosts.conf"] = "denyhosts",
["dict.conf"] = "dictconf",
["dictd.conf"] = "dictdconf",
["elinks.conf"] = "elinks",
["exim.conf"] = "exim",
["exports"] = "exports",
["fglrxrc"] = "xml",
["fstab"] = "fstab",
["gitconfig"] = "gitconfig",
["git-rebase-todo"] = "gitrebase",
["gitolite.conf"] = "gitolite",
["gnashpluginrc"] = "gnash",
["gnashrc"] = "gnash",
["go.mod"] = "gomod",
["gtkrc"] = "gtkrc",
["indentrc"] = "indent",
["inittab"] = "inittab",
["inputrc"] = "readline",
["ipf.conf"] = "ipfilter",
["ipf.rules"] = "ipfilter",
["ipf6.conf"] = "ipfilter",
["irbrc"] = "ruby",
["Jenkinsfile"] = "groovy",
["lftp.conf"] = "lftp",
["lilo.conf"] = "lilo",
["lltxxxxx.txt"] = "gedcom",
["lynx.cfg"] = "lynx",
["m3makefile"] = "m3build",
["m3overrides"] = "m3build",
["mailcap"] = "mailcap",
["main.cf"] = "pfmain",
["man.config"] = "manconf",
["meson.build"] = "meson",
["meson_options.txt"] = "meson",
["mplayer.conf"] = "mplayerconf",
["mrxvtrc"] = "mrxvtrc",
["mtab"] = "fstab",
["named.root"] = "bindzone",
["npmrc"] = "dosini",
["opam"] = "opam",
["pam_env.conf"] = "pamenv",
["pf.conf"] = "pf",
["pinerc"] = "pine",
["pinercex"] = "pine",
["ratpoisonrc"] = "ratpoison",
["resolv.conf"] = "resolv",
["robots.txt"] = "robots",
["sbclrc"] = "lisp",
["screenrc"] = "screen",
["sendmail.cf"] = "sm",
["smb.conf"] = "samba",
["snort.conf"] = "hog",
["squid.conf"] = "squid",
["ssh_config"] = "sshconfig",
["sshd_config"] = "sshdconfig",
["sudoers.tmp"] = "sudoers",
["tags"] = "tags",
["texmf.cnf"] = "texmf",
["tfrc"] = "tf",
["tidy.conf"] = "tidy",
["tidyrc"] = "tidy",
["trustees.conf"] = "trustees",
["vgrindefs"] = "vgrindefs",
["vision.conf"] = "hog",
["wgetrc"] = "wget",
["wvdial.conf"] = "wvdial",
}

View File

@ -0,0 +1,238 @@
"""This is the script I used to automatically convert most of the vim autocommands into lua.
Warning: This is crappy code. Please don't use this for anything! I only included this file
in case anyone was curious how I made the plugin.
"""
import pprint
import re
VIM_SCRIPT = "misc/filetype.vim"
def find_normal_globs():
ext_glob = re.compile(r"au\s+BufNewFile,BufRead\s+([\+\*\,\/\.\w]+)\s+setf\s+(\w+)")
asterisks = re.compile(r"\*")
mapping: dict[str, str] = {}
for line in lines:
# if "cxx" in line:
# print(line)
# exit(1)
match = ext_glob.search(line)
if match is None:
continue
glob, ft = match.groups()
for g in glob.split(","):
mapping[g] = ft
# print(f'["{g}"] = "{ft}"')
# pprint.pprint(mapping)
extensions: dict[str, str] = {}
simple: dict[str, str] = {}
complex: dict[str, str] = {}
for glob, ft in mapping.items():
num_asts = len(asterisks.findall(glob))
if glob.startswith("*") and num_asts == 1:
extensions[glob] = ft
elif num_asts == 0:
simple[glob] = ft
else:
complex[glob] = ft
lua_print(simple)
lua_print(extensions)
lua_print(complex)
def find_function_cmds(flines):
non_setf = re.compile(r"au\s+BufNewFile,BufRead\s+(\S+)\s*$")
for line in flines:
m = non_setf.search(line)
if m is not None:
print(m.group(1))
elif "|" in line and "au" in line:
print(line)
def find_star_setfs():
star_glob = re.compile(
r"au\s+BufNewFile,BufRead\s+(\S+)\s+call\s+s:StarSetf\('(\w+)'\)"
)
mappings: dict[str, str] = {}
for line in lines:
match = star_glob.search(line)
if match is None:
continue
glob, ft = match.groups()
mappings[glob] = ft
for k, v in mappings.items():
print(f"['{k}'] = '{v}',")
text = """M.star_sets = {
[".*/etc/Muttrc%.d/.*"] = "muttrc",
[".*/etc/proftpd/.*%.conf.*,.*/etc/proftpd/conf%..*/.*"] = "apachestyle",
["proftpd%.conf.*"] = "apachestyle",
["access%.conf.*,apache%.conf.*,apache2%.conf.*,httpd%.conf.*,srm%.conf.*"] = "apache",
[".*/etc/apache2/.*%.conf.*,.*/etc/apache2/conf%..*/.*,.*/etc/apache2/mods-.*/.*,.*/etc/apache2/sites-.*/.*,.*/etc/httpd/conf%.d/.*%.conf.*"] = "apache",
[".*asterisk/.*%.conf.*"] = "asterisk",
[".*asterisk.*/.*voicemail%.conf.*"] = "asteriskvm",
[".*/named/db%..*,.*/bind/db%..*"] = "bindzone",
["cabal%.project%..*"] = "cabalproject",
["crontab,crontab%..*,.*/etc/cron%.d/.*"] = "crontab",
[".*/etc/dnsmasq%.d/.*"] = "dnsmasq",
["drac%..*"] = "dracula",
[".*/%.fvwm/.*"] = "fvwm",
[".*/tmp/lltmp.*"] = "gedcom",
[".*/%.gitconfig%.d/.*,/etc/gitconfig%.d/.*"] = "gitconfig",
[".*/gitolite-admin/conf/.*"] = "gitolite",
["%.gtkrc.*,gtkrc.*"] = "gtkrc",
["Prl.*%..*,JAM.*%..*"] = "jam",
[".*%.properties_??_??_.*"] = "jproperties",
["Kconfig%..*"] = "kconfig",
["lilo%.conf.*"] = "lilo",
[".*/etc/logcheck/.*%.d.*/.*"] = "logcheck",
["[mM]akefile.*"] = "make",
["[rR]akefile.*"] = "ruby",
["reportbug-.*"] = "mail",
[".*/etc/modprobe%..*"] = "modconf",
["%.mutt{ng,}rc.*,.*/%.mutt{ng,}/mutt{ng,}rc.*"] = "muttrc",
["mutt{ng,}rc.*,Mutt{ng,}rc.*"] = "muttrc",
["%.neomuttrc.*,.*/%.neomutt/neomuttrc.*"] = "neomuttrc",
["neomuttrc.*,Neomuttrc.*"] = "neomuttrc",
["tmac%..*"] = "nroff",
["/etc/hostname%..*"] = "config",
[".*/etc/pam%.d/.*"] = "pamconf",
["%.reminders.*"] = "remind",
["sgml%.catalog.*"] = "catalog",
[".*%.vhdl_[0-9].*"] = "vhdl",
[".*vimrc.*"] = "vim",
["Xresources.*,.*/app-defaults/.*,.*/Xresources/.*"] = "xdefaults",
[".*xmodmap.*"] = "xmodmap",
[".*/etc/xinetd%.d/.*"] = "xinetd",
[".*/etc/yum%.repos%.d/.*"] = "dosini",
["%.zsh.*,%.zlog.*,%.zcompdump.*"] = "zsh",
["zsh.*,zlog.*"] = "zsh",
}"""
# def find_call_setfs():
# star_glob = re.compile(r"au\s+BufNewFile,BufRead\s+(\S+)\s+call\s+(.+)")
# mappings: dict[str, str] = {}
# for line in lines:
# match = star_glob.search(line)
# if match is None:
# continue
# glob, ft = match.groups()
# if "StarS" in ft:
# continue
# mappings[glob] = ft
# extension = {}
# literal = {}
# complex = {}
# for glob, ft in mappings.items():
# num_asts = len(asterisks.findall(glob))
# if glob.startswith('*') and num_asts == 1:
# print(f"['{k}'] = [[call {v}]],")
def lua_print(d: dict):
print("M.thing = {")
for k, v in d.items():
print(f"['{k}'] = [[{v}]],")
print("}")
def find_function_globs():
ext_glob = re.compile(r"au\s+BufNewFile,BufRead\s+(\S+)\s+call\s+(.+)")
asterisks = re.compile(r"\*")
mapping: dict[str, str] = {}
for line in lines:
match = ext_glob.search(line)
if match is None:
continue
glob, ft = match.groups()
if "s:Star" in ft:
continue
for g in glob.split(","):
mapping[g] = ft
extensions: dict[str, str] = {}
simple: dict[str, str] = {}
complex: dict[str, str] = {}
for glob, ft in mapping.items():
num_asts = len(asterisks.findall(glob))
if glob.startswith("*") and num_asts == 1:
extensions[glob] = ft
elif num_asts == 0:
simple[glob] = ft
else:
complex[glob] = ft
lua_print(simple)
lua_print(extensions)
lua_print(complex)
def convert_glob_to_lua_regex(glob):
glob = glob.replace(".", "%.")
glob = glob.replace("*", ".*")
return glob
# ["*.git/modules/*/config",
def fix_lua_regexes(text):
keyvals = re.compile(r'\["([^"]+)"\] = "([^"]+)"')
matches = {}
for line in text.split("\n"):
result = keyvals.search(line)
if result is None:
continue
glob, ft = result.groups()
matches[glob] = ft
fixed = {}
for glob, ft in matches.items():
if "{" not in glob:
for pat in glob.split(","):
fixed[pat] = ft
else:
fixed[glob] = ft
lua_print(fixed)
def convert_lua_regex_vim(e):
e = re.sub(r"%\.", r"\.", e)
e = re.sub("/", r"\/", e)
return e
def convert_lua_regexps_to_vim(flines):
for line in flines:
matches = re.search(r'"([^"]+)":', line)
if matches is None:
continue
matches = matches.group(1)
matches = re.sub(r"%\.", r"\.", matches)
matches = re.sub("/", r"\/", matches)
new_line = re.sub(r'"([^"]+)":', '"' + matches + '":', line)
print(new_line.strip())
lines = open(VIM_SCRIPT).readlines()
find_function_cmds(lines)

View File

@ -0,0 +1,8 @@
# Customized
indent_type = "Spaces"
column_width = 79
# Default
line_endings = "Unix"
indent_width = 4
quote_style = "AutoPreferDouble"
no_call_parentheses = false

View File

@ -0,0 +1,71 @@
# This is a basic workflow to help you get started with Actions
name: CI
# Controls when the workflow will run
on:
# Triggers the workflow on push or pull request events but only for the main branch
push:
branches: [ main ]
pull_request:
branches: [ main ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
strategy:
fail-fast: true
matrix:
neovim_branch: ['v0.7.0', 'master']
# The type of runner that the job will run on
runs-on: ubuntu-latest
env:
NEOVIM_BRANCH: ${{ matrix.neovim_branch }}
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- name: Setup build dependencies
run: |
sudo apt update &&
sudo apt install -y \
autoconf \
automake \
cmake \
g++ \
gettext \
gperf \
libjemalloc-dev \
libluajit-5.1-dev \
libmsgpack-dev \
libtermkey-dev \
libtool \
libtool-bin \
libunibilium-dev \
libvterm-dev \
lua-bitop \
lua-lpeg \
lua-mpack \
ninja-build \
pkg-config \
unzip
- name: Cache neovim
uses: actions/cache@v2
with:
path: neovim-${{env.NEOVIM_BRANCH}}
key: build-${{env.NEOVIM_BRANCH}}
- name: Build Neovim
run: make neovim NEOVIM_BRANCH=$NEOVIM_BRANCH
- name: Run Test
run: make test

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Lewis Russell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,33 @@
.DEFAULT_GOAL := test
NEOVIM_BRANCH := master
FILTER=.*
NEOVIM := neovim-$(NEOVIM_BRANCH)
.PHONY: neovim
neovim: $(NEOVIM)
$(NEOVIM):
git clone --depth 1 https://github.com/neovim/neovim --branch $(NEOVIM_BRANCH) $@
make -C $@
export VIMRUNTIME=$(PWD)/$(NEOVIM)/runtime
.PHONY: test
test: $(NEOVIM)
$(NEOVIM)/.deps/usr/bin/busted \
-v \
--lazy \
--helper=$(PWD)/test/preload.lua \
--output test.busted.outputHandlers.nvim \
--lpath=$(PWD)/$(NEOVIM)/?.lua \
--lpath=$(PWD)/$(NEOVIM)/build/?.lua \
--lpath=$(PWD)/$(NEOVIM)/runtime/lua/?.lua \
--lpath=$(PWD)/?.lua \
--lpath=$(PWD)/lua/?.lua \
--filter=$(FILTER) \
$(PWD)/test
-@stty sane

View File

@ -0,0 +1,805 @@
# impatient.nvim
[![CI](https://github.com/lewis6991/impatient.nvim/workflows/CI/badge.svg?branch=main)](https://github.com/lewis6991/impatient.nvim/actions?query=workflow%3ACI)
Speed up loading Lua modules in Neovim to improve startup time.
## Optimisations
This plugin does several things to speed loading Lua modules and files.
### Implements a chunk cache
This is done by using `loadstring` to compile the Lua modules to bytecode and stores them in a cache file. The cache is invalidated using as hash consisting of:
- The modified time (`sec` and `nsec`) of the file path.
- The file size.
The cache file is located in `$XDG_CACHE_HOME/nvim/luacache_chunks`.
### Implements a module resolution cache
This is done by maintaining a table of module name to path. The cache is invalidated only if a path no longer exists.
The cache file is located in `$XDG_CACHE_HOME/nvim/luacache_modpaths`.
**Note**: This optimization breaks the loading order guarantee of the paths in `'runtimepath'`.
If you rely on this ordering then you can disable this cache (`_G.__luacache_config = { modpaths = { enable = false } }`.
See configuration below for more details.
## Requirements
- Neovim v0.7
## Installation
[packer.nvim](https://github.com/wbthomason/packer.nvim):
```lua
-- Is using a standard Neovim install, i.e. built from source or using a
-- provided appimage.
use 'lewis6991/impatient.nvim'
```
## Setup
To use impatient, you need only to include it near the top of your `init.lua` or `init.vim`.
init.lua:
```lua
require('impatient')
```
init.vim:
```viml
lua require('impatient')
```
## Commands
`:LuaCacheClear`:
Remove the loaded cache and delete the cache file. A new cache file will be created the next time you load Neovim.
`:LuaCacheLog`:
View log of impatient.
`:LuaCacheProfile`:
View profiling data. To enable, Impatient must be setup with:
```viml
lua require'impatient'.enable_profile()
```
## Configuration
Unlike most plugins which provide a `setup()` function, Impatient uses a configuration table stored in the global state, `_G.__luacache_config`.
If you modify the default configuration, it must be done before `require('impatient')` is run.
Default config:
```lua
_G.__luacache_config = {
chunks = {
enable = true,
path = vim.fn.stdpath('cache')..'/luacache_chunks',
},
modpaths = {
enable = true,
path = vim.fn.stdpath('cache')..'/luacache_modpaths',
}
}
require('impatient')
```
## Performance Example
Measured on a M1 MacBook Air.
<details>
<summary>Standard</summary>
```
────────────┬────────────┐
Resolve │ Load │
────────────┼────────────┼─────────────────────────────────────────────────────────────────
Time │ Time │ Module
────────────┼────────────┼─────────────────────────────────────────────────────────────────
54.337ms │ 34.257ms │ Total
────────────┼────────────┼─────────────────────────────────────────────────────────────────
7.264ms │ 0.470ms │ octo.colors
3.154ms │ 0.128ms │ diffview.bootstrap
2.086ms │ 0.231ms │ gitsigns
0.320ms │ 0.982ms │ octo.date
0.296ms │ 1.004ms │ octo.writers
0.322ms │ 0.893ms │ octo.utils
0.293ms │ 0.854ms │ vim.diagnostic
0.188ms │ 0.819ms │ vim.lsp.util
0.261ms │ 0.739ms │ vim.lsp
0.330ms │ 0.620ms │ octo.model.octo-buffer
0.392ms │ 0.422ms │ packer.load
0.287ms │ 0.436ms │ octo.reviews
0.367ms │ 0.325ms │ octo
0.309ms │ 0.381ms │ octo.graphql
0.454ms │ 0.221ms │ octo.base64
0.295ms │ 0.338ms │ octo.reviews.file-panel
0.305ms │ 0.306ms │ octo.reviews.file-entry
0.183ms │ 0.386ms │ vim.treesitter.query
0.418ms │ 0.149ms │ vim.uri
0.342ms │ 0.213ms │ octo.config
0.110ms │ 0.430ms │ nvim-lsp-installer.ui.status-win
0.296ms │ 0.209ms │ octo.window
0.202ms │ 0.288ms │ vim.lsp.rpc
0.352ms │ 0.120ms │ octo.gh
0.287ms │ 0.184ms │ octo.reviews.layout
0.209ms │ 0.260ms │ vim.lsp.handlers
0.108ms │ 0.360ms │ luasnip.nodes.snippet
0.243ms │ 0.212ms │ dirvish
0.289ms │ 0.159ms │ octo.mappings
0.228ms │ 0.220ms │ trouble.view
0.145ms │ 0.293ms │ plenary.job
0.188ms │ 0.244ms │ vim.lsp.diagnostic
0.032ms │ 0.391ms │ packer_compiled
0.188ms │ 0.228ms │ vim.lsp.buf
0.186ms │ 0.227ms │ vim.lsp.protocol
0.141ms │ 0.264ms │ nvim-treesitter.install
0.205ms │ 0.190ms │ vim.lsp._snippet
0.114ms │ 0.281ms │ colorizer
0.124ms │ 0.262ms │ nvim-treesitter.parsers
0.331ms │ 0.052ms │ octo.model.body-metadata
0.325ms │ 0.054ms │ octo.constants
0.296ms │ 0.081ms │ octo.reviews.renderer
0.326ms │ 0.050ms │ octo.model.thread-metadata
0.258ms │ 0.117ms │ trouble
0.106ms │ 0.267ms │ cmp.core
0.286ms │ 0.085ms │ octo.completion
0.120ms │ 0.250ms │ luasnip
0.286ms │ 0.084ms │ octo.ui.bubbles
0.068ms │ 0.298ms │ diffview.utils
0.325ms │ 0.039ms │ octo.model.title-metadata
0.126ms │ 0.234ms │ treesitter-context
0.282ms │ 0.073ms │ octo.signs
0.299ms │ 0.043ms │ octo.folds
0.112ms │ 0.228ms │ luasnip.util.util
0.181ms │ 0.156ms │ vim.treesitter.languagetree
0.260ms │ 0.073ms │ vim.keymap
0.101ms │ 0.231ms │ cmp.entry
0.182ms │ 0.145ms │ vim.treesitter.highlighter
0.191ms │ 0.121ms │ trouble.util
0.190ms │ 0.119ms │ vim.lsp.codelens
0.190ms │ 0.117ms │ vim.lsp.sync
0.197ms │ 0.105ms │ vim.highlight
0.170ms │ 0.132ms │ spellsitter
0.086ms │ 0.213ms │ github_dark
0.200ms │ 0.099ms │ persistence
0.100ms │ 0.196ms │ cmp.view.custom_entries_view
0.118ms │ 0.176ms │ nvim-treesitter.configs
0.090ms │ 0.201ms │ gitsigns.git
0.114ms │ 0.170ms │ nvim-lsp-installer.ui.display
0.217ms │ 0.064ms │ plenary.async.async
0.195ms │ 0.078ms │ vim.lsp.log
0.191ms │ 0.081ms │ trouble.renderer
0.122ms │ 0.150ms │ nvim-treesitter.ts_utils
0.235ms │ 0.035ms │ plenary
0.100ms │ 0.168ms │ cmp.source
0.191ms │ 0.076ms │ vim.treesitter
0.106ms │ 0.160ms │ lspconfig.util
0.118ms │ 0.147ms │ nvim-treesitter.query
0.088ms │ 0.176ms │ gitsigns.config
0.108ms │ 0.150ms │ cmp
0.193ms │ 0.063ms │ trouble.providers
0.206ms │ 0.050ms │ tmux.version.parse
0.103ms │ 0.151ms │ cmp.view.wildmenu_entries_view
0.070ms │ 0.178ms │ diffview.path
0.189ms │ 0.058ms │ trouble.providers.lsp
0.096ms │ 0.147ms │ luasnip.util.parser
0.093ms │ 0.150ms │ gitsigns.manager
0.097ms │ 0.145ms │ null-ls.utils
0.155ms │ 0.087ms │ plenary.async.control
0.105ms │ 0.135ms │ nvim-lsp-installer.installers.std
0.107ms │ 0.130ms │ lspconfig.configs
0.097ms │ 0.140ms │ null-ls.helpers.generator_factory
0.188ms │ 0.047ms │ trouble.providers.telescope
0.191ms │ 0.040ms │ trouble.config
0.099ms │ 0.131ms │ cmp.utils.window
0.096ms │ 0.133ms │ luasnip.nodes.choiceNode
0.192ms │ 0.036ms │ trouble.providers.qf
0.104ms │ 0.124ms │ cmp.utils.keymap
0.089ms │ 0.139ms │ gitsigns.hunks
0.104ms │ 0.122ms │ nvim-lsp-installer.process
0.096ms │ 0.129ms │ null-ls.sources
0.116ms │ 0.108ms │ nvim-lsp-installer
0.096ms │ 0.128ms │ luasnip.nodes.dynamicNode
0.162ms │ 0.062ms │ tmux.copy
0.197ms │ 0.025ms │ trouble.folds
0.156ms │ 0.066ms │ plenary.async.util
0.150ms │ 0.071ms │ cmp.utils.highlight
0.105ms │ 0.116ms │ nvim-lsp-installer.server
0.118ms │ 0.100ms │ nvim-treesitter.utils
0.182ms │ 0.035ms │ trouble.providers.diagnostic
0.103ms │ 0.114ms │ luasnip.nodes.node
0.185ms │ 0.031ms │ trouble.colors
0.180ms │ 0.035ms │ vim.ui
0.162ms │ 0.053ms │ spaceless
0.118ms │ 0.097ms │ nvim-treesitter.shell_command_selectors
0.160ms │ 0.053ms │ tmux.wrapper.tmux
0.182ms │ 0.031ms │ vim.treesitter.language
0.178ms │ 0.035ms │ trouble.text
0.157ms │ 0.054ms │ plenary.vararg.rotate
0.106ms │ 0.104ms │ nvim-lsp-installer.installers.context
0.181ms │ 0.028ms │ tmux
0.158ms │ 0.050ms │ nvim-treesitter-playground
0.067ms │ 0.140ms │ diffview.oop
0.158ms │ 0.047ms │ tmux.resize
0.166ms │ 0.039ms │ tmux.log.convert
0.161ms │ 0.044ms │ tmux.layout
0.155ms │ 0.048ms │ plenary.async.structs
0.101ms │ 0.102ms │ cmp.view
0.096ms │ 0.105ms │ luasnip.util.environ
0.145ms │ 0.055ms │ plenary.async
0.163ms │ 0.037ms │ tmux.navigation.navigate
0.179ms │ 0.020ms │ tmux.keymaps
0.155ms │ 0.044ms │ plenary.functional
0.102ms │ 0.097ms │ cmp.matcher
0.103ms │ 0.095ms │ cmp.view.ghost_text_view
0.106ms │ 0.091ms │ colorizer.nvim
0.168ms │ 0.029ms │ tmux.log
0.106ms │ 0.090ms │ nvim-lsp-installer._generated.filetype_map
0.122ms │ 0.073ms │ nvim-treesitter.info
0.098ms │ 0.097ms │ null-ls.client
0.105ms │ 0.089ms │ nvim-lsp-installer.log
0.170ms │ 0.024ms │ tmux.navigation
0.109ms │ 0.084ms │ nvim-lsp-installer.servers
0.098ms │ 0.095ms │ null-ls.helpers.diagnostics
0.160ms │ 0.033ms │ tmux.configuration.options
0.100ms │ 0.091ms │ cmp.utils.misc
0.044ms │ 0.148ms │ lewis6991
0.104ms │ 0.088ms │ colorizer.trie
0.163ms │ 0.028ms │ ts_context_commentstring
0.054ms │ 0.136ms │ cmp-rg
0.130ms │ 0.060ms │ nvim-treesitter.query_predicates
0.151ms │ 0.039ms │ plenary.reload
0.096ms │ 0.094ms │ luasnip.nodes.insertNode
0.160ms │ 0.028ms │ tmux.layout.parse
0.096ms │ 0.093ms │ luasnip.nodes.restoreNode
0.166ms │ 0.022ms │ tmux.configuration.validate
0.100ms │ 0.088ms │ cmp.view.native_entries_view
0.155ms │ 0.033ms │ plenary.tbl
0.126ms │ 0.062ms │ lspconfig.server_configurations.sumneko_lua
0.029ms │ 0.160ms │ cmp_buffer.buffer
0.105ms │ 0.083ms │ cmp.utils.str
0.162ms │ 0.025ms │ tmux.log.severity
0.164ms │ 0.024ms │ tmux.wrapper.nvim
0.107ms │ 0.081ms │ nvim-lsp-installer.ui.status-win.components.settings-schema
0.021ms │ 0.167ms │ lewis6991.null-ls
0.163ms │ 0.024ms │ tmux.configuration
0.116ms │ 0.071ms │ nvim-treesitter.tsrange
0.161ms │ 0.026ms │ tmux.log.channels
0.094ms │ 0.091ms │ gitsigns.debug
0.163ms │ 0.021ms │ plenary.vararg
0.166ms │ 0.018ms │ tmux.version
0.160ms │ 0.022ms │ tmux.configuration.logging
0.155ms │ 0.026ms │ plenary.errors
0.127ms │ 0.053ms │ nvim-treesitter
0.094ms │ 0.085ms │ null-ls.info
0.100ms │ 0.079ms │ cmp.config
0.095ms │ 0.084ms │ null-ls.diagnostics
0.055ms │ 0.123ms │ cmp_path
0.139ms │ 0.038ms │ plenary.async.tests
0.098ms │ 0.078ms │ null-ls.config
0.100ms │ 0.076ms │ cmp.view.docs_view
0.102ms │ 0.074ms │ cmp.utils.feedkeys
0.089ms │ 0.085ms │ gitsigns.current_line_blame
0.127ms │ 0.047ms │ null-ls
0.107ms │ 0.066ms │ nvim-lsp-installer.installers
0.095ms │ 0.078ms │ luasnip.util.mark
0.106ms │ 0.066ms │ nvim-lsp-installer.fs
0.142ms │ 0.030ms │ persistence.config
0.100ms │ 0.070ms │ cmp.config.default
0.078ms │ 0.091ms │ foldsigns
0.120ms │ 0.048ms │ lua-dev
0.113ms │ 0.053ms │ nvim-lsp-installer.ui
0.029ms │ 0.138ms │ lewis6991.status
0.118ms │ 0.047ms │ lspconfig
0.113ms │ 0.051ms │ nvim-lsp-installer.jobs.outdated-servers
0.105ms │ 0.058ms │ nvim-lsp-installer.installers.npm
0.106ms │ 0.057ms │ nvim-lsp-installer.core.receipt
0.101ms │ 0.061ms │ cmp.utils.char
0.091ms │ 0.071ms │ gitsigns.signs
0.097ms │ 0.065ms │ luasnip.nodes.util
0.126ms │ 0.034ms │ treesitter-context.utils
0.096ms │ 0.065ms │ lua-dev.config
0.109ms │ 0.052ms │ nvim-lsp-installer.core.fetch
0.103ms │ 0.055ms │ cmp.types.lsp
0.099ms │ 0.059ms │ luasnip.nodes.functionNode
0.090ms │ 0.067ms │ gitsigns.util
0.110ms │ 0.047ms │ nvim-lsp-installer.jobs.outdated-servers.cargo
0.096ms │ 0.061ms │ luasnip.config
0.100ms │ 0.057ms │ cmp.utils.async
0.101ms │ 0.055ms │ cmp.context
0.091ms │ 0.064ms │ gitsigns.highlight
0.094ms │ 0.061ms │ lua-dev.sumneko
0.094ms │ 0.061ms │ gitsigns.subprocess
0.067ms │ 0.088ms │ cmp_luasnip
0.105ms │ 0.050ms │ nvim-lsp-installer.data
0.105ms │ 0.049ms │ nvim-lsp-installer.installers.pip3
0.120ms │ 0.034ms │ lspconfig.server_configurations.bashls
0.107ms │ 0.046ms │ nvim-lsp-installer.core.clients.github
0.107ms │ 0.045ms │ nvim-lsp-installer.installers.shell
0.099ms │ 0.053ms │ cmp.config.compare
0.109ms │ 0.043ms │ lspconfig.server_configurations.clangd
0.115ms │ 0.036ms │ lspconfig.server_configurations.vimls
0.097ms │ 0.054ms │ luasnip.util.pattern_tokenizer
0.097ms │ 0.053ms │ null-ls.helpers.make_builtin
0.101ms │ 0.049ms │ cmp.utils.api
0.118ms │ 0.032ms │ lspconfig.server_configurations.jedi_language_server
0.106ms │ 0.043ms │ nvim-lsp-installer.jobs.outdated-servers.pip3
0.106ms │ 0.043ms │ nvim-lsp-installer.jobs.outdated-servers.gem
0.108ms │ 0.040ms │ nvim-lsp-installer._generated.language_autocomplete_map
0.104ms │ 0.043ms │ nvim-lsp-installer.installers.composer
0.101ms │ 0.046ms │ cmp.config.mapping
0.047ms │ 0.100ms │ cmp_nvim_lsp_signature_help
0.109ms │ 0.037ms │ nvim-lsp-installer.servers.sumneko_lua
0.115ms │ 0.028ms │ nvim-treesitter.caching
0.096ms │ 0.047ms │ null-ls.state
0.090ms │ 0.053ms │ gitsigns.debounce
0.059ms │ 0.084ms │ cmp_tmux.tmux
0.096ms │ 0.045ms │ null-ls.builtins.diagnostics.flake8
0.106ms │ 0.034ms │ nvim-lsp-installer.jobs.pool
0.106ms │ 0.033ms │ nvim-lsp-installer.ui.status-win.server_hints
0.105ms │ 0.034ms │ nvim-lsp-installer.installers.gem
0.107ms │ 0.032ms │ nvim-lsp-installer.jobs.outdated-servers.npm
0.106ms │ 0.031ms │ nvim-lsp-installer.jobs.outdated-servers.git
0.114ms │ 0.022ms │ nvim-lsp-installer.servers.jedi_language_server
0.105ms │ 0.031ms │ nvim-lsp-installer.jobs.outdated-servers.composer
0.098ms │ 0.038ms │ null-ls.methods
0.109ms │ 0.026ms │ nvim-lsp-installer.jobs.outdated-servers.version-check-result
0.106ms │ 0.029ms │ nvim-lsp-installer.settings
0.107ms │ 0.027ms │ cmp.utils.debug
0.103ms │ 0.031ms │ cmp.types.cmp
0.070ms │ 0.064ms │ diffview.events
0.108ms │ 0.026ms │ nvim-lsp-installer.platform
0.097ms │ 0.037ms │ null-ls.helpers.command_resolver
0.104ms │ 0.029ms │ cmp.config.sources
0.107ms │ 0.026ms │ nvim-lsp-installer.jobs.outdated-servers.github_release_file
0.099ms │ 0.033ms │ cmp.utils.cache
0.107ms │ 0.025ms │ nvim-lsp-installer.path
0.101ms │ 0.030ms │ cmp.utils.autocmd
0.097ms │ 0.034ms │ null-ls.logger
0.100ms │ 0.031ms │ cmp.utils.event
0.088ms │ 0.042ms │ gitsigns.cache
0.103ms │ 0.027ms │ cmp.utils.pattern
0.108ms │ 0.022ms │ nvim-lsp-installer.jobs.outdated-servers.jdtls
0.103ms │ 0.027ms │ cmp.utils.buffer
0.095ms │ 0.034ms │ luasnip.nodes.textNode
0.096ms │ 0.033ms │ luasnip.util.dict
0.108ms │ 0.021ms │ nvim-lsp-installer.servers.bashls
0.108ms │ 0.021ms │ nvim-lsp-installer.ui.state
0.110ms │ 0.018ms │ nvim-lsp-installer.servers.vimls
0.101ms │ 0.027ms │ null-ls.helpers.range_formatting_args_factory
0.057ms │ 0.071ms │ cmp_treesitter.lru
0.105ms │ 0.022ms │ nvim-lsp-installer.dispatcher
0.097ms │ 0.030ms │ luasnip.extras.filetype_functions
0.103ms │ 0.024ms │ luasnip.session
0.105ms │ 0.021ms │ nvim-lsp-installer.core.clients.crates
0.105ms │ 0.021ms │ nvim-lsp-installer.jobs.outdated-servers.github_tag
0.110ms │ 0.016ms │ cmp.types
0.105ms │ 0.021ms │ nvim-lsp-installer.core.clients.eclipse
0.105ms │ 0.021ms │ nvim-lsp-installer.notify
0.089ms │ 0.036ms │ gitsigns.status
0.096ms │ 0.029ms │ null-ls.builtins.diagnostics.teal
0.097ms │ 0.027ms │ null-ls.builtins
0.103ms │ 0.021ms │ cmp.types.vim
0.060ms │ 0.062ms │ cmp_tmux.source
0.100ms │ 0.022ms │ null-ls.helpers
0.098ms │ 0.024ms │ null-ls.builtins.diagnostics.gitlint
0.065ms │ 0.056ms │ cmp_treesitter
0.024ms │ 0.097ms │ buftabline.buftab
0.095ms │ 0.026ms │ null-ls.builtins.diagnostics.shellcheck
0.095ms │ 0.026ms │ null-ls.builtins.diagnostics.luacheck
0.097ms │ 0.021ms │ null-ls.helpers.formatter_factory
0.097ms │ 0.022ms │ luasnip.util.events
0.097ms │ 0.021ms │ luasnip.util.types
0.096ms │ 0.022ms │ luasnip.util.functions
0.037ms │ 0.078ms │ cmp_cmdline
0.032ms │ 0.083ms │ cmp_buffer.source
0.040ms │ 0.074ms │ lewis6991.cmp
0.060ms │ 0.054ms │ cmp_treesitter.treesitter
0.089ms │ 0.025ms │ gitsigns.message
0.039ms │ 0.073ms │ cmp_nvim_lsp.source
0.055ms │ 0.054ms │ buftabline.build
0.026ms │ 0.083ms │ lewis6991.lsp
0.051ms │ 0.055ms │ cmp_nvim_lua
0.033ms │ 0.065ms │ cleanfold
0.071ms │ 0.025ms │ cmp_tmux
0.043ms │ 0.053ms │ cmp_nvim_lsp
0.058ms │ 0.033ms │ cmp-spell
0.043ms │ 0.037ms │ cmp_emoji
0.029ms │ 0.049ms │ lewis6991.floating_man
0.032ms │ 0.042ms │ cmp_buffer.timer
0.024ms │ 0.050ms │ lewis6991.treesitter
0.019ms │ 0.054ms │ lewis6991.cmp_gh
0.025ms │ 0.046ms │ buftabline.buffers
0.021ms │ 0.048ms │ lewis6991.telescope
0.024ms │ 0.031ms │ buftabline
0.035ms │ 0.019ms │ cmp_buffer
0.019ms │ 0.035ms │ buftabline.utils
0.021ms │ 0.030ms │ buftabline.highlights
0.020ms │ 0.032ms │ buftabline.tabpage-tab
0.019ms │ 0.030ms │ buftabline.options
0.020ms │ 0.026ms │ buftabline.tabpages
────────────┴────────────┴─────────────────────────────────────────────────────────────────
```
</details>
Total resolve: 54.337ms, total load: 34.257ms
<details>
<summary>With cache</summary>
```
────────────┬────────────┐
Resolve │ Load │
────────────┼────────────┼─────────────────────────────────────────────────────────────────
Time │ Time │ Module
────────────┼────────────┼─────────────────────────────────────────────────────────────────
6.357ms │ 6.796ms │ Total
────────────┼────────────┼─────────────────────────────────────────────────────────────────
0.041ms │ 2.021ms │ octo.writers
0.118ms │ 0.160ms │ lewis6991.plugins
0.050ms │ 0.144ms │ octo.date
0.035ms │ 0.153ms │ octo.utils
0.057ms │ 0.099ms │ octo.model.octo-buffer
0.047ms │ 0.105ms │ packer
0.058ms │ 0.080ms │ octo.colors
0.121ms │ 0.015ms │ gitsigns.cache
0.082ms │ 0.037ms │ packer.load
0.107ms │ 0.008ms │ gitsigns.debounce
0.048ms │ 0.064ms │ octo.config
0.048ms │ 0.061ms │ octo.graphql
0.049ms │ 0.051ms │ octo
0.043ms │ 0.057ms │ vim.diagnostic
0.085ms │ 0.013ms │ gitsigns.highlight
0.065ms │ 0.032ms │ octo.base64
0.035ms │ 0.060ms │ vim.lsp
0.056ms │ 0.035ms │ octo.gh
0.045ms │ 0.045ms │ octo.mappings
0.026ms │ 0.060ms │ octo.reviews
0.037ms │ 0.045ms │ packer.plugin_utils
0.030ms │ 0.049ms │ octo.reviews.file-panel
0.018ms │ 0.056ms │ vim.lsp.util
0.043ms │ 0.030ms │ packer.log
0.036ms │ 0.032ms │ packer.util
0.032ms │ 0.035ms │ octo.reviews.file-entry
0.021ms │ 0.045ms │ packer_compiled
0.052ms │ 0.014ms │ octo.model.body-metadata
0.033ms │ 0.027ms │ octo.reviews.layout
0.014ms │ 0.047ms │ nvim-treesitter.parsers
0.035ms │ 0.024ms │ vim.lsp.handlers
0.014ms │ 0.044ms │ nvim-lsp-installer.ui.status-win
0.046ms │ 0.012ms │ octo.completion
0.037ms │ 0.021ms │ octo.constants
0.032ms │ 0.025ms │ lewis6991
0.040ms │ 0.017ms │ persistence
0.030ms │ 0.026ms │ diffview.utils
0.035ms │ 0.020ms │ packer.result
0.015ms │ 0.040ms │ gitsigns.config
0.031ms │ 0.024ms │ packer.async
0.041ms │ 0.013ms │ vim.uri
0.044ms │ 0.010ms │ octo.model.thread-metadata
0.018ms │ 0.035ms │ gitsigns.debug
0.023ms │ 0.030ms │ github_dark
0.030ms │ 0.023ms │ packer.jobs
0.039ms │ 0.013ms │ buftabline.build
0.037ms │ 0.014ms │ octo.model.title-metadata
0.025ms │ 0.025ms │ vim.lsp.buf
0.022ms │ 0.027ms │ gitsigns
0.027ms │ 0.022ms │ lewis6991.status
0.016ms │ 0.032ms │ gitsigns.git
0.026ms │ 0.020ms │ octo.window
0.033ms │ 0.012ms │ octo.folds
0.037ms │ 0.008ms │ trouble.providers.lsp
0.016ms │ 0.028ms │ vim.lsp.protocol
0.028ms │ 0.016ms │ octo.signs
0.028ms │ 0.014ms │ null-ls
0.027ms │ 0.014ms │ octo.reviews.renderer
0.018ms │ 0.024ms │ trouble.view
0.017ms │ 0.025ms │ luasnip.nodes.snippet
0.023ms │ 0.018ms │ colorizer.nvim
0.017ms │ 0.024ms │ vim.lsp._snippet
0.015ms │ 0.025ms │ nvim-treesitter.install
0.018ms │ 0.022ms │ plenary.async.structs
0.018ms │ 0.021ms │ dirvish
0.027ms │ 0.012ms │ octo.ui.bubbles
0.019ms │ 0.020ms │ treesitter-context
0.015ms │ 0.024ms │ vim.lsp.diagnostic
0.016ms │ 0.023ms │ vim.lsp.rpc
0.022ms │ 0.016ms │ trouble
0.022ms │ 0.016ms │ null-ls.helpers.generator_factory
0.020ms │ 0.017ms │ luasnip
0.014ms │ 0.023ms │ plenary.job
0.026ms │ 0.011ms │ lewis6991.cmp
0.027ms │ 0.010ms │ trouble.providers
0.022ms │ 0.014ms │ nvim-treesitter.query
0.018ms │ 0.018ms │ vim.treesitter.highlighter
0.017ms │ 0.018ms │ nvim-treesitter.shell_command_selectors
0.014ms │ 0.021ms │ nvim-treesitter.configs
0.025ms │ 0.010ms │ lewis6991.floating_man
0.022ms │ 0.012ms │ vim.keymap
0.013ms │ 0.021ms │ cmp.entry
0.024ms │ 0.010ms │ lspconfig.server_configurations.bashls
0.018ms │ 0.016ms │ gitsigns.hunks
0.017ms │ 0.017ms │ gitsigns.status
0.014ms │ 0.019ms │ cmp.core
0.018ms │ 0.015ms │ spellsitter
0.014ms │ 0.019ms │ colorizer
0.024ms │ 0.009ms │ diffview.bootstrap
0.016ms │ 0.016ms │ null-ls.utils
0.021ms │ 0.011ms │ nvim-treesitter.info
0.022ms │ 0.010ms │ vim.highlight
0.016ms │ 0.016ms │ null-ls.info
0.019ms │ 0.013ms │ cmp_path
0.026ms │ 0.006ms │ cmp.utils.autocmd
0.021ms │ 0.011ms │ foldsigns
0.014ms │ 0.018ms │ lewis6991.null-ls
0.018ms │ 0.013ms │ cmp.view
0.017ms │ 0.014ms │ null-ls.client
0.016ms │ 0.015ms │ gitsigns.manager
0.013ms │ 0.018ms │ cmp.view.custom_entries_view
0.015ms │ 0.015ms │ nvim-lsp-installer.ui.display
0.020ms │ 0.010ms │ null-ls.methods
0.016ms │ 0.014ms │ plenary.async.control
0.019ms │ 0.011ms │ null-ls.diagnostics
0.014ms │ 0.015ms │ luasnip.util.util
0.017ms │ 0.013ms │ gitsigns.current_line_blame
0.013ms │ 0.016ms │ buftabline.buftab
0.015ms │ 0.015ms │ trouble.util
0.015ms │ 0.015ms │ luasnip.config
0.019ms │ 0.010ms │ plenary.async.async
0.018ms │ 0.012ms │ nvim-treesitter.tsrange
0.021ms │ 0.007ms │ cmp_nvim_lua
0.014ms │ 0.015ms │ vim.treesitter.query
0.015ms │ 0.014ms │ cmp.source
0.014ms │ 0.015ms │ vim.treesitter.languagetree
0.012ms │ 0.016ms │ nvim-lsp-installer._generated.filetype_map
0.015ms │ 0.014ms │ nvim-lsp-installer.servers
0.014ms │ 0.014ms │ lspconfig.util
0.011ms │ 0.017ms │ cmp
0.015ms │ 0.013ms │ cmp.view.wildmenu_entries_view
0.021ms │ 0.007ms │ lspconfig.server_configurations.jedi_language_server
0.015ms │ 0.013ms │ lua-dev
0.018ms │ 0.010ms │ gitsigns.util
0.014ms │ 0.014ms │ vim.lsp.codelens
0.017ms │ 0.011ms │ plenary.async.util
0.013ms │ 0.014ms │ null-ls.sources
0.015ms │ 0.012ms │ nvim-treesitter.query_predicates
0.013ms │ 0.015ms │ luasnip.nodes.choiceNode
0.015ms │ 0.013ms │ null-ls.helpers.diagnostics
0.017ms │ 0.011ms │ trouble.renderer
0.015ms │ 0.013ms │ luasnip.nodes.node
0.014ms │ 0.013ms │ lua-dev.sumneko
0.013ms │ 0.014ms │ cmp.utils.window
0.021ms │ 0.006ms │ treesitter-context.utils
0.018ms │ 0.009ms │ cleanfold
0.015ms │ 0.012ms │ nvim-treesitter.ts_utils
0.012ms │ 0.015ms │ nvim-lsp-installer.installers.std
0.015ms │ 0.012ms │ nvim-lsp-installer.server
0.014ms │ 0.012ms │ lewis6991.lsp
0.016ms │ 0.011ms │ gitsigns.signs
0.020ms │ 0.006ms │ buftabline
0.019ms │ 0.007ms │ plenary.tbl
0.013ms │ 0.013ms │ nvim-lsp-installer
0.018ms │ 0.008ms │ plenary
0.015ms │ 0.010ms │ cmp_luasnip
0.019ms │ 0.007ms │ null-ls.logger
0.016ms │ 0.010ms │ vim.lsp.sync
0.016ms │ 0.010ms │ spaceless
0.017ms │ 0.009ms │ gitsigns.subprocess
0.016ms │ 0.009ms │ plenary.functional
0.016ms │ 0.010ms │ buftabline.buffers
0.016ms │ 0.009ms │ vim.lsp.log
0.019ms │ 0.006ms │ cmp_tmux
0.013ms │ 0.012ms │ luasnip.nodes.dynamicNode
0.017ms │ 0.008ms │ vim.treesitter
0.013ms │ 0.013ms │ nvim-lsp-installer.process
0.013ms │ 0.012ms │ luasnip.util.environ
0.015ms │ 0.009ms │ lewis6991.treesitter
0.015ms │ 0.010ms │ null-ls.config
0.019ms │ 0.006ms │ ts_context_commentstring
0.013ms │ 0.012ms │ cmp_buffer.buffer
0.018ms │ 0.007ms │ null-ls.builtins.diagnostics.shellcheck
0.015ms │ 0.010ms │ null-ls.helpers.make_builtin
0.012ms │ 0.012ms │ diffview.path
0.016ms │ 0.008ms │ null-ls.builtins.diagnostics.gitlint
0.017ms │ 0.007ms │ trouble.providers.telescope
0.013ms │ 0.011ms │ diffview.oop
0.015ms │ 0.010ms │ cmp-rg
0.013ms │ 0.011ms │ cmp.utils.keymap
0.014ms │ 0.011ms │ nvim-treesitter
0.018ms │ 0.007ms │ cmp.utils.highlight
0.016ms │ 0.008ms │ lspconfig.server_configurations.sumneko_lua
0.015ms │ 0.009ms │ colorizer.trie
0.016ms │ 0.007ms │ plenary.vararg.rotate
0.015ms │ 0.009ms │ trouble.config
0.011ms │ 0.012ms │ lspconfig.configs
0.014ms │ 0.009ms │ null-ls.helpers.command_resolver
0.016ms │ 0.007ms │ cmp_tmux.source
0.016ms │ 0.007ms │ lspconfig
0.017ms │ 0.006ms │ plenary.vararg
0.012ms │ 0.011ms │ nvim-lsp-installer.installers.context
0.014ms │ 0.009ms │ cmp.view.native_entries_view
0.014ms │ 0.009ms │ cmp.config.default
0.017ms │ 0.006ms │ tmux.version.parse
0.016ms │ 0.007ms │ gitsigns.message
0.017ms │ 0.006ms │ persistence.config
0.013ms │ 0.010ms │ cmp_nvim_lsp_signature_help
0.012ms │ 0.010ms │ cmp.view.docs_view
0.017ms │ 0.006ms │ cmp.config.sources
0.013ms │ 0.009ms │ luasnip.nodes.restoreNode
0.014ms │ 0.009ms │ vim.ui
0.013ms │ 0.010ms │ luasnip.nodes.insertNode
0.013ms │ 0.010ms │ null-ls.state
0.014ms │ 0.008ms │ lspconfig.server_configurations.vimls
0.016ms │ 0.006ms │ plenary.errors
0.014ms │ 0.008ms │ null-ls.builtins.diagnostics.flake8
0.016ms │ 0.006ms │ null-ls.helpers
0.015ms │ 0.008ms │ null-ls.builtins.diagnostics.luacheck
0.014ms │ 0.008ms │ luasnip.util.mark
0.015ms │ 0.008ms │ cmp.utils.buffer
0.012ms │ 0.010ms │ nvim-lsp-installer.log
0.015ms │ 0.007ms │ luasnip.nodes.util
0.015ms │ 0.007ms │ null-ls.builtins.diagnostics.teal
0.016ms │ 0.006ms │ null-ls.helpers.range_formatting_args_factory
0.012ms │ 0.010ms │ nvim-treesitter.utils
0.015ms │ 0.007ms │ cmp.utils.event
0.013ms │ 0.009ms │ tmux.wrapper.tmux
0.015ms │ 0.007ms │ nvim-treesitter-playground
0.012ms │ 0.010ms │ cmp_buffer.source
0.015ms │ 0.007ms │ cmp_treesitter
0.013ms │ 0.009ms │ luasnip.util.parser
0.015ms │ 0.006ms │ trouble.providers.qf
0.014ms │ 0.008ms │ lewis6991.telescope
0.014ms │ 0.007ms │ cmp_tmux.tmux
0.014ms │ 0.007ms │ cmp_nvim_lsp.source
0.015ms │ 0.006ms │ plenary.reload
0.014ms │ 0.008ms │ buftabline.highlights
0.015ms │ 0.006ms │ trouble.providers.diagnostic
0.015ms │ 0.007ms │ nvim-lsp-installer.core.clients.github
0.014ms │ 0.007ms │ nvim-lsp-installer.installers.shell
0.016ms │ 0.005ms │ cmp-spell
0.014ms │ 0.007ms │ null-ls.builtins
0.013ms │ 0.008ms │ cmp_treesitter.lru
0.016ms │ 0.005ms │ buftabline.tabpages
0.015ms │ 0.006ms │ buftabline.options
0.016ms │ 0.005ms │ lua-dev.config
0.015ms │ 0.006ms │ nvim-lsp-installer.jobs.outdated-servers.cargo
0.014ms │ 0.007ms │ diffview.events
0.013ms │ 0.008ms │ nvim-lsp-installer.fs
0.013ms │ 0.008ms │ cmp.utils.feedkeys
0.013ms │ 0.007ms │ nvim-treesitter.caching
0.013ms │ 0.008ms │ nvim-lsp-installer._generated.language_autocomplete_map
0.013ms │ 0.007ms │ cmp.view.ghost_text_view
0.013ms │ 0.008ms │ cmp_nvim_lsp
0.013ms │ 0.007ms │ luasnip.nodes.functionNode
0.013ms │ 0.007ms │ nvim-lsp-installer.jobs.outdated-servers
0.012ms │ 0.008ms │ nvim-lsp-installer.ui.status-win.components.settings-schema
0.012ms │ 0.009ms │ lewis6991.cmp_gh
0.015ms │ 0.006ms │ luasnip.util.dict
0.013ms │ 0.007ms │ plenary.async
0.014ms │ 0.006ms │ nvim-lsp-installer.installers.composer
0.013ms │ 0.007ms │ cmp_treesitter.treesitter
0.014ms │ 0.006ms │ nvim-lsp-installer.jobs.outdated-servers.gem
0.015ms │ 0.005ms │ nvim-lsp-installer.platform
0.014ms │ 0.006ms │ buftabline.utils
0.013ms │ 0.007ms │ trouble.text
0.011ms │ 0.008ms │ cmp.config
0.013ms │ 0.006ms │ trouble.colors
0.012ms │ 0.007ms │ cmp.utils.misc
0.012ms │ 0.008ms │ nvim-lsp-installer.installers.npm
0.013ms │ 0.007ms │ lspconfig.server_configurations.clangd
0.012ms │ 0.007ms │ cmp_cmdline
0.011ms │ 0.008ms │ cmp.types.lsp
0.014ms │ 0.006ms │ vim.treesitter.language
0.014ms │ 0.006ms │ cmp.config.mapping
0.015ms │ 0.004ms │ luasnip.util.events
0.014ms │ 0.005ms │ luasnip.extras.filetype_functions
0.012ms │ 0.007ms │ cmp.utils.async
0.012ms │ 0.007ms │ cmp.config.compare
0.013ms │ 0.005ms │ cmp_emoji
0.015ms │ 0.004ms │ cmp_buffer
0.011ms │ 0.007ms │ nvim-lsp-installer.core.receipt
0.012ms │ 0.007ms │ nvim-lsp-installer.ui
0.013ms │ 0.006ms │ cmp.utils.api
0.012ms │ 0.007ms │ nvim-lsp-installer.core.fetch
0.013ms │ 0.005ms │ nvim-lsp-installer.jobs.pool
0.011ms │ 0.007ms │ nvim-lsp-installer.installers
0.012ms │ 0.007ms │ nvim-lsp-installer.data
0.013ms │ 0.006ms │ cmp.matcher
0.014ms │ 0.005ms │ tmux
0.011ms │ 0.008ms │ tmux.copy
0.013ms │ 0.005ms │ luasnip.util.types
0.014ms │ 0.004ms │ nvim-lsp-installer.servers.jedi_language_server
0.014ms │ 0.004ms │ nvim-lsp-installer.servers.vimls
0.014ms │ 0.004ms │ cmp.utils.cache
0.013ms │ 0.006ms │ luasnip.util.pattern_tokenizer
0.012ms │ 0.006ms │ luasnip.nodes.textNode
0.013ms │ 0.005ms │ null-ls.helpers.formatter_factory
0.013ms │ 0.006ms │ plenary.async.tests
0.013ms │ 0.005ms │ nvim-lsp-installer.jobs.outdated-servers.version-check-result
0.012ms │ 0.005ms │ nvim-lsp-installer.settings
0.011ms │ 0.006ms │ cmp.context
0.011ms │ 0.006ms │ cmp.utils.str
0.013ms │ 0.004ms │ luasnip.session
0.013ms │ 0.005ms │ nvim-lsp-installer.jobs.outdated-servers.composer
0.012ms │ 0.006ms │ nvim-lsp-installer.servers.sumneko_lua
0.012ms │ 0.005ms │ cmp_buffer.timer
0.011ms │ 0.006ms │ cmp.utils.char
0.013ms │ 0.004ms │ cmp.utils.pattern
0.011ms │ 0.006ms │ nvim-lsp-installer.installers.pip3
0.013ms │ 0.004ms │ luasnip.util.functions
0.013ms │ 0.005ms │ tmux.log.channels
0.012ms │ 0.005ms │ tmux.navigation
0.013ms │ 0.005ms │ trouble.folds
0.012ms │ 0.005ms │ nvim-lsp-installer.ui.status-win.server_hints
0.012ms │ 0.005ms │ nvim-lsp-installer.jobs.outdated-servers.pip3
0.012ms │ 0.005ms │ nvim-lsp-installer.jobs.outdated-servers.npm
0.011ms │ 0.006ms │ cmp.utils.debug
0.013ms │ 0.004ms │ nvim-lsp-installer.notify
0.011ms │ 0.006ms │ tmux.layout
0.013ms │ 0.004ms │ nvim-lsp-installer.servers.bashls
0.012ms │ 0.004ms │ nvim-lsp-installer.dispatcher
0.012ms │ 0.005ms │ buftabline.tabpage-tab
0.012ms │ 0.005ms │ nvim-lsp-installer.path
0.010ms │ 0.006ms │ tmux.resize
0.013ms │ 0.004ms │ cmp.types.vim
0.012ms │ 0.004ms │ nvim-lsp-installer.ui.state
0.011ms │ 0.005ms │ nvim-lsp-installer.installers.gem
0.012ms │ 0.005ms │ tmux.configuration.options
0.012ms │ 0.005ms │ nvim-lsp-installer.jobs.outdated-servers.git
0.012ms │ 0.004ms │ nvim-lsp-installer.jobs.outdated-servers.github_release_file
0.012ms │ 0.005ms │ cmp.types.cmp
0.013ms │ 0.004ms │ cmp.types
0.011ms │ 0.005ms │ tmux.log
0.011ms │ 0.005ms │ tmux.navigation.navigate
0.012ms │ 0.005ms │ tmux.configuration
0.012ms │ 0.004ms │ nvim-lsp-installer.jobs.outdated-servers.github_tag
0.011ms │ 0.005ms │ tmux.layout.parse
0.012ms │ 0.004ms │ nvim-lsp-installer.jobs.outdated-servers.jdtls
0.011ms │ 0.005ms │ tmux.log.convert
0.011ms │ 0.005ms │ tmux.log.severity
0.011ms │ 0.004ms │ tmux.version
0.012ms │ 0.004ms │ nvim-lsp-installer.core.clients.eclipse
0.011ms │ 0.004ms │ nvim-lsp-installer.core.clients.crates
0.011ms │ 0.004ms │ tmux.configuration.logging
0.011ms │ 0.004ms │ tmux.wrapper.nvim
0.011ms │ 0.004ms │ tmux.configuration.validate
0.011ms │ 0.004ms │ tmux.keymaps
────────────┴────────────┴─────────────────────────────────────────────────────────────────
```
</details>
Total resolve: 6.357ms, total load: 6.796ms
## Relevant Neovim PR's
[libs: vendor libmpack and libmpack-lua](https://github.com/neovim/neovim/pull/15566) [merged]
[fix(vim.mpack): rename pack/unpack => encode/decode](https://github.com/neovim/neovim/pull/16175) [merged]
[fix(runtime): add compressed representation to &rtp](https://github.com/neovim/neovim/pull/15867) [merged]
[fix(runtime): don't use regexes inside lua require'mod'](https://github.com/neovim/neovim/pull/15973) [merged]
[fix(lua): restore priority of the preloader](https://github.com/neovim/neovim/pull/17302) [merged]
[refactor(lua): call loadfile internally instead of luaL_loadfile](https://github.com/neovim/neovim/pull/17200) [merged]
[feat(lua): startup profiling](https://github.com/neovim/neovim/pull/15436)
## Credit
All credit goes to @bfredl who implemented the majority of this plugin in https://github.com/neovim/neovim/pull/15436.

View File

@ -0,0 +1,470 @@
local vim = vim
local api = vim.api
local uv = vim.loop
local _loadfile = loadfile
local get_runtime = api.nvim__get_runtime
local fs_stat = uv.fs_stat
local mpack = vim.mpack
local loadlib = package.loadlib
local std_cache = vim.fn.stdpath('cache')
local sep = ''
if (jit.os == 'Windows') then
sep = '\\'
else
sep = '/'
end
local std_dirs = {
['<APPDIR>'] = os.getenv('APPDIR'),
['<VIMRUNTIME>'] = os.getenv('VIMRUNTIME'),
['<STD_DATA>'] = vim.fn.stdpath('data'),
['<STD_CONFIG>'] = vim.fn.stdpath('config'),
}
local function modpath_mangle(modpath)
for name, dir in pairs(std_dirs) do
modpath = modpath:gsub(dir, name)
end
return modpath
end
local function modpath_unmangle(modpath)
for name, dir in pairs(std_dirs) do
modpath = modpath:gsub(name, dir)
end
return modpath
end
-- Overridable by user
local default_config = {
chunks = {
enable = true,
path = std_cache .. sep .. 'luacache_chunks',
},
modpaths = {
enable = true,
path = std_cache.. sep .. 'luacache_modpaths',
},
}
-- State used internally
local default_state = {
chunks = {
cache = {},
profile = nil,
dirty = false,
get = function(self, path)
return self.cache[modpath_mangle(path)]
end,
set = function(self, path, chunk)
self.cache[modpath_mangle(path)] = chunk
end
},
modpaths = {
cache = {},
profile = nil,
dirty = false,
get = function(self, mod)
if self.cache[mod] then
return modpath_unmangle(self.cache[mod])
end
end,
set = function(self, mod, path)
self.cache[mod] = modpath_mangle(path)
end
},
log = {}
}
---@diagnostic disable-next-line: undefined-field
local M = vim.tbl_deep_extend('keep', _G.__luacache_config or {}, default_config, default_state)
_G.__luacache = M
local function log(...)
M.log[#M.log+1] = table.concat({string.format(...)}, ' ')
end
local function print_log()
for _, l in ipairs(M.log) do
print(l)
end
end
local function hash(modpath)
local stat = fs_stat(modpath)
if stat then
return stat.mtime.sec..stat.mtime.nsec..stat.size
end
error('Could not hash '..modpath)
end
local function profile(m, entry, name, loader)
if m.profile then
local mp = m.profile
mp[entry] = mp[entry] or {}
if not mp[entry].loader and loader then
mp[entry].loader = loader
end
if not mp[entry][name] then
mp[entry][name] = uv.hrtime()
end
end
end
local function mprofile(mod, name, loader)
profile(M.modpaths, mod, name, loader)
end
local function cprofile(path, name, loader)
if M.chunks.profile then
path = modpath_mangle(path)
end
profile(M.chunks, path, name, loader)
end
function M.enable_profile()
local P = require('impatient.profile')
M.chunks.profile = {}
M.modpaths.profile = {}
loadlib = function(path, fun)
cprofile(path, 'load_start')
local f, err = package.loadlib(path, fun)
cprofile(path, 'load_end', 'standard')
return f, err
end
P.setup(M.modpaths.profile)
api.nvim_create_user_command('LuaCacheProfile', function()
P.print_profile(M, std_dirs)
end, {})
end
local function get_runtime_file_from_parent(basename, paths)
-- Look in the cache to see if we have already loaded a parent module.
-- If we have then try looking in the parents directory first.
local parents = vim.split(basename, sep)
for i = #parents, 1, -1 do
local parent = table.concat(vim.list_slice(parents, 1, i), sep)
local ppath = M.modpaths:get(parent)
if ppath then
if (ppath:sub(-9) == (sep .. 'init.lua')) then
ppath = ppath:sub(1, -10) -- a/b/init.lua -> a/b
else
ppath = ppath:sub(1, -5) -- a/b.lua -> a/b
end
for _, path in ipairs(paths) do
-- path should be of form 'a/b/c.lua' or 'a/b/c/init.lua'
local modpath = ppath..sep..path:sub(#('lua'..sep..parent)+2)
if fs_stat(modpath) then
return modpath, 'cache(p)'
end
end
end
end
end
local rtp = vim.split(vim.o.rtp, ',')
-- Make sure modpath is in rtp and that modpath is in paths.
local function validate_modpath(modpath, paths)
local match = false
for _, p in ipairs(paths) do
if vim.endswith(modpath, p) then
match = true
break
end
end
if not match then
return false
end
for _, dir in ipairs(rtp) do
if vim.startswith(modpath, dir) then
return fs_stat(modpath) ~= nil
end
end
return false
end
local function get_runtime_file_cached(basename, paths)
local modpath, loader
local mp = M.modpaths
if mp.enable then
local modpath_cached = mp:get(basename)
if modpath_cached then
modpath, loader = modpath_cached, 'cache'
else
modpath, loader = get_runtime_file_from_parent(basename, paths)
end
if modpath and not validate_modpath(modpath, paths) then
modpath = nil
-- Invalidate
mp.cache[basename] = nil
mp.dirty = true
end
end
if not modpath then
-- What Neovim does by default; slowest
modpath, loader = get_runtime(paths, false, {is_lua=true})[1], 'standard'
end
if modpath then
mprofile(basename, 'resolve_end', loader)
if mp.enable and loader ~= 'cache' then
log('Creating cache for module %s', basename)
mp:set(basename, modpath)
mp.dirty = true
end
end
return modpath
end
local function extract_basename(pats)
local basename
-- Deconstruct basename from pats
for _, pat in ipairs(pats) do
for i, npat in ipairs{
-- Ordered by most specific
'lua'.. sep ..'(.*)'..sep..'init%.lua',
'lua'.. sep ..'(.*)%.lua'
} do
local m = pat:match(npat)
if i == 2 and m and m:sub(-4) == 'init' then
m = m:sub(0, -6)
end
if not basename then
if m then
basename = m
end
elseif m and m ~= basename then
-- matches are inconsistent
return
end
end
end
return basename
end
local function get_runtime_cached(pats, all, opts)
local fallback = false
if all or not opts or not opts.is_lua then
-- Fallback
fallback = true
end
local basename
if not fallback then
basename = extract_basename(pats)
end
if fallback or not basename then
return get_runtime(pats, all, opts)
end
return {get_runtime_file_cached(basename, pats)}
end
-- Copied from neovim/src/nvim/lua/vim.lua with two lines changed
local function load_package(name)
local basename = name:gsub('%.', sep)
local paths = {"lua"..sep..basename..".lua", "lua"..sep..basename..sep.."init.lua"}
-- Original line:
-- local found = vim.api.nvim__get_runtime(paths, false, {is_lua=true})
local found = {get_runtime_file_cached(basename, paths)}
if #found > 0 then
local f, err = loadfile(found[1])
return f or error(err)
end
local so_paths = {}
for _,trail in ipairs(vim._so_trails) do
local path = "lua"..trail:gsub('?', basename) -- so_trails contains a leading slash
table.insert(so_paths, path)
end
-- Original line:
-- found = vim.api.nvim__get_runtime(so_paths, false, {is_lua=true})
found = {get_runtime_file_cached(basename, so_paths)}
if #found > 0 then
-- Making function name in Lua 5.1 (see src/loadlib.c:mkfuncname) is
-- a) strip prefix up to and including the first dash, if any
-- b) replace all dots by underscores
-- c) prepend "luaopen_"
-- So "foo-bar.baz" should result in "luaopen_bar_baz"
local dash = name:find("-", 1, true)
local modname = dash and name:sub(dash + 1) or name
local f, err = loadlib(found[1], "luaopen_"..modname:gsub("%.", "_"))
return f or error(err)
end
return nil
end
local function load_from_cache(path)
local mc = M.chunks
local cache = mc:get(path)
if not cache then
return nil, string.format('No cache for path %s', path)
end
local mhash, codes = unpack(cache)
if mhash ~= hash(path) then
mc:set(path)
mc.dirty = true
return nil, string.format('Stale cache for path %s', path)
end
local chunk = loadstring(codes)
if not chunk then
mc:set(path)
mc.dirty = true
return nil, string.format('Cache error for path %s', path)
end
return chunk
end
local function loadfile_cached(path)
cprofile(path, 'load_start')
local chunk, err
if M.chunks.enable then
chunk, err = load_from_cache(path)
if chunk and not err then
log('Loaded cache for path %s', path)
cprofile(path, 'load_end', 'cache')
return chunk
end
log(err)
end
chunk, err = _loadfile(path)
if not err and M.chunks.enable then
log('Creating cache for path %s', path)
M.chunks:set(path, {hash(path), string.dump(chunk)})
M.chunks.dirty = true
end
cprofile(path, 'load_end', 'standard')
return chunk, err
end
function M.save_cache()
local function _save_cache(t)
if not t.enable then
return
end
if t.dirty then
log('Updating chunk cache file: %s', t.path)
local f = assert(io.open(t.path, 'w+b'))
f:write(mpack.encode(t.cache))
f:flush()
t.dirty = false
end
end
_save_cache(M.chunks)
_save_cache(M.modpaths)
end
local function clear_cache()
local function _clear_cache(t)
t.cache = {}
os.remove(t.path)
end
_clear_cache(M.chunks)
_clear_cache(M.modpaths)
end
local function init_cache()
local function _init_cache(t)
if not t.enable then
return
end
if fs_stat(t.path) then
log('Loading cache file %s', t.path)
local f = assert(io.open(t.path, 'rb'))
local ok
ok, t.cache = pcall(function()
return mpack.decode(f:read'*a')
end)
if not ok then
log('Corrupted cache file, %s. Invalidating...', t.path)
os.remove(t.path)
t.cache = {}
end
t.dirty = not ok
end
end
if not uv.fs_stat(std_cache) then
vim.fn.mkdir(std_cache, 'p')
end
_init_cache(M.chunks)
_init_cache(M.modpaths)
end
local function setup()
init_cache()
-- Usual package loaders
-- 1. package.preload
-- 2. vim._load_package
-- 3. package.path
-- 4. package.cpath
-- 5. all-in-one
-- Override default functions
for i, loader in ipairs(package.loaders) do
if loader == vim._load_package then
package.loaders[i] = load_package
break
end
end
vim._load_package = load_package
vim.api.nvim__get_runtime = get_runtime_cached
loadfile = loadfile_cached
local augroup = api.nvim_create_augroup('impatient', {})
api.nvim_create_user_command('LuaCacheClear', clear_cache, {})
api.nvim_create_user_command('LuaCacheLog' , print_log , {})
api.nvim_create_autocmd({'VimEnter', 'VimLeave'}, {
group = augroup,
callback = M.save_cache
})
api.nvim_create_autocmd('OptionSet', {
group = augroup,
pattern = 'runtimepath',
callback = function()
rtp = vim.split(vim.o.rtp, ',')
end
})
end
setup()
return M

View File

@ -0,0 +1,253 @@
local M = {}
local sep = ''
if (jit.os == 'Windows') then
sep = '\\'
else
sep = '/'
end
local api, uv = vim.api, vim.loop
local function load_buffer(title, lines)
local bufnr = api.nvim_create_buf(false, false)
api.nvim_buf_set_lines(bufnr, 0, 0, false, lines)
api.nvim_buf_set_option(bufnr, 'bufhidden', 'wipe')
api.nvim_buf_set_option(bufnr, 'buftype', 'nofile')
api.nvim_buf_set_option(bufnr, 'swapfile', false)
api.nvim_buf_set_option(bufnr, "modifiable", false)
api.nvim_buf_set_name(bufnr, title)
api.nvim_set_current_buf(bufnr)
end
local function time_tostr(x)
if x == 0 then
return '?'
end
return string.format('%8.3fms', x)
end
local function mem_tostr(x)
local unit = ''
for _, u in ipairs{'K', 'M', 'G'} do
if x < 1000 then
break
end
x = x / 1000
unit = u
end
return string.format('%1.1f%s', x, unit)
end
function M.print_profile(I, std_dirs)
local mod_profile = I.modpaths.profile
local chunk_profile = I.chunks.profile
if not mod_profile and not chunk_profile then
print('Error: profiling was not enabled')
return
end
local total_resolve = 0
local total_load = 0
local modules = {}
for path, m in pairs(chunk_profile) do
m.load = m.load_end - m.load_start
m.load = m.load / 1000000
m.path = path or '?'
end
local module_content_width = 0
local unloaded = {}
for module, m in pairs(mod_profile) do
local module_dot = module:gsub(sep, '.')
m.module = module_dot
if not package.loaded[module_dot] and not package.loaded[module] then
unloaded[#unloaded+1] = m
else
m.resolve = 0
if m.resolve_start and m.resolve_end then
m.resolve = m.resolve_end - m.resolve_start
m.resolve = m.resolve / 1000000
end
m.loader = m.loader or m.loader_guess
local path = I.modpaths.cache[module]
local path_prof = chunk_profile[path]
m.path = path or '?'
if path_prof then
chunk_profile[path] = nil
m.load = path_prof.load
m.ploader = path_prof.loader
else
m.load = 0
m.ploader = 'NA'
end
total_resolve = total_resolve + m.resolve
total_load = total_load + m.load
if #module > module_content_width then
module_content_width = #module
end
modules[#modules+1] = m
end
end
table.sort(modules, function(a, b)
return (a.resolve + a.load) > (b.resolve + b.load)
end)
local paths = {}
local total_paths_load = 0
for _, m in pairs(chunk_profile) do
paths[#paths+1] = m
total_paths_load = total_paths_load + m.load
end
table.sort(paths, function(a, b)
return a.load > b.load
end)
local lines = {}
local function add(fmt, ...)
local args = {...}
for i, a in ipairs(args) do
if type(a) == 'number' then
args[i] = time_tostr(a)
end
end
lines[#lines+1] = string.format(fmt, unpack(args))
end
local time_cell_width = 12
local loader_cell_width = 11
local time_content_width = time_cell_width - 2
local loader_content_width = loader_cell_width - 2
local module_cell_width = module_content_width + 2
local tcwl = string.rep('', time_cell_width)
local lcwl = string.rep('', loader_cell_width)
local mcwl = string.rep('', module_cell_width+2)
local n = string.rep('', 200)
local module_cell_format = '%-'..module_cell_width..'s'
local loader_format = '%-'..loader_content_width..'s'
local line_format = '%s │ %s │ %s │ %s │ %s │ %s'
local row_fmt = line_format:format(
' %'..time_content_width..'s',
loader_format,
'%'..time_content_width..'s',
loader_format,
module_cell_format,
'%s')
local title_fmt = line_format:format(
' %-'..time_content_width..'s',
loader_format,
'%-'..time_content_width..'s',
loader_format,
module_cell_format,
'%s')
local title1_width = time_cell_width+loader_cell_width-1
local title1_fmt = ('%s │ %s │'):format(
' %-'..title1_width..'s', '%-'..title1_width..'s')
add('Note: this report is not a measure of startup time. Only use this for comparing')
add('between cached and uncached loads of Lua modules')
add('')
add('Cache files:')
for _, f in ipairs{ I.chunks.path, I.modpaths.path } do
local size = vim.loop.fs_stat(f).size
add(' %s %s', f, mem_tostr(size))
end
add('')
add('Standard directories:')
for alias, path in pairs(std_dirs) do
add(' %-12s -> %s', alias, path)
end
add('')
add('%s─%s┬%s─%s┐', tcwl, lcwl, tcwl, lcwl)
add(title1_fmt, 'Resolve', 'Load')
add('%s┬%s┼%s┬%s┼%s┬%s', tcwl, lcwl, tcwl, lcwl, mcwl, n)
add(title_fmt, 'Time', 'Method', 'Time', 'Method', 'Module', 'Path')
add('%s┼%s┼%s┼%s┼%s┼%s', tcwl, lcwl, tcwl, lcwl, mcwl, n)
add(row_fmt, total_resolve, '', total_load, '', 'Total', '')
add('%s┼%s┼%s┼%s┼%s┼%s', tcwl, lcwl, tcwl, lcwl, mcwl, n)
for _, p in ipairs(modules) do
add(row_fmt, p.resolve, p.loader, p.load, p.ploader, p.module, p.path)
end
add('%s┴%s┴%s┴%s┴%s┴%s', tcwl, lcwl, tcwl, lcwl, mcwl, n)
if #paths > 0 then
add('')
add(n)
local f3 = ' %'..time_content_width..'s │ %'..loader_content_width..'s │ %s'
add('Files loaded with no associated module')
add('%s┬%s┬%s', tcwl, lcwl, n)
add(f3, 'Time', 'Loader', 'Path')
add('%s┼%s┼%s', tcwl, lcwl, n)
add(f3, total_paths_load, '', 'Total')
add('%s┼%s┼%s', tcwl, lcwl, n)
for _, p in ipairs(paths) do
add(f3, p.load, p.loader, p.path)
end
add('%s┴%s┴%s', tcwl, lcwl, n)
end
if #unloaded > 0 then
add('')
add(n)
add('Modules which were unable to loaded')
add(n)
for _, p in ipairs(unloaded) do
lines[#lines+1] = p.module
end
add(n)
end
load_buffer('Impatient Profile Report', lines)
end
M.setup = function(profile)
local _require = require
require = function(mod)
local basename = mod:gsub('%.', sep)
if not profile[basename] then
profile[basename] = {}
profile[basename].resolve_start = uv.hrtime()
profile[basename].loader_guess = ''
end
return _require(mod)
end
-- Add profiling around all the loaders
local pl = package.loaders
for i = 1, #pl do
local l = pl[i]
pl[i] = function(mod)
local basename = mod:gsub('%.', sep)
profile[basename].loader_guess = i == 1 and 'preloader' or 'loader #'..i
return l(mod)
end
end
end
return M

View File

@ -0,0 +1,280 @@
local helpers = require('test.functional.helpers')()
local clear = helpers.clear
local exec_lua = helpers.exec_lua
local eq = helpers.eq
local cmd = helpers.command
local nvim07
local function gen_exp(exp)
local neovim_dir = nvim07 and 'neovim-v0.7.0' or 'neovim-master'
local cwd = exec_lua('return vim.loop.cwd()')
local exp1 = {}
for _, v in pairs(exp) do
if type(v) == 'string' then
v = v:gsub('{CWD}', cwd)
v = v:gsub('{NVIM}', neovim_dir)
exp1[#exp1+1] = v
end
end
return exp1
end
local gen_exp_cold = function()
return gen_exp{
'Creating cache for module plugins',
'No cache for path ./test/lua/plugins.lua',
'Creating cache for path ./test/lua/plugins.lua',
'Creating cache for module telescope',
'No cache for path {CWD}/scratch/telescope.nvim/lua/telescope/init.lua',
'Creating cache for path {CWD}/scratch/telescope.nvim/lua/telescope/init.lua',
'Creating cache for module telescope/_extensions',
'No cache for path {CWD}/scratch/telescope.nvim/lua/telescope/_extensions/init.lua',
'Creating cache for path {CWD}/scratch/telescope.nvim/lua/telescope/_extensions/init.lua',
'Creating cache for module gitsigns',
'No cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns.lua',
'Creating cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns.lua',
'Creating cache for module plenary/async/async',
'No cache for path {CWD}/scratch/plenary.nvim/lua/plenary/async/async.lua',
'Creating cache for path {CWD}/scratch/plenary.nvim/lua/plenary/async/async.lua',
'Creating cache for module plenary/vararg',
'No cache for path {CWD}/scratch/plenary.nvim/lua/plenary/vararg/init.lua',
'Creating cache for path {CWD}/scratch/plenary.nvim/lua/plenary/vararg/init.lua',
'Creating cache for module plenary/vararg/rotate',
'No cache for path {CWD}/scratch/plenary.nvim/lua/plenary/vararg/rotate.lua',
'Creating cache for path {CWD}/scratch/plenary.nvim/lua/plenary/vararg/rotate.lua',
'Creating cache for module plenary/tbl',
'No cache for path {CWD}/scratch/plenary.nvim/lua/plenary/tbl.lua',
'Creating cache for path {CWD}/scratch/plenary.nvim/lua/plenary/tbl.lua',
'Creating cache for module plenary/errors',
'No cache for path {CWD}/scratch/plenary.nvim/lua/plenary/errors.lua',
'Creating cache for path {CWD}/scratch/plenary.nvim/lua/plenary/errors.lua',
'Creating cache for module plenary/functional',
'No cache for path {CWD}/scratch/plenary.nvim/lua/plenary/functional.lua',
'Creating cache for path {CWD}/scratch/plenary.nvim/lua/plenary/functional.lua',
'Creating cache for module plenary/async/util',
'No cache for path {CWD}/scratch/plenary.nvim/lua/plenary/async/util.lua',
'Creating cache for path {CWD}/scratch/plenary.nvim/lua/plenary/async/util.lua',
'Creating cache for module plenary/async/control',
'No cache for path {CWD}/scratch/plenary.nvim/lua/plenary/async/control.lua',
'Creating cache for path {CWD}/scratch/plenary.nvim/lua/plenary/async/control.lua',
'Creating cache for module plenary/async/structs',
'No cache for path {CWD}/scratch/plenary.nvim/lua/plenary/async/structs.lua',
'Creating cache for path {CWD}/scratch/plenary.nvim/lua/plenary/async/structs.lua',
'Creating cache for module gitsigns/status',
'No cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/status.lua',
'Creating cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/status.lua',
'Creating cache for module gitsigns/git',
'No cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/git.lua',
'Creating cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/git.lua',
'Creating cache for module plenary/job',
'No cache for path {CWD}/scratch/plenary.nvim/lua/plenary/job.lua',
'Creating cache for path {CWD}/scratch/plenary.nvim/lua/plenary/job.lua',
'Creating cache for module gitsigns/debug',
'No cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/debug.lua',
'Creating cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/debug.lua',
'Creating cache for module gitsigns/util',
'No cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/util.lua',
'Creating cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/util.lua',
'Creating cache for module gitsigns/hunks',
'No cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/hunks.lua',
'Creating cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/hunks.lua',
'Creating cache for module gitsigns/signs',
'No cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/signs.lua',
'Creating cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/signs.lua',
'Creating cache for module gitsigns/config',
'No cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/config.lua',
'Creating cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/config.lua',
'Creating cache for module gitsigns/manager',
'No cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/manager.lua',
'Creating cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/manager.lua',
'Creating cache for module gitsigns/cache',
'No cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/cache.lua',
'Creating cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/cache.lua',
'Creating cache for module gitsigns/debounce',
'No cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/debounce.lua',
'Creating cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/debounce.lua',
'Creating cache for module gitsigns/highlight',
'No cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/highlight.lua',
'Creating cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/highlight.lua',
'Creating cache for module spellsitter',
'No cache for path {CWD}/scratch/spellsitter.nvim/lua/spellsitter.lua',
'Creating cache for path {CWD}/scratch/spellsitter.nvim/lua/spellsitter.lua',
'Creating cache for module vim/treesitter/query',
'No cache for path {CWD}/{NVIM}/runtime/lua/vim/treesitter/query.lua',
'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/treesitter/query.lua',
'Creating cache for module vim/treesitter/language',
'No cache for path {CWD}/{NVIM}/runtime/lua/vim/treesitter/language.lua',
'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/treesitter/language.lua',
'Creating cache for module vim/treesitter',
'No cache for path {CWD}/{NVIM}/runtime/lua/vim/treesitter.lua',
'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/treesitter.lua',
'Creating cache for module vim/treesitter/languagetree',
'No cache for path {CWD}/{NVIM}/runtime/lua/vim/treesitter/languagetree.lua',
'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/treesitter/languagetree.lua',
'Creating cache for module colorizer',
'No cache for path {CWD}/scratch/nvim-colorizer.lua/lua/colorizer.lua',
'Creating cache for path {CWD}/scratch/nvim-colorizer.lua/lua/colorizer.lua',
'Creating cache for module colorizer/nvim',
'No cache for path {CWD}/scratch/nvim-colorizer.lua/lua/colorizer/nvim.lua',
'Creating cache for path {CWD}/scratch/nvim-colorizer.lua/lua/colorizer/nvim.lua',
'Creating cache for module colorizer/trie',
'No cache for path {CWD}/scratch/nvim-colorizer.lua/lua/colorizer/trie.lua',
'Creating cache for path {CWD}/scratch/nvim-colorizer.lua/lua/colorizer/trie.lua',
'Creating cache for module lspconfig',
'No cache for path {CWD}/scratch/nvim-lspconfig/lua/lspconfig.lua',
'Creating cache for path {CWD}/scratch/nvim-lspconfig/lua/lspconfig.lua',
'Creating cache for module lspconfig/configs',
'No cache for path {CWD}/scratch/nvim-lspconfig/lua/lspconfig/configs.lua',
'Creating cache for path {CWD}/scratch/nvim-lspconfig/lua/lspconfig/configs.lua',
'Creating cache for module lspconfig/util',
'No cache for path {CWD}/scratch/nvim-lspconfig/lua/lspconfig/util.lua',
'Creating cache for path {CWD}/scratch/nvim-lspconfig/lua/lspconfig/util.lua',
'Creating cache for module vim/lsp',
'No cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp.lua',
'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp.lua',
'Creating cache for module vim/lsp/handlers',
'No cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/handlers.lua',
'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/handlers.lua',
'Creating cache for module vim/lsp/log',
'No cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/log.lua',
'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/log.lua',
'Creating cache for module vim/lsp/protocol',
'No cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/protocol.lua',
'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/protocol.lua',
'Creating cache for module vim/lsp/util',
'No cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/util.lua',
'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/util.lua',
'Creating cache for module vim/lsp/_snippet',
'No cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/_snippet.lua',
'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/_snippet.lua',
'Creating cache for module vim/highlight',
'No cache for path {CWD}/{NVIM}/runtime/lua/vim/highlight.lua',
'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/highlight.lua',
'Creating cache for module vim/lsp/rpc',
'No cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/rpc.lua',
'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/rpc.lua',
'Creating cache for module vim/lsp/sync',
'No cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/sync.lua',
'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/sync.lua',
'Creating cache for module vim/lsp/buf',
'No cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/buf.lua',
'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/buf.lua',
'Creating cache for module vim/lsp/diagnostic',
'No cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/diagnostic.lua',
'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/diagnostic.lua',
'Creating cache for module vim/lsp/codelens',
'No cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/codelens.lua',
'Creating cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/codelens.lua',
'Creating cache for module bufferline',
'No cache for path {CWD}/scratch/bufferline.nvim/lua/bufferline.lua',
'Creating cache for path {CWD}/scratch/bufferline.nvim/lua/bufferline.lua',
'Creating cache for module bufferline/constants',
'No cache for path {CWD}/scratch/bufferline.nvim/lua/bufferline/constants.lua',
'Creating cache for path {CWD}/scratch/bufferline.nvim/lua/bufferline/constants.lua',
'Creating cache for module bufferline/utils',
'No cache for path {CWD}/scratch/bufferline.nvim/lua/bufferline/utils.lua',
'Creating cache for path {CWD}/scratch/bufferline.nvim/lua/bufferline/utils.lua',
'Updating chunk cache file: scratch/cache/nvim/luacache_chunks',
'Updating chunk cache file: scratch/cache/nvim/luacache_modpaths'
}
end
local gen_exp_hot = function()
return gen_exp{
'Loading cache file scratch/cache/nvim/luacache_chunks',
'Loading cache file scratch/cache/nvim/luacache_modpaths',
'Loaded cache for path ./test/lua/plugins.lua',
'Loaded cache for path {CWD}/scratch/telescope.nvim/lua/telescope/init.lua',
'Loaded cache for path {CWD}/scratch/telescope.nvim/lua/telescope/_extensions/init.lua',
'Loaded cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns.lua',
'Loaded cache for path {CWD}/scratch/plenary.nvim/lua/plenary/async/async.lua',
'Loaded cache for path {CWD}/scratch/plenary.nvim/lua/plenary/vararg/init.lua',
'Loaded cache for path {CWD}/scratch/plenary.nvim/lua/plenary/vararg/rotate.lua',
'Loaded cache for path {CWD}/scratch/plenary.nvim/lua/plenary/tbl.lua',
'Loaded cache for path {CWD}/scratch/plenary.nvim/lua/plenary/errors.lua',
'Loaded cache for path {CWD}/scratch/plenary.nvim/lua/plenary/functional.lua',
'Loaded cache for path {CWD}/scratch/plenary.nvim/lua/plenary/async/util.lua',
'Loaded cache for path {CWD}/scratch/plenary.nvim/lua/plenary/async/control.lua',
'Loaded cache for path {CWD}/scratch/plenary.nvim/lua/plenary/async/structs.lua',
'Loaded cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/status.lua',
'Loaded cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/git.lua',
'Loaded cache for path {CWD}/scratch/plenary.nvim/lua/plenary/job.lua',
'Loaded cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/debug.lua',
'Loaded cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/util.lua',
'Loaded cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/hunks.lua',
'Loaded cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/signs.lua',
'Loaded cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/config.lua',
'Loaded cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/manager.lua',
'Loaded cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/cache.lua',
'Loaded cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/debounce.lua',
'Loaded cache for path {CWD}/scratch/gitsigns.nvim/lua/gitsigns/highlight.lua',
'Loaded cache for path {CWD}/scratch/spellsitter.nvim/lua/spellsitter.lua',
'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/treesitter/query.lua',
'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/treesitter/language.lua',
'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/treesitter.lua',
'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/treesitter/languagetree.lua',
'Loaded cache for path {CWD}/scratch/nvim-colorizer.lua/lua/colorizer.lua',
'Loaded cache for path {CWD}/scratch/nvim-colorizer.lua/lua/colorizer/nvim.lua',
'Loaded cache for path {CWD}/scratch/nvim-colorizer.lua/lua/colorizer/trie.lua',
'Loaded cache for path {CWD}/scratch/nvim-lspconfig/lua/lspconfig.lua',
'Loaded cache for path {CWD}/scratch/nvim-lspconfig/lua/lspconfig/configs.lua',
'Loaded cache for path {CWD}/scratch/nvim-lspconfig/lua/lspconfig/util.lua',
'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp.lua',
'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/handlers.lua',
'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/log.lua',
'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/protocol.lua',
'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/util.lua',
'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/_snippet.lua',
'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/highlight.lua',
'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/rpc.lua',
'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/sync.lua',
'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/buf.lua',
'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/diagnostic.lua',
'Loaded cache for path {CWD}/{NVIM}/runtime/lua/vim/lsp/codelens.lua',
'Loaded cache for path {CWD}/scratch/bufferline.nvim/lua/bufferline.lua',
'Loaded cache for path {CWD}/scratch/bufferline.nvim/lua/bufferline/constants.lua',
'Loaded cache for path {CWD}/scratch/bufferline.nvim/lua/bufferline/utils.lua'
}
end
describe('impatient', function()
local function reset()
clear()
nvim07 = exec_lua('return vim.version().minor') == 7
cmd [[set runtimepath=$VIMRUNTIME,.,./test]]
cmd [[let $XDG_CACHE_HOME='scratch/cache']]
cmd [[set packpath=]]
end
before_each(function()
reset()
end)
it('load plugins without impatient', function()
exec_lua([[require('plugins')]])
end)
local function run()
exec_lua[[
require('impatient')
require('plugins')
_G.__luacache.save_cache()
]]
end
it('creates cache', function()
os.execute[[rm -rf scratch/cache]]
run()
eq(gen_exp_cold(), exec_lua("return _G.__luacache.log"))
end)
it('loads cache', function()
run()
eq(gen_exp_hot(), exec_lua("return _G.__luacache.log"))
end)
end)

View File

@ -0,0 +1,43 @@
local init = {
['neovim/nvim-lspconfig'] = '2f026f21',
['nvim-lua/plenary.nvim'] = '06266e7b',
['nvim-lua/telescope.nvim'] = 'ac42f0c2',
['lewis6991/gitsigns.nvim'] = 'daa233aa',
['lewis6991/spellsitter.nvim'] = '7f9e8471',
['norcalli/nvim-colorizer.lua'] = '36c610a9',
['akinsho/bufferline.nvim'] = 'bede234e'
}
local testdir = 'scratch'
vim.fn.system{"mkdir", testdir}
for plugin, sha in pairs(init) do
local plugin_dir = plugin:match('.*/(.*)')
local plugin_dir2 = testdir..'/'..plugin_dir
vim.fn.system{
'git', '-C', testdir, 'clone',
'https://github.com/'..plugin, plugin_dir
}
-- local rev = (vim.fn.system{
-- 'git', '-C', plugin_dir2,
-- 'rev-list', 'HEAD', '-n', '1', '--first-parent', '--before=2021-09-05'
-- }):sub(1,-2)
-- if sha then
-- assert(vim.startswith(rev, sha), ('Plugin sha for %s does match %s != %s'):format(plugin, rev, sha))
-- end
vim.fn.system{'git', '-C', plugin_dir2, 'checkout', sha}
vim.opt.rtp:prepend(vim.loop.fs_realpath("scratch/"..plugin_dir))
end
require'telescope'
require'gitsigns'
require'spellsitter'
require'colorizer'
require'lspconfig'
require'bufferline'

View File

@ -0,0 +1,10 @@
-- Modules loaded here will not be cleared and reloaded by Busted.
-- Busted started doing this to help provide more isolation.
local global_helpers = require('test.helpers')
-- Bypoass CI behaviour logic
global_helpers.isCI = function(_)
return false
end
local helpers = require('test.functional.helpers')()

View File

@ -0,0 +1,51 @@
name: Bug report
description: Report a problem
labels: [bug]
body:
- type: checkboxes
attributes:
label: Contributing guidelines
options:
- label: I have read [CONTRIBUTING.md](https://github.com/echasnovski/mini.nvim/blob/main/CONTRIBUTING.md)
required: true
- label: I have read [CODE_OF_CONDUCT.md](https://github.com/echasnovski/mini.nvim/blob/main/CODE_OF_CONDUCT.md)
required: true
- label: I have updated 'mini.nvim' to latest version
required: true
- type: input
attributes:
label: "Module(s)"
description: "List one or several modules this bug is coming from"
validations:
required: true
- type: textarea
attributes:
label: "Description"
description: "A short description of a problem"
validations:
required: true
- type: input
attributes:
label: "Neovim version"
description: "Something like `0.5`, `0.5.1`, Neovim nightly (please, include latest commit)"
validations:
required: true
- type: textarea
attributes:
label: "Steps to reproduce"
description: "Steps to reproduce using as minimal config as possible"
value: |
1. `nvim -nu minimal.lua`
2. ...
validations:
required: true
- type: textarea
attributes:
label: "Expected behavior"
description: "A description of behavior you expected"
- type: textarea
attributes:
label: "Actual behavior"
description: "A description of behavior you observed (feel free to include images, gifs, etc.)"
validations:
required: true

View File

@ -0,0 +1 @@
blank_issues_enabled: true

View File

@ -0,0 +1,24 @@
name: Feature request
description: Describe a feature you want to see
labels: [feature-request]
body:
- type: checkboxes
attributes:
label: Contributing guidelines
options:
- label: I have read [CONTRIBUTING.md](https://github.com/echasnovski/mini.nvim/blob/main/CONTRIBUTING.md)
required: true
- label: I have read [CODE_OF_CONDUCT.md](https://github.com/echasnovski/mini.nvim/blob/main/CODE_OF_CONDUCT.md)
required: true
- type: input
attributes:
label: "Module(s)"
description: "List one or several modules this feature is related to"
validations:
required: true
- type: textarea
attributes:
label: "Description"
description: "A concise and justified description of a feature"
validations:
required: true

View File

@ -0,0 +1,2 @@
- [ ] I have read [CONTRIBUTING.md](https://github.com/echasnovski/mini.nvim/blob/main/CONTRIBUTING.md)
- [ ] I have read [CODE_OF_CONDUCT.md](https://github.com/echasnovski/mini.nvim/blob/main/CODE_OF_CONDUCT.md)

View File

@ -0,0 +1,60 @@
name: Linting and style checking
on: [push, pull_request]
jobs:
stylua:
name: Formatting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: JohnnyMorganz/stylua-action@1.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
version: v0.14.0
# CLI arguments
args: --color always --check .
gendoc:
name: Document generation
runs-on: ubuntu-latest
steps:
- name: Install Neovim
uses: rhysd/action-setup-vim@v1
id: neovim
with:
neovim: true
- uses: actions/checkout@v2
- name: Generate documentation
run: make --silent documentation
- name: Check for changes
run: if [[ -n $(git status -s) ]]; then exit 1; fi
case-sensitivity:
name: File case sensitivity
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Check Case Sensitivity
uses: credfeto/action-case-checker@v1.2.1
checkout:
# Test possibility of checking out and setting up 'mini.nvim' on non-Linux
# This can guard from possible problems:
# - Long file names (particularly from reference screenshots).
name: Test checkout
strategy:
matrix:
os: [windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: Setup neovim
uses: rhysd/action-setup-vim@v1
with:
# Uses latest stable by default
neovim: true
- name: Test setup
run: make basic_setup

View File

@ -0,0 +1,28 @@
name: Run tests
on: [push, pull_request]
jobs:
build:
name: Run tests
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
matrix:
neovim_version: ['v0.5.1', 'v0.6.1', 'v0.7.0', 'nightly']
steps:
- uses: actions/checkout@v2
- run: date +%F > todays-date
- name: Restore cache for today's nightly.
uses: actions/cache@v2
with:
path: _neovim
key: ${{ runner.os }}-x64-${{ hashFiles('todays-date') }}
- name: Setup neovim
uses: rhysd/action-setup-vim@v1
with:
neovim: true
version: ${{ matrix.neovim_version }}
- name: Run tests
run: make test

View File

@ -0,0 +1,2 @@
doc/tags
deps

View File

@ -0,0 +1,13 @@
repos:
- repo: local
hooks:
- id: stylua
name: StyLua
language: system
entry: stylua
types: [lua]
- id: gendocs
name: Gendocs
language: system
entry: make --silent documentation
types: []

View File

@ -0,0 +1,7 @@
column_width = 120
line_endings = "Unix"
indent_type = "Spaces"
indent_width = 2
quote_style = "AutoPreferSingle"
no_call_parentheses = false
collapse_simple_statement = "Always"

View File

@ -0,0 +1 @@
deps

View File

@ -0,0 +1,200 @@
# Version 0.5.0.9000
## mini.align
Introduction of new module.
## mini.base16
- FEATURE: Add support for many plugin integrations.
- FEATURE: Implement `MiniBase16.config.plugins` for configuring plugin integrations.
- BREAKING: Change some 'mini.nvim' highlights:
- `MiniCompletionActiveParameter` now highlights with background instead of underline.
- `MiniJump2dSpot` now explicitly defined to use plugin's palette.
- `MiniStarterItemPrefix` and `MiniStarterQuery` are now bold for better visibility.
- BREAKING: Update highlight for changed git diff to be more visible and to comply more with general guidelines.
## mini.jump
- BREAKING: Allow cursor to be positioned past the end of previous/current line (#113).
## mini.starter
- Item evaluation is now prepended with query reset, as it is rarely needed any more (#105).
## mini.surround
- BREAKING FEATURE: update 'mini.surround' to share as much with 'mini.ai' as possible. This provides more integrated experience while enabling more useful features. Details:
- Custom surrounding specification for input action has changed. Instead of `{ find = <string>, extract = <string> }` it is now `{ <function or composed pattern> }`. Previous format will work until the next release. See more in help file.
- Algorithm for finding surrounding is now more powerful. It allows searching for more complex surroundings (via composed patterns or array of region pairs) and respects `v:count`.
- Multiline input and output surroundings are now supported.
- Opening brackets (`(`, `[`, `{`, `<`) now include whitespace in surrounding: input surrounding selects all inner edge whitespace, output surrounding is padded with single space.
- Surrounding identifier `i` ("interactive") is soft deprecated in favor of `?` ("user prompt").
- New surrounding aliases:
- `b` for "brackets". Input - any of balanced `()`, `[]` `{}`. Output - `()`.
- `q` for "quotes". Input - any of `"`, `'`, `` ` ``. Output - `""`.
- Three new search methods `'prev'`, `'next'`, and `'nearest'` for finding non-covering previous and next surrounding.
- BREAKING FEATURE: Implement "last"/"next" extended mappings which force `'prev'` or `'next'` search method. Controlled with `config.mappings.suffix_last` and `config.mappings.suffix_next`respectively. This also means that custom surroundings with identifier equal to "last"/"next" mappings suffixes (defaults to 'l' and 'n') will work only with long enough delay after typing action mapping.
- FEATURE: Implement `MiniSurround.gen_spec` with generators of common surrounding specifications (like `MiniSurround.gen_spec.input.treesitter` for tree-sitter based input surrounding).
# Version 0.5.0
- Update all tests to use new 'mini.test' module.
- FEATURE: Implement buffer local configuration. This is done with `vim.b.mini*_config` buffer variables.
- Add new `minicyan` color scheme.
## mini.ai
Introduction of new module.
## mini.comment
- FEATURE: Now hooks can be used to terminate further actions by returning `false` (#108).
## mini.indentscope
- BREAKING: Soft deprecate `vim.b.miniindentscope_options` in favor of using `options` field of `miniindentscope_config`.
## mini.sessions
- FEATURE: Hooks are now called with active session data as argument.
## mini.starter
- FEATURE: Now it is possible to open multiple Starter buffers at the same time (#82). This comes with several changes which won't affect most users:
- BREAKING: `MiniStarter.content` is deprecated. Use `MiniStarter.get_content()`.
- All functions dealing with Starter buffer now have `buf_id` as argument (no breaking behavior).
## mini.statusline
- FEATURE: Implement `config.use_icons` which controls whether to use icons by default.
## mini.test
Introduction of new module.
## mini.trailspace
- FEATURE: Implement `MiniTrailspace.trim_last_lines()`.
# Version 0.4.0
- Update all modules to supply mapping description for Neovim>=0.7.
- Add new module 'mini.jump2d'.
- Cover all modules with extensive tests.
## mini.comment
- FEATURE: Implement `config.hooks` with `pre` and `post` hooks (executed before and after successful commenting). Fixes #50, #59.
## mini.completion
- Implement support for `additionalTextEdits` (issue #61).
## mini.jump
- FEATURE: Implement idle timeout to stop jumping automatically (@annenpolka, #56).
- FEATURE: Implement `MiniJump.state`: table with useful model-related information.
- BREAKING: Soft deprecate `config.highlight_delay` in favor of `config.delay.highlight`.
- Update process of querying target symbol: show help message after delay, allow `<C-c>` to stop selecting target.
## mini.jump2d
Introduction of new module.
## mini.pairs
- Create mappings for `<BS>` and `<CR>` in certain mode only after some pair is registered in that mode.
## mini.sessions
- FEATURE: Implement `MiniSessions.select()` to select session interactively and perform action on it.
- FEATURE: Implement `config.hooks` to execute hook functions before and after successful action.
- BREAKING: All feedback about incorrect behavior is now an error instead of message notifications.
## mini.starter
- Allow `config.header` and `config.footer` be any value, which will be converted to string via `tostring()`.
- Update query logic to not allow queries which result into no items.
- Add `<C-n>` and `<C-p>` to default mappings.
## mini.statusline
- BREAKING: change default icon for `MiniStatusline.section_diagnostics()` from ﯭ to  due to former having issues in some terminal emulators.
## mini.surround
- FEATURE: Implement `config.search_method`.
- FEATURE: Implement custom surroundings via `config.custom_surroundings`.
- FEATURE: Implement `MiniSurround.user_input()`.
- BREAKING: Deprecate `config.funname_pattern` option in favor of manually modifying `f` surrounding.
- BREAKING: Always move cursor to the right of left surrounding in `add()`, `delete()`, and `replace()` (instead of moving only if it was on the same line as left surrounding).
- Update process of getting user input: allow `<C-c>` to cancel and make empty string a valid input.
## mini.tabline
- FEATURE: Implement `config.tabpage_section`.
- BREAKING: Show listed buffers also in case of multiple tabpages (instead of using builtin behavior).
- Show quickfix/loclist buffers with special `*quickfix*` label.
# Version 0.3.0
- Update all modules to have annotations formatted for 'mini.doc'.
## mini.cursorword
- Current word under cursor now can be highlighted differently.
## mini.doc
Introduction of new module.
## mini.indentscope
Introduction of new module.
## mini.starter
- Implement `MiniStarter.set_query()` and make `<Esc>` mapping for resetting query.
# Version 0.2.0
## mini.base16
- Use new `Diagnostic*` highlight groups in Neovim 0.6.0.
## mini.comment
- Respect tab indentation (#20).
## mini.jump
Introduction of new module.
## mini.pairs
- Implement pair registration with custom mapping functions. More detailed:
- Implement `MiniPairs.map()`, `MiniPairs.map_buf()`, `MiniPairs.unmap()`, `MiniPairs.unmap_buf()` to (un)make mappings for pairs which automatically register them for `<BS>` and `<CR>`. Note, that this has a minor break of previous behavior: now `MiniPairs.bs()` and `MiniPairs.cr()` don't have any input argument. But default behavior didn't change.
- Allow setting global pair mappings inside `config` of `MiniPairs.setup()`.
## mini.sessions
Introduction of new module.
## mini.starter
Introduction of new module.
## mini.statusline
- Implement new section `MiniStatusline.section_searchcount()`.
- Update `section_diagnostics` to use `vim.diagnostic` in Neovim 0.6.0.
# Version 0.1.0
- Initial stable version.

View File

@ -0,0 +1,132 @@
# Contributor Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
`evgeni <dot> chasnovski |at| gmail >dot< com` .
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

View File

@ -0,0 +1,135 @@
# Contributing
Thank you for your willingness to contribute to 'mini.nvim'. It means a lot!
You can make contributions in the following ways:
- **Mention it** somehow to help reach broader audience. This helps a lot.
- **Create a GitHub issue**. It can be one of two types:
- **Bug report**. Describe your actions in a reproducible way along with their effect and what you expected should happen. Before making one, please make your best efforts to make sure that it is not an intended behavior (not described in documentation as such).
- **Feature request**. A concise and justified description of what one or several modules should be able to do. Before making one, please make your best efforts to make sure that it is not a feature that won't get implemented (these should be described in documentation; for example: block comments in 'mini.comment').
- **Create a pull request (PR)**. It can be one of two types:
- **Code related**. For example, fix a bug or implement a feature. **Before even starting one, please make sure that it is aligned with project vision and goals**. The best way to do it is to receive a positive feedback from maintainer on your initiative in one of the GitHub issues (existing one or created by you otherwise). Please, make sure to regenerate latest help file and that all tests are passed (see later sections).
- **Add plugin integration to 'mini.base16' color scheme**. See [](#implementation-notes) for checklist.
- **Documentation related**. For example, fix typo/wording in 'README.md', code comments or annotations (which are used to generate Neovim documentation; see later section). Feel free to make these without creating a GitHub issue.
- **Add explicit support to colorschemes**. Any 'mini.nvim' module supports any colorscheme right out of the box. This is done by making most highlight groups be linked to a semantically similar builtin highlight group. Other groups are hard-coded based on personal preference. However, these choices might be out of tune with a particular colorscheme. Updating as many colorschemes as possible to have explicit 'mini.nvim' support is highly appreciated. For your convenience, there is a list of all highlight groups in later section of this file.
- **Participate in [discussions](https://github.com/echasnovski/mini.nvim/discussions)**.
All well-intentioned, polite, and respectful contributions are always welcome! Thanks for reading this!
## Generating help file
If your contribution updates annotations used to generate help file, please regenerate it. You can make this with one of the following (assuming current directory being project root):
- From command line execute `make documentation`.
- Inside Neovim instance run `:luafile scripts/minidoc.lua`.
## Running tests
If your contribution updates code and you use Linux (not Windows or MacOS), please make sure that it doesn't break existing tests. If it adds new functionality or fixes a recognized bug, add new test case(s). There are two ways of running tests:
- From command line execute `make test` to run all tests or `FILE=<name of file> make test_file` to run tests only from file `<name of file>`.
- Inside Neovim instance execute `:lua require('mini.test').setup(); MiniTest.run()` to run all tests or `:lua require('mini.test').setup(); MiniTest.run_file()` to run tests only from current buffer.
This plugin uses 'mini.test' to manage its tests. For more hands-on introduction, see [TESTING.md](TESTING.md).
If you have Windows or MacOS and want to contribute code related change, make you best effort to not break existing behavior. It will later be tested automatically after making Pull Request. The reason for this distinction is that tests are not well designed to be run on those operating systems.
## Formatting
This project uses [StyLua](https://github.com/JohnnyMorganz/StyLua) version 0.14.0 for formatting Lua code. Before making changes to code, please:
- [Install StyLua](https://github.com/JohnnyMorganz/StyLua#installation). NOTE: use `v0.14.0`.
- Format with it. Currently there are two ways to do this:
- Manually run `stylua .` from the root directory of this project.
- [Install pre-commit](https://pre-commit.com/#install) and enable it with `pre-commit install` (from the root directory). This will auto-format relevant code before making commits.
## Implementation notes
- Use module's `H.get_config()` helper to get its `config`. This way allows using buffer local configuration.
- Checklist for adding new config setting:
- Add code which uses new setting.
- Update module's `H.setup_config()` with type check of new setting.
- Add default value to `Mini*.config` definition.
- Regenerate help file.
- Update module's README in 'readmes' directory.
- Update 'CHANGELOG.md'. In module's section of current version add line starting with `- FEATURE: Implement ...`.
- Checklist for adding new plugin integration:
- Update file 'lua/mini/base16.lua' in a way similar to other already added plugins:
- Add definitions for highlight groups.
- Add plugin entry in a list of supported plugins in help annotations.
- Regenerate documentation (see [](#generating-help-file)).
- Checklist for adding new module:
- Add Lua source code in 'lua' directory.
- Add tests in 'tests' directory. Use 'tests/dir-xxx' name for module-specific non-test helpers.
- Update 'lua/init.lua' to mention new module: both in initial table of contents and list of modules.
- Update 'scripts/minidoc.lua' to generate separate help file.
- Generate help files.
- Add README to 'readmes' directory.
- Update main README to mention new module: both in table of contents and subsection.
- Update 'CHANGELOG.md' to mention introduction of new module.
## List of highlight groups
Here is a list of all highlight groups defined inside 'mini.nvim' modules. See documentation in 'doc' directory to find out what they are used for.
- 'mini.completion':
- `MiniCompletionActiveParameter`
- 'mini.cursorword':
- `MiniCursorword`
- `MiniCursorwordCurrent`
- 'mini.indentscope':
- `MiniIndentscopeSymbol`
- `MiniIndentscopePrefix`
- 'mini.jump':
- `MiniJump`
- 'mini.jump2d':
- `MiniJump2dSpot`
- 'mini.starter':
- `MiniStarterCurrent`
- `MiniStarterFooter`
- `MiniStarterHeader`
- `MiniStarterInactive`
- `MiniStarterItem`
- `MiniStarterItemBullet`
- `MiniStarterItemPrefix`
- `MiniStarterSection`
- `MiniStarterQuery`
- 'mini.statusline':
- `MiniStatuslineDevinfo`
- `MiniStatuslineFileinfo`
- `MiniStatuslineFilename`
- `MiniStatuslineInactive`
- `MiniStatuslineModeCommand`
- `MiniStatuslineModeInsert`
- `MiniStatuslineModeNormal`
- `MiniStatuslineModeOther`
- `MiniStatuslineModeReplace`
- `MiniStatuslineModeVisual`
- 'mini.surround':
- `MiniSurround`
- 'mini.tabline':
- `MiniTablineCurrent`
- `MiniTablineFill`
- `MiniTablineHidden`
- `MiniTablineModifiedCurrent`
- `MiniTablineModifiedHidden`
- `MiniTablineModifiedVisible`
- `MiniTablineTabpagesection`
- `MiniTablineVisible`
- 'mini.test':
- `MiniTestEmphasis`
- `MiniTestFail`
- `MiniTestPass`
- 'mini.trailspace':
- `MiniTrailspace`

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Evgeni Chasnovski
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,22 @@
GROUP_DEPTH ?= 1
NVIM_EXEC ?= nvim
all: test documentation
test:
$(NVIM_EXEC) --version | head -n 1 && echo ''
$(NVIM_EXEC) --headless --noplugin -u ./scripts/minimal_init.lua \
-c "lua require('mini.test').setup()" \
-c "lua MiniTest.run({ execute = { reporter = MiniTest.gen_reporter.stdout({ group_depth = $(GROUP_DEPTH) }) } })"
test_file:
$(NVIM_EXEC) --version | head -n 1 && echo ''
$(NVIM_EXEC) --headless --noplugin -u ./scripts/minimal_init.lua \
-c "lua require('mini.test').setup()" \
-c "lua MiniTest.run_file('$(FILE)', { execute = { reporter = MiniTest.gen_reporter.stdout({ group_depth = $(GROUP_DEPTH) }) } })"
documentation:
$(NVIM_EXEC) --headless --noplugin -u ./scripts/minimal_init.lua -c "lua require('mini.doc').generate()" -c "qa!"
basic_setup:
$(NVIM_EXEC) --headless --noplugin -u ./scripts/basic-setup_init.lua

View File

@ -0,0 +1,324 @@
<img src="logo.png" width="800em"/> <br>
<!-- badges: start -->
[![GitHub license](https://badgen.net/github/license/echasnovski/mini.nvim)](https://github.com/echasnovski/mini.nvim/blob/main/LICENSE)
[![GitHub tag](https://badgen.net/github/tag/echasnovski/mini.nvim)](https://github.com/echasnovski/mini.nvim/tags/)
[![Current version](https://badgen.net/badge/Current%20version/development/cyan)](https://github.com/echasnovski/mini.nvim/blob/main/CHANGELOG.md)
<!-- badges: end -->
Library of 20+ independent Lua modules improving overall [Neovim](https://github.com/neovim/neovim) (version 0.5 and higher) experience with minimal effort. They all share same configuration approaches and general design principles.
Think about this project as "Swiss Army knife" among Neovim plugins: it has many different independent tools (modules) suitable for most common tasks. Each module can be used separately without any startup and usage overhead.
If you want to help this project grow but don't know where to start, check out [contributing guides](CONTRIBUTING.md) or leave a Github star.
## Table of contents
- [Installation](#installation)
- [Modules](#modules)
- [General principles](#general-principles)
- [Plugin colorschemes](#plugin-colorschemes)
- [Planned modules](#planned-modules)
## Installation
There are two branches to install from:
- `main` (default, **recommended**) will have latest development version of plugin. All changes since last stable release should be perceived as being in beta testing phase (meaning they already passed alpha-testing and are moderately settled).
- `stable` will be updated only upon releases with code tested during public beta-testing phase in `main` branch.
Here are code snippets for some common installation methods:
- Using [wbthomason/packer.nvim](https://github.com/wbthomason/packer.nvim):
| Branch | Code snippet |
|--------|------------------------------------------------------|
| Main | `use 'echasnovski/mini.nvim'` |
| Stable | `use { 'echasnovski/mini.nvim', branch = 'stable' }` |
- Using [junegunn/vim-plug](https://github.com/junegunn/vim-plug):
| Branch | Code snippet |
|--------|--------------------------------------------------------|
| Main | `Plug 'echasnovski/mini.nvim'` |
| Stable | `Plug 'echasnovski/mini.nvim', { 'branch': 'stable' }` |
**Important**: don't forget to call module's `setup()` (if required) to enable its functionality.
**Note**: if you are on Windows, there might be problems with too long file paths (like `error: unable to create file <some file name>: Filename too long`). Try doing one of the following:
- Enable corresponding git global config value: `git config --system core.longpaths true`. Then try to reinstall.
- Install plugin in other place with shorter path.
## Modules
| Module | Description | Overview | Details |
|------------------|---------------------------------------------|---------------------------------------|---------------------------------------|
| mini.ai | Extend and create `a`/`i` textobjects | [README](readmes/mini-ai.md) | [Help file](doc/mini-ai.txt) |
| mini.align | Align text interactively | [README](readmes/mini-align.md) | [Help file](doc/mini-align.txt) |
| mini.base16 | Base16 colorscheme creation | [README](readmes/mini-base16.md) | [Help file](doc/mini-base16.txt) |
| mini.bufremove | Remove buffers | [README](readmes/mini-bufremove.md) | [Help file](doc/mini-bufremove.txt) |
| mini.comment | Comment | [README](readmes/mini-comment.md) | [Help file](doc/mini-comment.txt) |
| mini.completion | Completion and signature help | [README](readmes/mini-completion.md) | [Help file](doc/mini-completion.txt) |
| mini.cursorword | Autohighlight word under cursor | [README](readmes/mini-cursorword.md) | [Help file](doc/mini-cursorword.txt) |
| mini.doc | Generate Neovim help files | [README](readmes/mini-doc.md) | [Help file](doc/mini-doc.txt) |
| mini.fuzzy | Fuzzy matching | [README](readmes/mini-fuzzy.md) | [Help file](doc/mini-fuzzy.txt) |
| mini.indentscope | Visualize and operate on indent scope | [README](readmes/mini-indentscope.md) | [Help file](doc/mini-indentscope.txt) |
| mini.jump | Jump forward/backward to a single character | [README](readmes/mini-jump.md) | [Help file](doc/mini-jump.txt) |
| mini.jump2d | Jump within visible lines | [README](readmes/mini-jump2d.md) | [Help file](doc/mini-jump2d.txt) |
| mini.misc | Miscellaneous functions | [README](readmes/mini-misc.md) | [Help file](doc/mini-misc.txt) |
| mini.pairs | Autopairs | [README](readmes/mini-pairs.md) | [Help file](doc/mini-pairs.txt) |
| mini.sessions | Session management | [README](readmes/mini-sessions.md) | [Help file](doc/mini-sessions.txt) |
| mini.starter | Start screen | [README](readmes/mini-starter.md) | [Help file](doc/mini-starter.txt) |
| mini.statusline | Statusline | [README](readmes/mini-statusline.md) | [Help file](doc/mini-statusline.txt) |
| mini.surround | Surround actions | [README](readmes/mini-surround.md) | [Help file](doc/mini-surround.txt) |
| mini.tabline | Tabline | [README](readmes/mini-tabline.md) | [Help file](doc/mini-tabline.txt) |
| mini.test | Test Neovim plugins | [README](readmes/mini-test.md) | [Help file](doc/mini-test.txt) |
| mini.trailspace | Trailspace (highlight and remove) | [README](readmes/mini-trailspace.md) | [Help file](doc/mini-trailspace.txt) |
<a name='mini.ai'></a>
### mini.ai
Extend and create `a`/`i` textobjects (like in `di(` or `va"`).
- It enhances some builtin textobjects (like `a(`, `a)`, `a'`, and more), creates new ones (like `a*`, `a<Space>`, `af`, `a?`, and more), and allows user to create their own (like based on treesitter, and more).
- Supports dot-repeat, `v:count`, different search methods, consecutive application, and customization via Lua patterns or functions.
- Has builtins for brackets, quotes, function call, argument, tag, user prompt, and any punctuation/digit/whitespace character.
For video demo and quick overview see its [README](readmes/mini-ai.md). For more details see its [help file](doc/mini-ai.txt).
---
<a name='mini.align'></a>
### mini.align
Align text interactively (with or without instant preview).
For video demo and quick overview see its [README](readmes/mini-align.md). For more details see its [help file](doc/mini-align.txt).
---
<a name='mini.base16'></a>
### mini.base16
Fast implementation of [chriskempson/base16](https://github.com/chriskempson/base16) theme for manually supplied palette.
- Supports 30+ plugin integrations.
- Has unique palette generator which needs only background and foreground colors.
- Comes with several hand-picked color schemes.
For video demo and quick overview see its [README](readmes/mini-base16.md). For more details see its [help file](doc/mini-base16.txt).
---
<a name='mini.bufremove'></a>
### mini.bufremove
Buffer removing (unshow, delete, wipeout), which saves window layout.
For video demo and quick overview see its [README](readmes/mini-bufremove.md). For more details see its [help file](doc/mini-bufremove.txt).
---
<a name='mini.comment'></a>
### mini.comment
Fast and familiar per-line commenting.
For video demo and quick overview see its [README](readmes/mini-comment.md). For more details see its [help file](doc/mini-comment.txt).
---
<a name='mini.completion'></a>
### mini.completion
Autocompletion and signature help plugin.
- Async (with customizable 'debounce' delay) 'two-stage chain completion': first builtin LSP, then configurable fallback.
- Has functionality for completion item info and function signature (both in floating window appearing after customizable delay).
For video demo and quick overview see its [README](readmes/mini-completion.md). For more details see its [help file](doc/mini-completion.txt).
---
<a name='mini.cursorword'></a>
### mini.cursorword
Automatic highlighting of word under cursor (displayed after customizable delay).
For video demo and quick overview see its [README](readmes/mini-cursorword.md). For more details see its [help file](doc/mini-cursorword.txt).
---
<a name='mini.doc'></a>
### mini.doc
Generation of help files from EmmyLua-like annotations. Allows flexible customization of output via hook functions. Used for documenting this plugin.
For video demo and quick overview see its [README](readmes/mini-doc.md). For more details see its [help file](doc/mini-doc.txt).
---
<a name='mini.fuzzy'></a>
### mini.fuzzy
Minimal and fast fuzzy matching.
For video demo and quick overview see its [README](readmes/mini-fuzzy.md). For more details see its [help file](doc/mini-fuzzy.txt).
---
<a name='mini.indentscope'></a>
### mini.indentscope
Visualize and operate on indent scope. Supports customization of debounce delay, animation style, and different granularity of options for scope computing algorithm.
- Customizable debounce delay, animation style, and scope computation options.
- Implements scope-related motions and textobjects.
For video demo and quick overview see its [README](readmes/mini-indentscope.md). For more details see its [help file](doc/mini-indentscope.txt).
---
<a name='mini.jump'></a>
### mini.jump
Smarter forward/backward jumping to a single character.
For video demo and quick overview see its [README](readmes/mini-jump.md). For more details see its [help file](doc/mini-jump.txt).
---
<a name='mini.jump2d'></a>
### mini.jump2d
Jump within visible lines via iterative label filtering.
For video demo and quick overview see its [README](readmes/mini-jump2d.md). For more details see its [help file](doc/mini-jump2d.txt).
---
<a name='mini.misc'></a>
### mini.misc
Miscellaneous useful functions.
For video demo and quick overview see its [README](readmes/mini-misc.md). For more details see its [help file](doc/mini-misc.txt).
---
<a name='mini.pairs'></a>
### mini.pairs
Minimal and fast autopairs.
For video demo and quick overview see its [README](readmes/mini-pairs.md). For more details see its [help file](doc/mini-pairs.txt).
---
<a name='mini.sessions'></a>
### mini.sessions
Session management (read, write, delete).
For video demo and quick overview see its [README](readmes/mini-sessions.md). For more details see its [help file](doc/mini-sessions.txt).
---
<a name='mini.starter'></a>
### mini.starter
Fast and flexible start screen
For video demo and quick overview see its [README](readmes/mini-starter.md). For more details see its [help file](doc/mini-starter.txt).
---
<a name='mini.statusline'></a>
### mini.statusline
Minimal and fast statusline module with opinionated default look.
For video demo and quick overview see its [README](readmes/mini-statusline.md). For more details see its [help file](doc/mini-statusline.txt).
---
<a name='mini.surround'></a>
### mini.surround
Fast and feature-rich surround plugin
- Add, delete, replace, find, highlight surrounding (like pair of parenthesis, quotes, etc.).
- Supports dot-repeat, `v:count`, different search methods, "last"/"next" extended mappings, customization via Lua patterns or functions, and more.
- Has builtins for brackets, function call, tag, user prompt, and any alphanumeric/punctuation/whitespace character.
- Has maintained configuration of setup similar to 'tpope/vim-surround'.
For video demo and quick overview see its [README](readmes/mini-surround.md). For more details see its [help file](doc/mini-surround.txt).
---
<a name='mini.tabline'></a>
### mini.tabline
Minimal and fast tabline showing listed buffers
For video demo and quick overview see its [README](readmes/mini-tabline.md). For more details see its [help file](doc/mini-tabline.txt).
---
<a name='mini.test'></a>
### mini.test
Write and use extensive Neovim plugin tests
- Supports hierarchical tests, hooks, parametrization, filtering (like from current file or cursor position), screen tests, "busted-style" emulation, customizable reporters, and more.
- Designed to be used with provided wrapper for managing child Neovim processes.
For video demo and quick overview see its [README](readmes/mini-test.md). For more details see its [help file](doc/mini-test.txt).
---
<a name='mini.trailspace'></a>
### mini.trailspace
Work with trailing whitespace
For video demo and quick overview see its [README](readmes/mini-trailspace.md). For more details see its [help file](doc/mini-trailspace.txt).
---
## General principles
- **Design**. Each module is designed to solve a particular problem targeting balance between feature-richness (handling as many edge-cases as possible) and simplicity of implementation/support. Granted, not all of them ended up with the same balance, but it is the goal nevertheless.
- **Independence**. Modules are independent of each other and can be run without external dependencies. Although some of them may need dependencies for full experience.
- **Structure**. Each module is a submodule for a placeholder "mini" module. So, for example, "surround" module should be referred to as "mini.surround". As later will be explained, this plugin can also be referred to as "MiniSurround".
- **Setup**:
- Each module (if needed) should be setup separately with `require(<name of module>).setup({})` (possibly replace {} with your config table or omit to use defaults). You can supply only values which differ from defaults, which will be used for the rest ones.
- Call to module's `setup()` always creates a global Lua object with coherent camel-case name: `require('mini.surround').setup()` creates `_G.MiniSurround`. This allows for a simpler usage of plugin functionality: instead of `require('mini.surround')` use `MiniSurround` (or manually `:lua MiniSurround.*` in command line); available from `v:lua` like `v:lua.MiniSurround`. Considering this, "module" and "Lua object" names can be used interchangeably: 'mini.surround' and 'MiniSurround' will mean the same thing.
- Each supplied `config` table is stored in `config` field of global object. Like `MiniSurround.config`.
- Values of `config`, which affect runtime activity, can be changed on the fly to have effect. For example, `MiniSurround.config.n_lines` can be changed during runtime; but changing `MiniSurround.config.mappings` won't have any effect (as mappings are created once during `setup()`).
- **Buffer local configuration**. Each module can be additionally configured to use certain runtime config settings locally to buffer. See `mini.nvim-buffer-local-config` section in help file for more information.
- **Disabling**. Each module's core functionality can be disabled globally or locally to buffer by creating appropriate global or buffer-scoped variables equal to `v:true`. See `mini.nvim-disabling-recipes` section in help file for common recipes.
- **Highlight groups**. Appearance of module's output is controlled by certain highlight group (see `:h highlight-groups`). To customize them, use `highlight` command. **Note**: currently not many Neovim themes support this plugin's highlight groups; fixing this situation is highly appreciated. To see a more calibrated look, use MiniBase16 or plugin's colorscheme `minischeme`.
- **Stability**. Each module upon release is considered to be relatively stable: both in terms of setup and functionality. Any non-bugfix backward-incompatible change will be released gradually as much as possible.
## Plugin colorschemes
This plugin comes with several color schemes (all of them are made with 'mini.base16' and have both dark and light variants):
- `minischeme` - blue and yellow main colors with high contrast and saturation palette. All examples use this colorscheme.
- `minicyan` - cyan and grey main colors with moderate contrast and saturation palette.
Activate them as regular `colorscheme` (for example, `:colorscheme minicyan`). You can see how they look in [demo of 'mini.base16'](readmes/mini-base16.md#demo).
## Planned modules
This is the list of modules I currently intend to implement eventually (as my free time and dedication will allow), in alphabetical order:
- 'mini.align' - align text with respect to some separators. Something like [junegunn/vim-easy-align](https://github.com/junegunn/vim-easy-align).
- 'mini.basics' - configurable collection of options and mappings sets intended mostly for quick "up and running" Neovim config. Something like a combination of [tpope/vim-sensible](https://github.com/tpope/vim-sensible) and [tpope/vim-unimpaired](https://github.com/tpope/vim-unimpaired).
- 'mini.clue' - "show as you type" floating window with customizable information. Something like [folke/which-key.nvim](https://github.com/folke/which-key.nvim) and [anuvyklack/hydra.nvim](https://github.com/anuvyklack/hydra.nvim)
- 'mini.filetree' - file tree viewer. Simplified version of [kyazdani42/nvim-tree](https://github.com/kyazdani42/nvim-tree.lua).
- 'mini.root' - automatically change current working directory. Something like [airblade/vim-rooter](https://github.com/airblade/vim-rooter).
- 'mini.snippets' - work with snippets. Something like [L3MON4D3/LuaSnip](https://github.com/L3MON4D3/LuaSnip) but only with more straightforward functionality.
- 'mini.swap' - exchange two regions of text. Something like [tommcdo/vim-exchange](https://github.com/tommcdo/vim-exchange).
- 'mini.terminals' - coherently manage terminal windows and send text from buffers to terminal windows. Something like [kassio/neoterm](https://github.com/kassio/neoterm).

View File

@ -0,0 +1,964 @@
# How to test with 'mini.test'
Writing tests for Neovim Lua plugin is hard. Writing good tests for Neovim Lua plugin is even harder. The 'mini.test' module is designed to make it reasonably easier while still allowing lots of flexibility. It deliberately favors a more verbose and program-like style of writing tests, opposite to "human readable, DSL like" approach of [nvim-lua/plenary.nvim](https://github.com/nvim-lua/plenary.nvim) ("busted-style testing" from [Olivine-Labs/busted](https://github.com/Olivine-Labs/busted)). Although the latter is also possible.
This file is intended as a hands-on introduction to 'mini.test' with examples. For more details, see 'mini.test' section of [help file](doc/mini.txt) and tests of this plugin's modules.
General approach of writing test files:
- Organize tests in separate Lua files.
- Each file should be associated with a test set table (output of `MiniTest.new_set()`). Recommended approach is to create it manually in each test file and then return it.
- Each test action should be defined in separate function assign to an entry of test set.
- It is strongly encouraged to use custom Neovim processes to do actual testing inside test action. See [Using child process](#using-child-process).
**NOTES**:
- All commands are assumed to be executed with current working directory being a root of your Neovim plugin project. That is both for shell and Neovim commands.
- All paths are assumed to be relative to current working directory.
## Example plugin
In this file we will be testing 'hello_lines' plugin (once some basic concepts are introduced). It will have functionality to add prefix 'Hello ' to lines. It will have single file 'lua/hello_lines/init.lua' with the following content:
<details><summary>'hello_lines/init.lua'</summary>
```lua
local M = {}
--- Prepend 'Hello ' to every element
---@param lines table Array. Default: { 'world' }.
---@return table Array of strings.
M.compute = function(lines)
lines = lines or { 'world' }
return vim.tbl_map(function(x) return 'Hello ' .. tostring(x) end, lines)
end
local ns_id = vim.api.nvim_create_namespace('hello_lines')
--- Set lines with highlighted 'Hello ' prefix
---@param buf_id number Buffer handle where lines should be set. Default: 0.
---@param lines table Array. Default: { 'world' }.
M.set_lines = function(buf_id, lines)
buf_id = buf_id or 0
lines = lines or { 'world' }
vim.api.nvim_buf_set_lines(buf_id or 0, 0, -1, true, M.compute(lines))
for i = 1, #lines do
vim.highlight.range(buf_id, ns_id, 'Special', { i - 1, 0 }, { i - 1, 5 }, {})
end
end
return M
```
</details>
## Quick demo
Here is a quick demo of how tests with 'mini.test' look like:
<details><summary>'tests/test_hello_lines.lua'</summary>
```lua
-- Define helper aliases
local new_set = MiniTest.new_set
local expect, eq = MiniTest.expect, MiniTest.expect.equality
-- Create (but not start) child Neovim object
local child = MiniTest.new_child_neovim()
-- Define main test set of this file
local T = new_set({
-- Register hooks
hooks = {
-- This will be executed before every (even nested) case
pre_case = function()
-- Restart child process with custom 'init.lua' script
child.restart({ '-u', 'scripts/minimal_init.lua' })
-- Load tested plugin
child.lua([[M = require('hello_lines')]])
end,
-- This will be executed one after all tests from this set are finished
post_once = child.stop,
},
})
-- Test set fields define nested structure
T['compute()'] = new_set()
-- Define test action as callable field of test set.
-- If it produces error - test fails.
T['compute()']['works'] = function()
-- Execute Lua code inside child process, get its result and compare with
-- expected result
eq(child.lua_get([[M.compute({'a', 'b'})]]), { 'Hello a', 'Hello b' })
end
T['compute()']['uses correct defaults'] = function()
eq(child.lua_get([[M.compute()]]), { 'Hello world' })
end
-- Make parametrized tests. This will create three copies of each case
T['set_lines()'] = new_set({ parametrize = { {}, { 0, { 'a' } }, { 0, { 1, 2, 3 } } } })
-- Use arguments from test parametrization
T['set_lines()']['works'] = function(buf_id, lines)
-- Directly modify some options to make better test
child.o.lines, child.o.columns = 10, 20
child.bo.readonly = false
-- Execute Lua code without returning value
child.lua('M.set_lines(...)', { buf_id, lines })
-- Test screen state. On first run it will automatically create reference
-- screenshots with text and look information in predefined location. On
-- later runs it will compare current screenshot with reference. Will throw
-- informative error with helpful information if they don't match exactly.
expect.reference_screenshot(child.get_screenshot())
end
-- Return test set which will be collected and execute inside `MiniTest.run()`
return T
```
</details>
## File organization
It might be a bit overwhelming. It actually is for most of the people. However, it should be done once and then you rarely need to touch it.
Overview of full file structure used in for testing 'hello_lines' plugin:
```
.
├── deps
│   └── mini.nvim # Mandatory
├── lua
│   └── hello_lines
│   └── init.lua # Mandatory
├── Makefile # Recommended
├── scripts
│   ├── minimal_init.lua # Mandatory
│   └── minitest.lua # Recommended
└── tests
└── test_hello_lines.lua # Mandatory
```
To write tests, you'll need these files:
Mandatory:
- **Your Lua plugin in 'lua' directory**. Here we will be testing 'hello_lines' plugin.
- **Test files**. By default they should be Lua files located in 'tests/' directory and named with 'test_' prefix. For example, we will write everything in 'test_hello_lines.lua'. It is usually a good idea to follow this template (will be assumed for the rest of this file):
<details><summary>Template for test files</summary>
```lua
local new_set = MiniTest.new_set
local expect, eq = MiniTest.expect, MiniTest.expect.equality
local T = new_set()
-- Actual tests definitions will go here
return T
```
</details><br>
- **'mini.nvim' dependency**. It is needed to use its 'mini.test' module. Proposed way to store it is in 'deps/mini.nvim' directory. Create it with `git`:
```bash
mkdir -p deps
git clone --depth 1 https://github.com/echasnovski/mini.nvim deps/mini.nvim
```
- **Manual Neovim startup file** (a.k.a 'init.lua') with proposed path 'scripts/minimal_init.lua'. It will be used to ensure that Neovim processes can recognize your tested plugin and 'mini.nvim' dependency. Proposed minimal content:
<details><summary>'scripts/minimal_init.lua'</summary>
```lua
-- Add current directory to 'runtimepath' to be able to use 'lua' files
vim.cmd([[let &rtp.=','.getcwd()]])
-- Set up 'mini.test' only when calling headless Neovim (like with `make test`)
if #vim.api.nvim_list_uis() == 0 then
-- Add 'mini.nvim' to 'runtimepath' to be able to use 'mini.test'
-- Assumed that 'mini.nvim' is stored in 'deps/mini.nvim'
vim.cmd('set rtp+=deps/mini.nvim')
-- Set up 'mini.test'
require('mini.test').setup()
end
```
</details><br>
Recommended:
- **Makefile**. In order to simplify running tests from shell and inside Continuous Integration services (like Github Actions), it is recommended to define Makefile. It will define steps for running tests. Proposed template:
<details><summary>Template for Makefile</summary>
```
# Run all test files
test: deps/mini.nvim
nvim --headless --noplugin -u ./scripts/minimal_init.lua -c "lua MiniTest.run()"
# Run test from file at `$FILE` environment variable
test_file: deps/mini.nvim
nvim --headless --noplugin -u ./scripts/minimal_init.lua -c "lua MiniTest.run_file('$(FILE)')"
# Download 'mini.nvim' to use its 'mini.test' testing module
deps/mini.nvim:
@mkdir -p deps
git clone --depth 1 https://github.com/echasnovski/mini.nvim $@
```
</details><br>
- **'mini.test' script** at 'scripts/minitest.lua'. Use it to customize what is tested (which files, etc.) and how. Usually not needed, but otherwise should have some variant of a call to `MiniTest.run()`.
## Running tests
The 'mini.test' module out of the box supports two major ways of running tests:
- **Interactive**. All test files will be run directly inside current Neovim session. This proved to be very useful for debugging while writing tests. To run tests, simply execute `:lua MiniTest.run()` or `:lua MiniTest.run_file()` (assuming, you already have 'mini.test' set up with `require('mini.test').setup()`). With default configuration this will result into floating window with information about results of test execution. Press `q` to close it. **Note**: Be careful though, as it might affect your current setup. To avoid this, [use child processes](#using-child-process) inside tests.
- **Headless** (from shell). Start headless Neovim process with proper startup file and execute `lua MiniTest.run()`. Assuming full file organization from previous section, this can be achieved with `make test`. This will show information about results of test execution directly in shell.
## Basics
These sections will show some basic capabilities of 'mini.test' and how to use them. In all examples code blocks represent some whole test file (like 'tests/test_basics.lua').
### First test
A test is defined as function assigned to a field of test set. If it throws error, test has failed. Test file should return single test set. Here is an example:
```lua
local T = MiniTest.new_set()
T['works'] = function()
local x = 1 + 1
if x ~= 2 then
error('`x` is not equal to 2')
end
end
return T
```
Writing `if .. error() .. end` is too tiresome. That is why 'mini.test' comes with very minimal but usually quite enough set of *expectations*: `MiniTest.expect`. They display the intended expectation between objects and will throw error with informative message if it doesn't hold. Here is a rewritten previous example:
```lua
local T = MiniTest.new_set()
T['works'] = function()
local x = 1 + 1
MiniTest.expect(x, 2)
end
return T
```
Test sets can be nested. This will be useful in combination with [hooks](#hooks) and [parametrization](#test-parametrization):
```lua
local T = MiniTest.new_set()
T['big scope'] = new_set()
T['big scope']['works'] = function()
local x = 1 + 1
MiniTest.expect.equality(x, 2)
end
T['big scope']['also works'] = function()
local x = 2 + 2
MiniTest.expect.equality(x, 4)
end
T['out of scope'] = function()
local x = 3 + 3
MiniTest.expect.equality(x, 6)
end
return T
```
**NOTE**: 'mini.test' supports emulation of busted-style testing by default. So previous example can be written like this:
```lua
describe('big scope', function()
it('works', function()
local x = 1 + 1
MiniTest.expect.equality(x, 2)
end)
it('also works', function()
local x = 2 + 2
MiniTest.expect.equality(x, 4)
end)
end)
it('out of scope', function()
local x = 3 + 3
MiniTest.expect.equality(x, 6)
end)
-- NOTE: when using this style, no test set should be returned
```
Although this is possible, the rest of this file will use a recommended test set approach.
### Builtin expectations
There are four builtin expectations:
```lua
local T = MiniTest.new_set()
local expect, eq = MiniTest.expect, MiniTest.expect.equality
local x = 1 + 1
-- This is so frequently used that having short alias proved useful
T['expect.equality'] = function()
eq(x, 2)
end
T['expect.no_equality'] = function()
expect.no_equality(x, 1)
end
T['expect.error'] = function()
-- This expectation will pass because function will throw an error
expect.error(function()
if x == 2 then error('Deliberate error') end
end)
end
T['expect.no_error'] = function()
-- This expectation will pass because function will *not* throw an error
expect.no_error(function()
if x ~= 2 then error('This should not be thrown') end
end)
end
return T
```
### Writing custom expectation
Although you can use `if ... error() ... end` approach, there is `MiniTest.new_expectation()` to simplify this process for some repetitive expectation. Here is an example used in this plugin:
```lua
local T = MiniTest.new_set()
local expect_match = MiniTest.new_expectation(
-- Expectation subject
'string matching',
-- Predicate
function(str, pattern) return str:find(pattern) ~= nil end,
-- Fail context
function(str, pattern)
return string.format('Pattern: %s\nObserved string: %s', vim.inspect(pattern), str)
end
)
T['string matching'] = function()
local x = 'abcd'
-- This will pass
expect_match(x, '^a')
-- This will fail
expect_match(x, 'x')
end
return T
```
Executing this content from file 'tests/test_basics.lua' will fail with the following message:
```
FAIL in "tests/test_basics.lua | string matching":
Failed expectation for string matching.
Pattern: "x"
Observed string: abcd
Traceback:
tests/test_basics.lua:20
```
### Hooks
Hooks are functions that will be called without arguments at predefined stages of test execution. They are defined for a test set. There are four types of hooks:
- **pre_once** - executed before first (filtered) node.
- **pre_case** - executed before each case (even nested).
- **post_case** - executed after each case (even nested).
- **post_once** - executed after last (filtered) node.
Example:
```lua
local new_set = MiniTest.new_set
local expect, eq = MiniTest.expect, MiniTest.expect.equality
local T = new_set()
local n = 0
local increase_n = function() n = n + 1 end
T['hooks'] = new_set({
hooks = { pre_once = increase_n, pre_case = increase_n, post_case = increase_n, post_once = increase_n },
})
T['hooks']['work'] = function()
-- `n` will be increased twice: in `pre_once` and `pre_case`
eq(n, 2)
end
T['hooks']['work again'] = function()
-- `n` will be increased twice: in `post_case` from previous case and
-- `pre_case` before this one
eq(n, 4)
end
T['after hooks set'] = function()
-- `n` will be again increased twice: in `post_case` from previous case and
-- `post_once` after last case in T['hooks'] test set
eq(n, 6)
end
return T
```
### Test parametrization
One of the distinctive features of 'mini.test' is ability to leverage test parametrization. As hooks, it is a feature of test set.
Example of simple parametrization:
```lua
local new_set = MiniTest.new_set
local eq = MiniTest.expect.equality
local T = new_set()
-- Each parameter should be an array to allow parametrizing multiple arguments
T['parametrize'] = new_set({ parametrize = { { 1 }, { 2 } } })
-- This will result into two cases. First will fail.
T['parametrize']['works'] = function(x)
eq(x, 2)
end
-- Parametrization can be nested. Cases are "multiplied" with every combination
-- of parameters.
T['parametrize']['nested'] = new_set({ parametrize = { { '1' }, { '2' } } })
-- This will result into four cases. Two of them will fail.
T['parametrize']['nested']['works'] = function(x, y)
eq(tostring(x), y)
end
-- Parametrizing multiple arguments
T['parametrize multiple arguments'] = new_set({ parametrize = { { 1, 1 }, { 2, 2 } } })
-- This will result into two cases. Both will pass.
T['parametrize multiple arguments']['works'] = function(x, y)
eq(x, y)
end
return T
```
### Runtime access to current cases
There is `MiniTest.current` table containing information about "current" test cases. It has `all_cases` and `case` fields with all currently executed tests and *the* current case.
Test case is a single unit of sequential test execution. It contains all information needed to execute test case along with data about its execution. Example:
```lua
local new_set = MiniTest.new_set
local eq = MiniTest.expect.equality
local T = new_set()
T['MiniTest.current.all_cases'] = function()
-- A useful hack: show runtime data with expecting it to be something else
eq(MiniTest.current.all_cases, 0)
end
T['MiniTest.current.case'] = function()
eq(MiniTest.current.case, 0)
end
return T
```
This will result into following lengthy fails:
<details><summary>Fail information</summary>
```
FAIL in "tests/test_basics.lua | MiniTest.current.all_cases":
Failed expectation for equality.
Left: { {
args = {},
data = {},
desc = { "tests/test_basics.lua", "MiniTest.current.all_cases" },
exec = {
fails = {},
notes = {},
state = "Executing test"
},
hooks = {
post = {},
pre = {}
},
test = <function 1>
}, {
args = {},
data = {},
desc = { "tests/test_basics.lua", "MiniTest.current.case" },
hooks = {
post = {},
pre = {}
},
test = <function 2>
} }
Right: 0
Traceback:
tests/test_basics.lua:8
FAIL in "tests/test_basics.lua | MiniTest.current.case":
Failed expectation for equality.
Left: {
args = {},
data = {},
desc = { "tests/test_basics.lua", "MiniTest.current.case" },
exec = {
fails = {},
notes = {},
state = "Executing test"
},
hooks = {
post = {},
pre = {}
},
test = <function 1>
}
Right: 0
Traceback:
tests/test_basics.lua:12
```
</details>
### Case helpers
There are some functions intended to help writing more robust cases: `skip()`, `finally()`, and `add_note()`. The `MiniTest.current` table with all
Example:
```lua
local T = MiniTest.new_set()
-- `MiniTest.skip()` allows skipping rest of test execution while giving an
-- informative note. This test will pass with notes.
T['skip()'] = function()
if 1 + 1 == 2 then
MiniTest.skip('Apparently, 1 + 1 is 2')
end
error('1 + 1 is not 2')
end
-- `MiniTest.add_note()` allows adding notes. Final state will have
-- "with notes" suffix.
T['add_note()'] = function()
MiniTest.add_note('This test is not important.')
error('Custom error.')
end
-- `MiniTest.finally()` allows registering some function to be executed after
-- this case is finished executing (with or without an error).
T['finally()'] = function()
-- Add note only if test fails
MiniTest.finally(function()
if #MiniTest.current.case.exec.fails > 0 then
MiniTest.add_note('This test is flaky.')
end
end)
error('Expected error from time to time')
end
return T
```
This will result into following messages:
```
NOTE in "tests/test_basics.lua | skip()": Apparently, 1 + 1 is 2
FAIL in "tests/test_basics.lua | add_note()": tests/test_basics.lua:16: Custom error.
NOTE in "tests/test_basics.lua | add_note()": This test is not important.
FAIL in "tests/test_basics.lua | finally()": tests/test_basics.lua:28: Expected error from time to time
NOTE in "tests/test_basics.lua | finally()": This test is flaky.
```
## Customizing test run
Test run consists from two stages:
- **Collection**. It will source each appropriate file (customizable), combine all test sets into single test set, convert it from hierarchical to sequential form (array of test cases), and filter cases based on customizable predicate.
- **Execution**. It will safely execute array of test cases (with each pre-hooks, test action, post-hooks) one after another in scheduled asynchronous fashion while collecting information about it went and calling customizable reporter methods.
All configuration goes into `opts` argument of `MiniTest.run()`.
### Collection: custom files and filter
You can customize which files will be sourced and which cases will be later executed. Example:
```lua
local new_set = MiniTest.new_set
T = new_set()
-- Use `data` field to pass custom information for easier test management
T['fast'] = new_set({ data = { type = 'fast' } })
T['fast']['first test'] = function() end
T['fast']['second test'] = function() end
T['slow'] = new_set({ data = { type = 'slow' } })
T['slow']['first test'] = function() vim.loop.sleep(1000) end
T['slow']['second test'] = function() vim.loop.sleep(1000) end
return T
```
You can run only this file ('tests/test_basics.lua') and only "fast" cases with
```lua
MiniTest.run({
collect = {
find_files = function() return { 'tests/test_basics.lua' } end,
filter_cases = function(case) return case.data.type == 'fast' end,
}
})
```
### Execution: custom reporter and stop on first error
You can customize execution of test cases with custom reporter (how test results are displayed in real time) and whether to stop on first error. Execution doesn't result into any output, instead it updates `MiniTest.current.all_cases` in place: each case gets an `exec` field with information about how its execution went.
Example of showing status summary table in the command line after everything is finished:
```lua
local reporter = {
-- Other used methods are `start(cases)` and `update(case_num)`
finish = function()
local summary = {}
for _, c in ipairs(MiniTest.current.all_cases) do
local state = c.exec.state
summary[state] = summary[state] == nil and 1 or (summary[state] + 1)
end
print(vim.inspect(summary, { newline = ' ', indent = '' }))
end,
}
MiniTest.run({ execute = { reporter = reporter } })
```
## Using child process
Main feature of 'mini.test' which differs it from other Lua testing frameworks is its design towards **custom usage of child Neovim process inside tests**. Ultimately, each test should be done with fresh Neovim process initialized with bare minimum setup (like allowing to load your plugin). To make this easier, there is a dedicated function `MiniTest.new_child_neovim()`. It returns an object with many useful helper methods, like for start/stop/restart, redirected execution (write code in current process, it gets executed in child one), emulating typing keys, **testing screen state**, etc.
### Start/stop/restart
You can start/stop/restart child process associated with this child Neovim object. Current (from which testing is initiated) and child Neovim processes can "talk" to each through RPC messages (see `:h RPC`). It means you can programmatically execute code inside child process, get some output, and test if it meets your expectation. Also by default child process is "full" (i.e. not headless) which allows you to test things such as extmarks, floating windows, etc.
Although this approach proved to be useful and efficient, it is not ideal. Here are some limitations:
- Due to current RPC protocol implementation functions and userdata can't be used in both input and output with child process. Indicator of this issue is a `Cannot convert given lua type` error. Usual solution is to move some logic on the side of child process, like create and use global functions (those will be "forgotten" after next restart).
- Sometimes hanging process will occur: it stops executing without any output. Most of the time it is because Neovim process is "blocked", i.e. it waits for user input and won't return from other call. Common causes are active hit-enter-prompt (increase prompt height to a bigger value) or Operator-pending mode (exit it). To mitigate this experience, most helper methods will throw an error if they can deduct that immediate execution will lead to hanging state.
Here is recommended setup for managing child processes. It will make fresh Neovim process before every test case:
```lua
local child = MiniTest.new_child_neovim()
local T = MiniTest.new_set({
hooks = {
pre_case = function()
-- Restart child process with custom 'init.lua' script
child.restart({ '-u', 'scripts/minimal_init.lua' })
-- Load tested plugin
child.lua([[M = require('hello_lines')]])
end,
-- Stop once all test cases are finished
post_once = child.stop,
},
})
-- Define some tests here
return T
```
### Executing Lua code
Previous section already demonstrated that there is a `child.lua()` method. It will execute arbitrary Lua code in the form of a single string. This is basically a wrapper for `vim.api.nvim_exec_lua()`. There is also a convenience wrapper `child.lua_get()` which is essentially a `child.lua('return ' .. s, ...)`. Examples:
```lua
local eq = MiniTest.expect.equality
local child = MiniTest.new_child_neovim()
local T = MiniTest.new_set({
hooks = {
pre_case = function()
child.restart({ '-u', 'scripts/minimal_init.lua' })
child.lua([[M = require('hello_lines')]])
end,
post_once = child.stop,
},
})
T['lua()'] = MiniTest.new_set()
T['lua()']['works'] = function()
child.lua('_G.n = 0; _G.n = _G.n + 1')
eq(child.lua('return _G.n'), 1)
end
T['lua()']['can use tested plugin'] = function()
eq(child.lua([[return M.compute()]]), { 'Hello world' })
eq(child.lua([[return M.compute({'a', 'b'})]]), { 'Hello a', 'Hello b' })
end
T['lua_get()'] = function()
child.lua('_G.n = 0')
eq(child.lua_get('_G.n'), child.lua('return _G.n'))
end
return T
```
### Managing Neovim options and state
Although ability to execute arbitrary Lua code is technically enough to write any tests, it gets cumbersome very quickly due to ability to only take string. That is why there are many convenience helpers with the same idea: write code inside current Neovim process that will be automatically executed same way in child process. Here is the showcase:
```lua
local new_set = MiniTest.new_set
local eq = MiniTest.expect.equality
local child = MiniTest.new_child_neovim()
local T = MiniTest.new_set({
hooks = {
pre_case = function()
child.restart({ '-u', 'scripts/minimal_init.lua' })
child.lua([[M = require('hello_lines')]])
end,
post_once = child.stop,
},
})
-- These methods will "redirect" execution to child through `vim.rpcrequest()`
-- and `vim.rpcnotify()` respectively. Any call `child.api.xxx(...)` returns
-- the output of `vim.api.xxx(...)` executed inside child process.
T['api()/api_notify()'] = function()
-- Set option. For some reason, first buffer is 'readonly' which leads to
-- high delay in test execution
child.api.nvim_buf_set_option(0, 'readonly', false)
-- Set lal lines
child.api.nvim_buf_set_lines(0, 0, -1, true, { 'aaa' })
-- Get all lines and test with expected ones
eq(child.api.nvim_buf_get_lines(0, 0, -1, true), { 'aaa' })
end
-- Execute Vimscript with or without capturing its output
T['cmd()/cmd()'] = function()
child.cmd('hi Comment guifg=#AAAAAA')
eq(child.cmd_capture('hi Comment'), 'Comment xxx ctermfg=14 guifg=#aaaaaa')
end
-- There are redirection tables for most of the main Neovim functionality
T['various redirection tables with methods'] = function()
eq(child.fn.fnamemodify('hello_lines.lua', ':t:r'), 'hello_lines')
eq(child.loop.hrtime() > 0, true)
eq(child.lsp.get_active_clients(), {})
-- And more
end
-- There are redirection tables for scoped (buffer, window, etc.) variables
-- You can use them to both set and get values
T['redirection tables for variables'] = function()
child.b.aaa = true
eq(child.b.aaa, true)
eq(child.b.aaa, child.lua_get('vim.b.aaa'))
end
-- There are redirection tables for scoped (buffer, window, etc.) options
-- You can use them to both set and get values
T['redirection tables for options'] = function()
child.o.lines, child.o.columns = 5, 12
eq(child.o.lines, 5)
eq({ child.o.lines, child.o.columns }, child.lua_get('{ vim.o.lines, vim.o.columns }'))
end
return T
```
### Emulate typing keys
Very important part of testing is emulating user typing keys. There is a special `child.type_keys()` helper method for that. Examples:
```lua
local eq = MiniTest.expect.equality
local child = MiniTest.new_child_neovim()
local T = MiniTest.new_set({
hooks = {
pre_case = function()
child.restart({ '-u', 'scripts/minimal_init.lua' })
child.bo.readonly = false
child.lua([[M = require('hello_lines')]])
end,
post_once = child.stop,
},
})
local get_lines = function() return child.api.nvim_buf_get_lines(0, 0, -1, true) end
T['type_keys()'] = MiniTest.new_set()
T['type_keys()']['works'] = function()
-- It can take one string
child.type_keys('iabcde<Esc>')
eq(get_lines(), { 'abcde' })
eq(child.fn.mode(), 'n')
-- Or several strings which improves readability
child.type_keys('cc', 'fghij', '<Esc>')
eq(get_lines(), { 'fghij' })
-- Or tables of strings (possibly nested)
child.type_keys({ 'cc', { 'j', 'k', 'l', 'm', 'n' } })
eq(get_lines(), { 'jklmn' })
end
T['type_keys()']['allows custom delay'] = function()
-- This adds delay of 500 ms after each supplied string (three times here)
child.type_keys(500, 'i', 'abcde', '<Esc>')
eq(get_lines(), { 'abcde' })
end
return T
```
### Test screen state with screenshots
One of the main difficulties in testing Neovim plugins is verifying that something is actually displayed in the way you intend. Like general highlighting, statusline, tabline, sign column, extmarks, etc. Testing screen state with screenshots makes this a lot easier. There is a `child.get_screenshot()` method which basically calls `screenstring()` (`:h screenstring()`) and `screenattr()` (`:h screenattr()`) for every visible cell (row from 1 to 'lines' option, column from 1 to 'columns' option). It then returns two layers of screenshot:
- <text> - "2d array" (row-column) of single characters displayed at particular cells.
- <attr> - "2d array" (row-column) of symbols representing how text is displayed (basically, "coded" appearance/highlighting). They should be used only in relation to each other: same/different symbols for two cells mean same/different visual appearance. Note: there will be false positives if there are more than 94 different attribute values. To make output more portable and visually useful, outputs of `screenattr()` are coded with single character symbols.
Couple of caveats:
- As is apparent from use of `screenattr()`, these screenshots **can't tell how exactly cell is highlighted**, only **if two cells are highlighted the same**. This is due to the currently lacking functionality in Neovim itself. This might change in the future.
- It works only for Neovim>=0.6 because `screenstring()` was introduced in 0.6.
- Due to implementation details of `screenstring()` and `screenattr()` in Neovim<=0.7, this function won't recognize floating windows displayed on screen. It will throw an error if there is a visible floating window. Use Neovim>=0.8 (current nightly) to properly handle floating windows. Details:
- https://github.com/neovim/neovim/issues/19013
- https://github.com/neovim/neovim/pull/19020
To help manage testing screen state, there is a special `MiniTest.expect.reference_screenshot(screenshot, path, opts)` method. It takes screenshot table along with optional path of where to save this screenshot (if not supplied, inferred from test case description and put in 'tests/screenshots' directory). On first run it will automatically create reference screenshot at `path`. On later runs it will compare current screenshot with reference. Will throw informative error with helpful information if they don't match exactly.
Example:
```lua
local expect = MiniTest.expect
local child = MiniTest.new_child_neovim()
local T = MiniTest.new_set({
hooks = {
pre_case = function()
child.restart({ '-u', 'scripts/minimal_init.lua' })
child.bo.readonly = false
child.lua([[M = require('hello_lines')]])
end,
post_once = child.stop,
},
})
T['set_lines()'] = MiniTest.new_set({ parametrize = { {}, { 0, { 'a' } }, { 0, { 1, 2, 3 } } } })
T['set_lines()']['works'] = function(buf_id, lines)
child.o.lines, child.o.columns = 10, 15
child.lua('M.set_lines(...)', { buf_id, lines })
expect.reference_screenshot(child.get_screenshot())
end
return T
```
This will result into three files in 'tests/screenshots' with names containing test case description along with supplied arguments. Here is example reference screenshot for `{ 0, { 1, 2, 3 } }` arguments (line numbers and ruler for columns is added as file specification to make it easier to find differences between two screenshots):
```
--|---------|-----
01|Hello 1
02|Hello 2
03|Hello 3
04|~
05|~
06|~
07|~
08|~
09|<e] [+] 1,1 All
10|
--|---------|-----
01|000001111111111
02|000001111111111
03|000001111111111
04|222222222222222
05|222222222222222
06|222222222222222
07|222222222222222
08|222222222222222
09|333333333333333
10|444444444444444
```
## General tips
- Create a 'tests/helpers.lua' file with code that can be useful in multiple files. It can have "monkey-patched" versions of 'mini.test' functions. Example:
```lua
local Helpers = {}
Helpers.new_child_neovim = function()
local child = MiniTest.new_child_neovim()
child.setup = function()
child.restart({'-u', 'scripts/minimal_init.lua'})
child.bo.readonly = false
child.lua([[M = require('hello_lines')]])
end
return child
end
return Helpers
```
- Write aliases for commonly used functions at top of the file. It will make your life a little bit easier and usually will lead to more readable tests. Example:
```lua
-- Some code setting up `child`
local set_lines = function(lines) child.api.nvim_buf_set_lines(0, 0, -1, true, lines) end
```

View File

@ -0,0 +1,35 @@
# Benchmarks for 'mini.starter'
This directory contains code and results of benchmarking 'mini.starter' and its alternatives. Target benchmarked value is a total startup time using configuration file (with `-u <init-file>`) corresponding to benchmarked setup. Configuration files are created in three different groups:
- 'init_starter-default.lua' and 'init_empty.lua' represent default 'mini.starter' setup and corresponding 'init.lua' without 'mini.starter'.
- 'init_startify-starter', 'init_startify-original', and 'init_startify-alpha' have comparable output imitating default 'vim-startify' with empty header.
- 'init_dashboard-starter', 'init_dashboard-original', and 'init_dashboard-alpha' have comparable output imitating default 'dashboard-nvim' enabled keybindings.
Summary of startup-times for various 'init' files from 'init-files/' directory can be seen in 'startup-summary.md'. Current benchmark was done with Neovim 0.5.1 on Ubuntu 18.04 (i3-6100). Exact states of plugins used:
- [echasnovski/mini.nvim](https://github.com/echasnovski/mini.nvim/tree/cfa108eeaead1abd8854a1f1cfb02e72482641ce)
- [mhinz/vim-startify](https://github.com/mhinz/vim-startify/tree/81e36c352a8deea54df5ec1e2f4348685569bed2)
- [glepnir/dashboard-nvim](https://github.com/glepnir/dashboard-nvim/tree/ba98ab86487b8eda3b0934b5423759944b5f7ebd)
- [goolord/alpha-nvim](https://github.com/goolord/alpha-nvim/tree/7a49086bf9197f573b396d4ac46262c02dfb9aec)
To rerun locally execute these commands (preferably without anything else running in the background and monitor always on):
```bash
chmod +x install.sh
./install.sh
# This will create file 'startup-times.csv' and update 'startup-summary.md'
# WARNING: this will lead to screen flicker
chmod +x benchmark.sh
./benchmark.sh
```
Structure:
- 'init-files/' - directory with all configuration files being benchmarked. NOTE: all of them contain auto-closing command at the end (`defer_fn(...)`) to most accurately measure startup time. To view its output, remove this command.
- 'benchmark.sh' - script for performing benchmark which is as close to real-world usage as reasonably possible and computing its summary. Its outputs are 'startup-times.csv' and 'startup-summary.md'. All configuration files are benchmarked in alternate fashion: first 'init' file, second, ..., last, first, etc. WARNING: EXECUTION OF THIS SCRIPT LEADS TO MONITOR FLICKERING WHICH MAY CAUSE HARM TO YOUR HEALTH. This is needed to ensure that Neovim was actually opened and something was drawn.
- 'install.sh' - script for installing all required plugins. NOTE: run `chmod +x install.sh` to make it executable.
- 'make_summary.py' - Python script to compute summary statistics of csv-file.
- 'startup-times.csv' (ignored by Git, latest one can be seen in [this gist](https://gist.github.com/echasnovski/85c334396df6fd0cea7bb42246efb97b)) - csv-file with measured startup times. Each row represent single startup round: when all 'init' files are run alternately. Each column represents startup times of single 'init' file.
- 'startup-summary.md' - markdown file as output of 'make_summary.py'. Contains summaries of 'startup-times.csv'.

View File

@ -0,0 +1,56 @@
#! /bin/bash
# Perform benchmarking of startup times with different Neovim 'init' files.
# Execute `nvim -u <*> --startuptime <*>` several times (as closely to actual
# usage as possible) in rounds alternating between input 'init' files (to "mix"
# possible random noise). Store output in .csv file with rows containing
# startup times for a single round, columns - for a single 'init' file.
# WARNING: EXECUTION OF THIS SCRIPT LEADS TO FLICKERING OF SCREEN WHICH WHICH
# MAY CAUSE HARM TO YOUR HEALTH. This is because every 'init' file leads to an
# actual opening of Neovim with later automatic closing.
# Number of rounds to perform benchmark
n_rounds=1000
# Path to output .csv file with startup times per round
csv_file=startup-times.csv
# Path to output .md file with summary table
summary_file=startup-summary.md
# 'Init' files ids with actual paths computed as 'init-files/init_*.lua'
init_files=(starter-default empty startify-starter startify-original startify-alpha dashboard-starter dashboard-original dashboard-alpha)
function comma_join { local IFS=","; shift; echo "$*"; }
function benchmark {
rm -f "$csv_file"
touch "$csv_file"
local tmp_bench_file="tmp-bench.txt"
touch "$tmp_bench_file"
comma_join -- "$@" >> startup-times.csv
for i in $(seq 1 $n_rounds); do
echo "Round $i"
local bench_times=()
for init_file in "$@"; do
nvim -u "init-files/init_$init_file.lua" --startuptime "$tmp_bench_file"
local b_time=$(tail -n 1 "$tmp_bench_file" | cut -d " " -f1)
bench_times=("${bench_times[@]}" "$b_time")
done
comma_join -- "${bench_times[@]}" >> "$csv_file"
rm "$tmp_bench_file"
done
}
benchmark "${init_files[@]}"
# Produce output summary
./make_summary.py "${csv_file}" "${summary_file}"

View File

@ -0,0 +1,21 @@
vim.cmd([[set packpath=/tmp/nvim/site]])
vim.cmd([[packadd alpha-nvim]])
local alpha = require('alpha')
local dashboard = require('alpha.themes.dashboard')
alpha.setup(dashboard.opts)
vim.g.mapleader = ' '
-- Some of these keybindings doesn't exactly match with what is shown in
-- buffer, but this doesn't really matter. Main point is that some keybindings
-- should be pre-made.
vim.api.nvim_set_keymap('n', '<Leader>ff', ':Telescope find_files<CR>', { noremap = true, silent = true })
vim.api.nvim_set_keymap('n', '<Leader>fh', ':Telescope oldfiles<CR>', { noremap = true, silent = true })
vim.api.nvim_set_keymap('n', '<Leader>fr', ':Telescope jumplist<CR>', { noremap = true, silent = true })
vim.api.nvim_set_keymap('n', '<Leader>fg', ':Telescope live_grep<CR>', { noremap = true, silent = true })
vim.api.nvim_set_keymap('n', '<Leader>fm', ':Telescope marks<CR>', { noremap = true, silent = true })
vim.api.nvim_set_keymap('n', '<Leader>sl', ':Telescope command_history<CR>', { noremap = true, silent = true })
-- Close Neovim just after fully opening it. Randomize to make "more real".
vim.defer_fn(function() vim.cmd([[quit]]) end, 100 + 200 * math.random())

View File

@ -0,0 +1,17 @@
vim.cmd([[set packpath=/tmp/nvim/site]])
vim.cmd([[packadd dashboard-nvim]])
vim.g.mapleader = ' '
vim.g.dashboard_default_executive = 'telescope'
vim.api.nvim_set_keymap('n', '<Leader>ss', ':<C-u>SessionSave<CR>', {})
vim.api.nvim_set_keymap('n', '<Leader>sl', ':<C-u>SessionLoad<CR>', {})
vim.api.nvim_set_keymap('n', '<Leader>fh', ':DashboardFindHistory<CR>', { noremap = true, silent = true })
vim.api.nvim_set_keymap('n', '<Leader>ff', ':DashboardFindFile<CR>', { noremap = true, silent = true })
vim.api.nvim_set_keymap('n', '<Leader>tc', ':DashboardChangeColorscheme<CR>', { noremap = true, silent = true })
vim.api.nvim_set_keymap('n', '<Leader>fa', ':DashboardFindWord<CR>', { noremap = true, silent = true })
vim.api.nvim_set_keymap('n', '<Leader>fb', ':DashboardJumpMark<CR>', { noremap = true, silent = true })
vim.api.nvim_set_keymap('n', '<Leader>cn', ':DashboardNewFile<CR>', { noremap = true, silent = true })
-- Close Neovim just after fully opening it. Randomize to make "more real".
vim.defer_fn(function() vim.cmd([[quit]]) end, 100 + 200 * math.random())

View File

@ -0,0 +1,18 @@
vim.cmd([[set packpath=/tmp/nvim/site]])
vim.cmd([[packadd mini.nvim]])
local starter = require('mini.starter')
starter.setup({
items = {
{ name = 'Edit file', action = [[enew]], section = 'Actions' },
{ name = 'Quit', action = [[quit]], section = 'Actions' },
starter.sections.telescope(),
},
content_hooks = {
starter.gen_hook.adding_bullet(),
starter.gen_hook.aligning('center', 'center'),
},
})
-- Close Neovim just after fully opening it. Randomize to make "more real".
vim.defer_fn(function() vim.cmd([[quit]]) end, 100 + 200 * math.random())

View File

@ -0,0 +1,4 @@
vim.cmd([[set packpath=/tmp/nvim/site]])
-- Close Neovim just after fully opening it. Randomize to make "more real".
vim.defer_fn(function() vim.cmd([[quit]]) end, 100 + 200 * math.random())

View File

@ -0,0 +1,7 @@
vim.cmd([[set packpath=/tmp/nvim/site]])
vim.cmd([[packadd mini.nvim]])
require('mini.starter').setup()
-- Close Neovim just after fully opening it. Randomize to make "more real".
vim.defer_fn(function() vim.cmd([[quit]]) end, 100 + 200 * math.random())

View File

@ -0,0 +1,10 @@
vim.cmd([[set packpath=/tmp/nvim/site]])
vim.cmd([[packadd alpha-nvim]])
local alpha = require('alpha')
local startify = require('alpha.themes.startify')
startify.nvim_web_devicons.enabled = false
alpha.setup(startify.opts)
-- Close Neovim just after fully opening it. Randomize to make "more real".
vim.defer_fn(function() vim.cmd([[quit]]) end, 100 + 200 * math.random())

View File

@ -0,0 +1,7 @@
vim.cmd([[set packpath=/tmp/nvim/site]])
vim.cmd([[packadd vim-startify]])
vim.g.startify_custom_header = ''
-- Close Neovim just after fully opening it. Randomize to make "more real".
vim.defer_fn(function() vim.cmd([[quit]]) end, 100 + 200 * math.random())

View File

@ -0,0 +1,20 @@
vim.cmd([[set packpath=/tmp/nvim/site]])
vim.cmd([[packadd mini.nvim]])
local starter = require('mini.starter')
starter.setup({
evaluate_single = true,
items = {
starter.sections.builtin_actions(),
starter.sections.recent_files(10, false),
starter.sections.recent_files(10, true),
},
content_hooks = {
starter.gen_hook.adding_bullet(),
starter.gen_hook.indexing('all', { 'Builtin actions' }),
starter.gen_hook.padding(3, 2),
},
})
-- Close Neovim just after fully opening it. Randomize to make "more real".
vim.defer_fn(function() vim.cmd([[quit]]) end, 100 + 200 * math.random())

View File

@ -0,0 +1,10 @@
#! /bin/bash
PLUGINPATH=/tmp/nvim/site/pack/bench/opt
rm -rf $PLUGINPATH
mkdir -p $PLUGINPATH
cd $PLUGINPATH
git clone --depth 1 https://github.com/echasnovski/mini.nvim
git clone --depth 1 https://github.com/goolord/alpha-nvim
git clone --depth 1 https://github.com/glepnir/dashboard-nvim
git clone --depth 1 https://github.com/mhinz/vim-startify

View File

@ -0,0 +1,68 @@
#!/usr/bin/env python
import argparse
import csv
import statistics
def read_csv_columns(csv_path):
with open(csv_path, "r") as csvfile:
reader = csv.DictReader(csvfile)
res = {h: [] for h in reader.fieldnames}
for line_dict in reader:
for h, val in line_dict.items():
res[h].append(float(val))
return res
def summarise_array(x):
return {
"median": statistics.median(x),
"mean": statistics.mean(x),
"stdev": statistics.stdev(x),
"minimum": min(x),
"maximum": max(x),
}
def save_md_summary(summary, output_path):
lines = []
row_names = list(summary.keys())
col_names = ["init file"] + list(summary[row_names[0]].keys())
lines.append(" | ".join(col_names))
lines.append(" | ".join("---" for _ in col_names))
for row_n in row_names:
l = [row_n] + [str(round(x, 1)) + 'ms' for x in summary[row_n].values()]
lines.append(" | ".join(l))
lines = ["| " + l + " |\n" for l in lines]
with open(output_path, "w") as output:
for l in lines:
output.write(l)
def compute_summary(csv_path):
columns = read_csv_columns(csv_path)
return {h: summarise_array(x) for h, x in columns.items()}
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"input_csv", help="path to file with startup times in csv format", type=str
)
parser.add_argument(
"output_md",
help="output path where markdown summary table will be written",
type=str,
)
args = parser.parse_args()
save_md_summary(compute_summary(args.input_csv), args.output_md)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,10 @@
| init file | median | mean | stdev | minimum | maximum |
| --- | --- | --- | --- | --- | --- |
| starter-default | 69.0ms | 70.8ms | 4.3ms | 63.6ms | 82.7ms |
| empty | 64.1ms | 65.8ms | 4.3ms | 60.0ms | 79.4ms |
| startify-starter | 70.2ms | 71.9ms | 4.5ms | 64.3ms | 85.7ms |
| startify-original | 80.3ms | 82.2ms | 4.5ms | 76.4ms | 107.2ms |
| startify-alpha | 72.1ms | 73.8ms | 4.3ms | 66.5ms | 86.9ms |
| dashboard-starter | 68.4ms | 70.3ms | 4.5ms | 63.3ms | 102.9ms |
| dashboard-original | 70.6ms | 72.3ms | 4.4ms | 65.5ms | 99.6ms |
| dashboard-alpha | 69.8ms | 71.5ms | 4.4ms | 64.7ms | 97.2ms |

View File

@ -0,0 +1,95 @@
-- 'Minicyan' color scheme
-- Derived from base16 (https://github.com/chriskempson/base16) and
-- mini_palette palette generator
local use_cterm, palette
-- Dark palette is an output of 'MiniBase16.mini_palette':
-- - Background '#0A2A2A' (LCh(uv) = 15-10-192)
-- - Foreground '#D0D0D0' (Lch(uv) = 83-0-0)
-- - Accent chroma 50
if vim.o.background == 'dark' then
palette = {
base00 = '#0a2a2a',
base01 = '#324747',
base02 = '#556868',
base03 = '#788a8a',
base04 = '#bbbbbb',
base05 = '#d0d0d0',
base06 = '#e6e6e6',
base07 = '#fcfcfc',
base08 = '#ebcd91',
base09 = '#9f8340',
base0A = '#209870',
base0B = '#82e3ba',
base0C = '#bb6d9b',
base0D = '#a9d4ff',
base0E = '#ffb9e5',
base0F = '#598ab9',
}
use_cterm = {
base00 = 235,
base01 = 238,
base02 = 241,
base03 = 245,
base04 = 250,
base05 = 252,
base06 = 7,
base07 = 15,
base08 = 186,
base09 = 137,
base0A = 29,
base0B = 115,
base0C = 132,
base0D = 153,
base0E = 218,
base0F = 67,
}
end
-- Light palette is an 'inverted dark', output of 'MiniBase16.mini_palette':
-- - Background '#C0D2D2' (LCh(uv) = 83-10-192)
-- - Foreground '#262626' (Lch(uv) = 15-0-0)
-- - Accent chroma 80
if vim.o.background == 'light' then
palette = {
base00 = '#c0d2d2',
base01 = '#9badad',
base02 = '#778989',
base03 = '#546767',
base04 = '#353535',
base05 = '#262626',
base06 = '#181818',
base07 = '#040404',
base08 = '#402100',
base09 = '#855f00',
base0A = '#007d3c',
base0B = '#003d00',
base0C = '#b12985',
base0D = '#003fb6',
base0E = '#7e0052',
base0F = '#006cb4',
}
use_cterm = {
base00 = 252,
base01 = 248,
base02 = 102,
base03 = 241,
base04 = 236,
base05 = 235,
base06 = 234,
base07 = 0,
base08 = 52,
base09 = 94,
base0A = 29,
base0B = 22,
base0C = 126,
base0D = 25,
base0E = 89,
base0F = 25,
}
end
if palette then
require('mini.base16').setup({ palette = palette, use_cterm = use_cterm })
vim.g.colors_name = 'minicyan'
end

View File

@ -0,0 +1,95 @@
-- 'Minischeme' color scheme
-- Derived from base16 (https://github.com/chriskempson/base16) and
-- mini_palette palette generator
local use_cterm, palette
-- Dark palette is an output of 'MiniBase16.mini_palette':
-- - Background '#112641' (LCh(uv) = 15-20-250)
-- - Foreground '#e2e98f' (Lch(uv) = 90-60-90)
-- - Accent chroma 75
if vim.o.background == 'dark' then
palette = {
base00 = '#112641',
base01 = '#3a475e',
base02 = '#606b81',
base03 = '#8691a7',
base04 = '#d5dc81',
base05 = '#e2e98f',
base06 = '#eff69c',
base07 = '#fcffaa',
base08 = '#ffcfa0',
base09 = '#cc7e46',
base0A = '#46a436',
base0B = '#9ff895',
base0C = '#ca6ecf',
base0D = '#42f7ff',
base0E = '#ffc4ff',
base0F = '#00a5c5',
}
use_cterm = {
base00 = 235,
base01 = 238,
base02 = 60,
base03 = 103,
base04 = 186,
base05 = 186,
base06 = 229,
base07 = 229,
base08 = 223,
base09 = 173,
base0A = 71,
base0B = 156,
base0C = 170,
base0D = 87,
base0E = 225,
base0F = 38,
}
end
-- Light palette is an 'inverted dark', output of 'MiniBase16.mini_palette':
-- - Background '#e2e5ca' (LCh(uv) = 90-20-90)
-- - Foreground '#002a83' (Lch(uv) = 15-60-250)
-- - Accent chroma 75
if vim.o.background == 'light' then
palette = {
base00 = '#e2e5ca',
base01 = '#bcbfa4',
base02 = '#979a7e',
base03 = '#73765a',
base04 = '#324490',
base05 = '#002a83',
base06 = '#0000e4',
base07 = '#080500',
base08 = '#5e2200',
base09 = '#a86400',
base0A = '#008818',
base0B = '#004500',
base0C = '#b34aad',
base0D = '#004b76',
base0E = '#7d0077',
base0F = '#0086ae',
}
use_cterm = {
base00 = 254,
base01 = 250,
base02 = 246,
base03 = 243,
base04 = 60,
base05 = 18,
base06 = 4,
base07 = 232,
base08 = 52,
base09 = 130,
base0A = 28,
base0B = 22,
base0C = 133,
base0D = 24,
base0E = 90,
base0F = 31,
}
end
if palette then
require('mini.base16').setup({ palette = palette, use_cterm = use_cterm })
vim.g.colors_name = 'minischeme'
end

View File

@ -0,0 +1,758 @@
==============================================================================
------------------------------------------------------------------------------
*mini.ai*
*MiniAi*
Module for extending and creating `a`/`i` textobjects. It enhances some builtin
|text-objects| (like |a(|, |a)|, |a'|, and more), creates new ones (like `a*`, `a<Space>`,
`af`, `a?`, and more), and allows user to create their own.
Features:
- Customizable creation of `a`/`i` textobjects using Lua patterns and functions.
Supports:
- Dot-repeat.
- |v:count|.
- Different search methods (see |MiniAi.config|).
- Consecutive application (update selection without leaving Visual mode).
- Aliases for multiple textobjects.
- Comprehensive builtin textobjects (see more in |MiniAi-textobject-builtin|):
- Balanced brackets (with and without whitespace) plus alias.
- Balanced quotes plus alias.
- Function call.
- Argument.
- Tag.
- Derived from user prompt.
- Default for punctuation, digit, or whitespace single character.
- Motions for jumping to left/right edge of textobject.
- Set of specification generators to tweak some builtin textobjects (see
|MiniAi.gen_spec|).
- Treesitter textobjects (through |MiniAi.gen_spec.treesitter()| helper).
This module works by defining mappings for both `a` and `i` in Visual and
Operator-pending mode. After typing, they wait for single character user input
treated as textobject identifier and apply resolved textobject specification
(fall back to other mappings if can't find proper textobject id). For more
information see |MiniAi-textobject-specification| and |MiniAi-algorithm|.
Known issues which won't be resolved:
- Search for builtin textobjects is done mostly using Lua patterns
(regex-like approach). Certain amount of false positives is to be expected.
- During search for builtin textobjects there is no distinction if it is
inside string or comment. For example, in the following case there will
be wrong match for a function call: `f(a = ")", b = 1)`.
General rule of thumb: any instrument using available parser for document
structure (like treesitter) will usually provide more precise results. This
module has builtins mostly for plain text textobjects which are useful
most of the times (like "inside brackets", "around quotes/underscore", etc.).
For advanced use cases define function specification for custom textobjects.
What it doesn't (and probably won't) do:
- Have special operators to specially handle whitespace (like `I` and `A`
in 'targets.vim'). Whitespace handling is assumed to be done inside
textobject specification (like `i(` and `i)` handle whitespace differently).
# Setup~
This module needs a setup with `require('mini.ai').setup({})` (replace
`{}` with your `config` table). It will create global Lua table `MiniAi`
which you can use for scripting or manually (with `:lua MiniAi.*`).
See |MiniAi.config| for available config settings.
You can override runtime config settings (like `config.custom_textobjects`)
locally to buffer inside `vim.b.miniai_config` which should have same structure
as `MiniAi.config`. See |mini.nvim-buffer-local-config| for more details.
# Comparisons~
- 'wellle/targets.vim':
- Has limited support for creating own textobjects: it is constrained
to pre-defined detection rules. 'mini.ai' allows creating own rules
via Lua patterns and functions (see |MiniAi-textobject-specification|).
- Doesn't provide any programmatical API for getting information about
textobjects. 'mini.ai' does it via |MiniAi.find_textobject()|.
- Has no implementation of "moving to edge of textobject". 'mini.ai'
does it via |MiniAi.move_cursor()| and `g[` and `g]` default mappings.
- Has elaborate ways to control searching of the next textobject.
'mini.ai' relies on handful of 'config.search_method'.
- Implements `A`, `I` operators. 'mini.ai' does not by design: it is
assumed to be a property of textobject, not operator.
- Doesn't implement "function call" and "user prompt" textobjects.
'mini.ai' does (with `f` and `?` identifiers).
- Has limited support for "argument" textobject. Although it works in
most situations, it often misdetects commas as argument separator
(like if it is inside quotes or `{}`). 'mini.ai' deals with these cases.
- 'nvim-treesitter/nvim-treesitter-textobjects':
- Along with textobject functionality provides a curated and maintained
set of popular textobject queries for many languages (which can power
|MiniAi.gen_spec.treesitter()| functionality).
- Operates with custome treesitter directives (see
|lua-treesitter-directives|) allowing more fine-tuned textobjects.
- Implements only textobjects based on treesitter.
- Doesn't support |v:count|.
- Doesn't support multiple search method (basically, only 'cover').
- Doesn't support consecutive application of target textobject.
# Disabling~
To disable, set `g:miniai_disable` (globally) or `b:miniai_disable`
(for a buffer) to `v:true`. Considering high number of different scenarios
and customization intentions, writing exact rules for disabling module's
functionality is left to user. See |mini.nvim-disabling-recipes| for common
recipes.
------------------------------------------------------------------------------
*MiniAi-textobject-builtin*
Builtin textobjects~
This table describes all builtin textobjects along with what they
represent. Explanation:
- `Key` represents the textobject identifier: single character which should
be typed after `a`/`i`.
- `Name` is a description of textobject.
- `Example line` contains a string for which examples are constructed. The
`*` denotes the cursor position.
- `a`/`i` describe inclusive region representing `a` and `i` textobjects.
Use numbers in separators for easier navigation.
- `2a`/`2i` describe either `2a`/`2i` (support for |v:count|) textobjects
or `a`/`i` textobject followed by another `a`/`i` textobject (consecutive
application leads to incremental selection).
Example: typing `va)` with cursor on `*` leads to selection from column 2
to column 12. Another typing `a)` changes selection to [1; 13]. Also, besides
visual selection, any |operator| can be used or `g[`/`g]` motions to move
to left/right edge of `a` textobject.
>
|Key| Name | Example line | a | i | 2a | 2i |
|---|---------------|-1234567890123456-|--------|--------|--------|--------|
| ( | Balanced () | (( *a (bb) )) | | | | |
| [ | Balanced [] | [[ *a [bb] ]] | [2;12] | [4;10] | [1;13] | [2;12] |
| { | Balanced {} | {{ *a {bb} }} | | | | |
| < | Balanced <> | << *a <bb> >> | | | | |
|---|---------------|-1234567890123456-|--------|--------|--------|--------|
| ) | Balanced () | (( *a (bb) )) | | | | |
| ] | Balanced [] | [[ *a [bb] ]] | | | | |
| } | Balanced {} | {{ *a {bb} }} | [2;12] | [3;11] | [1;13] | [2;12] |
| > | Balanced <> | << *a <bb> >> | | | | |
| b | Alias for | [( *a {bb} )] | | | | |
| | ), ], or } | | | | | |
|---|---------------|-1234567890123456-|--------|--------|--------|--------|
| " | Balanced " | "*a" " bb " | | | | |
| ' | Balanced ' | '*a' ' bb ' | | | | |
| ` | Balanced ` | `*a` ` bb ` | [1;4] | [2;3] | [6;11] | [7;10] |
| q | Alias for | '*a' " bb " | | | | |
| | ", ', or ` | | | | | |
|---|---------------|-1234567890123456-|--------|--------|--------|--------|
| ? | User prompt | e*e o e o o | [3;5] | [4;4] | [7;9] | [8;8] |
| |(typed e and o)| | | | | |
|---|---------------|-1234567890123456-|--------|--------|--------|--------|
| t | Tag | <x>*</x><y>b</y> | [1;8] | [4;4] | [9;16] |[12;12] |
|---|---------------|-1234567890123456-|--------|--------|--------|--------|
| f | Function call | f(a, g(*b, c) ) | [6;13] | [8;12] | [1;15] | [3;14] |
|---|---------------|-1234567890123456-|--------|--------|--------|--------|
| a | Argument | f(*a, g(b, c) ) | [3;5] | [3;4] | [5;14] | [7;13] |
|---|---------------|-1234567890123456-|--------|--------|--------|--------|
| | Default | | | | | |
| | (digits, | aa_*b__cc___ | [4;7] | [4;5] | [8;12] | [8;9] |
| | punctuation, | (example for _) | | | | |
| | or whitespace)| | | | | |
|---|---------------|-1234567890123456-|--------|--------|--------|--------|
<
Notes:
- All examples assume default `config.search_method`.
- Open brackets differ from close brackets by how they treat inner edge
whitespace for `i` textobject: open ignores it, close - includes.
- Default textobject is activated for identifiers from digits (0, ..., 9),
punctuation (like `_`, `*`, `,`, etc.), whitespace (space, tab, etc.).
They are designed to be treated as separators, so include only right edge
in `a` textobject. To include both edges, use custom textobjects
(see |MiniAi-textobject-specification| and |MiniAi.config|).
------------------------------------------------------------------------------
*MiniAi-glossary*
- REGION - table representing region in a buffer. Fields: <from> and
<to> for inclusive start and end positions (<to> might be `nil` to
describe empty region). Each position is also a table with line <line>
and column <col> (both start at 1). Examples:
- `{ from = { line = 1, col = 1 }, to = { line = 2, col = 1 } }`
- `{ from = { line = 10, col = 10 } }` - empty region.
- PATTERN - string describing Lua pattern.
- SPAN - interval inside a string (end-exclusive). Like [1, 5). Equal
`from` and `to` edges describe empty span at that point.
- SPAN `A = [a1, a2)` COVERS `B = [b1, b2)` if every element of
`B` is within `A` (`a1 <= b < a2`).
It also is described as B IS NESTED INSIDE A.
- NESTED PATTERN - array of patterns aimed to describe nested spans.
- SPAN MATCHES NESTED PATTERN if there is a sequence of consecutively
nested spans each matching corresponding pattern within substring of
previous span (or input string for first span). Example:
Nested patterns: `{ '%b()', '^. .* .$' }` (balanced `()` with inner space)
Input string: `( ( () ( ) ) )`
`123456789012345`
Here are all matching spans [1, 15) and [3, 13). Both [5, 7) and [8, 10)
match first pattern but not second. All other combinations of `(` and `)`
don't match first pattern (not balanced).
- COMPOSED PATTERN: array with each element describing possible pattern
(or array of them) at that place. Composed pattern basically defines all
possible combinations of nested pattern (their cartesian product).
Examples:
1. Composed pattern: `{ { '%b()', '%b[]' }, '^. .* .$' }`
Composed pattern expanded into equivalent array of nested patterns:
`{ '%b()', '^. .* .$' }` and `{ '%b[]', '^. .* .$' }`
Description: either balanced `()` or balanced `[]` but both with
inner edge space.
2. Composed pattern:
`{ { { '%b()', '^. .* .$' }, { '%b[]', '^.[^ ].*[^ ].$' } }, '.....' }`
Composed pattern expanded into equivalent array of nested patterns:
`{ '%b()', '^. .* .$', '.....' }` and
`{ '%b[]', '^.[^ ].*[^ ].$', '.....' }`
Description: either "balanced `()` with inner edge space" or
"balanced `[]` with no inner edge space", both with 5 or more characters.
- SPAN MATCHES COMPOSED PATTERN if it matches at least one nested pattern
from expanded composed pattern.
------------------------------------------------------------------------------
*MiniAi-textobject-specification*
Textobject specification has a structure of composed pattern (see
|MiniAi-glossary|) with two differences:
- Last pattern(s) should have even number of empty capture groups denoting
how the last string should be processed to extract `a` or `i` textobject:
- Zero captures mean that whole string represents both `a` and `i`.
Example: `xxx` will define textobject matching string `xxx` literally.
- Two captures represent `i` textobject inside of them. `a` - whole string.
Example: `x()x()x` defines `a` textobject to be `xxx`, `i` - middle `x`.
- Four captures define `a` textobject inside captures 1 and 4, `i` -
inside captures 2 and 3. Example: `x()()x()x()` defines `a`
textobject to be last `xx`, `i` - middle `x`.
- Allows callable objects (see |vim.is_callable()|) in certain places
(enables more complex textobjects in exchange of increase in configuration
complexity and computations):
- If specification itself is a callable, it will be called with the same
arguments as |MiniAi.find_textobject()| and should return one of:
- Composed pattern. Useful for implementing user input. Example of
simplified variant of textobject for function call with name taken
from user prompt:
>
function()
local left_edge = vim.pesc(vim.fn.input('Function name: '))
return { string.format('%s+%%b()', left_edge), '^.-%(().*()%)$' }
end
<
- Single output region. Useful to allow full control over
textobject. Will be taken as is. Example of returning whole buffer:
>
function()
local from = { line = 1, col = 1 }
local to = {
line = vim.fn.line('$'),
col = math.max(vim.fn.getline('$'):len(), 1)
}
return { from = from, to = to }
end
<
- Array of output region(s). Useful for incorporating other
instruments, like treesitter (see |MiniAi.gen_spec.treesitter()|).
The best region will be picked in the same manner as with composed
pattern (respecting options `n_lines`, `search_method`, etc.).
Example of selecting "best" line with display width more than 80:
>
function(_, _, _)
local res = {}
for i = 1, vim.api.nvim_buf_line_count(0) do
local cur_line = vim.fn.getline(i)
if vim.fn.strdisplaywidth(cur_line) > 80 then
local region = {
from = { line = i, col = 1 },
to = { line = i, col = cur_line:len() },
}
table.insert(res, region)
end
end
return res
end
<
- If there is a callable instead of assumed string pattern, it is expected
to have signature `(line, init)` and behave like `pattern:find()`.
It should return two numbers representing span in `line` next after
or at `init` (`nil` if there is no such span).
!IMPORTANT NOTE!: it means that output's `from` shouldn't be strictly
to the left of `init` (it will lead to infinite loop). Not allowed as
last item (as it should be pattern with captures).
Example of matching only balanced parenthesis with big enough width:
>
{
'%b()',
function(s, init)
if init > 1 or s:len() < 5 then return end
return 1, s:len()
end,
'^.().*().$'
}
>
More examples:
- See |MiniAi.gen_spec| for function wrappers to create commonly used
textobject specifications.
- Pair of balanced brackets from set (used for builtin `b` identifier):
`{ { '%b()', '%b[]', '%b{}' }, '^.().*().$' }`
- Imitate word ignoring digits and punctuation (supports only Latin alphabet):
`{ '()()%f[%w]%w+()[ \t]*()' }`
- Word with camel case support (also supports only Latin alphabet):
`{`
`{`
`'%u[%l%d]+%f[^%l%d]',`
`'%f[%S][%l%d]+%f[^%l%d]',`
`'%f[%P][%l%d]+%f[^%l%d]',`
`'^[%l%d]+%f[^%l%d]',`
`},`
`'^().*()$'`
`}`
- Number: `{ '%f[%d]%d+' }`
- Date in 'YYYY-MM-DD' format: `{ '()%d%d%d%d%-%d%d%-%d%d()' }`
- Lua block string: `{ '%[%[().-()%]%]' }`
------------------------------------------------------------------------------
*MiniAi-algorithm*
Algorithm design
Search for the textobjects relies on these principles:
- It uses same input data as described in |MiniAi.find_textobject()|,
i.e. whether it is `a` or `i` textobject, its identifier, reference region, etc.
- Textobject specification is constructed based on textobject identifier
(see |MiniAi-textobject-specification|).
- General search is done by converting some 2d buffer region (neighborhood
of reference region) into 1d string (each line is appended with `\n`).
Then search for a best span matching textobject specification is done
inside string (see |MiniAi-glossary|). After that, span is converted back
into 2d region. Note: first search is done inside reference region lines,
and only after that - inside its neighborhood within `config.n_lines`
(see |MiniAi.config|).
- The best matching span is chosen by iterating over all spans matching
textobject specification and comparing them with "current best".
Comparison also depends on reference region (tighter covering is better,
otherwise closer is better) and search method (if span is even considered).
- Extract span based on extraction pattern (last item in nested pattern).
- If task is to perform a consecutive search (`opts.n_times` is greater than 1),
steps are repeated with current best match becoming reference region.
One such additional step is also done if final region is equal to
reference region (this enables consecutive application).
Notes:
- Iteration over all matched spans is done in depth-first fashion with
respect to nested pattern.
- It is guaranteed that span is compared only once.
- For the sake of increasing functionality, during iteration over all
matching spans, some Lua patterns in composed pattern are handled
specially.
- `%bxx` (`xx` is two identical characters). It denotes balanced pair
of identical characters and results into "paired" matches. For
example, `%b""` for `"aa" "bb"` would match `"aa"` and `"bb"`, but
not middle `" "`.
- `x.-y` (`x` and `y` are different strings). It results only in matches with
smallest width. For example, `e.-o` for `e e o o` will result only in
middle `e o`. Note: it has some implications for when parts have
quantifiers (like `+`, etc.), which usually can be resolved with
frontier pattern `%f[]` (see examples in |MiniAi-textobject-specification|).
------------------------------------------------------------------------------
*MiniAi.setup()*
`MiniAi.setup`({config})
Module setup
Parameters~
{config} `(table|nil)` Module config table. See |MiniAi.config|.
Usage~
`require('mini.ai').setup({})` (replace `{}` with your `config` table)
------------------------------------------------------------------------------
*MiniAi.config*
`MiniAi.config`
Module config
Default values:
>
MiniAi.config = {
-- Table with textobject id as fields, textobject specification as values.
-- Also use this to disable builtin textobjects. See |MiniAi.config|.
custom_textobjects = nil,
-- Module mappings. Use `''` (empty string) to disable one.
mappings = {
-- Main textobject prefixes
around = 'a',
inside = 'i',
-- Next/last textobjects
around_next = 'an',
inside_next = 'in',
around_last = 'al',
inside_last = 'il',
-- Move cursor to corresponding edge of `a` textobject
goto_left = 'g[',
goto_right = 'g]',
},
-- Number of lines within which textobject is searched
n_lines = 50,
-- How to search for object (first inside current line, then inside
-- neighborhood). One of 'cover', 'cover_or_next', 'cover_or_prev',
-- 'cover_or_nearest', 'next', 'previous', 'nearest'.
search_method = 'cover_or_next',
}
<
# Options ~
## Custom textobjects ~
Each named entry of `config.custom_textobjects` is a textobject with
that identifier and specification (see |MiniAi-textobject-specification|).
They are also used to override builtin ones (|MiniAi-textobject-builtin|).
Supply non-table input to disable builtin textobject. Examples:
>
require('mini.ai').setup({
custom_textobjects = {
-- Disables function call textobject
f = false,
-- Tweaks argument textobject
a = require('mini.ai').gen_spec.argument({ brackets = { '%b()' } }),
-- Now `vax` should select `xxx` and `vix` - middle `x`
x = { 'x()x()x' },
-- Whole buffer
g = function()
local from = { line = 1, col = 1 }
local to = {
line = vim.fn.line('$'), col = vim.fn.getline('$'):len()
}
return { from = from, to = to }
end
}
})
-- Use `vim.b.miniai_config` to customize per buffer
-- Example of specification useful for Markdown files:
local spec_pair = require('mini.ai').gen_spec.pair
vim.b.miniai_config = {
custom_textobjects = {
['*'] = spec_pair('*', '*', { type = 'greedy' }),
['_'] = spec_pair('_', '_', { type = 'greedy' }),
},
}
<
There are more example specifications in |MiniAi-textobject-specification|.
## Search method~
Value of `config.search_method` defines how best match search is done.
Based on its value, one of the following matches will be selected:
- Covering match. Left/right edge is before/after left/right edge of
reference region.
- Previous match. Left/right edge is before left/right edge of reference
region.
- Next match. Left/right edge is after left/right edge of reference region.
- Nearest match. Whichever is closest among previous and next matches.
Possible values are:
- `'cover'` - use only covering match. Don't use either previous or
next; report that there is no textobject found.
- `'cover_or_next'` (default) - use covering match. If not found, use next.
- `'cover_or_prev'` - use covering match. If not found, use previous.
- `'cover_or_nearest'` - use covering match. If not found, use nearest.
- `'next'` - use next match.
- `'previous'` - use previous match.
- `'nearest'` - use nearest match.
Note: search is first performed on the reference region lines and only
after failure - on the whole neighborhood defined by `config.n_lines`. This
means that with `config.search_method` not equal to `'cover'`, "previous"
or "next" textobject will end up as search result if they are found on
first stage although covering match might be found in bigger, whole
neighborhood. This design is based on observation that most of the time
operation is done within reference region lines (usually cursor line).
Here is an example of what `a)` textobject is based on a value of
`'config.search_method'` when cursor is inside `bbb` word:
- `'cover'`: `(a) bbb (c)` -> none
- `'cover_or_next'`: `(a) bbb (c)` -> `(c)`
- `'cover_or_prev'`: `(a) bbb (c)` -> `(a)`
- `'cover_or_nearest'`: depends on cursor position.
For first and second `b` - as in `cover_or_prev` (as previous match is
nearer), for third - as in `cover_or_next` (as next match is nearer).
- `'next'`: `(a) bbb (c)` -> `(c)`. Same outcome for `(bbb)`.
- `'prev'`: `(a) bbb (c)` -> `(a)`. Same outcome for `(bbb)`.
- `'nearest'`: depends on cursor position (same as in `'cover_or_nearest'`).
## Mappings~
Mappings `around_next`/`inside_next` and `around_last`/`inside_last` are
essentially `around`/`inside` but using search method `'next'` and `'prev'`.
------------------------------------------------------------------------------
*MiniAi.find_textobject()*
`MiniAi.find_textobject`({ai_type}, {id}, {opts})
Find textobject region
Parameters~
{ai_type} `(string)` One of `'a'` or `'i'`.
{id} `(string)` Single character string representing textobject id. It is
used to get specification which is later used to compute textobject region.
Note: if specification is a function, it is called with all present
arguments (`opts` is populated with default arguments).
{opts} `(table|nil)` Options. Possible fields:
- <n_lines> - Number of lines within which textobject is searched.
Default: `config.n_lines` (see |MiniAi.config|).
- <n_times> - Number of times to perform a consecutive search. Each one
is done with reference region being previous found textobject region.
Default: 1.
- <reference_region> - region to try to cover (see |MiniAi-glossary|). It
is guaranteed that output region will not be inside or equal to this one.
Default: empty region at cursor position.
- <search_method> - Search method. Default: `config.search_method`.
Return~
`(table|nil)` Region of textobject or `nil` if no textobject different
from `opts.reference_region` was consecutively found `opts.n_times` times.
------------------------------------------------------------------------------
*MiniAi.move_cursor()*
`MiniAi.move_cursor`({side}, {ai_type}, {id}, {opts})
Move cursor to edge of textobject
Parameters~
{side} `(string)` One of `'left'` or `'right'`.
{ai_type} `(string)` One of `'a'` or `'i'`.
{id} `(string)` Single character string representing textobject id.
{opts} `(table|nil)` Same as in |MiniAi.find_textobject()|.
`opts.n_times` means number of *actual* jumps (important when cursor
already on the potential jump spot).
------------------------------------------------------------------------------
*MiniAi.gen_spec*
`MiniAi.gen_spec`
Generate common textobject specifications
This is a table with function elements. Call to actually get specification.
Example: >
local gen_spec = require('mini.ai').gen_spec
require('mini.ai').setup({
custom_textobjects = {
-- Tweak argument to be recognized only inside `()` between `;`
a = gen_spec.argument({ brackets = { '%b()' }, separators = { ';' } }),
-- Tweak function call to not detect dot in function name
f = gen_spec.function_call({ name_pattern = '[%w_]' }),
-- Function definition (needs treesitter queries with these captures)
F = gen_spec.treesitter({ a = '@function.outer', i = '@function.inner' }),
-- Make `|` select both edges in non-balanced way
['|'] = gen_spec.pair('|', '|', { type = 'non-balanced' }),
}
})
------------------------------------------------------------------------------
*MiniAi.gen_spec.argument()*
`MiniAi.gen_spec.argument`({opts})
Argument specification
Argument textobject (has default `a` identifier) is a region inside
balanced bracket between allowed not excluded separators. Use this function
to tweak how it works.
Examples:
- `argument({ brackets = { '%b()' } })` will search for an argument only
inside balanced `()`.
- `argument({ separators = { ',', ';' } })` will consider both `,` and `;`
to be separators.
- `argument({ exclude_regions = { '%b()' } })` will exclude separators
which are inside balanced `()` (inside outer brackets).
Parameters~
{opts} `(table|nil)` Options. Allowed fields:
- <brackets> - table with patterns for outer balanced brackets.
Default: `{ '%b()', '%b[]', '%b{}' }` (any `()`, `[]`, or `{}` can
enclose arguments).
- <separators> - table with single character separators.
Default: `{ ',' }` (arguments are separated with `,`).
- <exclude_regions> - table with patterns for regions inside which
separators will be ignored.
Default: `{ '%b""', "%b''", '%b()', '%b[]', '%b{}' }` (separators
inside balanced quotes or brackets are ignored).
------------------------------------------------------------------------------
*MiniAi.gen_spec.function_call()*
`MiniAi.gen_spec.function_call`({opts})
Function call specification
Function call textobject (has default `f` identifier) is a region with some
characters followed by balanced `()`. Use this function to tweak how it works.
Example:
- `function_call({ name_pattern = '[%w_]' })` will recognize function name with
only alphanumeric or underscore (not dot).
Parameters~
{opts} `(table|nil)` Optsion. Allowed fields:
- <name_pattern> - string pattern of character set allowed in function name.
Default: `'[%w_%.]'` (alphanumeric, underscore, or dot).
Note: should be enclosed in `[]`.
------------------------------------------------------------------------------
*MiniAi.gen_spec.pair()*
`MiniAi.gen_spec.pair`({left}, {right}, {opts})
Pair specification
Use it to define textobject for region surrounded with `left` from left and
`right` from right. The `a` textobject includes both edges, `i` - excludes them.
Region can be one of several types (controlled with `opts.type`). All
examples are for default search method, `a` textobject, and use `'_'` as
both `left` and `right`:
- Non-balanced (`{ type = 'non-balanced' }`), default. Equivalent to using
`x.-y` as first pattern. Example: on line '_a_b_c_' it consecutively
matches '_a_', '_b_', '_c_'.
- Balanced (`{ type = 'balanced' }`). Equivalent to using `%bxy` as first
pattern. Example: on line '_a_b_c_' it consecutively matches '_a_', '_c_'.
Note: both `left` and `right` should be single character.
- Greedy (`{ type = 'greedy' }`). Like non-balanced but will select maximum
consecutive `left` and `right` edges. Example: on line '__a__b_' it
consecutively selects '__a__' and '__b_'. Note: both `left` and `right`
should be single character.
Parameters~
{left} `(string)` Left edge.
{right} `(string)` Right edge.
{opts} `(table|nil)` Options. Possible fields:
- <type> - Type of a pair. One of `'non-balanced'` (default), `'balanced'`,
`'greedy'`.
------------------------------------------------------------------------------
*MiniAi.gen_spec.treesitter()*
`MiniAi.gen_spec.treesitter`({ai_captures}, {opts})
Treesitter specification
This is a specification in function form. When called with a pair of
treesitter captures, it returns a specification function outputting an
array of regions that match corresponding (`a` or `i`) capture.
In order for this to work, apart from working treesitter parser for desired
language, user should have a reachable language-specific 'textobjects'
query (see |get_query()|). The most straightforward way for this is to have
'textobjects.scm' query file with treesitter captures stored in some
recognized path. This is primarily designed to be compatible with
'nvim-treesitter/nvim-treesitter-textobjects' plugin, but can be used
without it.
Two most common approaches for having a query file:
- Install 'nvim-treesitter/nvim-treesitter-textobjects'. It has curated and
well maintained builtin query files for many languages with a standardized
capture names, like `function.outer`, `function.inner`, etc.
- Manually create file 'after/queries/<language name>/textobjects.scm' in
your |$XDG_CONFIG_HOME| directory. It should contain queries with
captures (later used to define textobjects). See |lua-treesitter-query|.
To verify that query file is reachable, run (example for "lua" language)
`:lua print(vim.inspect(vim.treesitter.get_query_files('lua', 'textobjects')))`
(output should have at least an intended file).
Example configuration for function definition textobject with
'nvim-treesitter/nvim-treesitter-textobjects' captures:
>
local spec_treesitter = require('mini.ai').gen_spec.treesitter
require('mini.ai').setup({
custom_textobjects = {
F = spec_treesitter({ a = '@function.outer', i = '@function.inner' }),
o = spec_treesitter({
a = { '@conditional.outer', '@loop.outer' },
i = { '@conditional.inner', '@loop.inner' },
})
}
})
>
Notes:
- By default query is done using 'nvim-treesitter' plugin if it is present
(falls back to builtin methods otherwise). This allows for a more
advanced features (like multiple buffer languages, custom directives, etc.).
See `opts.use_nvim_treesitter` for how to disable this.
- It uses buffer's |filetype| to determine query language.
- On large files it is slower than pattern-based textobjects. Still very
fast though (one search should be magnitude of milliseconds or tens of
milliseconds on really large file).
Parameters~
{ai_captures} `(table)` Captures for `a` and `i` textobjects: table with
<a> and <i> fields with captures for `a` and `i` textobjects respectively.
Each value can be either a string capture (should start with `'@'`) or an
array of such captures (best among all matches will be chosen).
{opts} `(table)` Options. Possible values:
- <use_nvim_treesitter> - whether to try to use 'nvim-treesitter' plugin
(if present) to do the query. It implements more advanced behavior at
cost of increased execution time. Provides more coherent experience if
'nvim-treesitter-textobjects' queries are used. Default: `true`.
Return~
`(function)` Function with |MiniAi.find_textobject()| signature which
returns array of current buffer regions representing matches for
corresponding (`a` or `i`) treesitter capture.
See also~
|MiniAi-textobject-specification| for how this type of textobject
specification is processed.
|get_query()| for how query is fetched in case of no 'nvim-treesitter'.
|Query:iter_captures()| for how all query captures are iterated in case of
no 'nvim-treesitter'.
------------------------------------------------------------------------------
*MiniAi.select_textobject()*
`MiniAi.select_textobject`({ai_type}, {id}, {opts})
Visually select textobject region
Does nothing if no region is found.
Parameters~
{ai_type} `(string)` One of `'a'` or `'i'`.
{id} `(string)` Single character string representing textobject id.
{opts} `(table|nil)` Same as in |MiniAi.find_textobject()|. Extra fields:
- <vis_mode> - One of `'v'`, `'V'`, `'<C-v>'`. Default: Latest visual mode.
- <operator_pending> - Whether selection is for Operator-pending mode.
Used in that mode's mappings, shouldn't be used directly. Default: `false`.
------------------------------------------------------------------------------
*MiniAi.expr_textobject()*
`MiniAi.expr_textobject`({mode}, {ai_type}, {opts})
Make expression to visually select textobject
Designed to be used inside expression mapping. No need to use directly.
Textobject identifier is taken from user single character input.
Default `n_times` option is taken from |v:count1|.
Parameters~
{mode} `(string)` One of 'x' (Visual) or 'o' (Operator-pending).
{ai_type} `(string)` One of `'a'` or `'i'`.
{opts} `(table|nil)` Same as in |MiniAi.select_textobject()|.
------------------------------------------------------------------------------
*MiniAi.expr_motion()*
`MiniAi.expr_motion`({side})
Make expression for moving cursor to edge of textobject
Designed to be used inside expression mapping (powers `config.goto_left`
and `config.goto_right` mappings). No need to use directly.
Textobject identifier is taken from user single character input.
Default `n_times` option is taken from |v:count1|.
Parameters~
{side} `(string)` One of `'left'` or `'right'`.
vim:tw=78:ts=8:noet:ft=help:norl:

View File

@ -0,0 +1,912 @@
==============================================================================
------------------------------------------------------------------------------
*mini.align*
*MiniAlign*
Align text interactively (with or without instant preview). Allows rich and
flexible customization of both alignment rules and user interaction. Works
with charwise, linewise, and blockwise selections in both Normal mode (on
textobject/motion; with dot-repeat) and Visual mode.
Features:
- Alignment is done in three main steps:
- <Split> lines into parts based on Lua pattern(s) or user-supplied rule.
- <Justify> parts for certain side(s) to be same width inside columns.
- <Merge> parts to be lines, with customizable delimiter(s).
Each main step can be preceded by other steps (pre-steps) to achieve
highly customizable outcome. See `steps` value in |MiniAlign.config|. For
more details, see |MiniAlign-glossary| and |MiniAlign-algorithm|.
- User can control alignment interactively by pressing customizable modifiers
(single keys representing how alignment steps and/or options should change).
Some of default modifiers:
- Press `s` to enter split Lua pattern.
- Press `j` to choose justification side from available ones ("left",
"center", "right", "none").
- Press `m` to enter merge delimiter.
- Press `f` to enter filter Lua expression to configure which parts
will be affected (like "align only first column").
- Press `i` to ignore some commonly unwanted split matches.
- Press `p` to pair neighboring parts so they be aligned together.
- Press `t` to trim whitespace from parts.
- Press `<BS>` (backspace) to delete some last pre-step.
For more details, see |MiniAlign-modifiers-builtin| and |MiniAlign-examples|.
- Alignment can be done with instant preview (result is updated after each
modifier) or without it (result is shown and accepted after non-default
split pattern is set).
- Every user interaction is accompanied with helper status message showing
relevant information about current alignment process.
# Setup~
This module needs a setup with `require('mini.align').setup({})` (replace
`{}` with your `config` table). It will create global Lua table `MiniAlign`
which you can use for scripting or manually (with `:lua MiniAlign.*`).
See |MiniAlign.config| for available config settings.
You can override runtime config settings (like `config.modifiers`) locally
to buffer inside `vim.b.minialign_config` which should have same structure
as `MiniAlign.config`. See |mini.nvim-buffer-local-config| for more details.
# Comparisons~
- 'junegunn/vim-easy-align':
- 'mini.align' is mostly designed after 'junegunn/vim-easy-align', so
there are a lot of similarities.
- Both plugins allow users to change alignment options interactively by
pressing modifier keys (albeit completely different default ones).
'junegunn/vim-easy-align' has those modifiers fixed, while 'mini.align'
allows their full customization. See |MiniAlign.config| for examples.
- 'junegunn/vim-easy-align' is designed to treat delimiters differently
than other parts of strings. 'mini.align' doesn't distinguish split
parts from one another by design: splitting is allowed to be done
based on some other logic than by splitting on delimiters.
- 'junegunn/vim-easy-align' initially aligns by only first delimiter.
'mini.align' initially aligns by all delimiter.
- 'junegunn/vim-easy-align' implements special filtering by delimiter
row number. 'mini.align' has builtin filtering based on Lua code
supplied by user in modifier phase. See |MiniAlign.gen_step.filter|
and 'f' builtin modifier.
- 'mini.align' treats any non-registered modifier as a plain delimiter
pattern, while 'junegunn/vim-easy-align' does not.
- 'mini.align' exports core Lua function used for aligning strings
(|MiniAlign.align_strings()|).
- 'godlygeek/tabular':
- 'godlygeek/tabular' is mostly designed around single command which is
customized by printing its parameters. 'mini.align' implements
different concept of interactive alignment through pressing
customizable single character modifiers.
- 'godlygeek/tabular' can detect region upon which alignment can be
desirable. 'mini.align' does not by design: use Visual selection or
textobject/motion to explicitly define region to align.
# Disabling~
To disable, set `g:minialign_disable` (globally) or `b:minialign_disable`
(for a buffer) to `v:true`. Considering high number of different scenarios
and customization intentions, writing exact rules for disabling module's
functionality is left to user. See |mini.nvim-disabling-recipes| for common
recipes.
------------------------------------------------------------------------------
*MiniAlign-glossary*
Glossary
PARTS 2d array of strings (array of arrays of strings).
See more in |MiniAlign.as_parts()|.
ROW First-level array of parts (like `parts[1]`).
COLUMN Array of strings, constructed from parts elements with the same
second-level index (like `{ parts[1][1],` `parts[2][1], ... }`).
STEP A named callable. See |MiniAlign.new_step()|. When used in terms of
alignment steps, callable takes two arguments: some object (parts
or string array) and option table.
SPLIT Process of taking array of strings and converting it into parts.
JUSTIFY Process of taking parts and converting them to aligned parts (all
elements have same widths inside columns).
MERGE Process of taking parts and converting it back to array of strings.
Usually by concatenating rows into strings.
REGION Table representing region in a buffer. Fields: <from> and <to> for
inclusive start and end positions (<to> might be `nil` to describe
empty region). Each position is also a table with line <line> and
column <col> (both start at 1).
MODE Either charwise ("char", `v`, |charwise|), linewise ("line", `V`,
|linewise|) or blockwise ("block", `<C-v>`, |blockwise-visual|)
------------------------------------------------------------------------------
*MiniAlign-algorithm*
Algorithm design
There are two main processes implemented in 'mini.align': strings alignment
and interactive region alignment. See |MiniAlign-glossary| for more information
about used terms.
Strings alignment ~
Main implementation is in |MiniAlign.align_strings()|. Its input is array of
strings and output - array of aligned strings. The process consists from three
main steps (split, justify, merge) which can be preceded by any number of
preliminary steps (pre-split, pre-justify, pre-merge).
Algorithm:
- <Pre-split>. Take input array of strings and consecutively apply all
pre-split steps (`steps.pre_split`). Each one has `(strings, opts)` signature
and should modify array in place.
- <Split>. Take array of strings and convert it to parts with `steps.split()`.
It has `(strings, opts)` signature and should return parts.
- <Pre-justify>. Take parts and consecutively apply all pre-justify
steps (`steps.pre_justify`). Each one has `(parts, opts)` signature and
should modify parts in place.
- <Justify>. Take parts and apply `steps.justify()`. It has `(parts, opts)`
signature and should modify parts in place.
- <Pre-merge>. Take parts and consecutively apply all pre-merge
steps (`steps.pre_merge`). Each one has `(parts, opts)` signature and
should modify parts in place.
- <Merge>. Take parts and convert it to array of strings with `steps.merge()`.
It has `(parts, opts)` signature and should return array of strings.
Notes:
- All table objects are initially copied so that modification in place doesn't
affect workflow.
- Default main steps are designed to be controlled via options. See
|MiniAlign.align_strings()| and default step entries in |MiniAlign.gen_step|.
- All steps are guaranteed to take same option table as second argument.
This allows steps to "talk" to each other, i.e. earlier steps can pass data
to later ones.
Interactive region alignment ~
Interactive alignment is a main entry point for most users. It can be done
in two flavors:
- <Without preview>. Initiated via mapping defined in `start` of
`MiniAlign.config.mappings`. Alignment is accepted once split pattern becomes
non-default.
- <With preview>. Initiated via mapping defined in `start_with_preview` of
`MiniAlign.config.mappings`. Alignment result is shown after every modifier
and is accepted after `<CR>` (`Enter`) is hit. Note: each preview is done by
applying current alignment steps and options to the initial region lines,
not the ones currently displaying in preview.
Lifecycle (assuming default mappings):
- <Initiate alignment>:
- In Normal mode type `ga` (or `gA` to show preview) followed by textobject
or motion defining region to be aligned.
- In Visual mode select region and type `ga` (or `gA` to show preview).
Strings contained in selected region will be used as input to
|MiniAlign.align_strings()|.
Beware of mode when selecting region: charwise (`v`), linewise (`V`), or
blockwise (`<C-v>`). They all behave differently.
- <Press modifiers>. Press single keys one at a time:
- If pressed key is among table keys of `modifiers` table of
|MiniAlign.config|, its function value is executed. It usually modifies
some options(s) and/or affects some pre-step(s).
- If pressed key is not among defined modifiers, it is treated as plain
split pattern.
This process can either end by itself (usually in case of no preview and
non-default split pattern being set) or you can choose to end it manually.
- <Accept or discard>. In case of active preview, accept current result by
pressing `<CR>`. Discard any result and return to initial regions with
either `<Esc>` or `<C-c>`.
See more in |MiniAlign-modifiers-builtin| and |MiniAlign-examples|.
Notes:
- Visual blockwise selection works best with 'virtualedit' equal to "block"
or "all".
------------------------------------------------------------------------------
*MiniAlign-modifiers-builtin*
Overview of builtin modifiers
All examples assume interactive alignment with preview in linewise mode. With
default mappings, use `V` to select lines and `gA` to initiate alignment. It
might be helpful to copy lines into modifiable buffer and experiment yourself.
Notes:
- Any pressed key which doesn't have defined modifier will be treated as
plain split pattern.
- All modifiers can be customized inside |MiniAlign.setup|. See "Modifiers"
section of |MiniAlign.config|.
Main option modifiers ~
<s> Enter split pattern (confirm prompt by pressing `<CR>`). Input is treated
as plain delimiter.
Before: >
a-b-c
aa-bb-cc
<
After typing `s-<CR>`: >
a -b -c
aa-bb-cc
<
<j> Choose justify side. Prompts user (with helper message) to type single
character identifier of side: `l`eft, `c`enter, `r`ight, `n`one.
Before: >
a_b_c
aa_bb_cc
<
After typing `_jr` (first make split by `_`): >
a_ b_ c
aa_bb_cc
<
<m> Enter merge delimiter (confirm prompt by pressing `<CR>`).
Before: >
a_b_c
aa_bb_cc
<
After typing `_m--<CR>` (first make split by `_`): >
a --_--b --_--c
aa--_--bb--_--cc
<
Modifiers adding pre-steps ~
<f> Enter filter expression. See more details in |MiniAlign.gen_step.filter()|.
Before: >
a_b_c
aa_bb_cc
<
After typing `_fn==1<CR>` (first make split by `_`): >
a _b_c
aa_bb_cc
<
<i> Ignore some split matches. It modifies `split_exclude_patterns` option by
adding commonly wanted patterns. See more details in
|MiniAlign.gen_step.ignore_split()|.
Before: >
/* This_is_assumed_to_be_comment */
a"_"_b
aa_bb
<
After typing `_i` (first make split by `_`): >
/* This_is_assumed_to_be_comment */
a"_"_b
aa _bb
<
<p> Pair neighboring parts.
Before: >
a_b_c
aaa_bbb_ccc
<
After typing `_p` (first make split by `_`): >
a_ b_ c
aaa_bbb_ccc
<
<t> Trim parts from whitespace on both sides (keeping indentation).
Before: >
a _ b _ c
aa _bb _cc
<
After typing `_t` (first make split by `_`): >
a _b _c
aa_bb_cc
<
Delete some last pre-step ~
<BS> Delete one of the pre-steps. If there is only one kind of pre-steps,
remove its latest added one. If not, prompt user to choose pre-step kind
by entering single character: `s`plit, `j`ustify, `m`erge.
Examples:
- `tp<BS>` results in only "trim" step to be left.
- `it<BS>` prompts to choose which step to delete (pre-split or
pre-justify in this case).
Special configurations for common splits ~
<=> Use special pattern to align by a group of consecutive "=". It can be
preceded by any number of punctuation marks and followed by some sommon
punctuation characters. Trim whitespace and merge with single space.
Before: >
a=b
aa<=bb
aaa===bbb
aaaa = cccc
<
After typing `=`: >
a = b
aa <= bb
aaa === bbb
aaaa = cccc
<
<,> Besides splitting by "," character, trim whitespace, pair neighboring
parts and merge with single space.
Before: >
a,b
aa,bb
aaa , bbb
<
After typing `,`: >
a, b
aa, bb
aaa, bbb
<
< > (Space bar) Squash consecutive whitespace into single single space (accept
possible indentation) and split by `%s+` pattern (keeps indentation).
Before: >
a b c
aa bb cc
<
After typing `<Space>`: >
a b c
aa bb cc
------------------------------------------------------------------------------
*MiniAlign-examples*
More complex examples to explore functionality
Copy lines in modifiable buffer, initiate alignment with preview (`gAip`)
and try typing suggested key sequences.
These are modified examples taken from 'junegunn/vim-easy-align'.
Equal sign ~
Lines:
# This=is=assumed=to be a comment
"a ="
a =
a = 1
bbbb = 2
ccccccc = 3
ccccccccccccccc
ddd = 4
eeee === eee = eee = eee=f
fff = ggg += gg &&= gg
g != hhhhhhhh == 888
i := 5
i %= 5
i *= 5
j =~ 5
j >= 5
aa => 123
aa <<= 123
aa >>= 123
bbb => 123
c => 1233123
d => 123
dddddd &&= 123
dddddd ||= 123
dddddd /= 123
gg <=> ee
Key sequences:
- `=`
- `=jc`
- `=jr`
- `=m!<CR>`
- `=p`
- `=i` (execute `:lua vim.o.commentstring = '# %s'` for full experience)
- `=<BS>`
- `=<BS>p`
- `=fn==1<CR>`
- `=<BS>fn==1<CR>t`
- `=frow>7<CR>`
------------------------------------------------------------------------------
*MiniAlign.setup()*
`MiniAlign.setup`({config})
Module setup
Parameters~
{config} `(table|nil)` Module config table. See |MiniAlign.config|.
Usage~
`require('mini.align').setup({})` (replace `{}` with your `config` table)
------------------------------------------------------------------------------
*MiniAlign.config*
`MiniAlign.config`
Module config
Default values:
>
MiniAlign.config = {
-- Module mappings. Use `''` (empty string) to disable one.
mappings = {
start = 'ga',
start_with_preview = 'gA',
},
-- Modifiers changing alignment steps and/or options
modifiers = {
-- Main option modifiers
['s'] = --<function: enter split pattern>,
['j'] = --<function: choose justify side>,
['m'] = --<function: enter merge delimiter>,
-- Modifiers adding pre-steps
['f'] = --<function: filter parts by entering Lua expression>,
['i'] = --<function: ignore some split matches>,
['p'] = --<function: pair parts>,
['t'] = --<function: trim parts>,
-- Delete some last pre-step
['<BS>'] = --<function: delete some last pre-step>,
-- Special configurations for common splits
['='] = --<function: enhanced setup for '='>,
[','] = --<function: enhanced setup for ','>,
[' '] = --<function: enhanced setup for ' '>,
},
-- Default options controlling alignment process
options = {
split_pattern = '',
justify_side = 'left',
merge_delimiter = '',
},
-- Default steps performing alignment (if `nil`, default is used)
steps = {
pre_split = {},
split = nil,
pre_justify = {},
justify = nil,
pre_merge = {},
merge = nil,
},
}
<
# Options ~
## Modifiers ~
`MiniAlign.config.modifiers` is used to define interactive user experience
of managing alignment process. It is a table with single character keys and
modifier function values.
Each modifier function:
- Is called when corresponding modifier key is pressed.
- Has signature `(steps, opts)` and should modify any of its input in place.
Examples:
- Modifier function used for default 'i' modifier:
>
function(steps, _)
table.insert(steps.pre_split, MiniAlign.gen_step.ignore_split())
end
<
- Tweak 't' modifier to use highest indentation instead of keeping it:
>
require('mini.align').setup({
t = function(steps, _)
table.insert(steps.pre_justify, MiniAlign.gen_step.trim('both', 'high'))
end
})
<
- Tweak `j` modifier to cycle through available "justify_side" option
values (like in 'junegunn/vim-easy-align'):
>
require('mini.align').setup({
modifiers = {
j = function(_, opts)
local next_option = ({
left = 'center', center = 'right', right = 'none', none = 'left',
})[opts.justify_side]
opts.justify_side = next_option or 'left'
end,
},
})
<
## Options ~
`MiniAlign.config.options` defines default values of options used to control
behavior of steps.
Examples:
- Set `justify_side = 'center'` to center align at initialization.
For more details about options see |MiniAlign.align_strings()| and entries of
|MiniAlign.gen_step| for default main steps.
## Steps ~
`MiniAlign.config.steps` defines default steps to be applied during
alignment process.
Examples:
- Align by default only first pair of columns:
>
local align = require('mini.align')
align.setup({
steps = {
pre_justify = { align.gen_step.filter('n == 1') }
},
})
------------------------------------------------------------------------------
*MiniAlign.align_strings()*
`MiniAlign.align_strings`({strings}, {opts}, {steps})
Align strings
For details about alignment process see |MiniAlign-algorithm|.
Parameters~
{strings} `(table)` Array of strings.
{opts} `(table|nil)` Options. Its copy will be passed to steps as second
argument. Extended with `MiniAlign.config.options`.
This is a place to control default main steps:
- `opts.split_pattern` - Lua pattern(s) used to make split parts.
- `opts.split_exclude_patterns` - which split matches should be ignored.
- `opts.justify_side` - which direction(s) alignment should be done.
- `opts.justify_offsets` - offsets tweaking width of first column
- `opts.merge_delimiter` - which delimiter(s) to use when merging.
For more information see |MiniAlign.gen_step| entry for corresponding
default step.
{steps} `(table|nil)` Steps. Extended with `MiniAlign.config.steps`.
Possible `nil` values are replaced with corresponding default steps:
- `split` - |MiniAlign.gen_step.default_split()|.
- `justify` - |MiniAlign.gen_step.default_justify()|.
- `merge` - |MiniAlign.gen_step.default_merge()|.
------------------------------------------------------------------------------
*MiniAlign.align_user()*
`MiniAlign.align_user`({mode})
Align current region with user-supplied steps
Mostly designed to be used inside mappings.
Will use |MiniAlign.align_strings()| and set the following options in `opts`:
- `justify_offsets` - array of offsets used to achieve actual alignment of
a region. It is non-trivial (not array of zeros) only for charwise
selection: offset of first string is computed as width of prefix to the
left of region start.
- `region` - current affected region (see |MiniAlign-glossary|). Can be
used to create more advanced steps.
- `mode` - mode of selection (see |MiniAlign-glossary|).
Parameters~
{mode} `(string)` Selection mode. One of "char", "line", "block".
------------------------------------------------------------------------------
*MiniAlign.action_normal()*
`MiniAlign.action_normal`({with_preview})
Perfrom action in Normal mode
Used in Normal mode mapping. No need to use it directly.
Parameters~
{with_preview} `(boolean|nil)` Whether to align with live preview.
------------------------------------------------------------------------------
*MiniAlign.action_visual()*
`MiniAlign.action_visual`({with_preview})
Perfrom action in Visual mode
Used in Visual mode mapping. No need to use it directly.
Parameters~
{with_preview} `(boolean|nil)` Whether to align with live preview.
------------------------------------------------------------------------------
*MiniAlign.as_parts()*
`MiniAlign.as_parts`({arr2d})
Convert 2d array of strings to parts
This function verifies if input is a proper 2d array of strings and adds
methods to its copy.
Class~
{parts}
Fields~
{apply} `(function)` Takes callable `f` and applies it to every part.
Callable should have signature `(s, data)`: `s` is a string part,
`data` - table with its data (<row> has row number, <col> has column number).
Returns new 2d array.
{apply_inplace} `(function)` Takes callable `f` and applies it to every part.
Should have same signature as in `apply` method. Outputs (should all be
strings) are assigned in place to a corresponding parts element. Returns
parts itself to enable method chaining.
{get_dims} `(function)` Return dimensions of parts array: a table with
<row> and <col> keys having number of rows and number of columns (maximum
number of elements across all rows).
{group} `(function)` Concatenate neighboring strings based on supplied
boolean mask and direction (one of "left", default, or "right"). Has
signature `(mask, direction)` and modifies parts in place. Returns parts
itself to enable method chaining.
Example:
- Parts: { { "a", "b", "c" }, { "d", "e" }, { "f" } }
- Mask: { { false, false, true }, { true, false }, { false } }
- Result for direction "left": { { "abc" }, { "d", "e" }, { "f" } }
- Result for direction "right": { { "ab","c" }, { "de" }, { "f" } }
{pair} `(function)` Concatenate neighboring element pairs. Takes
`direction` as input (one of "left", default, or "right") and applies
`group()` for an alternating mask.
Example:
- Parts: { { "a", "b", "c" }, { "d", "e" }, { "f" } }
- Result for direction "left": { { "ab", "c" }, { "de" }, { "f" } }
- Result for direction "right": { { "a", "bc" }, { "de" }, { "f" } }
{slice_col} `(function)` Return column with input index `j`. Note: it might
not be an array if rows have unequal number of columns.
{slice_row} `(function)` Return row with input index `i`.
{trim} `(function)` Trim elements whitespace. Has signature `(direction, indent)`
and modifies parts in place. Returns parts itself to enable method chaining.
- Possible values of `direction`: "both" (default), "left", "right",
"none". Defines from which side whitespaces should be removed.
- Possible values of `indent`: "keep" (default), "low", "high", "remove".
Defines what to do with possible indent (left whitespace of first string
in a row). Value "keep" keeps it; "low" makes all indent equal to the
lowest across rows; "high" - highest across rows; "remove" - removes indent.
Usage~
>
parts = MiniAlign.as_parts({ { 'a', 'b' }, { 'c' } })
print(vim.inspect(parts.get_dims())) -- Should be { row = 2, col = 2 }
parts.apply_inplace(function(s, data)
return ' ' .. data.row .. s .. data.col .. ' '
end)
print(vim.inspect(parts)) -- Should be { { ' 1a1 ', ' 1b2 ' }, { ' 2c1 ' } }
parts.trim('both', 'remove').pair()
print(vim.inspect(parts)) -- Should be { { '1a11b2' }, { '2c1' } }
------------------------------------------------------------------------------
*MiniAlign.new_step()*
`MiniAlign.new_step`({name}, {action})
Create step
A step is basically a named callable object. Having a name bundled with
some action powers helper status message during interactive alignment process.
Parameters~
{name} `(string)` Step name.
{action} `(function|table)` Step action. Should be a callable object
(see |vim.is_callable()|).
Return~
`(table)` A table with keys: <name> with `name` argument, <action> with `action`.
------------------------------------------------------------------------------
*MiniAlign.gen_step*
`MiniAlign.gen_step`
Generate common action steps
This is a table with function elements. Call to actually get step.
Each step action is a function that has signature `(object, opts)`, where
`object` is either parts or array of strings (depends on which stage of
alignment process it is assumed to be applied) and `opts` is table of options.
Outputs of elements named `default_*` are used as default corresponding main
step (split, justify, merge). Behavior of all of them depend on values from
supplied options (second argument).
Outputs of other elements depend on both step generator input values and
options supplied at execution. This design is mostly because their output
can be used several times in pre-steps.
Usage~
>
local align = require('mini.align')
align.setup({
modifiers = {
-- Use 'T' modifier to remove both whitespace and indent
T = function(steps, _)
table.insert(steps.pre_justify, align.gen_step.trim('both', 'remove'))
end,
},
options = {
-- By default align "right", "left", "right", "left", ...
justify_side = { 'right', 'left' },
},
steps = {
-- Align by default only first pair of columns
pre_justify = { align.gen_step.filter('n == 1') },
},
})
------------------------------------------------------------------------------
*MiniAlign.gen_step.default_split()*
`MiniAlign.gen_step.default_split`()
Generate default split step
Output splits strings using matches of Lua pattern(s) from `split_pattern`
option which are not dismissed by `split_exclude_patterns` option.
Outline of how single string is split:
- Convert `split_pattern` option to array of strings (string is converted
as one-element array). This array will be recycled in case there are more
split matches than in converted `split_pattern` array (which almost always).
- Find all forbidden spans (intervals inside string) - all matches of all
patterns in `split_exclude_patterns`.
- Find match for the next pattern. If it is not inside any forbidden span,
add preceding unmatched substring and matched split as two parts. Repeat
with the next pattern.
- If no pattern match is found, add the rest of string as final part.
Output uses following options (as part second argument, `opts` table):
- <split_pattern> - string or array of strings used to detect split matches
and create parts. Default: `''` meaning no matches (whole string is used
as part). Examples: `'%s+'`, `{ '<', '>' }`.
- <split_exclude_patterns> - array of strings defining which regions to
exclude from being matched. Default: `{}`. Examples: `{ '".-"', '^%s*#.*' }`.
Return~
`(table)` A step named "split" and with appropriate callable action.
See also~
|MiniAlign.gen_step.ignore_split()| heavily uses `split_exclude_patterns`.
------------------------------------------------------------------------------
*MiniAlign.gen_step.default_justify()*
`MiniAlign.gen_step.default_justify`()
Generate default justify step
Output makes column elements of string parts have equal width by adding
left and/or right whitespace padding. Which side(s) to pad is defined by
`justify_side` option. Width of first column can be tweaked with `justify_offsets`
option.
Outline of how parts are justified:
- Convert `justify_side` option to array of strings (single string is
converted as one-element array). Recycle this array to have length equal
to number of columns in parts.
- For all columns compute maximum width of strings from it (add offsets from
`justify_offsets` to first column widths). Note: for left alignment, width
of last row element does not affect column width. This is mainly because
it won't be padded and helps dealing with "no single match" lines.
- Make all elements have same width inside column by adding appropriate
amount of whitespace. Which side(s) to add is controlled by the corresponding
`justify_side` array element. Note: padding is done with spaces which
might conflict with tab indentation.
Output uses following options (as part second argument, `opts` table):
- <justify_side> - string or array of strings. Each element can be one of
"left" (pad right side), "center" (pad both sides equally), "right" (pad
left side), "none" (no padding). Default: "left".
- <justify_offsets> - array of numeric left offsets of rows. Used to adjust
for possible not equal indents, like in case of charwise selection when
left edge is not on the first column. Default: array of zeros. Set
automatically during interactive alignment in charwise mode.
Return~
`(table)` A step named "justify" and with appropriate callable action.
------------------------------------------------------------------------------
*MiniAlign.gen_step.default_merge()*
`MiniAlign.gen_step.default_merge`()
Generate default merge step
Output merges rows of parts into strings by placing merge delimiter(s)
between them.
Outline of how parts are converted to array of strings:
- Convert `merge_delimiter` option to array of strings (single string is
converted as one-element array). Recycle this array to have length equal
to number of columns in parts minus 1.
- Exclude empty strings from parts. They add nothing to output except extra
usage of merge delimiter.
- Concatenate each row interleaving with array of merge delimiters.
Output uses following options (as part second argument, `opts` table):
- <merge_delimiter> - string or array of strings. Default: `''`.
Examples: `' '`, `{ '', ' ' }`.
Return~
`(table)` A step named "merge" and with appropriate callable action.
------------------------------------------------------------------------------
*MiniAlign.gen_step.filter()*
`MiniAlign.gen_step.filter`({expr})
Generate filter step
Construct function predicate from supplied Lua string expression and make
step evaluating it on every part element.
Outline of how filtering is done:
- Convert Lua filtering expression into function predicate which can be
evaluated in manually created context (some specific variables being set).
- Compute boolean mask for parts by applying predicate to each element of
2d array with special variables set to specific values (see next section).
- Group parts with compted mask. See `group()` method of parts in
|MiniAlign.as_parts()|.
Special variables which can be used in expression:
- <row> - row number of current element.
- <ROW> - total number of rows in parts.
- <col> - column number of current element.
- <COL> - total number of columns in current row.
- <s> - string value of current element.
- <n> - column pair number of current element. Useful when filtering by
result of pattern splitting.
- <N> - total number of column pairs in current row.
- All variables from global table `_G`.
Tips:
- This general filtering approach can be used to both include and exclude
certain parts from alignment. Examples:
- Use `row ~= 2` to align all parts except from second row.
- Use `n == 1` to align only by first pair of columns.
- Filtering by last equal sign usually can be done with `n >= (N - 1)`
(because there is usually something to the right of it).
Parameters~
{expr} `(string)` Lua expression as a string which will be used as predicate.
Return~
`(table)` A step named "filter" and with appropriate callable action.
------------------------------------------------------------------------------
*MiniAlign.gen_step.ignore_split()*
`MiniAlign.gen_step.ignore_split`({patterns}, {exclude_comment})
Generate ignore step
Output adds certain values to `split_exclude_patterns` option. Should be
used as pre-split step.
Parameters~
{patterns} `(table)` Array of patterns to be added to
`split_exclude_patterns` as is. Default: `{ [[".-"]] }` (excludes strings
for most cases).
{exclude_comment} `(boolean)` Whether to add comment pattern to
`split_exclude_patterns`. Comment pattern is derived from 'commentstring'
option. Default: `true`.
Return~
`(table)` A step named "ignore" and with appropriate callable action.
See also~
|MiniAlign.gen_step.default_split()| for details about
`split_exclude_patterns` option.
------------------------------------------------------------------------------
*MiniAlign.gen_step.pair()*
`MiniAlign.gen_step.pair`({direction})
Generate pair step
Output calls `pair()` method of parts (see |MiniAlign.as_parts()|) with
supplied `direction` argument.
Parameters~
{direction} `(string)` Which direction to pair. One of "left" (default) or
Return~
`(table)` A step named "pair" and with appropriate callable action.
------------------------------------------------------------------------------
*MiniAlign.gen_step.trim()*
`MiniAlign.gen_step.trim`({direction}, {indent})
Generate trim step
Output calls `trim()` method of parts (see |MiniAlign.as_parts()|) with
supplied `direction` and `indent` arguments.
Parameters~
{direction} `(string)` Which sides to trim whitespace. One of "both"
(default), "left", "right", "none".
{indent} `(string)` What to do with possible indent (left whitespace of first
string in a row). One of "keep" (default), "low", "high", "remove".
Return~
`(table)` A step named "trim" and with appropriate callable action.
vim:tw=78:ts=8:noet:ft=help:norl:

View File

@ -0,0 +1,268 @@
==============================================================================
------------------------------------------------------------------------------
*mini.base16*
*MiniBase16*
Fast implementation of 'chriskempson/base16' color scheme (with Copyright
(C) 2012 Chris Kempson) adapted for modern Neovim Lua plugins.
Extra features:
- Configurable automatic support of cterm colors (see |highlight-cterm|).
- Opinionated palette generator based only on background and foreground
colors.
Supported highlight groups:
- Builtin-in Neovim LSP and diagnostic.
- Plugins (either with explicit definition or by verification that default
highlighting works appropriately):
- 'echasnovski/mini.nvim'
- 'akinsho/bufferline.nvim'
- 'anuvyklack/hydra.nvim'
- 'DanilaMihailov/beacon.nvim'
- 'folke/todo-comments.nvim'
- 'folke/trouble.nvim'
- 'folke/which-key.nvim'
- 'ggandor/leap.nvim'
- 'ggandor/lightspeed.nvim'
- 'glepnir/dashboard-nvim'
- 'glepnir/lspsaga.nvim'
- 'hrsh7th/nvim-cmp'
- 'justinmk/vim-sneak'
- 'kyazdani42/nvim-tree.lua'
- 'lewis6991/gitsigns.nvim'
- 'lukas-reineke/indent-blankline.nvim'
- 'neoclide/coc.nvim'
- 'nvim-lualine/lualine.nvim'
- 'nvim-neo-tree/neo-tree.nvim'
- 'nvim-telescope/telescope.nvim'
- 'p00f/nvim-ts-rainbow'
- 'phaazon/hop.nvim'
- 'rcarriga/nvim-dap-ui'
- 'rcarriga/nvim-notify'
- 'rlane/pounce.nvim'
- 'romgrk/barbar.nvim'
- 'simrat39/symbols-outline.nvim'
- 'stevearc/aerial.nvim'
- 'TimUntersberger/neogit'
- 'williamboman/mason.nvim'
# Setup~
This module needs a setup with `require('mini.base16').setup({})` (replace
`{}` with your `config` table). It will create global Lua table
`MiniBase16` which you can use for scripting or manually (with
`:lua MiniBase16.*`).
See |MiniBase16.config| for `config` structure and default values.
This module doesn't have runtime options, so using `vim.b.minibase16_config`
will have no effect here.
Example:
>
require('mini.base16').setup({
palette = {
base00 = '#112641',
base01 = '#3a475e',
base02 = '#606b81',
base03 = '#8691a7',
base04 = '#d5dc81',
base05 = '#e2e98f',
base06 = '#eff69c',
base07 = '#fcffaa',
base08 = '#ffcfa0',
base09 = '#cc7e46',
base0A = '#46a436',
base0B = '#9ff895',
base0C = '#ca6ecf',
base0D = '#42f7ff',
base0E = '#ffc4ff',
base0F = '#00a5c5',
},
use_cterm = true,
plugins = {
default = false,
['echasnovski/mini.nvim'] = true,
},
})
<
# Notes~
1. This is used to create plugin's colorschemes (see |mini.nvim-color-schemes|).
2. Using `setup()` doesn't actually create a |colorscheme|. It basically
creates a coordinated set of |highlight|s. To create your own theme:
- Put "myscheme.lua" file (name after your chosen theme name) inside
any "colors" directory reachable from 'runtimepath' ("colors" inside
your Neovim config directory is usually enough).
- Inside "myscheme.lua" call `require('mini.base16').setup()` with your
palette and only after that set |g:colors_name| to "myscheme".
------------------------------------------------------------------------------
*mini-color-schemes*
# Plugin colorschemes~
This plugin comes with several color schemes. All of them are a
|MiniBase16| theme created with faster version of the following Lua code:
>
require('mini.base16').setup({ palette = palette, use_cterm = true })
<
Activate them as regular |colorscheme| (for example, `:colorscheme minischeme`).
## minischeme~
Blue and yellow main colors with high contrast and saturation palette.
Palettes are:
- For dark 'background':
`MiniBase16.mini_palette('#112641', '#e2e98f', 75)`
- For light 'background':
`MiniBase16.mini_palette('#e2e5ca', '#002a83', 75)`
## minicyan~
Cyan and grey main colors with moderate contrast and saturation palette.
Palettes are:
- For dark 'background':
`MiniBase16.mini_palette('#0A2A2A', '#D0D0D0', 50)`
- For light 'background':
`MiniBase16.mini_palette('#C0D2D2', '#262626', 80)`
------------------------------------------------------------------------------
*MiniBase16.setup()*
`MiniBase16.setup`({config})
Module setup
Setup is done by applying base16 palette to enable colorscheme. Highlight
groups make an extended set from original
[base16-vim](https://github.com/chriskempson/base16-vim/) plugin. It is a
good idea to have `config.palette` respect the original [styling
principles](https://github.com/chriskempson/base16/blob/master/styling.md).
By default only 'gui highlighting' (see |highlight-gui| and
|termguicolors|) is supported. To support 'cterm highlighting' (see
|highlight-cterm|) supply `config.use_cterm` argument in one of the formats:
- `true` to auto-generate from `palette` (as closest colors).
- Table with similar structure to `palette` but having terminal colors
(integers from 0 to 255) instead of hex strings.
Parameters~
{config} `(table)` Module config table. See |MiniBase16.config|.
Usage~
`require('mini.base16').setup({})` (replace `{}` with your `config`
table; `config.palette` should be a table with colors)
------------------------------------------------------------------------------
*MiniBase16.config*
`MiniBase16.config`
Module config
Default values:
>
MiniBase16.config = {
-- Table with names from `base00` to `base0F` and values being strings of
-- HEX colors with format "#RRGGBB". NOTE: this should be explicitly
-- supplied in `setup()`.
palette = nil,
-- Whether to support cterm colors. Can be boolean, `nil` (same as
-- `false`), or table with cterm colors. See `setup()` documentation for
-- more information.
use_cterm = nil,
-- Plugin integrations. Use `default = false` to disable all integrations.
-- Also can be set per plugin (see |MiniBase16.config|).
plugins = { default = true },
}
<
# Options ~
## Plugin integrations ~
`config.plugins` defines for which supported plugins highlight groups will
be created. Limiting number of integrations slightly decreases startup time.
It is a table with boolean (`true`/`false`) values which are applied as follows:
- If plugin name (as listed in |mini.base16|) has entry, it is used.
- Otherwise `config.plugins.default` is used.
Example which will load only "mini.nvim" integration:
>
require('mini.base16').setup({
palette = require('mini.base16').mini_palette('#112641', '#e2e98f', 75),
plugins = {
default = false,
['echasnovski/mini.nvim'] = true,
}
})
------------------------------------------------------------------------------
*MiniBase16.mini_palette()*
`MiniBase16.mini_palette`({background}, {foreground}, {accent_chroma})
Create 'mini' palette
Create base16 palette based on the HEX (string '#RRGGBB') colors of main
background and foreground with optional setting of accent chroma (see
details).
# Algorithm design~
- Main operating color space is
[CIELCh(uv)](https://en.wikipedia.org/wiki/CIELUV#Cylindrical_representation_(CIELCh))
which is a cylindrical representation of a perceptually uniform CIELUV
color space. It defines color by three values: lightness L (values from 0
to 100), chroma (positive values), and hue (circular values from 0 to 360
degress). Useful converting tool: https://www.easyrgb.com/en/convert.php
- There are four important lightness values: background, foreground, focus
(around the middle of background and foreground, leaning towards
foreground), and edge (extreme lightness closest to foreground).
- First four colors have the same chroma and hue as `background` but
lightness progresses from background towards focus.
- Second four colors have the same chroma and hue as `foreground` but
lightness progresses from foreground towards edge in such a way that
'base05' color is main foreground color.
- The rest eight colors are accent colors which are created in pairs
- Each pair has same hue from set of hues 'most different' to
background and foreground hues (if respective chorma is positive).
- All colors have the same chroma equal to `accent_chroma` (if not
provided, chroma of foreground is used, as they will appear next
to each other). Note: this means that in case of low foreground
chroma, it is a good idea to set `accent_chroma` manually.
Values from 30 (low chorma) to 80 (high chroma) are common.
- Within pair there is base lightness (equal to foreground
lightness) and alternative (equal to focus lightness). Base
lightness goes to colors which will be used more frequently in
code: base08 (variables), base0B (strings), base0D (functions),
base0E (keywords).
How exactly accent colors are mapped to base16 palette is a result of
trial and error. One rule of thumb was: colors within one hue pair should
be more often seen next to each other. This is because it is easier to
distinguish them and seems to be more visually appealing. That is why
`base0D` and `base0F` have same hues because they usually represent
functions and delimiter (brackets included).
Parameters~
{background} `(string)` Background HEX color (formatted as `#RRGGBB`).
{foreground} `(string)` Foreground HEX color (formatted as `#RRGGBB`).
{accent_chroma} `(number)` Optional positive number (usually between 0
and 100). Default: chroma of foreground color.
Return~
`(table)` Table with base16 palette.
Usage~
`local palette = require('mini.base16').mini_palette('#112641', '#e2e98f', 75)`
`require('mini.base16').setup({palette = palette})`
------------------------------------------------------------------------------
*MiniBase16.rgb_palette_to_cterm_palette()*
`MiniBase16.rgb_palette_to_cterm_palette`({palette})
Converts palette with RGB colors to terminal colors
Useful for caching `use_cterm` variable to increase speed.
Parameters~
{palette} `(table)` Table with base16 palette (same as in
`MiniBase16.config.palette`).
Return~
`(table)` Table with base16 palette using |highlight-cterm|.
vim:tw=78:ts=8:noet:ft=help:norl:

View File

@ -0,0 +1,114 @@
==============================================================================
------------------------------------------------------------------------------
*mini.bufremove*
*MiniBufremove*
Buffer removing (unshow, delete, wipeout), which saves window layout
(opposite to builtin Neovim's commands).
# Setup~
This module doesn't need setup, but it can be done to improve usability.
Setup with `require('mini.bufremove').setup({})` (replace `{}` with your
`config` table). It will create global Lua table `MiniBufremove` which you
can use for scripting or manually (with `:lua MiniBufremove.*`).
See |MiniBufremove.config| for `config` structure and default values.
This module doesn't have runtime options, so using `vim.b.minibufremove_config`
will have no effect here.
# Notes~
1. Which buffer to show in window(s) after its current buffer is removed is
decided by the algorithm:
- If alternate buffer (see |CTRL-^|) is listed (see |buflisted()|), use it.
- If previous listed buffer (see |bprevious|) is different, use it.
- Otherwise create a scratch one with `nvim_create_buf(true, true)` and use
it.
# Disabling~
To disable core functionality, set `g:minibufremove_disable` (globally) or
`b:minibufremove_disable` (for a buffer) to `v:true`. Considering high
number of different scenarios and customization intentions, writing exact
rules for disabling module's functionality is left to user. See
|mini.nvim-disabling-recipes| for common recipes.
------------------------------------------------------------------------------
*MiniBufremove.setup()*
`MiniBufremove.setup`({config})
Module setup
Parameters~
{config} `(table)` Module config table. See |MiniBufremove.config|.
Usage~
`require('mini.bufremove').setup({})` (replace `{}` with your `config` table)
------------------------------------------------------------------------------
*MiniBufremove.config*
`MiniBufremove.config`
Module config
Default values:
>
MiniBufremove.config = {
-- Whether to set Vim's settings for buffers (allow hidden buffers)
set_vim_settings = true,
}
<
------------------------------------------------------------------------------
*MiniBufremove.delete()*
`MiniBufremove.delete`({buf_id}, {force})
Delete buffer `buf_id` with |:bdelete| after unshowing it
Parameters~
{buf_id} `(number)` Buffer identifier (see |bufnr()|) to use. Default:
0 for current.
{force} `(boolean)` Whether to ignore unsaved changes (using `!` version of
command). Default: `false`.
Return~
`(boolean)` Whether operation was successful.
------------------------------------------------------------------------------
*MiniBufremove.wipeout()*
`MiniBufremove.wipeout`({buf_id}, {force})
Wipeout buffer `buf_id` with |:bwipeout| after unshowing it
Parameters~
{buf_id} `(number)` Buffer identifier (see |bufnr()|) to use. Default:
0 for current.
{force} `(boolean)` Whether to ignore unsaved changes (using `!` version of
command). Default: `false`.
Return~
`(boolean)` Whether operation was successful.
------------------------------------------------------------------------------
*MiniBufremove.unshow()*
`MiniBufremove.unshow`({buf_id})
Stop showing buffer `buf_id` in all windows
Parameters~
{buf_id} `(number)` Buffer identifier (see |bufnr()|) to use. Default:
0 for current.
Return~
`(boolean)` Whether operation was successful.
------------------------------------------------------------------------------
*MiniBufremove.unshow_in_window()*
`MiniBufremove.unshow_in_window`({win_id})
Stop showing current buffer of window `win_id`
Parameters~
{win_id} `(number)` Window identifier (see |win_getid()|) to use.
Default: 0 for current.
Return~
`(boolean)` Whether operation was successful.
vim:tw=78:ts=8:noet:ft=help:norl:

View File

@ -0,0 +1,137 @@
==============================================================================
------------------------------------------------------------------------------
*mini.comment*
*MiniComment*
Fast and familiar per-line commenting. Commenting in Normal mode respects
|count| and is dot-repeatable. Comment structure is inferred from
'commentstring'. Handles both tab and space indenting (but not when they
are mixed). Allows custom hooks before and after successful commenting.
What it doesn't do:
- Block and sub-line comments. This will only support per-line commenting.
- Configurable (from module) comment structure. Modify |commentstring|
instead. To enhance support for commenting in multi-language files, see
"JoosepAlviste/nvim-ts-context-commentstring" plugin along with `hooks`
option of this module (see |MiniComment.config|).
- Handle indentation with mixed tab and space.
- Preserve trailing whitespace in empty lines.
# Setup~
This module needs a setup with `require('mini.comment').setup({})` (replace
`{}` with your `config` table). It will create global Lua table
`MiniComment` which you can use for scripting or manually (with
`:lua MiniComment.*`).
See |MiniComment.config| for `config` structure and default values.
You can override runtime config settings locally to buffer inside
`vim.b.minicomment_config` which should have same structure as
`MiniComment.config`. See |mini.nvim-buffer-local-config| for more details.
# Disabling~
To disable core functionality, set `g:minicomment_disable` (globally) or
`b:minicomment_disable` (for a buffer) to `v:true`. Considering high number
of different scenarios and customization intentions, writing exact rules
for disabling module's functionality is left to user. See
|mini.nvim-disabling-recipes| for common recipes.
------------------------------------------------------------------------------
*MiniComment.setup()*
`MiniComment.setup`({config})
Module setup
Parameters~
{config} `(table)` Module config table. See |MiniComment.config|.
Usage~
`require('mini.comment').setup({})` (replace `{}` with your `config` table)
------------------------------------------------------------------------------
*MiniComment.config*
`MiniComment.config`
Module config
Default values:
>
MiniComment.config = {
-- Module mappings. Use `''` (empty string) to disable one.
mappings = {
-- Toggle comment (like `gcip` - comment inner paragraph) for both
-- Normal and Visual modes
comment = 'gc',
-- Toggle comment on current line
comment_line = 'gcc',
-- Define 'comment' textobject (like `dgc` - delete whole comment block)
textobject = 'gc',
},
-- Hook functions to be executed at certain stage of commenting
hooks = {
-- Before successful commenting. Does nothing by default.
pre = function() end,
-- After successful commenting. Does nothing by default.
post = function() end,
},
}
<
------------------------------------------------------------------------------
*MiniComment.operator()*
`MiniComment.operator`({mode})
Main function to be mapped
It is meant to be used in expression mappings (see |map-<expr>|) to enable
dot-repeatability and commenting on range. There is no need to do this
manually, everything is done inside |MiniComment.setup()|.
It has a somewhat unintuitive logic (because of how expression mapping with
dot-repeatability works): it should be called without arguments inside
expression mapping and with argument when action should be performed.
Parameters~
{mode} `(string)` Optional string with 'operatorfunc' mode (see |g@|).
Return~
`(string)` 'g@' if called without argument, '' otherwise (but after
performing action).
------------------------------------------------------------------------------
*MiniComment.toggle_lines()*
`MiniComment.toggle_lines`({line_start}, {line_end})
Toggle comments between two line numbers
It uncomments if lines are comment (every line is a comment) and comments
otherwise. It respects indentation and doesn't insert trailing
whitespace. Toggle commenting not in visual mode is also dot-repeatable
and respects |count|.
Before successful commenting it executes `config.hooks.pre`.
After successful commenting it executes `config.hooks.post`.
If hook returns `false`, any further action is terminated.
# Notes~
1. Currently call to this function will remove marks inside written range.
Use |lockmarks| to preserve marks.
Parameters~
{line_start} `(number)` Start line number (inclusive from 1 to number of lines).
{line_end} `(number)` End line number (inclusive from 1 to number of lines).
------------------------------------------------------------------------------
*MiniComment.textobject()*
`MiniComment.textobject`()
Comment textobject
This selects all commented lines adjacent to cursor line (if it itself is
commented). Designed to be used with operator mode mappings (see |mapmode-o|).
Before successful textobject usage it executes `config.hooks.pre`.
After successful textobject usage it executes `config.hooks.post`.
If hook returns `false`, any further action is terminated.
vim:tw=78:ts=8:noet:ft=help:norl:

View File

@ -0,0 +1,282 @@
==============================================================================
------------------------------------------------------------------------------
*mini.completion*
*MiniCompletion*
Autocompletion and signature help plugin. Key design ideas:
- Have an async (with customizable "debounce" delay) "two-stage chain
completion": first try to get completion items from LSP client (if set
up) and if no result, fallback to custom action.
- Managing completion is done as much with Neovim's built-in tools as
possible.
Features:
- Two-stage chain completion:
- First stage is an LSP completion implemented via
|MiniCompletion.completefunc_lsp()|. It should be set up as either
|completefunc| or |omnifunc|. It tries to get completion items from
LSP client (via 'textDocument/completion' request). Custom
preprocessing of response items is possible (with
`MiniCompletion.config.lsp_completion.process_items`), for example
with fuzzy matching. By default items which are not snippets and
directly start with completed word are kept and sorted according to
LSP specification. Supports `additionalTextEdits`, like auto-import
and others (see 'Notes').
- If first stage is not set up or resulted into no candidates, fallback
action is executed. The most tested actions are Neovim's built-in
insert completion (see |ins-completion|).
- Automatic display in floating window of completion item info (via
'completionItem/resolve' request) and signature help (with highlighting
of active parameter if LSP server provides such information). After
opening, window for signature help is fixed and is closed when there is
nothing to show, text is different or
when leaving Insert mode.
- Automatic actions are done after some configurable amount of delay. This
reduces computational load and allows fast typing (completion and
signature help) and item selection (item info)
- User can force two-stage completion via
|MiniCompletion.complete_twostage()| (by default is mapped to
`<C-Space>`) or fallback completion via
|MiniCompletion.complete_fallback()| (maped to `<M-Space>`).
What it doesn't do:
- Snippet expansion.
- Many configurable sources.
- Automatic mapping of `<CR>`, `<Tab>`, etc., as those tend to have highly
variable user expectations. See 'Helpful key mappings' for suggestions.
# Setup~
This module needs a setup with `require('mini.completion').setup({})`
(replace `{}` with your `config` table). It will create global Lua table
`MiniCompletion` which you can use for scripting or manually (with
`:lua MiniCompletion.*`).
See |MiniCompletion.config| for `config` structure and default values.
You can override runtime config settings locally to buffer inside
`vim.b.minicompletion_config` which should have same structure as
`MiniCompletion.config`. See |mini.nvim-buffer-local-config| for more details.
# Notes~
- More appropriate, albeit slightly advanced, LSP completion setup is to set
it not on every `BufEnter` event (default), but on every attach of LSP
client. To do that:
- Use in initial config:
`lsp_completion = {source_func = 'omnifunc', auto_setup = false}`.
- In `on_attach()` of every LSP client set 'omnifunc' option to exactly
`v:lua.MiniCompletion.completefunc_lsp`.
- If you have trouble using custom (overriden) |vim.ui.input| (like from
'stevearc/dressing.nvim'), make automated disable of 'mini.completion'
for input buffer. For example, currently for 'dressing.nvim' it can be
with `au FileType DressingInput lua vim.b.minicompletion_disable = true`.
- Support of `additionalTextEdits` tries to handle both types of servers:
- When `additionalTextEdits` are supplied in response to
'textDocument/completion' request (like currently in 'pyright').
- When `additionalTextEdits` are supplied in response to
'completionItem/resolve' request (like currently in
'typescript-language-server'). In this case to apply edits user needs
to trigger such request, i.e. select completion item and wait for
`MiniCompletion.config.delay.info` time plus server response time.
# Comparisons~
- 'nvim-cmp':
- More complex design which allows multiple sources each in form of
separate plugin. `MiniCompletion` has two built in: LSP and fallback.
- Supports snippet expansion.
- Doesn't have customizable delays for basic actions.
- Doesn't allow fallback action.
- Doesn't provide signature help.
# Helpful key mappings~
To use `<Tab>` and `<S-Tab>` for navigation through completion list, make
these key mappings:
`vim.api.nvim_set_keymap('i', '<Tab>', [[pumvisible() ? "\<C-n>" : "\<Tab>"]], { noremap = true, expr = true })`
`vim.api.nvim_set_keymap('i', '<S-Tab>', [[pumvisible() ? "\<C-p>" : "\<S-Tab>"]], { noremap = true, expr = true })`
To get more consistent behavior of `<CR>`, you can use this template in
your 'init.lua' to make customized mapping: >
local keys = {
['cr'] = vim.api.nvim_replace_termcodes('<CR>', true, true, true),
['ctrl-y'] = vim.api.nvim_replace_termcodes('<C-y>', true, true, true),
['ctrl-y_cr'] = vim.api.nvim_replace_termcodes('<C-y><CR>', true, true, true),
}
_G.cr_action = function()
if vim.fn.pumvisible() ~= 0 then
-- If popup is visible, confirm selected item or add new line otherwise
local item_selected = vim.fn.complete_info()['selected'] ~= -1
return item_selected and keys['ctrl-y'] or keys['ctrl-y_cr']
else
-- If popup is not visible, use plain `<CR>`. You might want to customize
-- according to other plugins. For example, to use 'mini.pairs', replace
-- next line with `return require('mini.pairs').cr()`
return keys['cr']
end
end
vim.api.nvim_set_keymap('i', '<CR>', 'v:lua._G.cr_action()', { noremap = true, expr = true })
<
# Highlight groups~
* `MiniCompletionActiveParameter` - highlighting of signature active parameter.
By default displayed as plain underline.
To change any highlight group, modify it directly with |:highlight|.
# Disabling~
To disable, set `g:minicompletion_disable` (globally) or
`b:minicompletion_disable` (for a buffer) to `v:true`. Considering high
number of different scenarios and customization intentions, writing exact
rules for disabling module's functionality is left to user. See
|mini.nvim-disabling-recipes| for common recipes.
------------------------------------------------------------------------------
*MiniCompletion.setup()*
`MiniCompletion.setup`({config})
Module setup
Parameters~
{config} `(table)` Module config table. See |MiniCompletion.config|.
Usage~
`require('mini.completion').setup({})` (replace `{}` with your `config` table)
------------------------------------------------------------------------------
*MiniCompletion.config*
`MiniCompletion.config`
Module config
Default values:
>
MiniCompletion.config = {
-- Delay (debounce type, in ms) between certain Neovim event and action.
-- This can be used to (virtually) disable certain automatic actions by
-- setting very high delay time (like 10^7).
delay = { completion = 100, info = 100, signature = 50 },
-- Maximum dimensions of floating windows for certain actions. Action
-- entry should be a table with 'height' and 'width' fields.
window_dimensions = {
info = { height = 25, width = 80 },
signature = { height = 25, width = 80 },
},
-- Way of how module does LSP completion
lsp_completion = {
-- `source_func` should be one of 'completefunc' or 'omnifunc'.
source_func = 'completefunc',
-- `auto_setup` should be boolean indicating if LSP completion is set up
-- on every `BufEnter` event.
auto_setup = true,
-- `process_items` should be a function which takes LSP
-- 'textDocument/completion' response items and word to complete. Its
-- output should be a table of the same nature as input items. The most
-- common use-cases are custom filtering and sorting. You can use
-- default `process_items` as `MiniCompletion.default_process_items()`.
process_items = --<function: filters out snippets; sorts by LSP specs>,
},
-- Fallback action. It will always be run in Insert mode. To use Neovim's
-- built-in completion (see `:h ins-completion`), supply its mapping as
-- string. Example: to use 'whole lines' completion, supply '<C-x><C-l>'.
fallback_action = --<function: like `<C-n>` completion>,
-- Module mappings. Use `''` (empty string) to disable one. Some of them
-- might conflict with system mappings.
mappings = {
force_twostep = '<C-Space>', -- Force two-step completion
force_fallback = '<A-Space>', -- Force fallback completion
},
-- Whether to set Vim's settings for better experience (modifies
-- `shortmess` and `completeopt`)
set_vim_settings = true,
}
<
------------------------------------------------------------------------------
*MiniCompletion.auto_completion()*
`MiniCompletion.auto_completion`()
Auto completion
Designed to be used with |autocmd|. No need to use it directly, everything
is setup in |MiniCompletion.setup|.
------------------------------------------------------------------------------
*MiniCompletion.complete_twostage()*
`MiniCompletion.complete_twostage`({fallback}, {force})
Run two-stage completion
Parameters~
{fallback} `(boolean)` Whether to use fallback completion.
{force} `(boolean)` Whether to force update of completion popup.
------------------------------------------------------------------------------
*MiniCompletion.complete_fallback()*
`MiniCompletion.complete_fallback`()
Run fallback completion
------------------------------------------------------------------------------
*MiniCompletion.auto_info()*
`MiniCompletion.auto_info`()
Auto completion entry information
Designed to be used with |autocmd|. No need to use it directly, everything
is setup in |MiniCompletion.setup|.
------------------------------------------------------------------------------
*MiniCompletion.auto_signature()*
`MiniCompletion.auto_signature`()
Auto function signature
Designed to be used with |autocmd|. No need to use it directly, everything
is setup in |MiniCompletion.setup|.
------------------------------------------------------------------------------
*MiniCompletion.stop()*
`MiniCompletion.stop`({actions})
Stop actions
This stops currently active (because of module delay or LSP answer delay)
actions.
Designed to be used with |autocmd|. No need to use it directly, everything
is setup in |MiniCompletion.setup|.
Parameters~
{actions} `(table)` Array containing any of 'completion', 'info', or
'signature' string.
------------------------------------------------------------------------------
*MiniCompletion.on_text_changed_i()*
`MiniCompletion.on_text_changed_i`()
Act on every |TextChangedI|
------------------------------------------------------------------------------
*MiniCompletion.on_text_changed_p()*
`MiniCompletion.on_text_changed_p`()
Act on every |TextChangedP|
------------------------------------------------------------------------------
*MiniCompletion.completefunc_lsp()*
`MiniCompletion.completefunc_lsp`({findstart}, {base})
Module's |complete-function|
This is the main function which enables two-stage completion. It should be
set as one of |completefunc| or |omnifunc|.
No need to use it directly, everything is setup in |MiniCompletion.setup|.
------------------------------------------------------------------------------
*MiniCompletion.default_process_items()*
`MiniCompletion.default_process_items`({items}, {base})
Default `MiniCompletion.config.lsp_completion.process_items`
vim:tw=78:ts=8:noet:ft=help:norl:

View File

@ -0,0 +1,109 @@
==============================================================================
------------------------------------------------------------------------------
*mini.cursorword*
*MiniCursorword*
Autohighlight word under cursor with customizable delay. Current word under
cursor can be highlighted differently. Highlighting is triggered only if
current cursor character is a |[:keyword:]|. "Word under cursor" is meant
as in Vim's |<cword>|: something user would get as 'iw' text object.
Highlighting stops in insert and terminal modes.
# Setup~
This module needs a setup with `require('mini.cursorword').setup({})`
(replace `{}` with your `config` table). It will create global Lua table
`MiniCursorword` which you can use for scripting or manually (with
`:lua MiniCursorword.*`).
See |MiniCursorword.config| for `config` structure and default values.
You can override runtime config settings locally to buffer inside
`vim.b.minicursorword_config` which should have same structure as
`MiniCursorword.config`. See |mini.nvim-buffer-local-config| for more details.
# Highlight groups~
* `MiniCursorword` - highlight group of cursor word. Default: plain underline.
* `MiniCursorwordCurrent` - highlight group of a current word under
cursor. It will be displayed on top of `MiniCursorword`
(so `:hi clear MiniCursorwordCurrent` will lead to showing
`MiniCursorword` highlight group). Note: To not highlight it, use
`:hi! MiniCursorwordCurrent gui=nocombine guifg=NONE guibg=NONE` .
To change any highlight group, modify it directly with |:highlight|.
# Disabling~
To disable core functionality, set `g:minicursorword_disable` (globally) or
`b:minicursorword_disable` (for a buffer) to `v:true`. Considering high
number of different scenarios and customization intentions, writing exact
rules for disabling module's functionality is left to user. See
|mini.nvim-disabling-recipes| for common recipes. Note: after disabling
there might be highlighting left; it will be removed after next
highlighting update.
Module-specific disabling:
- Don't show highlighting if cursor is on the word that is in a blocklist
of current filetype. In this example, blocklist for "lua" is "local" and
"require" words, for "javascript" - "import":
>
_G.cursorword_blocklist = function()
local curword = vim.fn.expand('<cword>')
local filetype = vim.api.nvim_buf_get_option(0, 'filetype')
-- Add any disabling global or filetype-specific logic here
local blocklist = {}
if filetype == 'lua' then
blocklist = { 'local', 'require' }
elseif filetype == 'javascript' then
blocklist = { 'import' }
end
vim.b.minicursorword_disable = vim.tbl_contains(blocklist, curword)
end
-- Make sure to add this autocommand *before* calling module's `setup()`.
vim.cmd('au CursorMoved * lua _G.cursorword_blocklist()')
------------------------------------------------------------------------------
*MiniCursorword.setup()*
`MiniCursorword.setup`({config})
Module setup
Parameters~
{config} `(table)` Module config table. See |MiniCursorword.config|.
Usage~
`require('mini.cursorword').setup({})` (replace `{}` with your `config` table)
------------------------------------------------------------------------------
*MiniCursorword.config*
`MiniCursorword.config`
Module config
Default values:
>
MiniCursorword.config = {
-- Delay (in ms) between when cursor moved and when highlighting appeared
delay = 100,
}
<
------------------------------------------------------------------------------
*MiniCursorword.auto_highlight()*
`MiniCursorword.auto_highlight`()
Auto highlight word under cursor
Designed to be used with |autocmd|. No need to use it directly,
everything is setup in |MiniCursorword.setup|.
------------------------------------------------------------------------------
*MiniCursorword.auto_unhighlight()*
`MiniCursorword.auto_unhighlight`()
Auto unhighlight word under cursor
Designed to be used with |autocmd|. No need to use it directly, everything
is setup in |MiniCursorword.setup|.
vim:tw=78:ts=8:noet:ft=help:norl:

View File

@ -0,0 +1,426 @@
==============================================================================
------------------------------------------------------------------------------
*mini.doc*
*MiniDoc*
Generation of help files from EmmyLua-like annotations
Key design ideas:
- Keep documentation next to code by writing EmmyLua-like annotation
comments. They will be parsed as is, so formatting should follow built-in
guide in |help-writing|. However, custom hooks are allowed at many
generation stages for more granular management of output help file.
- Generation is done by processing a set of ordered files line by line.
Each line can either be considered as a part of documentation block (if
it matches certain configurable pattern) or not (considered to be an
"afterline" of documentation block). See |MiniDoc.generate()| for more
details.
- Processing is done by using nested data structures (section, block, file,
doc) describing certain parts of help file. See |MiniDoc-data-structures|
for more details.
- Project specific script can be written as plain Lua file with
configuratble path. See |MiniDoc.generate()| for more details.
What it doesn't do:
- It doesn't support markdown or other markup language inside annotations.
- It doesn't use treesitter in favor of Lua string manipulation for basic
tasks (parsing annotations, formatting, auto-generating tags, etc.). This
is done to manage complexity and be dependency free.
# Setup~
This module needs a setup with `require('mini.doc').setup({})` (replace
`{}` with your `config` table). It will create global Lua table `MiniDoc`
which you can use for scripting or manually (with `:lua MiniDoc.*`).
See |MiniDoc.config| for available config settings.
You can override runtime config settings locally to buffer inside
`vim.b.minidoc_config` which should have same structure as `MiniDoc.config`.
See |mini.nvim-buffer-local-config| for more details.
# Tips~
- Some settings tips that might make writing annotation comments easier:
- Set up appropriate 'comments' for `lua` file type to respect
EmmyLua-like's `---` comment leader. Value `:---,:--` seems to work.
- Set up appropriate 'formatoptions' (see also |fo-table|). Consider
adding `j`, `n`, `q`, and `r` flags.
- Set up appropriate 'formatlistpat' to help auto-formatting lists (if
`n` flag is added to 'formatoptions'). One suggestion (not entirely
ideal) is a value `^\s*[0-9\-\+\*]\+[\.\)]*\s\+`. This reads as 'at
least one special character (digit, `-`, `+`, `*`) possibly followed
by some punctuation (`.` or `)`) followed by at least one space is a
start of list item'.
- Probably one of the most reliable resources for what is considered to be
best practice when using this module is this whole plugin. Look at source
code for the reference.
# Comparisons~
- 'tjdevries/tree-sitter-lua':
- Its key design is to use treesitter grammar to parse both Lua code
and annotation comments. This makes it not easy to install,
customize, and support.
- It takes more care about automating output formatting (like auto
indentation and line width fit). This plugin leans more to manual
formatting with option to supply customized post-processing hooks.
# Disabling~
To disable, set `g:minidoc_disable` (globally) or `b:minidoc_disable` (for
a buffer) to `v:true`. Considering high number of different scenarios and
customization intentions, writing exact rules for disabling module's
functionality is left to user. See |mini.nvim-disabling-recipes| for common
recipes.
------------------------------------------------------------------------------
*MiniDoc-data-structures*
Data structures
Data structures are basically arrays of other structures accompanied with
some fields (keys with data values) and methods (keys with function
values):
- `Section structure` is an array of string lines describing one aspect
(determined by section id like '@param', '@return', '@text') of an
annotation subject. All lines will be used directly in help file.
- `Block structure` is an array of sections describing one annotation
subject like function, table, concept.
- `File structure` is an array of blocks describing certain file on disk.
Basically, file is split into consecutive blocks: annotation lines go
inside block, non-annotation - inside `block_afterlines` element of info.
- `Doc structure` is an array of files describing a final help file. Each
string line from section (when traversed in depth-first fashion) goes
directly into output file.
All structures have these keys:
- Fields:
- `info` - contains additional information about current structure.
For more details see next section.
- `parent` - table of parent structure (if exists).
- `parent_index` - index of this structure in its parent's array. Useful
for adding to parent another structure near current one.
- `type` - string with structure type (section, block, file, doc).
- Methods (use them as `x:method(args)`):
- `insert(self, [index,] child)` - insert `child` to `self` at position
`index` (optional; if not supplied, child will be appended to end).
Basically, a `table.insert()`, but adds `parent` and `parent_index`
fields to `child` while properly updating `self`.
- `remove(self [,index])` - remove from `self` element at position
`index`. Basically, a `table.remove()`, but properly updates `self`.
- `has_descendant(self, predicate)` - whether there is a descendant
(structure or string) for which `predicate` returns `true`. In case of
success also returns the first such descendant as second value.
- `has_lines(self)` - whether structure has any lines (even empty ones)
to be put in output file. For section structures this is equivalent to
`#self`, but more useful for higher order structures.
- `clear_lines(self)` - remove all lines from structure. As a result,
this structure won't contribute to output help file.
Description of `info` fields per structure type:
- `Section`:
- `id` - captured section identifier. Can be empty string meaning no
identifier is captured.
- `line_begin` - line number inside file at which section begins (-1 if
not generated from file).
- `line_end` - line number inside file at which section ends (-1 if not
generated from file).
- `Block`:
- `afterlines` - array of strings which were parsed from file after
this annotation block (up until the next block or end of file).
Useful for making automated decisions about what is being documented.
- `line_begin` - line number inside file at which block begins (-1 if
not generated from file).
- `line_end` - line number inside file at which block ends (-1 if not
generated from file).
- `File`:
- `path` - absolute path to a file (`''` if not generated from file).
- `Doc`:
- `input` - array of input file paths (as in |MiniDoc.generate|).
- `output` - output path (as in |MiniDoc.generate|).
- `config` - configuration used (as in |MiniDoc.generate|).
------------------------------------------------------------------------------
*MiniDoc.setup()*
`MiniDoc.setup`({config})
Module setup
Parameters~
{config} `(table)` Module config table. See |MiniDoc.config|.
Usage~
`require('mini.doc').setup({})` (replace `{}` with your `config` table)
------------------------------------------------------------------------------
*MiniDoc.config*
`MiniDoc.config`
Module config
Default values:
>
MiniDoc.config = {
-- Function which extracts part of line used to denote annotation.
-- For more information see 'Notes' in |MiniDoc.config|.
annotation_extractor = function(l) return string.find(l, '^%-%-%-(%S*) ?') end,
-- Identifier of block annotation lines until first captured identifier
default_section_id = '@text',
-- Hooks to be applied at certain stage of document life cycle. Should
-- modify its input in place (and not return new one).
hooks = {
-- Applied to block before anything else
block_pre = --<function: infers header sections (tag and/or signature)>,
-- Applied to section before anything else
section_pre = --<function: replaces current aliases>,
-- Applied if section has specified captured id
sections = {
['@alias'] = --<function: registers alias in MiniDoc.current.aliases>,
['@class'] = --<function>,
['@diagnostic'] = --<function: ignores any section content>,
-- For most typical usage see |MiniDoc.afterlines_to_code|
['@eval'] = --<function: evaluates lines; replaces with their return>,
['@field'] = --<function>,
['@overload'] = --<function>,
['@param'] = --<function>,
['@private'] = --<function: registers block for removal>,
['@return'] = --<function>,
['@seealso'] = --<function>,
['@signature'] = --<function: formats signature of documented object>,
['@tag'] = --<function: turns its line in proper tag lines>,
['@text'] = --<function: purposefully does nothing>,
['@toc'] = --<function: clears all section lines>,
['@toc_entry'] = --<function: registers lines for table of contents>,
['@type'] = --<function>,
['@usage'] = --<function>,
},
-- Applied to section after all previous steps
section_post = --<function: currently does nothing>,
-- Applied to block after all previous steps
block_post = --<function: does many things>,
-- Applied to file after all previous steps
file = --<function: adds separator>,
-- Applied to doc after all previous steps
doc = --<function: adds modeline>,
-- Applied to after output help file is written. Takes doc as argument.
write_post = --<function: various convenience actions>,
},
-- Path (relative to current directory) to script which handles project
-- specific help file generation (like custom input files, hooks, etc.).
script_path = 'scripts/minidoc.lua',
}
<
# Notes ~
- `annotation_extractor` takes single string line as input. Output
describes what makes an input to be an annotation (if anything). It
should be similar to `string.find` with one capture group: start and end
of annotation indicator (whole part will be removed from help line) with
third value being string of section id (if input describes first line of
section; `nil` or empty string otherwise). Output should be `nil` if line
is not part of annotation.
Default value means that annotation line should:
- Start with `---` at first column.
- Any non-whitespace after `---` will be treated as new section id.
- Single whitespace at the start of main text will be ignored.
- Hooks are expected to be functions. Their default values might do many
things which might change over time, so for more information please look
at source code. Some more information can be found in
|MiniDoc.default_hooks|.
------------------------------------------------------------------------------
*MiniDoc.current*
`MiniDoc.current`
Table with information about current state of auto-generation
It is reset at the beginning and end of `MiniDoc.generate()`.
At least these keys are supported:
- {aliases} - table with keys being alias name and values - alias
description and single string (using `\n` to separate lines).
- {eval_section} - input section of `@eval` section hook. Can be used for
information about current block, etc.
- {toc} - array with table of contents entries. Each entry is a whole
`@toc_entry` section.
------------------------------------------------------------------------------
*MiniDoc.default_hooks*
`MiniDoc.default_hooks`
Default hooks
This is default value of `MiniDoc.config.hooks`. Use it if only a little
tweak is needed.
Some more insight about their behavior:
- Default inference of documented object metadata (tag and object signature
at the moment) is done in `block_pre`. Inference is based on string
pattern matching, so can lead to false results, although works in most
cases. It intentionally works only if first line after block has no
indentation and contains all necessary information to determine if
inference should happen.
- Hooks for sections describing some "variable-like" object ('@class',
'@field', '@param') automatically enclose first word in '{}'.
- Hooks for sections which supposed to have "type-like" data ('@field',
'@param', '@return', '@type') automatically enclose *first found*
"type-like" word and its neighbor characters in '`(<type>)`' (expect
false positives). Algoritm is far from being 100% correct, but seems to
work with present allowed type annotation. For allowed types see
https://github.com/sumneko/lua-language-server/wiki/EmmyLua-Annotations#types-and-type
or, better yet, look in source code of this module.
- Automated creation of table of contents (TOC) is done in the following way:
- Put section with `@toc_entry` id in the annotation block. Section's
lines will be registered as TOC entry.
- Put `@toc` section where you want to insert rendered table of
contents. TOC entries will be inserted on the left, references for
their respective tag section (only first, if present) on the right.
Render is done in default `doc` hook (because it should be done after
processing all files).
- The `write_post` hook executes some actions convenient for iterative
annotations writing:
- Generate `:helptags` for directory containing output file.
- Silently reload buffer containing output file (if such exists).
- Display notification message about result.
------------------------------------------------------------------------------
*MiniDoc.generate()*
`MiniDoc.generate`({input}, {output}, {config})
Generate help file
# Algoritm~
- Main parameters for help generation are an array of input file paths and
path to output help file.
- Parse all inputs:
- For each file, lines are processed top to bottom in order to create an
array of documentation blocks. Each line is tested whether it is an
annotation by applying `MiniDoc.config.annotation_extractor`: if
anything is extracted, it is considered to be an annotation. Annotation
line goes to "current block" after removing extracted annotation
indicator, otherwise - to afterlines of "current block".
- Each block's annotation lines are processed top to bottom. If line had
captured section id, it is a first line of "current section" (first
block lines are allowed to not specify section id; by default it is
`@text`). All subsequent lines without captured section id go into
"current section".
- Apply structure hooks (they should modify its input in place, which is
possible due to 'table nature' of all inputs):
- Each block is processed by `MiniDoc.config.hooks.block_pre`. This is a
designated step for auto-generation of sections from descibed
annotation subject (like sections with id `@tag`, `@type`).
- Each section is processed by `MiniDoc.config.hooks.section_pre`.
- Each section is processed by corresponding
`MiniDoc.config.hooks.sections` function (table key equals to section
id). This is a step where most of formatting should happen (like
wrap first word of `@param` section with `{` and `}`, append empty
line to section, etc.).
- Each section is processed by `MiniDoc.config.hooks.section_post`.
- Each block is processed by `MiniDoc.config.hooks.block_post`. This is
a step for processing block after formatting is done (like add first
line with `----` delimiter).
- Each file is processed by `MiniDoc.config.hooks.file`. This is a step
for adding any file-related data (like add first line with `====`
delimiter).
- Doc is processed by `MiniDoc.config.hooks.doc`. This is a step for
adding any helpfile-related data (maybe like table of contents).
- Collect all strings from sections in depth-first fashion (equivalent to
nested "for all files -> for all blocks -> for all sections -> for all
strings -> add string to output") and write them to output file. Strings
can have `\n` character indicating start of new line.
- Execute `MiniDoc.config.write_post` hook. This is useful for showing some
feedback and making actions involving newly updated help file (like
generate tags, etc.).
# Project specific script~
If all arguments have default `nil` values, first there is an attempt to
source project specific script. This is basically a `luafile
<MiniDoc.config.script_path>` with current Lua runtime while caching and
restoring current `MiniDoc.config`. Its successful execution stops any
further generation actions while error means proceeding generation as if no
script was found.
Typical script content might include definition of custom hooks, input and
output files with eventual call to `require('mini.doc').generate()` (with
or without arguments).
Parameters~
{input} `(table)` Array of file paths which will be processed in supplied
order. Default: all '.lua' files from current directory following by all
such files in these subdirectories: 'lua/', 'after/', 'colors/'. Note:
any 'init.lua' file is placed before other files from the same directory.
{output} `(string)` Path for output help file. Default:
`doc/<current_directory>.txt` (designed to be used for generating help
file for plugin).
{config} `(table)` Configuration overriding parts of |MiniDoc.config|.
Return~
`(table)` Document structure which was generated and used for output
help file. In case `MiniDoc.config.script_path` was successfully used,
this is a return from the latest call of this function.
------------------------------------------------------------------------------
*MiniDoc.afterlines_to_code()*
`MiniDoc.afterlines_to_code`({struct})
Convert afterlines to code
This function is designed to be used together with `@eval` section to
automate documentation of certain values (notable default values of a
table). It processes afterlines based on certain directives and makes
output looking like a code block.
Most common usage is by adding the following section in your annotation:
`@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)`
# Directives ~
Directives are special comments that are processed using Lua string pattern
capabilities (so beware of false positives). Each directive should be put
on its separate line. Supported directives:
- `--minidoc_afterlines_end` denotes a line at afterlines end. Only all
lines before it will be considered as afterlines. Useful if there is
extra code in afterlines which shouldn't be used.
- `--minidoc_replace_start <replacement>` and `--minidoc_replace_end`
denote lines between them which should be replaced with `<replacement>`.
Useful for manually changing what should be placed in output like in case
of replacing function body with something else.
Here is an example. Suppose having these afterlines:
>
--minidoc_replace_start {
M.config = {
--minidoc_replace_end
param_one = 1,
--minidoc_replace_start param_fun = --<function>
param_fun = function(x)
return x + 1
end
--minidoc_replace_end
}
--minidoc_afterlines_end
return M
<
After adding `@eval` section those will be formatted as:
>
{
param_one = 1,
param_fun = --<function>
}
<
Parameters~
{struct} `(table)` Block or section structure which after lines will be
converted to code.
Return~
`(string)` Single string (using `\n` to separate lines) describing
afterlines as code block in help file.
vim:tw=78:ts=8:noet:ft=help:norl:

View File

@ -0,0 +1,147 @@
==============================================================================
------------------------------------------------------------------------------
*mini.fuzzy*
*MiniFuzzy*
Minimal and fast fuzzy matching.
# Setup~
This module doesn't need setup, but it can be done to improve usability.
Setup with `require('mini.fuzzy').setup({})` (replace `{}` with your
`config` table). It will create global Lua table `MiniFuzzy` which you can
use for scripting or manually (with `:lua MiniFuzzy.*`).
See |MiniFuzzy.config| for `config` structure and default values.
You can override runtime config settings locally to buffer inside
`vim.b.minifuzzy_config` which should have same structure as
`MiniFuzzy.config`.
See |mini.nvim-buffer-local-config| for more details.
# Notes~
1. Currently there is no explicit design to work with multibyte symbols,
but simple examples should work.
2. Smart case is used: case insensitive if input word (which is usually a
user input) is all lower ase. Case sensitive otherwise.
------------------------------------------------------------------------------
*MiniFuzzy-algorithm*
# Algorithm design~
General design uses only width of found match and index of first letter
match. No special characters or positions (like in fzy and fzf) are used.
Given input `word` and target `candidate`:
- The goal is to find matching between `word`'s letters and letters in
`candidate`, which minimizes certain score. It is assumed that order of
letters in `word` and those matched in `candidate` should be the same.
- Matching is represented by matched positions: an array `positions` of
integers with length equal to number of letters in `word`. The following
should be always true in case of a match: `candidate`'s letter at index
`positions[i]` is letters[i]` for all valid `i`.
- Matched positions are evaluated based only on two features: their width
(number of indexes between first and last positions) and first match
(index of first letter match). There is a global setting `cutoff` for
which all feature values greater than it can be considered "equally bad".
- Score of matched positions is computed with following explicit formula:
`cutoff * min(width, cutoff) + min(first, cutoff)`. It is designed to be
equivalent to first comparing widths (lower is better) and then comparing
first match (lower is better). For example, if `word = 'time'`:
- '_time' (width 4) will have a better match than 't_ime' (width 5).
- 'time_a' (width 4, first 1) will have a better match than 'a_time'
(width 4, first 3).
- Final matched positions are those which minimize score among all possible
matched positions of `word` and `candidate`.
------------------------------------------------------------------------------
*MiniFuzzy.setup()*
`MiniFuzzy.setup`({config})
Module setup
Parameters~
{config} `(table)` Module config table. See |MiniFuzzy.config|.
Usage~
`require('mini.fuzzy').setup({})` (replace `{}` with your `config` table)
------------------------------------------------------------------------------
*MiniFuzzy.config*
`MiniFuzzy.config`
Module config
Default values:
>
MiniFuzzy.config = {
-- Maximum allowed value of match features (width and first match). All
-- feature values greater than cutoff can be considered "equally bad".
cutoff = 100,
}
<
------------------------------------------------------------------------------
*MiniFuzzy.match()*
`MiniFuzzy.match`({word}, {candidate})
Compute match data of input `word` and `candidate` strings
It tries to find best match for input string `word` (usually user input)
and string `candidate`. Returns table with elements:
- `positions` - array with letter indexes inside `candidate` which
matched to corresponding letters in `word`. Or `nil` if no match.
- `score` - positive number representing how good the match is (lower is
better). Or `-1` if no match.
Parameters~
{word} `(string)` Input word (usually user input).
{candidate} `(string)` Target word (usually with which matching is done).
Return~
`(table)` Table with matching information (see function's description).
------------------------------------------------------------------------------
*MiniFuzzy.filtersort()*
`MiniFuzzy.filtersort`({word}, {candidate_array})
Filter string array
This leaves only those elements of input array which matched with `word`
and sorts from best to worst matches (based on score and index in original
array, both lower is better).
Parameters~
{word} `(string)` String which will be searched.
{candidate_array} `(table)` Lua array of strings inside which word will be
searched.
Return~
`(...)` Arrays of matched candidates and their indexes in original input.
------------------------------------------------------------------------------
*MiniFuzzy.process_lsp_items()*
`MiniFuzzy.process_lsp_items`({items}, {base})
Fuzzy matching for `lsp_completion.process_items` of |MiniCompletion.config|
Parameters~
{items} `(table)` Lua array with LSP 'textDocument/completion' response items.
{base} `(string)` Word to complete.
------------------------------------------------------------------------------
*MiniFuzzy.get_telescope_sorter()*
`MiniFuzzy.get_telescope_sorter`({opts})
Custom getter for `telescope.nvim` sorter
Designed to be used as value for |telescope.defaults.file_sorter| and
|telescope.defaults.generic_sorter| inside `setup()` call.
Parameters~
{opts} `(table)` Options (currently not used).
Usage~
>
require('telescope').setup({
defaults = {
generic_sorter = require('mini.fuzzy').get_telescope_sorter
}
})
vim:tw=78:ts=8:noet:ft=help:norl:

View File

@ -0,0 +1,397 @@
==============================================================================
------------------------------------------------------------------------------
*mini.indentscope*
*MiniIndentscope*
Visualize and operate on indent scope
Indent scope (or just "scope") is a maximum set of consecutive lines which
contains certain reference line (cursor line by default) and every member
has indent not less than certain reference indent ("indent at cursor" by
default: minimum between cursor column and indent of cursor line).
Features:
- Visualize scope with animated vertical line. It is very fast and done
automatically in a non-blocking way (other operations can be performed,
like moving cursor). You can customize debounce delay and animation rule.
- Customization of scope computation options can be done on global level
(in |MiniIndentscope.config|), for a certain buffer (using
`vim.b.miniindentscope_config` buffer variable), or within a call (using
`opts` variable in |MiniIndentscope.get_scope|).
- Customizable notion of a border: which adjacent lines with strictly lower
indent are recognized as such. This is useful for a certain filetypes
(for example, Python or plain text).
- Customizable way of line to be considered "border first". This is useful
if you want to place cursor on function header and get scope of its body.
- There are textobjects and motions to operate on scope. Support |count|
and dot-repeat (in operator pending mode).
# Setup~
This module needs a setup with `require('mini.indentscope').setup({})`
(replace `{}` with your `config` table). It will create global Lua table
`MiniIndentscope` which you can use for scripting or manually (with `:lua
MiniIndentscope.*`).
See |MiniIndentscope.config| for available config settings.
You can override runtime config settings locally to buffer inside
`vim.b.miniindentscope_config` which should have same structure as
`MiniIndentscope.config`. See |mini.nvim-buffer-local-config| for more details.
# Comparisons~
- 'lukas-reineke/indent-blankline.nvim':
- Its main functionality is about showing static guides of indent levels.
- Implementation of 'mini.indentscope' is similar to
'indent-blankline.nvim' (using |extmarks| on first column to be shown
even on blank lines). They can be used simultaneously, but it will
lead to one of the visualizations being on top (hiding) of another.
# Highlight groups~
* `MiniIndentscopeSymbol` - symbol showing on every line of scope.
* `MiniIndentscopePrefix` - space before symbol. By default made so as to
appear as nothing is displayed.
To change any highlight group, modify it directly with |:highlight|.
# Disabling~
To disable autodrawing, set `g:miniindentscope_disable` (globally) or
`b:miniindentscope_disable` (for a buffer) to `v:true`. Considering high
number of different scenarios and customization intentions, writing exact
rules for disabling module's functionality is left to user. See
|mini.nvim-disabling-recipes| for common recipes.
------------------------------------------------------------------------------
*MiniIndentscope-drawing*
Drawing of scope indicator
Draw of scope indicator is done as iterative animation. It has the
following design:
- Draw indicator on origin line (where cursor is at) immediately. Indicator
is visualized as `MiniIndentscope.config.symbol` placed to the right of
scope's border indent. This creates a line from top to bottom scope edges.
- Draw upward and downward concurrently per one line. Progression by one
line in both direction is considered to be one step of animation.
- Before each step wait certain amount of time, which is decided by
"animation function". It takes next and total step numbers (both are one
or bigger) and returns number of milliseconds to wait before drawing next
step. Comparing to a more popular "easing functions" in animation (input:
duration since animation start; output: percent of animation done), it is
a discrete inverse version of its derivative. Such interface proved to be
more appropriate for kind of task at hand.
Special cases~
- When scope to be drawn intersects (same indent, ranges overlap) currently
visible one (at process or finished drawing), drawing is done immediately
without animation. With most common example being typing new text, this
feels more natural.
- Scope for the whole buffer is not drawn as it is isually redundant.
Technically, it can be thought as drawn at column 0 (because border
indent is -1) which is not visible.
------------------------------------------------------------------------------
*MiniIndentscope.setup()*
`MiniIndentscope.setup`({config})
Module setup
Parameters~
{config} `(table)` Module config table. See |MiniIndentscope.config|.
Usage~
`require('mini.indentscope').setup({})` (replace `{}` with your `config` table)
------------------------------------------------------------------------------
*MiniIndentscope.config*
`MiniIndentscope.config`
Module config
Default values:
>
MiniIndentscope.config = {
draw = {
-- Delay (in ms) between event and start of drawing scope indicator
delay = 100,
-- Animation rule for scope's first drawing. A function which, given
-- next and total step numbers, returns wait time (in ms). See
-- |MiniIndentscope.gen_animation()| for builtin options. To disable
-- animation, use `require('mini.indentscope').gen_animation('none')`.
animation = --<function: implements constant 20ms between steps>,
},
-- Module mappings. Use `''` (empty string) to disable one.
mappings = {
-- Textobjects
object_scope = 'ii',
object_scope_with_border = 'ai',
-- Motions (jump to respective border line; if not present - body line)
goto_top = '[i',
goto_bottom = ']i',
},
-- Options which control scope computation
options = {
-- Type of scope's border: which line(s) with smaller indent to
-- categorize as border. Can be one of: 'both', 'top', 'bottom', 'none'.
border = 'both',
-- Whether to use cursor column when computing reference indent.
-- Useful to see incremental scopes with horizontal cursor movements.
indent_at_cursor = true,
-- Whether to first check input line to be a border of adjacent scope.
-- Use it if you want to place cursor on function header to get scope of
-- its body.
try_as_border = false,
},
-- Which character to use for drawing scope indicator
symbol = '╎',
}
<
# Options ~
- Options can be supplied globally (from this `config`), locally to buffer
(via `options` field of `vim.b.miniindentscope_config` buffer variable),
or locally to call (as argument to |MiniIndentscope.get_scope()|).
- Option `border` controls which line(s) with smaller indent to categorize
as border. This matters for textobjects and motions.
It also controls how empty lines are treated: they are included in scope
only if followed by a border. Another way of looking at it is that indent
of blank line is computed based on value of `border` option.
Here is an illustration of how `border` works in presense of empty lines:
>
|both|bottom|top|none|
1|function foo() | 0 | 0 | 0 | 0 |
2| | 4 | 0 | 4 | 0 |
3| print('Hello world') | 4 | 4 | 4 | 4 |
4| | 4 | 4 | 2 | 2 |
5| end | 2 | 2 | 2 | 2 |
<
Numbers inside a table are indent values of a line computed with certain
value of `border`. So, for example, a scope with reference line 3 and
right-most column has body range depending on value of `border` option:
- `border` is "both": range is 2-4, border is 1 and 5 with indent 2.
- `border` is "top": range is 2-3, border is 1 with indent 0.
- `border` is "bottom": range is 3-4, border is 5 with indent 0.
- `border` is "none": range is 3-3, border is empty with indent `nil`.
- Option `indent_at_cursor` controls if cursor position should affect
computation of scope. If `true`, reference indent is a minimum of
reference line's indent and cursor column. In main example, here how
scope's body range differs depending on cursor column and `indent_at_cursor`
value (assuming cursor is on line 3 and it is whole buffer):
>
Column\Option true|false
1 and 2 2-5 | 2-4
3 and more 2-4 | 2-4
<
- Option `try_as_border` controls how to act when input line can be
recognized as a border of some neighbor indent scope. In main example,
when input line is 1 and can be recognized as border for inner scope,
value `try_as_border = true` means that inner scope will be returned.
Similar, for input line 5 inner scope will be returned if it is
recognized as border.
------------------------------------------------------------------------------
*MiniIndentscope.get_scope()*
`MiniIndentscope.get_scope`({line}, {col}, {opts})
Compute indent scope
Indent scope (or just "scope") is a maximum set of consecutive lines which
contains certain reference line (cursor line by default) and every member
has indent not less than certain reference indent ("indent at column" by
default). Here "indent at column" means minimum between input column value
and indent of reference line. When using cursor column, this allows for a
useful interactive view of nested indent scopes by making horizontal
movements within line.
Options controlling actual computation is taken from these places in order:
- Argument `opts`. Use it to ensure independence from other sources.
- Buffer local variable `vim.b.miniindentscope_config` (`options` field).
Useful to define local behavior (for example, for a certain filetype).
- Global options from |MiniIndentscope.config|.
Algorithm overview~
- Compute reference "indent at column". Reference line is an input `line`
which might be modified to one of its neighbors if `try_as_border` option
is `true`: if it can be viewed as border of some neighbor scope, it will.
- Process upwards and downwards from reference line to search for line with
indent strictly less than reference one. This is like casting rays up and
down from reference line and reference indent until meeting "a wall"
(character to the right of indent or buffer edge). Latest line before
meeting is a respective end of scope body. It always exists because
reference line is a such one.
- Based on top and bottom lines with strictly lower indent, construct
scopes's border. The way it is computed is decided based on `border`
option (see |MiniIndentscope.config| for more information).
- Compute border indent as maximum indent of border lines (or reference
indent minus one in case of no border). This is used during drawing
visual indicator.
Indent computation~
For every line indent is intended to be computed unambiguously:
- For "normal" lines indent is an output of |indent()|.
- Indent is `-1` for imaginary lines 0 and past last line.
- For blank and empty lines indent is computed based on previous
(|prevnonblank()|) and next (|nextnonblank()|) non-blank lines. The way
it is computed is decided based on `border` in order to not include blank
lines at edge of scope's body if there is no border there. See
|MiniIndentscope.config| for a details example.
Parameters~
{line} `(number)` Input line number (starts from 1). Can be modified to a
neighbor if `try_as_border` is `true`. Default: cursor line.
{col} `(number)` Column number (starts from 1). Default: if
`indent_at_cursor` option is `true` - cursor column from `curswant` of
|getcurpos()| (allows for more natural behavior on empty lines);
`math.huge` otherwise in order to not incorporate cursor in computation.
{opts} `(table)` Options to override global or buffer local ones (see
|MiniIndentscope.config|).
Return~
`(table)` Table with scope information:
- <body> - table with <top> (top line of scope, inclusive), <bottom>
(bottom line of scope, inclusive), and <indent> (minimum indent withing
scope) keys. Line numbers start at 1.
- <border> - table with <top> (line of top border, might be `nil`),
<bottom> (line of bottom border, might be `nil`), and <indent> (indent
of border) keys. Line numbers start at 1.
- <buf_id> - identifier of current buffer.
- <reference> - table with <line> (reference line), <column> (reference
column), and <indent> ("indent at column") keys.
------------------------------------------------------------------------------
*MiniIndentscope.auto_draw()*
`MiniIndentscope.auto_draw`({opts})
Auto draw scope indicator based on movement events
Designed to be used with |autocmd|. No need to use it directly, everything
is setup in |MiniIndentscope.setup|.
Parameters~
{opts} `(table)` Options.
------------------------------------------------------------------------------
*MiniIndentscope.draw()*
`MiniIndentscope.draw`({scope}, {opts})
Draw scope manually
Scope is visualized as a vertical line withing scope's body range at column
equal to border indent plus one (or body indent if border is absent).
Numbering starts from one.
Parameters~
{scope} `(table)` Scope. Default: output of |MiniIndentscope.get_scope|
with default arguments.
{opts} `(table)` Options. Currently supported:
- <animation_fun> - animation function for drawing. See
|MiniIndentscope-drawing| and |MiniIndentscope.gen_animation()|.
------------------------------------------------------------------------------
*MiniIndentscope.undraw()*
`MiniIndentscope.undraw`()
Undraw currently visible scope manually
------------------------------------------------------------------------------
*MiniIndentscope.gen_animation()*
`MiniIndentscope.gen_animation`({easing}, {opts})
Generate builtin animation function
This is a builtin source to generate animation function for usage in
`MiniIndentscope.config.draw.animation`. Most of them are variations of
common easing functions, which provide certain type of progression for
revealing scope visual indicator.
Supported easing types:
- `'none'` - show indicator immediately. Equivalent to animation function
always returning 0.
- `'linear'` - linear progression.
- Quadratic progression:
- `'quadraticIn'` - accelerating from zero speed.
- `'quadraticOut'` - decelerating to zero speed.
- `'quadraticInOut'` - accelerating halfway, decelerating after.
- Cubic progression:
- `'cubicIn'` - accelerating from zero speed.
- `'cubicOut'` - decelerating to zero speed.
- `'cubicInOut'` - accelerating halfway, decelerating after.
- Quartic progression:
- `'quarticIn'` - accelerating from zero speed.
- `'quarticOut'` - decelerating to zero speed.
- `'quarticInOut'` - accelerating halfway, decelerating after.
- Exponential progression:
- `'exponentialIn'` - accelerating from zero speed.
- `'exponentialOut'` - decelerating to zero speed.
- `'exponentialInOut'` - accelerating halfway, decelerating after.
Customization of duration and other general behavior of output animation
function is done through `opts` argument.
Parameters~
{easing} `(string)` One of supported easing types.
{opts} `(table)` Options that control progression. Possible keys:
- <duration> `(number)` - duration (in ms) of a unit. Default: 20.
- <unit> `(string)` - which unit's duration `opts.duration` controls. One
of "step" (default; ensures average duration of step to be `opts.duration`)
or "total" (ensures fixed total duration regardless of scope's range).
Return~
`(function)` Animation function (see |MiniIndentscope-drawing|).
Examples~
- Don't use animation: `gen_animation('none')`
- Use quadratic "out" easing with total duration of 1000 ms:
`gen_animation('quadraticOut', { duration = 1000, unit = 'total' })`
See also~
|MiniIndentscope-drawing| for more information about how drawing is done.
------------------------------------------------------------------------------
*MiniIndentscope.move_cursor()*
`MiniIndentscope.move_cursor`({side}, {use_border}, {scope})
Move cursor within scope
Cursor is placed on a first non-blank character of target line.
Parameters~
{side} `(string)` One of "top" or "bottom".
{use_border} `(boolean)` Whether to move to border or withing scope's body.
If particular border is absent, body is used.
{scope} `(table)` Scope to use. Default: output of |MiniIndentscope.get_scope()|.
------------------------------------------------------------------------------
*MiniIndentscope.operator()*
`MiniIndentscope.operator`({side}, {add_to_jumplist})
Function for motion mappings
Move to a certain side of border. Respects |count| and dot-repeat (in
operator-pending mode). Doesn't move cursor for scope that is not shown
(drawing indent less that zero).
Parameters~
{side} `(string)` One of "top" or "bottom".
{add_to_jumplist} `(boolean)` Whether to add movement to jump list. It is
`true` only for Normal mode mappings.
------------------------------------------------------------------------------
*MiniIndentscope.textobject()*
`MiniIndentscope.textobject`({use_border})
Function for textobject mappings
Respects |count| and dot-repeat (in operator-pending mode). Doesn't work
for scope that is not shown (drawing indent less that zero).
Parameters~
{use_border} `(boolean)` Whether to include border in textobject. When
`true` and `try_as_border` option is `false`, allows "chaining" calls for
incremental selection.
vim:tw=78:ts=8:noet:ft=help:norl:

View File

@ -0,0 +1,192 @@
==============================================================================
------------------------------------------------------------------------------
*mini.jump*
*MiniJump*
Smarter forward/backward jumping to a single character.
Features:
- Extend f, F, t, T to work on multiple lines.
- Repeat jump by pressing f, F, t, T again. It is reset when cursor moved
as a result of not jumping or timeout after idle time (duration
customizable).
- Highlight (after customizable delay) all possible target characters and
stop it after some (customizable) idle time.
- Normal, Visual, and Operator-pending (with full dot-repeat) modes are
supported.
This module follows vim's 'ignorecase' and 'smartcase' options. When
'ignorecase' is set, f, F, t, T will match case-insensitively. When
'smartcase' is also set, f, F, t, T will only match lowercase
characters case-insensitively.
# Setup~
This module needs a setup with `require('mini.jump').setup({})`
(replace `{}` with your `config` table). It will create global Lua table
`MiniJump` which you can use for scripting or manually (with
`:lua MiniJump.*`).
See |MiniJump.config| for `config` structure and default values.
You can override runtime config settings locally to buffer inside
`vim.b.minijump_config` which should have same structure as
`MiniJump.config`. See |mini.nvim-buffer-local-config| for more details.
# Highlight groups~
* `MiniJump` - all possible cursor positions.
To change any highlight group, modify it directly with |:highlight|.
# Disabling~
To disable core functionality, set `g:minijump_disable` (globally) or
`b:minijump_disable` (for a buffer) to `v:true`. Considering high number of
different scenarios and customization intentions, writing exact rules for
disabling module's functionality is left to user. See
|mini.nvim-disabling-recipes| for common recipes.
------------------------------------------------------------------------------
*MiniJump.setup()*
`MiniJump.setup`({config})
Module setup
Parameters~
{config} `(table)` Module config table. See |MiniJump.config|.
Usage~
`require('mini.jump').setup({})` (replace `{}` with your `config` table)
------------------------------------------------------------------------------
*MiniJump.config*
`MiniJump.config`
Module config
Default values:
>
MiniJump.config = {
-- Module mappings. Use `''` (empty string) to disable one.
mappings = {
forward = 'f',
backward = 'F',
forward_till = 't',
backward_till = 'T',
repeat_jump = ';',
},
-- Delay values (in ms) for different functionalities. Set any of them to
-- a very big number (like 10^7) to virtually disable.
delay = {
-- Delay between jump and highlighting all possible jumps
highlight = 250,
-- Delay between jump and automatic stop if idle (no jump is done)
idle_stop = 10000000,
},
}
<
------------------------------------------------------------------------------
*MiniJump.state*
`MiniJump.state`
Data about jumping state
It stores various information used in this module. All elements, except
`jumping`, is about the latest jump. They are used as default values for
similar arguments.
Class~
{JumpingState}
Fields~
{target} `(string)` The string to jump to.
{backward} `(boolean)` Whether to jump backward.
{till} `(boolean)` Whether to jump just before/after the match instead of
exactly on target. This includes positioning cursor past the end of
previous/current line. Note that with backward jump this might lead to
cursor being on target if can't be put past the line.
{n_times} `(number)` Number of times to perform consecutive jumps.
{mode} `(string)` Mode of latest jump (output of |mode()| with non-zero argument).
{jumping} `(boolean)` Whether module is currently in "jumping mode": usage of
|MiniJump.smart_jump| and all mappings won't require target.
Initial values:
>
MiniJump.state = {
target = nil,
backward = false,
till = false,
n_times = 1,
mode = nil,
jumping = false,
}
<
------------------------------------------------------------------------------
*MiniJump.jump()*
`MiniJump.jump`({target}, {backward}, {till}, {n_times})
Jump to target
Takes a string and jumps to its first occurrence in desired direction.
All default values are taken from |MiniJump.state| to emulate latest jump.
Parameters~
{target} `(string)` The string to jump to.
{backward} `(boolean)` Whether to jump backward.
{till} `(boolean)` Whether to jump just before/after the match instead of
exactly on target. This includes positioning cursor past the end of
previous/current line. Note that with backward jump this might lead to
cursor being on target if can't be put past the line.
{n_times} `(number)` Number of times to perform consecutive jumps.
------------------------------------------------------------------------------
*MiniJump.smart_jump()*
`MiniJump.smart_jump`({backward}, {till})
Make smart jump
If the last movement was a jump, perform another jump with the same target.
Otherwise, wait for a target input (via |getchar()|). Respects |v:count|.
All default values are taken from |MiniJump.state| to emulate latest jump.
Parameters~
{backward} `(boolean)` Whether to jump backward.
{till} `(boolean)` Whether to jump just before/after the match instead of
exactly on target. This includes positioning cursor past the end of
previous/current line. Note that with backward jump this might lead to
cursor being on target if can't be put past the line.
------------------------------------------------------------------------------
*MiniJump.expr_jump()*
`MiniJump.expr_jump`({backward}, {till})
Make expression jump
Cache information about the jump and return string with command to perform
jump. Designed to be used inside Operator-pending mapping (see
|omap-info|). Always asks for target (via |getchar()|). Respects |v:count|.
All default values are taken from |MiniJump.state| to emulate latest jump.
Parameters~
{backward} `(boolean)` Whether to jump backward.
{till} `(boolean)` Whether to jump just before/after the match instead of
exactly on target. This includes positioning cursor past the end of
previous/current line. Note that with backward jump this might lead to
cursor being on target if can't be put past the line.
------------------------------------------------------------------------------
*MiniJump.stop_jumping()*
`MiniJump.stop_jumping`()
Stop jumping
Removes highlights (if any) and forces the next smart jump to prompt for
the target. Automatically called on appropriate Neovim |events|.
------------------------------------------------------------------------------
*MiniJump.on_cursormoved()*
`MiniJump.on_cursormoved`()
Act on |CursorMoved|
vim:tw=78:ts=8:noet:ft=help:norl:

View File

@ -0,0 +1,395 @@
==============================================================================
------------------------------------------------------------------------------
*mini.jump2d*
*MiniJump2d*
Jump within visible lines via iterative label filtering.
Features:
- Make jump by iterative filtering of possible, equally considered jump
spots until there is only one. Filtering is done by typing a label
character that is visualized at jump spot.
- Customizable:
- Way of computing possible jump spots with opinionated default.
- Characters used to label jump spots during iterative filtering.
- Action hooks to be executed at certain events during jump.
- Allowed windows: current and/or not current.
- Allowed lines: whether to process blank or folded lines, lines
before/at/after cursor line, etc. Example: user can configure to look
for spots only inside current window at or after cursor line.
Example: user can configure to look for word starts only inside current
window at or after cursor line with 'j' and 'k' labels performing some
action after jump.
- Works in Visual and Operator-pending (with dot-repeat) modes.
- Preconfigured ways of computing jump spots (see |MiniJump2d.builtin_opts|).
- Works with multibyte characters.
General overview of how jump is intended to be performed:
- Lock eyes on desired location ("spot") recognizable by future jump.
Should be within visible lines at place where cursor can be placed.
- Initiate jump. Either by custom keybinding or with a call to
|MiniJump2d.start()| (allows customization options). This will highlight
all possible jump spots with their labels (letters from "a" to "z" by
default). For more details, read |MiniJump2d.start()| and |MiniJump2d.config|.
- Type character that appeared over desired location. If its label was
unique, jump is performed. If it wasn't unique, possible jump spots are
filtered to those having the same label character.
- Repeat previous step until there is only one possible jump spot or type `<CR>`
to jump to first available jump spot. Typing anything else stops jumping
without moving cursor.
# Setup~
This module needs a setup with `require('mini.jump2d').setup({})` (replace
`{}` with your `config` table). It will create global Lua table
`MiniJump2d` which you can use for scripting or manually (with
`:lua MiniJump2d.*`).
See |MiniJump2d.config| for available config settings.
You can override runtime config settings locally to buffer inside
`vim.b.minijump2d_config` which should have same structure as
`MiniJump2d.config`. See |mini.nvim-buffer-local-config| for more details.
# Example usage~
- Modify default jumping to use only current window at or after cursor line: >
require('mini.jump2d').setup({
allowed_lines = { cursor_before = false },
allowed_windows = { not_current = false },
})
- `lua MiniJump2d.start(MiniJump2d.builtin_opts.line_start)` - jump to word
start using combination of options supplied in |MiniJump2d.config| and
|MiniJump2d.builtin_opts.line_start|.
- `lua MiniJump2d.start(MiniJump2d.builtin_opts.single_character)` - jump
to single character typed after executing this command.
- See more examples in |MiniJump2d.start| and |MiniJump2d.builtin_opts|.
# Comparisons~
- 'phaazon/hop.nvim':
- Both are fast, customizable, and extensible (user can write their own
ways to define jump spots).
- Both have several builtin ways to specify type of jump (word start,
line start, one character or query based on user input). 'hop.nvim'
does that by exporting many targeted Neovim commands, while this
module has preconfigured basic options leaving others to
customization with Lua code (see |MiniJump2d.builtin_opts|).
- 'hop.nvim' computes labels (called "hints") differently. Contrary to
this module deliberately not having preference of one jump spot over
another, 'hop.nvim' uses specialized algorithm that produces sequence
of keys in a slightly biased manner: some sequences are intentionally
shorter than the others (leading to fewer average keystrokes). They
are put near cursor (by default) and highlighted differently. Final
order of sequences is based on distance to the cursor.
- 'hop.nvim' visualizes labels differently. It is designed to show
whole sequences at once, while this module intentionally shows only
current one at a time.
- 'mini.jump2d' has opinionated default algorithm of computing jump
spots. See |MiniJump2d.default_spotter|.
# Highlight groups~
* `MiniJump2dSpot` - highlighting of jump spots. By default it uses label
with highest contrast while not being too visually demanding: white on
black for dark 'background', black on white for light. If it doesn't
suit your liking, try couple of these alternatives (or choose your own,
of course):
- `hi MiniJump2dSpot gui=reverse` - reverse underlying highlighting (more
colorful while being visible in any colorscheme).
- `hi MiniJump2dSpot gui=bold,italic` - bold italic.
- `hi MiniJump2dSpot gui=undercurl guisp=red` - red undercurl.
To change any highlight group, modify it directly with |:highlight|.
# Disabling~
To disable, set `g:minijump2d_disable` (globally) or `b:minijump2d_disable`
(for a buffer) to `v:true`. Considering high number of different scenarios
and customization intentions, writing exact rules for disabling module's
functionality is left to user. See |mini.nvim-disabling-recipes| for common
recipes.
------------------------------------------------------------------------------
*MiniJump2d.setup()*
`MiniJump2d.setup`({config})
Module setup
Parameters~
{config} `(table)` Module config table. See |MiniJump2d.config|.
Usage~
`require('mini.jump2d').setup({})` (replace `{}` with your `config` table)
------------------------------------------------------------------------------
*MiniJump2d.config*
`MiniJump2d.config`
Module config
Default values:
>
MiniJump2d.config = {
-- Function producing jump spots (byte indexed) for a particular line.
-- For more information see |MiniJump2d.start|.
-- If `nil` (default) - use |MiniJump2d.default_spotter|
spotter = nil,
-- Characters used for labels of jump spots (in supplied order)
labels = 'abcdefghijklmnopqrstuvwxyz',
-- Which lines are used for computing spots
allowed_lines = {
blank = true, -- Blank line (not sent to spotter even if `true`)
cursor_before = true, -- Lines before cursor line
cursor_at = true, -- Cursor line
cursor_after = true, -- Lines after cursor line
fold = true, -- Start of fold (not sent to spotter even if `true`)
},
-- Which windows from current tabpage are used for visible lines
allowed_windows = {
current = true,
not_current = true,
},
-- Functions to be executed at certain events
hooks = {
before_start = nil, -- Before jump start
after_jump = nil, -- After jump was actually done
},
-- Module mappings. Use `''` (empty string) to disable one.
mappings = {
start_jumping = '<CR>',
},
}
<
# Options~
## Spotter function~
Actual computation of possible jump spots is done through spotter function.
It should have the following arguments:
- `line_num` is a line number inside buffer.
- `args` - table with additional arguments:
- {win_id} - identifier of a window where input line number is from.
- {win_id_init} - identifier of a window which was current when
`MiniJump2d.start()` was called.
Its output is a list of byte-indexed positions that should be considered as
possible jump spots for this particular line in this particular window.
Note: for a more aligned visualization this list should be (but not
strictly necessary) sorted increasingly.
Note: spotter function is always called with `win_id` window being
"temporary current" (see |nvim_win_call|). This allows using builtin
Vimscript functions that operate only inside current window.
## Allowed lines~
Option `allowed_lines` controls which lines will be used for computing
possible jump spots:
- If `blank` or `fold` is `true`, it is possible to jump to first column of blank
line (determined by |prevnonblank|) or first folded one (determined by
|foldclosed|) respectively. Otherwise they are skipped. These lines are
not processed by spotter function even if the option is `true`.
- If `cursor_before`, (`cursor_at`, `cursor_after`) is `true`, lines before
(at, after) cursor line of all processed windows are forwarded to spotter
function. Otherwise, they don't. This allows control of jump "direction".
## Hooks~
Following hook functions can be used to further tweak jumping experience:
- `before_start` - called without arguments first thing when jump starts.
One of the possible use cases is to ask for user input and update spotter
function with it.
- `after_jump` - called after jump was actually done. Useful to make
post-adjustments (like move cursor to first non-whitespace character).
------------------------------------------------------------------------------
*MiniJump2d.start()*
`MiniJump2d.start`({opts})
Start jumping
Compute possible jump spots, visualize them and wait for iterative filtering.
First computation of possible jump spots~
- Process allowed windows (current and/or not current; controlled by
`allowed_windows` option) by visible lines from top to bottom. For each
one see if it is allowed (controlled by `allowed_lines` option). If not
allowed, then do nothing. If allowed and should be processed by
`spotter`, process it.
- Apply spotter function from `spotter` option for each appropriate line
and concatenate outputs. This means that eventual order of jump spots
aligns with lexicographical order within "window id" - "line number" -
"position in `spotter` output" tuples.
- For each possible jump compute its label: a single character from
`labels` option used to filter jump spots. Each possible label character
might be used more than once to label several "consecutive" jump spots.
It is done in an optimal way under assumption of no preference of one
spot over another. Basically, it means "use all labels at each step of
iterative filtering as equally as possible".
Visualization~
Current label for each possible jump spot is shown at that position
overriding everything underneath it.
Iterative filtering~
Labels of possible jump spots are computed in order to use them as equally
as possible.
Example:
- With `abc` as `labels` option, initial labels for 10 possible jumps
are "aaaabbbccc". As there are 10 spots which should be "coded" with 3
symbols, at least 2 symbols need 3 steps to filter them out. With current
implementation those are always the "first ones".
- After typing `a`, it filters first four jump spots and recomputes its
labels to be "aabc".
- After typing `a` again, it filters first two spots and recomputes its
labels to be "ab".
- After typing either `a` or `b` it filters single spot and makes jump.
With default 26 labels for most real-world cases 2 steps is enough for
default spotter function. Rarely 3 steps are needed with several windows.
Parameters~
{opts} `(table)` Configuration of jumping, overriding global and buffer
local values.config|. Has the same structure as |MiniJump2d.config|
without <mappings> field. Extra allowed fields:
- <hl_group> - which highlight group to use (default: "MiniJump2dSpot").
Usage~
- Start default jumping:
`MiniJump2d.start()`
- Jump to word start:
`MiniJump2d.start(MiniJump2d.builtin_opts.word_start)`
- Jump to single character from user input (follow by typing one character):
`MiniJump2d.start(MiniJump2d.builtin_opts.single_character)`
- Jump to first character of punctuation group only inside current window
which is placed at cursor line; visualize with 'hl-Search': >
MiniJump2d.start({
spotter = MiniJump2d.gen_pattern_spotter('%p+'),
allowed_lines = { cursor_before = false, cursor_after = false },
allowed_windows = { not_current = false },
hl_group = 'Search'
})
See also~
|MiniJump2d.config|
------------------------------------------------------------------------------
*MiniJump2d.stop()*
`MiniJump2d.stop`()
Stop jumping
------------------------------------------------------------------------------
*MiniJump2d.gen_pattern_spotter()*
`MiniJump2d.gen_pattern_spotter`({pattern}, {side})
Generate spotter for Lua pattern
Parameters~
{pattern} `(string|nil)` Lua pattern. Default: `'[^%s%p]+'` which matches group
of "non-whitespace non-punctuation characters" (basically a way of saying
"group of alphanumeric characters" that works with multibyte characters).
{side} `(string|nil)` Which side of pattern match should be considered as
jumping spot. Should be one of 'start' (start of match, default), 'end'
(inclusive end of match), or 'none' (match for spot is done manually
inside pattern with plain `()` matching group).
Usage~
- Match any punctuation:
`MiniJump2d.gen_pattern_spotter('%p')`
- Match first from line start non-whitespace character:
`MiniJump2d.gen_pattern_spotter('^%s*%S', 'end')`
- Match start of last word:
`MiniJump2d.gen_pattern_spotter('[^%s%p]+[%s%p]-$', 'start')`
- Match letter followed by another letter (example of manual matching
inside pattern):
`MiniJump2d.gen_pattern_spotter('%a()%a', 'none')`
------------------------------------------------------------------------------
*MiniJump2d.default_spotter*
`MiniJump2d.default_spotter`
Default spotter function
Spot is possible for jump if it is one of the following:
- Start or end of non-whitespace character group.
- Alphanumeric character followed or preceeded by punctuation (useful for
snake case names).
- Start of uppercase character group (useful for camel case names). Usually
only Lating alphabet is recognized due to Lua patterns shortcomings.
These rules are derived in an attempt to balance between two intentions:
- Allow as much useful jumping spots as possible.
- Make labeled jump spots easily distinguishable.
Usually takes from 2 to 3 keystrokes to get to destination.
------------------------------------------------------------------------------
*MiniJump2d.builtin_opts*
`MiniJump2d.builtin_opts`
Table with builtin `opts` values for |MiniJump2d.start()|
Each element of table is itself a table defining one or several options for
`MiniJump2d.start()`. Read help description to see which options it defines
(like in |MiniJump2d.builtin_opts.line_start|).
Usage~
Using |MiniJump2d.builtin_opts.line_start| as example:
- Command:
`:lua MiniJump2d.start(MiniJump2d.builtin_opts.line_start)`
- Custom mapping: >
vim.api.nvim_set_keymap(
'n', '<CR>',
'<Cmd>lua MiniJump2d.start(MiniJump2d.builtin_opts.line_start)<CR>', {}
)
- Inside |MiniJump2d.setup| (make sure to use all defined options): >
local jump2d = require('mini.jump2d')
local jump_line_start = jump2d.builtin_opts.line_start
jump2d.setup({
spotter = jump_line_start.spotter,
hooks = { after_jump = jump_line_start.hooks.after_jump }
})
<
------------------------------------------------------------------------------
*MiniJump2d.builtin_opts.default*
`MiniJump2d.builtin_opts.default`
Jump with |MiniJump2d.default_spotter()|
Defines `spotter`.
------------------------------------------------------------------------------
*MiniJump2d.builtin_opts.line_start*
`MiniJump2d.builtin_opts.line_start`
Jump to line start
Defines `spotter` and `hooks.after_jump`.
------------------------------------------------------------------------------
*MiniJump2d.builtin_opts.word_start*
`MiniJump2d.builtin_opts.word_start`
Jump to word start
Defines `spotter`.
------------------------------------------------------------------------------
*MiniJump2d.builtin_opts.single_character*
`MiniJump2d.builtin_opts.single_character`
Jump to single character taken from user input
Defines `spotter`, `allowed_lines.blank`, `allowed_lines.fold`, and
`hooks.before_start`.
------------------------------------------------------------------------------
*MiniJump2d.builtin_opts.query*
`MiniJump2d.builtin_opts.query`
Jump to query taken from user input
Defines `spotter`, `allowed_lines.blank`, `allowed_lines.fold`, and
`hooks.before_start`.
vim:tw=78:ts=8:noet:ft=help:norl:

View File

@ -0,0 +1,184 @@
==============================================================================
------------------------------------------------------------------------------
*mini.misc*
*MiniMisc*
Miscellaneous useful functions.
# Setup~
This module doesn't need setup, but it can be done to improve usability.
Setup with `require('mini.misc').setup({})` (replace `{}` with your
`config` table). It will create global Lua table `MiniMisc` which you can
use for scripting or manually (with `:lua MiniMisc.*`).
See |MiniMisc.config| for `config` structure and default values.
This module doesn't have runtime options, so using `vim.b.minimisc_config`
will have no effect here.
------------------------------------------------------------------------------
*MiniMisc.setup()*
`MiniMisc.setup`({config})
Module setup
Parameters~
{config} `(table)` Module config table. See |MiniMisc.config|.
Usage~
`require('mini.misc').setup({})` (replace `{}` with your `config` table)
------------------------------------------------------------------------------
*MiniMisc.config*
`MiniMisc.config`
Module config
Default values:
>
MiniMisc.config = {
-- Array of fields to make global (to be used as independent variables)
make_global = { 'put', 'put_text' },
}
<
------------------------------------------------------------------------------
*MiniMisc.bench_time()*
`MiniMisc.bench_time`({f}, {n}, {...})
Execute `f` several times and time how long it took
Parameters~
{f} `(function)` Function which execution to benchmark.
{n} `(number)` Number of times to execute `f(...)`. Default: 1.
{...} `(any)` Arguments when calling `f`.
Return~
`(...)` Table with durations (in seconds; up to microseconds) and
output of (last) function execution.
------------------------------------------------------------------------------
*MiniMisc.get_gutter_width()*
`MiniMisc.get_gutter_width`({win_id})
Compute width of gutter (info column on the left of the window)
Parameters~
{win_id} `(number)` Window identifier (see |win_getid()|) for which gutter
width is computed. Default: 0 for current.
------------------------------------------------------------------------------
*MiniMisc.put()*
`MiniMisc.put`({...})
Print Lua objects in command line
Parameters~
{...} `(any)` Any number of objects to be printed each on separate line.
------------------------------------------------------------------------------
*MiniMisc.put_text()*
`MiniMisc.put_text`({...})
Print Lua objects in current buffer
Parameters~
{...} `(any)` Any number of objects to be printed each on separate line.
------------------------------------------------------------------------------
*MiniMisc.resize_window()*
`MiniMisc.resize_window`({win_id}, {text_width})
Resize window to have exact number of editable columns
Parameters~
{win_id} `(number)` Window identifier (see |win_getid()|) to be resized.
Default: 0 for current.
{text_width} `(number)` Number of editable columns resized window will
display. Default: first element of 'colorcolumn' or otherwise 'textwidth'
(using screen width as its default but not more than 79).
------------------------------------------------------------------------------
*MiniMisc.stat_summary()*
`MiniMisc.stat_summary`({t})
Compute summary statistics of numerical array
This might be useful to compute summary of time benchmarking with
|MiniMisc.bench_time|.
Parameters~
{t} `(table)` Array (table suitable for `ipairs`) of numbers.
Return~
`(table)` Table with summary values under following keys (may be
extended in the future): <maximum>, <mean>, <median>, <minimum>, <n>
(number of elements), <sd> (sample standard deviation).
------------------------------------------------------------------------------
*MiniMisc.tbl_head()*
`MiniMisc.tbl_head`({t}, {n})
Return "first" elements of table as decided by `pairs`
Note: order of elements might vary.
Parameters~
{t} `(table)` Input table.
{n} `(number)` Maximum number of first elements. Default: 5.
Return~
`(table)` Table with at most `n` first elements of `t` (with same keys).
------------------------------------------------------------------------------
*MiniMisc.tbl_tail()*
`MiniMisc.tbl_tail`({t}, {n})
Return "last" elements of table as decided by `pairs`
This function makes two passes through elements of `t`:
- First to count number of elements.
- Second to construct result.
Note: order of elements might vary.
Parameters~
{t} `(table)` Input table.
{n} `(number)` Maximum number of last elements. Default: 5.
Return~
`(table)` Table with at most `n` last elements of `t` (with same keys).
------------------------------------------------------------------------------
*MiniMisc.use_nested_comments()*
`MiniMisc.use_nested_comments`({buf_id})
Add possibility of nested comment leader
This works by parsing 'commentstring' buffer option, extracting
non-whitespace comment leader (symbols on the left of commented line), and
locally modifying 'comments' option (by prepending `n:<leader>`). Does
nothing if 'commentstring' is empty or has comment symbols both in front
and back (like "/*%s*/").
Nested comment leader added with this function is useful for formatting
nested comments. For example, have in Lua "first-level" comments with '--'
and "second-level" comments with '----'. With nested comment leader second
type can be formatted with `gq` in the same way as first one.
Recommended usage is with |autocmd|:
`autocmd BufEnter * lua pcall(require('mini.misc').use_nested_comments)`
Note: for most filetypes 'commentstring' option is added only when buffer
with this filetype is entered, so using non-current `buf_id` can not lead
to desired effect.
Parameters~
{buf_id} `(number)` Buffer identifier (see |bufnr()|) in which function
will operate. Default: 0 for current.
------------------------------------------------------------------------------
*MiniMisc.zoom()*
`MiniMisc.zoom`({buf_id}, {config})
Zoom in and out of a buffer, making it full screen in a floating window
This function is useful when working with multiple windows but temporarily
needing to zoom into one to see more of the code from that buffer. Call it
again (without arguments) to zoom out.
Parameters~
{buf_id} `(number)` Buffer identifier (see |bufnr()|) to be zoomed.
Default: 0 for current.
{config} `(table)` Optional config for window (as for |nvim_open_win()|).
vim:tw=78:ts=8:noet:ft=help:norl:

View File

@ -0,0 +1,285 @@
==============================================================================
------------------------------------------------------------------------------
*mini.pairs*
*MiniPairs*
Minimal and fast autopairs.
Features:
- Functionality to work with 'paired' characters conditional on cursor's
neighborhood (two characters to its left and right).
- Usage should be through making appropriate mappings using |MiniPairs.map|
or in |MiniPairs.setup| (for global mapping), |MiniPairs.map_buf| (for
buffer mapping).
- Pairs get automatically registered to be recognized by `<BS>` and `<CR>`.
What it doesn't do:
- It doesn't support multiple characters as "open" and "close" symbols. Use
snippets for that.
- It doesn't support dependency on filetype. Use |i_CTRL-V| to insert
single symbol or `autocmd` command or 'after/ftplugin' approach to:
- `lua MiniPairs.map_buf(0, 'i', <*>, <pair_info>)` : make new mapping
for '<*>' in current buffer.
- `lua MiniPairs.unmap_buf(0, 'i', <*>, <pair>)`: unmap key `<*>` while
unregistering `<pair>` pair in current buffer. Note: this reverts
mapping done by |MiniPairs.map_buf|. If mapping was done with
|MiniPairs.map|, unmap for buffer in usual Neovim manner:
`inoremap <buffer> <*> <*>` (this maps `<*>` key to do the same it
does by default).
- Disable module for buffer (see 'Disabling' section).
# Setup~
This module needs a setup with `require('mini.pairs').setup({})`
(replace `{}` with your `config` table). It will create global Lua table
`MiniPairs` which you can use for scripting or manually (with
`:lua MiniPairs.*`).
See |MiniPairs.config| for `config` structure and default values.
This module doesn't have runtime options, so using `vim.b.minipairs_config`
will have no effect here.
# Example mappings~
- Register quotes inside `config` of |MiniPairs.setup|: >
mappings = {
['"'] = { register = { cr = true } },
["'"] = { register = { cr = true } },
}
<
- Insert `<>` pair if `<` is typed at line start, don't register for `<CR>`: >
lua MiniPairs.map('i', '<', { action = 'open', pair = '<>', neigh_pattern = '\r.', register = { cr = false } })
lua MiniPairs.map('i', '>', { action = 'close', pair = '<>', register = { cr = false } })
<
- Create symmetrical `$$` pair only in Tex files: >
au FileType tex lua MiniPairs.map_buf(0, 'i', '$', {action = 'closeopen', pair = '$$'})
<
# Notes~
- Make sure to make proper mapping of `<CR>` in order to support completion
plugin of your choice:
- For |MiniCompletion| see 'Helpful key mappings' section.
- For current implementation of "hrsh7th/nvim-cmp" there is no need to
make custom mapping. You can use default setup, which will confirm
completion selection if popup is visible and expand pair otherwise.
- Having mapping in terminal mode can conflict with:
- Autopairing capabilities of interpretators (`ipython`, `radian`).
- Vim mode of terminal itself.
# Disabling~
To disable, set `g:minipairs_disable` (globally) or `b:minipairs_disable`
(for a buffer) to `v:true`. Considering high number of different scenarios
and customization intentions, writing exact rules for disabling module's
functionality is left to user. See |mini.nvim-disabling-recipes| for common
recipes.
------------------------------------------------------------------------------
*MiniPairs.setup()*
`MiniPairs.setup`({config})
Module setup
Parameters~
{config} `(table)` Module config table. See |MiniPairs.config|.
Usage~
`require('mini.completion').setup({})` (replace `{}` with your `config` table)
------------------------------------------------------------------------------
*MiniPairs.config*
`MiniPairs.config`
Module config
Default values:
>
MiniPairs.config = {
-- In which modes mappings from this `config` should be created
modes = { insert = true, command = false, terminal = false },
-- Global mappings. Each right hand side should be a pair information, a
-- table with at least these fields (see more in |MiniPairs.map|):
-- - <action> - one of "open", "close", "closeopen".
-- - <pair> - two character string for pair to be used.
-- By default pair is not inserted after `\`, quotes are not recognized by
-- `<CR>`, `'` does not insert pair after a letter.
-- Only parts of tables can be tweaked (others will use these defaults).
mappings = {
['('] = { action = 'open', pair = '()', neigh_pattern = '[^\\].' },
['['] = { action = 'open', pair = '[]', neigh_pattern = '[^\\].' },
['{'] = { action = 'open', pair = '{}', neigh_pattern = '[^\\].' },
[')'] = { action = 'close', pair = '()', neigh_pattern = '[^\\].' },
[']'] = { action = 'close', pair = '[]', neigh_pattern = '[^\\].' },
['}'] = { action = 'close', pair = '{}', neigh_pattern = '[^\\].' },
['"'] = { action = 'closeopen', pair = '""', neigh_pattern = '[^\\].', register = { cr = false } },
["'"] = { action = 'closeopen', pair = "''", neigh_pattern = '[^%a\\].', register = { cr = false } },
['`'] = { action = 'closeopen', pair = '``', neigh_pattern = '[^\\].', register = { cr = false } },
},
}
<
------------------------------------------------------------------------------
*MiniPairs.map()*
`MiniPairs.map`({mode}, {lhs}, {pair_info}, {opts})
Make global mapping
This is a wrapper for |nvim_set_keymap()| but instead of right hand side of
mapping (as string) it expects table with pair information:
- `action` - one of "open" (for |MiniPairs.open|), "close" (for
|MiniPairs.close|), or "closeopen" (for |MiniPairs.closeopen|).
- `pair` - two character string to be used as argument for action function.
- `neigh_pattern` - optional 'two character' neighborhood pattern to be
used as argument for action function. Default: '..' (no restriction from
neighborhood).
- `register` - optional table with information about whether this pair
should be recognized by `<BS>` (in |MiniPairs.bs|) and/or `<CR>` (in
|MiniPairs.cr|). Should have boolean elements `bs` and `cr` which are
both `true` by default (if not overriden explicitly).
Using this function instead of |nvim_set_keymap()| allows automatic
registration of pairs which will be recognized by `<BS>` and `<CR>`.
For Neovim>=0.7 it also infers mapping description from `pair_info`.
Parameters~
{mode} `(string)` `mode` for |nvim_set_keymap()|.
{lhs} `(string)` `lhs` for |nvim_set_keymap()|.
{pair_info} `(table)` Table with pair information.
{opts} `(table)` Optional table `opts` for |nvim_set_keymap()|. Elements
`expr` and `noremap` won't be recognized (`true` by default).
------------------------------------------------------------------------------
*MiniPairs.map_buf()*
`MiniPairs.map_buf`({buffer}, {mode}, {lhs}, {pair_info}, {opts})
Make buffer mapping
This is a wrapper for |nvim_buf_set_keymap()| but instead of string right
hand side of mapping it expects table with pair information similar to one
in |MiniPairs.map|.
Using this function instead of |nvim_buf_set_keymap()| allows automatic
registration of pairs which will be recognized by `<BS>` and `<CR>`.
For Neovim>=0.7 it also infers mapping description from `pair_info`.
Parameters~
{buffer} `(number)` `buffer` for |nvim_buf_set_keymap()|.
{mode} `(string)` `mode` for |nvim_buf_set_keymap()|.
{lhs} `(string)` `lhs` for |nvim_buf_set_keymap()|.
{pair_info} `(table)` Table with pair information.
{opts} `(table)` Optional table `opts` for |nvim_buf_set_keymap()|.
Elements `expr` and `noremap` won't be recognized (`true` by default).
------------------------------------------------------------------------------
*MiniPairs.unmap()*
`MiniPairs.unmap`({mode}, {lhs}, {pair})
Remove global mapping
A wrapper for |nvim_del_keymap()| which registers supplied `pair`.
Parameters~
{mode} `(string)` `mode` for |nvim_del_keymap()|.
{lhs} `(string)` `lhs` for |nvim_del_keymap()|.
{pair} `(string)` Pair which should be unregistered from both
`<BS>` and `<CR>`. Should be explicitly supplied to avoid confusion.
Supply `''` to not unregister pair.
------------------------------------------------------------------------------
*MiniPairs.unmap_buf()*
`MiniPairs.unmap_buf`({buffer}, {mode}, {lhs}, {pair})
Remove buffer mapping
Wrapper for |nvim_buf_del_keymap()| which also unregisters supplied `pair`.
Note: this only reverts mapping done by |MiniPairs.map_buf|. If mapping was
done with |MiniPairs.map|, unmap for buffer in usual Neovim manner:
`inoremap <buffer> <*> <*>` (this maps `<*>` key to do the same it does by
default).
Parameters~
{buffer} `(number)` `buffer` for |nvim_buf_del_keymap()|.
{mode} `(string)` `mode` for |nvim_buf_del_keymap()|.
{lhs} `(string)` `lhs` for |nvim_buf_del_keymap()|.
{pair} `(string)` Pair which should be unregistered from both
`<BS>` and `<CR>`. Should be explicitly supplied to avoid confusion.
Supply `''` to not unregister pair.
------------------------------------------------------------------------------
*MiniPairs.open()*
`MiniPairs.open`({pair}, {neigh_pattern})
Process "open" symbols
Used as |map-expr| mapping for "open" symbols in asymmetric pair ('(', '[',
etc.). If neighborhood doesn't match supplied pattern, function results
into "open" symbol. Otherwise, it pastes whole pair and moves inside pair
with |<Left>|.
Used inside |MiniPairs.map| and |MiniPairs.map_buf| for an actual mapping.
Parameters~
{pair} `(string)` String with two characters representing pair.
{neigh_pattern} `(string)` Pattern for two neighborhood characters ("\r" line
start, "\n" - line end).
------------------------------------------------------------------------------
*MiniPairs.close()*
`MiniPairs.close`({pair}, {neigh_pattern})
Process "close" symbols
Used as |map-expr| mapping for "close" symbols in asymmetric pair (')',
']', etc.). If neighborhood doesn't match supplied pattern, function
results into "close" symbol. Otherwise it jumps over symbol to the right of
cursor (with |<Right>|) if it is equal to "close" one and inserts it
otherwise.
Used inside |MiniPairs.map| and |MiniPairs.map_buf| for an actual mapping.
Parameters~
{pair} `(string)` String with two characters representing pair.
{neigh_pattern} `(string)` Pattern for two neighborhood characters ("\r" line
start, "\n" - line end).
------------------------------------------------------------------------------
*MiniPairs.closeopen()*
`MiniPairs.closeopen`({pair}, {neigh_pattern})
Process "closeopen" symbols
Used as |map-expr| mapping for 'symmetrical' symbols (from pairs '""',
'\'\'', '``'). It tries to perform 'closeopen action': move over right
character (with |<Right>|) if it is equal to second character from pair or
conditionally paste pair otherwise (with |MiniPairs.open()|).
Used inside |MiniPairs.map| and |MiniPairs.map_buf| for an actual mapping.
Parameters~
{pair} `(string)` String with two characters representing pair.
{neigh_pattern} `(string)` Pattern for two neighborhood characters ("\r" line
start, "\n" - line end).
------------------------------------------------------------------------------
*MiniPairs.bs()*
`MiniPairs.bs`()
Process |<BS>|
Used as |map-expr| mapping for `<BS>`. It removes whole pair (via
`<BS><Del>`) if neighborhood is equal to a whole pair recognized for
current buffer. Pair is recognized for current buffer if it is registered
for global or current buffer mapping. Pair is registered as a result of
calling |MiniPairs.map| or |MiniPairs.map_buf|.
Mapped by default inside |MiniPairs.setup|.
------------------------------------------------------------------------------
*MiniPairs.cr()*
`MiniPairs.cr`()
Process |i_<CR>|
Used as |map-expr| mapping for `<CR>` in insert mode. It puts "close"
symbol on next line (via `<CR><C-o>O`) if neighborhood is equal to a whole
pair recognized for current buffer. Pair is recognized for current buffer
if it is registered for global or current buffer mapping. Pair is
registered as a result of calling |MiniPairs.map| or |MiniPairs.map_buf|.
Mapped by default inside |MiniPairs.setup|.
vim:tw=78:ts=8:noet:ft=help:norl:

View File

@ -0,0 +1,220 @@
==============================================================================
------------------------------------------------------------------------------
*mini.sessions*
*MiniSessions*
Session management (read, write, delete), which works using |mksession|
(meaning 'sessionoptions' is fully respected). This is intended as a
drop-in Lua replacement for session management part of 'mhinz/vim-startify'
(works out of the box with sessions created by it). Implements both global
(from configured directory) and local (from current directory) sessions.
Key design ideas:
- Sessions are represented by readable files (results of applying
|mksession|). There are two kinds of sessions:
- Global: any file inside a configurable directory.
- Local: configurable file inside current working directory (|getcwd|).
- All session files are detected during `MiniSessions.setup()` with session
names being file names (including their possible extension).
- Store information about detected sessions in separate table
(|MiniSessions.detected|) and operate only on it. Meaning if this
information changes, there will be no effect until next detection. So to
avoid confusion, don't directly use |mksession| and |source| for writing
and reading sessions files.
Features:
- Autoread default session (local if detected, latest otherwise) if Neovim
was called without intention to show something else.
- Autowrite current session before quitting Neovim.
- Configurable severity level of all actions.
# Setup~
This module needs a setup with `require('mini.sessions').setup({})`
(replace `{}` with your `config` table). It will create global Lua table
`MiniSessions` which you can use for scripting or manually (with
`:lua MiniSessions.*`).
See |MiniSessions.config| for `config` structure and default values.
This module doesn't benefit from buffer local configuration, so using
`vim.b.minimisc_config` will have no effect here.
# Disabling~
To disable core functionality, set `g:minisessions_disable` (globally) or
`b:minisessions_disable` (for a buffer) to `v:true`. Considering high
number of different scenarios and customization intentions, writing exact
rules for disabling module's functionality is left to user. See
|mini.nvim-disabling-recipes| for common recipes.
------------------------------------------------------------------------------
*MiniSessions.setup()*
`MiniSessions.setup`({config})
Module setup
Parameters~
{config} `(table)` Module config table. See |MiniSessions.config|.
Usage~
`require('mini.sessions').setup({})` (replace `{}` with your `config` table)
------------------------------------------------------------------------------
*MiniSessions.config*
`MiniSessions.config`
Module config
Default values:
>
MiniSessions.config = {
-- Whether to read latest session if Neovim opened without file arguments
autoread = false,
-- Whether to write current session before quitting Neovim
autowrite = true,
-- Directory where global sessions are stored (use `''` to disable)
directory = --<"session" subdir of user data directory from |stdpath()|>,
-- File for local session (use `''` to disable)
file = 'Session.vim',
-- Whether to force possibly harmful actions (meaning depends on function)
force = { read = false, write = true, delete = false },
-- Hook functions for actions. Default `nil` means 'do nothing'.
-- Takes table with active session data as argument.
hooks = {
-- Before successful action
pre = { read = nil, write = nil, delete = nil },
-- After successful action
post = { read = nil, write = nil, delete = nil },
},
-- Whether to print session path after action
verbose = { read = false, write = true, delete = true },
}
<
------------------------------------------------------------------------------
*MiniSessions.detected*
`MiniSessions.detected`
Table of detected sessions. Keys represent session name. Values are tables
with session information that currently has these fields (but subject to
change):
- <modify_time> `(number)` modification time (see |getftime|) of session file.
- <name> `(string)` name of session (should be equal to table key).
- <path> `(string)` full path to session file.
- <type> `(string)` type of session ('global' or 'local').
------------------------------------------------------------------------------
*MiniSessions.read()*
`MiniSessions.read`({session_name}, {opts})
Read detected session
What it does:
- Delete all current buffers with |bwipeout|. This is needed to correctly
restore buffers from target session. If `force` is not `true`, checks
beforehand for unsaved listed buffers and stops if there is any.
- Source session with supplied name.
Parameters~
{session_name} `(string)` Name of detected session file to read. Default:
`nil` for default session: local (if detected) or latest session (see
|MiniSessions.get_latest|).
{opts} `(table)` Table with options. Current allowed keys:
- <force> (whether to delete unsaved buffers; default:
`MiniSessions.config.force.read`).
- <verbose> (whether to print session path after action; default
`MiniSessions.config.verbose.read`).
- <hooks> (a table with <pre> and <post> function hooks to be executed
with session data argument before and after successful read; overrides
`MiniSessions.config.hooks.pre.read` and
`MiniSessions.config.hooks.post.read`).
------------------------------------------------------------------------------
*MiniSessions.write()*
`MiniSessions.write`({session_name}, {opts})
Write session
What it does:
- Check if file for supplied session name already exists. If it does and
`force` is not `true`, then stop.
- Write session with |mksession| to a file named `session_name`. Its
directory is determined based on type of session:
- It is at location |v:this_session| if `session_name` is `nil` and
there is current session.
- It is current working directory (|getcwd|) if `session_name` is equal
to `MiniSessions.config.file` (represents local session).
- It is `MiniSessions.config.directory` otherwise (represents global
session).
- Update |MiniSessions.detected|.
Parameters~
{session_name} `(string)` Name of session file to write. Default: `nil` for
current session (|v:this_session|).
{opts} `(table)` Table with options. Current allowed keys:
- <force> (whether to ignore existence of session file; default:
`MiniSessions.config.force.write`).
- <verbose> (whether to print session path after action; default
`MiniSessions.config.verbose.write`).
- <hooks> (a table with <pre> and <post> function hooks to be executed
with session data argument before and after successful write; overrides
`MiniSessions.config.hooks.pre.write` and
`MiniSessions.config.hooks.post.write`).
------------------------------------------------------------------------------
*MiniSessions.delete()*
`MiniSessions.delete`({session_name}, {opts})
Delete detected session
What it does:
- Check if session name is a current one. If yes and `force` is not `true`,
then stop.
- Delete session.
- Update |MiniSessions.detected|.
Parameters~
{session_name} `(string)` Name of detected session file to delete. Default:
`nil` for name of current session (taken from |v:this_session|).
{opts} `(table)` Table with options. Current allowed keys:
- <force> (whether to allow deletion of current session; default:
`MiniSessions.config.force.delete`).
- <verbose> (whether to print session path after action; default
`MiniSessions.config.verbose.delete`).
- <hooks> (a table with <pre> and <post> function hooks to be executed
with session data argument before and after successful delete; overrides
`MiniSessions.config.hooks.pre.delete` and
`MiniSessions.config.hooks.post.delete`).
------------------------------------------------------------------------------
*MiniSessions.select()*
`MiniSessions.select`({action}, {opts})
Select session interactively and perform action
Note: this uses |vim.ui.select| function, which is present in Neovim
starting from 0.6 version. For more user-friendly experience, override it
(for example, with external plugins like "stevearc/dressing.nvim").
Parameters~
{action} `(string)` Action to perform. Should be one of "read" (default),
"write", or "delete".
{opts} `(table)` Options for specified action.
------------------------------------------------------------------------------
*MiniSessions.get_latest()*
`MiniSessions.get_latest`()
Get name of latest detected session
Latest session is the session with the latest modification time determined
by |getftime|.
Return~
`(string|nil)` Name of latest session or `nil` if there is no sessions.
------------------------------------------------------------------------------
*MiniSessions.on_vimenter()*
`MiniSessions.on_vimenter`()
Act on |VimEnter|
vim:tw=78:ts=8:noet:ft=help:norl:

View File

@ -0,0 +1,596 @@
==============================================================================
------------------------------------------------------------------------------
*mini.starter*
*MiniStarter*
Fast and flexible start screen. Displayed items are fully customizable both
in terms of what they do and how they look (with reasonable defaults). Item
selection can be done using prefix query with instant visual feedback.
Key design ideas:
- All available actions are defined inside items. Each item should have the
following info:
- <action> - function or string for |vim.cmd| which is executed when
item is chosen. Empty string result in placeholder "inactive" item.
- <name> - string which will be displayed and used for choosing.
- <section> - string representing to which section item belongs.
There are pre-configured whole sections in |MiniStarter.sections|.
- Configure what items are displayed by supplying an array which can be
normalized to an array of items. Read about how supplied items are
normalized in |MiniStarter.refresh|.
- Modify the final look by supplying content hooks: functions which take
buffer content as input (see |MiniStarter.get_content()| for more
information) and return buffer content as output. There are
pre-configured content hook generators in |MiniStarter.gen_hook|.
- Choosing an item can be done in two ways:
- Type prefix query to filter item by matching its name (ignoring
case). Displayed information is updated after every typed character.
For every item its unique prefix is highlighted.
- Use Up/Down arrows and hit Enter.
- Allow multiple simultaneously open Starter buffers.
What is doesn't do:
- It doesn't support fuzzy query for items. And probably will never do.
# Setup~
This module needs a setup with `require('mini.starter').setup({})`
(replace `{}` with your `config` table). It will create global Lua table
`MiniStarter` which you can use for scripting or manually (with
`:lua MiniStarter.*`).
See |MiniStarter.config| for `config` structure and default values. For
some configuration examples (including one similar to 'vim-startify' and
'dashboard-nvim'), see |MiniStarter-example-config|.
You can override runtime config settings locally to buffer inside
`vim.b.ministarter_config` which should have same structure as
`MiniStarter.config`. See |mini.nvim-buffer-local-config| for more details.
Note: `vim.b.ministarter_config` is copied to Starter buffer from current
buffer allowing full customization.
# Highlight groups~
* `MiniStarterCurrent` - current item.
* `MiniStarterFooter` - footer units.
* `MiniStarterHeader` - header units.
* `MiniStarterInactive` - inactive item.
* `MiniStarterItem` - item name.
* `MiniStarterItemBullet` - units from |MiniStarter.gen_hook.adding_bullet|.
* `MiniStarterItemPrefix` - unique query for item.
* `MiniStarterSection` - section units.
* `MiniStarterQuery` - current query in active items.
To change any highlight group, modify it directly with |:highlight|.
# Disabling~
To disable core functionality, set `g:ministarter_disable` (globally) or
`b:ministarter_disable` (for a buffer) to `v:true`. Considering high number
of different scenarios and customization intentions, writing exact rules
for disabling module's functionality is left to user. See
|mini.nvim-disabling-recipes| for common recipes.
------------------------------------------------------------------------------
*MiniStarter-example-config*
Example configurations
Configuration similar to 'mhinz/vim-startify':
>
local starter = require('mini.starter')
starter.setup({
evaluate_single = true,
items = {
starter.sections.builtin_actions(),
starter.sections.recent_files(10, false),
starter.sections.recent_files(10, true),
-- Use this if you set up 'mini.sessions'
starter.sections.sessions(5, true)
},
content_hooks = {
starter.gen_hook.adding_bullet(),
starter.gen_hook.indexing('all', { 'Builtin actions' }),
starter.gen_hook.padding(3, 2),
},
})
<
Configuration similar to 'glepnir/dashboard-nvim':
>
local starter = require('mini.starter')
starter.setup({
items = {
starter.sections.telescope(),
},
content_hooks = {
starter.gen_hook.adding_bullet(),
starter.gen_hook.aligning('center', 'center'),
},
})
<
Elaborated configuration showing capabilities of custom items,
header/footer, and content hooks:
>
local my_items = {
{ name = 'Echo random number', action = 'lua print(math.random())', section = 'Section 1' },
function()
return {
{ name = 'Item #1 from function', action = [[echo 'Item #1']], section = 'From function' },
{ name = 'Placeholder (always incative) item', action = '', section = 'From function' },
function()
return {
name = 'Item #1 from double function',
action = [[echo 'Double function']],
section = 'From double function',
}
end,
}
end,
{ name = [[Another item in 'Section 1']], action = 'lua print(math.random() + 10)', section = 'Section 1' },
}
local footer_n_seconds = (function()
local timer = vim.loop.new_timer()
local n_seconds = 0
timer:start(0, 1000, vim.schedule_wrap(function()
if vim.api.nvim_buf_get_option(0, 'filetype') ~= 'starter' then
timer:stop()
return
end
n_seconds = n_seconds + 1
MiniStarter.refresh()
end))
return function()
return 'Number of seconds since opening: ' .. n_seconds
end
end)()
local hook_top_pad_10 = function(content)
-- Pad from top
for _ = 1, 10 do
-- Insert at start a line with single content unit
table.insert(content, 1, { { type = 'empty', string = '' } })
end
return content
end
local starter = require('mini.starter')
starter.setup({
items = my_items,
footer = footer_n_seconds,
content_hooks = { hook_top_pad_10 },
})
<
------------------------------------------------------------------------------
*MiniStarter-lifecycle*
# Lifecycle of Starter buffer~
- Open with |MiniStarter.open()|. It includes creating buffer with
appropriate options, mappings, behavior; call to |MiniStarter.refresh()|;
issue `MiniStarterOpened` |User| event.
- Wait for user to choose an item. This is done using following logic:
- Typing any character from `MiniStarter.config.query_updaters` leads
to updating query. Read more in |MiniStarter.add_to_query|.
- <BS> deletes latest character from query.
- <Down>/<Up>, <C-n>/<C-p>, <M-j>/<M-k> move current item.
- <CR> executes action of current item.
- <C-c> closes Starter buffer.
- Evaluate current item when appropriate (after `<CR>` or when there is a
single item and `MiniStarter.config.evaluate_single` is `true`). This
executes item's `action`.
------------------------------------------------------------------------------
*MiniStarter.setup()*
`MiniStarter.setup`({config})
Module setup
Parameters~
{config} `(table)` Module config table. See |MiniStarter.config|.
Usage~
`require('mini.starter').setup({})` (replace `{}` with your `config` table)
------------------------------------------------------------------------------
*MiniStarter.config*
`MiniStarter.config`
Module config
Default values:
>
MiniStarter.config = {
-- Whether to open starter buffer on VimEnter. Not opened if Neovim was
-- started with intent to show something else.
autoopen = true,
-- Whether to evaluate action of single active item
evaluate_single = false,
-- Items to be displayed. Should be an array with the following elements:
-- - Item: table with <action>, <name>, and <section> keys.
-- - Function: should return one of these three categories.
-- - Array: elements of these three types (i.e. item, array, function).
-- If `nil` (default), default items will be used (see |mini.starter|).
items = nil,
-- Header to be displayed before items. Converted to single string via
-- `tostring` (use `\n` to display several lines). If function, it is
-- evaluated first. If `nil` (default), polite greeting will be used.
header = nil,
-- Footer to be displayed after items. Converted to single string via
-- `tostring` (use `\n` to display several lines). If function, it is
-- evaluated first. If `nil` (default), default usage help will be shown.
footer = nil,
-- Array of functions to be applied consecutively to initial content.
-- Each function should take and return content for 'Starter' buffer (see
-- |mini.starter| and |MiniStarter.get_content()| for more details).
content_hooks = nil,
-- Characters to update query. Each character will have special buffer
-- mapping overriding your global ones. Be careful to not add `:` as it
-- allows you to go into command mode.
query_updaters = 'abcdefghijklmnopqrstuvwxyz0123456789_-.',
}
<
------------------------------------------------------------------------------
*MiniStarter.on_vimenter()*
`MiniStarter.on_vimenter`()
Act on |VimEnter|.
------------------------------------------------------------------------------
*MiniStarter.open()*
`MiniStarter.open`({buf_id})
Open Starter buffer
- Create buffer if necessary and move into it.
- Set buffer options. Note that settings are done with |noautocmd| to
achieve a massive speedup.
- Set buffer mappings. Besides basic mappings (described inside "Lifecycle
of Starter buffer" of |mini.starter|), map every character from
`MiniStarter.config.query_updaters` to add itself to query with
|MiniStarter.add_to_query|.
- Populate buffer with |MiniStarter.refresh|.
- Issue custom `MiniStarterOpened` event to allow acting upon opening
Starter buffer. Use it with
`autocmd User MiniStarterOpened <your command>`.
Note: to fully use it in autocommand, it is recommended to utilize
|autocmd-nested|. Example:
`autocmd TabNewEntered * ++nested lua MiniStarter.open()`
Parameters~
{buf_id} `(number)` Identifier of existing valid buffer (see |bufnr()|) to
open inside. Default: create a new one.
------------------------------------------------------------------------------
*MiniStarter.refresh()*
`MiniStarter.refresh`({buf_id})
Refresh Starter buffer
- Normalize `MiniStarter.config.items`:
- Flatten: recursively (in depth-first fashion) parse its elements. If
function is found, execute it and continue with parsing its output
(this allows deferring item collection up until it is actually
needed). If proper item is found (table with fields `action`,
`name`, `section`), add it to output.
- Sort: order first by section and then by item id (both in order of
appearance).
- Normalize `MiniStarter.config.header` and `MiniStarter.config.footer` to
be multiple lines by splitting at `\n`. If function - evaluate it first.
- Make initial buffer content (see |MiniStarter.get_content()| for a
description of what a buffer content is). It consist from content lines
with single content unit:
- First lines contain strings of normalized header.
- Body is for normalized items. Section names have own lines preceded
by empty line.
- Last lines contain separate strings of normalized footer.
- Sequentially apply hooks from `MiniStarter.config.content_hooks` to
content. Output of one hook serves as input to the next.
- Gather final items from content with |MiniStarter.content_to_items|.
- Convert content to buffer lines with |MiniStarter.content_to_lines| and
add them to buffer.
- Add highlighting of content units.
- Position cursor.
- Make current query. This results into some items being marked as
"inactive" and updating highlighting of current query on "active" items.
Note: this function is executed on every |VimResized| to allow more
responsive behavior.
Parameters~
{buf_id} `(number|nil)` Buffer identifier of a valid Starter buffer.
Default: current buffer.
------------------------------------------------------------------------------
*MiniStarter.close()*
`MiniStarter.close`({buf_id})
Close Starter buffer
Parameters~
{buf_id} `(number|nil)` Buffer identifier of a valid Starter buffer.
Default: current buffer.
------------------------------------------------------------------------------
*MiniStarter.sections*
`MiniStarter.sections`
Table of pre-configured sections
------------------------------------------------------------------------------
*MiniStarter.sections.builtin_actions()*
`MiniStarter.sections.builtin_actions`()
Section with builtin actions
Return~
`(table)` Array of items.
------------------------------------------------------------------------------
*MiniStarter.sections.sessions()*
`MiniStarter.sections.sessions`({n}, {recent})
Section with |MiniSessions| sessions
Sessions are taken from |MiniSessions.detected|. Notes:
- If it shows "'mini.sessions' is not set up", it means that you didn't
call `require('mini.sessions').setup()`.
- If it shows "There are no detected sessions in 'mini.sessions'", it means
that there are no sessions at the current sessions directory. Either
create session or supply different directory where session files are
stored (see |MiniSessions.setup|).
- Local session (if detected) is always displayed first.
Parameters~
{n} `(number)` Number of returned items. Default: 5.
{recent} `(boolean)` Whether to use recent sessions (instead of
alphabetically by name). Default: true.
Return~
`(function)` Function which returns array of items.
------------------------------------------------------------------------------
*MiniStarter.sections.recent_files()*
`MiniStarter.sections.recent_files`({n}, {current_dir}, {show_path})
Section with most recently used files
Files are taken from |vim.v.oldfiles|.
Parameters~
{n} `(number)` Number of returned items. Default: 5.
{current_dir} `(boolean)` Whether to return files only from current working
directory. Default: `false`.
{show_path} `(boolean)` Whether to append file name with its full path.
Default: `true`.
Return~
`(function)` Function which returns array of items.
------------------------------------------------------------------------------
*MiniStarter.sections.telescope()*
`MiniStarter.sections.telescope`()
Section with basic Telescope pickers relevant to start screen
Return~
`(function)` Function which returns array of items.
------------------------------------------------------------------------------
*MiniStarter.gen_hook*
`MiniStarter.gen_hook`
Table with pre-configured content hook generators
Each element is a function which returns content hook. So to use them
inside |MiniStarter.setup|, call them.
------------------------------------------------------------------------------
*MiniStarter.gen_hook.padding()*
`MiniStarter.gen_hook.padding`({left}, {top})
Hook generator for padding
Output is a content hook which adds constant padding from left and top.
This allows tweaking the screen position of buffer content.
Parameters~
{left} `(number)` Number of empty spaces to add to start of each content
line. Default: 0.
{top} `(number)` Number of empty lines to add to start of content.
Default: 0.
Return~
`(function)` Content hook.
------------------------------------------------------------------------------
*MiniStarter.gen_hook.adding_bullet()*
`MiniStarter.gen_hook.adding_bullet`({bullet}, {place_cursor})
Hook generator for adding bullet to items
Output is a content hook which adds supplied string to be displayed to the
left of item.
Parameters~
{bullet} `(string)` String to be placed to the left of item name.
Default: "░ ".
{place_cursor} `(boolean)` Whether to place cursor on the first character
of bullet when corresponding item becomes current. Default: true.
Return~
`(function)` Content hook.
------------------------------------------------------------------------------
*MiniStarter.gen_hook.indexing()*
`MiniStarter.gen_hook.indexing`({grouping}, {exclude_sections})
Hook generator for indexing items
Output is a content hook which adds unique index to the start of item's
name. It results into shortening queries required to choose an item (at
expense of clarity).
Parameters~
{grouping} `(string)` One of "all" (number indexing across all sections) or
"section" (letter-number indexing within each section). Default: "all".
{exclude_sections} `(table)` Array of section names (values of `section`
element of item) for which index won't be added. Default: `{}`.
Return~
`(function)` Content hook.
------------------------------------------------------------------------------
*MiniStarter.gen_hook.aligning()*
`MiniStarter.gen_hook.aligning`({horizontal}, {vertical})
Hook generator for aligning content
Output is a content hook which independently aligns content horizontally
and vertically. Basically, this computes left and top pads for
|MiniStarter.gen_hook.padding| such that output lines would appear aligned
in certain way.
Parameters~
{horizontal} `(string)` One of "left", "center", "right". Default: "left".
{vertical} `(string)` One of "top", "center", "bottom". Default: "top".
Return~
`(function)` Content hook.
------------------------------------------------------------------------------
*MiniStarter.get_content()*
`MiniStarter.get_content`({buf_id})
Get content of Starter buffer
Generally, buffer content is a table in the form of "2d array" (or rather
"2d list" because number of elements can differ):
- Each element represents content line: an array with content units to be
displayed in one buffer line.
- Each content unit is a table with at least the following elements:
- "type" - string with type of content. Something like "item",
"section", "header", "footer", "empty", etc.
- "string" - which string should be displayed. May be an empty string.
- "hl" - which highlighting should be applied to content string. May be
`nil` for no highlighting.
See |MiniStarter.content_to_lines| for converting content to buffer lines
and |MiniStarter.content_to_items| - to list of parsed items.
Notes:
- Content units with type "item" also have `item` element with all
information about an item it represents. Those elements are used directly
to create an array of items used for query.
Parameters~
{buf_id} `(number|nil)` Buffer identifier of a valid Starter buffer.
Default: current buffer.
------------------------------------------------------------------------------
*MiniStarter.content_coords()*
`MiniStarter.content_coords`({content}, {predicate})
Helper to iterate through content
Basically, this traverses content "2d array" (in depth-first fashion; top
to bottom, left to right) and returns "coordinates" of units for which
`predicate` is true-ish.
Parameters~
{content} `(table)` Content "2d array". Default: content of current buffer.
{predicate} `(function|string|nil)` Predictate to filter units. If it is:
- Function, then it is evaluated with unit as input.
- String, then it checks unit to have this type (allows easy getting of
units with some type).
- `nil`, all units are kept.
Return~
`(table)` Array of resulting units' coordinates. Each coordinate is a
table with <line> and <unit> keys. To retrieve actual unit from coordinate
`c`, use `content[c.line][c.unit]`.
------------------------------------------------------------------------------
*MiniStarter.content_to_lines()*
`MiniStarter.content_to_lines`({content})
Convert content to buffer lines
One buffer line is made by concatenating `string` element of units within
same content line.
Parameters~
{content} `(table)` Content "2d array". Default: content of current buffer.
Return~
`(table)` Array of strings for each buffer line.
------------------------------------------------------------------------------
*MiniStarter.content_to_items()*
`MiniStarter.content_to_items`({content})
Convert content to items
Parse content (in depth-first fashion) and retrieve each item from `item`
element of content units with type "item". This also:
- Computes some helper information about how item will be actually
displayed (after |MiniStarter.content_to_lines|) and minimum number of
prefix characters needed for a particular item to be queried single.
- Modifies item's `name` element taking it from corresponing `string`
element of content unit. This allows modifying item's `name` at the stage
of content hooks (like, for example, in |MiniStarter.gen_hook.indexing|).
Parameters~
{content} `(table)` Content "2d array". Default: content of current buffer.
Return~
`(table)` Array of items.
------------------------------------------------------------------------------
*MiniStarter.eval_current_item()*
`MiniStarter.eval_current_item`({buf_id})
Evaluate current item
Note that it resets current query before evaluation, as it is rarely needed
any more.
Parameters~
{buf_id} `(number|nil)` Buffer identifier of a valid Starter buffer.
Default: current buffer.
------------------------------------------------------------------------------
*MiniStarter.update_current_item()*
`MiniStarter.update_current_item`({direction}, {buf_id})
Update current item
This makes next (with respect to `direction`) active item to be current.
Parameters~
{direction} `(string)` One of "next" or "previous".
{buf_id} `(number|nil)` Buffer identifier of a valid Starter buffer.
Default: current buffer.
------------------------------------------------------------------------------
*MiniStarter.add_to_query()*
`MiniStarter.add_to_query`({char}, {buf_id})
Add character to current query
- Update current query by appending `char` to its end (only if it results
into at least one active item) or delete latest character if `char` is `nil`.
- Recompute status of items: "active" if its name starts with new query,
"inactive" otherwise.
- Update highlighting: whole strings for "inactive" items, current query
for "active" items.
Parameters~
{char} `(string)` Single character to be added to query. If `nil`, deletes
latest character from query.
{buf_id} `(number|nil)` Buffer identifier of a valid Starter buffer.
Default: current buffer.
------------------------------------------------------------------------------
*MiniStarter.set_query()*
`MiniStarter.set_query`({query}, {buf_id})
Set current query
Parameters~
{query} `(string|nil)` Query to be set (only if it results into at least one
active item). Default: `nil` for setting query to empty string, which
essentially resets query.
{buf_id} `(number|nil)` Buffer identifier of a valid Starter buffer.
Default: current buffer.
------------------------------------------------------------------------------
*MiniStarter.on_cursormoved()*
`MiniStarter.on_cursormoved`({buf_id})
Act on |CursorMoved| by repositioning cursor in fixed place.
vim:tw=78:ts=8:noet:ft=help:norl:

View File

@ -0,0 +1,308 @@
==============================================================================
------------------------------------------------------------------------------
*mini.statusline*
*MiniStatusline*
Minimal and fast statusline with opinionated default look.
Features:
- Define own custom statusline structure for active and inactive windows.
This is done with a function which should return string appropriate for
|statusline|. Its code should be similar to default one with structure:
- Compute string data for every section you want to be displayed.
- Combine them in groups with |MiniStatusline.combine_groups()|.
- Built-in active mode indicator with colors.
- Sections can hide information when window is too narrow (specific window
width is configurable per section).
# Dependencies~
Suggested dependencies (provide extra functionality, statusline will work
without them):
- Nerd font (to support extra icons).
- Plugin 'lewis6991/gitsigns.nvim' for Git information in
|MiniStatusline.section_git|. If missing, no section will be shown.
- Plugin 'kyazdani42/nvim-web-devicons' for filetype icons in
`MiniStatusline.section_fileinfo`. If missing, no icons will be shown.
# Setup~
This module needs a setup with `require('mini.statusline').setup({})`
(replace `{}` with your `config` table). It will create global Lua table
`MiniStatusline` which you can use for scripting or manually (with
`:lua MiniStatusline.*`).
See |MiniStatusline.config| for `config` structure and default values. For
some content examples, see |MiniStatusline-example-content|.
You can override runtime config settings locally to buffer inside
`vim.b.ministatusline_config` which should have same structure as
`MiniStatusline.config`. See |mini.nvim-buffer-local-config| for more details.
# Highlight groups~
Highlight depending on mode (second output from |MiniStatusline.section_mode|):
* `MiniStatuslineModeNormal` - Normal mode.
* `MiniStatuslineModeInsert` - Insert mode.
* `MiniStatuslineModeVisual` - Visual mode.
* `MiniStatuslineModeReplace` - Replace mode.
* `MiniStatuslineModeCommand` - Command mode.
* `MiniStatuslineModeOther` - other modes (like Terminal, etc.).
Highlight used in default statusline:
* `MiniStatuslineDevinfo` - for "dev info" group
(|MiniStatusline.section_git| and |MiniStatusline.section_diagnostics|).
* `MiniStatuslineFilename` - for |MiniStatusline.section_filename| section.
* `MiniStatuslineFileinfo` - for |MiniStatusline.section_fileinfo| section.
Other groups:
* `MiniStatuslineInactive` - highliting in not focused window.
To change any highlight group, modify it directly with |:highlight|.
# Disabling~
To disable (show empty statusline), set `g:ministatusline_disable`
(globally) or `b:ministatusline_disable` (for a buffer) to `v:true`.
Considering high number of different scenarios and customization
intentions, writing exact rules for disabling module's functionality is
left to user. See |mini.nvim-disabling-recipes| for common recipes.
------------------------------------------------------------------------------
*MiniStatusline-example-content*
Example content
# Default content~
This function is used as default value for active content:
>
function()
local mode, mode_hl = MiniStatusline.section_mode({ trunc_width = 120 })
local git = MiniStatusline.section_git({ trunc_width = 75 })
local diagnostics = MiniStatusline.section_diagnostics({ trunc_width = 75 })
local filename = MiniStatusline.section_filename({ trunc_width = 140 })
local fileinfo = MiniStatusline.section_fileinfo({ trunc_width = 120 })
local location = MiniStatusline.section_location({ trunc_width = 75 })
return MiniStatusline.combine_groups({
{ hl = mode_hl, strings = { mode } },
{ hl = 'MiniStatuslineDevinfo', strings = { git, diagnostics } },
'%<', -- Mark general truncate point
{ hl = 'MiniStatuslineFilename', strings = { filename } },
'%=', -- End left alignment
{ hl = 'MiniStatuslineFileinfo', strings = { fileinfo } },
{ hl = mode_hl, strings = { location } },
})
end
<
# Show boolean options~
To compute section string for boolean option use variation of this code
snippet inside content function (you can modify option itself, truncation
width, short and long displayed names):
>
local spell = vim.wo.spell and (MiniStatusline.is_truncated(120) and 'S' or 'SPELL') or ''
<
Here `x and y or z` is a common Lua way of doing ternary operator: if `x`
is `true`-ish then return `y`, if not - return `z`.
------------------------------------------------------------------------------
*MiniStatusline.setup()*
`MiniStatusline.setup`({config})
Module setup
Parameters~
{config} `(table)` Module config table. See |MiniStatusline.config|.
Usage~
`require('mini.statusline').setup({})` (replace `{}` with your `config` table)
------------------------------------------------------------------------------
*MiniStatusline.config*
`MiniStatusline.config`
Module config
Default values:
>
MiniStatusline.config = {
-- Content of statusline as functions which return statusline string. See
-- `:h statusline` and code of default contents (used instead of `nil`).
content = {
-- Content for active window
active = nil,
-- Content for inactive window(s)
inactive = nil,
},
-- Whether to use icons by default
use_icons = true,
-- Whether to set Vim's settings for statusline (make it always shown with
-- 'laststatus' set to 2). To use global statusline in Neovim>=0.7.0, set
-- this to `false` and 'laststatus' to 3.
set_vim_settings = true,
}
<
------------------------------------------------------------------------------
*MiniStatusline.active()*
`MiniStatusline.active`()
Compute content for active window
------------------------------------------------------------------------------
*MiniStatusline.inactive()*
`MiniStatusline.inactive`()
Compute content for inactive window
------------------------------------------------------------------------------
*MiniStatusline.combine_groups()*
`MiniStatusline.combine_groups`({groups})
Combine groups of sections
Each group can be either a string or a table with fields `hl` (group's
highlight group) and `strings` (strings representing sections).
General idea of this function is as follows;
- String group is used as is (useful for special strings like `%<` or `%=`).
- Each table group has own highlighting in `hl` field (if missing, the
previous one is used) and string parts in `strings` field. Non-empty
strings from `strings` are separated by one space. Non-empty groups are
separated by two spaces (one for each highlighting).
Parameters~
{groups} `(string|table)` Array of groups.
Return~
`(string)` String suitable for 'statusline'.
------------------------------------------------------------------------------
*MiniStatusline.is_truncated()*
`MiniStatusline.is_truncated`({trunc_width})
Decide whether to truncate
This basically computes window width and compares it to `trunc_width`: if
window is smaller then truncate; otherwise don't. Don't truncate by
default.
Use this to manually decide if section needs truncation or not.
Parameters~
{trunc_width} `(number)` Truncation width. If `nil`, output is `false`.
Return~
`(boolean)` Whether to truncate.
------------------------------------------------------------------------------
*MiniStatusline.section_mode()*
`MiniStatusline.section_mode`({args})
Section for Vim |mode()|
Short output is returned if window width is lower than `args.trunc_width`.
Parameters~
{args} `(table)` Section arguments.
Return~
`(...)` Section string and mode's highlight group.
------------------------------------------------------------------------------
*MiniStatusline.section_git()*
`MiniStatusline.section_git`({args})
Section for Git information
Normal output contains name of `HEAD` (via |b:gitsigns_head|) and chunk
information (via |b:gitsigns_status|). Short output - only name of `HEAD`.
Note: requires 'lewis6991/gitsigns' plugin.
Short output is returned if window width is lower than `args.trunc_width`.
Parameters~
{args} `(table)` Section arguments. Use `args.icon` to supply your own icon.
Return~
`(string)` Section string.
------------------------------------------------------------------------------
*MiniStatusline.section_diagnostics()*
`MiniStatusline.section_diagnostics`({args})
Section for Neovim's builtin diagnostics
Shows nothing if there is no attached LSP clients or for short output.
Otherwise uses builtin Neovim capabilities to compute and show number of
errors ('E'), warnings ('W'), information ('I'), and hints ('H').
Short output is returned if window width is lower than `args.trunc_width`.
Parameters~
{args} `(table)` Section arguments. Use `args.icon` to supply your own icon.
Return~
`(string)` Section string.
------------------------------------------------------------------------------
*MiniStatusline.section_filename()*
`MiniStatusline.section_filename`({args})
Section for file name
Show full file name or relative in short output.
Short output is returned if window width is lower than `args.trunc_width`.
Parameters~
{args} `(table)` Section arguments.
Return~
`(string)` Section string.
------------------------------------------------------------------------------
*MiniStatusline.section_fileinfo()*
`MiniStatusline.section_fileinfo`({args})
Section for file information
Short output contains only extension and is returned if window width is
lower than `args.trunc_width`.
Parameters~
{args} `(table)` Section arguments.
Return~
`(string)` Section string.
------------------------------------------------------------------------------
*MiniStatusline.section_location()*
`MiniStatusline.section_location`({args})
Section for location inside buffer
Show location inside buffer in the form:
- Normal: '<cursor line>|<total lines>│<cursor column>|<total columns>'.
- Short: '<cursor line>│<cursor column>'.
Short output is returned if window width is lower than `args.trunc_width`.
Parameters~
{args} `(table)` Section arguments.
Return~
`(string)` Section string.
------------------------------------------------------------------------------
*MiniStatusline.section_searchcount()*
`MiniStatusline.section_searchcount`({args})
Section for current search count
Show the current status of |searchcount()|. Empty output is returned if
window width is lower than `args.trunc_width`, search highlighting is not
on (see |v:hlsearch|), or if number of search result is 0.
`args.options` is forwarded to |searchcount()|. By default it recomputes
data on every call which can be computationally expensive (although still
usually same order of magnitude as 0.1 ms). To prevent this, supply
`args.options = {recompute = false}`.
Parameters~
{args} `(table)` Section arguments.
Return~
`(string)` Section string.
vim:tw=78:ts=8:noet:ft=help:norl:

View File

@ -0,0 +1,785 @@
==============================================================================
------------------------------------------------------------------------------
*mini.surround*
*MiniSurround*
Fast and feature-rich surrounding. This is mostly a reimplementation of the
core features of 'machakann/vim-sandwich' with more on top (find
surrounding, highlight surrounding, flexible customization). Can be
configured to have experience similar to 'tpope/vim-surround'.
Features:
- Actions (all of them are dot-repeatable out of the box and respect
|v:count| for searching surrounding) with configurable keymappings:
- Add surrounding with `sa` (in visual mode or on motion).
- Delete surrounding with `sd`.
- Replace surrounding with `sr`.
- Find surrounding with `sf` or `sF` (move cursor right or left).
- Highlight surrounding with `sh`.
- Change number of neighbor lines with `sn` (see |MiniSurround-algorithm|).
- Surrounding is identified by a single character as both "input" (in
`delete` and `replace` start, `find`, and `highlight`) and "output" (in
`add` and `replace` end):
- 'f' - function call (string of alphanumeric symbols or '_' or '.'
followed by balanced '()'). In "input" finds function call, in
"output" prompts user to enter function name.
- 't' - tag. In "input" finds tab with same identifier, in "output"
prompts user to enter tag name.
- All symbols in brackets '()', '[]', '{}', '<>". In "input' represents
balanced brackets (open - with whitespace pad, close - without), in
"output" - left and right parts of brackets.
- '?' - interactive. Prompts user to enter left and right parts.
- All other alphanumeric, punctuation, or space characters represent
surrounding with identical left and right parts.
- Configurable search methods to find not only covering but possibly next,
previous, or nearest surrounding. See more in |MiniSurround.config|.
- All actions involving finding surrounding (delete, replace, find,
highlight) can be used with suffix that changes search method to find
previous/last. See more in |MiniSurround.config|.
Known issues which won't be resolved:
- Search for surrounding is done using Lua patterns (regex-like approach).
So certain amount of false positives should be expected.
- When searching for "input" surrounding, there is no distinction if it is
inside string or comment. So in this case there will be not proper match
for a function call: 'f(a = ")", b = 1)'.
- Tags are searched using regex-like methods, so issues are inevitable.
Overall it is pretty good, but certain cases won't work. Like self-nested
tags won't match correctly on both ends: '<a><a></a></a>'.
# Setup~
This module needs a setup with `require('mini.surround').setup({})`
(replace `{}` with your `config` table). It will create global Lua table
`MiniSurround` which you can use for scripting or manually (with
`:lua MiniSurround.*`).
See |MiniSurround.config| for `config` structure and default values. It
also has example setup providing experience similar to 'tpope/vim-surround'.
You can override runtime config settings locally to buffer inside
`vim.b.minisurround_config` which should have same structure as
`MiniSurround.config`. See |mini.nvim-buffer-local-config| for more details.
# Example usage~
Regular mappings:
- `saiw)` - add (`sa`) for inner word (`iw`) parenthesis (`)`).
- `saiwi[[<CR>]]<CR>` - add (`sa`) for inner word (`iw`) interactive
surrounding (`i`): `[[` for left and `]]` for right.
- `2sdf` - delete (`sd`) second (`2`) surrounding function call (`f`).
- `sr)tdiv<CR>` - replace (`sr`) surrounding parenthesis (`)`) with tag
(`t`) with identifier 'div' (`div<CR>` in command line prompt).
- `sff` - find right (`sf`) part of surrounding function call (`f`).
- `sh}` - highlight (`sh`) for a brief period of time surrounding curly
brackets (`}`).
Extended mappings (temporary force "prev"/"next" search methods):
- `sdnf` - delete (`sd`) next (`n`) function call (`f`).
- `srlf(` - replace (`sd`) last (`l`) function call (`f`) with padded
bracket (`(`).
- `2sfnt` - find (`sf`) second (2) next (`n`) tag (`t`).
- `shl}` - highlight (`sh`) last (`l`) second (`2`) curly bracket (`}`).
# Comparisons~
- 'tpope/vim-surround':
- 'vim-surround' has completely different, with other focus set of
default mappings, while 'mini.surround' has a more coherent set.
- 'mini.surround' supports dot-repeat, customized search path (see
|MiniSurround.config|), customized specifications (see
|MiniSurround-surround-specification|) allowing usage of tree-sitter
queries (see |MiniSurround.gen_spec.input.treesitter()|),
highlighting and finding surrounding, "last"/"next" extended
mappings. While 'vim-surround' does not.
- 'machakann/vim-sandwich':
- Both have same keybindings for common actions (add, delete, replace).
- Otherwise same differences as with 'tpop/vim-surround' (except
dot-repeat because 'vim-sandwich' supports it).
- 'kylechui/nvim-surround':
- 'nvim-surround' is designed after 'tpope/vim-surround' with same
default mappings and logic, while 'mini.surround' has mappings
similar to 'machakann/vim-sandwich'.
- 'mini.surround' has more flexible customization of input surrounding
(with composed patterns, region pair(s), search methods).
- 'mini.surround' supports |v:count| in input surrounding while
'nvim-surround' doesn't.
- 'mini.surround' supports "last"/"next" extended mappings.
- |mini.ai|:
- Both use similar logic for finding target: textobject in 'mini.ai'
and surrounding pair in 'mini.surround'. While 'mini.ai' uses
extraction pattern for separate `a` and `i` textobjects,
'mini.surround' uses it to select left and right surroundings
(basically a difference between `a` and `i` textobjects).
- Some builtin specifications are slightly different:
- Quotes in 'mini.ai' are balanced, in 'mini.surround' they are not.
- The 'mini.surround' doesn't have argument surrounding.
- Default behavior in 'mini.ai' selects one of the edges into `a`
textobject, while 'mini.surround' - both.
# Highlight groups~
* `MiniSurround` - highlighting of requested surrounding.
To change any highlight group, modify it directly with |:highlight|.
# Disabling~
To disable, set `g:minisurround_disable` (globally) or
`b:minisurround_disable` (for a buffer) to `v:true`. Considering high
number of different scenarios and customization intentions, writing exact
rules for disabling module's functionality is left to user. See
|mini.nvim-disabling-recipes| for common recipes.
------------------------------------------------------------------------------
*MiniSurround-surround-builtin*
Builtin surroundings~
This table describes all builtin surroundings along with what they
represent. Explanation:
- `Key` represents the surrounding identifier: single character which should
be typed after action mappings (see |MiniSurround.config.mappings|).
- `Name` is a description of surrounding.
- `Example line` contains a string for which examples are constructed. The
`*` denotes the cursor position over `a` character.
- `Delete` shows the result of typing `sd` followed by surrounding identifier.
It aims to demonstrate "input" surrounding which is also used in replace
with `sr` (surrounding id is typed first), highlight with `sh`, find with
`sf` and `sF`.
- `Replace` shows the result of typing `sr!` followed by surrounding
identifier (with possible follow up from user). It aims to demonstrate
"output" surrounding which is also used in adding with `sa` (followed by
textobject/motion or in Visual mode).
Example: typing `sd)` with cursor on `*` (covers `a` character) changes line
`!( *a (bb) )!` into `! aa (bb) !`. Typing `sr!)` changes same initial line
into `(( aa (bb) ))`.
>
|Key| Name | Example line | Delete | Replace |
|---|---------------|---------------|-------------|-----------------|
| ( | Balanced () | !( *a (bb) )! | !aa (bb)! | ( ( aa (bb) ) ) |
| [ | Balanced [] | ![ *a [bb] ]! | !aa [bb]! | [ [ aa [bb] ] ] |
| { | Balanced {} | !{ *a {bb} }! | !aa {bb}! | { { aa {bb} } } |
| < | Balanced <> | !< *a <bb> >! | !aa <bb>! | < < aa <bb> > > |
|---|---------------|---------------|-------------|-----------------|
| ) | Balanced () | !( *a (bb) )! | ! aa (bb) ! | (( aa (bb) )) |
| ] | Balanced [] | ![ *a [bb] ]! | ! aa [bb] ! | [[ aa [bb] ]] |
| } | Balanced {} | !{ *a {bb} }! | ! aa {bb} ! | {{ aa {bb} }} |
| > | Balanced <> | !< *a <bb> >! | ! aa <bb> ! | << aa <bb> >> |
| b | Alias for | !( *a {bb} )! | ! aa {bb} ! | (( aa {bb} )) |
| | ), ], or } | | | |
|---|---------------|---------------|-------------|-----------------|
| q | Alias for | !'aa'*a'aa'! | !'aaaaaa'! | "'aa'aa'aa'" |
| | ", ', or ` | | | |
|---|---------------|---------------|-------------|-----------------|
| ? | User prompt | !e * o! | ! a ! | ee a oo |
| |(typed e and o)| | | |
|---|---------------|---------------|-------------|-----------------|
| t | Tag | !<x>*</x>! | !a! | <y><x>a</x></y> |
| | | | | (typed y) |
|---|---------------|---------------|-------------|-----------------|
| f | Function call | !f(*a, bb)! | !aa, bb! | g(f(*a, bb)) |
| | | | | (typed g) |
|---|---------------|---------------|-------------|-----------------|
| | Default | !_a*a_! | !aaa! | __aaa__ |
| | (typed _) | | | |
|---|---------------|---------------|-------------|-----------------|
<
Notes:
- All examples assume default `config.search_method`.
- Open brackets differ from close brackets by how they treat inner edge
whitespace: open includes it left and right parts, close does not.
- Output value of `b` alias is same as `)`. For `q` alias - same as `"`.
- Default surrounding is activated for all characters which are not
configured surrounding identifiers. Note: due to special handling of
underlying `x.-x` Lua pattern (see |MiniSurround-search-algorithm|), it
doesn't really support non-trivial `v:count` for "cover" search method.
------------------------------------------------------------------------------
*MiniSurround-glossary*
Note: this is similar to |MiniAi-glossary|.
- REGION - table representing region in a buffer. Fields: <from> and
<to> for inclusive start and end positions (<to> might be `nil` to
describe empty region). Each position is also a table with line <line>
and column <col> (both start at 1). Examples:
- `{ from = { line = 1, col = 1 }, to = { line = 2, col = 1 } }`
- `{ from = { line = 10, col = 10 } }` - empty region.
- REGION PAIR - table representing regions for left and right surroundings.
Fields: <left> and <right> with regions. Examples:
`{`
`left = { from = { line = 1, col = 1 }, to = { line = 1, col = 1 } },`
`right = { from = { line = 1, col = 3 } },`
`}`
- PATTERN - string describing Lua pattern.
- SPAN - interval inside a string (end-exclusive). Like [1, 5). Equal
`from` and `to` edges describe empty span at that point.
- SPAN `A = [a1, a2)` COVERS `B = [b1, b2)` if every element of
`B` is within `A` (`a1 <= b < a2`).
It also is described as B IS NESTED INSIDE A.
- NESTED PATTERN - array of patterns aimed to describe nested spans.
- SPAN MATCHES NESTED PATTERN if there is a sequence of consecutively
nested spans each matching corresponding pattern within substring of
previous span (or input string for first span). Example:
Nested patterns: `{ '%b()', '^. .* .$' }` (balanced `()` with inner space)
Input string: `( ( () ( ) ) )`
`123456789012345`
Here are all matching spans [1, 15) and [3, 13). Both [5, 7) and [8, 10)
match first pattern but not second. All other combinations of `(` and `)`
don't match first pattern (not balanced).
- COMPOSED PATTERN: array with each element describing possible pattern
(or array of them) at that place. Composed pattern basically defines all
possible combinations of nested pattern (their cartesian product).
Examples:
1. Composed pattern: `{ { '%b()', '%b[]' }, '^. .* .$' }`
Composed pattern expanded into equivalent array of nested patterns:
`{ '%b()', '^. .* .$' }` and `{ '%b[]', '^. .* .$' }`
Description: either balanced `()` or balanced `[]` but both with
inner edge space.
2. Composed pattern:
`{ { { '%b()', '^. .* .$' }, { '%b[]', '^.[^ ].*[^ ].$' } }, '.....' }`
Composed pattern expanded into equivalent array of nested patterns:
`{ '%b()', '^. .* .$', '.....' }` and
`{ '%b[]', '^.[^ ].*[^ ].$', '.....' }`
Description: either "balanced `()` with inner edge space" or
"balanced `[]` with no inner edge space", both with 5 or more characters.
- SPAN MATCHES COMPOSED PATTERN if it matches at least one nested pattern
from expanded composed pattern.
------------------------------------------------------------------------------
*MiniSurround-surround-specification*
Surround specification is a table with keys:
- <input> - defines how to find and extract surrounding for "input"
operations (like `delete`). See more in 'Input surrounding' setction.
- <output> - defines what to add on left and right for "output" operations
(like `add`). See more in 'Output surrounding' section.
Example of surround info for builtin `)` identifier: >
{
input = { '%b()', '^.().*().$' },
output = { left = '(', right = ')' }
}
<
# Input surrounding ~
Specification for input surrounding has a structure of composed pattern
(see |MiniSurround-glossary|) with two differences:
- Last pattern(s) should have two or four empty capture groups denoting
how the last string should be processed to extract surrounding parts:
- Two captures represent left part from start of string to first
capture and right part - from second capture to end of string.
Example: `a()b()c` defines left surrounding as 'a', right - 'c'.
- Four captures define left part inside captures 1 and 2, right part -
inside captures 3 and 4. Example: `a()()b()c()` defines left part as
empty, right part as 'c'.
- Allows callable objects (see |vim.is_callable()|) in certain places
(enables more complex surroundings in exchange of increase in configuration
complexity and computations):
- If specification itself is a callable, it will be called without
arguments and should return one of:
- Composed pattern. Useful for implementing user input. Example of
simplified variant of input surrounding for function call with
name taken from user prompt:
>
function()
local left_edge = vim.pesc(vim.fn.input('Function name: '))
return { string.format('%s+%%b()', left_edge), '^.-%(().*()%)$' }
end
<
- Single region pair (see |MiniSurround-glossary|). Useful to allow
full control over surrounding. Will be taken as is. Example of
returning first and last lines of a buffer:
>
function()
local n_lines = vim.fn.line('$')
return {
left = {
from = { line = 1, col = 1 },
to = { line = 1, col = vim.fn.getline(1):len() },
},
right = {
from = { line = n_lines, col = 1 },
to = { line = n_lines, col = vim.fn.getline(n_lines):len() },
},
}
end
<
- Array of region pairs. Useful for incorporating other instruments,
like treesitter (see |MiniSurround.gen_spec.treesitter()|). The
best region pair will be picked in the same manner as with composed
pattern (respecting options `n_lines`, `search_method`, etc.) using
output region (from start of left region to end of right region).
Example using edges of "best" line with display width more than 80:
>
function()
local make_line_region_pair = function(n)
local left = { line = n, col = 1 }
local right = { line = n, col = vim.fn.getline(n):len() }
return {
left = { from = left, to = left },
right = { from = right, to = right },
}
end
local res = {}
for i = 1, vim.fn.line('$') do
if vim.fn.getline(i):len() > 80 then
table.insert(res, make_line_region_pair(i))
end
end
return res
end
<
- If there is a callable instead of assumed string pattern, it is expected
to have signature `(line, init)` and behave like `pattern:find()`.
It should return two numbers representing span in `line` next after
or at `init` (`nil` if there is no such span).
!IMPORTANT NOTE!: it means that output's `from` shouldn't be strictly
to the left of `init` (it will lead to infinite loop). Not allowed as
last item (as it should be pattern with captures).
Example of matching only balanced parenthesis with big enough width:
>
{
'%b()',
function(s, init)
if init > 1 or s:len() < 5 then return end
return 1, s:len()
end,
'^.().*().$'
}
>
More examples:
- See |MiniSurround.gen_spec| for function wrappers to create commonly used
surrounding specifications.
- Pair of balanced brackets from set (used for builtin `b` identifier):
`{ { '%b()', '%b[]', '%b{}' }, '^.().*().$' }`
- Lua block string: `{ '%[%[().-()%]%]' }`
# Output surrounding ~
A table with <left> (plain text string) and <right> (plain text string)
fields. Strings can contain new lines charater `\n` to add multiline parts.
Examples:
- Lua block string: `{ left = '[[', right = ']]' }`
- Brackets on separate lines (indentation is not preserved):
`{ left = '(\n', right = '\n)' }`
# Transition from previous specification ~
Previous specification format for input surrounding was a table with <find>
and <extract> fields. They are now replaced with composed pattern (see
|MiniSurround-glossary|). Previous format will work until next release.
To convert, remove `find = ` and `extract = ` while replacing left and right
captures in `extract` with appropriate empty capture(s). Example:
- Previous: `{ find = '%[%[.-%]%]', extract = '^(..).*(..)$' }`.
Current: `{ '%[%[().-()%]%]' }`
------------------------------------------------------------------------------
*MiniSurround-search-algorithm*
Search algorithm design
Search for the input surrounding relies on these principles:
- Input surrounding specification is constructed based on surrounding
identifier (see |MiniSurround-surround-specification|).
- General search is done by converting some 2d buffer region (neighborhood
of reference region) into 1d string (each line is appended with `\n`).
Then search for a best span matching specification is done inside string
(see |MiniSurround-glossary|). After that, span is converted back into 2d
region. Note: first search is done inside reference region lines, and
only after that - inside its neighborhood within `config.n_lines` (see
|MiniSurround.config|).
- The best matching span is chosen by iterating over all spans matching
surrounding specification and comparing them with "current best".
Comparison also depends on reference region (tighter covering is better,
otherwise closer is better) and search method (if span is even considered).
- Extract pair of spans (for left and right regions in region pair) based
on extraction pattern (last item in nested pattern).
- For |v:count| greater than 1, steps are repeated with current best match
becoming reference region. One such additional step is also done if final
region is equal to reference region. Note: |v:count| is not supported for
output surroundings because it brings a lot of inconvenience (for adding
it affects textobject/motion, for replacing it will be used for both
input and output).
Notes:
- Iteration over all matched spans is done in depth-first fashion with
respect to nested pattern.
- It is guaranteed that span is compared only once.
- For the sake of increasing functionality, during iteration over all
matching spans, some Lua patterns in composed pattern are handled
specially.
- `%bxx` (`xx` is two identical characters). It denotes balanced pair
of identical characters and results into "paired" matches. For
example, `%b""` for `"aa" "bb"` would match `"aa"` and `"bb"`, but
not middle `" "`.
- `x.-y` (`x` and `y` are different strings). It results only in matches with
smallest width. For example, `e.-o` for `e e o o` will result only in
middle `e o`. Note: it has some implications for when parts have
quantifiers (like `+`, etc.), which usually can be resolved with
frontier pattern `%f[]`.
------------------------------------------------------------------------------
*MiniSurround.setup()*
`MiniSurround.setup`({config})
Module setup
Parameters~
{config} `(table)` Module config table. See |MiniSurround.config|.
Usage~
`require('mini.surround').setup({})` (replace `{}` with your `config` table)
------------------------------------------------------------------------------
*MiniSurround.config*
`MiniSurround.config`
Module config
Default values:
>
MiniSurround.config = {
-- Add custom surroundings to be used on top of builtin ones. For more
-- information with examples, see `:h MiniSurround.config`.
custom_surroundings = nil,
-- Duration (in ms) of highlight when calling `MiniSurround.highlight()`
highlight_duration = 500,
-- Module mappings. Use `''` (empty string) to disable one.
mappings = {
add = 'sa', -- Add surrounding in Normal and Visual modes
delete = 'sd', -- Delete surrounding
find = 'sf', -- Find surrounding (to the right)
find_left = 'sF', -- Find surrounding (to the left)
highlight = 'sh', -- Highlight surrounding
replace = 'sr', -- Replace surrounding
update_n_lines = 'sn', -- Update `n_lines`
suffix_last = 'l', -- Suffix to search with "prev" method
suffix_next = 'n', -- Suffix to search with "next" method
},
-- Number of lines within which surrounding is searched
n_lines = 20,
-- How to search for surrounding (first inside current line, then inside
-- neighborhood). One of 'cover', 'cover_or_next', 'cover_or_prev',
-- 'cover_or_nearest', 'next', 'prev', 'nearest'. For more details,
-- see `:h MiniSurround.config`.
search_method = 'cover',
}
<
# Setup similar to 'tpope/vim-surround'~
This module is primarily designed after 'machakann/vim-sandwich'. To get
behavior closest to 'tpope/vim-surround' (but not identical), use this setup:
>
require('mini.surround').setup({
mappings = {
add = 'ys',
delete = 'ds',
find = '',
find_left = '',
highlight = '',
replace = 'cs',
update_n_lines = '',
-- Add this only if you don't want to use extended mappings
suffix_last = '',
suffix_next = '',
},
search_method = 'cover_or_next',
})
-- Remap adding surrounding to Visual mode selection
vim.api.nvim_del_keymap('x', 'ys')
vim.api.nvim_set_keymap('x', 'S', [[:<C-u>lua MiniSurround.add('visual')<CR>]], { noremap = true })
-- Make special mapping for "add surrounding for line"
vim.api.nvim_set_keymap('n', 'yss', 'ys_', { noremap = false })
<
# Options~
## Custom surroundings~
User can define own surroundings by supplying `config.custom_surroundings`.
It should be a **table** with keys being single character surrounding
identifier and values - surround specification (see
|MiniSurround-surround-specification|).
General recommendations:
- In `config.custom_surroundings` only some data can be defined (like only
`output`). Other fields will be taken from builtin surroundings.
- Function returning surround info at <input> or <output> fields of
specification is helpful when user input is needed (like asking for
function name). Use |input()| or |MiniSurround.user_input()|. Return
`nil` to stop any current surround operation.
Examples of using `config.custom_surroundings` (see more examples at
|MiniSurround.gen_spec|):
>
local surround = require('mini.surround')
surround.setup({
custom_surroundings = {
-- Make `)` insert parts with spaces. `input` pattern stays the same.
[')'] = { output = { left = '( ', right = ' )' } },
-- Use function to compute surrounding info
['*'] = {
input = function()
local n_star = MiniSurround.user_input('Number of * to find: ')
local many_star = string.rep('%*', tonumber(n_star) or 1)
return { many_star .. '().-()' .. many_star }
end,
output = function()
local n_star = MiniSurround.user_input('Number of * to output: ')
local many_star = string.rep('*', tonumber(n_star) or 1)
return { left = many_star, right = many_star }
end,
},
},
})
-- Create custom surrouding for Lua's block string `[[...]]`. Use this inside
-- autocommand or 'after/ftplugin/lua.lua' file.
vim.b.minisurround_config = {
custom_surroundings = {
s = {
input = { '%[%[().-()%]%]' },
output = { left = '[[', right = ']]' },
},
},
}
<
## Search method~
Value of `config.search_method` defines how best match search is done.
Based on its value, one of the following matches will be selected:
- Covering match. Left/right edge is before/after left/right edge of
reference region.
- Previous match. Left/right edge is before left/right edge of reference
region.
- Next match. Left/right edge is after left/right edge of reference region.
- Nearest match. Whichever is closest among previous and next matches.
Possible values are:
- `'cover'` - use only covering match. Don't use either previous or
next; report that there is no surrounding found.
- `'cover_or_next'` (default) - use covering match. If not found, use next.
- `'cover_or_prev'` - use covering match. If not found, use previous.
- `'cover_or_nearest'` - use covering match. If not found, use nearest.
- `'next'` - use next match.
- `'previous'` - use previous match.
- `'nearest'` - use nearest match.
Note: search is first performed on the reference region lines and only
after failure - on the whole neighborhood defined by `config.n_lines`. This
means that with `config.search_method` not equal to `'cover'`, "previous"
or "next" surrounding will end up as search result if they are found on
first stage although covering match might be found in bigger, whole
neighborhood. This design is based on observation that most of the time
operation is done within reference region lines (usually cursor line).
Here is an example of how replacing `)` with `]` surrounding is done based
on a value of `'config.search_method'` when cursor is inside `bbb` word:
- `'cover'`: `(a) bbb (c)` -> `(a) bbb (c)` (with message)
- `'cover_or_next'`: `(a) bbb (c)` -> `(a) bbb [c]`
- `'cover_or_prev'`: `(a) bbb (c)` -> `[a] bbb (c)`
- `'cover_or_nearest'`: depends on cursor position.
For first and second `b` - as in `cover_or_prev` (as previous match is
nearer), for third - as in `cover_or_next` (as next match is nearer).
- `'next'`: `(a) bbb (c)` -> `(a) bbb [c]`. Same outcome for `(bbb)`.
- `'prev'`: `(a) bbb (c)` -> `[a] bbb (c)`. Same outcome for `(bbb)`.
- `'nearest'`: depends on cursor position (same as in `'cover_or_nearest'`).
## Search suffixes~
To provide more searching possibilities, 'mini.surround' creates extended
mappings force "prev" and "next" methods for particular search. It does so
by appending mapping with certain suffix: `config.mappings.suffix_last` for
mappings which will use "prev" search method, `config.mappings.suffix_next`
- "next" search method.
Notes:
- It creates new mappings only for actions involving surrounding search:
delete, replace, find (right and left), highlight.
- All new mappings behave the same way as if `config.search_method` is set
to certain search method. They are dot-repeatable, respect |v:count|, etc.
- Supply empty string to disable creation of corresponding set of mappings.
Example with default values (`n` for `suffix_next`, `l` for `suffix_last`)
and initial line `(aa) (bb) (cc)`.
- Typing `sdn)` with cursor inside `(aa)` results into `(aa) bb (cc)`.
- Typing `sdl)` with cursor inside `(cc)` results into `(aa) bb (cc)`.
- Typing `2srn)]` with cursor inside `(aa)` results into `(aa) (bb) [cc]`.
------------------------------------------------------------------------------
*MiniSurround.operator()*
`MiniSurround.operator`({task}, {cache})
Surround operator
Main function to be used in expression mappings. No need to use it
directly, everything is setup in |MiniSurround.setup|.
Parameters~
{task} `(string)` Name of surround task.
{cache} `(table)` Task cache.
------------------------------------------------------------------------------
*MiniSurround.add()*
`MiniSurround.add`({mode})
Add surrounding
No need to use it directly, everything is setup in |MiniSurround.setup|.
Parameters~
{mode} `(string)` Mapping mode (normal by default).
------------------------------------------------------------------------------
*MiniSurround.delete()*
`MiniSurround.delete`()
Delete surrounding
No need to use it directly, everything is setup in |MiniSurround.setup|.
------------------------------------------------------------------------------
*MiniSurround.replace()*
`MiniSurround.replace`()
Replace surrounding
No need to use it directly, everything is setup in |MiniSurround.setup|.
------------------------------------------------------------------------------
*MiniSurround.find()*
`MiniSurround.find`()
Find surrounding
No need to use it directly, everything is setup in |MiniSurround.setup|.
------------------------------------------------------------------------------
*MiniSurround.highlight()*
`MiniSurround.highlight`()
Highlight surrounding
No need to use it directly, everything is setup in |MiniSurround.setup|.
------------------------------------------------------------------------------
*MiniSurround.update_n_lines()*
`MiniSurround.update_n_lines`()
Update `MiniSurround.config.n_lines`
Convenient wrapper for updating `MiniSurround.config.n_lines` in case the
default one is not appropriate.
------------------------------------------------------------------------------
*MiniSurround.user_input()*
`MiniSurround.user_input`({prompt}, {text})
Ask user for input
This is mainly a wrapper for |input()| which allows empty string as input,
cancelling with `<Esc>` and `<C-c>`, and slightly modifies prompt. Use it
to ask for input inside function custom surrounding (see |MiniSurround.config|).
------------------------------------------------------------------------------
*MiniSurround.gen_spec*
`MiniSurround.gen_spec`
Generate common surrounding specifications
This is a table with two sets of generator functions: <input> and <output>
(currently empty). Each is a table with values being function generating
corresponding surrounding specification.
Example: >
local ts_input = require('mini.surround').gen_spec.input.treesitter
require('mini.surround').setup({
custom_surroundings = {
-- Use tree-sitter to search for function call
f = {
input = ts_input({ outer = '@call.outer', inner = '@call.inner' })
},
}
})
See also~
|MiniAi.gen_spec|
------------------------------------------------------------------------------
*MiniSurround.gen_spec.input.treesitter()*
`MiniSurround.gen_spec.input.treesitter`({captures}, {opts})
Treesitter specification for input surrounding
This is a specification in function form. When called with a pair of
treesitter captures, it returns a specification function outputting an
array of region pairs derived from <outer> and <inner> captures. It first
searches for all matched nodes of outer capture and then completes each one
with the biggest match of inner capture inside that node (if any). The result
region pair is a difference between regions of outer and inner captures.
In order for this to work, apart from working treesitter parser for desired
language, user should have a reachable language-specific 'textobjects'
query (see |get_query()|). The most straightforward way for this is to have
'textobjects.scm' query file with treesitter captures stored in some
recognized path. This is primarily designed to be compatible with
'nvim-treesitter/nvim-treesitter-textobjects' plugin, but can be used
without it.
Two most common approaches for having a query file:
- Install 'nvim-treesitter/nvim-treesitter-textobjects'. It has curated and
well maintained builtin query files for many languages with a standardized
capture names, like `call.outer`, `call.inner`, etc.
- Manually create file 'after/queries/<language name>/textobjects.scm' in
your |$XDG_CONFIG_HOME| directory. It should contain queries with
captures (later used to define surrounding parts). See |lua-treesitter-query|.
To verify that query file is reachable, run (example for "lua" language)
`:lua print(vim.inspect(vim.treesitter.get_query_files('lua', 'textobjects')))`
(output should have at least an intended file).
Example configuration for function definition textobject with
'nvim-treesitter/nvim-treesitter-textobjects' captures:
>
local ts_input = require('mini.surround').gen_spec.input.treesitter
require('mini.surround').setup({
custom_textobjects = {
f = ts_input({ outer = '@call.outer', inner = '@call.inner' }),
}
})
>
Notes:
- By default query is done using 'nvim-treesitter' plugin if it is present
(falls back to builtin methods otherwise). This allows for a more
advanced features (like multiple buffer languages, custom directives, etc.).
See `opts.use_nvim_treesitter` for how to disable this.
- It uses buffer's |filetype| to determine query language.
- On large files it is slower than pattern-based textobjects. Still very
fast though (one search should be magnitude of milliseconds or tens of
milliseconds on really large file).
Parameters~
{captures} `(table)` Captures for outer and inner parts of region pair:
table with <outer> and <inner> fields with captures for outer
(`[left.form; right.to]`) and inner (`(left.to; right.from)` both edges
exclusive, i.e. they won't be a part of surrounding) regions. Each value
should be a string capture starting with `'@'`.
{opts} `(table)` Options. Possible values:
- <use_nvim_treesitter> - whether to try to use 'nvim-treesitter' plugin
(if present) to do the query. It implements more advanced behavior at
cost of increased execution time. Provides more coherent experience if
'nvim-treesitter-textobjects' queries are used. Default: `true`.
Return~
`(function)` Function which returns array of current buffer region pairs
representing differences between outer and inner captures.
See also~
|MiniSurround-surround-specification| for how this type of
surrounding specification is processed.
|get_query()| for how query is fetched in case of no 'nvim-treesitter'.
|Query:iter_captures()| for how all query captures are iterated in case of
no 'nvim-treesitter'.
|MiniAi.gen_spec.treesitter()| for similar 'mini.ai' generator.
vim:tw=78:ts=8:noet:ft=help:norl:

View File

@ -0,0 +1,103 @@
==============================================================================
------------------------------------------------------------------------------
*mini.tabline*
*MiniTabline*
Minimal and fast tabline showing listed buffers. General idea: show all
listed buffers in readable way with minimal total width. Also allow showing
extra information section in case of multiple vim tabpages.
Features:
- Buffers are listed in the order of their identifier (see |bufnr()|).
- Different highlight groups for "states" of buffer affecting 'buffer tabs'.
- Buffer names are made unique by extending paths to files or appending
unique identifier to buffers without name.
- Current buffer is displayed "optimally centered" (in center of screen
while maximizing the total number of buffers shown) when there are many
buffers open.
- 'Buffer tabs' are clickable if Neovim allows it.
What it doesn't do:
- Custom buffer order is not supported.
# Dependencies~
Suggested dependencies (provide extra functionality, tabline will work
without them):
- Plugin 'kyazdani42/nvim-web-devicons' for filetype icons near the buffer
name. If missing, no icons will be shown.
# Setup~
This module needs a setup with `require('mini.tabline').setup({})`
(replace `{}` with your `config` table). It will create global Lua table
`MiniTabline` which you can use for scripting or manually (with
`:lua MiniTabline.*`).
See |MiniTabline.config| for `config` structure and default values.
You can override runtime config settings locally to buffer inside
`vim.b.minitabline_config` which should have same structure as
`MiniTabline.config`. See |mini.nvim-buffer-local-config| for more details.
# Highlight groups~
* `MiniTablineCurrent` - buffer is current (has cursor in it).
* `MiniTablineVisible` - buffer is visible (displayed in some window).
* `MiniTablineHidden` - buffer is hidden (not displayed).
* `MiniTablineModifiedCurrent` - buffer is modified and current.
* `MiniTablineModifiedVisible` - buffer is modified and visible.
* `MiniTablineModifiedHidden` - buffer is modified and hidden.
* `MiniTablineFill` - unused right space of tabline.
* `MiniTablineTabpagesection` - section with tabpage information.
To change any highlight group, modify it directly with |:highlight|.
# Disabling~
To disable (show empty tabline), set `g:minitabline_disable` (globally) or
`b:minitabline_disable` (for a buffer) to `v:true`. Considering high number
of different scenarios and customization intentions, writing exact rules
for disabling module's functionality is left to user. See
|mini.nvim-disabling-recipes| for common recipes. Note: after disabling,
tabline is not updated right away, but rather after dedicated event (see
|events| and `MiniTabline` |augroup|).
------------------------------------------------------------------------------
*MiniTabline.setup()*
`MiniTabline.setup`({config})
Module setup
Parameters~
{config} `(table)` Module config table. See |MiniTabline.config|.
Usage~
`require('mini.tabline').setup({})` (replace `{}` with your `config` table)
------------------------------------------------------------------------------
*MiniTabline.config*
`MiniTabline.config`
Module config
Default values:
>
MiniTabline.config = {
-- Whether to show file icons (requires 'kyazdani42/nvim-web-devicons')
show_icons = true,
-- Whether to set Vim's settings for tabline (make it always shown and
-- allow hidden buffers)
set_vim_settings = true,
-- Where to show tabpage section in case of multiple vim tabpages.
-- One of 'left', 'right', 'none'.
tabpage_section = 'left',
}
<
------------------------------------------------------------------------------
*MiniTabline.make_tabline_string()*
`MiniTabline.make_tabline_string`()
Make string for |tabline|
vim:tw=78:ts=8:noet:ft=help:norl:

View File

@ -0,0 +1,884 @@
==============================================================================
------------------------------------------------------------------------------
*mini.test*
*MiniTest*
Write and use extensive neovim plugin tests
Features:
- Test action is defined as a named callable entry of a table.
- Helper for creating child Neovim process which is designed to be used in
tests (including taking and verifying screenshots). See
|MiniTest.new_child_neovim()| and |Minitest.expect.reference_screenshot()|.
- Hierarchical organization of tests with custom hooks, parametrization,
and user data. See |MiniTest.new_set()|.
- Emulation of 'Olivine-Labs/busted' interface (`describe`, `it`, etc.).
- Predefined small yet usable set of expectations (`assert`-like functions).
See |MiniTest.expect|.
- Customizable definition of what files should be tested.
- Test case filtering. There are predefined wrappers for testing a file
(|MiniTest.run_file()|) and case at a location like current cursor position
(|MiniTest.run_at_location()|).
- Customizable reporter of output results. There are two predefined ones:
- |MiniTest.gen_reporter.buffer()| for interactive usage.
- |MiniTest.gen_reporter.stdout()| for headless Neovim.
- Customizable project specific testing script.
What it doesn't support:
- Parallel execution. Due to idea of limiting implementation complexity.
- Mocks, stubs, etc. Use child Neovim process and manually override what is
needed. Reset child process it afterwards.
- "Overly specific" expectations. Tests for (no) equality and (absence of)
errors usually cover most of the needs. Adding new expectations is a
subject to weighing its usefulness against additional implementation
complexity. Use |MiniTest.new_expectation()| to create custom ones.
For more information see:
- 'TESTING.md' file for a hands-on introduction based on examples.
- Code of this plugin's tests. Consider it to be an example of intended
way to use 'mini.test' for test organization and creation.
# Workflow
- Organize tests in separate files. Each test file should return a test set
(explicitly or implicitly by using "busted" style functions).
- Write test actions as callable entries of test set. Use child process
inside test actions (see |MiniTest.new_child_neovim()|) and builtin
expectations (see |MiniTest.expect|).
- Run tests. This does two steps:
- *Collect*. This creates single hierarchical test set, flattens into
array of test cases (see |MiniTest-test-case|) while expanding with
parametrization, and possibly filters them.
- *Execute*. This safely calls hooks and main test actions in specified
order while allowing reporting progress in asynchronous fashion.
Detected errors means test case fail; otherwise - pass.
# Setup~
This module needs a setup with `require('mini.test').setup({})` (replace
`{}` with your `config` table). It will create global Lua table `MiniTest`
which you can use for scripting or manually (with `:lua MiniTest.*`).
See |MiniTest.config| for available config settings.
You can override runtime config settings locally to buffer inside
`vim.b.minitest_config` which should have same structure as `MiniTest.config`.
See |mini.nvim-buffer-local-config| for more details.
# Comparisons~
- Testing infrastructure from 'nvim-lua/plenary.nvim':
- Executes each file in separate headless Neovim process with customizable
'init.vim' file. While 'mini.test' executes everything in current
Neovim process encouraging writing tests with help of manually
managed child Neovim process (see |MiniTest.new_child_neovim()|).
- Tests are expected to be written with embedded simplified versions of
'Olivine-Labs/busted' and 'Olivine-Labs/luassert'. While 'mini.test'
uses concepts of test set (see |MiniTest.new_set()|) and test case
(see |MiniTest-test-case|). It also can emulate bigger part of
"busted" framework.
- Has single way of reporting progress (shows result after every case
without summary). While 'mini.test' can have customized reporters
with defaults for interactive and headless usage (provide more
compact and user-friendly summaries).
- Allows parallel execution, while 'mini.test' does not.
- Allows making mocks, stubs, and spies, while 'mini.test' does not in
favor of manually overwriting functionality in child Neovim process.
Although 'mini.test' supports emulation of "busted style" testing, it will
be more stable to use its designed approach of defining tests (with
`MiniTest.new_set()` and explicit table fields). Couple of reasons:
- "Busted" syntax doesn't support full capabilities offered by 'mini.test'.
Mainly it is about parametrization and supplying user data to test sets.
- It is an emulation, not full support. So some subtle things might not
work the way you expect.
Some hints for converting from 'plenary.nvim' tests to 'mini.test':
- Rename files from "***_spec.lua" to "test_***.lua" and put them in
"tests" directory.
- Replace `assert` calls with 'mini.test' expectations. See |MiniTest.expect|.
- Create main test set `T = MiniTest.new_set()` and eventually return it.
- Make new sets (|MiniTest.new_set()|) from `describe` blocks. Convert
`before_each()` and `after_each` to `pre_case` and `post_case` hooks.
- Make test cases from `it` blocks.
# Highlight groups~
* `MiniTestEmphasis` - emphasis highlighting. By default it is a bold text.
* `MiniTestFail` - highlighting of failed cases. By default it is a bold
text with `vim.g.terminal_color_1` color (red).
* `MiniTestPass` - highlighting of passed cases. By default it is a bold
text with `vim.g.terminal_color_2` color (green).
To change any highlight group, modify it directly with |:highlight|.
# Disabling~
To disable, set `g:minitest_disable` (globally) or `b:minitest_disable`
(for a buffer) to `v:true`. Considering high number of different scenarios
and customization intentions, writing exact rules for disabling module's
functionality is left to user. See |mini.nvim-disabling-recipes| for common
recipes.
------------------------------------------------------------------------------
*MiniTest.setup()*
`MiniTest.setup`({config})
Module setup
Parameters~
{config} `(table|nil)` Module config table. See |MiniTest.config|.
Usage~
`require('mini.test').setup({})` (replace `{}` with your `config` table)
------------------------------------------------------------------------------
*MiniTest.config*
`MiniTest.config`
Module config
Default values:
>
MiniTest.config = {
-- Options for collection of test cases. See `:h MiniTest.collect()`.
collect = {
-- Temporarily emulate functions from 'busted' testing framework
-- (`describe`, `it`, `before_each`, `after_each`, and more)
emulate_busted = true,
-- Function returning array of file paths to be collected.
-- Default: all Lua files in 'tests' directory starting with 'test_'.
find_files = function()
return vim.fn.globpath('tests', '**/test_*.lua', true, true)
end,
-- Predicate function indicating if test case should be executed
filter_cases = function(case) return true end,
},
-- Options for execution of test cases. See `:h MiniTest.execute()`.
execute = {
-- Table with callable fields `start()`, `update()`, and `finish()`
reporter = nil,
-- Whether to stop execution after first error
stop_on_error = false,
},
-- Path (relative to current directory) to script which handles project
-- specific test running
script_path = 'scripts/minitest.lua',
}
<
------------------------------------------------------------------------------
*MiniTest.current*
`MiniTest.current`
Table with information about current state of test execution
Use it to examine result of |MiniTest.execute()|. It is reset at the
beginning of every call.
At least these keys are supported:
- <all_cases> - array with all cases being currently executed. Basically,
an input of `MiniTest.execute()`.
- <case> - currently executed test case. See |MiniTest-test-case|. Use it
to customize execution output (like adding custom notes, etc).
------------------------------------------------------------------------------
*MiniTest.new_set()*
`MiniTest.new_set`({opts}, {tbl})
Create test set
Test set is one of the two fundamental data structures. It is a table that
defines hierarchical test organization as opposed to sequential
organization with |MiniTest-test-case|.
All its elements are one of three categories:
- A callable (object that can be called; function or table with `__call`
metatble entry) is considered to define a test action. It will be called
with "current arguments" (result of all nested `parametrize` values, read
further). If it throws error, test has failed.
- A test set (output of this function) defines nested structure. Its
options during collection (see |MiniTest.collect()|) will be extended
with options of this (parent) test set.
- Any other elements are considered helpers and don't directly participate
in test structure.
Set options allow customization of test collection and execution (more
details in `opts` description):
- `hooks` - table with elements that will be called without arguments at
predefined stages of test execution.
- `parametrize` - array defining different arguments with which main test
actions will be called. Any non-trivial parametrization will lead to
every element (even nested) be "multiplied" and processed with every
element of `parametrize`. This allows handling many different combination
of tests with little effort.
- `data` - table with user data that will be forwarded to cases. Primary
objective is to be used for customized case filtering.
Notes:
- Preferred way of adding elements is by using syntax `T[name] = element`.
This way order of added elements will be preserved. Any other way won't
guarantee any order.
- Supplied options `opts` are stored in `opts` field of metatable
(`getmetatable(set).opts`).
Parameters~
{opts} `(table|nil)` Allowed options:
- <hooks> - table with fields:
- <pre_once> - executed before first filtered node.
- <pre_case> - executed before each case (even nested).
- <post_case> - executed after each case (even nested).
- <post_once> - executed after last filtered node.
- <parametrize> - array where each element is itself an array of
parameters to be appended to "current parameters" of callable fields.
Note: don't use plain `{}` as it is equivalent to "parametrization into
zero cases", so no cases will be collected from this set. Calling test
actions with no parameters is equivalent to `{{}}` or not supplying
`parametrize` option at all.
- <data> - user data to be forwarded to cases. Can be used for a more
granular filtering.
{tbl} `(table|nil)` Initial test items (possibly nested). Will be executed
without any guarantees on order.
Return~
`(table)` A single test set.
Usage~
>
-- Use with defaults
T = MiniTest.new_set()
T['works'] = function() MiniTest.expect.equality(1, 1) end
-- Use with custom options. This will result into two actual cases: first
-- will pass, second - fail.
T['nested'] = MiniTest.new_set({
hooks = { pre_case = function() _G.x = 1 end },
parametrize = { { 1 }, { 2 } }
})
T['nested']['works'] = function(x)
MiniTest.expect.equality(_G.x, x)
end
------------------------------------------------------------------------------
*MiniTest-test-case*
Test case
An item of sequential test organization, as opposed to hierarchical with
test set (see |MiniTest.new_set()|). It is created as result of test
collection with |MiniTest.collect()| to represent all necessary information
of test execution.
Execution of test case goes by the following rules:
- Call functions in order:
- All elements of `hooks.pre` from first to last without arguments.
- Field `test` with arguments unpacked from `args`.
- All elements of `hooks.post` from first to last without arguments.
- Error in any call gets appended to `exec.fails`, meaning error in any
hook will lead to test fail.
- State (`exec.state`) is changed before every call and after last call.
Class~
{Test-case}
Fields~
{args} `(table)` Array of arguments with which `test` will be called.
{data} `(table)` User data: all fields of `opts.data` from nested test sets.
{desc} `(table)` Description: array of fields from nested test sets.
{exec} `(table|nil)` Information about test case execution. Value of `nil` means
that this particular case was not (yet) executed. Has following fields:
- <fails> - array of strings with failing information.
- <notes> - array of strings with non-failing information.
- <state> - state of test execution. One of:
- 'Executing <name of what is being executed>' (during execution).
- 'Pass' (no fails, no notes).
- 'Pass with notes' (no fails, some notes).
- 'Fail' (some fails, no notes).
- 'Fail with notes' (some fails, some notes).
{hooks} `(table)` Hooks to be executed as part of test case. Has fields
<pre> and <post> with arrays to be consecutively executed before and
after execution of `test`.
{test} `(function|table)` Main callable object representing test action.
------------------------------------------------------------------------------
*MiniTest.skip()*
`MiniTest.skip`({msg})
Skip rest of current callable execution
Can be used inside hooks and main test callable of test case. Note: at the
moment implemented as a specially handled type of error.
Parameters~
{msg} `(string|nil)` Message to be added to current case notes.
------------------------------------------------------------------------------
*MiniTest.add_note()*
`MiniTest.add_note`({msg})
Add note to currently executed test case
Appends `msg` to `exec.notes` field of |MiniTest.current.case|.
Parameters~
{msg} `(string)` Note to add.
------------------------------------------------------------------------------
*MiniTest.finally()*
`MiniTest.finally`({f})
Register callable execution after current callable
Can be used inside hooks and main test callable of test case.
Parameters~
{f} `(function)` Callable to be executed after current callable is finished
executing (regardless of whether it ended with error or not).
------------------------------------------------------------------------------
*MiniTest.run()*
`MiniTest.run`({opts})
Run tests
- Try executing project specific script at path `opts.script_path`. If
successful (no errors), then stop.
- Collect cases with |MiniTest.collect()| and `opts.collect`.
- Execute collected cases with |MiniTest.execute()| and `opts.execute`.
Parameters~
{opts} `(table|nil)` Options with structure similar to |MiniTest.config|.
Absent values are inferred from there.
------------------------------------------------------------------------------
*MiniTest.run_file()*
`MiniTest.run_file`({file}, {opts})
Run specific test file
Basically a |MiniTest.run()| wrapper with custom `collect.find_files` option.
Parameters~
{file} `(string|nil)` Path to test file. By default a path of current buffer.
{opts} `(table|nil)` Options for |MiniTest.run()|.
------------------------------------------------------------------------------
*MiniTest.run_at_location()*
`MiniTest.run_at_location`({location}, {opts})
Run case(s) covering location
Try filtering case(s) covering location, meaning that definition of its
main `test` action (as taken from builtin `debug.getinfo`) is located in
specified file and covers specified line. Note that it can result in
multiple cases if they come from parametrized test set (see `parametrize`
option in |MiniTest.new_set()|).
Basically a |MiniTest.run()| wrapper with custom `collect.find_files` option.
Parameters~
{location} `(table|nil)` Table with fields <file> (path to file) and <line>
(line number in that file). Default is taken from current cursor position.
------------------------------------------------------------------------------
*MiniTest.collect()*
`MiniTest.collect`({opts})
Collect test cases
Overview of collection process:
- If `opts.emulate_busted` is `true`, temporary make special global
functions (removed at the end of collection). They can be used inside
test files to create hierarchical structure of test cases.
- Source each file from array output of `opts.find_files`. It should output
a test set (see |MiniTest.new_set()|) or `nil` (if "busted" style is used;
test set is created implicitly).
- Combine all test sets into single set with fields equal to its file path.
- Convert from hierarchical test configuration to sequential: from single
test set to array of test cases (see |MiniTest-test-case|). Conversion is
done in the form of "for every table element do: for every `parametrize`
element do: ...". Details:
- If element is a callable, construct test case with it being main
`test` action. Description is appended with key of element in current
test set table. Hooks, arguments, and data are taken from "current
nested" ones. Add case to output array.
- If element is a test set, process it in similar, recursive fashion.
The "current nested" information is expanded:
- `args` is extended with "current element" from `parametrize`.
- `desc` is appended with element key.
- `hooks` are appended to their appropriate places. `*_case` hooks
will be inserted closer to all child cases than hooks from parent
test sets: `pre_case` at end, `post_case` at start.
- `data` is extended via |vim.tbl_deep_extend()|.
- Any other element is not processed.
- Filter array with `opts.filter_cases`. Note that input case doesn't contain
all hooks, as `*_once` hooks will be added after filtration.
- Add `*_once` hooks to appropriate cases.
Parameters~
{opts} `(table|nil)` Options controlling case collection. Possible fields:
- <emulate_busted> - whether to emulate 'Olivine-Labs/busted' interface.
It emulates these global functions: `describe`, `it`, `setup`, `teardown`,
`before_each`, `after_each`. Use |MiniTest.skip()| instead of `pending()`
and |MiniTest.finally()| instead of `finally`.
- <find_files> - function which when called without arguments returns
array with file paths. Each file should be a Lua file returning single
test set or `nil`.
- <filter_cases> - function which when called with single test case
(see |MiniTest-test-case|) returns `false` if this case should be filtered
out; `true` otherwise.
Return~
`(table)` Array of test cases ready to be used by |MiniTest.execute()|.
------------------------------------------------------------------------------
*MiniTest.execute()*
`MiniTest.execute`({cases}, {opts})
Execute array of test cases
Overview of execution process:
- Reset `all_cases` in |MiniTest.current| with `cases` input.
- Call `reporter.start(cases)` (if present).
- Execute each case in natural array order (aligned with their integer
keys). Set `MiniTest.current.case` to currently executed case. Detailed
test case execution is described in |MiniTest-test-case|. After any state
change, call `reporter.update(case_num)` (if present), where `case_num` is an
integer key of current test case.
- Call `reporter.finish()` (if present).
Notes:
- Execution is done in asynchronous fashion with scheduling. This allows
making meaningful progress report during execution.
- This function doesn't return anything. Instead, it updates `cases` in
place with proper `exec` field. Use `all_cases` at |MiniTest.current| to
look at execution result.
Parameters~
{cases} `(table)` Array of test cases (see |MiniTest-test-case|).
{opts} `(table|nil)` Options controlling case collection. Possible fields:
- <reporter> - table with possible callable fields `start`, `update`,
`finish`. Default: |MiniTest.gen_reporter.buffer()| in interactive
usage and |MiniTest.gen_reporter.stdout()| in headless usage.
- <stop_on_error> - whether to stop execution (see |MiniTest.stop()|)
after first error. Default: `false`.
------------------------------------------------------------------------------
*MiniTest.stop()*
`MiniTest.stop`({opts})
Stop test execution
Parameters~
{opts} `(table|nil)` Options with fields:
- <close_all_child_neovim> - whether to close all child neovim processes
created with |MiniTest.new_child_neovim()|. Default: `true`.
------------------------------------------------------------------------------
*MiniTest.is_executing()*
`MiniTest.is_executing`()
Check if tests are being executed
Return~
`(boolean)`
------------------------------------------------------------------------------
*MiniTest.expect*
`MiniTest.expect`
Table with expectation functions
Each function has the following behavior:
- Silently returns `true` if expectation is fulfilled.
- Throws an informative error with information helpful for debugging.
Mostly designed to be used within 'mini.test' framework.
Usage~
>
local x = 1 + 1
MiniTest.expect.equality(x, 2) -- passes
MiniTest.expect.equality(x, 1) -- fails
------------------------------------------------------------------------------
*MiniTest.expect.equality()*
`MiniTest.expect.equality`({left}, {right})
Expect equality of two objects
Equality is tested via |vim.deep_equal()|.
Parameters~
{left} `(any)` First object.
{right} `(any)` Second object.
------------------------------------------------------------------------------
*MiniTest.expect.no_equality()*
`MiniTest.expect.no_equality`({left}, {right})
Expect no equality of two objects
Equality is tested via |vim.deep_equal()|.
Parameters~
{left} `(any)` First object.
{right} `(any)` Second object.
------------------------------------------------------------------------------
*MiniTest.expect.error()*
`MiniTest.expect.error`({f}, {pattern}, {...})
Expect function call to raise error
Parameters~
{f} `(function)` Function to be tested for raising error.
{pattern} `(string|nil)` Pattern which error message should match.
Use `nil` or empty string to not test for pattern matching.
{...} `(any)` Extra arguments with which `f` will be called.
------------------------------------------------------------------------------
*MiniTest.expect.no_error()*
`MiniTest.expect.no_error`({f}, {...})
Expect function call to not raise error
Parameters~
{f} `(function)` Function to be tested for raising error.
{...} `(any)` Extra arguments with which `f` will be called.
------------------------------------------------------------------------------
*MiniTest.expect.reference_screenshot()*
`MiniTest.expect.reference_screenshot`({screenshot}, {path}, {opts})
Expect equality to reference screenshot
Parameters~
{screenshot} `(table|nil)` Array with screenshot information. Usually an output
of `child.get_screenshot()` (see |MiniTest-child-neovim.get_screenshot()|).
If `nil`, expectation passed.
{path} `(string|nil)` Path to reference screenshot. If `nil`, constructed
automatically in directory 'tests/screenshots' from current case info and
total number of times it was called inside current case. If there is no
file at `path`, it is created with content of `screenshot`.
{opts} `(table|nil)` Options:
- <force> - whether to forcefuly create reference screenshot.
Temporary useful during test writing. Default: `false`.
------------------------------------------------------------------------------
*MiniTest.new_expectation()*
`MiniTest.new_expectation`({subject}, {predicate}, {fail_context})
Create new expectation function
Helper for writing custom functions with behavior similar to other methods
of |MiniTest.expect|.
Parameters~
{subject} `(string|function)` Subject of expectation. If function, called with
expectation input arguments to produce string value.
{predicate} `(function)` Predicate function. Called with expectation input
arguments. Output `false` or `nil` means failed expectation.
{fail_context} `(string|function)` Information about fail. If function, called
with expectation input arguments to produce string value.
Return~
`(function)` Expectation function.
Usage~
>
local expect_truthy = MiniTest.new_expectation(
'truthy',
function(x) return x end,
function(x) return 'Object: ' .. vim.inspect(x) end
)
------------------------------------------------------------------------------
*MiniTest.gen_reporter*
`MiniTest.gen_reporter`
Table with pre-configured report generators
Each element is a function which returns reporter - table with callable
`start`, `update`, and `finish` fields.
------------------------------------------------------------------------------
*MiniTest.gen_reporter.buffer()*
`MiniTest.gen_reporter.buffer`({opts})
Generate buffer reporter
This is a default choice for interactive (not headless) usage. Opens a window
with dedicated non-terminal buffer and updates it with throttled redraws.
Opened buffer has the following helpful Normal mode mappings:
- `<Esc>` - stop test execution if executing (see |MiniTest.is_executing()|
and |MiniTest.stop()|). Close window otherwise.
- `q` - same as `<Esc>` for convenience and compatibility.
General idea:
- Group cases by concatenating first `opts.group_depth` elements of case
description (`desc` field). Groups by collected files if using default values.
- In `start()` show some stats to know how much is scheduled to be executed.
- In `update()` show symbolic overview of current group and state of current
case. Each symbol represents one case and its state:
- `?` - case didn't finish executing.
- `o` - pass.
- `O` - pass with notes.
- `x` - fail.
- `X` - fail with notes.
- In `finish()` show all fails and notes ordered by case.
Parameters~
{opts} `(table|nil)` Table with options. Used fields:
- <group_depth> - number of first elements of case description (can be zero)
used for grouping. Higher values mean higher granularity of output.
Default: 1.
- <throttle_delay> - minimum number of milliseconds to wait between
redrawing. Reduces screen flickering but not amount of computations.
Default: 10.
- <window> - definition of window to open. Can take one of the forms:
- Callable. It is called expecting output to be target window id
(current window is used if output is `nil`). Use this to open in
"normal" window (like `function() vim.cmd('vsplit') end`).
- Table. Used as `config` argument in |nvim_open_win()|.
Default: table for centered floating window.
------------------------------------------------------------------------------
*MiniTest.gen_reporter.stdout()*
`MiniTest.gen_reporter.stdout`({opts})
Generate stdout reporter
This is a default choice for headless usage. Writes to `stdout`. Uses
coloring ANSI escape sequences to make pretty and informative output
(should work in most modern terminals and continuous integration providers).
It has same general idea as |MiniTest.gen_reporter.buffer()| with slightly
less output (it doesn't overwrite previous text) to overcome typical
terminal limitations.
Parameters~
{opts} `(table|nil)` Table with options. Used fields:
- <group_depth> - number of first elements of case description (can be zero)
used for grouping. Higher values mean higher granularity of output.
Default: 1.
- <quit_on_finish> - whether to quit after finishing test execution.
Default: `true`.
------------------------------------------------------------------------------
*MiniTest.new_child_neovim()*
`MiniTest.new_child_neovim`()
Create child Neovim process
This creates an object designed to be a fundamental piece of 'mini.test'
methodology. It can start/stop/restart a separate (child) Neovim process in
full (non-headless) mode together with convenience helpers to interact with
it through |RPC| messages.
For more information see |MiniTest-child-neovim|.
Return~
`child` Object of |MiniTest-child-neovim|.
Usage~
>
-- Initiate
local child = MiniTest.new_child_neovim()
child.start()
-- Use API functions
child.api.nvim_buf_set_lines(0, 0, -1, true, { 'Line inside child Neovim' })
-- Execute Lua code, Vimscript commands, etc.
child.lua('_G.n = 0')
child.cmd('au CursorMoved * lua _G.n = _G.n + 1')
child.type_keys('l')
print(child.lua_get('_G.n')) -- Should be 1
-- Use other `vim.xxx` Lua wrappers (executed inside child process)
vim.b.aaa = 'current process'
child.b.aaa = 'child process'
print(child.lua_get('vim.b.aaa')) -- Should be 'child process'
-- Always stop process after it is not needed
child.stop()
------------------------------------------------------------------------------
*MiniTest-child-neovim*
Child class
It offers a great set of tools to write reliable and reproducible tests by
allowing to use fresh process in any test action. Interaction with it is done
through |RPC| protocol.
Although quite flexible, at the moment it has certain limitations:
- Doesn't allow using functions or userdata for child's both inputs and
outputs. Usual solution is to move computations from current Neovim process
to child process. Use `child.lua()` and `child.lua_get()` for that.
- When writing tests, it is common to end up with "hanging" process: it
stops executing without any output. Most of the time it is because Neovim
process is "blocked", i.e. it waits for user input and won't return from
other call (like `child.api.nvim_exec_lua()`). Common causes are active
|hit-enter-prompt| (increase prompt height to a bigger value) or
Operator-pending mode (exit it). To mitigate this experience, most helpers
will throw an error if its immediate execution will lead to hanging state.
Also in case of hanging state try `child.api_notify` instead of `child.api`.
Notes:
- An important type of field is a "redirection table". It acts as a
convenience wrapper for corresponding `vim.*` table. Can be used both to
return and set values. Examples:
- `child.api.nvim_buf_line_count(0)` will execute
`vim.api.nvim_buf_line_count(0)` inside child process and return its
output to current process.
- `child.bo.filetype = 'lua'` will execute `vim.bo.filetype = 'lua'`
inside child process.
They still have same limitations listed above, so are not perfect. In
case of a doubt, use `child.lua()`.
- Almost all methods use |vim.rpcrequest()| (i.e. wait for call to finish and
then return value). See for `*_notify` variant to use |vim.rpcnotify()|.
- All fields and methods should be called with `.`, not `:`.
Class~
{child}
Fields~
{start} `(function)` Start child process. See |MiniTest-child-neovim.start()|.
{stop} `(function)` Stop current child process.
{restart} `(function)` Restart child process: stop if running and then
start a new one. Takes same arguments as `child.start()` but uses values
from most recent `start()` call as defaults.
{type_keys} `(function)` Emulate typing keys.
See |MiniTest-child-neovim.type_keys()|. Doesn't check for blocked state.
{cmd} `(function)` Execute Vimscript code from a string.
A wrapper for |nvim_exec()| without capturing output.
{cmd_capture} `(function)` Execute Vimscript code from a string and
capture output. A wrapper for |nvim_exec()| with capturing output.
{lua} `(function)` Execute Lua code. A wrapper for |nvim_exec_lua()|.
{lua_get} `(function)` Execute Lua code and return result. A wrapper
for |nvim_exec_lua()| but prepends string code with `return`.
{is_blocked} `(function)` Check whether child process is blocked.
{is_running} `(function)` Check whether child process is currently running.
{ensure_normal_mode} `(function)` Ensure normal mode.
{get_screenshot} `(function)` Returns table with two "2d arrays" of single
characters representing what is displayed on screen and how it looks.
Note: works only in Neovim>=0.6. See |MiniTest-child-neovim.get_screenshot()|.
{job} `(table|nil)` Information about current job. If `nil`, child is not running.
{api} `(table)` Redirection table for `vim.api`. Doesn't check for blocked state.
{api_notify} `(table)` Same as `api`, but uses |vim.rpcnotify()|.
{diagnostic} `(table)` Redirection table for |vim.diagnostic|.
{fn} `(table)` Redirection table for |vim.fn|.
{highlight} `(table)` Redirection table for `vim.highlight` (|lua-highlight)|.
{json} `(table)` Redirection table for `vim.json`.
{loop} `(table)` Redirection table for |vim.loop|.
{lsp} `(table)` Redirection table for `vim.lsp` (|lsp-core)|.
{mpack} `(table)` Redirection table for |vim.mpack|.
{spell} `(table)` Redirection table for |vim.spell|.
{treesitter} `(table)` Redirection table for |vim.treesitter|.
{ui} `(table)` Redirection table for `vim.ui` (|lua-ui|). Currently of no
use because it requires sending function through RPC, which is impossible
at the moment.
{g} `(table)` Redirection table for |vim.g|.
{b} `(table)` Redirection table for |vim.b|.
{w} `(table)` Redirection table for |vim.w|.
{t} `(table)` Redirection table for |vim.t|.
{v} `(table)` Redirection table for |vim.v|.
{env} `(table)` Redirection table for |vim.env|.
{o} `(table)` Redirection table for |vim.o|.
{go} `(table)` Redirection table for |vim.go|.
{bo} `(table)` Redirection table for |vim.bo|.
{wo} `(table)` Redirection table for |vim.wo|.
------------------------------------------------------------------------------
*MiniTest-child-neovim.start()*
child.start(args, opts)~
Start child process and connect to it. Won't work if child is already running.
Parameters~
{args} `(table)` Array with arguments for executable. Will be prepended
with `{'--clean', '-n', '--listen', <some address>}` (see |startup-options|).
{opts} `(table)` Options:
- <nvim_executable> - name of Neovim executable. Default: |v:progpath|.
- <connection_timeout> - stop trying to connect after this amount of
milliseconds. Default: 5000.
Usage~
>
child = MiniTest.new_child_neovim()
-- Start default clean Neovim instance
child.start()
-- Start with custom 'init.lua' file
child.start({ '-u', 'scripts/minimal_init.lua' })
------------------------------------------------------------------------------
*MiniTest-child-neovim.type_keys()*
child.type_keys(wait, ...)~
Basically a wrapper for |nvim_input()| applied inside child process.
Differences:
- Can wait after each group of characters.
- Raises error if typing keys resulted into error in child process (i.e. its
|v:errmsg| was updated).
- Key '<' as separate entry may not be escaped as '<LT>'.
Parameters~
{wait} `(number)` Number of milliseconds to wait after each entry. May be
omitted, in which case no waiting is done.
{...} `(string|table<number,string>)` Separate entries for |nvim_input()|,
after which `wait` will be applied. Can be either string or array of strings.
Usage~
>
-- All of these type keys 'c', 'a', 'w'
child.type_keys('caw')
child.type_keys('c', 'a', 'w')
child.type_keys('c', { 'a', 'w' })
-- Waits 5 ms after `c` and after 'w'
child.type_keys(5, 'c', { 'a', 'w' })
-- Special keys can also be used
child.type_keys('i', 'Hello world', '<Esc>')
------------------------------------------------------------------------------
*MiniTest-child-neovim.get_screenshot()*
child.get_screenshot()~
Compute what is displayed on (default TUI) screen and how it is displayed.
This basically calls |screenstring()| and |screenattr()| for every visible
cell (row from 1 to 'lines', column from 1 to 'columns').
Notes:
- This requires Neovim>=0.6 as `screenstring()` was introduced only in 0.6.
- Due to implementation details of `screenstring()` and `screenattr()` in
Neovim<=0.7, this function won't recognize floating windows displayed on
screen. It will throw an error if there is a visible floating window. Use
Neovim>=0.8 (current nightly) to properly handle floating windows. Details:
- https://github.com/neovim/neovim/issues/19013
- https://github.com/neovim/neovim/pull/19020
- To make output more portable and visually useful, outputs of
`screenattr()` are coded with single character symbols. Those are taken from
94 characters (ASCII codes between 33 and 126), so there will be duplicates
in case of more than 94 different ways text is displayed on screen.
Return~
`(table|nil)` Screenshot table with the following fields:
- <text> - "2d array" (row-column) of single characters displayed at
particular cells.
- <attr> - "2d array" (row-column) of symbols representing how text is
displayed (basically, "coded" appearance/highlighting). They should be
used only in relation to each other: same/different symbols for two
cells mean same/different visual appearance. Note: there will be false
positives if there are more than 94 different attribute values.
It also can be used with `tostring()` to convert to single string (used
for writing to reference file). It results into two visual parts
(separated by empty line), for `text` and `attr`. Each part has "ruler"
above content and line numbers for each line.
Returns `nil` if couldn't get a reasonable screenshot.
Usage~
>
local screenshot = child.get_screenshot()
-- Show character displayed row=3 and column=4
print(screenshot.text[3][4])
-- Convert to string
tostring(screenshot)
vim:tw=78:ts=8:noet:ft=help:norl:

View File

@ -0,0 +1,95 @@
==============================================================================
------------------------------------------------------------------------------
*mini.trailspace*
*MiniTrailspace*
Work with trailing whitespace
Features:
- Highlighting is done only in modifiable buffer by default, only in Normal
mode, and stops in Insert mode and when leaving window.
- Trim all trailing whitespace with |MiniTrailspace.trim()|.
- Trim all trailing empty lines with |MiniTrailspace.trim_last_lines()|.
# Setup~
This module needs a setup with `require('mini.trailspace').setup({})`
(replace `{}` with your `config` table). It will create global Lua table
`MiniTrailspace` which you can use for scripting or manually (with
`:lua MiniTrailspace.*`).
See |MiniTrailspace.config| for `config` structure and default values.
You can override runtime config settings locally to buffer inside
`vim.b.minitrailspace_config` which should have same structure as
`MiniTrailspace.config`. See |mini.nvim-buffer-local-config| for more details.
# Highlight groups~
* `MiniTrailspace` - highlight group for trailing space.
To change any highlight group, modify it directly with |:highlight|.
# Disabling~
To disable, set `g:minitrailspace_disable` (globally) or
`b:minitrailspace_disable` (for a buffer) to `v:true`. Considering high
number of different scenarios and customization intentions, writing exact
rules for disabling module's functionality is left to user. See
|mini.nvim-disabling-recipes| for common recipes. Note: after disabling
there might be highlighting left; it will be removed after next
highlighting update (see |events| and `MiniTrailspace` |augroup|).
------------------------------------------------------------------------------
*MiniTrailspace.setup()*
`MiniTrailspace.setup`({config})
Module setup
Parameters~
{config} `(table)` Module config table. See |MiniTrailspace.config|.
Usage~
`require('mini.trailspace').setup({})` (replace `{}` with your `config` table)
------------------------------------------------------------------------------
*MiniTrailspace.config*
`MiniTrailspace.config`
Module config
Default values:
>
MiniTrailspace.config = {
-- Highlight only in normal buffers (ones with empty 'buftype'). This is
-- useful to not show trailing whitespace where it usually doesn't matter.
only_in_normal_buffers = true,
}
<
------------------------------------------------------------------------------
*MiniTrailspace.highlight()*
`MiniTrailspace.highlight`()
Highlight trailing whitespace in current window
------------------------------------------------------------------------------
*MiniTrailspace.unhighlight()*
`MiniTrailspace.unhighlight`()
Unhighlight trailing whitespace in current window
------------------------------------------------------------------------------
*MiniTrailspace.trim()*
`MiniTrailspace.trim`()
Trim trailing whitespace
------------------------------------------------------------------------------
*MiniTrailspace.trim_last_lines()*
`MiniTrailspace.trim_last_lines`()
Trim last blank lines
------------------------------------------------------------------------------
*MiniTrailspace.track_normal_buffer()*
`MiniTrailspace.track_normal_buffer`()
Track normal buffer
Designed to be used with |autocmd|. No need to use it directly.
vim:tw=78:ts=8:noet:ft=help:norl:

Some files were not shown because too many files have changed in this diff Show More