commit 86508c397c1fb26a55c8f146e69919cf5527a10b Author: Zane Schaffer Date: Tue Oct 11 16:59:29 2022 -0700 Init diff --git a/about.txt b/about.txt new file mode 100755 index 0000000..5dd577c --- /dev/null +++ b/about.txt @@ -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 + diff --git a/contact.txt b/contact.txt new file mode 100755 index 0000000..74875a5 --- /dev/null +++ b/contact.txt @@ -0,0 +1,5 @@ +mail@zane.town + +zane @ irc.tilde.chat + +zane#9090 @ discord diff --git a/cv/ZaneSchafferCV.pdf b/cv/ZaneSchafferCV.pdf new file mode 100755 index 0000000..ac1f0b2 Binary files /dev/null and b/cv/ZaneSchafferCV.pdf differ diff --git a/cv/index.html b/cv/index.html new file mode 100755 index 0000000..cadfe0d --- /dev/null +++ b/cv/index.html @@ -0,0 +1,826 @@ + + + + + + + resume + + + + + +

Zane Schaffer

+

Musician +with an interest in embedded, firmware development, and DevOps.

+
[ website] . [ git ] . [ z@zane.town ] . [ 206 373 1904 ]
+

Experience

+

Campaign Specialist, Cox Automotive (2021-present, +Remote)

+ +

Front End Web Developer, Zoe Bios Creative +(2020-2021, Remote)

+ +

Skills

+

Programming: Javascript, Go, C, RISC-V, SQL, git, +HTML/CSS

+

Music: Sound Design, Composition, Scoring

+

Achievements

+ +

Projects

+

zane.town +(2021-present)

+ +

Jenga +(2022)

+ + + diff --git a/dotfiles/.gitconfig b/dotfiles/.gitconfig new file mode 100755 index 0000000..e793440 --- /dev/null +++ b/dotfiles/.gitconfig @@ -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 + diff --git a/dotfiles/.tmux.conf b/dotfiles/.tmux.conf new file mode 100755 index 0000000..c639cf5 --- /dev/null +++ b/dotfiles/.tmux.conf @@ -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' diff --git a/dotfiles/.zshrc b/dotfiles/.zshrc new file mode 100755 index 0000000..76a3ae2 --- /dev/null +++ b/dotfiles/.zshrc @@ -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 diff --git a/dotfiles/init.lua b/dotfiles/init.lua new file mode 100755 index 0000000..681dfd5 --- /dev/null +++ b/dotfiles/init.lua @@ -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", "", {noremap = true}) +vim.api.nvim_set_keymap("i", "jk", "", {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", "qf_qf_previous", {noremap = false}) +vim.api.nvim_set_keymap("n", "]q", "qf_qf_next", {noremap = false}) +require("mini.comment").setup() +require("tmux").setup() diff --git a/dotfiles/pack/plugins/start/dirbuf.nvim/.github/workflows/ci.yml b/dotfiles/pack/plugins/start/dirbuf.nvim/.github/workflows/ci.yml new file mode 100755 index 0000000..41cb322 --- /dev/null +++ b/dotfiles/pack/plugins/start/dirbuf.nvim/.github/workflows/ci.yml @@ -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 diff --git a/dotfiles/pack/plugins/start/dirbuf.nvim/.luacheckrc b/dotfiles/pack/plugins/start/dirbuf.nvim/.luacheckrc new file mode 100755 index 0000000..27136c6 --- /dev/null +++ b/dotfiles/pack/plugins/start/dirbuf.nvim/.luacheckrc @@ -0,0 +1,3 @@ +std = luajit +globals = { "vim", "_" } +exclude_files = { "plenary.nvim" } diff --git a/dotfiles/pack/plugins/start/dirbuf.nvim/Justfile b/dotfiles/pack/plugins/start/dirbuf.nvim/Justfile new file mode 100755 index 0000000..54307d9 --- /dev/null +++ b/dotfiles/pack/plugins/start/dirbuf.nvim/Justfile @@ -0,0 +1,7 @@ +all: lint test + +test: + nvim --headless --clean -u tests/test_init.vim +Test + +lint: + luacheck . diff --git a/dotfiles/pack/plugins/start/dirbuf.nvim/LICENSE b/dotfiles/pack/plugins/start/dirbuf.nvim/LICENSE new file mode 100755 index 0000000..be3f7b2 --- /dev/null +++ b/dotfiles/pack/plugins/start/dirbuf.nvim/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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. + + + Copyright (C) + + 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 . + +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 +. diff --git a/dotfiles/pack/plugins/start/dirbuf.nvim/README.md b/dotfiles/pack/plugins/start/dirbuf.nvim/README.md new file mode 100755 index 0000000..0f723e1 --- /dev/null +++ b/dotfiles/pack/plugins/start/dirbuf.nvim/README.md @@ -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 `` 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: +* ``: 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 diff --git a/dotfiles/pack/plugins/start/dirbuf.nvim/doc/dirbuf.txt b/dotfiles/pack/plugins/start/dirbuf.nvim/doc/dirbuf.txt new file mode 100755 index 0000000..700d813 --- /dev/null +++ b/dotfiles/pack/plugins/start/dirbuf.nvim/doc/dirbuf.txt @@ -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 mapping and their default mapping. +If a mapping to the version already exists, then the default mapping is +not made. + +Global ~ + (dirbuf_up) + - Opens the current file's directory or the [count]th parent + directory. + +Buffer-local (filetype=dirbuf) ~ + (dirbuf_up) + - Opens the current file's directory or the [count]th parent + directory. + (dirbuf_enter) + Opens file or directory at cursor. + (dirbuf_toggle_hide) + gh Toggles whether hidden files (i.e. dot files) are + displayed. + (dirbuf_history_forward) + Moves forward [count] times in the directory buffer + history. + (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: diff --git a/dotfiles/pack/plugins/start/dirbuf.nvim/doc/tags b/dotfiles/pack/plugins/start/dirbuf.nvim/doc/tags new file mode 100755 index 0000000..deec078 --- /dev/null +++ b/dotfiles/pack/plugins/start/dirbuf.nvim/doc/tags @@ -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* diff --git a/dotfiles/pack/plugins/start/dirbuf.nvim/ftplugin/dirbuf.vim b/dotfiles/pack/plugins/start/dirbuf.nvim/ftplugin/dirbuf.vim new file mode 100755 index 0000000..70817b4 --- /dev/null +++ b/dotfiles/pack/plugins/start/dirbuf.nvim/ftplugin/dirbuf.vim @@ -0,0 +1,14 @@ +if !hasmapto('(dirbuf_enter)') + nmap (dirbuf_enter) +endif +if !hasmapto('(dirbuf_toggle_hide)') + nmap gh (dirbuf_toggle_hide) +endif +if !hasmapto('(dirbuf_up)') + nmap - (dirbuf_up) +endif + +augroup dirbuf_local + autocmd! * + autocmd BufWriteCmd execute v:lua.require('dirbuf.config').get('write_cmd') +augroup END diff --git a/dotfiles/pack/plugins/start/dirbuf.nvim/lua/dirbuf.lua b/dotfiles/pack/plugins/start/dirbuf.nvim/lua/dirbuf.lua new file mode 100755 index 0000000..3dad030 --- /dev/null +++ b/dotfiles/pack/plugins/start/dirbuf.nvim/lua/dirbuf.lua @@ -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 diff --git a/dotfiles/pack/plugins/start/dirbuf.nvim/lua/dirbuf/buffer.lua b/dotfiles/pack/plugins/start/dirbuf.nvim/lua/dirbuf/buffer.lua new file mode 100755 index 0000000..f7768bb --- /dev/null +++ b/dotfiles/pack/plugins/start/dirbuf.nvim/lua/dirbuf/buffer.lua @@ -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 diff --git a/dotfiles/pack/plugins/start/dirbuf.nvim/lua/dirbuf/config.lua b/dotfiles/pack/plugins/start/dirbuf.nvim/lua/dirbuf/config.lua new file mode 100755 index 0000000..9fc5432 --- /dev/null +++ b/dotfiles/pack/plugins/start/dirbuf.nvim/lua/dirbuf/config.lua @@ -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 diff --git a/dotfiles/pack/plugins/start/dirbuf.nvim/lua/dirbuf/fs.lua b/dotfiles/pack/plugins/start/dirbuf.nvim/lua/dirbuf/fs.lua new file mode 100755 index 0000000..1d6ec81 --- /dev/null +++ b/dotfiles/pack/plugins/start/dirbuf.nvim/lua/dirbuf/fs.lua @@ -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 diff --git a/dotfiles/pack/plugins/start/dirbuf.nvim/lua/dirbuf/planner.lua b/dotfiles/pack/plugins/start/dirbuf.nvim/lua/dirbuf/planner.lua new file mode 100755 index 0000000..9fab07e --- /dev/null +++ b/dotfiles/pack/plugins/start/dirbuf.nvim/lua/dirbuf/planner.lua @@ -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 diff --git a/dotfiles/pack/plugins/start/dirbuf.nvim/plugin/dirbuf.vim b/dotfiles/pack/plugins/start/dirbuf.nvim/plugin/dirbuf.vim new file mode 100755 index 0000000..96e4085 --- /dev/null +++ b/dotfiles/pack/plugins/start/dirbuf.nvim/plugin/dirbuf.vim @@ -0,0 +1,37 @@ +if exists('g:loaded_dirbuf') + finish +endif + +command! -nargs=? -complete=dir Dirbuf lua require'dirbuf'.open() +command! DirbufQuit lua require'dirbuf'.quit() +command! -nargs=? -complete=customlist,s:DirbufSyncOptions DirbufSync lua require'dirbuf'.sync() + +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 (dirbuf_up) execute 'Dirbuf %:p'.repeat(':h', v:count1 + isdirectory(expand('%'))) +noremap (dirbuf_enter) execute 'lua require"dirbuf".enter()' +noremap (dirbuf_toggle_hide) execute 'lua require"dirbuf".toggle_hide()' +noremap (dirbuf_history_forward) execute 'lua require"dirbuf".jump_history('v:count1')' +noremap (dirbuf_history_backward) execute 'lua require"dirbuf".jump_history(-'v:count1')' + +if mapcheck('-', 'n') ==# '' && !hasmapto('(dirbuf_up)', 'n') + nmap - (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 diff --git a/dotfiles/pack/plugins/start/dirbuf.nvim/stylua.toml b/dotfiles/pack/plugins/start/dirbuf.nvim/stylua.toml new file mode 100755 index 0000000..0435f67 --- /dev/null +++ b/dotfiles/pack/plugins/start/dirbuf.nvim/stylua.toml @@ -0,0 +1,2 @@ +indent_type = "Spaces" +indent_width = 2 diff --git a/dotfiles/pack/plugins/start/dirbuf.nvim/syntax/dirbuf.vim b/dotfiles/pack/plugins/start/dirbuf.nvim/syntax/dirbuf.vim new file mode 100755 index 0000000..6341bb3 --- /dev/null +++ b/dotfiles/pack/plugins/start/dirbuf.nvim/syntax/dirbuf.vim @@ -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 diff --git a/dotfiles/pack/plugins/start/dirbuf.nvim/tests/buffer_spec.lua b/dotfiles/pack/plugins/start/dirbuf.nvim/tests/buffer_spec.lua new file mode 100755 index 0000000..14fa0bc --- /dev/null +++ b/dotfiles/pack/plugins/start/dirbuf.nvim/tests/buffer_spec.lua @@ -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) diff --git a/dotfiles/pack/plugins/start/dirbuf.nvim/tests/config_spec.lua b/dotfiles/pack/plugins/start/dirbuf.nvim/tests/config_spec.lua new file mode 100755 index 0000000..810bf94 --- /dev/null +++ b/dotfiles/pack/plugins/start/dirbuf.nvim/tests/config_spec.lua @@ -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) diff --git a/dotfiles/pack/plugins/start/dirbuf.nvim/tests/e2e_spec.lua b/dotfiles/pack/plugins/start/dirbuf.nvim/tests/e2e_spec.lua new file mode 100755 index 0000000..8f27165 --- /dev/null +++ b/dotfiles/pack/plugins/start/dirbuf.nvim/tests/e2e_spec.lua @@ -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) diff --git a/dotfiles/pack/plugins/start/dirbuf.nvim/tests/planner_spec.lua b/dotfiles/pack/plugins/start/dirbuf.nvim/tests/planner_spec.lua new file mode 100755 index 0000000..429ca21 --- /dev/null +++ b/dotfiles/pack/plugins/start/dirbuf.nvim/tests/planner_spec.lua @@ -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) diff --git a/dotfiles/pack/plugins/start/dirbuf.nvim/tests/test_init.vim b/dotfiles/pack/plugins/start/dirbuf.nvim/tests/test_init.vim new file mode 100755 index 0000000..17ffd3b --- /dev/null +++ b/dotfiles/pack/plugins/start/dirbuf.nvim/tests/test_init.vim @@ -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'} diff --git a/dotfiles/pack/plugins/start/filetype.nvim/README.md b/dotfiles/pack/plugins/start/filetype.nvim/README.md new file mode 100755 index 0000000..70be126 --- /dev/null +++ b/dotfiles/pack/plugins/start/filetype.nvim/README.md @@ -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** + +
+Sample log + + ```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 + ``` +
+ + +### With `filetype.nvim` + +Average startup time (100 rounds): **26.492 ms** + +
+ Sample log + + ```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 + ``` +
+ +\* 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` diff --git a/dotfiles/pack/plugins/start/filetype.nvim/filetype.vim b/dotfiles/pack/plugins/start/filetype.nvim/filetype.vim new file mode 100755 index 0000000..532a3d7 --- /dev/null +++ b/dotfiles/pack/plugins/start/filetype.nvim/filetype.vim @@ -0,0 +1,6 @@ +let g:did_load_filetypes = 1 + +augroup filetypedetect + au! + au BufNewFile,BufRead * lua require('filetype').resolve() +augroup END diff --git a/dotfiles/pack/plugins/start/filetype.nvim/lua/filetype/init.lua b/dotfiles/pack/plugins/start/filetype.nvim/lua/filetype/init.lua new file mode 100755 index 0000000..723fe0e --- /dev/null +++ b/dotfiles/pack/plugins/start/filetype.nvim/lua/filetype/init.lua @@ -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("") + 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 diff --git a/dotfiles/pack/plugins/start/filetype.nvim/lua/filetype/mappings/complex.lua b/dotfiles/pack/plugins/start/filetype.nvim/lua/filetype/mappings/complex.lua new file mode 100755 index 0000000..4e67903 --- /dev/null +++ b/dotfiles/pack/plugins/start/filetype.nvim/lua/filetype/mappings/complex.lua @@ -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 diff --git a/dotfiles/pack/plugins/start/filetype.nvim/lua/filetype/mappings/extensions.lua b/dotfiles/pack/plugins/start/filetype.nvim/lua/filetype/mappings/extensions.lua new file mode 100755 index 0000000..877a184 --- /dev/null +++ b/dotfiles/pack/plugins/start/filetype.nvim/lua/filetype/mappings/extensions.lua @@ -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", +} diff --git a/dotfiles/pack/plugins/start/filetype.nvim/lua/filetype/mappings/function.lua b/dotfiles/pack/plugins/start/filetype.nvim/lua/filetype/mappings/function.lua new file mode 100755 index 0000000..399dc14 --- /dev/null +++ b/dotfiles/pack/plugins/start/filetype.nvim/lua/filetype/mappings/function.lua @@ -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("") 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("") ~= "read.me" + and vim.fn.expand("") ~= "click.me" + then + return "nroff" + end + end, + ["m4"] = function() + if not vim.fn.expand(""):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 diff --git a/dotfiles/pack/plugins/start/filetype.nvim/lua/filetype/mappings/literal.lua b/dotfiles/pack/plugins/start/filetype.nvim/lua/filetype/mappings/literal.lua new file mode 100755 index 0000000..3aee760 --- /dev/null +++ b/dotfiles/pack/plugins/start/filetype.nvim/lua/filetype/mappings/literal.lua @@ -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", +} diff --git a/dotfiles/pack/plugins/start/filetype.nvim/misc/convert.py b/dotfiles/pack/plugins/start/filetype.nvim/misc/convert.py new file mode 100755 index 0000000..7477f61 --- /dev/null +++ b/dotfiles/pack/plugins/start/filetype.nvim/misc/convert.py @@ -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) diff --git a/dotfiles/pack/plugins/start/filetype.nvim/stylua.toml b/dotfiles/pack/plugins/start/filetype.nvim/stylua.toml new file mode 100755 index 0000000..59af393 --- /dev/null +++ b/dotfiles/pack/plugins/start/filetype.nvim/stylua.toml @@ -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 diff --git a/dotfiles/pack/plugins/start/impatient.nvim/.github/workflows/ci.yml b/dotfiles/pack/plugins/start/impatient.nvim/.github/workflows/ci.yml new file mode 100755 index 0000000..83964bb --- /dev/null +++ b/dotfiles/pack/plugins/start/impatient.nvim/.github/workflows/ci.yml @@ -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 diff --git a/dotfiles/pack/plugins/start/impatient.nvim/LICENSE b/dotfiles/pack/plugins/start/impatient.nvim/LICENSE new file mode 100755 index 0000000..867f510 --- /dev/null +++ b/dotfiles/pack/plugins/start/impatient.nvim/LICENSE @@ -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. diff --git a/dotfiles/pack/plugins/start/impatient.nvim/Makefile b/dotfiles/pack/plugins/start/impatient.nvim/Makefile new file mode 100755 index 0000000..3e8ccb8 --- /dev/null +++ b/dotfiles/pack/plugins/start/impatient.nvim/Makefile @@ -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 diff --git a/dotfiles/pack/plugins/start/impatient.nvim/README.md b/dotfiles/pack/plugins/start/impatient.nvim/README.md new file mode 100755 index 0000000..f4ae90f --- /dev/null +++ b/dotfiles/pack/plugins/start/impatient.nvim/README.md @@ -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. + +
+Standard + +``` +────────────┬────────────┐ + 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 +────────────┴────────────┴───────────────────────────────────────────────────────────────── +``` +
+ +Total resolve: 54.337ms, total load: 34.257ms + +
+With cache + +``` +────────────┬────────────┐ + 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 +────────────┴────────────┴───────────────────────────────────────────────────────────────── +``` + +
+ +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. diff --git a/dotfiles/pack/plugins/start/impatient.nvim/lua/impatient.lua b/dotfiles/pack/plugins/start/impatient.nvim/lua/impatient.lua new file mode 100755 index 0000000..fe0f4d2 --- /dev/null +++ b/dotfiles/pack/plugins/start/impatient.nvim/lua/impatient.lua @@ -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 = { + [''] = os.getenv('APPDIR'), + [''] = os.getenv('VIMRUNTIME'), + [''] = vim.fn.stdpath('data'), + [''] = 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 diff --git a/dotfiles/pack/plugins/start/impatient.nvim/lua/impatient/profile.lua b/dotfiles/pack/plugins/start/impatient.nvim/lua/impatient/profile.lua new file mode 100755 index 0000000..d7bcab5 --- /dev/null +++ b/dotfiles/pack/plugins/start/impatient.nvim/lua/impatient/profile.lua @@ -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 diff --git a/dotfiles/pack/plugins/start/impatient.nvim/test/impatient_spec.lua b/dotfiles/pack/plugins/start/impatient.nvim/test/impatient_spec.lua new file mode 100755 index 0000000..639a57c --- /dev/null +++ b/dotfiles/pack/plugins/start/impatient.nvim/test/impatient_spec.lua @@ -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) diff --git a/dotfiles/pack/plugins/start/impatient.nvim/test/lua/plugins.lua b/dotfiles/pack/plugins/start/impatient.nvim/test/lua/plugins.lua new file mode 100755 index 0000000..004c089 --- /dev/null +++ b/dotfiles/pack/plugins/start/impatient.nvim/test/lua/plugins.lua @@ -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' diff --git a/dotfiles/pack/plugins/start/impatient.nvim/test/preload.lua b/dotfiles/pack/plugins/start/impatient.nvim/test/preload.lua new file mode 100755 index 0000000..7facdde --- /dev/null +++ b/dotfiles/pack/plugins/start/impatient.nvim/test/preload.lua @@ -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')() diff --git a/dotfiles/pack/plugins/start/mini.nvim/.github/ISSUE_TEMPLATE/bug-report.yml b/dotfiles/pack/plugins/start/mini.nvim/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100755 index 0000000..671754d --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/.github/ISSUE_TEMPLATE/bug-report.yml @@ -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 diff --git a/dotfiles/pack/plugins/start/mini.nvim/.github/ISSUE_TEMPLATE/config.yml b/dotfiles/pack/plugins/start/mini.nvim/.github/ISSUE_TEMPLATE/config.yml new file mode 100755 index 0000000..0086358 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: true diff --git a/dotfiles/pack/plugins/start/mini.nvim/.github/ISSUE_TEMPLATE/feature-request.yml b/dotfiles/pack/plugins/start/mini.nvim/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100755 index 0000000..4bbada4 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/.github/ISSUE_TEMPLATE/feature-request.yml @@ -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 diff --git a/dotfiles/pack/plugins/start/mini.nvim/.github/PULL_REQUEST_TEMPLATE.md b/dotfiles/pack/plugins/start/mini.nvim/.github/PULL_REQUEST_TEMPLATE.md new file mode 100755 index 0000000..6031157 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/.github/PULL_REQUEST_TEMPLATE.md @@ -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) diff --git a/dotfiles/pack/plugins/start/mini.nvim/.github/workflows/lint.yml b/dotfiles/pack/plugins/start/mini.nvim/.github/workflows/lint.yml new file mode 100755 index 0000000..9a76c4c --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/.github/workflows/lint.yml @@ -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 diff --git a/dotfiles/pack/plugins/start/mini.nvim/.github/workflows/tests.yml b/dotfiles/pack/plugins/start/mini.nvim/.github/workflows/tests.yml new file mode 100755 index 0000000..f981e47 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/.github/workflows/tests.yml @@ -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 diff --git a/dotfiles/pack/plugins/start/mini.nvim/.gitignore b/dotfiles/pack/plugins/start/mini.nvim/.gitignore new file mode 100755 index 0000000..e78a541 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/.gitignore @@ -0,0 +1,2 @@ +doc/tags +deps diff --git a/dotfiles/pack/plugins/start/mini.nvim/.pre-commit-config.yaml b/dotfiles/pack/plugins/start/mini.nvim/.pre-commit-config.yaml new file mode 100755 index 0000000..2665844 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/.pre-commit-config.yaml @@ -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: [] diff --git a/dotfiles/pack/plugins/start/mini.nvim/.stylua.toml b/dotfiles/pack/plugins/start/mini.nvim/.stylua.toml new file mode 100755 index 0000000..7774cde --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/.stylua.toml @@ -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" diff --git a/dotfiles/pack/plugins/start/mini.nvim/.styluaignore b/dotfiles/pack/plugins/start/mini.nvim/.styluaignore new file mode 100755 index 0000000..3f9cfaf --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/.styluaignore @@ -0,0 +1 @@ +deps diff --git a/dotfiles/pack/plugins/start/mini.nvim/CHANGELOG.md b/dotfiles/pack/plugins/start/mini.nvim/CHANGELOG.md new file mode 100755 index 0000000..b0c8b54 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/CHANGELOG.md @@ -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 = , extract = }` it is now `{ }`. 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 `` to stop selecting target. + +## mini.jump2d + +Introduction of new module. + +## mini.pairs + +- Create mappings for `` and `` 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 `` and `` 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 `` 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 `` 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 `` and ``. 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. diff --git a/dotfiles/pack/plugins/start/mini.nvim/CODE_OF_CONDUCT.md b/dotfiles/pack/plugins/start/mini.nvim/CODE_OF_CONDUCT.md new file mode 100755 index 0000000..6c0a89d --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/CODE_OF_CONDUCT.md @@ -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 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 diff --git a/dotfiles/pack/plugins/start/mini.nvim/CONTRIBUTING.md b/dotfiles/pack/plugins/start/mini.nvim/CONTRIBUTING.md new file mode 100755 index 0000000..86a4044 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/CONTRIBUTING.md @@ -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= make test_file` to run tests only from 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` diff --git a/dotfiles/pack/plugins/start/mini.nvim/LICENSE b/dotfiles/pack/plugins/start/mini.nvim/LICENSE new file mode 100755 index 0000000..782731f --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/LICENSE @@ -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. diff --git a/dotfiles/pack/plugins/start/mini.nvim/Makefile b/dotfiles/pack/plugins/start/mini.nvim/Makefile new file mode 100755 index 0000000..ac7e8b8 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/Makefile @@ -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 diff --git a/dotfiles/pack/plugins/start/mini.nvim/README.md b/dotfiles/pack/plugins/start/mini.nvim/README.md new file mode 100755 index 0000000..4717501 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/README.md @@ -0,0 +1,324 @@ +
+ + +[![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) + + +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 : 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) | + + +### 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`, `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). + +--- + + +### 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). + +--- + + +### 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). + +--- + + +### 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). + +--- + + +### 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). + +--- + + +### 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). + +--- + + +### 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). + +--- + + +### 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). + +--- + + +### 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). + +--- + + +### 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). + +--- + + +### 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). + +--- + + +### 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). + +--- + + +### 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). + +--- + + +### 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). + +--- + + +### 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). + +--- + + +### 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). + +--- + + +### 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). + +--- + + +### 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). + +--- + + +### 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). + +--- + + +### 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). + +--- + + +### 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().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). diff --git a/dotfiles/pack/plugins/start/mini.nvim/TESTING.md b/dotfiles/pack/plugins/start/mini.nvim/TESTING.md new file mode 100755 index 0000000..6a0f897 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/TESTING.md @@ -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: + +
'hello_lines/init.lua' + +```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 +``` + +
+ +## Quick demo + +Here is a quick demo of how tests with 'mini.test' look like: + +
'tests/test_hello_lines.lua' + +```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 +``` + +
+ +## 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): + +
Template for test files + +```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 +``` + +

+ +- **'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: + +
'scripts/minimal_init.lua' + +```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 +``` + +

+ +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: + +
Template for Makefile + +``` +# 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 $@ +``` + +

+ +- **'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: + +
Fail information + +``` +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 = + }, { + args = {}, + data = {}, + desc = { "tests/test_basics.lua", "MiniTest.current.case" }, + hooks = { + post = {}, + pre = {} + }, + test = + } } + 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 = + } + Right: 0 + Traceback: + tests/test_basics.lua:12 +``` + +
+ +### 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') + eq(get_lines(), { 'abcde' }) + eq(child.fn.mode(), 'n') + + -- Or several strings which improves readability + child.type_keys('cc', 'fghij', '') + 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', '') + 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: + +- - "2d array" (row-column) of single characters displayed at particular cells. +- - "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|`) 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'. diff --git a/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/benchmark.sh b/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/benchmark.sh new file mode 100755 index 0000000..0d6ee26 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/benchmark.sh @@ -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}" diff --git a/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/init-files/init_dashboard-alpha.lua b/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/init-files/init_dashboard-alpha.lua new file mode 100755 index 0000000..fa9497f --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/init-files/init_dashboard-alpha.lua @@ -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', 'ff', ':Telescope find_files', { noremap = true, silent = true }) +vim.api.nvim_set_keymap('n', 'fh', ':Telescope oldfiles', { noremap = true, silent = true }) +vim.api.nvim_set_keymap('n', 'fr', ':Telescope jumplist', { noremap = true, silent = true }) +vim.api.nvim_set_keymap('n', 'fg', ':Telescope live_grep', { noremap = true, silent = true }) +vim.api.nvim_set_keymap('n', 'fm', ':Telescope marks', { noremap = true, silent = true }) +vim.api.nvim_set_keymap('n', 'sl', ':Telescope command_history', { 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()) diff --git a/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/init-files/init_dashboard-original.lua b/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/init-files/init_dashboard-original.lua new file mode 100755 index 0000000..afaf266 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/init-files/init_dashboard-original.lua @@ -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', 'ss', ':SessionSave', {}) +vim.api.nvim_set_keymap('n', 'sl', ':SessionLoad', {}) + +vim.api.nvim_set_keymap('n', 'fh', ':DashboardFindHistory', { noremap = true, silent = true }) +vim.api.nvim_set_keymap('n', 'ff', ':DashboardFindFile', { noremap = true, silent = true }) +vim.api.nvim_set_keymap('n', 'tc', ':DashboardChangeColorscheme', { noremap = true, silent = true }) +vim.api.nvim_set_keymap('n', 'fa', ':DashboardFindWord', { noremap = true, silent = true }) +vim.api.nvim_set_keymap('n', 'fb', ':DashboardJumpMark', { noremap = true, silent = true }) +vim.api.nvim_set_keymap('n', 'cn', ':DashboardNewFile', { 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()) diff --git a/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/init-files/init_dashboard-starter.lua b/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/init-files/init_dashboard-starter.lua new file mode 100755 index 0000000..f9593e2 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/init-files/init_dashboard-starter.lua @@ -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()) diff --git a/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/init-files/init_empty.lua b/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/init-files/init_empty.lua new file mode 100755 index 0000000..f7f2c15 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/init-files/init_empty.lua @@ -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()) diff --git a/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/init-files/init_starter-default.lua b/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/init-files/init_starter-default.lua new file mode 100755 index 0000000..515afb3 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/init-files/init_starter-default.lua @@ -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()) diff --git a/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/init-files/init_startify-alpha.lua b/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/init-files/init_startify-alpha.lua new file mode 100755 index 0000000..ec7b477 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/init-files/init_startify-alpha.lua @@ -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()) diff --git a/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/init-files/init_startify-original.lua b/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/init-files/init_startify-original.lua new file mode 100755 index 0000000..fe2640f --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/init-files/init_startify-original.lua @@ -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()) diff --git a/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/init-files/init_startify-starter.lua b/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/init-files/init_startify-starter.lua new file mode 100755 index 0000000..743daf8 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/init-files/init_startify-starter.lua @@ -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()) diff --git a/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/install.sh b/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/install.sh new file mode 100755 index 0000000..44580fd --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/install.sh @@ -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 diff --git a/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/make_summary.py b/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/make_summary.py new file mode 100755 index 0000000..a3a2e67 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/make_summary.py @@ -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() diff --git a/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/startup-summary.md b/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/startup-summary.md new file mode 100755 index 0000000..3495085 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/benchmarks/starter/startup-summary.md @@ -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 | diff --git a/dotfiles/pack/plugins/start/mini.nvim/colors/minicyan.lua b/dotfiles/pack/plugins/start/mini.nvim/colors/minicyan.lua new file mode 100755 index 0000000..a7fcf2e --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/colors/minicyan.lua @@ -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 diff --git a/dotfiles/pack/plugins/start/mini.nvim/colors/minischeme.lua b/dotfiles/pack/plugins/start/mini.nvim/colors/minischeme.lua new file mode 100755 index 0000000..7306e8b --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/colors/minischeme.lua @@ -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 diff --git a/dotfiles/pack/plugins/start/mini.nvim/doc/mini-ai.txt b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-ai.txt new file mode 100755 index 0000000..f310468 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-ai.txt @@ -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`, +`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 >> | | | | | + |---|---------------|-1234567890123456-|--------|--------|--------|--------| + | ) | Balanced () | (( *a (bb) )) | | | | | + | ] | Balanced [] | [[ *a [bb] ]] | | | | | + | } | Balanced {} | {{ *a {bb} }} | [2;12] | [3;11] | [1;13] | [2;12] | + | > | Balanced <> | << *a >> | | | | | + | 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 | *b | [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: and + for inclusive start and end positions ( might be `nil` to + describe empty region). Each position is also a table with line + and column (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: + - - Number of lines within which textobject is searched. + Default: `config.n_lines` (see |MiniAi.config|). + - - Number of times to perform a consecutive search. Each one + is done with reference region being previous found textobject region. + Default: 1. + - - 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. 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: + - - table with patterns for outer balanced brackets. + Default: `{ '%b()', '%b[]', '%b{}' }` (any `()`, `[]`, or `{}` can + enclose arguments). + - - table with single character separators. + Default: `{ ',' }` (arguments are separated with `,`). + - - 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: + - - 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 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//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 + and 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: + - - 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: + - - One of `'v'`, `'V'`, `''`. Default: Latest visual mode. + - - 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: \ No newline at end of file diff --git a/dotfiles/pack/plugins/start/mini.nvim/doc/mini-align.txt b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-align.txt new file mode 100755 index 0000000..9ab841d --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-align.txt @@ -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: + - lines into parts based on Lua pattern(s) or user-supplied rule. + - parts for certain side(s) to be same width inside columns. + - 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 `` (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: and for + inclusive start and end positions ( might be `nil` to describe + empty region). Each position is also a table with line and + column (both start at 1). + +MODE Either charwise ("char", `v`, |charwise|), linewise ("line", `V`, + |linewise|) or blockwise ("block", ``, |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: +- . 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. +- . Take array of strings and convert it to parts with `steps.split()`. + It has `(strings, opts)` signature and should return parts. +- . Take parts and consecutively apply all pre-justify + steps (`steps.pre_justify`). Each one has `(parts, opts)` signature and + should modify parts in place. +- . Take parts and apply `steps.justify()`. It has `(parts, opts)` + signature and should modify parts in place. +- . Take parts and consecutively apply all pre-merge + steps (`steps.pre_merge`). Each one has `(parts, opts)` signature and + should modify parts in place. +- . 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: +- . Initiated via mapping defined in `start` of + `MiniAlign.config.mappings`. Alignment is accepted once split pattern becomes + non-default. +- . Initiated via mapping defined in `start_with_preview` of + `MiniAlign.config.mappings`. Alignment result is shown after every modifier + and is accepted after `` (`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): +- : + - 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 (``). They all behave differently. +- . 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. +- . In case of active preview, accept current result by + pressing ``. Discard any result and return to initial regions with + either `` or ``. + +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 ~ + + Enter split pattern (confirm prompt by pressing ``). Input is treated + as plain delimiter. + + Before: > + a-b-c + aa-bb-cc +< + After typing `s-`: > + a -b -c + aa-bb-cc +< + 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 +< + Enter merge delimiter (confirm prompt by pressing ``). + + Before: > + a_b_c + aa_bb_cc +< + After typing `_m--` (first make split by `_`): > + a --_--b --_--c + aa--_--bb--_--cc +< +Modifiers adding pre-steps ~ + + Enter filter expression. See more details in |MiniAlign.gen_step.filter()|. + + Before: > + a_b_c + aa_bb_cc +< + After typing `_fn==1` (first make split by `_`): > + a _b_c + aa_bb_cc +< + 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 +< +

Pair neighboring parts. + + Before: > + a_b_c + aaa_bbb_ccc +< + After typing `_p` (first make split by `_`): > + a_ b_ c + aaa_bbb_ccc +< + 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 ~ + + 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` results in only "trim" step to be left. + - `it` 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 ``: > + 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!` +- `=p` +- `=i` (execute `:lua vim.o.commentstring = '# %s'` for full experience) +- `=` +- `=p` +- `=fn==1` +- `=fn==1t` +- `=frow>7` + + +------------------------------------------------------------------------------ + *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'] = --, + ['j'] = --, + ['m'] = --, + + -- Modifiers adding pre-steps + ['f'] = --, + ['i'] = --, + ['p'] = --, + ['t'] = --, + + -- Delete some last pre-step + [''] = --, + + -- Special configurations for common splits + ['='] = --, + [' '] = --, + }, + + -- 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 ( has row number, 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 + and 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: with `name` argument, 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): +- - 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+'`, `{ '<', '>' }`. +- - 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): +- - 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". +- - 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): +- - 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 number of current element. +- - total number of rows in parts. +- - column number of current element. +- - total number of columns in current row. +- - string value of current element. +- - column pair number of current element. Useful when filtering by + result of pattern splitting. +- - 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: \ No newline at end of file diff --git a/dotfiles/pack/plugins/start/mini.nvim/doc/mini-base16.txt b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-base16.txt new file mode 100755 index 0000000..3372b9f --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-base16.txt @@ -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: \ No newline at end of file diff --git a/dotfiles/pack/plugins/start/mini.nvim/doc/mini-bufremove.txt b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-bufremove.txt new file mode 100755 index 0000000..503dbd5 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-bufremove.txt @@ -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: \ No newline at end of file diff --git a/dotfiles/pack/plugins/start/mini.nvim/doc/mini-comment.txt b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-comment.txt new file mode 100755 index 0000000..690828a --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-comment.txt @@ -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-|) 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: \ No newline at end of file diff --git a/dotfiles/pack/plugins/start/mini.nvim/doc/mini-completion.txt b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-completion.txt new file mode 100755 index 0000000..a6a29cb --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-completion.txt @@ -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 + ``) or fallback completion via + |MiniCompletion.complete_fallback()| (maped to ``). + +What it doesn't do: +- Snippet expansion. +- Many configurable sources. +- Automatic mapping of ``, ``, 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 `` and `` for navigation through completion list, make +these key mappings: +`vim.api.nvim_set_keymap('i', '', [[pumvisible() ? "\" : "\"]], { noremap = true, expr = true })` +`vim.api.nvim_set_keymap('i', '', [[pumvisible() ? "\" : "\"]], { noremap = true, expr = true })` + +To get more consistent behavior of ``, you can use this template in +your 'init.lua' to make customized mapping: > + local keys = { + ['cr'] = vim.api.nvim_replace_termcodes('', true, true, true), + ['ctrl-y'] = vim.api.nvim_replace_termcodes('', true, true, true), + ['ctrl-y_cr'] = vim.api.nvim_replace_termcodes('', 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 ``. 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', '', '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 = --, + }, + + -- 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 ''. + fallback_action = --` completion>, + + -- Module mappings. Use `''` (empty string) to disable one. Some of them + -- might conflict with system mappings. + mappings = { + force_twostep = '', -- Force two-step completion + force_fallback = '', -- 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: \ No newline at end of file diff --git a/dotfiles/pack/plugins/start/mini.nvim/doc/mini-cursorword.txt b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-cursorword.txt new file mode 100755 index 0000000..2a465b0 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-cursorword.txt @@ -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 ||: 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('') + 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: \ No newline at end of file diff --git a/dotfiles/pack/plugins/start/mini.nvim/doc/mini-doc.txt b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-doc.txt new file mode 100755 index 0000000..e9f2818 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-doc.txt @@ -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 = --, + + -- Applied to section before anything else + section_pre = --, + + -- Applied if section has specified captured id + sections = { + ['@alias'] = --, + ['@class'] = --, + ['@diagnostic'] = --, + -- For most typical usage see |MiniDoc.afterlines_to_code| + ['@eval'] = --, + ['@field'] = --, + ['@overload'] = --, + ['@param'] = --, + ['@private'] = --, + ['@return'] = --, + ['@seealso'] = --, + ['@signature'] = --, + ['@tag'] = --, + ['@text'] = --, + ['@toc'] = --, + ['@toc_entry'] = --, + ['@type'] = --, + ['@usage'] = --, + }, + + -- Applied to section after all previous steps + section_post = --, + + -- Applied to block after all previous steps + block_post = --, + + -- Applied to file after all previous steps + file = --, + + -- Applied to doc after all previous steps + doc = --, + + -- Applied to after output help file is written. Takes doc as argument. + write_post = --, + }, + + -- 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 '`()`' (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 +` 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/.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 ` and `--minidoc_replace_end` + denote lines between them which should be replaced with ``. + 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 = -- + 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 = -- + } +< +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: \ No newline at end of file diff --git a/dotfiles/pack/plugins/start/mini.nvim/doc/mini-fuzzy.txt b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-fuzzy.txt new file mode 100755 index 0000000..55a89df --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-fuzzy.txt @@ -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: \ No newline at end of file diff --git a/dotfiles/pack/plugins/start/mini.nvim/doc/mini-indentscope.txt b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-indentscope.txt new file mode 100755 index 0000000..ce5df73 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-indentscope.txt @@ -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 = --, + }, + + -- 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: + - - table with (top line of scope, inclusive), + (bottom line of scope, inclusive), and (minimum indent withing + scope) keys. Line numbers start at 1. + - - table with (line of top border, might be `nil`), + (line of bottom border, might be `nil`), and (indent + of border) keys. Line numbers start at 1. + - - identifier of current buffer. + - - table with (reference line), (reference + column), and ("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 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: + - `(number)` - duration (in ms) of a unit. Default: 20. + - `(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: \ No newline at end of file diff --git a/dotfiles/pack/plugins/start/mini.nvim/doc/mini-jump.txt b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-jump.txt new file mode 100755 index 0000000..cb95699 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-jump.txt @@ -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: \ No newline at end of file diff --git a/dotfiles/pack/plugins/start/mini.nvim/doc/mini-jump2d.txt b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-jump2d.txt new file mode 100755 index 0000000..2c3bbf9 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-jump2d.txt @@ -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 `` + 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 = '', + }, + } +< +# 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 field. Extra allowed fields: + - - 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', '', + 'lua MiniJump2d.start(MiniJump2d.builtin_opts.line_start)', {} + ) +- 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: \ No newline at end of file diff --git a/dotfiles/pack/plugins/start/mini.nvim/doc/mini-misc.txt b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-misc.txt new file mode 100755 index 0000000..35b02ed --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-misc.txt @@ -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): , , , , + (number of elements), (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:`). 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: \ No newline at end of file diff --git a/dotfiles/pack/plugins/start/mini.nvim/doc/mini-pairs.txt b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-pairs.txt new file mode 100755 index 0000000..915b13a --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-pairs.txt @@ -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 `` and ``. + +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', <*>, )` : make new mapping + for '<*>' in current buffer. + - `lua MiniPairs.unmap_buf(0, 'i', <*>, )`: unmap key `<*>` while + unregistering `` 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 <*> <*>` (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 ``: > + 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 `` 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|): + -- - - one of "open", "close", "closeopen". + -- - - two character string for pair to be used. + -- By default pair is not inserted after `\`, quotes are not recognized by + -- ``, `'` 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 `` (in |MiniPairs.bs|) and/or `` (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 `` and ``. +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 `` and ``. +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 + `` and ``. 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 <*> <*>` (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 + `` and ``. 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 ||. + +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 ||) 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 ||) 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 || + +Used as |map-expr| mapping for ``. It removes whole pair (via +``) 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_| + +Used as |map-expr| mapping for `` in insert mode. It puts "close" +symbol on next line (via `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: \ No newline at end of file diff --git a/dotfiles/pack/plugins/start/mini.nvim/doc/mini-sessions.txt b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-sessions.txt new file mode 100755 index 0000000..1d77e71 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-sessions.txt @@ -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): +- `(number)` modification time (see |getftime|) of session file. +- `(string)` name of session (should be equal to table key). +- `(string)` full path to session file. +- `(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: + - (whether to delete unsaved buffers; default: + `MiniSessions.config.force.read`). + - (whether to print session path after action; default + `MiniSessions.config.verbose.read`). + - (a table with

 and  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:
+  -  (whether to ignore existence of session file; default:
+    `MiniSessions.config.force.write`).
+  -  (whether to print session path after action; default
+    `MiniSessions.config.verbose.write`).
+  -  (a table with 
 and  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:
+  -  (whether to allow deletion of current session; default:
+    `MiniSessions.config.force.delete`).
+  -  (whether to print session path after action; default
+    `MiniSessions.config.verbose.delete`).
+  -  (a table with 
 and  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:
\ No newline at end of file
diff --git a/dotfiles/pack/plugins/start/mini.nvim/doc/mini-starter.txt b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-starter.txt
new file mode 100755
index 0000000..7379af3
--- /dev/null
+++ b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-starter.txt
@@ -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:
+    -  - function or string for |vim.cmd| which is executed when
+      item is chosen. Empty string result in placeholder "inactive" item.
+    -  - string which will be displayed and used for choosing.
+    - 
- 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|. + - deletes latest character from query. + - /, /, / move current item. + - executes action of current item. + - closes Starter buffer. +- Evaluate current item when appropriate (after `` 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 , , and
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 `. + +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 and 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: \ No newline at end of file diff --git a/dotfiles/pack/plugins/start/mini.nvim/doc/mini-statusline.txt b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-statusline.txt new file mode 100755 index 0000000..5cea2cf --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-statusline.txt @@ -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: '||'. +- Short: ''. + +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: \ No newline at end of file diff --git a/dotfiles/pack/plugins/start/mini.nvim/doc/mini-surround.txt b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-surround.txt new file mode 100755 index 0000000..020d239 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-surround.txt @@ -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: ''. + +# 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[[]]` - 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` - replace (`sr`) surrounding parenthesis (`)`) with tag + (`t`) with identifier 'div' (`div` 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 >! | !aa ! | < < aa > > | + |---|---------------|---------------|-------------|-----------------| + | ) | Balanced () | !( *a (bb) )! | ! aa (bb) ! | (( aa (bb) )) | + | ] | Balanced [] | ![ *a [bb] ]! | ! aa [bb] ! | [[ aa [bb] ]] | + | } | Balanced {} | !{ *a {bb} }! | ! aa {bb} ! | {{ aa {bb} }} | + | > | Balanced <> | !< *a >! | ! aa ! | << aa >> | + | 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 | !*! | !a! | a | + | | | | | (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: and + for inclusive start and end positions ( might be `nil` to + describe empty region). Each position is also a table with line + and column (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: and 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: +- - defines how to find and extract surrounding for "input" + operations (like `delete`). See more in 'Input surrounding' setction. +- - 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 (plain text string) and (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 +and 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', [[:lua MiniSurround.add('visual')]], { 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 or 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 `` and ``, 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: and +(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 and 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//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 and 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: + - - 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: \ No newline at end of file diff --git a/dotfiles/pack/plugins/start/mini.nvim/doc/mini-tabline.txt b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-tabline.txt new file mode 100755 index 0000000..51c6489 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-tabline.txt @@ -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: \ No newline at end of file diff --git a/dotfiles/pack/plugins/start/mini.nvim/doc/mini-test.txt b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-test.txt new file mode 100755 index 0000000..19ec101 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-test.txt @@ -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: +- - array with all cases being currently executed. Basically, + an input of `MiniTest.execute()`. +- - 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: + - - table with fields: + - - executed before first filtered node. + - - executed before each case (even nested). + - - executed after each case (even nested). + - - executed after last filtered node. + - - 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. + - - 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: + - - array of strings with failing information. + - - array of strings with non-failing information. + - - state of test execution. One of: + - 'Executing ' (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 +
 and  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  (path to file) and 
+  (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:
+  -  - 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`.
+  -  - function which when called without arguments returns
+    array with file paths. Each file should be a Lua file returning single
+    test set or `nil`.
+  -  - 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:
+  -  - table with possible callable fields `start`, `update`,
+    `finish`. Default: |MiniTest.gen_reporter.buffer()| in interactive
+    usage and |MiniTest.gen_reporter.stdout()| in headless usage.
+  -  - 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:
+  -  - 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:
+  -  - 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:
+- `` - stop test execution if executing (see |MiniTest.is_executing()|
+  and |MiniTest.stop()|). Close window otherwise.
+- `q` - same as `` 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:
+  -  - number of first elements of case description (can be zero)
+    used for grouping. Higher values mean higher granularity of output.
+    Default: 1.
+  -  - minimum number of milliseconds to wait between
+    redrawing. Reduces screen flickering but not amount of computations.
+    Default: 10.
+  -  - 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:
+  -  - number of first elements of case description (can be zero)
+    used for grouping. Higher values mean higher granularity of output.
+    Default: 1.
+  -  - 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', }` (see |startup-options|).
+{opts} `(table)` Options:
+  -  - name of Neovim executable. Default: |v:progpath|.
+  -  - 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 ''.
+
+Parameters~
+{wait} `(number)` Number of milliseconds to wait after each entry. May be
+  omitted, in which case no waiting is done.
+{...} `(string|table)` 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', '')
+
+------------------------------------------------------------------------------
+                                        *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:
+  -  - "2d array" (row-column) of single characters displayed at
+    particular cells.
+  -  - "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:
\ No newline at end of file
diff --git a/dotfiles/pack/plugins/start/mini.nvim/doc/mini-trailspace.txt b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-trailspace.txt
new file mode 100755
index 0000000..117e11d
--- /dev/null
+++ b/dotfiles/pack/plugins/start/mini.nvim/doc/mini-trailspace.txt
@@ -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:
\ No newline at end of file
diff --git a/dotfiles/pack/plugins/start/mini.nvim/doc/mini.txt b/dotfiles/pack/plugins/start/mini.nvim/doc/mini.txt
new file mode 100755
index 0000000..dd76d91
--- /dev/null
+++ b/dotfiles/pack/plugins/start/mini.nvim/doc/mini.txt
@@ -0,0 +1,234 @@
+==============================================================================
+------------------------------------------------------------------------------
+                                                                     *mini.nvim*
+*mini.txt*  Collection of minimal, independent and fast Lua modules
+
+Author:  Evgeni Chasnovski
+License: MIT
+
+|mini.nvim| is a collection of minimal, independent, and fast Lua modules
+dedicated to improve Neovim (version 0.5 and higher) experience. Each
+module can be considered as a separate sub-plugin.
+
+Table of contents:
+  General overview.................................................|mini.nvim|
+  Disabling recepies.............................|mini.nvim-disabling-recipes|
+  Buffer-local config..........................|mini.nvim-buffer-local-config|
+  Plugin colorschemes.....................................|mini-color-schemes|
+  Extend and create a/i textobjects..................................|mini.ai|
+  Align text interactively........................................|mini.align|
+  Base16 colorscheme creation....................................|mini.base16|
+  Remove buffers..............................................|mini.bufremove|
+  Comment.......................................................|mini.comment|
+  Completion and signature help..............................|mini.completion|
+  Autohighlight word under cursor............................|mini.cursorword|
+  Generate Neovim help files........................................|mini.doc|
+  Fuzzy matching..................................................|mini.fuzzy|
+  Visualize and operate on indent scope.....................|mini.indentscope|
+  Jump forward/backward to a single character......................|mini.jump|
+  Jump within visible lines......................................|mini.jump2d|
+  Miscellaneous functions..........................................|mini.misc|
+  Autopairs.......................................................|mini.pairs|
+  Session management...........................................|mini.sessions|
+  Start screen..................................................|mini.starter|
+  Statusline.................................................|mini.statusline|
+  Surround actions.............................................|mini.surround|
+  Tabline.......................................................|mini.tabline|
+  Test Neovim plugins..............................................|mini.test|
+  Trailspace (highlight and remove)..........................|mini.trailspace|
+
+# General principles~
+
+- . 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.
+- . Modules are independent of each other and can be run
+  without external dependencies. Although some of them may need
+  dependencies for full experience.
+- . 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".
+- :
+    - Each module (if needed) should be setup separately with
+      `require().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 (after extending with default values) 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()`).
+- . Each module can be additionally configured
+  to use certain runtime config settings locally to buffer. See
+  |mini.nvim-buffer-local-config| for more information.
+- . Each module's core functionality can be disabled globally or
+  buffer-locally by creating appropriate global or buffer-scoped variables
+  equal to |v:true|. See |mini.nvim-disabling-recipes| for common recipes.
+- . Appearance of module's output is controlled by
+  certain highlight group (see |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 colorschemes.
+- . 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.
+
+# List of modules~
+
+- |MiniAi| - extend and create `a`/`i` textobjects (like in `di(` or
+  `va"`). It enhances some builtin |text-objects| (like |a(|, |a)|, |a'|,
+  and more), creates new ones (like `a*`, `a`, `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.
+- |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.
+- |MiniBase16| - fast implementation of base16 theme for manually supplied
+  palette. Supports 30+ plugin integrations. Has unique palette generator
+  which needs only background and foreground colors.
+- |MiniBufremove| - buffer removing (unshow, delete, wipeout) while saving
+  window layout.
+- |MiniComment| - fast and familiar per-line code commenting.
+- |MiniCompletion| - async (with customizable 'debounce' delay) 'two-stage
+  chain completion': first builtin LSP, then configurable fallback. Also
+  has functionality for completion item info and function signature (both
+  in floating window appearing after customizable delay).
+- |MiniCursorword| - automatic highlighting of word under cursor (displayed
+  after customizable delay). Current word under cursor can be highlighted
+  differently.
+- |MiniDoc| - generation of help files from EmmyLua-like annotations.
+  Allows flexible customization of output via hook functions. Used for
+  documenting this plugin.
+- |MiniFuzzy| - functions for fast and simple fuzzy matching. It has
+  not only functions to perform fuzzy matching of one string to others, but
+  also a sorter for |telescope.nvim|.
+- |MiniIndentscope| - Visualize and operate on indent scope. Supports
+  customization of debounce delay, animation style, and different
+  granularity of options for scope computing algorithm.
+- |MiniJump| - minimal and fast module for smarter jumping to a single
+  character.
+- |MiniJump2d| - minimal and fast Lua plugin for jumping (moving cursor)
+  within visible lines via iterative label filtering. Supports custom jump
+  targets (spots), labels, hooks, allowed windows and lines, and more.
+- |MiniMisc| - collection of miscellaneous useful functions. Like `put()`
+  and `put_text()` which print Lua objects to command line and current
+  buffer respectively.
+- |MiniPairs| - autopairs plugin which has minimal defaults and
+  functionality to do per-key expression mappings.
+- |MiniSessions| - session management (read, write, delete) which works
+  using |mksession|. Implements both global (from configured directory) and
+  local (from current directory) sessions.
+- |MiniStarter| - minimal, 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.
+- |MiniStatusline| - minimal and fast statusline. Has ability to use custom
+  content supplied with concise function (using module's provided section
+  functions) along with builtin default. For full experience needs [Nerd
+  font](https://www.nerdfonts.com/),
+  [gitsigns.nvim](https://github.com/lewis6991/gitsigns.nvim) plugin, and
+  [nvim-web-devicons](https://github.com/kyazdani42/nvim-web-devicons)
+  plugin (but works without any them).
+- |MiniSurround| - 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.
+- |MiniTest| - framework for writing 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.
+- |MiniTabline| - minimal tabline which always shows listed (see 'buflisted')
+  buffers. Allows showing extra information section in case of multiple vim
+  tabpages. For full experience needs
+  [nvim-web-devicons](https://github.com/kyazdani42/nvim-web-devicons).
+- |MiniTrailspace| - automatic highlighting of trailing whitespace with
+  functionality to remove it.
+
+------------------------------------------------------------------------------
+                                                   *mini.nvim-disabling-recipes*
+Common recipes for disabling functionality
+
+Each module's core functionality can be disabled globally or buffer-locally
+by creating appropriate global or buffer-scoped variables equal to
+|v:true|. Functionality is disabled if at least one of `g:` or `b:`
+variables is equal to `v:true`.
+
+Variable names have the same structure: `{g,b}:mini*_disable` where `*` is
+module's lowercase name. For example, `g:minicursorword_disable` disables
+|mini.cursorword| globally and `b:minicursorword_disable` - for
+corresponding buffer. Note: in this section disabling 'mini.cursorword' is
+used as example; everything holds for other module variables.
+
+Considering high number of different scenarios and customization intentions,
+writing exact rules for disabling module's functionality is left to user.
+
+# Manual disabling~
+
+- Disable globally:
+  Lua       - `:lua vim.g.minicursorword_disable=true`
+  Vimscript - `:let g:minicursorword_disable=v:true`
+- Disable for current buffer:
+  Lua       - `:lua vim.b.minicursorword_disable=true`
+  Vimscript - `:let b:minicursorword_disable=v:true`
+- Toggle (disable if enabled, enable if disabled):
+  Globally   - `:lua vim.g.minicursorword_disable = not vim.g.minicursorword_disable`
+  For buffer - `:lua vim.b.minicursorword_disable = not vim.b.minicursorword_disable`
+
+# Automated disabling~
+
+- Disable for a certain |filetype| (for example, "markdown"):
+  `autocmd Filetype markdown lua vim.b.minicursorword_disable = true`
+- Enable only for certain filetypes (for example, "lua" and "python"):
+  `au FileType * if index(['lua', 'python'], &ft) < 0 | let b:minicursorword_disable=v:true | endif`
+- Disable in Insert mode (use similar pattern for Terminal mode or indeed
+  any other mode change with |ModeChanged| starting from Neovim 0.7.0):
+  `au InsertEnter * lua vim.b.minicursorword_disable = true`
+  `au InsertLeave * lua vim.b.minicursorword_disable = false`
+- Disable in Terminal buffer:
+  `au TermOpen * lua vim.b.minicursorword_disable = true`
+
+------------------------------------------------------------------------------
+                                                 *mini.nvim-buffer-local-config*
+Buffer local config
+
+Each module can be additionally configured locally to buffer by creating
+appropriate buffer-scoped variable with values you want to override. It
+will affect only runtime options and not those used once during setup (like
+`mappings` or `set_vim_settings`).
+
+Variable names have the same structure: `b:mini*_config` where `*` is
+module's lowercase name. For example, `b:minicursorword_config` can store
+information about how |mini.cursorword| will act inside current buffer. Its
+value should be a table with same structure as module's `config`.
+Continuing example, `vim.b.minicursorword_config = { delay = 500 }` will
+use delay 500 inside current buffer.
+
+Considering high number of different scenarios and customization intentions,
+writing exact rules for module's buffer local configuration is left to
+user. It is done in similar fashion to |mini.nvim-disabling-recipes|.
+
+Note: using function values inside buffer variables requires Neovim>=0.7.
+
+
+ vim:tw=78:ts=8:noet:ft=help:norl:
\ No newline at end of file
diff --git a/dotfiles/pack/plugins/start/mini.nvim/logo.png b/dotfiles/pack/plugins/start/mini.nvim/logo.png
new file mode 100755
index 0000000..27a9a37
Binary files /dev/null and b/dotfiles/pack/plugins/start/mini.nvim/logo.png differ
diff --git a/dotfiles/pack/plugins/start/mini.nvim/lua/mini/ai.lua b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/ai.lua
new file mode 100755
index 0000000..6545880
--- /dev/null
+++ b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/ai.lua
@@ -0,0 +1,1925 @@
+-- MIT License Copyright (c) 2022 Evgeni Chasnovski
+
+-- Documentation ==============================================================
+--- 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`,
+--- `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.
+---@tag mini.ai
+---@tag MiniAi
+
+--- 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  >>    |        |        |        |        |
+---  |---|---------------|-1234567890123456-|--------|--------|--------|--------|
+---  | ) |  Balanced ()  | (( *a (bb) ))    |        |        |        |        |
+---  | ] |  Balanced []  | [[ *a [bb] ]]    |        |        |        |        |
+---  | } |  Balanced {}  | {{ *a {bb} }}    | [2;12] | [3;11] | [1;13] | [2;12] |
+---  | > |  Balanced <>  | << *a  >>    |        |        |        |        |
+---  | 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      | *b | [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|).
+---@tag MiniAi-textobject-builtin
+
+--- - REGION - table representing region in a buffer. Fields:  and
+---    for inclusive start and end positions ( might be `nil` to
+---   describe empty region). Each position is also a table with line 
+---   and column  (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.
+---@tag MiniAi-glossary
+
+--- 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: `{ '%[%[().-()%]%]' }`
+---@tag MiniAi-textobject-specification
+
+--- 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|).
+---@tag MiniAi-algorithm
+
+-- Module definition ==========================================================
+local MiniAi = {}
+local H = {}
+
+--- Module setup
+---
+---@param config table|nil Module config table. See |MiniAi.config|.
+---
+---@usage `require('mini.ai').setup({})` (replace `{}` with your `config` table)
+MiniAi.setup = function(config)
+  -- Export module
+  _G.MiniAi = MiniAi
+
+  -- Setup config
+  config = H.setup_config(config)
+
+  -- Apply config
+  H.apply_config(config)
+end
+
+--- Module config
+---
+--- Default values:
+---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
+---@text # 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.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',
+}
+--minidoc_afterlines_end
+
+-- Module functionality =======================================================
+--- Find textobject region
+---
+---@param ai_type string One of `'a'` or `'i'`.
+---@param 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).
+---@param opts table|nil Options. Possible fields:
+---   -  - Number of lines within which textobject is searched.
+---     Default: `config.n_lines` (see |MiniAi.config|).
+---   -  - Number of times to perform a consecutive search. Each one
+---     is done with reference region being previous found textobject region.
+---     Default: 1.
+---   -  - 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. 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.find_textobject = function(ai_type, id, opts)
+  if not (ai_type == 'a' or ai_type == 'i') then H.error([[`ai_type` should be one of 'a' or 'i'.]]) end
+  opts = vim.tbl_deep_extend('force', H.get_default_opts(), opts or {})
+  H.validate_search_method(opts.search_method)
+
+  -- Get textobject specification
+  local tobj_spec = H.get_textobject_spec(id, { ai_type, id, opts })
+  if tobj_spec == nil then return end
+  if H.is_region(tobj_spec) then return tobj_spec end
+
+  -- Find region
+  local res = H.find_textobject_region(tobj_spec, ai_type, opts)
+
+  if res == nil then
+    local msg = string.format(
+      [[No textobject %s found covering region%s within %d line%s and `search_method = '%s'`.]],
+      vim.inspect(ai_type .. id),
+      opts.n_times == 1 and '' or (' %s times'):format(opts.n_times),
+      opts.n_lines,
+      opts.n_lines == 1 and '' or 's',
+      opts.search_method
+    )
+    H.message(msg)
+  end
+
+  return res
+end
+
+--- Move cursor to edge of textobject
+---
+---@param side string One of `'left'` or `'right'`.
+---@param ai_type string One of `'a'` or `'i'`.
+---@param id string Single character string representing textobject id.
+---@param 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.move_cursor = function(side, ai_type, id, opts)
+  if not (side == 'left' or side == 'right') then H.error([[`side` should be one of 'left' or 'right'.]]) end
+  opts = opts or {}
+  local init_pos = vim.api.nvim_win_get_cursor(0)
+
+  -- Compute single textobject first to find out if it would move the cursor.
+  -- If not, then eventual `n_times` should be bigger by 1 to imitate `n_times`
+  -- *actual* jumps. This implements consecutive jumps and has logic of "If
+  -- cursor is strictly inside region, move to its side first".
+  local new_opts = vim.tbl_deep_extend('force', opts, { n_times = 1 })
+  local tobj_single = MiniAi.find_textobject(ai_type, id, new_opts)
+  if tobj_single == nil then return end
+  local tobj_side = side == 'left' and 'from' or 'to'
+
+  -- Allow empty region
+  tobj_single.to = tobj_single.to or tobj_single.from
+
+  new_opts.n_times = opts.n_times or 1
+  if (init_pos[1] == tobj_single[tobj_side].line) and (init_pos[2] == tobj_single[tobj_side].col - 1) then
+    new_opts.n_times = new_opts.n_times + 1
+  end
+
+  -- Compute actually needed textobject while avoiding unnecessary computation
+  -- in a most common usage (`v:count1 == 1`)
+  local pos = tobj_single[tobj_side]
+  if new_opts.n_times > 1 then
+    local tobj = MiniAi.find_textobject(ai_type, id, new_opts)
+    if tobj == nil then return end
+    tobj.to = tobj.to or tobj.from
+    pos = tobj[tobj_side]
+  end
+
+  -- Move cursor and open enough folds
+  vim.api.nvim_win_set_cursor(0, { pos.line, pos.col - 1 })
+  vim.cmd('normal! zv')
+end
+
+--- 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 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).
+---
+---@param opts table|nil Options. Allowed fields:
+---   -  - table with patterns for outer balanced brackets.
+---     Default: `{ '%b()', '%b[]', '%b{}' }` (any `()`, `[]`, or `{}` can
+---     enclose arguments).
+---   -  - table with single character separators.
+---     Default: `{ ',' }` (arguments are separated with `,`).
+---   -  - 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.argument = function(opts)
+  opts = vim.tbl_deep_extend('force', {
+    brackets = { '%b()', '%b[]', '%b{}' },
+    separators = { ',' },
+    exclude_regions = { '%b""', "%b''", '%b()', '%b[]', '%b{}' },
+  }, opts or {})
+  local brackets, separators, exclude_regions = opts.brackets, opts.separators, opts.exclude_regions
+
+  if type(opts.brackets) == 'string' then opts.brackets = { opts.brackets } end
+  local separators_esc = vim.tbl_map(vim.pesc, separators)
+  local sep_str = table.concat(separators_esc, '')
+  local sep_pattern, nosep_pattern = '[' .. sep_str .. ']', '[^' .. sep_str .. ']'
+
+  local res = {}
+  -- Match brackets
+  res[1] = brackets
+
+  -- Match argument with both left and right separators/brackets
+  res[2] = function(s, init)
+    -- Cache string separators per spec as they are used multiple times.
+    -- Storing per spec allows coexistence of several argument specifications.
+    H.cache.argument_seps = H.cache.argument_seps or {}
+    H.cache.argument_seps[res] = H.cache.argument_seps[res] or {}
+    local seps = H.cache.argument_seps[res][s] or H.arg_get_separators(s, sep_pattern, exclude_regions)
+    H.cache.argument_seps[res][s] = seps
+
+    -- Return span fully on right of `init`, `nil` otherwise
+    -- For first argument returns left bracket; for last - right one.
+    for i = 1, #seps - 1 do
+      if init <= seps[i] then return seps[i], seps[i + 1] end
+    end
+
+    return nil
+  end
+
+  -- Make extraction part
+  local match_and_shrink = function(left, left_keep, right, right_keep)
+    local pattern = '^' .. left .. '.*' .. right .. '$'
+    return function(s, init)
+      if init > 1 then return nil end
+      if not s:find(pattern) then return nil end
+      return left_keep and 1 or 2, s:len() - (right_keep and 0 or 1)
+    end
+  end
+
+  -- `a` type depends on argument number, `i` - as `a` but without whitespace
+  -- The reason for this complex solution is the following requirements:
+  -- - Don't match argument region when cursor is on the outer bracket.
+  --   Example: `f(xxx)` should select argument only when cursor is on 'x'.
+  -- - Don't select edge whitespace for first and last argument BUT match when
+  --   cursor is on them. This is useful when working with padded brackets.
+  --   Example for `f(  xx  ,  yy  )`:
+  --     - `a` object should select 'xx  ,' when cursor is on all '  xx  ';
+  --       should select ',  yy' when cursor is on all '  yy  '.
+  --     - `i` object should select 'xx' when cursor is on all '  xx  ';
+  --       should select 'yy' when cursor is on all '  yy  '.
+  res[3] = {
+    -- Middle argument. Include only left separator.
+    { match_and_shrink(sep_pattern, true, sep_pattern, false), '^.%s*().-()%s*$' },
+
+    -- First argument. Include right separator, exclude left whitespace.
+    { match_and_shrink(nosep_pattern, false, sep_pattern, true), '^%s*()().-()%s*.()$' },
+
+    -- Last argument. Include left separator, exclude right whitespace.
+    -- NOTE: it misbehaves for whitespace argument. It's OK because it's rare.
+    { match_and_shrink(sep_pattern, true, nosep_pattern, false), '^().%s*().-()()%s*$' },
+
+    -- Single argument. Include both whitespace (makes `aa` and `ia` differ).
+    { match_and_shrink(nosep_pattern, false, nosep_pattern, false), '^%s*().-()%s*$' },
+  }
+
+  return res
+end
+
+--- 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).
+---
+---@param opts table|nil Optsion. Allowed fields:
+---   -  - string pattern of character set allowed in function name.
+---     Default: `'[%w_%.]'` (alphanumeric, underscore, or dot).
+---     Note: should be enclosed in `[]`.
+MiniAi.gen_spec.function_call = function(opts)
+  opts = vim.tbl_deep_extend('force', { name_pattern = '[%w_%.]' }, opts or {})
+  -- Use frontier pattern to select widest possible name
+  return { '%f' .. opts.name_pattern .. opts.name_pattern .. '+%b()', '^.-%(().*()%)$' }
+end
+
+--- 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.
+---
+---@param left string Left edge.
+---@param right string Right edge.
+---@param opts table|nil Options. Possible fields:
+---   -  - Type of a pair. One of `'non-balanced'` (default), `'balanced'`,
+---   `'greedy'`.
+MiniAi.gen_spec.pair = function(left, right, opts)
+  if not (type(left) == 'string' and type(right) == 'string') then
+    H.error('Both `left` and `right` should be strings.')
+  end
+  opts = vim.tbl_deep_extend('force', { type = 'non-balanced' }, opts or {})
+
+  if (opts.type == 'balanced' or opts.type == 'greedy') and not (left:len() == 1 and right:len() == 1) then
+    local msg =
+      string.format([[Both `left` and `right` should be single character for `opts.type == '%s'`.]], opts.type)
+    H.error(msg)
+  end
+
+  local left_esc = vim.pesc(left)
+  local right_esc = vim.pesc(right)
+
+  if opts.type == 'balanced' then return { string.format('%%b%s%s', left, right), '^.().*().$' } end
+  if opts.type == 'non-balanced' then return { string.format('%s().-()%s', left_esc, right_esc) } end
+  if opts.type == 'greedy' then
+    return { string.format('%%f[%s]%s+()[^%s]-()%s+%%f[^%s]', left_esc, left_esc, left_esc, right_esc, right_esc) }
+  end
+
+  H.error([[`opts.type` should be one of 'balanced', 'non-balanced', 'greedy'.]])
+end
+
+--- 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//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).
+---
+---@param ai_captures table Captures for `a` and `i` textobjects: table with
+---    and  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).
+---@param opts table Options. Possible values:
+---   -  - 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.
+---
+---@seealso |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.gen_spec.treesitter = function(ai_captures, opts)
+  opts = vim.tbl_deep_extend('force', { use_nvim_treesitter = true }, opts or {})
+  ai_captures = H.prepare_ai_captures(ai_captures)
+
+  return function(ai_type, _, _)
+    -- Get array of matched treesitter nodes
+    local target_captures = ai_captures[ai_type]
+    local has_nvim_treesitter, _ = pcall(require, 'nvim-treesitter')
+    local node_querier = (has_nvim_treesitter and opts.use_nvim_treesitter) and H.get_matched_nodes_plugin
+      or H.get_matched_nodes_builtin
+    local matched_nodes = node_querier(target_captures)
+
+    -- Return array of regions
+    return vim.tbl_map(function(node)
+      local line_from, col_from, line_to, col_to = node:range()
+      -- `node:range()` returns 0-based numbers for end-exclusive region
+      return { from = { line = line_from + 1, col = col_from + 1 }, to = { line = line_to + 1, col = col_to } }
+    end, matched_nodes)
+  end
+end
+
+--- Visually select textobject region
+---
+--- Does nothing if no region is found.
+---
+---@param ai_type string One of `'a'` or `'i'`.
+---@param id string Single character string representing textobject id.
+---@param opts table|nil Same as in |MiniAi.find_textobject()|. Extra fields:
+---   -  - One of `'v'`, `'V'`, `''`. Default: Latest visual mode.
+---   -  - Whether selection is for Operator-pending mode.
+---     Used in that mode's mappings, shouldn't be used directly. Default: `false`.
+MiniAi.select_textobject = function(ai_type, id, opts)
+  if H.is_disabled() then return end
+
+  opts = opts or {}
+
+  -- Exit to Normal before getting textobject id. This way invalid id doesn't
+  -- result into staying in current mode (which seems to be more convenient).
+  H.exit_to_normal_mode()
+
+  local tobj = MiniAi.find_textobject(ai_type, id, opts)
+  if tobj == nil then return end
+
+  local set_cursor = function(position) vim.api.nvim_win_set_cursor(0, { position.line, position.col - 1 }) end
+
+  -- Allow empty regions
+  local tobj_is_empty = tobj.to == nil
+  tobj.to = tobj.to or tobj.from
+
+  local prev_vis_mode = vim.fn.visualmode()
+  prev_vis_mode = prev_vis_mode == '' and 'v' or prev_vis_mode
+  local vis_mode = opts.vis_mode and vim.api.nvim_replace_termcodes(opts.vis_mode, true, true, true) or prev_vis_mode
+
+  -- Allow going past end of line in order to collapse multiline regions
+  local cache_virtualedit = vim.o.virtualedit
+  local cache_eventignore = vim.o.eventignore
+
+  pcall(function()
+    -- Do nothing in Operator-pending mode for empty region (except `c` or `d`)
+    if tobj_is_empty and opts.operator_pending and not (vim.v.operator == 'c' or vim.v.operator == 'd') then
+      H.message('Textobject region is empty. Nothing is done.')
+      return
+    end
+
+    -- Allow setting cursor past line end (allows collapsing multiline region)
+    vim.o.virtualedit = 'onemore'
+
+    -- Open enough folds to show left and right edges
+    set_cursor(tobj.from)
+    vim.cmd('normal! zv')
+    set_cursor(tobj.to)
+    vim.cmd('normal! zv')
+
+    -- Respect exclusive selection
+    if vim.o.selection == 'exclusive' then vim.cmd('normal! l') end
+
+    -- Start selection
+    vim.cmd('normal! ' .. vis_mode)
+    set_cursor(tobj.from)
+
+    if tobj_is_empty and opts.operator_pending then
+      -- Add single space (without triggering events) and visually select it.
+      -- Seems like the only way to make `ci)` and `di)` move inside empty
+      -- brackets. Original idea is from 'wellle/targets.vim'.
+      vim.o.eventignore = 'all'
+
+      -- First escape from previously started Visual mode
+      vim.cmd([[silent! execute "normal! \i \v"]])
+    end
+  end)
+
+  -- Restore options
+  vim.o.virtualedit = cache_virtualedit
+  vim.o.eventignore = cache_eventignore
+end
+
+--- 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|.
+---
+---@param mode string One of 'x' (Visual) or 'o' (Operator-pending).
+---@param ai_type string One of `'a'` or `'i'`.
+---@param opts table|nil Same as in |MiniAi.select_textobject()|.
+MiniAi.expr_textobject = function(mode, ai_type, opts)
+  local tobj_id = H.user_textobject_id(ai_type)
+
+  if tobj_id == nil then return '' end
+
+  -- Possibly fall back to builtin `a`/`i` textobjects
+  if H.is_disabled() or not H.is_valid_textobject_id(tobj_id) then
+    local res = ai_type .. tobj_id
+    -- If fallback is an existing user mapping, prepend it with ''.
+    -- This deals with `:h recursive_mapping`. Shouldn't prepend if it is a
+    -- builtin textobject. Also see https://github.com/vim/vim/issues/10907 .
+    if vim.fn.maparg(res, mode) ~= '' then res = H.keys.ignore .. res end
+    return res
+  end
+  opts = vim.tbl_deep_extend('force', H.get_default_opts(), opts or {})
+
+  -- Clear cache
+  H.cache = {}
+
+  -- Construct call options based on mode
+  local reference_region_field, operator_pending_field, vis_mode_field = 'nil', 'nil', 'nil'
+
+  if mode == 'x' then
+    -- Use Visual selection as reference region for Visual mode mappings
+    reference_region_field = vim.inspect(H.get_visual_region(), { newline = '', indent = '' })
+  end
+
+  if mode == 'o' then
+    -- Supply `operator_pending` flag in Operator-pending mode
+    operator_pending_field = 'true'
+
+    -- Take into account forced Operator-pending modes ('nov', 'noV', 'no')
+    vis_mode_field = vim.fn.mode(1):gsub('^no', '')
+    vis_mode_field = vim.inspect(vis_mode_field == '' and 'v' or vis_mode_field)
+  end
+
+  -- Make expression
+  return H.keys.cmd_lua
+    .. string.format(
+      [[MiniAi.select_textobject('%s', '%s', { search_method = '%s', n_times = %d, reference_region = %s, operator_pending = %s, vis_mode = %s })]],
+      ai_type,
+      vim.fn.escape(tobj_id, "'"),
+      opts.search_method,
+      vim.v.count1,
+      reference_region_field,
+      operator_pending_field,
+      vis_mode_field
+    )
+    .. H.keys.cr
+end
+
+--- 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|.
+---
+---@param side string One of `'left'` or `'right'`.
+MiniAi.expr_motion = function(side)
+  if H.is_disabled() then return '' end
+
+  if not (side == 'left' or side == 'right') then H.error([[`side` should be one of 'left' or 'right'.]]) end
+
+  -- Get user input
+  local tobj_id = H.user_textobject_id('a')
+  if tobj_id == nil then return end
+
+  -- Clear cache
+  H.cache = {}
+
+  -- Make expression for moving cursor
+  return H.keys.cmd_lua
+    .. string.format(
+      [[MiniAi.move_cursor('%s', 'a', '%s', { n_times = %d })]],
+      side,
+      vim.fn.escape(tobj_id, "'"),
+      vim.v.count1
+    )
+    .. H.keys.cr
+end
+
+-- Helper data ================================================================
+-- Module default config
+H.default_config = MiniAi.config
+
+-- Cache for various operations
+H.cache = {}
+
+-- Builtin textobjects
+H.builtin_textobjects = {
+  -- Use balanced pair for brackets. Use opening ones to possibly remove edge
+  -- whitespace from `i` textobject.
+  ['('] = { '%b()', '^.%s*().-()%s*.$' },
+  [')'] = { '%b()', '^.().*().$' },
+  ['['] = { '%b[]', '^.%s*().-()%s*.$' },
+  [']'] = { '%b[]', '^.().*().$' },
+  ['{'] = { '%b{}', '^.%s*().-()%s*.$' },
+  ['}'] = { '%b{}', '^.().*().$' },
+  ['<'] = { '%b<>', '^.%s*().-()%s*.$' },
+  ['>'] = { '%b<>', '^.().*().$' },
+  -- Use special "same balanced" pattern to select quotes in pairs
+  ["'"] = { "%b''", '^.().*().$' },
+  ['"'] = { '%b""', '^.().*().$' },
+  ['`'] = { '%b``', '^.().*().$' },
+  -- Derived from user prompt
+  ['?'] = function()
+    -- Using cache allows for a dot-repeat without another user input
+    if H.cache.prompted_textobject ~= nil then return H.cache.prompted_textobject end
+
+    local left = H.user_input('Left edge')
+    if left == nil or left == '' then return end
+    local right = H.user_input('Right edge')
+    if right == nil or right == '' then return end
+
+    local left_esc, right_esc = vim.pesc(left), vim.pesc(right)
+    local res = { string.format('%s().-()%s', left_esc, right_esc) }
+    H.cache.prompted_textobject = res
+    return res
+  end,
+  -- Argument
+  ['a'] = MiniAi.gen_spec.argument(),
+  -- Brackets
+  ['b'] = { { '%b()', '%b[]', '%b{}' }, '^.().*().$' },
+  -- Function call
+  ['f'] = MiniAi.gen_spec.function_call(),
+  -- Tag
+  ['t'] = { '<(%w-)%f[^<%w][^<>]->.-', '^<.->().*()$' },
+  -- Quotes
+  ['q'] = { { "%b''", '%b""', '%b``' }, '^.().*().$' },
+}
+
+-- Module's namespaces
+H.ns_id = {
+  -- Track user input
+  input = vim.api.nvim_create_namespace('MiniAiInput'),
+}
+
+-- Commonly used keys
+H.keys = {
+  ignore = vim.api.nvim_replace_termcodes('', true, true, true),
+  cmd_lua = vim.api.nvim_replace_termcodes('lua ', true, true, true),
+  cr = vim.api.nvim_replace_termcodes('', true, true, true),
+}
+
+-- Helper functionality =======================================================
+-- Settings -------------------------------------------------------------------
+H.setup_config = function(config)
+  -- General idea: if some table elements are not present in user-supplied
+  -- `config`, take them from default config
+  vim.validate({ config = { config, 'table', true } })
+  config = vim.tbl_deep_extend('force', H.default_config, config or {})
+
+  vim.validate({
+    custom_textobjects = { config.custom_textobjects, 'table', true },
+    mappings = { config.mappings, 'table' },
+    n_lines = { config.n_lines, 'number' },
+    search_method = { config.search_method, H.is_search_method },
+  })
+
+  vim.validate({
+    ['mappings.around'] = { config.mappings.around, 'string' },
+    ['mappings.inside'] = { config.mappings.inside, 'string' },
+    ['mappings.around_next'] = { config.mappings.around_next, 'string' },
+    ['mappings.inside_next'] = { config.mappings.inside_next, 'string' },
+    ['mappings.around_last'] = { config.mappings.around_last, 'string' },
+    ['mappings.inside_last'] = { config.mappings.inside_last, 'string' },
+    ['mappings.goto_left'] = { config.mappings.goto_left, 'string' },
+    ['mappings.goto_right'] = { config.mappings.goto_right, 'string' },
+  })
+
+  return config
+end
+
+--stylua: ignore
+H.apply_config = function(config)
+  MiniAi.config = config
+
+  -- Make mappings
+  local maps = config.mappings
+  local m = function(mode, lhs, rhs, opts)
+    opts.expr = true
+    -- Allow recursive mapping to support falling back on user defined mapping
+    opts.noremap = false
+    H.map(mode, lhs, rhs, opts)
+  end
+
+  -- Usage of expression maps implements dot-repeat support
+  m('n', maps.goto_left,  [[v:lua.MiniAi.expr_motion('left')]],   { desc = 'Move to left "around"' })
+  m('n', maps.goto_right, [[v:lua.MiniAi.expr_motion('right')]],  { desc = 'Move to right "around"' })
+  m('x', maps.goto_left,  [[v:lua.MiniAi.expr_motion('left')]],   { desc = 'Move to left "around"' })
+  m('x', maps.goto_right, [[v:lua.MiniAi.expr_motion('right')]],  { desc = 'Move to right "around"' })
+  m('o', maps.goto_left,  [[v:lua.MiniAi.expr_motion('left')]],   { desc = 'Move to left "around"' })
+  m('o', maps.goto_right, [[v:lua.MiniAi.expr_motion('right')]],  { desc = 'Move to right "around"' })
+
+  m('x', maps.around, [[v:lua.MiniAi.expr_textobject('x', 'a')]], { desc = 'Around textobject' })
+  m('x', maps.inside, [[v:lua.MiniAi.expr_textobject('x', 'i')]], { desc = 'Inside textobject' })
+  m('o', maps.around, [[v:lua.MiniAi.expr_textobject('o', 'a')]], { desc = 'Around textobject' })
+  m('o', maps.inside, [[v:lua.MiniAi.expr_textobject('o', 'i')]], { desc = 'Inside textobject' })
+
+  m('x', maps.around_next, [[v:lua.MiniAi.expr_textobject('x', 'a', {'search_method': 'next'})]], { desc = 'Around next textobject' })
+  m('x', maps.around_last, [[v:lua.MiniAi.expr_textobject('x', 'a', {'search_method': 'prev'})]], { desc = 'Around last textobject' })
+  m('x', maps.inside_next, [[v:lua.MiniAi.expr_textobject('x', 'i', {'search_method': 'next'})]], { desc = 'Inside next textobject' })
+  m('x', maps.inside_last, [[v:lua.MiniAi.expr_textobject('x', 'i', {'search_method': 'prev'})]], { desc = 'Inside last textobject' })
+  m('o', maps.around_next, [[v:lua.MiniAi.expr_textobject('o', 'a', {'search_method': 'next'})]], { desc = 'Around next textobject' })
+  m('o', maps.around_last, [[v:lua.MiniAi.expr_textobject('o', 'a', {'search_method': 'prev'})]], { desc = 'Around last textobject' })
+  m('o', maps.inside_next, [[v:lua.MiniAi.expr_textobject('o', 'i', {'search_method': 'next'})]], { desc = 'Inside next textobject' })
+  m('o', maps.inside_last, [[v:lua.MiniAi.expr_textobject('o', 'i', {'search_method': 'prev'})]], { desc = 'Inside last textobject' })
+end
+
+H.is_disabled = function() return vim.g.miniai_disable == true or vim.b.miniai_disable == true end
+
+H.get_config =
+  function(config) return vim.tbl_deep_extend('force', MiniAi.config, vim.b.miniai_config or {}, config or {}) end
+
+H.is_search_method = function(x, x_name)
+  x = x or H.get_config().search_method
+  x_name = x_name or '`config.search_method`'
+
+  local allowed_methods = vim.tbl_keys(H.span_compare_methods)
+  if vim.tbl_contains(allowed_methods, x) then return true end
+
+  table.sort(allowed_methods)
+  local allowed_methods_string = table.concat(vim.tbl_map(vim.inspect, allowed_methods), ', ')
+  local msg = ([[%s should be one of %s.]]):format(x_name, allowed_methods_string)
+  return false, msg
+end
+
+H.validate_search_method = function(x, x_name)
+  local is_valid, msg = H.is_search_method(x, x_name)
+  if not is_valid then H.error(msg) end
+end
+
+-- Work with textobject info --------------------------------------------------
+H.make_textobject_table = function()
+  -- Extend builtins with data from `config`. Don't use `tbl_deep_extend()`
+  -- because only top level keys should be merged.
+  local textobjects = vim.tbl_extend('force', H.builtin_textobjects, H.get_config().custom_textobjects or {})
+
+  -- Use default textobject pattern only for some characters: punctuation,
+  -- whitespace, digits.
+  return setmetatable(textobjects, {
+    __index = function(_, key)
+      if not (type(key) == 'string' and string.find(key, '^[%p%s%d]$')) then return end
+      local key_esc = vim.pesc(key)
+      -- Use `%f[]` to ensure maximum stretch in both directions. Include only
+      -- right edge in `a` textobject.
+      -- Example output: '_()()[^_]-()_+%f[^_]()'
+      return { string.format('%s()()[^%s]-()%s+%%f[^%s]()', key_esc, key_esc, key_esc, key_esc) }
+    end,
+  })
+end
+
+H.get_textobject_spec = function(id, args)
+  local textobject_tbl = H.make_textobject_table()
+  local spec = textobject_tbl[id]
+
+  -- Allow function returning spec or region(s)
+  if vim.is_callable(spec) then spec = spec(unpack(args)) end
+
+  -- Wrap callable tables to be an actual functions. Otherwise they might be
+  -- confused with list of patterns.
+  if H.is_composed_pattern(spec) then return vim.tbl_map(H.wrap_callable_table, spec) end
+
+  if not (H.is_region(spec) or H.is_region_array(spec)) then return nil end
+  return spec
+end
+
+H.is_valid_textobject_id = function(id)
+  local textobject_tbl = H.make_textobject_table()
+  return textobject_tbl[id] ~= nil
+end
+
+H.is_region = function(x)
+  if type(x) ~= 'table' then return false end
+  local from_is_valid = type(x.from) == 'table' and type(x.from.line) == 'number' and type(x.from.col) == 'number'
+  -- Allow `to` to be `nil` to describe empty regions
+  local to_is_valid = true
+  if x.to ~= nil then
+    to_is_valid = type(x.to) == 'table' and type(x.to.line) == 'number' and type(x.to.col) == 'number'
+  end
+  return from_is_valid and to_is_valid
+end
+
+H.is_region_array = function(x)
+  if not vim.tbl_islist(x) then return false end
+  for _, v in ipairs(x) do
+    if not H.is_region(v) then return false end
+  end
+  return true
+end
+
+H.is_composed_pattern = function(x)
+  if not (vim.tbl_islist(x) and #x > 0) then return false end
+  for _, val in ipairs(x) do
+    local val_type = type(val)
+    if not (val_type == 'table' or val_type == 'string' or vim.is_callable(val)) then return false end
+  end
+  return true
+end
+
+-- Work with finding textobjects ----------------------------------------------
+---@param tobj_spec table Composed pattern. Last item(s) - extraction template.
+---@param ai_type string One of `'a'` or `'i'`.
+---@param opts table Textobject options with all fields present.
+---@private
+H.find_textobject_region = function(tobj_spec, ai_type, opts)
+  local reference_region, n_times, n_lines = opts.reference_region, opts.n_times, opts.n_lines
+
+  if n_times == 0 then return end
+
+  -- Find `n_times` matching spans evolving from reference region span
+  -- First try to find inside 0-neighborhood
+  local neigh = H.get_neighborhood(reference_region, 0)
+  local reference_span = neigh.region_to_span(reference_region)
+
+  local find_next = function(cur_reference_span)
+    local res = H.find_best_match(neigh, tobj_spec, cur_reference_span, opts)
+
+    -- If didn't find in 0-neighborhood, possibly try extend one
+    if res.span == nil then
+      -- Stop if no need to extend neighborhood
+      if n_lines == 0 or neigh.n_neighbors > 0 then return {} end
+
+      -- Update data with respect to new neighborhood
+      local cur_reference_region = neigh.span_to_region(cur_reference_span)
+      neigh = H.get_neighborhood(reference_region, n_lines)
+      reference_span = neigh.region_to_span(reference_region)
+      cur_reference_span = neigh.region_to_span(cur_reference_region)
+
+      -- Recompute based on new neighborhood
+      res = H.find_best_match(neigh, tobj_spec, cur_reference_span, opts)
+    end
+
+    return res
+  end
+
+  local find_res = { span = reference_span }
+  for _ = 1, n_times do
+    find_res = find_next(find_res.span)
+    if find_res.span == nil then return end
+  end
+
+  -- Extract final span
+  local extract = function(span, extract_pattern)
+    -- Use `nil` extract pattern to allow array of regions as textobject spec
+    if extract_pattern == nil then return span end
+
+    -- First extract local (with respect to best matched span) span
+    local s = neigh['1d']:sub(span.from, span.to - 1)
+    local local_span = H.extract_span(s, extract_pattern, ai_type)
+
+    -- Convert local span to global
+    local offset = span.from - 1
+    return { from = local_span.from + offset, to = local_span.to + offset }
+  end
+
+  local final_span = extract(find_res.span, find_res.extract_pattern)
+
+  -- Ensure that output region is different from reference. This is needed if
+  -- final span was shrinked during extraction and resulted into equal to input
+  -- reference. This powers consecutive application of most `i` textobjects.
+  if H.is_span_covering(reference_span, final_span) then
+    find_res = find_next(find_res.span)
+    if find_res.span == nil then return end
+    final_span = extract(find_res.span, find_res.extract_pattern)
+    if H.is_span_covering(reference_span, final_span) then return end
+  end
+
+  -- Convert to region
+  return neigh.span_to_region(final_span)
+end
+
+H.get_default_opts = function()
+  local config = H.get_config()
+  local cur_pos = vim.api.nvim_win_get_cursor(0)
+  return {
+    n_lines = config.n_lines,
+    n_times = vim.v.count1,
+    -- Empty region at cursor position
+    reference_region = { from = { line = cur_pos[1], col = cur_pos[2] + 1 } },
+    search_method = config.search_method,
+  }
+end
+
+-- Work with argument textobject ----------------------------------------------
+H.arg_get_separators = function(s, sep_pattern, exclude_regions)
+  if s:len() <= 2 then return {} end
+
+  -- Get all separators
+  local seps = {}
+  s:gsub('()' .. sep_pattern, function(x) table.insert(seps, x) end)
+  if #seps == 0 then return { 1, s:len() } end
+
+  -- Remove separators that are in "excluded regions": by default, inside
+  -- brackets or quotes
+  local inner_s, forbidden = s:sub(2, -2), {}
+  local add_to_forbidden = function(l, r) table.insert(forbidden, { l + 1, r }) end
+
+  for _, pat in ipairs(exclude_regions) do
+    local capture_pat = string.format('()%s()', pat)
+    inner_s:gsub(capture_pat, add_to_forbidden)
+  end
+
+  local res = vim.tbl_filter(function(x) return not H.is_point_inside_spans(x, forbidden) end, seps)
+
+  -- Append edge separators (assumes first and last characters are from
+  -- brackets). This allows single argument and ensures at least 2 elements.
+  table.insert(res, 1, 1)
+  table.insert(res, s:len())
+  return res
+end
+
+-- Work with treesitter textobject --------------------------------------------
+H.prepare_ai_captures = function(ai_captures)
+  local is_capture = function(x)
+    if type(x) == 'string' then x = { x } end
+    if not vim.tbl_islist(x) then return false end
+
+    for _, v in ipairs(x) do
+      if not (type(v) == 'string' and v:sub(1, 1) == '@') then return false end
+    end
+    return true
+  end
+
+  if not (type(ai_captures) == 'table' and is_capture(ai_captures.a) and is_capture(ai_captures.i)) then
+    H.error('Wrong format for `ai_captures`. See `MiniAi.gen_spec.treesitter()` for details.')
+  end
+
+  local prepare = function(x)
+    if type(x) == 'string' then return { x } end
+    return x
+  end
+
+  return { a = prepare(ai_captures.a), i = prepare(ai_captures.i) }
+end
+
+H.get_matched_nodes_plugin = function(captures)
+  -- Hope that 'nvim-treesitter.query' is stable enough
+  local ts_queries = require('nvim-treesitter.query')
+  return vim.tbl_map(
+    function(match) return match.node end,
+    -- This call should handle multiple languages in buffer
+    ts_queries.get_capture_matches_recursively(0, captures, 'textobjects')
+  )
+end
+
+H.get_matched_nodes_builtin = function(captures)
+  -- Fetch treesitter data for buffer
+  local lang = vim.bo.filetype
+  local ok, parser = pcall(vim.treesitter.get_parser, 0, lang)
+  if not ok then H.error_treesitter('parser', lang) end
+
+  local query = vim.treesitter.get_query(lang, 'textobjects')
+  if query == nil then H.error_treesitter('query', lang) end
+
+  -- Compute matched captures
+  captures = vim.tbl_map(function(x) return x:sub(2) end, captures)
+  local res = {}
+  for _, tree in ipairs(parser:trees()) do
+    for capture_id, node, _ in query:iter_captures(tree:root(), 0) do
+      if vim.tbl_contains(captures, query.captures[capture_id]) then table.insert(res, node) end
+    end
+  end
+  return res
+end
+
+H.error_treesitter = function(failed_get, lang)
+  local bufnr = vim.api.nvim_get_current_buf()
+  local msg = string.format([[Can not get %s for buffer %d and language '%s'.]], failed_get, bufnr, lang)
+  H.error(msg)
+end
+
+-- Work with matching spans ---------------------------------------------------
+---@param neighborhood table Output of `get_neighborhood()`.
+---@param tobj_spec table
+---@param reference_span table Span to cover.
+---@param opts table Fields: .
+---@private
+H.find_best_match = function(neighborhood, tobj_spec, reference_span, opts)
+  local best_span, best_nested_pattern, current_nested_pattern
+  local f = function(span)
+    if H.is_better_span(span, best_span, reference_span, opts) then
+      best_span = span
+      best_nested_pattern = current_nested_pattern
+    end
+  end
+
+  if H.is_region_array(tobj_spec) then
+    -- Iterate over all spans representing regions in array
+    for _, region in ipairs(tobj_spec) do
+      -- Consider region only if it is completely within neighborhood
+      if neighborhood.is_region_inside(region) then f(neighborhood.region_to_span(region)) end
+    end
+  else
+    -- Iterate over all matched spans
+    for _, nested_pattern in ipairs(H.cartesian_product(tobj_spec)) do
+      current_nested_pattern = nested_pattern
+      H.iterate_matched_spans(neighborhood['1d'], nested_pattern, f)
+    end
+  end
+
+  local extract_pattern
+  if best_nested_pattern ~= nil then extract_pattern = best_nested_pattern[#best_nested_pattern] end
+  return { span = best_span, extract_pattern = extract_pattern }
+end
+
+H.iterate_matched_spans = function(line, nested_pattern, f)
+  local max_level = #nested_pattern
+  -- Keep track of visited spans to ensure only one call of `f`.
+  -- Example: `((a) (b))`, `{'%b()', '%b()'}`
+  local visited = {}
+
+  local process
+  process = function(level, level_line, level_offset)
+    local pattern = nested_pattern[level]
+    local next_span = function(s, init) return H.string_find(s, pattern, init) end
+    if vim.is_callable(pattern) then next_span = pattern end
+
+    local is_same_balanced = type(pattern) == 'string' and pattern:match('^%%b(.)%1$') ~= nil
+    local init = 1
+    while init <= level_line:len() do
+      local from, to = next_span(level_line, init)
+      if from == nil then break end
+
+      if level == max_level then
+        local found_match = H.new_span(from + level_offset, to + level_offset)
+        local found_match_id = string.format('%s_%s', found_match.from, found_match.to)
+        if not visited[found_match_id] then
+          f(found_match)
+          visited[found_match_id] = true
+        end
+      else
+        local next_level_line = level_line:sub(from, to)
+        local next_level_offset = level_offset + from - 1
+        process(level + 1, next_level_line, next_level_offset)
+      end
+
+      -- Start searching from right end to implement "balanced" pair.
+      -- This doesn't work with regular balanced pattern because it doesn't
+      -- capture nested brackets.
+      init = (is_same_balanced and to or from) + 1
+    end
+  end
+
+  process(1, line, 0)
+end
+
+-- NOTE: spans are end-exclusive to allow empty spans via `from == to`
+H.new_span = function(from, to) return { from = from, to = to == nil and from or (to + 1) } end
+
+---@param candidate table Candidate span to test agains `current`.
+---@param current table|nil Current best span.
+---@param reference table Reference span to cover.
+---@param opts table Fields: .
+---@private
+H.is_better_span = function(candidate, current, reference, opts)
+  -- Candidate should be never equal or nested inside reference
+  if H.is_span_covering(reference, candidate) or H.is_span_equal(candidate, reference) then return false end
+
+  return H.span_compare_methods[opts.search_method](candidate, current, reference)
+end
+
+H.span_compare_methods = {
+  cover = function(candidate, current, reference)
+    local res = H.is_better_covering_span(candidate, current, reference)
+    if res ~= nil then return res end
+    -- If both are not covering, `candidate` is not better (as it must cover)
+    return false
+  end,
+
+  cover_or_next = function(candidate, current, reference)
+    local res = H.is_better_covering_span(candidate, current, reference)
+    if res ~= nil then return res end
+
+    -- If not covering, `candidate` must be "next" and closer to reference
+    if not H.is_span_on_left(reference, candidate) then return false end
+    if current == nil then return true end
+
+    local dist = H.span_distance.next
+    return dist(candidate, reference) < dist(current, reference)
+  end,
+
+  cover_or_prev = function(candidate, current, reference)
+    local res = H.is_better_covering_span(candidate, current, reference)
+    if res ~= nil then return res end
+
+    -- If not covering, `candidate` must be "previous" and closer to reference
+    if not H.is_span_on_left(candidate, reference) then return false end
+    if current == nil then return true end
+
+    local dist = H.span_distance.prev
+    return dist(candidate, reference) < dist(current, reference)
+  end,
+
+  cover_or_nearest = function(candidate, current, reference)
+    local res = H.is_better_covering_span(candidate, current, reference)
+    if res ~= nil then return res end
+
+    -- If not covering, `candidate` must be closer to reference
+    if current == nil then return true end
+
+    local dist = H.span_distance.near
+    return dist(candidate, reference) < dist(current, reference)
+  end,
+
+  next = function(candidate, current, reference)
+    if H.is_span_covering(candidate, reference) then return false end
+
+    -- `candidate` must be "next" and closer to reference
+    if not H.is_span_on_left(reference, candidate) then return false end
+    if current == nil then return true end
+
+    local dist = H.span_distance.next
+    return dist(candidate, reference) < dist(current, reference)
+  end,
+
+  prev = function(candidate, current, reference)
+    if H.is_span_covering(candidate, reference) then return false end
+
+    -- `candidate` must be "previous" and closer to reference
+    if not H.is_span_on_left(candidate, reference) then return false end
+    if current == nil then return true end
+
+    local dist = H.span_distance.prev
+    return dist(candidate, reference) < dist(current, reference)
+  end,
+
+  nearest = function(candidate, current, reference)
+    if H.is_span_covering(candidate, reference) then return false end
+
+    -- `candidate` must be closer to reference
+    if current == nil then return true end
+
+    local dist = H.span_distance.near
+    return dist(candidate, reference) < dist(current, reference)
+  end,
+}
+
+H.span_distance = {
+  -- Other possible choices of distance between [a1, a2] and [b1, b2]:
+  -- - Hausdorff distance: max(|a1 - b1|, |a2 - b2|).
+  --   Source:
+  --   https://math.stackexchange.com/questions/41269/distance-between-two-ranges
+  -- - Minimum distance: min(|a1 - b1|, |a2 - b2|).
+
+  -- Distance is chosen so that "next span" in certain direction is the closest
+  next = function(span_1, span_2) return math.abs(span_1.from - span_2.from) end,
+  prev = function(span_1, span_2) return math.abs(span_1.to - span_2.to) end,
+  near = function(span_1, span_2) return math.min(math.abs(span_1.from - span_2.from), math.abs(span_1.to - span_2.to)) end,
+}
+
+H.is_better_covering_span = function(candidate, current, reference)
+  local candidate_is_covering = H.is_span_covering(candidate, reference)
+  local current_is_covering = H.is_span_covering(current, reference)
+
+  if candidate_is_covering and current_is_covering then
+    -- Covering candidate is better than covering current if it is narrower
+    return (candidate.to - candidate.from) < (current.to - current.from)
+  end
+  if candidate_is_covering and not current_is_covering then return true end
+  if not candidate_is_covering and current_is_covering then return false end
+
+  -- Return `nil` if neither span is covering
+  return nil
+end
+
+--stylua: ignore
+H.is_span_covering = function(span, span_to_cover)
+  if span == nil or span_to_cover == nil then return false end
+  if span.from == span.to then
+    return (span.from == span_to_cover.from) and (span_to_cover.to == span.to)
+  end
+  if span_to_cover.from == span_to_cover.to then
+    return (span.from <= span_to_cover.from) and (span_to_cover.to < span.to)
+  end
+
+  return (span.from <= span_to_cover.from) and (span_to_cover.to <= span.to)
+end
+
+H.is_span_equal = function(span_1, span_2)
+  if span_1 == nil or span_2 == nil then return false end
+  return (span_1.from == span_2.from) and (span_1.to == span_2.to)
+end
+
+H.is_span_on_left = function(span_1, span_2)
+  if span_1 == nil or span_2 == nil then return false end
+  return (span_1.from <= span_2.from) and (span_1.to <= span_2.to)
+end
+
+H.is_point_inside_spans = function(point, spans)
+  for _, span in ipairs(spans) do
+    if span[1] <= point and point <= span[2] then return true end
+  end
+  return false
+end
+
+-- Work with Lua patterns -----------------------------------------------------
+H.extract_span = function(s, extract_pattern, ai_type)
+  local positions = { s:match(extract_pattern) }
+
+  if #positions == 1 and type(positions[1]) == 'string' then
+    if s:len() == 0 then return H.new_span(0, 0) end
+    return H.new_span(1, s:len())
+  end
+
+  local is_all_numbers = true
+  for _, pos in ipairs(positions) do
+    if type(pos) ~= 'number' then is_all_numbers = false end
+  end
+
+  local is_valid_positions = is_all_numbers and (#positions == 2 or #positions == 4)
+  if not is_valid_positions then
+    local msg = 'Could not extract proper positions (two or four empty captures) from '
+      .. string.format([[string '%s' with extraction pattern '%s'.]], s, extract_pattern)
+    H.error(msg)
+  end
+
+  local ai_spans
+  if #positions == 2 then
+    ai_spans = { a = H.new_span(1, s:len()), i = H.new_span(positions[1], positions[2] - 1) }
+  else
+    ai_spans = { a = H.new_span(positions[1], positions[4] - 1), i = H.new_span(positions[2], positions[3] - 1) }
+  end
+
+  return ai_spans[ai_type]
+end
+
+-- Work with cursor neighborhood ----------------------------------------------
+---@param reference_region table Reference region.
+---@param n_neighbors number Maximum number of neighbors to include before
+---   start line and after end line.
+---@private
+H.get_neighborhood = function(reference_region, n_neighbors)
+  -- Compute '2d neighborhood' of (possibly empty) region
+  local from_line, to_line = reference_region.from.line, (reference_region.to or reference_region.from).line
+  local line_start = math.max(1, from_line - n_neighbors)
+  local line_end = math.min(vim.api.nvim_buf_line_count(0), to_line + n_neighbors)
+  local neigh2d = vim.api.nvim_buf_get_lines(0, line_start - 1, line_end, false)
+  -- Append 'newline' character to distinguish between lines in 1d case
+  for k, v in pairs(neigh2d) do
+    neigh2d[k] = v .. '\n'
+  end
+
+  -- '1d neighborhood': position is determined by offset from start
+  local neigh1d = table.concat(neigh2d, '')
+
+  -- Convert 2d buffer position to 1d offset
+  local pos_to_offset = function(pos)
+    if pos == nil then return nil end
+    local line_num = line_start
+    local offset = 0
+    while line_num < pos.line do
+      offset = offset + neigh2d[line_num - line_start + 1]:len()
+      line_num = line_num + 1
+    end
+
+    return offset + pos.col
+  end
+
+  -- Convert 1d offset to 2d buffer position
+  local offset_to_pos = function(offset)
+    if offset == nil then return nil end
+    local line_num = 1
+    local line_offset = 0
+    while line_num <= #neigh2d and line_offset + neigh2d[line_num]:len() < offset do
+      line_offset = line_offset + neigh2d[line_num]:len()
+      line_num = line_num + 1
+    end
+
+    return { line = line_start + line_num - 1, col = offset - line_offset }
+  end
+
+  -- Convert 2d region to 1d span
+  local region_to_span = function(region)
+    if region == nil then return nil end
+    local is_empty = region.to == nil
+    local to = region.to or region.from
+    return { from = pos_to_offset(region.from), to = pos_to_offset(to) + (is_empty and 0 or 1) }
+  end
+
+  -- Convert 1d span to 2d region
+  local span_to_region = function(span)
+    if span == nil then return nil end
+    -- NOTE: this might lead to outside of line positions due to added `\n` at
+    -- the end of lines in 1d-neighborhood. However, this is crucial for
+    -- allowing `i` textobjects to collapse multiline selections.
+    local res = { from = offset_to_pos(span.from) }
+
+    -- Convert empty span to empty region
+    if span.from < span.to then res.to = offset_to_pos(span.to - 1) end
+    return res
+  end
+
+  local is_region_inside = function(region)
+    local res = line_start <= region.from.line
+    if region.to ~= nil then res = res and (region.to.line <= line_end) end
+    return res
+  end
+
+  return {
+    n_neighbors = n_neighbors,
+    region = reference_region,
+    ['1d'] = neigh1d,
+    ['2d'] = neigh2d,
+    pos_to_offset = pos_to_offset,
+    offset_to_pos = offset_to_pos,
+    region_to_span = region_to_span,
+    span_to_region = span_to_region,
+    is_region_inside = is_region_inside,
+  }
+end
+
+-- Work with user input -------------------------------------------------------
+H.user_textobject_id = function(ai_type)
+  -- Get from user single character textobject identifier
+  local needs_help_msg = true
+  vim.defer_fn(function()
+    if not needs_help_msg then return end
+
+    local msg = string.format('Enter `%s` textobject identifier (single character) ', ai_type)
+    H.message(msg)
+  end, 1000)
+  local ok, char = pcall(vim.fn.getchar)
+  needs_help_msg = false
+
+  -- Terminate if couldn't get input (like with ) or it is ``
+  if not ok or char == 27 then return nil end
+
+  if type(char) == 'number' then char = vim.fn.nr2char(char) end
+  if char:find('^[%w%p%s]$') == nil then
+    H.message('Input must be single character: alphanumeric, punctuation, or space.')
+    return nil
+  end
+
+  return char
+end
+
+H.user_input = function(prompt, text)
+  -- Register temporary keystroke listener to distinguish between cancel with
+  -- `` and immediate ``.
+  local on_key = vim.on_key or vim.register_keystroke_callback
+  local was_cancelled = false
+  on_key(function(key)
+    if key == vim.api.nvim_replace_termcodes('', true, true, true) then was_cancelled = true end
+  end, H.ns_id.input)
+
+  -- Ask for input
+  local opts = { prompt = '(mini.ai) ' .. prompt .. ': ', default = text or '' }
+  -- Use `pcall` to allow `` to cancel user input
+  local ok, res = pcall(vim.fn.input, opts)
+
+  -- Stop key listening
+  on_key(nil, H.ns_id.input)
+
+  if not ok or was_cancelled then return end
+  return res
+end
+
+-- Work with Visual mode ------------------------------------------------------
+H.is_visual_mode = function()
+  local cur_mode = vim.fn.mode()
+  -- '\22' is an escaped ``
+  return cur_mode == 'v' or cur_mode == 'V' or cur_mode == '\22', cur_mode
+end
+
+H.exit_to_normal_mode = function()
+  -- '\28\14' is an escaped version of ``
+  vim.cmd('normal! \28\14')
+end
+
+H.get_visual_region = function()
+  local is_vis, _ = H.is_visual_mode()
+  if not is_vis then return end
+  local res = {
+    from = { line = vim.fn.line('v'), col = vim.fn.col('v') },
+    to = { line = vim.fn.line('.'), col = vim.fn.col('.') },
+  }
+  if res.from.line > res.to.line or (res.from.line == res.to.line and res.from.col > res.to.col) then
+    res = { from = res.to, to = res.from }
+  end
+  return res
+end
+
+-- Utilities ------------------------------------------------------------------
+H.message = function(msg)
+  vim.cmd([[echon '']])
+  vim.cmd('redraw')
+  vim.cmd('echomsg ' .. vim.inspect('(mini.ai) ' .. msg))
+end
+
+H.error = function(msg) error(string.format('(mini.ai) %s', msg), 0) end
+
+H.map = function(mode, key, rhs, opts)
+  if key == '' then return end
+
+  opts = vim.tbl_deep_extend('force', { noremap = true, silent = true }, opts or {})
+
+  -- Use mapping description only in Neovim>=0.7
+  if vim.fn.has('nvim-0.7') == 0 then opts.desc = nil end
+
+  vim.api.nvim_set_keymap(mode, key, rhs, opts)
+end
+
+H.string_find = function(s, pattern, init)
+  init = init or 1
+
+  -- Match only start of full string if pattern says so.
+  -- This is needed because `string.find()` doesn't do this.
+  -- Example: `string.find('(aaa)', '^.*$', 4)` returns `4, 5`
+  if pattern:sub(1, 1) == '^' then
+    if init > 1 then return nil end
+    return string.find(s, pattern)
+  end
+
+  -- Handle patterns `x.-y` differently: make match as small as possible. This
+  -- doesn't allow `x` be present inside `.-` match, just as with `yyy`. Which
+  -- also leads to a behavior similar to punctuation id (like with `va_`): no
+  -- covering is possible, only next, previous, or nearest.
+  local check_left, _, prev = string.find(pattern, '(.)%.%-')
+  local is_pattern_special = check_left ~= nil and prev ~= '%'
+  if not is_pattern_special then return string.find(s, pattern, init) end
+
+  -- Make match as small as possible
+  local from, to = string.find(s, pattern, init)
+  if from == nil then return end
+
+  local cur_from, cur_to = from, to
+  while cur_to == to do
+    from, to = cur_from, cur_to
+    cur_from, cur_to = string.find(s, pattern, cur_from + 1)
+  end
+
+  return from, to
+end
+
+---@param arr table List of items. If item is list, consider as set for
+---   product. Else - make it single item list.
+---@private
+H.cartesian_product = function(arr)
+  if not (type(arr) == 'table' and #arr > 0) then return {} end
+  arr = vim.tbl_map(function(x) return vim.tbl_islist(x) and x or { x } end, arr)
+
+  local res, cur_item = {}, {}
+  local process
+  process = function(level)
+    for i = 1, #arr[level] do
+      table.insert(cur_item, arr[level][i])
+      if level == #arr then
+        -- Flatten array to allow tables as elements of step tables
+        table.insert(res, vim.tbl_flatten(cur_item))
+      else
+        process(level + 1)
+      end
+      table.remove(cur_item, #cur_item)
+    end
+  end
+
+  process(1)
+  return res
+end
+
+H.wrap_callable_table = function(x)
+  if vim.is_callable(x) and type(x) == 'table' then return function(...) return x(...) end end
+  return x
+end
+
+return MiniAi
diff --git a/dotfiles/pack/plugins/start/mini.nvim/lua/mini/align.lua b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/align.lua
new file mode 100755
index 0000000..3ff34ef
--- /dev/null
+++ b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/align.lua
@@ -0,0 +1,2052 @@
+-- MIT License Copyright (c) 2022 Evgeni Chasnovski
+
+-- Documentation ==============================================================
+--- 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:
+---     -  lines into parts based on Lua pattern(s) or user-supplied rule.
+---     -  parts for certain side(s) to be same width inside columns.
+---     -  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 `` (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.
+---@tag mini.align
+---@tag MiniAlign
+
+--- 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:  and  for
+---         inclusive start and end positions ( might be `nil` to describe
+---         empty region). Each position is also a table with line  and
+---         column  (both start at 1).
+---
+--- MODE    Either charwise ("char", `v`, |charwise|), linewise ("line", `V`,
+---         |linewise|) or blockwise ("block", ``, |blockwise-visual|)
+---@tag MiniAlign-glossary
+
+--- 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:
+--- - . 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.
+--- - . Take array of strings and convert it to parts with `steps.split()`.
+---   It has `(strings, opts)` signature and should return parts.
+--- - . Take parts and consecutively apply all pre-justify
+---   steps (`steps.pre_justify`). Each one has `(parts, opts)` signature and
+---   should modify parts in place.
+--- - . Take parts and apply `steps.justify()`. It has `(parts, opts)`
+---   signature and should modify parts in place.
+--- - . Take parts and consecutively apply all pre-merge
+---   steps (`steps.pre_merge`). Each one has `(parts, opts)` signature and
+---   should modify parts in place.
+--- - . 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:
+--- - . Initiated via mapping defined in `start` of
+---   `MiniAlign.config.mappings`. Alignment is accepted once split pattern becomes
+---   non-default.
+--- - . Initiated via mapping defined in `start_with_preview` of
+---   `MiniAlign.config.mappings`. Alignment result is shown after every modifier
+---   and is accepted after `` (`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):
+--- - :
+---     - 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 (``). They all behave differently.
+--- - . 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.
+--- - . In case of active preview, accept current result by
+---   pressing ``. Discard any result and return to initial regions with
+---   either `` or ``.
+---
+--- See more in |MiniAlign-modifiers-builtin| and |MiniAlign-examples|.
+---
+--- Notes:
+--- - Visual blockwise selection works best with 'virtualedit' equal to "block"
+---   or "all".
+---@tag MiniAlign-algorithm
+
+--- 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 ~
+---
+---  Enter split pattern (confirm prompt by pressing ``). Input is treated
+---     as plain delimiter.
+---
+---     Before: >
+---     a-b-c
+---     aa-bb-cc
+--- <
+---     After typing `s-`: >
+---     a -b -c
+---     aa-bb-cc
+--- <
+---  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
+--- <
+---  Enter merge delimiter (confirm prompt by pressing ``).
+---
+---     Before: >
+---     a_b_c
+---     aa_bb_cc
+--- <
+---     After typing `_m--` (first make split by `_`): >
+---     a --_--b --_--c
+---     aa--_--bb--_--cc
+--- <
+--- Modifiers adding pre-steps ~
+---
+---  Enter filter expression. See more details in |MiniAlign.gen_step.filter()|.
+---
+---     Before: >
+---     a_b_c
+---     aa_bb_cc
+--- <
+---     After typing `_fn==1` (first make split by `_`): >
+---     a _b_c
+---     aa_bb_cc
+--- <
+---  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
+--- <
+--- 

Pair neighboring parts. +--- +--- Before: > +--- a_b_c +--- aaa_bbb_ccc +--- < +--- After typing `_p` (first make split by `_`): > +--- a_ b_ c +--- aaa_bbb_ccc +--- < +--- 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 ~ +--- +--- 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` results in only "trim" step to be left. +--- - `it` 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 ``: > +--- a b c +--- aa bb cc +---@tag MiniAlign-modifiers-builtin + +--- 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!` +--- - `=p` +--- - `=i` (execute `:lua vim.o.commentstring = '# %s'` for full experience) +--- - `=` +--- - `=p` +--- - `=fn==1` +--- - `=fn==1t` +--- - `=frow>7` +--- +---@tag MiniAlign-examples + +---@alias __with_preview boolean|nil Whether to align with live preview. + +-- Module definition ========================================================== +local MiniAlign = {} +local H = {} + +--- Module setup +--- +---@param config table|nil Module config table. See |MiniAlign.config|. +--- +---@usage `require('mini.align').setup({})` (replace `{}` with your `config` table) +MiniAlign.setup = function(config) + -- Export module + _G.MiniAlign = MiniAlign + + -- Setup config + config = H.setup_config(config) + + -- Apply config + H.apply_config(config) +end + +--- Module config +--- +--- Default values: +---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section) +---@text # 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.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 + --minidoc_replace_start ['s'] = --, + ['s'] = function(_, opts) + local input = H.user_input('Enter split Lua pattern') + if input == nil then return end + opts.split_pattern = input + end, + --minidoc_replace_end + --minidoc_replace_start ['j'] = --, + ['j'] = function(_, opts) + -- stylua: ignore + H.echo({ + { 'Select justify: ', 'ModeMsg' }, { 'l', 'Question' }, { 'eft, ' }, + { 'c', 'Question' }, { 'enter, ' }, { 'r', 'Question' }, { 'ight, ' }, + { 'n', 'Question' }, { 'one' } + }) + local ok, char = pcall(vim.fn.getchar) + if not ok or char == 27 then return end + if type(char) == 'number' then char = vim.fn.nr2char(char) end + + local direction = ({ l = 'left', c = 'center', r = 'right', n = 'none' })[char] + if direction == nil then return end + opts.justify_side = direction + end, + --minidoc_replace_end + --minidoc_replace_start ['m'] = --, + ['m'] = function(_, opts) + local input = H.user_input('Enter merge delimiter') + if input == nil then return end + opts.merge_delimiter = input + end, + --minidoc_replace_end + + -- Modifiers adding pre-steps + --minidoc_replace_start ['f'] = --, + ['f'] = function(steps, _) + local input = H.user_input('Enter filter expression') + local step = MiniAlign.gen_step.filter(input) + if step == nil then return end + table.insert(steps.pre_justify, step) + end, + --minidoc_replace_end + --minidoc_replace_start ['i'] = --, + ['i'] = function(steps, _) table.insert(steps.pre_split, MiniAlign.gen_step.ignore_split()) end, + --minidoc_replace_end + --minidoc_replace_start ['p'] = --, + ['p'] = function(steps, _) table.insert(steps.pre_justify, MiniAlign.gen_step.pair()) end, + --minidoc_replace_end + --minidoc_replace_start ['t'] = --, + ['t'] = function(steps, _) table.insert(steps.pre_justify, MiniAlign.gen_step.trim()) end, + --minidoc_replace_end + + -- Delete some last pre-step + --minidoc_replace_start [''] = --, + [vim.api.nvim_replace_termcodes('', true, true, true)] = function(steps, _) + local has_pre = {} + for _, pre in ipairs({ 'pre_split', 'pre_justify', 'pre_merge' }) do + if #steps[pre] > 0 then table.insert(has_pre, pre) end + end + + if #has_pre == 0 then return end + + if #has_pre == 1 then + local pre = steps[has_pre[1]] + table.remove(pre, #pre) + return + end + + --stylua: ignore + H.echo({ + { 'Select pre-step to remove: ', 'ModeMsg' }, { 's', 'Question' }, { 'plit, ' }, + { 'j', 'Question' }, { 'ustify, ' }, { 'm', 'Question' }, { 'erge' }, + }) + local ok, char = pcall(vim.fn.getchar) + if not ok or char == 27 then return end + if type(char) == 'number' then char = vim.fn.nr2char(char) end + + if char == 's' then table.remove(steps.pre_split, #steps.pre_split) end + if char == 'j' then table.remove(steps.pre_justify, #steps.pre_justify) end + if char == 'm' then table.remove(steps.pre_merge, #steps.pre_merge) end + end, + --minidoc_replace_end + + -- Special configurations for common splits + --minidoc_replace_start ['='] = --, + [','] = function(steps, opts) + opts.split_pattern = ',' + table.insert(steps.pre_justify, MiniAlign.gen_step.trim()) + table.insert(steps.pre_justify, MiniAlign.gen_step.pair()) + opts.merge_delimiter = ' ' + end, + --minidoc_replace_end + --minidoc_replace_start [' '] = --, + [' '] = function(steps, opts) + table.insert( + steps.pre_split, + MiniAlign.new_step('squash', function(strings) + -- Replace all space sequences with single space (except indent) + for i, s in ipairs(strings) do + strings[i] = s:gsub('()(%s+)', function(n, space) return n == 1 and space or ' ' end) + end + end) + ) + -- Don't use `' '` to respect indent + opts.split_pattern = '%s+' + end, + --minidoc_replace_end + }, + + -- 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, + }, +} +--minidoc_afterlines_end + +-- Module functionality ======================================================= +--- Align strings +--- +--- For details about alignment process see |MiniAlign-algorithm|. +--- +---@param strings table Array of strings. +---@param 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. +---@param 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_strings = function(strings, opts, steps) + -- Validate arguments + if not H.is_array_of(strings, H.is_string) then + H.error('First argument of `MiniAlign.align_strings()` should be array of strings.') + end + opts = H.normalize_opts(opts) + steps = H.normalize_steps(steps, 'steps') + + -- Make a copy so that modification in place doesn't affect input + strings = vim.deepcopy(strings) + + -- Pre split + for _, step in ipairs(steps.pre_split) do + H.apply_step(step, strings, opts, 'pre_split') + end + + -- Split + local parts = H.apply_step(steps.split, strings, opts, 'split') + if not H.is_parts(parts) then + if H.can_be_parts(parts) then + parts = MiniAlign.as_parts(parts) + else + H.error('Output of `split` step should be convertable to parts. See `:h MiniAlign.as_parts()`.') + end + end + + -- Pre justify + for _, step in ipairs(steps.pre_justify) do + H.apply_step(step, parts, opts, 'pre_justify') + end + + -- Justify + H.apply_step(steps.justify, parts, opts, 'justify') + + -- Pre merge + for _, step in ipairs(steps.pre_merge) do + H.apply_step(step, parts, opts, 'pre_merge') + end + + -- Merge + local new_strings = H.apply_step(steps.merge, parts, opts, 'merge') + if not H.is_array_of(new_strings, H.is_string) then H.error('Output of `merge` step should be array of strings.') end + return new_strings +end + +--- 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|). +--- +---@param mode string Selection mode. One of "char", "line", "block". +MiniAlign.align_user = function(mode) + local modifiers = H.get_config().modifiers + local with_preview = H.cache.with_preview + local opts = H.cache.opts or H.normalize_opts() + local steps = H.cache.steps or H.normalize_steps() + + local steps_are_from_cache = H.cache.steps ~= nil + H.cache.region = nil + + -- Track if lines were actually set to properly undo during preview + local lines_were_set = false + + -- Make initial process + lines_were_set = H.process_current_region(lines_were_set, mode, opts, steps) + + -- Make early return: + -- - If cache is present (enables dot-repeat). + -- - If `split` is not default with no preview (no more information needed). + if steps_are_from_cache or (not with_preview and opts.split_pattern ~= '') then return end + + -- Ask user to input modifier id until no more is needed + local n_iter = 0 + while true do + -- Get modifier from user + local id = H.user_modifier(with_preview, H.make_status_msg_chunks(opts, steps)) + n_iter = n_iter + 1 + + -- Stop in case user supplied inappropriate modifer id (abort) + -- Also stop in case of too many iterations (guard from infinite cycle) + if id == nil or n_iter > 1000 then + if lines_were_set then H.undo() end + if n_iter > 1000 then H.echo({ { 'Too many modifiers typed.', 'WarningMsg' } }, true) end + break + end + + -- Stop preview after `` (confirmation) + if with_preview and id == '\r' then break end + + -- Apply modifier + local mod = modifiers[id] + if mod == nil then + -- Use supplied identifier as split pattern + opts.split_pattern = vim.pesc(id) + else + -- Modifier should change input `steps` table in place + local ok, out = pcall(modifiers[id], steps, opts) + if not ok then + -- Force message to appear for 500ms because it might be overridden by + -- helper status message + local msg = string.format('Modifier %s should be properly callable. Reason: %s', vim.inspect(id), out) + H.echo({ { msg, 'WarningMsg' } }, true) + vim.cmd('redraw') + vim.loop.sleep(500) + end + end + + -- Normalize steps and options while validating their correctness + opts = H.normalize_opts(opts) + steps = H.normalize_steps(steps, opts) + + -- Process region while tracking if lines were set at least once + local lines_now_set = H.process_current_region(lines_were_set, mode, opts, steps) + lines_were_set = lines_were_set or lines_now_set + + -- Stop in "no preview" mode right after `split` is defined + if not with_preview and opts.split_pattern ~= '' then break end + end + + -- Remove helper status message (if shown) + H.unecho() +end + +--- Perfrom action in Normal mode +--- +--- Used in Normal mode mapping. No need to use it directly. +--- +---@param with_preview __with_preview +MiniAlign.action_normal = function(with_preview) + if H.is_disabled() then return end + + H.cache = { with_preview = with_preview } + + -- Set 'operatorfunc' which will be later called with appropriate marks set + vim.cmd('set operatorfunc=v:lua.MiniAlign.align_user') + return 'g@' +end + +--- Perfrom action in Visual mode +--- +--- Used in Visual mode mapping. No need to use it directly. +--- +---@param with_preview __with_preview +MiniAlign.action_visual = function(with_preview) + if H.is_disabled() then return end + + H.cache = { with_preview = with_preview } + + -- Perform action and exit Visual mode + local mode = ({ ['v'] = 'char', ['V'] = 'line', ['\22'] = 'block' })[vim.fn.mode(1)] + MiniAlign.align_user(mode) + vim.cmd('normal! \27') +end + +--- 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 +--- +---@field 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 ( has row number, has column number). +--- Returns new 2d array. +--- +---@field 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. +--- +---@field get_dims function Return dimensions of parts array: a table with +--- and keys having number of rows and number of columns (maximum +--- number of elements across all rows). +--- +---@field 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" } } +--- +---@field 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" } } +--- +---@field slice_col function Return column with input index `j`. Note: it might +--- not be an array if rows have unequal number of columns. +--- +---@field slice_row function Return row with input index `i`. +--- +---@field 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.as_parts = function(arr2d) + local ok, msg = H.can_be_parts(arr2d) + if not ok then H.error('Input of `as_parts()` ' .. msg) end + + local parts = vim.deepcopy(arr2d) + local methods = {} + + methods.apply = function(f) + local res = {} + for i, row in ipairs(parts) do + res[i] = {} + for j, s in ipairs(row) do + res[i][j] = f(s, { row = i, col = j }) + end + end + return res + end + + methods.apply_inplace = function(f) + for i, row in ipairs(parts) do + for j, s in ipairs(row) do + local new_val = f(s, { row = i, col = j }) + if type(new_val) ~= 'string' then H.error('Input of `apply_inplace()` method should always return string.') end + parts[i][j] = new_val + end + end + + return parts + end + + methods.get_dims = function() + local n_cols = 0 + for _, row in ipairs(parts) do + n_cols = math.max(n_cols, #row) + end + return { row = #parts, col = n_cols } + end + + -- Group cells into single string based on boolean mask. + -- Can be used for filtering separators and sticking separator to its part. + methods.group = function(mask, direction) + direction = direction or 'left' + for i, row in ipairs(parts) do + local group_tables = H.group_by_mask(row, mask[i], direction) + parts[i] = vim.tbl_map(table.concat, group_tables) + end + return parts + end + + methods.pair = function(direction) + direction = direction or 'left' + + local mask = {} + for i, row in ipairs(parts) do + mask[i] = {} + for j, _ in ipairs(row) do + -- Count from corresponding end + local num = direction == 'left' and j or (#row - j + 1) + mask[i][j] = num % 2 == 0 + end + end + + parts.group(mask, direction) + return parts + end + + -- NOTE: output might not be an array (some rows can not have input column) + -- Use `vim.tbl_keys()` and `vim.tbl_values()` + methods.slice_col = function(j) + return vim.tbl_map(function(row) return row[j] end, parts) + end + + methods.slice_row = function(i) return parts[i] or {} end + + methods.trim = function(direction, indent) + direction = direction or 'both' + indent = indent or 'keep' + + -- Verify arguments + local trim_fun = H.trim_functions[direction] + if not vim.is_callable(trim_fun) then + local allowed = vim.tbl_map(vim.inspect, vim.tbl_keys(H.trim_functions)) + table.sort(allowed) + H.error('`direction` should be one of ' .. table.concat(allowed, ', ') .. '.') + end + + local indent_fun = H.indent_functions[indent] + if not vim.is_callable(indent_fun) then + local allowed = vim.tbl_map(vim.inspect, vim.tbl_keys(H.indent_functions)) + table.sort(allowed) + H.error('`indent` should be one of ' .. table.concat(allowed, ', ') .. '.') + end + + -- Compute indentation to restore later + local row_indent = vim.tbl_map(function(row) return row[1]:match('^(%s*)') end, parts) + row_indent = indent_fun(row_indent) + + -- Trim + parts.apply_inplace(trim_fun) + + -- Restore indentation if it was removed + if vim.tbl_contains({ 'both', 'left' }, direction) then + for i, row in ipairs(parts) do + row[1] = string.format('%s%s', row_indent[i], row[1]) + end + end + + return parts + end + + return setmetatable(parts, { class = 'parts', __index = methods }) +end + +--- 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. +--- +---@param name string Step name. +---@param action function|table Step action. Should be a callable object +--- (see |vim.is_callable()|). +--- +---@return table A table with keys: with `name` argument, with `action`. +MiniAlign.new_step = function(name, action) + if type(name) ~= 'string' then H.error('Step name should be string.') end + if not vim.is_callable(action) then H.error('Step action should be callable.') end + return { name = name, action = action } +end + +--- 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 = {} + +--- 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): +--- - - 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+'`, `{ '<', '>' }`. +--- - - 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. +--- +---@seealso |MiniAlign.gen_step.ignore_split()| heavily uses `split_exclude_patterns`. +MiniAlign.gen_step.default_split = function() return MiniAlign.new_step('split', H.default_action_split) end + +--- 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): +--- - - 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". +--- - - 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_justify = function() return MiniAlign.new_step('justify', H.default_action_justify) end + +--- 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): +--- - - string or array of strings. Default: `''`. +--- Examples: `' '`, `{ '', ' ' }`. +--- +---@return table A step named "merge" and with appropriate callable action. +MiniAlign.gen_step.default_merge = function() return MiniAlign.new_step('merge', H.default_action_merge) end + +--- 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 number of current element. +--- - - total number of rows in parts. +--- - - column number of current element. +--- - - total number of columns in current row. +--- - - string value of current element. +--- - - column pair number of current element. Useful when filtering by +--- result of pattern splitting. +--- - - 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). +--- +---@param 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.filter = function(expr) + local action = H.make_filter_action(expr) + if action == nil then return end + return MiniAlign.new_step('filter', action) +end + +--- Generate ignore step +--- +--- Output adds certain values to `split_exclude_patterns` option. Should be +--- used as pre-split step. +--- +---@param patterns table Array of patterns to be added to +--- `split_exclude_patterns` as is. Default: `{ [[".-"]] }` (excludes strings +--- for most cases). +---@param 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. +--- +---@seealso |MiniAlign.gen_step.default_split()| for details about +--- `split_exclude_patterns` option. +MiniAlign.gen_step.ignore_split = function(patterns, exclude_comment) + patterns = patterns or { '".-"' } + if exclude_comment == nil then exclude_comment = true end + + -- Validate ingput + if not H.is_array_of(patterns, H.is_string) then + H.error('Argument `patterns` of `ignore_split()` should be array of strings.') + end + if type(exclude_comment) ~= 'boolean' then + H.error('Argument `exclude_comment` of `ignore_split()` should be boolean.') + end + + -- Make action which modifies `opts.split_exclude_patterns` + local action = function(_, opts) + local excl = opts.split_exclude_patterns or {} + + -- Add supplied patterns while avoiding duplication + for _, patt in ipairs(patterns) do + if not vim.tbl_contains(excl, patt) then table.insert(excl, patt) end + end + + -- Possibly add current comment pattern while avoiding duplication + if exclude_comment then + -- In 'commentstring', `%s` denotes the comment content + local comment_pattern = vim.pesc(vim.o.commentstring):gsub('%%%%s', '.-') + -- Ignore to the end of the string if 'commentstring' is like "xxx%s" + comment_pattern = comment_pattern:gsub('%.%-%s*$', '.*') + if not vim.tbl_contains(excl, comment_pattern) then table.insert(excl, comment_pattern) end + end + + opts.split_exclude_patterns = excl + end + + return MiniAlign.new_step('ignore', action) +end + +--- Generate pair step +--- +--- Output calls `pair()` method of parts (see |MiniAlign.as_parts()|) with +--- supplied `direction` argument. +--- +---@param direction string Which direction to pair. One of "left" (default) or +---"right". +--- +---@return table A step named "pair" and with appropriate callable action. +MiniAlign.gen_step.pair = function(direction) + return MiniAlign.new_step('pair', function(parts, _) parts.pair(direction) end) +end + +--- Generate trim step +--- +--- Output calls `trim()` method of parts (see |MiniAlign.as_parts()|) with +--- supplied `direction` and `indent` arguments. +--- +---@param direction string Which sides to trim whitespace. One of "both" +--- (default), "left", "right", "none". +---@param 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. +MiniAlign.gen_step.trim = function(direction, indent) + return MiniAlign.new_step('trim', function(parts, _) parts.trim(direction, indent) end) +end + +-- Helper data ================================================================ +-- Module default config +H.default_config = MiniAlign.config + +-- Cache for various operations +H.cache = {} + +-- Module's namespaces +H.ns_id = { + -- Track user input + input = vim.api.nvim_create_namespace('MiniAlignInput'), +} + +-- Pad functions for supported justify directions +-- Allow to not add trailing whitespace +H.pad_functions = { + left = function(x, n_spaces, no_trailing) + if no_trailing or H.is_infinite(n_spaces) then return x end + return string.format('%s%s', x, string.rep(' ', n_spaces)) + end, + center = function(x, n_spaces, no_trailing) + local n_left = math.floor(0.5 * n_spaces) + return H.pad_functions.right(H.pad_functions.left(x, n_left, no_trailing), n_spaces - n_left, no_trailing) + end, + right = function(x, n_spaces, no_trailing) + if (no_trailing and H.is_whitespace(x)) or H.is_infinite(n_spaces) then return x end + return string.format('%s%s', string.rep(' ', n_spaces), x) + end, + none = function(x, _, _) return x end, +} + +-- Trim functions +H.trim_functions = { + both = function(x) return H.trim_functions.left(H.trim_functions.right(x)) end, + left = function(x) return string.gsub(x, '^%s*', '') end, + right = function(x) return string.gsub(x, '%s*$', '') end, + none = function(x) return x end, +} + +-- Indentation functions +H.indent_functions = { + keep = function(indent_arr) return indent_arr end, + high = function(indent_arr) + local max_indent = indent_arr[1] + for i = 2, #indent_arr do + max_indent = (max_indent:len() < indent_arr[i]:len()) and indent_arr[i] or max_indent + end + return vim.tbl_map(function() return max_indent end, indent_arr) + end, + low = function(indent_arr) + local min_indent = indent_arr[1] + for i = 2, #indent_arr do + min_indent = (indent_arr[i]:len() < min_indent:len()) and indent_arr[i] or min_indent + end + return vim.tbl_map(function() return min_indent end, indent_arr) + end, + remove = function(indent_arr) + return vim.tbl_map(function() return '' end, indent_arr) + end, +} + +-- Helper functionality ======================================================= +-- Settings ------------------------------------------------------------------- +H.setup_config = function(config) + -- General idea: if some table elements are not present in user-supplied + -- `config`, take them from default config + vim.validate({ config = { config, 'table', true } }) + config = vim.tbl_deep_extend('force', H.default_config, config or {}) + + vim.validate({ + mappings = { config.mappings, 'table' }, + modifiers = { config.modifiers, H.is_valid_modifiers }, + steps = { config.steps, H.is_valid_steps }, + options = { config.options, 'table' }, + }) + + vim.validate({ + ['mappings.start'] = { config.mappings.start, 'string' }, + ['mappings.start_with_preview'] = { config.mappings.start_with_preview, 'string' }, + }) + + return config +end + +H.apply_config = function(config) + MiniAlign.config = config + + --stylua: ignore start + H.map('n', config.mappings.start, 'v:lua.MiniAlign.action_normal(v:false)', { expr = true, desc = 'Align' }) + H.map('x', config.mappings.start, 'lua MiniAlign.action_visual(false)', { desc = 'Align' }) + + H.map('n', config.mappings.start_with_preview, 'v:lua.MiniAlign.action_normal(v:true)', { expr = true, desc = 'Align with preview' }) + H.map('x', config.mappings.start_with_preview, 'lua MiniAlign.action_visual(true)', { desc = 'Align with preview' }) + --stylua: ignore end +end + +H.is_disabled = function() return vim.g.minialign_disable == true or vim.b.minialign_disable == true end + +H.get_config = + function(config) return vim.tbl_deep_extend('force', MiniAlign.config, vim.b.minialign_config or {}, config or {}) end + +-- Work with steps and options ------------------------------------------------- +H.is_valid_steps = function(x, x_name) + x_name = x_name or 'config.steps' + + if type(x) ~= 'table' then return false, string.format('`%s` should be table.', x_name) end + + -- Validators + local is_steps_array = function(y) return H.is_array_of(y, H.is_step) end + local steps_array_msg = 'should be array of steps (see `:h MiniAlign.new_step()`).' + + local is_maybe_step = function(y) return y == nil or H.is_step(y) end + local step_msg = 'should be step (see `:h MiniAlign.new_step()`).' + + -- Actual checks + if not is_steps_array(x.pre_split) then return false, H.msg_bad_steps(x_name, 'pre_split', steps_array_msg) end + + if not is_maybe_step(x.split) then return false, H.msg_bad_steps(x_name, 'split', step_msg) end + + if not is_steps_array(x.pre_justify) then return false, H.msg_bad_steps(x_name, 'pre_justify', steps_array_msg) end + + if not is_maybe_step(x.justify) then return false, H.msg_bad_steps(x_name, 'justify', step_msg) end + + if not is_steps_array(x.pre_merge) then return false, H.msg_bad_steps(x_name, 'pre_merge', steps_array_msg) end + + if not is_maybe_step(x.merge) then return false, H.msg_bad_steps(x_name, 'merge', step_msg) end + + return true +end + +H.validate_steps = function(x, x_name) + local is_valid, msg = H.is_valid_steps(x, x_name) + if not is_valid then H.error(msg) end +end + +H.normalize_steps = function(steps, steps_name) + -- Infer all defaults from module config + local res = vim.tbl_deep_extend('force', H.get_config().steps, steps or {}) + + H.validate_steps(res, steps_name) + + -- Possibly fill in default main steps + res.split = res.split or MiniAlign.gen_step.default_split() + res.justify = res.justify or MiniAlign.gen_step.default_justify() + res.merge = res.merge or MiniAlign.gen_step.default_merge() + + -- Deep copy to ensure that table values will not be affected (because if a + -- table value is present only in one input, it is taken as is). + return vim.deepcopy(res) +end + +H.normalize_opts = function(opts) + local res = vim.tbl_deep_extend('force', H.get_config().options, opts or {}) + return vim.deepcopy(res) +end + +H.msg_bad_steps = function(steps_name, key, msg) return string.format('`%s.%s` %s', steps_name, key, msg) end + +H.apply_step = function(step, arr, opts, step_container_name) + local arr_name, predicate, suggest = 'parts', H.is_parts, ' See `:h MiniAlign.as_parts()`.' + if not H.is_parts(arr) then + arr_name = 'strings' + predicate = function(x) return H.is_array_of(x, H.is_string) end + suggest = '' + end + + local res = step.action(arr, opts) + + if not predicate(arr) then + --stylua: ignore + local msg = string.format( + 'Step `%s` of `%s` should preserve structure of `%s`.%s', + step.name, step_container_name, arr_name, suggest + ) + H.error(msg) + end + + return res +end + +-- Work with default actions --------------------------------------------------- +H.default_action_split = function(string_array, opts) + -- Prepare options + local pattern = opts.split_pattern + if not (H.is_string(pattern) or H.is_array_of(pattern, H.is_string)) then + H.error('Option `split_pattern` should be string or array of strings.') + end + if type(pattern) == 'string' then pattern = { pattern } end + + local exclude_patterns = opts.split_exclude_patterns or {} + if not H.is_array_of(exclude_patterns, H.is_string) then + H.error('Option `split_exclude_patterns` should be array of strings.') + end + + local capture_exclude_regions = vim.tbl_map(function(x) + local patt = x + patt = x:sub(1, 1) == '^' and ('^()' .. patt:sub(2)) or ('()' .. patt) + patt = x:sub(-1, -1) == '$' and (patt:sub(1, -2) .. '()$') or (patt .. '()') + return patt + end, exclude_patterns) + + local forbidden_spans = {} + local add_to_forbidden = function(l, r) table.insert(forbidden_spans, { l, r - 1 }) end + local make_forbidden_spans = function(s) + forbidden_spans = {} + for _, capture_pat in ipairs(capture_exclude_regions) do + s:gsub(capture_pat, add_to_forbidden) + end + return forbidden_spans + end + + -- Make splits excluding matches inside forbidden regions + local res = vim.tbl_map( + function(s) return H.default_action_split_string(s, pattern, make_forbidden_spans) end, + string_array + ) + return MiniAlign.as_parts(res) +end + +H.default_action_split_string = function(s, pattern_arr, make_forbidden_spans) + -- Construct forbidden spans for string + local forbidden_spans = make_forbidden_spans(s) + + -- Split by recycled `pattern_arr` + local res = {} + local n_total, n_latest_add, n_find = s:len(), 0, 0 + local n_pair = 1 + + while true do + local cur_split = H.slice_mod(pattern_arr, n_pair) + local sep_left, sep_right = H.string_find(s, cur_split, n_find) + + if sep_left == nil then + -- Avoid adding empty string to non-empty input because it does nothing + -- but confuses "don't add trailspace" logic + local rest = s:sub(n_latest_add, n_total) + if not (rest == '' and #res > 0) then table.insert(res, rest) end + break + end + + local is_good = #forbidden_spans == 0 + or not H.is_any_point_inside_any_span({ sep_left, sep_right }, forbidden_spans) + if is_good then + table.insert(res, s:sub(n_latest_add, sep_left - 1)) + table.insert(res, s:sub(sep_left, sep_right)) + n_latest_add = sep_right + 1 + n_pair = n_pair + 1 + end + + if (sep_right + 1) <= n_find then + H.error(string.format('Pattern %s can not advance search.', vim.inspect(cur_split))) + end + n_find = sep_right + 1 + end + + return res +end + +H.default_action_justify = function(parts, opts) + -- Prepare options + local side = opts.justify_side + if not (H.is_justify_side(side) or H.is_array_of(side, H.is_justify_side)) then + H.error([[Option `justify_side` should be one of 'left', 'center', 'right', 'none', or array of those.]]) + end + if type(side) == 'string' then side = { side } end + + local offsets = opts.justify_offsets or H.tbl_repeat(0, #parts) + + -- Recycle `justify` array and precompute padding functions + local dims = parts.get_dims() + local pad_funs, side_arr = {}, {} + for j = 1, dims.col do + local s = H.slice_mod(side, j) + side_arr[j] = s + pad_funs[j] = H.pad_functions[s] + end + + -- Compute cell width and maximum column widths (adjusting for offsets) + local width_col = {} + for j = 1, dims.col do + width_col[j] = 0 + end + + local width = {} + for i, row in ipairs(parts) do + width[i] = {} + for j, s in ipairs(row) do + local w = vim.fn.strdisplaywidth(s) + width[i][j] = w + + -- Compute offset + local off = j == 1 and offsets[i] or 0 + + -- Don't use last column in row to compute column width in case of left + -- justification (it won't be padded so shouldn't contribute to column) + if not (j == #row and side_arr[j] == 'left') then width_col[j] = math.max(off + w, width_col[j]) end + end + end + + -- Pad cells to have same width across columns (adjusting for offsets) + for i, row in ipairs(parts) do + for j, s in ipairs(row) do + local off = j == 1 and offsets[i] or 0 + local n_space = width_col[j] - width[i][j] - off + -- Don't add trailing whitespace for last column + parts[i][j] = pad_funs[j](s, n_space, j == #row) + end + end +end + +H.default_action_merge = function(parts, opts) + -- Prepare options + local delimiter = opts.merge_delimiter + if not (H.is_string(delimiter) or H.is_array_of(delimiter, H.is_string)) then + H.error('Option `merge_delimiter` should be string or array of strings.') + end + if type(delimiter) == 'string' then delimiter = { delimiter } end + + -- Precompute combination strings (recycle `merge` array) + local dims = parts.get_dims() + local delimiter_arr = {} + for j = 1, dims.col - 1 do + delimiter_arr[j] = H.slice_mod(delimiter, j) + end + + -- Concat non-empty cells (empty cells at this point add only extra merge) + return vim.tbl_map(function(row) + local row_no_empty = vim.tbl_filter(function(s) return s ~= '' end, row) + return H.concat_array(row_no_empty, delimiter_arr) + end, parts) +end + +-- Work with modifiers -------------------------------------------------------- +H.is_valid_modifiers = function(x, x_name) + x_name = x_name or 'config.modifiers' + + if type(x) ~= 'table' then return false, string.format('`%s` should be table.', x_name) end + for k, v in pairs(x) do + if not vim.is_callable(v) then + return false, string.format('`%s[%s]` should be callable.', x_name, vim.inspect(k)) + end + end + + return true +end + +H.make_filter_action = function(expr) + if expr == nil then return nil end + if expr == '' then expr = 'true' end + + local is_loaded, f = pcall(function() return assert(loadstring('return ' .. expr)) end) + if not (is_loaded and vim.is_callable(f)) then H.error(vim.inspect(expr) .. ' is not a valid filter expression.') end + + local predicate = function(data) + local context = setmetatable(data, { __index = _G }) + debug.setfenv(f, context) + return f() + end + + return function(parts, _) + local mask = {} + local data = { ROW = #parts } + for i, row in ipairs(parts) do + data.row = i + mask[i] = {} + for j, s in ipairs(row) do + data.col, data.COL = j, #row + data.s = s + + -- Current and total number of pairs + data.n = math.ceil(0.5 * j) + data.N = math.ceil(0.5 * #row) + + mask[i][j] = predicate(data) + end + end + + parts.group(mask) + end +end + +-- Work with regions ---------------------------------------------------------- +---@return boolean Whether some lines were actually set. +---@private +H.process_current_region = function(lines_were_set, mode, opts, steps) + -- Cache current options and steps for dot-repeat + H.cache.opts, H.cache.steps = opts, steps + + -- Undo previously set lines + if lines_were_set then H.undo() end + + -- Get current region. NOTE: use cached value to ensure that the same region + -- is processed during preview. Otherwise there might be problems with + -- getting "current" regions in Normal mode as necessary marks (`[` and `]`) + -- can be not valid. + local region = H.cache.region or H.get_current_region() + H.cache.region = region + + -- Enrich options + opts.region = region + opts.mode = mode + opts.justify_offsets = H.tbl_repeat(0, region.to.line - region.from.line + 1) + if mode == 'char' then + -- Compute offset of first line for charwise selection + local prefix = vim.fn.getline(region.from.line):sub(1, region.from.col - 1) + opts.justify_offsets[1] = vim.fn.strdisplaywidth(prefix) + end + + -- Actually process region + local strings = H.region_get_text(region, mode) + local strings_aligned = MiniAlign.align_strings(strings, opts, steps) + H.region_set_text(region, mode, strings_aligned) + + -- Make sure that latest changes are shown + vim.cmd('redraw') + + -- Confirm that lines were actually set + return true +end + +H.get_current_region = function() + local from_expr, to_expr = "'[", "']" + if H.is_visual_mode() then + from_expr, to_expr = '.', 'v' + end + + -- Add offset (*_pos[4]) to allow position go past end of line + local from_pos = vim.fn.getpos(from_expr) + local from = { line = from_pos[2], col = from_pos[3] + from_pos[4] } + local to_pos = vim.fn.getpos(to_expr) + local to = { line = to_pos[2], col = to_pos[3] + to_pos[4] } + + -- Ensure correct order + if to.line < from.line or (to.line == from.line and to.col < from.col) then + from, to = to, from + end + + return { from = from, to = to } +end + +H.region_get_text = function(region, mode) + local from, to = region.from, region.to + + if mode == 'char' then + local to_col_offset = vim.o.selection == 'exclusive' and 1 or 0 + return H.get_text(from.line - 1, from.col - 1, to.line - 1, to.col - to_col_offset) + end + + if mode == 'line' then return H.get_lines(from.line - 1, to.line) end + + if mode == 'block' then + -- Use virtual columns to respect multibyte characters + local left_virtcol, right_virtcol = H.region_virtcols(region) + local n_cols = right_virtcol - left_virtcol + 1 + + return vim.tbl_map( + -- `strcharpart()` returns empty string for out of bounds span, so no + -- need for extra columns check + function(l) return vim.fn.strcharpart(l, left_virtcol - 1, n_cols) end, + H.get_lines(from.line - 1, to.line) + ) + end +end + +H.region_set_text = function(region, mode, text) + local from, to = region.from, region.to + + if mode == 'char' then + -- Ensure not going past last column (can happen with `$` in Visual mode) + local to_line_n_cols = vim.fn.col({ to.line, '$' }) - 1 + local to_col = math.min(to.col, to_line_n_cols) + local to_col_offset = vim.o.selection == 'exclusive' and 1 or 0 + H.set_text(from.line - 1, from.col - 1, to.line - 1, to_col - to_col_offset, text) + end + + if mode == 'line' then H.set_lines(from.line - 1, to.line, text) end + + if mode == 'block' then + if #text ~= (to.line - from.line + 1) then + H.error('Number of replacement lines should fit the region in blockwise mode') + end + + -- Use virtual columns to respect multibyte characters + local left_virtcol, right_virtcol = H.region_virtcols(region) + local lines = H.get_lines(from.line - 1, to.line) + for i, l in ipairs(lines) do + -- Use zero-based indexes + local line_num = from.line + i - 2 + + local n_virtcols = vim.fn.virtcol({ line_num + 1, '$' }) - 1 + -- Don't set text if all region is past end of line + if left_virtcol <= n_virtcols then + -- Make sure to not go past the line end + local line_left_col, line_right_col = left_virtcol, math.min(right_virtcol, n_virtcols) + + -- Convert back to byte columns (columns are end-exclusive) + local start_col, end_col = vim.fn.byteidx(l, line_left_col - 1), vim.fn.byteidx(l, line_right_col) + start_col, end_col = math.max(start_col, 0), math.max(end_col, 0) + + -- vim.api.nvim_buf_set_text(0, line_num, start_col, line_num, end_col, { text[i] }) + H.set_text(line_num, start_col, line_num, end_col, { text[i] }) + end + end + end +end + +H.region_virtcols = function(region) + -- Account for multibyte characters and position past the line end + local from_virtcol = H.pos_to_virtcol(region.from) + local to_virtcol = H.pos_to_virtcol(region.to) + + local left_virtcol, right_virtcol = math.min(from_virtcol, to_virtcol), math.max(from_virtcol, to_virtcol) + right_virtcol = right_virtcol - (vim.o.selection == 'exclusive' and 1 or 0) + + return left_virtcol, right_virtcol +end + +H.pos_to_virtcol = function(pos) + -- Account for position past line end + local eol_col = vim.fn.col({ pos.line, '$' }) + if eol_col < pos.col then return vim.fn.virtcol({ pos.line, '$' }) + pos.col - eol_col end + + return vim.fn.virtcol({ pos.line, pos.col }) +end + +-- Work with user interaction -------------------------------------------------- +H.user_modifier = function(with_preview, msg_chunks) + -- Get from user single character modifier + local needs_help_msg = true + local delay = (H.cache.msg_shown or with_preview) and 0 or 1000 + vim.defer_fn(function() + if not needs_help_msg then return end + + table.insert(msg_chunks, { ' Enter modifier' }) + H.echo(msg_chunks) + H.cache.msg_shown = true + end, delay) + local ok, char = pcall(vim.fn.getchar) + needs_help_msg = false + + -- Terminate if couldn't get input (like with ) or it is `` + if not ok or char == 27 then return nil end + + if type(char) == 'number' then char = vim.fn.nr2char(char) end + return char +end + +H.user_input = function(prompt, text) + -- Register temporary keystroke listener to distinguish between cancel with + -- `` and immediate ``. + local on_key = vim.on_key or vim.register_keystroke_callback + local was_cancelled = false + on_key(function(key) + if key == '\27' then was_cancelled = true end + end, H.ns_id.input) + + -- Ask for input + local opts = { prompt = '(mini.align) ' .. prompt .. ': ', default = text or '' } + vim.cmd('echohl Question') + -- Use `pcall` to allow `` to cancel user input + local ok, res = pcall(vim.fn.input, opts) + vim.cmd('echohl None | redraw') + + -- Stop key listening + on_key(nil, H.ns_id.input) + + if not ok or was_cancelled then return end + return res +end + +H.make_status_msg_chunks = function(opts, steps) + local single_to_string = function(pre_steps, opts_value) + local steps_str = '' + if #pre_steps > 0 then + local pre_names = vim.tbl_map(function(x) return x.name end, pre_steps) + steps_str = string.format('(%s) ', table.concat(pre_names, ', ')) + end + return steps_str .. vim.inspect(opts_value) + end + + return { + { 'Split: ', 'ModeMsg' }, + { single_to_string(steps.pre_split, opts.split_pattern) }, + { ' | ', 'Question' }, + { 'Justify: ', 'ModeMsg' }, + { single_to_string(steps.pre_justify, opts.justify_side) }, + { ' | ', 'Question' }, + { 'Merge: ', 'ModeMsg' }, + { single_to_string(steps.pre_merge, opts.merge_delimiter) }, + { ' |', 'Question' }, + } +end + +-- Predicates ----------------------------------------------------------------- +H.is_array_of = function(x, predicate) + if not vim.tbl_islist(x) then return false end + for _, v in ipairs(x) do + if not predicate(v) then return false end + end + return true +end + +H.is_step = function(x) return type(x) == 'table' and type(x.name) == 'string' and vim.is_callable(x.action) end + +H.is_string = function(v) return type(v) == 'string' end + +H.is_justify_side = function(x) return x == 'left' or x == 'center' or x == 'right' or x == 'none' end + +H.is_nonempty_region = function(x) + if type(x) ~= 'table' then return false end + local from_is_valid = type(x.from) == 'table' and type(x.from.line) == 'number' and type(x.from.col) == 'number' + local to_is_valid = type(x.to) == 'table' and type(x.to.line) == 'number' and type(x.to.col) == 'number' + return from_is_valid and to_is_valid +end + +H.is_parts = function(x) return H.can_be_parts(x) and (getmetatable(x) or {}).class == 'parts' end + +H.can_be_parts = function(x) + if type(x) ~= 'table' then return false, 'should be table' end + for i = 1, #x do + if not H.is_array_of(x[i], H.is_string) then return false, 'values should be an array of strings' end + end + return true +end + +H.is_infinite = function(x) return x == math.huge or x == -math.huge end + +H.is_visual_mode = function() return vim.tbl_contains({ 'v', 'V', '\22' }, vim.fn.mode(1)) end + +H.is_whitespace = function(x) return type(x) == 'string' and x:find('^%s*$') ~= nil end + +-- Work with get/set text ----------------------------------------------------- +--- Get text from current buffer +--- +--- Needed for compatibility with Neovim<=0.6 which doesn't have +--- `vim.api.nvim_buf_get_text()`. +---@private +H.get_text = function(start_row, start_col, end_row, end_col) + -- TODO: Remove this whole function after Neovim<=0.6 support is dropped + if vim.api.nvim_buf_get_text ~= nil then + return vim.api.nvim_buf_get_text(0, start_row, start_col, end_row, end_col, {}) + end + local text = H.get_lines(start_row, end_row + 1) + if #text == 0 then return text end + text[#text] = text[#text]:sub(1, end_col) + text[1] = text[1]:sub(start_col + 1) + return text +end + +--- Get lines from current buffer +--- +--- Added for completeness. +---@private +H.get_lines = function(start_row, end_row) return vim.api.nvim_buf_get_lines(0, start_row, end_row, true) end + +--- Set text in current buffer without affecting marks +---@private +H.set_text = function(start_row, start_col, end_row, end_col, replacement) + --stylua: ignore + local cmd = string.format( + 'lockmarks lua vim.api.nvim_buf_set_text(0, %d, %d, %d, %d, %s)', + start_row, start_col, end_row, end_col, vim.inspect(replacement) + ) + vim.cmd(cmd) +end + +--- Set lines in current buffer without affecting marks +---@private +H.set_lines = function(start_row, end_row, replacement) + --stylua: ignore + local cmd = string.format( + 'lockmarks lua vim.api.nvim_buf_set_lines(0, %d, %d, true, %s)', + start_row, end_row, vim.inspect(replacement) + ) + vim.cmd(cmd) +end + +-- Utilities ------------------------------------------------------------------ +H.echo = function(msg, add_to_history) + -- Construct message chunks + msg = type(msg) == 'string' and { { msg } } or msg + table.insert(msg, 1, { '(mini.align) ', 'WarningMsg' }) + + -- Avoid hit-enter-prompt + local max_width = vim.o.columns * math.max(vim.o.cmdheight - 1, 0) + vim.v.echospace + local chunks, tot_width = {}, 0 + for _, ch in ipairs(msg) do + local new_ch = { vim.fn.strcharpart(ch[1], 0, max_width - tot_width), ch[2] } + table.insert(chunks, new_ch) + tot_width = tot_width + vim.fn.strdisplaywidth(new_ch[1]) + if tot_width >= max_width then break end + end + + -- Echo. Force redraw to ensure that it is effective (`:h echo-redraw`) + vim.cmd([[echo '' | redraw]]) + vim.api.nvim_echo(chunks, add_to_history, {}) +end + +H.unecho = function() + if H.cache.msg_shown then vim.cmd([[echo '' | redraw]]) end +end + +H.error = function(msg) error(string.format('(mini.align) %s', msg), 0) end + +H.map = function(mode, key, rhs, opts) + if key == '' then return end + + opts = vim.tbl_deep_extend('force', { noremap = true, silent = true }, opts or {}) + + -- Use mapping description only in Neovim>=0.7 + if vim.fn.has('nvim-0.7') == 0 then opts.desc = nil end + + vim.api.nvim_set_keymap(mode, key, rhs, opts) +end + +H.slice_mod = function(x, i) return x[((i - 1) % #x) + 1] end + +H.tbl_repeat = function(val, n) + local res = {} + for i = 1, n do + res[i] = val + end + return res +end + +H.group_by_mask = function(arr, mask, direction) + local res, cur_group = {}, {} + + -- Construct actors based on direction + local from, to, by = 1, #arr, 1 + local insert = function(t, v) table.insert(t, v) end + if direction == 'right' then + from, to, by = to, from, -1 + insert = function(t, v) table.insert(t, 1, v) end + end + + -- Group + for i = from, to, by do + insert(cur_group, arr[i]) + if mask[i] or i == to then + insert(res, cur_group) + cur_group = {} + end + end + + return res +end + +H.concat_array = function(target_arr, concat_arr) + local ext_arr = {} + for i = 1, #target_arr - 1 do + table.insert(ext_arr, target_arr[i]) + table.insert(ext_arr, concat_arr[i]) + end + table.insert(ext_arr, target_arr[#target_arr]) + return table.concat(ext_arr, '') +end + +H.string_find = function(s, pattern, init) + init = init or 1 + + -- Match only start of full string if pattern says so. + -- This is needed because `string.find()` doesn't do this. + -- Example: `string.find('(aaa)', '^.*$', 4)` returns `4, 5` + if pattern:sub(1, 1) == '^' and init > 1 then return nil end + + -- Treat `''` as if nothing is found (treats it as "reset split"). If not + -- altered, results in infinite loop. + if pattern == '' then return nil end + + return string.find(s, pattern, init) +end + +H.is_any_point_inside_any_span = function(points, spans) + for _, point in ipairs(points) do + for _, span in ipairs(spans) do + if span[1] <= point and point <= span[2] then return true end + end + end + return false +end + +H.undo = function() + if H.is_visual_mode() then + -- Can't use `u` in Visual mode because it makes all selection lowercase + local cur_mode = vim.fn.mode(1) + vim.cmd('silent! normal! \27') + + -- Undo + vim.cmd('silent! lockmarks undo') + + -- Manually restore selection. There are issues with using restoring marks + -- via `gv` (couldn't figure out how to reliably preserve visual mode). + -- As this is called only if lines were set, region is cached. + local region = H.cache.region + vim.api.nvim_win_set_cursor(0, { region.from.line, region.from.col - 1 }) + vim.cmd('silent! normal!' .. cur_mode) + vim.api.nvim_win_set_cursor(0, { region.to.line, region.to.col - 1 }) + else + vim.cmd('silent! lockmarks normal! u') + end +end + +return MiniAlign diff --git a/dotfiles/pack/plugins/start/mini.nvim/lua/mini/base16.lua b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/base16.lua new file mode 100755 index 0000000..1bc2d55 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/base16.lua @@ -0,0 +1,1403 @@ +-- MIT License Copyright (c) 2021 Evgeni Chasnovski + +-- Documentation ============================================================== +--- 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". +---@tag mini.base16 +---@tag MiniBase16 + +--- # 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)` +---@tag mini-color-schemes + +-- Module definition ========================================================== +local MiniBase16 = {} +local H = {} + +--- 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. +--- +---@param 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.setup = function(config) + -- Export module + _G.MiniBase16 = MiniBase16 + + -- Setup config + config = H.setup_config(config) + + -- Apply config + H.apply_config(config) +end + +--- Module config +--- +--- Default values: +---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section) +---@text # 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.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 }, +} +--minidoc_afterlines_end + +-- Module functionality ======================================================= +--- 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). +--- +---@param background string Background HEX color (formatted as `#RRGGBB`). +---@param foreground string Foreground HEX color (formatted as `#RRGGBB`). +---@param 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.mini_palette = function(background, foreground, accent_chroma) + H.validate_hex(background, 'background') + H.validate_hex(foreground, 'foreground') + if accent_chroma and not (type(accent_chroma) == 'number' and accent_chroma >= 0) then + error('(mini.base16) `accent_chroma` should be a positive number or `nil`.') + end + local bg, fg = H.hex2lch(background), H.hex2lch(foreground) + accent_chroma = accent_chroma or fg.c + + local palette = {} + + -- Target lightness values + -- Justification for skewness towards foreground in focus is mainly because + -- it will be paired with foreground lightness and used for text. + local focus_l = 0.4 * bg.l + 0.6 * fg.l + local edge_l = fg.l > 50 and 99 or 1 + + -- Background colors + local bg_step = (focus_l - bg.l) / 3 + palette[1] = { l = bg.l + 0 * bg_step, c = bg.c, h = bg.h } + palette[2] = { l = bg.l + 1 * bg_step, c = bg.c, h = bg.h } + palette[3] = { l = bg.l + 2 * bg_step, c = bg.c, h = bg.h } + palette[4] = { l = bg.l + 3 * bg_step, c = bg.c, h = bg.h } + + -- Foreground colors Possible negative value of `palette[5].l` will be + -- handled in future conversion to hex. + local fg_step = (edge_l - fg.l) / 2 + palette[5] = { l = fg.l - 1 * fg_step, c = fg.c, h = fg.h } + palette[6] = { l = fg.l + 0 * fg_step, c = fg.c, h = fg.h } + palette[7] = { l = fg.l + 1 * fg_step, c = fg.c, h = fg.h } + palette[8] = { l = fg.l + 2 * fg_step, c = fg.c, h = fg.h } + + -- Accent colors + + -- Only try to avoid color if it has positive chroma, because with zero + -- chroma hue is meaningless (as in polar coordinates) + local present_hues = {} + if bg.c > 0 then table.insert(present_hues, bg.h) end + if fg.c > 0 then table.insert(present_hues, fg.h) end + local hues = H.make_different_hues(present_hues, 4) + + -- stylua: ignore start + palette[9] = { l = fg.l, c = accent_chroma, h = hues[1] } + palette[10] = { l = focus_l, c = accent_chroma, h = hues[1] } + palette[11] = { l = focus_l, c = accent_chroma, h = hues[2] } + palette[12] = { l = fg.l, c = accent_chroma, h = hues[2] } + palette[13] = { l = focus_l, c = accent_chroma, h = hues[4] } + palette[14] = { l = fg.l, c = accent_chroma, h = hues[3] } + palette[15] = { l = fg.l, c = accent_chroma, h = hues[4] } + palette[16] = { l = focus_l, c = accent_chroma, h = hues[3] } + -- stylua: ignore end + + -- Convert to base16 palette + local base16_palette = {} + for i, lch in ipairs(palette) do + local name = H.base16_names[i] + -- It is ensured in `lch2hex` that only valid HEX values are produced + base16_palette[name] = H.lch2hex(lch) + end + + return base16_palette +end + +--- Converts palette with RGB colors to terminal colors +--- +--- Useful for caching `use_cterm` variable to increase speed. +--- +---@param palette table Table with base16 palette (same as in +--- `MiniBase16.config.palette`). +--- +---@return table Table with base16 palette using |highlight-cterm|. +MiniBase16.rgb_palette_to_cterm_palette = function(palette) + H.validate_base16_palette(palette, 'palette') + + -- Create cterm palette only when it is needed to decrease load time + H.ensure_cterm_palette() + + return vim.tbl_map(function(hex) return H.nearest_rgb_id(H.hex2rgb(hex), H.cterm_palette) end, palette) +end + +-- Helper data ================================================================ +-- Module default config +H.default_config = MiniBase16.config + +-- Helper functionality ======================================================= +-- Settings ------------------------------------------------------------------- +H.setup_config = function(config) + -- General idea: if some table elements are not present in user-supplied + -- `config`, take them from default config + vim.validate({ config = { config, 'table', true } }) + config = vim.tbl_deep_extend('force', H.default_config, config or {}) + + -- Validate settings + H.validate_base16_palette(config.palette, 'config.palette') + H.validate_use_cterm(config.use_cterm, 'config.use_cterm') + vim.validate({ plugins = { config.plugins, 'table' } }) + + return config +end + +H.apply_config = function(config) + MiniBase16.config = config + + H.apply_palette(config.palette, config.use_cterm) +end + +-- Validators ----------------------------------------------------------------- +H.base16_names = { + 'base00', + 'base01', + 'base02', + 'base03', + 'base04', + 'base05', + 'base06', + 'base07', + 'base08', + 'base09', + 'base0A', + 'base0B', + 'base0C', + 'base0D', + 'base0E', + 'base0F', +} + +H.validate_base16_palette = function(x, x_name) + if type(x) ~= 'table' then error(string.format('(mini.base16) `%s` is not a table.', x_name)) end + + for _, color_name in pairs(H.base16_names) do + local c = x[color_name] + if c == nil then + local msg = string.format('(mini.base16) `%s` does not have value %s.', x_name, color_name) + error(msg) + end + H.validate_hex(c, string.format('%s.%s', x_name, color_name)) + end + + return true +end + +H.validate_use_cterm = function(x, x_name) + if not x or type(x) == 'boolean' then return true end + + if type(x) ~= 'table' then + local msg = string.format('(mini.base16) `%s` should be boolean or table with cterm colors.', x_name) + error(msg) + end + + for _, color_name in pairs(H.base16_names) do + local c = x[color_name] + if c == nil then + local msg = string.format('(mini.base16) `%s` does not have value %s.', x_name, color_name) + error(msg) + end + if not (type(c) == 'number' and 0 <= c and c <= 255) then + local msg = string.format('(mini.base16) `%s.%s` is not a cterm color.', x_name, color_name) + error(msg) + end + end + + return true +end + +H.validate_hex = function(x, x_name) + local is_hex = type(x) == 'string' and x:len() == 7 and x:sub(1, 1) == '#' and (tonumber(x:sub(2), 16) ~= nil) + + if not is_hex then + local msg = string.format('(mini.base16) `%s` is not a HEX color (string "#RRGGBB").', x_name) + error(msg) + end + + return true +end + +-- Highlighting --------------------------------------------------------------- +H.apply_palette = function(palette, use_cterm) + -- Prepare highlighting application. Notes: + -- - Clear current highlight only if other theme was loaded previously. + -- - No need to `syntax reset` because *all* syntax groups are defined later. + if vim.g.colors_name then vim.cmd('highlight clear') end + -- As this doesn't create colorscheme, don't store any name. Not doing it + -- might cause some issues with `syntax on`. + vim.g.colors_name = nil + + local p, hi + if use_cterm then + p, hi = H.make_compound_palette(palette, use_cterm), H.highlight_both + else + p, hi = palette, H.highlight_gui + end + + -- NOTE: recommendations for adding new highlight groups: + -- - Put all related groups (like for new plugin) in single paragraph. + -- - Sort within group alphabetically (by hl-group name) ignoring case. + -- - Link all repeated groups within paragraph (lowers execution time). + -- - Align by commas. + + -- stylua: ignore start + -- Builtin highlighting groups. Some groups which are missing in 'base16-vim' + -- are added based on groups to which they are linked. + hi('ColorColumn', {fg=nil, bg=p.base01, attr=nil, sp=nil}) + hi('Conceal', {fg=p.base0D, bg=p.base00, attr=nil, sp=nil}) + hi('Cursor', {fg=p.base00, bg=p.base05, attr=nil, sp=nil}) + hi('CursorColumn', {fg=nil, bg=p.base01, attr=nil, sp=nil}) + hi('CursorIM', {fg=p.base00, bg=p.base05, attr=nil, sp=nil}) + hi('CursorLine', {fg=nil, bg=p.base01, attr=nil, sp=nil}) + hi('CursorLineNr', {fg=p.base04, bg=p.base01, attr=nil, sp=nil}) + hi('DiffAdd', {fg=p.base0B, bg=p.base01, attr=nil, sp=nil}) + -- Differs from base16-vim, but according to general style guide + hi('DiffChange', {fg=p.base0E, bg=p.base01, attr=nil, sp=nil}) + hi('DiffDelete', {fg=p.base08, bg=p.base01, attr=nil, sp=nil}) + hi('DiffText', {fg=p.base0D, bg=p.base01, attr=nil, sp=nil}) + hi('Directory', {fg=p.base0D, bg=nil, attr=nil, sp=nil}) + hi('EndOfBuffer', {fg=p.base03, bg=nil, attr=nil, sp=nil}) + hi('ErrorMsg', {fg=p.base08, bg=p.base00, attr=nil, sp=nil}) + hi('FoldColumn', {fg=p.base0C, bg=p.base01, attr=nil, sp=nil}) + hi('Folded', {fg=p.base03, bg=p.base01, attr=nil, sp=nil}) + hi('IncSearch', {fg=p.base01, bg=p.base09, attr=nil, sp=nil}) + hi('lCursor', {fg=p.base00, bg=p.base05, attr=nil, sp=nil}) + hi('LineNr', {fg=p.base03, bg=p.base01, attr=nil, sp=nil}) + -- Slight difference from base16, where `bg=base03` is used. This makes + -- it possible to comfortably see this highlighting in comments. + hi('MatchParen', {fg=nil, bg=p.base02, attr=nil, sp=nil}) + hi('ModeMsg', {fg=p.base0B, bg=nil, attr=nil, sp=nil}) + hi('MoreMsg', {fg=p.base0B, bg=nil, attr=nil, sp=nil}) + hi('MsgArea', {fg=p.base05, bg=p.base00, attr=nil, sp=nil}) + hi('MsgSeparator', {fg=p.base04, bg=p.base02, attr=nil, sp=nil}) + hi('NonText', {fg=p.base03, bg=nil, attr=nil, sp=nil}) + hi('Normal', {fg=p.base05, bg=p.base00, attr=nil, sp=nil}) + hi('NormalFloat', {fg=p.base05, bg=p.base01, attr=nil, sp=nil}) + hi('NormalNC', {fg=p.base05, bg=p.base00, attr=nil, sp=nil}) + hi('PMenu', {fg=p.base05, bg=p.base01, attr=nil, sp=nil}) + hi('PMenuSbar', {fg=nil, bg=p.base02, attr=nil, sp=nil}) + hi('PMenuSel', {fg=p.base01, bg=p.base05, attr=nil, sp=nil}) + hi('PMenuThumb', {fg=nil, bg=p.base07, attr=nil, sp=nil}) + hi('Question', {fg=p.base0D, bg=nil, attr=nil, sp=nil}) + hi('QuickFixLine', {fg=nil, bg=p.base01, attr=nil, sp=nil}) + hi('Search', {fg=p.base01, bg=p.base0A, attr=nil, sp=nil}) + hi('SignColumn', {fg=p.base03, bg=p.base01, attr=nil, sp=nil}) + hi('SpecialKey', {fg=p.base03, bg=nil, attr=nil, sp=nil}) + hi('SpellBad', {fg=nil, bg=nil, attr='undercurl', sp=p.base08}) + hi('SpellCap', {fg=nil, bg=nil, attr='undercurl', sp=p.base0D}) + hi('SpellLocal', {fg=nil, bg=nil, attr='undercurl', sp=p.base0C}) + hi('SpellRare', {fg=nil, bg=nil, attr='undercurl', sp=p.base0E}) + hi('StatusLine', {fg=p.base04, bg=p.base02, attr=nil, sp=nil}) + hi('StatusLineNC', {fg=p.base03, bg=p.base01, attr=nil, sp=nil}) + hi('Substitute', {fg=p.base01, bg=p.base0A, attr=nil, sp=nil}) + hi('TabLine', {fg=p.base03, bg=p.base01, attr=nil, sp=nil}) + hi('TabLineFill', {fg=p.base03, bg=p.base01, attr=nil, sp=nil}) + hi('TabLineSel', {fg=p.base0B, bg=p.base01, attr=nil, sp=nil}) + hi('TermCursor', {fg=nil, bg=nil, attr='reverse', sp=nil}) + hi('TermCursorNC', {fg=nil, bg=nil, attr='reverse', sp=nil}) + hi('Title', {fg=p.base0D, bg=nil, attr=nil, sp=nil}) + hi('VertSplit', {fg=p.base02, bg=p.base02, attr=nil, sp=nil}) + hi('Visual', {fg=nil, bg=p.base02, attr=nil, sp=nil}) + hi('VisualNOS', {fg=p.base08, bg=nil, attr=nil, sp=nil}) + hi('WarningMsg', {fg=p.base08, bg=nil, attr=nil, sp=nil}) + hi('Whitespace', {fg=p.base03, bg=nil, attr=nil, sp=nil}) + hi('WildMenu', {fg=p.base08, bg=p.base0A, attr=nil, sp=nil}) + + if vim.fn.hlexists('WinBar') == 1 then + hi('WinBar', {fg=p.base04, bg=p.base02, attr=nil, sp=nil}) + hi('WinBarNC', {fg=p.base03, bg=p.base01, attr=nil, sp=nil}) + end + + -- Standard syntax (affects treesitter) + hi('Boolean', {fg=p.base09, bg=nil, attr=nil, sp=nil}) + hi('Character', {fg=p.base08, bg=nil, attr=nil, sp=nil}) + hi('Comment', {fg=p.base03, bg=nil, attr=nil, sp=nil}) + hi('Conditional', {fg=p.base0E, bg=nil, attr=nil, sp=nil}) + hi('Constant', {fg=p.base09, bg=nil, attr=nil, sp=nil}) + hi('Debug', {fg=p.base08, bg=nil, attr=nil, sp=nil}) + hi('Define', {fg=p.base0E, bg=nil, attr=nil, sp=nil}) + hi('Delimiter', {fg=p.base0F, bg=nil, attr=nil, sp=nil}) + hi('Error', {fg=p.base00, bg=p.base08, attr=nil, sp=nil}) + hi('Exception', {fg=p.base08, bg=nil, attr=nil, sp=nil}) + hi('Float', {fg=p.base09, bg=nil, attr=nil, sp=nil}) + hi('Function', {fg=p.base0D, bg=nil, attr=nil, sp=nil}) + hi('Identifier', {fg=p.base08, bg=nil, attr=nil, sp=nil}) + hi('Ignore', {fg=p.base0C, bg=nil, attr=nil, sp=nil}) + hi('Include', {fg=p.base0D, bg=nil, attr=nil, sp=nil}) + hi('Keyword', {fg=p.base0E, bg=nil, attr=nil, sp=nil}) + hi('Label', {fg=p.base0A, bg=nil, attr=nil, sp=nil}) + hi('Macro', {fg=p.base08, bg=nil, attr=nil, sp=nil}) + hi('Number', {fg=p.base09, bg=nil, attr=nil, sp=nil}) + hi('Operator', {fg=p.base05, bg=nil, attr=nil, sp=nil}) + hi('PreCondit', {fg=p.base0A, bg=nil, attr=nil, sp=nil}) + hi('PreProc', {fg=p.base0A, bg=nil, attr=nil, sp=nil}) + hi('Repeat', {fg=p.base0A, bg=nil, attr=nil, sp=nil}) + hi('Special', {fg=p.base0C, bg=nil, attr=nil, sp=nil}) + hi('SpecialChar', {fg=p.base0F, bg=nil, attr=nil, sp=nil}) + hi('SpecialComment', {fg=p.base0C, bg=nil, attr=nil, sp=nil}) + hi('Statement', {fg=p.base08, bg=nil, attr=nil, sp=nil}) + hi('StorageClass', {fg=p.base0A, bg=nil, attr=nil, sp=nil}) + hi('String', {fg=p.base0B, bg=nil, attr=nil, sp=nil}) + hi('Structure', {fg=p.base0E, bg=nil, attr=nil, sp=nil}) + hi('Tag', {fg=p.base0A, bg=nil, attr=nil, sp=nil}) + hi('Todo', {fg=p.base0A, bg=p.base01, attr=nil, sp=nil}) + hi('Type', {fg=p.base0A, bg=nil, attr=nil, sp=nil}) + hi('Typedef', {fg=p.base0A, bg=nil, attr=nil, sp=nil}) + + -- Other from 'base16-vim' + hi('Bold', {fg=nil, bg=nil, attr='bold', sp=nil}) + hi('Italic', {fg=nil, bg=nil, attr='italic', sp=nil}) + hi('TooLong', {fg=p.base08, bg=nil, attr=nil, sp=nil}) + hi('Underlined', {fg=nil, bg=nil, attr='underline', sp=nil}) + + -- Git diff + hi('DiffAdded', {fg=p.base0B, bg=p.base00, attr=nil, sp=nil}) + hi('DiffFile', {fg=p.base08, bg=p.base00, attr=nil, sp=nil}) + hi('DiffLine', {fg=p.base0D, bg=p.base00, attr=nil, sp=nil}) + hi('DiffNewFile', {link='DiffAdded'}) + hi('DiffRemoved', {link='DiffFile'}) + + -- Git commit + hi('gitcommitBranch', {fg=p.base09, bg=nil, attr='bold', sp=nil}) + hi('gitcommitComment', {link='Comment'}) + hi('gitcommitDiscarded', {link='Comment'}) + hi('gitcommitDiscardedFile', {fg=p.base08, bg=nil, attr='bold', sp=nil}) + hi('gitcommitDiscardedType', {fg=p.base0D, bg=nil, attr=nil, sp=nil}) + hi('gitcommitHeader', {fg=p.base0E, bg=nil, attr=nil, sp=nil}) + hi('gitcommitOverflow', {fg=p.base08, bg=nil, attr=nil, sp=nil}) + hi('gitcommitSelected', {link='Comment'}) + hi('gitcommitSelectedFile', {fg=p.base0B, bg=nil, attr='bold', sp=nil}) + hi('gitcommitSelectedType', {link='gitcommitDiscardedType'}) + hi('gitcommitSummary', {fg=p.base0B, bg=nil, attr=nil, sp=nil}) + hi('gitcommitUnmergedFile', {link='gitcommitDiscardedFile'}) + hi('gitcommitUnmergedType', {link='gitcommitDiscardedType'}) + hi('gitcommitUntracked', {link='Comment'}) + hi('gitcommitUntrackedFile', {fg=p.base0A, bg=nil, attr=nil, sp=nil}) + + -- Built-in diagnostic + if vim.fn.has("nvim-0.6.0") == 1 then + hi('DiagnosticError', {fg=p.base08, bg=nil, attr=nil, sp=nil}) + hi('DiagnosticHint', {fg=p.base0D, bg=nil, attr=nil, sp=nil}) + hi('DiagnosticInfo', {fg=p.base0C, bg=nil, attr=nil, sp=nil}) + hi('DiagnosticWarn', {fg=p.base0E, bg=nil, attr=nil, sp=nil}) + + hi('DiagnosticFloatingError', {fg=p.base08, bg=p.base01, attr=nil, sp=nil}) + hi('DiagnosticFloatingHint', {fg=p.base0D, bg=p.base01, attr=nil, sp=nil}) + hi('DiagnosticFloatingInfo', {fg=p.base0C, bg=p.base01, attr=nil, sp=nil}) + hi('DiagnosticFloatingWarn', {fg=p.base0E, bg=p.base01, attr=nil, sp=nil}) + + hi('DiagnosticSignError', {link='DiagnosticFloatingError'}) + hi('DiagnosticSignHint', {link='DiagnosticFloatingHint'}) + hi('DiagnosticSignInfo', {link='DiagnosticFloatingInfo'}) + hi('DiagnosticSignWarn', {link='DiagnosticFloatingWarn'}) + + hi('DiagnosticUnderlineError', {fg=nil, bg=nil, attr='underline', sp=p.base08}) + hi('DiagnosticUnderlineHint', {fg=nil, bg=nil, attr='underline', sp=p.base0D}) + hi('DiagnosticUnderlineInfo', {fg=nil, bg=nil, attr='underline', sp=p.base0C}) + hi('DiagnosticUnderlineWarn', {fg=nil, bg=nil, attr='underline', sp=p.base0E}) + else + hi('LspDiagnosticsDefaultError', {fg=p.base08, bg=nil, attr=nil, sp=nil}) + hi('LspDiagnosticsDefaultHint', {fg=p.base0D, bg=nil, attr=nil, sp=nil}) + hi('LspDiagnosticsDefaultInformation', {fg=p.base0C, bg=nil, attr=nil, sp=nil}) + hi('LspDiagnosticsDefaultWarning', {fg=p.base0E, bg=nil, attr=nil, sp=nil}) + + hi('LspDiagnosticsFloatingError', {fg=p.base08, bg=p.base01, attr=nil, sp=nil}) + hi('LspDiagnosticsFloatingHint', {fg=p.base0D, bg=p.base01, attr=nil, sp=nil}) + hi('LspDiagnosticsFloatingInformation', {fg=p.base0C, bg=p.base01, attr=nil, sp=nil}) + hi('LspDiagnosticsFloatingWarning', {fg=p.base0E, bg=p.base01, attr=nil, sp=nil}) + + hi('LspDiagnosticsSignError', {link='LspDiagnosticsFloatingError'}) + hi('LspDiagnosticsSignHint', {link='LspDiagnosticsFloatingHint'}) + hi('LspDiagnosticsSignInformation', {link='LspDiagnosticsFloatingInformation'}) + hi('LspDiagnosticsSignWarning', {link='LspDiagnosticsFloatingWarning'}) + + hi('LspDiagnosticsUnderlineError', {fg=nil, bg=nil, attr='underline', sp=p.base08}) + hi('LspDiagnosticsUnderlineHint', {fg=nil, bg=nil, attr='underline', sp=p.base0D}) + hi('LspDiagnosticsUnderlineInformation', {fg=nil, bg=nil, attr='underline', sp=p.base0C}) + hi('LspDiagnosticsUnderlineWarning', {fg=nil, bg=nil, attr='underline', sp=p.base0E}) + end + + -- Built-in LSP + hi('LspReferenceText', {fg=nil, bg=p.base02, attr=nil, sp=nil}) + hi('LspReferenceRead', {link='LspReferenceText'}) + hi('LspReferenceWrite', {link='LspReferenceText'}) + + hi('LspSignatureActiveParameter', {link='LspReferenceText'}) + + hi('LspCodeLens', {link='Comment'}) + hi('LspCodeLensSeparator', {link='Comment'}) + + -- Plugins + -- echasnovski/mini.nvim + if H.has_integration('echasnovski/mini.nvim') then + hi('MiniCompletionActiveParameter', {fg=nil, bg=p.base02, attr=nil, sp=nil}) + + hi('MiniCursorword', {fg=nil, bg=nil, attr='underline', sp=nil}) + hi('MiniCursorwordCurrent', {fg=nil, bg=nil, attr='underline', sp=nil}) + + hi('MiniIndentscopeSymbol', {fg=p.base0F, bg=nil, attr=nil, sp=nil}) + hi('MiniIndentscopePrefix', {fg=nil, bg=nil, attr='nocombine', sp=nil}) + + hi('MiniJump', {link='SpellRare'}) + + hi('MiniJump2dSpot', {fg=p.base07, bg=p.base01, attr='bold,nocombine', sp=nil}) + + hi('MiniStarterCurrent', {fg=nil, bg=nil, attr=nil, sp=nil}) + hi('MiniStarterFooter', {fg=p.base0D, bg=nil, attr=nil, sp=nil}) + hi('MiniStarterHeader', {fg=p.base0D, bg=nil, attr=nil, sp=nil}) + hi('MiniStarterInactive', {link='Comment'}) + hi('MiniStarterItem', {fg=p.base05, bg=nil, attr=nil, sp=nil}) + hi('MiniStarterItemBullet', {fg=p.base0F, bg=nil, attr=nil, sp=nil}) + hi('MiniStarterItemPrefix', {fg=p.base08, bg=nil, attr='bold', sp=nil}) + hi('MiniStarterSection', {fg=p.base0F, bg=nil, attr=nil, sp=nil}) + hi('MiniStarterQuery', {fg=p.base0B, bg=nil, attr='bold', sp=nil}) + + hi('MiniStatuslineDevinfo', {fg=p.base04, bg=p.base02, attr=nil, sp=nil}) + hi('MiniStatuslineFileinfo', {link='MiniStatuslineDevinfo'}) + hi('MiniStatuslineFilename', {fg=p.base03, bg=p.base01, attr=nil, sp=nil}) + hi('MiniStatuslineInactive', {link='MiniStatuslineFilename'}) + hi('MiniStatuslineModeCommand', {fg=p.base00, bg=p.base08, attr='bold', sp=nil}) + hi('MiniStatuslineModeInsert', {fg=p.base00, bg=p.base0D, attr='bold', sp=nil}) + hi('MiniStatuslineModeNormal', {fg=p.base00, bg=p.base05, attr='bold', sp=nil}) + hi('MiniStatuslineModeOther', {fg=p.base00, bg=p.base03, attr='bold', sp=nil}) + hi('MiniStatuslineModeReplace', {fg=p.base00, bg=p.base0E, attr='bold', sp=nil}) + hi('MiniStatuslineModeVisual', {fg=p.base00, bg=p.base0B, attr='bold', sp=nil}) + + hi('MiniSurround', {link='IncSearch'}) + + hi('MiniTablineCurrent', {fg=p.base05, bg=p.base02, attr='bold', sp=nil}) + hi('MiniTablineFill', {fg=nil, bg=nil, attr=nil, sp=nil}) + hi('MiniTablineHidden', {fg=p.base04, bg=p.base01, attr=nil, sp=nil}) + hi('MiniTablineModifiedCurrent', {fg=p.base02, bg=p.base05, attr='bold', sp=nil}) + hi('MiniTablineModifiedHidden', {fg=p.base01, bg=p.base04, attr=nil, sp=nil}) + hi('MiniTablineModifiedVisible', {fg=p.base02, bg=p.base04, attr='bold', sp=nil}) + hi('MiniTablineTabpagesection', {fg=p.base01, bg=p.base0A, attr='bold', sp=nil}) + hi('MiniTablineVisible', {fg=p.base05, bg=p.base01, attr='bold', sp=nil}) + + hi('MiniTestEmphasis', {fg=nil, bg=nil, attr='bold', sp=nil}) + hi('MiniTestFail', {fg=p.base08, bg=nil, attr='bold', sp=nil}) + hi('MiniTestPass', {fg=p.base0B, bg=nil, attr='bold', sp=nil}) + + hi('MiniTrailspace', {link='Error'}) + end + + if H.has_integration('akinsho/bufferline.nvim') then + hi('BufferLineBuffer', {fg=p.base04, bg=nil, attr=nil, sp=nil}) + hi('BufferLineBufferSelected', {fg=p.base05, bg=nil, attr='bold', sp=nil}) + hi('BufferLineBufferVisible', {fg=p.base05, bg=nil, attr=nil, sp=nil}) + hi('BufferLineCloseButton', {link='BufferLineBackground'}) + hi('BufferLineCloseButtonSelected', {link='BufferLineBufferSelected'}) + hi('BufferLineCloseButtonVisible', {link='BufferLineBufferVisible'}) + hi('BufferLineFill', {link='Normal'}) + hi('BufferLineTab', {fg=p.base00, bg=p.base0A, attr=nil, sp=nil}) + hi('BufferLineTabSelected', {fg=p.base00, bg=p.base0A, attr='bold', sp=nil}) + end + + if H.has_integration('anuvyklack/hydra.nvim') then + hi('HydraRed', {fg=p.base08, bg=nil, attr=nil, sp=nil}) + hi('HydraBlue', {fg=p.base0D, bg=nil, attr=nil, sp=nil}) + hi('HydraAmaranth', {fg=p.base0E, bg=nil, attr=nil, sp=nil}) + hi('HydraTeal', {fg=p.base0B, bg=nil, attr=nil, sp=nil}) + hi('HydraPink', {fg=p.base09, bg=nil, attr=nil, sp=nil}) + hi('HydraHint', {link='NormalFloat'}) + end + + if H.has_integration('DanilaMihailov/beacon.nvim') then + hi('Beacon', {fg=nil, bg=p.base07, attr=nil, sp=nil}) + end + + -- folke/trouble.nvim + -- Everything works correctly out of the box + + -- folke/todo-comments.nvim + -- Everything works correctly out of the box + + if H.has_integration('folke/which-key.nvim') then + hi('WhichKey', {fg=p.base0D, bg=nil, attr=nil, sp=nil}) + hi('WhichKeyDesc', {fg=p.base05, bg=nil, attr=nil, sp=nil}) + hi('WhichKeyFloat', {fg=p.base05, bg=p.base01, attr=nil, sp=nil}) + hi('WhichKeyGroup', {fg=p.base0E, bg=nil, attr=nil, sp=nil}) + hi('WhichKeySeparator', {fg=p.base0B, bg=p.base01, attr=nil, sp=nil}) + hi('WhichKeyValue', {fg=p.base03, bg=nil, attr=nil, sp=nil}) + end + + if H.has_integration('ggandor/leap.nvim') then + hi('LeapMatch', {fg=p.base0E, bg=nil, attr='bold,nocombine', sp=nil}) + hi('LeapLabelPrimary', {fg=p.base08, bg=nil, attr='bold,nocombine', sp=nil}) + hi('LeapLabelSecondary', {fg=p.base05, bg=nil, attr='bold,nocombine', sp=nil}) + hi('LeapLabelSelected', {fg=p.base09, bg=nil, attr='bold,nocombine', sp=nil}) + hi('LeapBackdrop', {link='Comment'}) + end + + if H.has_integration('ggandor/lightspeed.nvim') then + hi('LightspeedLabel', {fg=p.base0E, bg=nil, attr='bold,underline', sp=nil}) + hi('LightspeedLabelDistant', {fg=p.base0D, bg=nil, attr='bold,underline', sp=nil}) + hi('LightspeedShortcut', {fg=p.base07, bg=nil, attr='bold', sp=nil}) + hi('LightspeedMaskedChar', {fg=p.base04, bg=nil, attr=nil, sp=nil}) + hi('LightspeedUnlabeledMatch', {fg=p.base05, bg=nil, attr='bold', sp=nil}) + hi('LightspeedGreyWash', {link='Comment'}) + hi('LightspeedUniqueChar', {link='LightspeedUnlabeledMatch'}) + hi('LightspeedOneCharMatch', {link='LightspeedShortcut'}) + hi('LightspeedPendingOpArea', {link='IncSearch'}) + hi('LightspeedCursor', {link='Cursor'}) + end + + if H.has_integration('glepnir/dashboard-nvim') then + hi('DashboardCenter', {link='Delimiter'}) + hi('DashboardFooter', {link='Title'}) + hi('DashboardHeader', {link='Title'}) + hi('DashboardShortCut', {link='WarningMsg'}) + end + + if H.has_integration('glepnir/lspsaga.nvim') then + hi('LspSagaCodeActionBorder', {fg=p.base0F, bg=nil, attr=nil, sp=nil}) + hi('LspSagaCodeActionContent', {fg=p.base05, bg=nil, attr=nil, sp=nil}) + hi('LspSagaCodeActionTitle', {fg=p.base0D, bg=nil, attr='bold', sp=nil}) + + hi('Definitions', {fg=p.base0B, bg=nil, attr=nil, sp=nil}) + hi('DefinitionsIcon', {fg=p.base0D, bg=nil, attr=nil, sp=nil}) + hi('FinderParam', {fg=p.base08, bg=nil, attr=nil, sp=nil}) + hi('FinderVirtText', {fg=p.base09, bg=nil, attr=nil, sp=nil}) + hi('LspSagaAutoPreview', {fg=p.base0F, bg=nil, attr=nil, sp=nil}) + hi('LspSagaFinderSelection', {fg=p.base0A, bg=nil, attr=nil, sp=nil}) + hi('LspSagaLspFinderBorder', {fg=p.base0F, bg=nil, attr=nil, sp=nil}) + hi('References', {fg=p.base0B, bg=nil, attr=nil, sp=nil}) + hi('ReferencesIcon', {fg=p.base0D, bg=nil, attr=nil, sp=nil}) + hi('TargetFileName', {fg=p.base05, bg=nil, attr=nil, sp=nil}) + + hi('FinderSpinner', {fg=p.base0B, bg=nil, attr=nil, sp=nil}) + hi('FinderSpinnerBorder', {fg=p.base0F, bg=nil, attr=nil, sp=nil}) + hi('FinderSpinnerTitle', {link='Title'}) + + hi('LspSagaDefPreviewBorder', {fg=p.base0F, bg=nil, attr=nil, sp=nil}) + + hi('LspSagaHoverBorder', {fg=p.base0F, bg=nil, attr=nil, sp=nil}) + + hi('LspSagaRenameBorder', {fg=p.base0F, bg=nil, attr=nil, sp=nil}) + + hi('LspSagaDiagnosticBorder', {fg=p.base0F, bg=nil, attr=nil, sp=nil}) + hi('LspSagaDiagnosticHeader', {link='Title'}) + hi('LspSagaDiagnosticSource', {fg=p.base0E, bg=nil, attr=nil, sp=nil}) + + hi('LspSagaBorderTitle', {link='Title'}) + + hi('LspSagaSignatureHelpBorder', {fg=p.base0F, bg=nil, attr=nil, sp=nil}) + + hi('LSOutlinePreviewBorder', {fg=p.base0F, bg=nil, attr=nil, sp=nil}) + hi('OutlineDetail', {fg=p.base03, bg=nil, attr=nil, sp=nil}) + hi('OutlineFoldPrefix', {fg=p.base08, bg=nil, attr=nil, sp=nil}) + hi('OutlineIndentEvn', {fg=p.base04, bg=nil, attr=nil, sp=nil}) + hi('OutlineIndentOdd', {fg=p.base05, bg=nil, attr=nil, sp=nil}) + end + + if H.has_integration('hrsh7th/nvim-cmp') then + hi('CmpItemAbbr', {fg=p.base05, bg=nil, attr=nil, sp=nil}) + hi('CmpItemAbbrDeprecated', {fg=p.base03, bg=nil, attr=nil, sp=nil}) + hi('CmpItemAbbrMatch', {fg=p.base0A, bg=nil, attr='bold', sp=nil}) + hi('CmpItemAbbrMatchFuzzy', {fg=p.base0A, bg=nil, attr='bold', sp=nil}) + hi('CmpItemKind', {fg=p.base0F, bg=p.base01, attr=nil, sp=nil}) + hi('CmpItemMenu', {fg=p.base05, bg=p.base01, attr=nil, sp=nil}) + + hi('CmpItemKindClass', {link='Type'}) + hi('CmpItemKindColor', {link='Special'}) + hi('CmpItemKindConstant', {link='Constant'}) + hi('CmpItemKindConstructor', {link='Type'}) + hi('CmpItemKindEnum', {link='Structure'}) + hi('CmpItemKindEnumMember', {link='Structure'}) + hi('CmpItemKindEvent', {link='Exception'}) + hi('CmpItemKindField', {link='Structure'}) + hi('CmpItemKindFile', {link='Tag'}) + hi('CmpItemKindFolder', {link='Directory'}) + hi('CmpItemKindFunction', {link='Function'}) + hi('CmpItemKindInterface', {link='Structure'}) + hi('CmpItemKindKeyword', {link='Keyword'}) + hi('CmpItemKindMethod', {link='Function'}) + hi('CmpItemKindModule', {link='Structure'}) + hi('CmpItemKindOperator', {link='Operator'}) + hi('CmpItemKindProperty', {link='Structure'}) + hi('CmpItemKindReference', {link='Tag'}) + hi('CmpItemKindSnippet', {link='Special'}) + hi('CmpItemKindStruct', {link='Structure'}) + hi('CmpItemKindText', {link='Statement'}) + hi('CmpItemKindTypeParameter', {link='Type'}) + hi('CmpItemKindUnit', {link='Special'}) + hi('CmpItemKindValue', {link='Identifier'}) + hi('CmpItemKindVariable', {link='Delimiter'}) + end + + if H.has_integration('justinmk/vim-sneak') then + hi('Sneak', {fg=p.base00, bg=p.base0E, attr=nil, sp=nil}) + hi('SneakScope', {fg=p.base00, bg=p.base07, attr=nil, sp=nil}) + hi('SneakLabel', {fg=p.base00, bg=p.base0E, attr='bold', sp=nil}) + end + + if H.has_integration('kyazdani42/nvim-tree.lua') then + hi('NvimTreeExecFile', {fg=p.base0B, bg=nil, attr='bold', sp=nil}) + hi('NvimTreeFolderIcon', {fg=p.base03, bg=nil, attr=nil, sp=nil}) + hi('NvimTreeGitDeleted', {fg=p.base08, bg=nil, attr=nil, sp=nil}) + hi('NvimTreeGitDirty', {link='NvimTreeGitDirty'}) + hi('NvimTreeGitMerge', {fg=p.base0C, bg=nil, attr=nil, sp=nil}) + hi('NvimTreeGitNew', {fg=p.base0D, bg=nil, attr=nil, sp=nil}) + hi('NvimTreeGitRenamed', {fg=p.base0E, bg=nil, attr=nil, sp=nil}) + hi('NvimTreeGitStaged', {fg=p.base0B, bg=nil, attr=nil, sp=nil}) + hi('NvimTreeImageFile', {fg=p.base0E, bg=nil, attr='bold', sp=nil}) + hi('NvimTreeIndentMarker', {link='NvimTreeFolderIcon'}) + hi('NvimTreeOpenedFile', {link='NvimTreeExecFile'}) + hi('NvimTreeRootFolder', {link='NvimTreeGitRenamed'}) + hi('NvimTreeSpecialFile', {fg=p.base0D, bg=nil, attr='bold,underline', sp=nil}) + hi('NvimTreeSymlink', {fg=p.base0F, bg=nil, attr='bold', sp=nil}) + hi('NvimTreeWindowPicker', {fg=p.base05, bg=p.base01, attr="bold", sp=nil}) + end + + if H.has_integration('lewis6991/gitsigns.nvim') then + hi('GitSignsAdd', {fg=p.base0B, bg=p.base01, attr=nil, sp=nil}) + hi('GitSignsAddLn', {link='GitSignsAdd'}) + hi('GitSignsAddInline', {link='GitSignsAdd'}) + hi('GitSignsChange', {fg=p.base0E, bg=p.base01, attr=nil, sp=nil}) + hi('GitSignsChangeLn', {link='GitSignsChange'}) + hi('GitSignsChangeInline', {link='GitSignsChange'}) + hi('GitSignsDelete', {fg=p.base08, bg=p.base01, attr=nil, sp=nil}) + hi('GitSignsDeleteLn', {link='GitSignsDelete'}) + hi('GitSignsDeleteInline', {link='GitSignsDelete'}) + end + + if H.has_integration('lukas-reineke/indent-blankline.nvim') then + hi('IndentBlanklineChar', {fg=p.base02, bg=nil, attr='nocombine', sp=nil}) + hi('IndentBlanklineContextChar', {fg=p.base0F, bg=nil, attr='nocombine', sp=nil}) + hi('IndentBlanklineContextStart', {fg=nil, bg=nil, attr='underline,nocombine', sp=p.base0F}) + hi('IndentBlanklineIndent1', {fg=p.base08, bg=nil, attr='nocombine', sp=nil}) + hi('IndentBlanklineIndent2', {fg=p.base09, bg=nil, attr='nocombine', sp=nil}) + hi('IndentBlanklineIndent3', {fg=p.base0A, bg=nil, attr='nocombine', sp=nil}) + hi('IndentBlanklineIndent4', {fg=p.base0B, bg=nil, attr='nocombine', sp=nil}) + hi('IndentBlanklineIndent5', {fg=p.base0C, bg=nil, attr='nocombine', sp=nil}) + hi('IndentBlanklineIndent6', {fg=p.base0D, bg=nil, attr='nocombine', sp=nil}) + hi('IndentBlanklineIndent7', {fg=p.base0E, bg=nil, attr='nocombine', sp=nil}) + hi('IndentBlanklineIndent8', {fg=p.base0F, bg=nil, attr='nocombine', sp=nil}) + end + + if H.has_integration('neoclide/coc.nvim') then + hi('CocErrorHighlight', {link='DiagnosticError'}) + hi('CocHintHighlight', {link='DiagnosticHint'}) + hi('CocInfoHighlight', {link='DiagnosticInfo'}) + hi('CocWarningHighlight', {link='DiagnosticWarn'}) + + hi('CocErrorFloat', {link='DiagnosticFloatingError'}) + hi('CocHintFloat', {link='DiagnosticFloatingHint'}) + hi('CocInfoFloat', {link='DiagnosticFloatingInfo'}) + hi('CocWarningFloat', {link='DiagnosticFloatingWarn'}) + + hi('CocErrorSign', {link='DiagnosticSignError'}) + hi('CocHintSign', {link='DiagnosticSignHint'}) + hi('CocInfoSign', {link='DiagnosticSignInfo'}) + hi('CocWarningSign', {link='DiagnosticSignWarn'}) + + hi('CocCodeLens', {link='LspCodeLens'}) + hi('CocDisabled', {link='Comment'}) + hi('CocMarkdownLink', {fg=p.base0F, bg=nil, attr=nil, sp=nil}) + hi('CocMenuSel', {fg=nil, bg=p.base02, attr=nil, sp=nil}) + hi('CocNotificationProgress', {link='CocMarkdownLink'}) + hi('CocPumVirtualText', {link='CocMarkdownLink'}) + hi('CocSearch', {fg=p.base0A, bg=nil, attr=nil, sp=nil}) + hi('CocSelectedText', {fg=p.base08, bg=nil, attr=nil, sp=nil}) + end + + -- nvim-lualine/lualine.nvim + -- Everything works correctly out of the box + + if H.has_integration('nvim-neo-tree/neo-tree.nvim') then + hi('NeoTreeDimText', {fg=p.base03, bg=nil, attr=nil, sp=nil}) + hi('NeoTreeDotfile', {fg=p.base04, bg=nil, attr=nil, sp=nil}) + hi('NeoTreeFadeText1', {link='NeoTreeDimText'}) + hi('NeoTreeFadeText2', {fg=p.base02, bg=nil, attr=nil, sp=nil}) + hi('NeoTreeGitAdded', {fg=p.base0B, bg=nil, attr=nil, sp=nil}) + hi('NeoTreeGitConflict', {fg=p.base08, bg=nil, attr='bold', sp=nil}) + hi('NeoTreeGitDeleted', {fg=p.base08, bg=nil, attr=nil, sp=nil}) + hi('NeoTreeGitModified', {fg=p.base0E, bg=nil, attr=nil, sp=nil}) + hi('NeoTreeGitUnstaged', {fg=p.base08, bg=nil, attr=nil, sp=nil}) + hi('NeoTreeGitUntracked', {fg=p.base0A, bg=nil, attr=nil, sp=nil}) + hi('NeoTreeMessage', {fg=p.base05, bg=p.base01, attr=nil, sp=nil}) + hi('NeoTreeModified', {fg=p.base07, bg=nil, attr=nil, sp=nil}) + hi('NeoTreeRootName', {fg=p.base0D, bg=nil, attr='bold', sp=nil}) + hi('NeoTreeTabInactive', {fg=p.base04, bg=nil, attr=nil, sp=nil}) + hi('NeoTreeTabSeparatorActive', {fg=p.base03, bg=p.base02, attr=nil, sp=nil}) + hi('NeoTreeTabSeparatorInactive', {fg=p.base01, bg=p.base01, attr=nil, sp=nil}) + end + + if H.has_integration('nvim-telescope/telescope.nvim') then + hi('TelescopeBorder', {fg=p.base0F, bg=nil, attr=nil, sp=nil}) + hi('TelescopeMatching', {fg=p.base0A, bg=nil, attr=nil, sp=nil}) + hi('TelescopeMultiSelection', {fg=nil, bg=p.base01, attr='bold', sp=nil}) + hi('TelescopeSelection', {fg=nil, bg=p.base01, attr='bold', sp=nil}) + end + + if H.has_integration('p00f/nvim-ts-rainbow') then + hi('rainbowcol1', {fg=p.base08, bg=nil, attr=nil, sp=nil}) + hi('rainbowcol2', {fg=p.base09, bg=nil, attr=nil, sp=nil}) + hi('rainbowcol3', {fg=p.base0A, bg=nil, attr=nil, sp=nil}) + hi('rainbowcol4', {fg=p.base0B, bg=nil, attr=nil, sp=nil}) + hi('rainbowcol5', {fg=p.base0C, bg=nil, attr=nil, sp=nil}) + hi('rainbowcol6', {fg=p.base0D, bg=nil, attr=nil, sp=nil}) + hi('rainbowcol7', {fg=p.base0E, bg=nil, attr=nil, sp=nil}) + end + + if H.has_integration('phaazon/hop.nvim') then + hi('HopNextKey', {fg=p.base0E, bg=nil, attr='bold,nocombine', sp=nil}) + hi('HopNextKey1', {fg=p.base08, bg=nil, attr='bold,nocombine', sp=nil}) + hi('HopNextKey2', {fg=p.base04, bg=nil, attr='bold,nocombine', sp=nil}) + hi('HopPreview', {fg=p.base09, bg=nil, attr='bold,nocombine', sp=nil}) + hi('HopUnmatched', {link='Comment'}) + end + + if H.has_integration('rcarriga/nvim-dap-ui') then + hi('DapUIScope', {link='Title'}) + hi('DapUIType', {link='Type'}) + hi('DapUIModifiedValue', {fg=p.base0E, bg=nil, attr='bold', sp=nil}) + hi('DapUIDecoration', {link='Title'}) + hi('DapUIThread', {link='String'}) + hi('DapUIStoppedThread', {link='Title'}) + hi('DapUISource', {link='Directory'}) + hi('DapUILineNumber', {link='Title'}) + hi('DapUIFloatBorder', {link='SpecialChar'}) + hi('DapUIWatchesEmpty', {link='ErrorMsg'}) + hi('DapUIWatchesValue', {link='String'}) + hi('DapUIWatchesError', {link='DiagnosticError'}) + hi('DapUIBreakpointsPath', {link='Directory'}) + hi('DapUIBreakpointsInfo', {link='DiagnosticInfo'}) + hi('DapUIBreakpointsCurrentLine', {fg=p.base0B, bg=nil, attr='bold', sp=nil}) + hi('DapUIBreakpointsDisabledLine', {link='Comment'}) + end + + if H.has_integration('rcarriga/nvim-notify') then + hi('NotifyDEBUGBorder', {fg=p.base03, bg=nil, attr=nil, sp=nil}) + hi('NotifyDEBUGIcon', {link='NotifyDEBUGBorder'}) + hi('NotifyDEBUGTitle', {link='NotifyDEBUGBorder'}) + hi('NotifyERRORBorder', {fg=p.base08, bg=nil, attr=nil, sp=nil}) + hi('NotifyERRORIcon', {link='NotifyERRORBorder'}) + hi('NotifyERRORTitle', {link='NotifyERRORBorder'}) + hi('NotifyINFOBorder', {fg=p.base0C, bg=nil, attr=nil, sp=nil}) + hi('NotifyINFOIcon', {link='NotifyINFOBorder'}) + hi('NotifyINFOTitle', {link='NotifyINFOBorder'}) + hi('NotifyTRACEBorder', {fg=p.base0D, bg=nil, attr=nil, sp=nil}) + hi('NotifyTRACEIcon', {link='NotifyTRACEBorder'}) + hi('NotifyTRACETitle', {link='NotifyTRACEBorder'}) + hi('NotifyWARNBorder', {fg=p.base0E, bg=nil, attr=nil, sp=nil}) + hi('NotifyWARNIcon', {link='NotifyWARNBorder'}) + hi('NotifyWARNTitle', {link='NotifyWARNBorder'}) + end + + if H.has_integration('rlane/pounce.nvim') then + hi('PounceMatch', {fg=p.base00, bg=p.base05, attr='bold,nocombine', sp=nil}) + hi('PounceGap', {fg=p.base00, bg=p.base03, attr='bold,nocombine', sp=nil}) + hi('PounceAccept', {fg=p.base00, bg=p.base08, attr='bold,nocombine', sp=nil}) + hi('PounceAcceptBest', {fg=p.base00, bg=p.base0B, attr='bold,nocombine', sp=nil}) + end + + if H.has_integration('romgrk/barbar.nvim') then + hi('BufferCurrent', {fg=p.base05, bg=p.base02, attr='bold', sp=nil}) + hi('BufferCurrentIcon', {fg=nil, bg=p.base02, attr=nil, sp=nil}) + hi('BufferCurrentIndex', {link='BufferCurrentIcon'}) + hi('BufferCurrentMod', {fg=p.base08, bg=p.base02, attr='bold', sp=nil}) + hi('BufferCurrentSign', {link='BufferCurrent'}) + hi('BufferCurrentTarget', {fg=p.base0E, bg=p.base02, attr='bold', sp=nil}) + + hi('BufferInactive', {fg=p.base04, bg=p.base01, attr=nil, sp=nil}) + hi('BufferInactiveIcon', {fg=nil, bg=p.base01, attr=nil, sp=nil}) + hi('BufferInactiveIndex', {link='BufferInactiveIcon'}) + hi('BufferInactiveMod', {fg=p.base08, bg=p.base01, attr=nil, sp=nil}) + hi('BufferInactiveSign', {link='BufferInactive'}) + hi('BufferInactiveTarget', {fg=p.base0E, bg=p.base01, attr='bold', sp=nil}) + + hi('BufferOffset', {link='Normal'}) + hi('BufferTabpages', {fg=p.base01, bg=p.base0A, attr='bold', sp=nil}) + hi('BufferTabpageFill', {link='Normal'}) + + hi('BufferVisible', {fg=p.base05, bg=p.base01, attr='bold', sp=nil}) + hi('BufferVisibleIcon', {fg=nil, bg=p.base01, attr=nil, sp=nil}) + hi('BufferVisibleIndex', {link='BufferVisibleIcon'}) + hi('BufferVisibleMod', {fg=p.base08, bg=p.base01, attr='bold', sp=nil}) + hi('BufferVisibleSign', {link='BufferVisible'}) + hi('BufferVisibleTarget', {fg=p.base0E, bg=p.base01, attr='bold', sp=nil}) + end + + -- simrat39/symbols-outline.nvim + -- Everything works correctly out of the box + + -- stevearc/aerial.nvim + -- Everything works correctly out of the box + + -- TimUntersberger/neogit + -- Everything works correctly out of the box + + if H.has_integration('williamboman/mason.nvim') then + hi('MasonError', {fg=p.base08, bg=nil, attr=nil, sp=nil}) + hi('MasonHeader', {fg=p.base00, bg=p.base0D, attr='bold', sp=nil}) + hi('MasonHeaderSecondary', {fg=p.base00, bg=p.base0F, attr='bold', sp=nil}) + hi('MasonHeading', {link='Bold'}) + hi('MasonHighlight', {fg=p.base0F, bg=nil, attr=nil, sp=nil}) + hi('MasonHighlightBlock', {fg=p.base00, bg=p.base0F, attr=nil, sp=nil}) + hi('MasonHighlightBlockBold', {link='MasonHeaderSecondary'}) + hi('MasonHighlightBlockBoldSecondary', {link='MasonHeader'}) + hi('MasonHighlightBlockSecondary', {fg=p.base00, bg=p.base0D, attr=nil, sp=nil}) + hi('MasonHighlightSecondary', {fg=p.base0D, bg=nil, attr=nil, sp=nil}) + hi('MasonLink', {link='MasonHighlight'}) + hi('MasonMuted', {link='Comment'}) + hi('MasonMutedBlock', {fg=p.base00, bg=p.base03, attr=nil, sp=nil}) + hi('MasonMutedBlockBold', {fg=p.base00, bg=p.base03, attr='bold', sp=nil}) + end + -- stylua: ignore end + + -- Terminal colors + vim.g.terminal_color_0 = palette.base00 + vim.g.terminal_color_1 = palette.base08 + vim.g.terminal_color_2 = palette.base0B + vim.g.terminal_color_3 = palette.base0A + vim.g.terminal_color_4 = palette.base0D + vim.g.terminal_color_5 = palette.base0E + vim.g.terminal_color_6 = palette.base0C + vim.g.terminal_color_7 = palette.base05 + vim.g.terminal_color_8 = palette.base03 + vim.g.terminal_color_9 = palette.base08 + vim.g.terminal_color_10 = palette.base0B + vim.g.terminal_color_11 = palette.base0A + vim.g.terminal_color_12 = palette.base0D + vim.g.terminal_color_13 = palette.base0E + vim.g.terminal_color_14 = palette.base0C + vim.g.terminal_color_15 = palette.base07 + vim.g.terminal_color_background = vim.g.terminal_color_0 + vim.g.terminal_color_foreground = vim.g.terminal_color_5 + if vim.o.background == 'light' then + vim.g.terminal_color_background = vim.g.terminal_color_7 + vim.g.terminal_color_foreground = vim.g.terminal_color_2 + end +end + +H.has_integration = function(name) + local entry = MiniBase16.config.plugins[name] + if entry == nil then return MiniBase16.config.plugins.default end + return entry +end + +H.highlight_gui = function(group, args) + -- NOTE: using `string.format` instead of gradually growing string with `..` + -- is faster. Crude estimate for this particular case: whole colorscheme + -- loading decreased from ~3.6ms to ~3.0ms, i.e. by about 20%. + local command + if args.link ~= nil then + command = string.format('highlight! link %s %s', group, args.link) + else + command = string.format( + 'highlight %s guifg=%s guibg=%s gui=%s guisp=%s', + group, + args.fg or 'NONE', + args.bg or 'NONE', + args.attr or 'NONE', + args.sp or 'NONE' + ) + end + vim.cmd(command) +end + +H.highlight_both = function(group, args) + local command + if args.link ~= nil then + command = string.format('highlight! link %s %s', group, args.link) + else + command = string.format( + 'highlight %s guifg=%s ctermfg=%s guibg=%s ctermbg=%s gui=%s cterm=%s guisp=%s', + group, + args.fg and args.fg.gui or 'NONE', + args.fg and args.fg.cterm or 'NONE', + args.bg and args.bg.gui or 'NONE', + args.bg and args.bg.cterm or 'NONE', + args.attr or 'NONE', + args.attr or 'NONE', + args.sp and args.sp.gui or 'NONE' + ) + end + vim.cmd(command) +end + +-- Compound (gui and cterm) palette ------------------------------------------- +H.make_compound_palette = function(palette, use_cterm) + local cterm_table = use_cterm + if type(use_cterm) == 'boolean' then cterm_table = MiniBase16.rgb_palette_to_cterm_palette(palette) end + + local res = {} + for name, _ in pairs(palette) do + res[name] = { gui = palette[name], cterm = cterm_table[name] } + end + return res +end + +-- Optimal scales. Make a set of equally spaced hues which are as different to +-- present hues as possible +H.make_different_hues = function(present_hues, n) + local max_offset = math.floor(360 / n + 0.5) + + local dist, best_dist = nil, -math.huge + local best_hues, new_hues + + for offset = 0, max_offset - 1, 1 do + new_hues = H.make_hue_scale(n, offset) + + -- Compute distance as usual 'minimum distance' between two sets + dist = H.dist_circle_set(new_hues, present_hues) + + -- Decide if it is the best + if dist > best_dist then + best_hues, best_dist = new_hues, dist + end + end + + return best_hues +end + +H.make_hue_scale = function(n, offset) + local step = math.floor(360 / n + 0.5) + local res = {} + for i = 0, n - 1, 1 do + table.insert(res, (offset + i * step) % 360) + end + return res +end + +-- Terminal colors ------------------------------------------------------------ +-- Sources: +-- - https://github.com/shawncplus/Vim-toCterm/blob/master/lib/Xterm.php +-- - https://gist.github.com/MicahElliott/719710 +-- stylua: ignore start +H.cterm_first16 = { + { r = 0, g = 0, b = 0 }, + { r = 205, g = 0, b = 0 }, + { r = 0, g = 205, b = 0 }, + { r = 205, g = 205, b = 0 }, + { r = 0, g = 0, b = 238 }, + { r = 205, g = 0, b = 205 }, + { r = 0, g = 205, b = 205 }, + { r = 229, g = 229, b = 229 }, + { r = 127, g = 127, b = 127 }, + { r = 255, g = 0, b = 0 }, + { r = 0, g = 255, b = 0 }, + { r = 255, g = 255, b = 0 }, + { r = 92, g = 92, b = 255 }, + { r = 255, g = 0, b = 255 }, + { r = 0, g = 255, b = 255 }, + { r = 255, g = 255, b = 255 }, +} +-- stylua: ignore end + +H.cterm_basis = { 0, 95, 135, 175, 215, 255 } + +H.cterm2rgb = function(i) + if i < 16 then return H.cterm_first16[i + 1] end + if 16 <= i and i <= 231 then + i = i - 16 + local r = H.cterm_basis[math.floor(i / 36) % 6 + 1] + local g = H.cterm_basis[math.floor(i / 6) % 6 + 1] + local b = H.cterm_basis[i % 6 + 1] + return { r = r, g = g, b = b } + end + if 232 <= i and i <= 255 then + local c = 8 + (i - 232) * 10 + return { r = c, g = c, b = c } + end +end + +H.ensure_cterm_palette = function() + if H.cterm_palette then return end + H.cterm_palette = {} + for i = 0, 255 do + H.cterm_palette[i] = H.cterm2rgb(i) + end +end + +-- Color conversion ----------------------------------------------------------- +-- Source: https://www.easyrgb.com/en/math.php +-- Accuracy is usually around 2-3 decimal digits, which should be fine + +-- HEX <-> CIELCh(uv) +H.hex2lch = function(hex) + local res = hex + for _, f in pairs({ H.hex2rgb, H.rgb2xyz, H.xyz2luv, H.luv2lch }) do + res = f(res) + end + return res +end + +H.lch2hex = function(lch) + local res = lch + for _, f in pairs({ H.lch2luv, H.luv2xyz, H.xyz2rgb, H.rgb2hex }) do + res = f(res) + end + return res +end + +-- HEX <-> RGB +H.hex2rgb = function(hex) + local dec = tonumber(hex:sub(2), 16) + + local b = math.fmod(dec, 256) + local g = math.fmod((dec - b) / 256, 256) + local r = math.floor(dec / 65536) + + return { r = r, g = g, b = b } +end + +H.rgb2hex = function(rgb) + -- Round and trim values + local t = vim.tbl_map(function(x) + x = math.min(math.max(x, 0), 255) + return math.floor(x + 0.5) + end, rgb) + + return '#' .. string.format('%02x', t.r) .. string.format('%02x', t.g) .. string.format('%02x', t.b) +end + +-- RGB <-> XYZ +H.rgb2xyz = function(rgb) + local t = vim.tbl_map(function(c) + c = c / 255 + if c > 0.04045 then + c = ((c + 0.055) / 1.055) ^ 2.4 + else + c = c / 12.92 + end + return 100 * c + end, rgb) + + -- Source of better matrix: http://brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html + local x = 0.41246 * t.r + 0.35757 * t.g + 0.18043 * t.b + local y = 0.21267 * t.r + 0.71515 * t.g + 0.07217 * t.b + local z = 0.01933 * t.r + 0.11919 * t.g + 0.95030 * t.b + return { x = x, y = y, z = z } +end + +H.xyz2rgb = function(xyz) + -- Source of better matrix: http://brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html + -- stylua: ignore start + local r = 3.24045 * xyz.x - 1.53713 * xyz.y - 0.49853 * xyz.z + local g = -0.96927 * xyz.x + 1.87601 * xyz.y + 0.04155 * xyz.z + local b = 0.05564 * xyz.x - 0.20403 * xyz.y + 1.05722 * xyz.z + -- stylua: ignore end + + return vim.tbl_map(function(c) + c = c / 100 + if c > 0.0031308 then + c = 1.055 * (c ^ (1 / 2.4)) - 0.055 + else + c = 12.92 * c + end + return 255 * c + end, { + r = r, + g = g, + b = b, + }) +end + +-- XYZ <-> CIELuv +-- Using white reference for D65 and 2 degress +H.ref_u = (4 * 95.047) / (95.047 + (15 * 100) + (3 * 108.883)) +H.ref_v = (9 * 100) / (95.047 + (15 * 100) + (3 * 108.883)) + +H.xyz2luv = function(xyz) + local x, y, z = xyz.x, xyz.y, xyz.z + if x + y + z == 0 then return { l = 0, u = 0, v = 0 } end + + local var_u = 4 * x / (x + 15 * y + 3 * z) + local var_v = 9 * y / (x + 15 * y + 3 * z) + local var_y = y / 100 + if var_y > 0.008856 then + var_y = var_y ^ (1 / 3) + else + var_y = (7.787 * var_y) + (16 / 116) + end + + local l = (116 * var_y) - 16 + local u = 13 * l * (var_u - H.ref_u) + local v = 13 * l * (var_v - H.ref_v) + return { l = l, u = u, v = v } +end + +H.luv2xyz = function(luv) + if luv.l == 0 then return { x = 0, y = 0, z = 0 } end + + local var_y = (luv.l + 16) / 116 + if var_y ^ 3 > 0.008856 then + var_y = var_y ^ 3 + else + var_y = (var_y - 16 / 116) / 7.787 + end + + local var_u = luv.u / (13 * luv.l) + H.ref_u + local var_v = luv.v / (13 * luv.l) + H.ref_v + + local y = var_y * 100 + local x = -(9 * y * var_u) / ((var_u - 4) * var_v - var_u * var_v) + local z = (9 * y - 15 * var_v * y - var_v * x) / (3 * var_v) + return { x = x, y = y, z = z } +end + +-- CIELuv <-> CIELCh(uv) +H.tau = 2 * math.pi + +H.luv2lch = function(luv) + local c = math.sqrt(luv.u ^ 2 + luv.v ^ 2) + local h + if c == 0 then + h = 0 + else + -- Convert [-pi, pi] radians to [0, 360] degrees + h = (math.atan2(luv.v, luv.u) % H.tau) * 360 / H.tau + end + return { l = luv.l, c = c, h = h } +end + +H.lch2luv = function(lch) + local angle = lch.h * H.tau / 360 + local u = lch.c * math.cos(angle) + local v = lch.c * math.sin(angle) + return { l = lch.l, u = u, v = v } +end + +-- Distances ------------------------------------------------------------------ +H.dist_circle = function(x, y) + local d = math.abs(x - y) % 360 + return d > 180 and (360 - d) or d +end + +H.dist_circle_set = function(set1, set2) + -- Minimum distance between all pairs + local dist = math.huge + local d + for _, x in pairs(set1) do + for _, y in pairs(set2) do + d = H.dist_circle(x, y) + if dist > d then dist = d end + end + end + return dist +end + +H.nearest_rgb_id = function(rgb_target, rgb_palette) + local best_dist = math.huge + local best_id, dist + for id, rgb in pairs(rgb_palette) do + dist = math.abs(rgb_target.r - rgb.r) + math.abs(rgb_target.g - rgb.g) + math.abs(rgb_target.b - rgb.b) + if dist < best_dist then + best_id, best_dist = id, dist + end + end + + return best_id +end + +return MiniBase16 diff --git a/dotfiles/pack/plugins/start/mini.nvim/lua/mini/bufremove.lua b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/bufremove.lua new file mode 100755 index 0000000..297d489 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/bufremove.lua @@ -0,0 +1,255 @@ +-- MIT License Copyright (c) 2021 Evgeni Chasnovski + +-- Documentation ============================================================== +--- 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. +---@tag mini.bufremove +---@tag MiniBufremove + +-- Module definition ========================================================== +local MiniBufremove = {} +local H = {} + +--- Module setup +--- +---@param config table Module config table. See |MiniBufremove.config|. +--- +---@usage `require('mini.bufremove').setup({})` (replace `{}` with your `config` table) +MiniBufremove.setup = function(config) + -- Export module + _G.MiniBufremove = MiniBufremove + + -- Setup config + config = H.setup_config(config) + + -- Apply config + H.apply_config(config) +end + +--- Module config +--- +--- Default values: +---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section) +MiniBufremove.config = { + -- Whether to set Vim's settings for buffers (allow hidden buffers) + set_vim_settings = true, +} +--minidoc_afterlines_end + +-- Module functionality ======================================================= +--- Delete buffer `buf_id` with |:bdelete| after unshowing it +--- +---@param buf_id number Buffer identifier (see |bufnr()|) to use. Default: +--- 0 for current. +---@param force boolean Whether to ignore unsaved changes (using `!` version of +--- command). Default: `false`. +--- +---@return boolean Whether operation was successful. +MiniBufremove.delete = function(buf_id, force) + if H.is_disabled() then return end + + return H.unshow_and_cmd(buf_id, force, 'bdelete') +end + +--- Wipeout buffer `buf_id` with |:bwipeout| after unshowing it +--- +---@param buf_id number Buffer identifier (see |bufnr()|) to use. Default: +--- 0 for current. +---@param force boolean Whether to ignore unsaved changes (using `!` version of +--- command). Default: `false`. +--- +---@return boolean Whether operation was successful. +MiniBufremove.wipeout = function(buf_id, force) + if H.is_disabled() then return end + + return H.unshow_and_cmd(buf_id, force, 'bwipeout') +end + +--- Stop showing buffer `buf_id` in all windows +--- +---@param buf_id number Buffer identifier (see |bufnr()|) to use. Default: +--- 0 for current. +--- +---@return boolean Whether operation was successful. +MiniBufremove.unshow = function(buf_id) + if H.is_disabled() then return end + + buf_id = H.normalize_buf_id(buf_id) + + if not H.is_valid_id(buf_id, 'buffer') then return false end + + vim.tbl_map(MiniBufremove.unshow_in_window, vim.fn.win_findbuf(buf_id)) + + return true +end + +--- Stop showing current buffer of window `win_id` +--- +---@param win_id number Window identifier (see |win_getid()|) to use. +--- Default: 0 for current. +--- +---@return boolean Whether operation was successful. +MiniBufremove.unshow_in_window = function(win_id) + if H.is_disabled() then return nil end + + win_id = (win_id == nil) and 0 or win_id + + if not H.is_valid_id(win_id, 'window') then return false end + + local cur_buf = vim.api.nvim_win_get_buf(win_id) + + -- Temporary use window `win_id` as current to have Vim's functions working + vim.api.nvim_win_call(win_id, function() + -- Try using alternate buffer + local alt_buf = vim.fn.bufnr('#') + if alt_buf ~= cur_buf and vim.fn.buflisted(alt_buf) == 1 then + vim.api.nvim_win_set_buf(win_id, alt_buf) + return + end + + -- Try using previous buffer + vim.cmd('bprevious') + if cur_buf ~= vim.api.nvim_win_get_buf(win_id) then return end + + -- Create new listed scratch buffer + local new_buf = vim.api.nvim_create_buf(true, true) + vim.api.nvim_win_set_buf(win_id, new_buf) + end) + + return true +end + +-- Helper data ================================================================ +-- Module default config +H.default_config = MiniBufremove.config + +-- Helper functionality ======================================================= +-- Settings ------------------------------------------------------------------- +H.setup_config = function(config) + -- General idea: if some table elements are not present in user-supplied + -- `config`, take them from default config + vim.validate({ config = { config, 'table', true } }) + config = vim.tbl_deep_extend('force', H.default_config, config or {}) + + vim.validate({ set_vim_settings = { config.set_vim_settings, 'boolean' } }) + + return config +end + +H.apply_config = function(config) + MiniBufremove.config = config + + if config.set_vim_settings then + vim.o.hidden = true -- Allow hidden buffers + end +end + +H.is_disabled = function() return vim.g.minibufremove_disable == true or vim.b.minibufremove_disable == true end + +-- Removing implementation ---------------------------------------------------- +H.unshow_and_cmd = function(buf_id, force, cmd) + buf_id = H.normalize_buf_id(buf_id) + if not H.is_valid_id(buf_id, 'buffer') then + H.message(buf_id .. ' is not a valid buffer id.') + return false + end + + if force == nil then force = false end + if type(force) ~= 'boolean' then + H.message('`force` should be boolean.') + return false + end + + local fun_name = ({ ['bdelete'] = 'delete', ['bwipeout'] = 'wipeout' })[cmd] + if not H.can_remove(buf_id, force, fun_name) then return false end + + -- Unshow buffer from all windows + MiniBufremove.unshow(buf_id) + + -- Execute command + local command = string.format('%s%s %d', cmd, force and '!' or '', buf_id) + -- Use `pcall` here to take care of case where `unshow()` was enough. This + -- can happen with 'bufhidden' option values: + -- - If `delete` then `unshow()` already `bdelete`d buffer. Without `pcall` + -- it gives E516 for `MiniBufremove.delete()` (`wipeout` works). + -- - If `wipe` then `unshow()` already `bwipeout`ed buffer. Without `pcall` + -- it gives E517 for module's `wipeout()` (still E516 for `delete()`). + local ok, result = pcall(vim.cmd, command) + if not (ok or result:find('E516') or result:find('E517')) then + H.message(result) + return false + end + + return true +end + +-- Utilities ------------------------------------------------------------------ +H.is_valid_id = function(x, type) + local is_valid = false + if type == 'buffer' then + is_valid = vim.api.nvim_buf_is_valid(x) + elseif type == 'window' then + is_valid = vim.api.nvim_win_is_valid(x) + end + + if not is_valid then H.message(string.format('%s is not a valid %s id.', tostring(x), type)) end + return is_valid +end + +-- Check if buffer can be removed with `MiniBufremove.fun_name` function +H.can_remove = function(buf_id, force, fun_name) + if force then return true end + + if vim.api.nvim_buf_get_option(buf_id, 'modified') then + H.message( + string.format( + 'Buffer %d has unsaved changes. Use `MiniBufremove.%s(%d, true)` to force.', + buf_id, + fun_name, + buf_id + ) + ) + return false + end + return true +end + +-- Compute 'true' buffer id (strictly positive integer). Treat `nil` and 0 as +-- current buffer. +H.normalize_buf_id = function(buf_id) + if buf_id == nil or buf_id == 0 then return vim.api.nvim_get_current_buf() end + return buf_id +end + +H.message = function(msg) vim.cmd('echomsg ' .. vim.inspect('(mini.bufremove) ' .. msg)) end + +return MiniBufremove diff --git a/dotfiles/pack/plugins/start/mini.nvim/lua/mini/comment.lua b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/comment.lua new file mode 100755 index 0000000..acffd7d --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/comment.lua @@ -0,0 +1,398 @@ +-- MIT License Copyright (c) 2021 Evgeni Chasnovski + +-- Documentation ============================================================== +--- 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. +---@tag mini.comment +---@tag MiniComment + +-- Module definition ========================================================== +local MiniComment = {} +local H = {} + +--- Module setup +--- +---@param config table Module config table. See |MiniComment.config|. +--- +---@usage `require('mini.comment').setup({})` (replace `{}` with your `config` table) +MiniComment.setup = function(config) + -- Export module + _G.MiniComment = MiniComment + + -- Setup config + config = H.setup_config(config) + + -- Apply config + H.apply_config(config) +end + +--- Module config +--- +--- Default values: +---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section) +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, + }, +} +--minidoc_afterlines_end + +-- Module functionality ======================================================= +--- Main function to be mapped +--- +--- It is meant to be used in expression mappings (see |map-|) 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. +--- +---@param mode string Optional string with 'operatorfunc' mode (see |g@|). +--- +---@return string 'g@' if called without argument, '' otherwise (but after +--- performing action). +MiniComment.operator = function(mode) + if H.is_disabled() then return '' end + + -- If used without arguments inside expression mapping: + -- - Set itself as `operatorfunc` to be called later to perform action. + -- - Return 'g@' which will then be executed resulting into waiting for a + -- motion or text object. This textobject will then be recorded using `'[` + -- and `']` marks. After that, `operatorfunc` is called with `mode` equal + -- to one of "line", "char", or "block". + -- NOTE: setting `operatorfunc` inside this function enables usage of 'count' + -- like `10gc_` toggles comments of 10 lines below (starting with current). + if mode == nil then + vim.cmd('set operatorfunc=v:lua.MiniComment.operator') + return 'g@' + end + + -- If called with non-nil `mode`, get target region and perform comment + -- toggling over it. + local mark_left, mark_right = '[', ']' + if mode == 'visual' then + mark_left, mark_right = '<', '>' + end + + local line_left, col_left = unpack(vim.api.nvim_buf_get_mark(0, mark_left)) + local line_right, col_right = unpack(vim.api.nvim_buf_get_mark(0, mark_right)) + + -- Do nothing if "left" mark is not on the left (earlier in text) of "right" + -- mark (indicating that there is nothing to do, like in comment textobject). + if (line_left > line_right) or (line_left == line_right and col_left > col_right) then return end + + -- Using `vim.cmd()` wrapper to allow usage of `lockmarks` command, because + -- raw execution will delete marks inside region (due to + -- `vim.api.nvim_buf_set_lines()`). + vim.cmd(string.format('lockmarks lua MiniComment.toggle_lines(%d, %d)', line_left, line_right)) + return '' +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. +--- +---@param line_start number Start line number (inclusive from 1 to number of lines). +---@param line_end number End line number (inclusive from 1 to number of lines). +MiniComment.toggle_lines = function(line_start, line_end) + if H.is_disabled() then return end + local n_lines = vim.api.nvim_buf_line_count(0) + if not (1 <= line_start and line_start <= n_lines and 1 <= line_end and line_end <= n_lines) then + error(('(mini.comment) `line_start` and `line_end` should be within range [1; %s].'):format(n_lines)) + end + if not (line_start <= line_end) then + error('(mini.comment) `line_start` should be less than or equal to `line_end`.') + end + + local config = H.get_config() + if config.hooks.pre() == false then return end + + local comment_parts = H.make_comment_parts() + local lines = vim.api.nvim_buf_get_lines(0, line_start - 1, line_end, false) + local indent, is_comment = H.get_lines_info(lines, comment_parts) + + local f + if is_comment then + f = H.make_uncomment_function(comment_parts) + else + f = H.make_comment_function(comment_parts, indent) + end + + for n, l in pairs(lines) do + lines[n] = f(l) + end + + -- NOTE: This function call removes marks inside written range. To write + -- lines in a way that saves marks, use one of: + -- - `lockmarks` command when doing mapping (current approach). + -- - `vim.fn.setline(line_start, lines)`, but this is **considerably** + -- slower: on 10000 lines 280ms compared to 40ms currently. + vim.api.nvim_buf_set_lines(0, line_start - 1, line_end, false, lines) + + if config.hooks.post() == false then return end +end + +--- 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. +MiniComment.textobject = function() + if H.is_disabled() then return end + + local config = H.get_config() + if config.hooks.pre() == false then return end + + local comment_parts = H.make_comment_parts() + local comment_check = H.make_comment_check(comment_parts) + local line_cur = vim.api.nvim_win_get_cursor(0)[1] + + if comment_check(vim.fn.getline(line_cur)) then + local line_start = line_cur + while (line_start >= 2) and comment_check(vim.fn.getline(line_start - 1)) do + line_start = line_start - 1 + end + + local line_end = line_cur + local n_lines = vim.api.nvim_buf_line_count(0) + while (line_end <= n_lines - 1) and comment_check(vim.fn.getline(line_end + 1)) do + line_end = line_end + 1 + end + + -- This visual selection doesn't seem to change `'<` and `'>` marks when + -- executed as `onoremap` mapping + vim.cmd(string.format('normal! %dGV%dG', line_start, line_end)) + end + + if config.hooks.post() == false then return end +end + +-- Helper data ================================================================ +-- Module default config +H.default_config = MiniComment.config + +-- Helper functionality ======================================================= +-- Settings ------------------------------------------------------------------- +H.setup_config = function(config) + -- General idea: if some table elements are not present in user-supplied + -- `config`, take them from default config + vim.validate({ config = { config, 'table', true } }) + config = vim.tbl_deep_extend('force', H.default_config, config or {}) + + -- Validate per nesting level to produce correct error message + vim.validate({ + mappings = { config.mappings, 'table' }, + hooks = { config.hooks, 'table' }, + }) + + vim.validate({ + ['mappings.comment'] = { config.mappings.comment, 'string' }, + ['mappings.comment_line'] = { config.mappings.comment_line, 'string' }, + ['mappings.textobject'] = { config.mappings.textobject, 'string' }, + ['hooks.pre'] = { config.hooks.pre, 'function' }, + ['hooks.post'] = { config.hooks.post, 'function' }, + }) + + return config +end + +H.apply_config = function(config) + MiniComment.config = config + + -- Make mappings + H.map('n', config.mappings.comment, 'v:lua.MiniComment.operator()', { expr = true, desc = 'Comment' }) + H.map( + 'x', + config.mappings.comment, + -- Using `:` instead of `` as latter results into executing before + -- proper update of `'<` and `'>` marks which is needed to work correctly. + [[:lua MiniComment.operator('visual')]], + { desc = 'Comment selection' } + ) + H.map('n', config.mappings.comment_line, 'v:lua.MiniComment.operator() . "_"', { expr = true, desc = 'Comment line' }) + H.map('o', config.mappings.textobject, 'lua MiniComment.textobject()', { desc = 'Comment textobject' }) +end + +H.is_disabled = function() return vim.g.minicomment_disable == true or vim.b.minicomment_disable == true end + +H.get_config = function(config) + return vim.tbl_deep_extend('force', MiniComment.config, vim.b.minicomment_config or {}, config or {}) +end + +-- Core implementations ------------------------------------------------------- +H.make_comment_parts = function() + local cs = vim.api.nvim_buf_get_option(0, 'commentstring') + + if cs == '' then + H.message([[(mini.comment) Option 'commentstring' is empty.]]) + return { left = '', right = '' } + end + + -- Assumed structure of 'commentstring': + -- <'%s'> + -- So this extracts parts without surrounding whitespace + local left, right = cs:match('^%s*(.-)%s*%%s%s*(.-)%s*$') + return { left = left, right = right } +end + +H.make_comment_check = function(comment_parts) + local l, r = comment_parts.left, comment_parts.right + -- String is commented if it has structure: + -- + local regex = string.format('^%%s-%s.*%s%%s-$', vim.pesc(l), vim.pesc(r)) + + return function(line) return line:find(regex) ~= nil end +end + +H.get_lines_info = function(lines, comment_parts) + local n_indent, n_indent_cur = math.huge, math.huge + local indent, indent_cur + + local is_comment = true + local comment_check = H.make_comment_check(comment_parts) + + for _, l in pairs(lines) do + -- Update lines indent: minimum of all indents except empty lines + if n_indent > 0 then + _, n_indent_cur, indent_cur = l:find('^(%s*)') + -- Condition "current n-indent equals line length" detects empty line + if (n_indent_cur < n_indent) and (n_indent_cur < l:len()) then + -- NOTE: Copy of actual indent instead of recreating it with `n_indent` + -- allows to handle both tabs and spaces + n_indent = n_indent_cur + indent = indent_cur + end + end + + -- Update comment info: lines are comment if every single line is comment + if is_comment then is_comment = comment_check(l) end + end + + -- `indent` can still be `nil` in case all `lines` are empty + return indent or '', is_comment +end + +H.make_comment_function = function(comment_parts, indent) + -- NOTE: this assumes that indent doesn't mix tabs with spaces + local nonindent_start = indent:len() + 1 + + local l, r = comment_parts.left, comment_parts.right + local lpad = (l == '') and '' or ' ' + local rpad = (r == '') and '' or ' ' + + local empty_comment = indent .. l .. r + -- Escape literal '%' symbols in comment parts (like in LaTeX) to be '%%' + -- because they have special meaning in `string.format` input. NOTE: don't + -- use `vim.pesc()` here because it also escapes other special characters + -- (like '-', '*', etc.). + local nonempty_format = indent .. l:gsub('%%', '%%%%') .. lpad .. '%s' .. rpad .. r:gsub('%%', '%%%%') + + return function(line) + -- Line is empty if it doesn't have anything except whitespace + if line:find('^%s*$') ~= nil then + -- If doesn't want to comment empty lines, return `line` here + return empty_comment + else + return string.format(nonempty_format, line:sub(nonindent_start)) + end + end +end + +H.make_uncomment_function = function(comment_parts) + local l, r = comment_parts.left, comment_parts.right + local lpad = (l == '') and '' or '[ ]?' + local rpad = (r == '') and '' or '[ ]?' + + -- Usage of `lpad` and `rpad` as possbile single space enables uncommenting + -- of commented empty lines without trailing whitespace (like ' #'). + local uncomment_regex = string.format('^(%%s*)%s%s(.-)%s%s%%s-$', vim.pesc(l), lpad, rpad, vim.pesc(r)) + + return function(line) + local indent, new_line = string.match(line, uncomment_regex) + -- Return original if line is not commented + if new_line == nil then return line end + -- Remove indent if line is a commented empty line + if new_line == '' then indent = '' end + return ('%s%s'):format(indent, new_line) + end +end + +-- Utilities ------------------------------------------------------------------ +H.map = function(mode, key, rhs, opts) + if key == '' then return end + + opts = vim.tbl_deep_extend('force', { noremap = true, silent = true }, opts or {}) + + -- Use mapping description only in Neovim>=0.7 + if vim.fn.has('nvim-0.7') == 0 then opts.desc = nil end + + vim.api.nvim_set_keymap(mode, key, rhs, opts) +end + +H.message = function(msg) vim.cmd('echomsg ' .. vim.inspect('(mini.comment) ' .. msg)) end + +return MiniComment diff --git a/dotfiles/pack/plugins/start/mini.nvim/lua/mini/completion.lua b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/completion.lua new file mode 100755 index 0000000..3b43501 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/completion.lua @@ -0,0 +1,1361 @@ +-- MIT License Copyright (c) 2021 Evgeni Chasnovski + +-- Documentation ============================================================== +--- 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 +--- ``) or fallback completion via +--- |MiniCompletion.complete_fallback()| (maped to ``). +--- +--- What it doesn't do: +--- - Snippet expansion. +--- - Many configurable sources. +--- - Automatic mapping of ``, ``, 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 `` and `` for navigation through completion list, make +--- these key mappings: +--- `vim.api.nvim_set_keymap('i', '', [[pumvisible() ? "\" : "\"]], { noremap = true, expr = true })` +--- `vim.api.nvim_set_keymap('i', '', [[pumvisible() ? "\" : "\"]], { noremap = true, expr = true })` +--- +--- To get more consistent behavior of ``, you can use this template in +--- your 'init.lua' to make customized mapping: > +--- local keys = { +--- ['cr'] = vim.api.nvim_replace_termcodes('', true, true, true), +--- ['ctrl-y'] = vim.api.nvim_replace_termcodes('', true, true, true), +--- ['ctrl-y_cr'] = vim.api.nvim_replace_termcodes('', 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 ``. 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', '', '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. +---@tag mini.completion +---@tag MiniCompletion + +-- Overall implementation design: +-- - Completion: +-- - On `InsertCharPre` event try to start auto completion. If needed, +-- start timer which after delay will start completion process. Stop this +-- timer if it is not needed. +-- - When timer is activated, first execute LSP source (if set up and there +-- is an active LSP client) by calling built-in complete function +-- (`completefunc` or `omnifunc`) which tries LSP completion by +-- asynchronously sending LSP 'textDocument/completion' request to all +-- LSP clients. When all are done, execute callback which processes +-- results, stores them in LSP cache and reruns built-in complete +-- function which produces completion popup. +-- - If previous step didn't result into any completion, execute (in Insert +-- mode and if no popup) fallback action. +-- - Documentation: +-- - On `CompleteChanged` start auto info with similar to completion timer +-- pattern. +-- - If timer is activated, try these sources of item info: +-- - 'info' field of completion item (see `:h complete-items`). +-- - 'documentation' field of LSP's previously returned result. +-- - 'documentation' field in result of asynchronous +-- 'completeItem/resolve' LSP request. +-- - If info doesn't consist only from whitespace, show floating window +-- with its content. Its dimensions and position are computed based on +-- current state of Neovim's data and content itself (which will be +-- displayed wrapped with `linebreak` option). +-- - Signature help (similar to item info): +-- - On `CursorMovedI` start auto signature (if there is any active LSP +-- client) with similar to completion timer pattern. Better event might +-- be `InsertCharPre` but there are issues with 'autopair-type' plugins. +-- - Check if character left to cursor is appropriate (')' or LSP's +-- signature help trigger characters). If not, do nothing. +-- - If timer is activated, send 'textDocument/signatureHelp' request to +-- all LSP clients. On callback, process their results. Window is opened +-- if not already with the same text (its characteristics are computed +-- similar to item info). For every LSP client it shows only active +-- signature (in case there are many). If LSP response has data about +-- active parameter, it is highlighted with +-- `MiniCompletionActiveParameter` highlight group. + +-- Module definition ========================================================== +local MiniCompletion = {} +local H = {} + +--- Module setup +--- +---@param config table Module config table. See |MiniCompletion.config|. +--- +---@usage `require('mini.completion').setup({})` (replace `{}` with your `config` table) +MiniCompletion.setup = function(config) + -- Export module + _G.MiniCompletion = MiniCompletion + + -- Setup config + config = H.setup_config(config) + + -- Apply config + H.apply_config(config) + + -- Setup module behavior + vim.api.nvim_exec( + [[augroup MiniCompletion + au! + au InsertCharPre * lua MiniCompletion.auto_completion() + au CompleteChanged * lua MiniCompletion.auto_info() + au CursorMovedI * lua MiniCompletion.auto_signature() + au InsertLeavePre * lua MiniCompletion.stop() + au CompleteDonePre * lua MiniCompletion.on_completedonepre() + au TextChangedI * lua MiniCompletion.on_text_changed_i() + au TextChangedP * lua MiniCompletion.on_text_changed_p() + + au FileType TelescopePrompt let b:minicompletion_disable=v:true + augroup END]], + false + ) + + if config.lsp_completion.auto_setup then + local command = string.format( + [[augroup MiniCompletion + au BufEnter * setlocal %s=v:lua.MiniCompletion.completefunc_lsp + augroup END]], + config.lsp_completion.source_func + ) + vim.api.nvim_exec(command, false) + end + + -- Create highlighting + vim.api.nvim_exec('hi default MiniCompletionActiveParameter term=underline cterm=underline gui=underline', false) +end + +--- Module config +--- +--- Default values: +---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section) +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()`. + --minidoc_replace_start process_items = --, + process_items = function(items, base) + local res = vim.tbl_filter(function(item) + -- Keep items which match the base and are not snippets + return vim.startswith(H.get_completion_word(item), base) and item.kind ~= 15 + end, items) + + table.sort(res, function(a, b) return (a.sortText or a.label) < (b.sortText or b.label) end) + + return res + end, + --minidoc_replace_end + }, + + -- 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 ''. + --minidoc_replace_start fallback_action = --` completion>, + fallback_action = function() vim.api.nvim_feedkeys(H.keys.ctrl_n, 'n', false) end, + --minidoc_replace_end + + -- Module mappings. Use `''` (empty string) to disable one. Some of them + -- might conflict with system mappings. + mappings = { + force_twostep = '', -- Force two-step completion + force_fallback = '', -- Force fallback completion + }, + + -- Whether to set Vim's settings for better experience (modifies + -- `shortmess` and `completeopt`) + set_vim_settings = true, +} +--minidoc_afterlines_end + +-- Module functionality ======================================================= +--- Auto completion +--- +--- Designed to be used with |autocmd|. No need to use it directly, everything +--- is setup in |MiniCompletion.setup|. +MiniCompletion.auto_completion = function() + if H.is_disabled() then return end + + H.completion.timer:stop() + + -- Don't do anything if popup is visible + if H.pumvisible() then + -- Keep completion source as it is needed all time when popup is visible + H.stop_completion(true) + return + end + + -- Stop everything if inserted character is not appropriate + local char_is_trigger = H.is_lsp_trigger(vim.v.char, 'completion') + if not (H.is_char_keyword(vim.v.char) or char_is_trigger) then + H.stop_completion(false) + return + end + + -- If character is purely lsp trigger, make new LSP request without fallback + -- and force new completion + if char_is_trigger then H.cancel_lsp() end + H.completion.fallback, H.completion.force = not char_is_trigger, char_is_trigger + + -- Cache id of Insert mode "text changed" event for a later tracking (reduces + -- false positive delayed triggers). The intention is to trigger completion + -- after the delay only if text wasn't changed during waiting. Using only + -- `InsertCharPre` is not enough though, as not every Insert mode change + -- triggers `InsertCharPre` event (notable example - hitting ``). + -- Also, using `+ 1` here because it is a `Pre` event and needs to cache + -- after inserting character. + H.completion.text_changed_id = H.text_changed_id + 1 + + -- If completion was requested after 'lsp' source exhausted itself (there + -- were matches on typing start, but they disappeared during filtering), call + -- fallback immediately. + if H.completion.source == 'lsp' then + H.trigger_fallback() + return + end + + -- Using delay (of debounce type) actually improves user experience + -- as it allows fast typing without many popups. + H.completion.timer:start(H.get_config().delay.completion, 0, vim.schedule_wrap(H.trigger_twostep)) +end + +--- Run two-stage completion +--- +---@param fallback boolean Whether to use fallback completion. +---@param force boolean Whether to force update of completion popup. +MiniCompletion.complete_twostage = function(fallback, force) + if H.is_disabled() then return end + + H.stop_completion() + H.completion.fallback, H.completion.force = fallback or true, force or true + H.trigger_twostep() +end + +--- Run fallback completion +MiniCompletion.complete_fallback = function() + if H.is_disabled() then return end + + H.stop_completion() + H.completion.fallback, H.completion.force = true, true + H.trigger_fallback() +end + +--- Auto completion entry information +--- +--- Designed to be used with |autocmd|. No need to use it directly, everything +--- is setup in |MiniCompletion.setup|. +MiniCompletion.auto_info = function() + if H.is_disabled() then return end + + H.info.timer:stop() + + -- Defer execution because of textlock during `CompleteChanged` event + -- Don't stop timer when closing info window because it is needed + vim.defer_fn(function() H.close_action_window(H.info, true) end, 0) + + -- Stop current LSP request that tries to get not current data + H.cancel_lsp({ H.info }) + + -- Update metadata before leaving to register a `CompleteChanged` event + H.info.event = vim.v.event + H.info.id = H.info.id + 1 + + -- Don't even try to show info if nothing is selected in popup + if vim.tbl_isempty(H.info.event.completed_item) then return end + + H.info.timer:start(H.get_config().delay.info, 0, vim.schedule_wrap(H.show_info_window)) +end + +--- Auto function signature +--- +--- Designed to be used with |autocmd|. No need to use it directly, everything +--- is setup in |MiniCompletion.setup|. +MiniCompletion.auto_signature = function() + if H.is_disabled() then return end + + H.signature.timer:stop() + if not H.has_lsp_clients('signatureHelpProvider') then return end + + local left_char = H.get_left_char() + local char_is_trigger = left_char == ')' or H.is_lsp_trigger(left_char, 'signature') + if not char_is_trigger then return end + + H.signature.timer:start(H.get_config().delay.signature, 0, vim.schedule_wrap(H.show_signature_window)) +end + +--- 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|. +--- +---@param actions table Array containing any of 'completion', 'info', or +--- 'signature' string. +MiniCompletion.stop = function(actions) + actions = actions or { 'completion', 'info', 'signature' } + for _, n in pairs(actions) do + H.stop_actions[n]() + end +end + +MiniCompletion.on_completedonepre = function() + -- Try to apply additional text edits + H.apply_additional_text_edits() + + -- Stop processes + MiniCompletion.stop({ 'completion', 'info' }) +end + +--- Act on every |TextChangedI| +MiniCompletion.on_text_changed_i = function() + -- Track Insert mode changes + H.text_changed_id = H.text_changed_id + 1 + + -- Stop 'info' processes in case no completion event is triggered but popup + -- is not visible. See https://github.com/neovim/neovim/issues/15077 + H.stop_info() +end + +--- Act on every |TextChangedP| +MiniCompletion.on_text_changed_p = function() + -- Track Insert mode changes + H.text_changed_id = H.text_changed_id + 1 +end + +--- 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.completefunc_lsp = function(findstart, base) + -- Early return + if not H.has_lsp_clients('completionProvider') or H.completion.lsp.status == 'sent' then + if findstart == 1 then + return -3 + else + return {} + end + end + + -- NOTE: having code for request inside this function enables its use + -- directly with `<...>`. + if H.completion.lsp.status ~= 'received' then + local current_id = H.completion.lsp.id + 1 + H.completion.lsp.id = current_id + H.completion.lsp.status = 'sent' + + local bufnr = vim.api.nvim_get_current_buf() + local params = vim.lsp.util.make_position_params() + + -- NOTE: it is CRUCIAL to make LSP request on the first call to + -- 'complete-function' (as in Vim's help). This is due to the fact that + -- cursor line and position are different on the first and second calls to + -- 'complete-function'. For example, when calling this function at the end + -- of the line ' he', cursor position on the first call will be + -- (, 4) and line will be ' he' but on the second call - + -- (, 2) and ' ' (because 2 is a column of completion start). + -- This request is executed only on second call because it returns `-3` on + -- first call (which means cancel and leave completion mode). + -- NOTE: using `buf_request_all()` (instead of `buf_request()`) to easily + -- handle possible fallback and to have all completion suggestions be + -- filtered with one `base` in the other route of this function. Anyway, + -- the most common situation is with one attached LSP client. + local cancel_fun = vim.lsp.buf_request_all(bufnr, 'textDocument/completion', params, function(result) + if not H.is_lsp_current(H.completion, current_id) then return end + + H.completion.lsp.status = 'received' + H.completion.lsp.result = result + + -- Trigger LSP completion to take 'received' route + H.trigger_lsp() + end) + + -- Cache cancel function to disable requests when they are not needed + H.completion.lsp.cancel_fun = cancel_fun + + -- End completion and wait for LSP callback + if findstart == 1 then + return -3 + else + return {} + end + else + if findstart == 1 then return H.get_completion_start() end + + local config = H.get_config() + + local words = H.process_lsp_response(H.completion.lsp.result, function(response, client_id) + -- Response can be `CompletionList` with 'items' field or `CompletionItem[]` + local items = H.table_get(response, { 'items' }) or response + if type(items) ~= 'table' then return {} end + items = config.lsp_completion.process_items(items, base) + return H.lsp_completion_response_items_to_complete_items(items, client_id) + end) + + H.completion.lsp.status = 'done' + + -- Maybe trigger fallback action + if vim.tbl_isempty(words) and H.completion.fallback then + H.trigger_fallback() + return + end + + -- Track from which source is current popup + H.completion.source = 'lsp' + return words + end +end + +--- Default `MiniCompletion.config.lsp_completion.process_items` +MiniCompletion.default_process_items = + function(items, base) return H.default_config.lsp_completion.process_items(items, base) end + +-- Helper data ================================================================ +-- Module default config +H.default_config = MiniCompletion.config + +-- Track Insert mode changes +H.text_changed_id = 0 + +-- Namespace for highlighting +H.ns_id = vim.api.nvim_create_namespace('MiniCompletion') + +-- Commonly used key sequences +H.keys = { + completefunc = vim.api.nvim_replace_termcodes('', true, false, true), + omnifunc = vim.api.nvim_replace_termcodes('', true, false, true), + ctrl_n = vim.api.nvim_replace_termcodes('', true, false, true), +} + +-- Caches for different actions ----------------------------------------------- +-- Field `lsp` is a table describing state of all used LSP requests. It has the +-- following structure: +-- - id: identifier (consecutive numbers). +-- - status: status. One of 'sent', 'received', 'done', 'canceled'. +-- - result: result of request. +-- - cancel_fun: function which cancels current request. + +-- Cache for completion +H.completion = { + fallback = true, + force = false, + source = nil, + text_changed_id = 0, + timer = vim.loop.new_timer(), + lsp = { id = 0, status = nil, result = nil, cancel_fun = nil }, +} + +-- Cache for completion item info +H.info = { + bufnr = nil, + event = nil, + id = 0, + timer = vim.loop.new_timer(), + winnr = nil, + lsp = { id = 0, status = nil, result = nil, cancel_fun = nil }, +} + +-- Cache for signature help +H.signature = { + bufnr = nil, + text = nil, + timer = vim.loop.new_timer(), + winnr = nil, + lsp = { id = 0, status = nil, result = nil, cancel_fun = nil }, +} + +-- Helper functionality ======================================================= +-- Settings ------------------------------------------------------------------- +H.setup_config = function(config) + -- General idea: if some table elements are not present in user-supplied + -- `config`, take them from default config + vim.validate({ config = { config, 'table', true } }) + config = vim.tbl_deep_extend('force', H.default_config, config or {}) + + -- Validate per nesting level to produce correct error message + vim.validate({ + delay = { config.delay, 'table' }, + window_dimensions = { config.window_dimensions, 'table' }, + lsp_completion = { config.lsp_completion, 'table' }, + fallback_action = { + config.fallback_action, + function(x) return type(x) == 'function' or type(x) == 'string' end, + 'function or string', + }, + mappings = { config.mappings, 'table' }, + set_vim_settings = { config.set_vim_settings, 'boolean' }, + }) + + vim.validate({ + ['delay.completion'] = { config.delay.completion, 'number' }, + ['delay.info'] = { config.delay.info, 'number' }, + ['delay.signature'] = { config.delay.signature, 'number' }, + + ['window_dimensions.info'] = { config.window_dimensions.info, 'table' }, + ['window_dimensions.signature'] = { config.window_dimensions.signature, 'table' }, + + ['lsp_completion.source_func'] = { + config.lsp_completion.source_func, + function(x) return x == 'completefunc' or x == 'omnifunc' end, + 'one of strings: "completefunc" or "omnifunc"', + }, + ['lsp_completion.auto_setup'] = { config.lsp_completion.auto_setup, 'boolean' }, + ['lsp_completion.process_items'] = { config.lsp_completion.process_items, 'function' }, + + ['mappings.force_twostep'] = { config.mappings.force_twostep, 'string' }, + ['mappings.force_fallback'] = { config.mappings.force_fallback, 'string' }, + }) + + vim.validate({ + ['window_dimensions.info.height'] = { config.window_dimensions.info.height, 'number' }, + ['window_dimensions.info.width'] = { config.window_dimensions.info.width, 'number' }, + ['window_dimensions.signature.height'] = { config.window_dimensions.signature.height, 'number' }, + ['window_dimensions.signature.width'] = { config.window_dimensions.signature.width, 'number' }, + }) + + return config +end + +H.apply_config = function(config) + MiniCompletion.config = config + + --stylua: ignore start + H.map('i', config.mappings.force_twostep, 'lua MiniCompletion.complete_twostage()', { desc = 'Complete with two-stage' }) + H.map('i', config.mappings.force_fallback, 'lua MiniCompletion.complete_fallback()', { desc = 'Complete with fallback' }) + --stylua: ignore end + + if config.set_vim_settings then + -- Don't give ins-completion-menu messages + vim.cmd('set shortmess+=c') + -- More common completion behavior + vim.cmd('set completeopt=menuone,noinsert,noselect') + end +end + +H.is_disabled = function() return vim.g.minicompletion_disable == true or vim.b.minicompletion_disable == true end + +H.get_config = function(config) + return vim.tbl_deep_extend('force', MiniCompletion.config, vim.b.minicompletion_config or {}, config or {}) +end + +-- Completion triggers -------------------------------------------------------- +H.trigger_twostep = function() + -- Trigger only in Insert mode and if text didn't change after trigger + -- request, unless completion is forced + -- NOTE: check for `text_changed_id` equality is still not 100% solution as + -- there are cases when, for example, `` is hit just before this check. + -- Because of asynchronous id update and this function call (called after + -- delay), these still match. + local allow_trigger = (vim.fn.mode() == 'i') + and (H.completion.force or (H.completion.text_changed_id == H.text_changed_id)) + if not allow_trigger then return end + + if H.has_lsp_clients('completionProvider') and H.has_lsp_completion() then + H.trigger_lsp() + elseif H.completion.fallback then + H.trigger_fallback() + end +end + +H.trigger_lsp = function() + -- Check for popup visibility is needed to reduce flickering. + -- Possible issue timeline (with 100ms delay with set up LSP): + -- 0ms: Key is pressed. + -- 100ms: LSP is triggered from first key press. + -- 110ms: Another key is pressed. + -- 200ms: LSP callback is processed, triggers complete-function which + -- processes "received" LSP request. + -- 201ms: LSP request is processed, completion is (should be almost + -- immediately) provided, request is marked as "done". + -- 210ms: LSP is triggered from second key press. As previous request is + -- "done", it will once make whole LSP request. Having check for visible + -- popup should prevent here the call to complete-function. + + -- When `force` is `true` then presence of popup shouldn't matter. + local no_popup = H.completion.force or (not H.pumvisible()) + if no_popup and vim.fn.mode() == 'i' then + local key = H.keys[H.get_config().lsp_completion.source_func] + vim.api.nvim_feedkeys(key, 'n', false) + end +end + +H.trigger_fallback = function() + local no_popup = H.completion.force or (not H.pumvisible()) + if no_popup and vim.fn.mode() == 'i' then + -- Track from which source is current popup + H.completion.source = 'fallback' + local config = H.get_config() + if type(config.fallback_action) == 'string' then + -- Having `` also (for some mysterious reason) helps to avoid + -- some weird behavior. For example, if `keys = ''` then Neovim + -- starts new line when there is no suggestions. + local keys = string.format('%s', config.fallback_action) + local trigger_keys = vim.api.nvim_replace_termcodes(keys, true, false, true) + vim.api.nvim_feedkeys(trigger_keys, 'n', false) + else + config.fallback_action() + end + end +end + +-- Stop actions --------------------------------------------------------------- +H.stop_completion = function(keep_source) + H.completion.timer:stop() + H.cancel_lsp({ H.completion }) + H.completion.fallback, H.completion.force = true, false + if not keep_source then H.completion.source = nil end +end + +H.stop_info = function() + -- Id update is needed to notify that all previous work is not current + H.info.id = H.info.id + 1 + H.info.timer:stop() + H.cancel_lsp({ H.info }) + H.close_action_window(H.info) +end + +H.stop_signature = function() + H.signature.text = nil + H.signature.timer:stop() + H.cancel_lsp({ H.signature }) + H.close_action_window(H.signature) +end + +H.stop_actions = { + completion = H.stop_completion, + info = H.stop_info, + signature = H.stop_signature, +} + +-- LSP ------------------------------------------------------------------------ +---@param capability string|table|`nil` Server capability (possibly nested +--- supplied via table) to check. +--- +---@return boolean Whether at least one LSP client supports `capability`. +---@private +H.has_lsp_clients = function(capability) + local clients = vim.lsp.buf_get_clients() + if vim.tbl_isempty(clients) then return false end + if not capability then return true end + + for _, c in pairs(clients) do + local has_capability = H.table_get(c.server_capabilities, capability) + if has_capability then return true end + end + return false +end + +H.has_lsp_completion = function() + local func = vim.api.nvim_buf_get_option(0, H.get_config().lsp_completion.source_func) + return func == 'v:lua.MiniCompletion.completefunc_lsp' +end + +H.is_lsp_trigger = function(char, type) + local triggers + local providers = { + completion = 'completionProvider', + signature = 'signatureHelpProvider', + } + + for _, client in pairs(vim.lsp.buf_get_clients()) do + triggers = H.table_get(client, { 'server_capabilities', providers[type], 'triggerCharacters' }) + if vim.tbl_contains(triggers or {}, char) then return true end + end + return false +end + +H.cancel_lsp = function(caches) + caches = caches or { H.completion, H.info, H.signature } + for _, c in pairs(caches) do + if vim.tbl_contains({ 'sent', 'received' }, c.lsp.status) then + if c.lsp.cancel_fun then c.lsp.cancel_fun() end + c.lsp.status = 'canceled' + end + + c.lsp.result = nil + c.lsp.cancel_fun = nil + end +end + +H.process_lsp_response = function(request_result, processor) + if not request_result then return {} end + + local res = {} + for client_id, item in pairs(request_result) do + if not item.err and item.result then vim.list_extend(res, processor(item.result, client_id) or {}) end + end + + return res +end + +H.is_lsp_current = function(cache, id) return cache.lsp.id == id and cache.lsp.status == 'sent' end + +-- Completion ----------------------------------------------------------------- +-- This is a truncated version of +-- `vim.lsp.util.text_document_completion_list_to_complete_items` which does +-- not filter and sort items. +-- For extra information see 'Response' section: +-- https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_completion +H.lsp_completion_response_items_to_complete_items = function(items, client_id) + if vim.tbl_count(items) == 0 then return {} end + + local res = {} + local docs, info + for _, item in pairs(items) do + -- Documentation info + docs = item.documentation + info = H.table_get(docs, { 'value' }) + if not info and type(docs) == 'string' then info = docs end + info = info or '' + + table.insert(res, { + word = H.get_completion_word(item), + abbr = item.label, + kind = vim.lsp.protocol.CompletionItemKind[item.kind] or 'Unknown', + menu = item.detail or '', + info = info, + icase = 1, + dup = 1, + empty = 1, + user_data = { nvim = { lsp = { completion_item = item, client_id = client_id } } }, + }) + end + return res +end + +H.get_completion_word = function(item) + -- Completion word (textEdit.newText > insertText > label). This doesn't + -- support snippet expansion. + return H.table_get(item, { 'textEdit', 'newText' }) or item.insertText or item.label or '' +end + +H.apply_additional_text_edits = function() + -- Code originally.inspired by https://github.com/neovim/neovim/issues/12310 + + -- Try to get `additionalTextEdits`. First from 'completionItem/resolve'; + -- then - from selected item. The reason for this is inconsistency in how + -- servers provide `additionTextEdits`: on 'textDocument/completion' or + -- 'completionItem/resolve'. + local resolve_data = H.process_lsp_response(H.info.lsp.result, function(response, client_id) + -- Return nested table because this will be a second argument of + -- `vim.list_extend()` and the whole inner table is a target value here. + return { { edits = response.additionalTextEdits, client_id = client_id } } + end) + local edits, client_id + if #resolve_data >= 1 then + edits, client_id = resolve_data[1].edits, resolve_data[1].client_id + else + local lsp_data = H.table_get(vim.v.completed_item, { 'user_data', 'nvim', 'lsp' }) or {} + edits = H.table_get(lsp_data, { 'completion_item', 'additionalTextEdits' }) + client_id = lsp_data.client_id + end + + if edits == nil then return end + client_id = client_id or 0 + + -- Use extmark to track relevant cursor postion after text edits + local cur_pos = vim.api.nvim_win_get_cursor(0) + local extmark_id = vim.api.nvim_buf_set_extmark(0, H.ns_id, cur_pos[1] - 1, cur_pos[2], {}) + + local offset_encoding = vim.lsp.get_client_by_id(client_id).offset_encoding + vim.lsp.util.apply_text_edits(edits, vim.api.nvim_get_current_buf(), offset_encoding) + + local extmark_data = vim.api.nvim_buf_get_extmark_by_id(0, H.ns_id, extmark_id, {}) + pcall(vim.api.nvim_buf_del_extmark, 0, H.ns_id, extmark_id) + pcall(vim.api.nvim_win_set_cursor, 0, { extmark_data[1] + 1, extmark_data[2] }) +end + +-- Completion item info ------------------------------------------------------- +H.show_info_window = function() + local event = H.info.event + if not event then return end + + -- Try first to take lines from LSP request result. + local lines + if H.info.lsp.status == 'received' then + lines = H.process_lsp_response(H.info.lsp.result, function(response) + if not response.documentation then return {} end + local res = vim.lsp.util.convert_input_to_markdown_lines(response.documentation) + return vim.lsp.util.trim_empty_lines(res) + end) + + H.info.lsp.status = 'done' + else + lines = H.info_window_lines(H.info.id) + end + + -- Don't show anything if there is nothing to show + if not lines or H.is_whitespace(lines) then return end + + -- If not already, create a permanent buffer where info will be + -- displayed. For some reason, it is important to have it created not in + -- `setup()` because in that case there is a small flash (which is really a + -- brief open of window at screen top, focus on it, and its close) on the + -- first show of info window. + H.ensure_buffer(H.info, 'MiniCompletion:completion-item-info') + + -- Add `lines` to info buffer. Use `wrap_at` to have proper width of + -- 'non-UTF8' section separators. + vim.lsp.util.stylize_markdown(H.info.bufnr, lines, { wrap_at = H.get_config().window_dimensions.info.width }) + + -- Compute floating window options + local opts = H.info_window_options() + + -- Defer execution because of textlock during `CompleteChanged` event + vim.defer_fn(function() + -- Ensure that window doesn't open when it shouldn't be + if not (H.pumvisible() and vim.fn.mode() == 'i') then return end + H.open_action_window(H.info, opts) + end, 0) +end + +H.info_window_lines = function(info_id) + -- Try to use 'info' field of Neovim's completion item + local completed_item = H.table_get(H.info, { 'event', 'completed_item' }) or {} + local text = completed_item.info or '' + + if not H.is_whitespace(text) then + -- Use `` to be properly processed by `stylize_markdown()` + local lines = { '' } + vim.list_extend(lines, vim.split(text, '\n', false)) + table.insert(lines, '') + return lines + end + + -- If popup is not from LSP then there is nothing more to do + if H.completion.source ~= 'lsp' then return nil end + + -- Try to get documentation from LSP's initial completion result + local lsp_completion_item = H.table_get(completed_item, { 'user_data', 'nvim', 'lsp', 'completion_item' }) + -- If there is no LSP's completion item, then there is no point to proceed as + -- it should serve as parameters to LSP request + if not lsp_completion_item then return end + local doc = lsp_completion_item.documentation + if doc then + local lines = vim.lsp.util.convert_input_to_markdown_lines(doc) + return vim.lsp.util.trim_empty_lines(lines) + end + + -- Finally, try request to resolve current completion to add documentation + local bufnr = vim.api.nvim_get_current_buf() + local params = lsp_completion_item + + local current_id = H.info.lsp.id + 1 + H.info.lsp.id = current_id + H.info.lsp.status = 'sent' + + local cancel_fun = vim.lsp.buf_request_all(bufnr, 'completionItem/resolve', params, function(result) + -- Don't do anything if there is other LSP request in action + if not H.is_lsp_current(H.info, current_id) then return end + + H.info.lsp.status = 'received' + + -- Don't do anything if completion item was changed + if H.info.id ~= info_id then return end + + H.info.lsp.result = result + H.show_info_window() + end) + + H.info.lsp.cancel_fun = cancel_fun + + return nil +end + +H.info_window_options = function() + local config = H.get_config() + + -- Compute dimensions based on lines to be displayed + local lines = vim.api.nvim_buf_get_lines(H.info.bufnr, 0, -1, {}) + local info_height, info_width = + H.floating_dimensions(lines, config.window_dimensions.info.height, config.window_dimensions.info.width) + + -- Compute position + local event = H.info.event + local left_to_pum = event.col - 1 + local right_to_pum = event.col + event.width + (event.scrollbar and 1 or 0) + + local space_left, space_right = left_to_pum, vim.o.columns - right_to_pum + + local anchor, col, space + -- Decide side at which info window will be displayed + if info_width <= space_right or space_left <= space_right then + anchor, col, space = 'NW', right_to_pum, space_right + else + anchor, col, space = 'NE', left_to_pum, space_left + end + + -- Possibly adjust floating window dimensions to fit screen + if space < info_width then + info_height, info_width = H.floating_dimensions(lines, config.window_dimensions.info.height, space) + end + + return { + relative = 'editor', + anchor = anchor, + row = event.row, + col = col, + width = info_width, + height = info_height, + focusable = false, + style = 'minimal', + } +end + +-- Signature help ------------------------------------------------------------- +H.show_signature_window = function() + -- If there is no received LSP result, make request and exit + if H.signature.lsp.status ~= 'received' then + local current_id = H.signature.lsp.id + 1 + H.signature.lsp.id = current_id + H.signature.lsp.status = 'sent' + + local bufnr = vim.api.nvim_get_current_buf() + local params = vim.lsp.util.make_position_params() + + local cancel_fun = vim.lsp.buf_request_all(bufnr, 'textDocument/signatureHelp', params, function(result) + if not H.is_lsp_current(H.signature, current_id) then return end + + H.signature.lsp.status = 'received' + H.signature.lsp.result = result + + -- Trigger `show_signature` again to take 'received' route + H.show_signature_window() + end) + + -- Cache cancel function to disable requests when they are not needed + H.signature.lsp.cancel_fun = cancel_fun + + return + end + + -- Make lines to show in floating window + local lines, hl_ranges = H.signature_window_lines() + H.signature.lsp.status = 'done' + + -- Close window and exit if there is nothing to show + if not lines or H.is_whitespace(lines) then + H.close_action_window(H.signature) + return + end + + -- Make markdown code block + table.insert(lines, 1, '```' .. vim.bo.filetype) + table.insert(lines, '```') + + -- If not already, create a permanent buffer for signature + H.ensure_buffer(H.signature, 'MiniCompletion:signature-help') + + -- Add `lines` to signature buffer. Use `wrap_at` to have proper width of + -- 'non-UTF8' section separators. + vim.lsp.util.stylize_markdown( + H.signature.bufnr, + lines, + { wrap_at = H.get_config().window_dimensions.signature.width } + ) + + -- Add highlighting of active parameter + for i, hl_range in ipairs(hl_ranges) do + if not vim.tbl_isempty(hl_range) and hl_range.first and hl_range.last then + vim.api.nvim_buf_add_highlight( + H.signature.bufnr, + H.ns_id, + 'MiniCompletionActiveParameter', + i - 1, + hl_range.first, + hl_range.last + ) + end + end + + -- If window is already opened and displays the same text, don't reopen it + local cur_text = table.concat(lines, '\n') + if H.signature.winnr and cur_text == H.signature.text then return end + + -- Cache lines for later checks if window should be reopened + H.signature.text = cur_text + + -- Ensure window is closed + H.close_action_window(H.signature) + + -- Compute floating window options + local opts = H.signature_window_opts() + + -- Ensure that window doesn't open when it shouldn't + if vim.fn.mode() == 'i' then H.open_action_window(H.signature, opts) end +end + +H.signature_window_lines = function() + local signature_data = H.process_lsp_response(H.signature.lsp.result, H.process_signature_response) + -- Each line is a single-line active signature string from one attached LSP + -- client. Each highlight range is a table which indicates (if not empty) + -- what parameter to highlight for every LSP client's signature string. + local lines, hl_ranges = {}, {} + for _, t in pairs(signature_data) do + -- `t` is allowed to be an empty table (in which case nothing is added) or + -- a table with two entries. This ensures that `hl_range`'s integer index + -- points to an actual line in future buffer. + table.insert(lines, t.label) + table.insert(hl_ranges, t.hl_range) + end + + return lines, hl_ranges +end + +H.process_signature_response = function(response) + if not response.signatures or vim.tbl_isempty(response.signatures) then return {} end + + -- Get active signature (based on textDocument/signatureHelp specification) + local signature_id = response.activeSignature or 0 + -- This is according to specification: "If ... value lies outside ... + -- defaults to zero" + local n_signatures = vim.tbl_count(response.signatures or {}) + if signature_id < 0 or signature_id >= n_signatures then signature_id = 0 end + local signature = response.signatures[signature_id + 1] + + -- Get displayed signature label + local signature_label = signature.label + + -- Get start and end of active parameter (for highlighting) + local hl_range = {} + local n_params = vim.tbl_count(signature.parameters or {}) + local has_params = signature.parameters and n_params > 0 + + -- Take values in this order because data inside signature takes priority + local parameter_id = signature.activeParameter or response.activeParameter or 0 + local param_id_inrange = 0 <= parameter_id and parameter_id < n_params + + -- Computing active parameter only when parameter id is inside bounds is not + -- strictly based on specification, as currently (v3.16) it says to treat + -- out-of-bounds value as first parameter. However, some clients seems to use + -- those values to indicate that nothing needs to be highlighted. + -- Sources: + -- https://github.com/microsoft/pyright/pull/1876 + -- https://github.com/microsoft/language-server-protocol/issues/1271 + if has_params and param_id_inrange then + local param_label = signature.parameters[parameter_id + 1].label + + -- Compute highlight range based on type of supplied parameter label: can + -- be string label which should be a part of signature label or direct start + -- (inclusive) and end (exclusive) range values + local first, last = nil, nil + if type(param_label) == 'string' then + first, last = signature_label:find(vim.pesc(param_label)) + -- Make zero-indexed and end-exclusive + if first then + first, last = first - 1, last + end + elseif type(param_label) == 'table' then + first, last = unpack(param_label) + end + if first then hl_range = { first = first, last = last } end + end + + -- Return nested table because this will be a second argument of + -- `vim.list_extend()` and the whole inner table is a target value here. + return { { label = signature_label, hl_range = hl_range } } +end + +H.signature_window_opts = function() + local config = H.get_config() + local lines = vim.api.nvim_buf_get_lines(H.signature.bufnr, 0, -1, {}) + local height, width = + H.floating_dimensions(lines, config.window_dimensions.signature.height, config.window_dimensions.signature.width) + + -- Compute position + local win_line = vim.fn.winline() + local space_above, space_below = win_line - 1, vim.fn.winheight(0) - win_line + + local anchor, row, space + if height <= space_above or space_below <= space_above then + anchor, row, space = 'SW', 0, space_above + else + anchor, row, space = 'NW', 1, space_below + end + + -- Possibly adjust floating window dimensions to fit screen + if space < height then + height, width = H.floating_dimensions(lines, space, config.window_dimensions.signature.width) + end + + -- Get zero-indexed current cursor position + local bufpos = vim.api.nvim_win_get_cursor(0) + bufpos[1] = bufpos[1] - 1 + + return { + relative = 'win', + bufpos = bufpos, + anchor = anchor, + row = row, + col = 0, + width = width, + height = height, + focusable = false, + style = 'minimal', + } +end + +-- Helpers for floating windows ----------------------------------------------- +H.ensure_buffer = function(cache, name) + if cache.bufnr then return end + + cache.bufnr = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(cache.bufnr, name) + -- Make this buffer a scratch (can close without saving) + vim.fn.setbufvar(cache.bufnr, '&buftype', 'nofile') +end + +-- Returns tuple of height and width +H.floating_dimensions = function(lines, max_height, max_width) + -- Simulate how lines will look in window with `wrap` and `linebreak`. + -- This is not 100% accurate (mostly when multibyte characters are present + -- manifesting into empty space at bottom), but does the job + local lines_wrap = {} + for _, l in pairs(lines) do + vim.list_extend(lines_wrap, H.wrap_line(l, max_width)) + end + -- Height is a number of wrapped lines truncated to maximum height + local height = math.min(#lines_wrap, max_height) + + -- Width is a maximum width of the first `height` wrapped lines truncated to + -- maximum width + local width = 0 + local l_width + for i, l in ipairs(lines_wrap) do + -- Use `strdisplaywidth()` to account for 'non-UTF8' characters + l_width = vim.fn.strdisplaywidth(l) + if i <= height and width < l_width then width = l_width end + end + -- It should already be less that that because of wrapping, so this is "just + -- in case" + width = math.min(width, max_width) + + return height, width +end + +H.open_action_window = function(cache, opts) + cache.winnr = vim.api.nvim_open_win(cache.bufnr, false, opts) + vim.api.nvim_win_set_option(cache.winnr, 'wrap', true) + vim.api.nvim_win_set_option(cache.winnr, 'linebreak', true) + vim.api.nvim_win_set_option(cache.winnr, 'breakindent', false) +end + +H.close_action_window = function(cache, keep_timer) + if not keep_timer then cache.timer:stop() end + + if cache.winnr then vim.api.nvim_win_close(cache.winnr, true) end + cache.winnr = nil + + -- For some reason 'buftype' might be reset. Ensure that buffer is scratch. + if cache.bufnr then vim.fn.setbufvar(cache.bufnr, '&buftype', 'nofile') end +end + +-- Utilities ------------------------------------------------------------------ +H.is_char_keyword = function(char) + -- Using Vim's `match()` and `keyword` enables respecting Cyrillic letters + return vim.fn.match(char, '[[:keyword:]]') >= 0 +end + +H.pumvisible = function() return vim.fn.pumvisible() > 0 end + +H.get_completion_start = function() + -- Compute start position of latest keyword (as in `vim.lsp.omnifunc`) + local pos = vim.api.nvim_win_get_cursor(0) + local line = vim.api.nvim_get_current_line() + local line_to_cursor = line:sub(1, pos[2]) + return vim.fn.match(line_to_cursor, '\\k*$') +end + +H.is_whitespace = function(s) + if type(s) == 'string' then return s:find('^%s*$') end + if type(s) == 'table' then + for _, val in pairs(s) do + if not H.is_whitespace(val) then return false end + end + return true + end + return false +end + +-- Simulate splitting single line `l` like how it would look inside window with +-- `wrap` and `linebreak` set to `true` +H.wrap_line = function(l, width) + local res = {} + + local success, width_id = true, nil + -- Use `strdisplaywidth()` to account for multibyte characters + while success and vim.fn.strdisplaywidth(l) > width do + -- Simulate wrap by looking at breaking character from end of current break + -- Use `pcall()` to handle complicated multibyte characters (like Chinese) + -- for which even `strdisplaywidth()` seems to return incorrect values. + success, width_id = pcall(vim.str_byteindex, l, width) + + if success then + local break_match = vim.fn.match(l:sub(1, width_id):reverse(), '[- \t.,;:!?]') + -- If no breaking character found, wrap at whole width + local break_id = width_id - (break_match < 0 and 0 or break_match) + table.insert(res, l:sub(1, break_id)) + l = l:sub(break_id + 1) + end + end + table.insert(res, l) + + return res +end + +H.table_get = function(t, id) + if type(id) ~= 'table' then return H.table_get(t, { id }) end + local success, res = true, t + for _, i in ipairs(id) do + --stylua: ignore start + success, res = pcall(function() return res[i] end) + if not success or res == nil then return end + --stylua: ignore end + end + return res +end + +H.get_left_char = function() + local line = vim.api.nvim_get_current_line() + local col = vim.api.nvim_win_get_cursor(0)[2] + + return string.sub(line, col, col) +end + +H.map = function(mode, key, rhs, opts) + if key == '' then return end + + opts = vim.tbl_deep_extend('force', { noremap = true, silent = true }, opts or {}) + + -- Use mapping description only in Neovim>=0.7 + if vim.fn.has('nvim-0.7') == 0 then opts.desc = nil end + + vim.api.nvim_set_keymap(mode, key, rhs, opts) +end + +return MiniCompletion diff --git a/dotfiles/pack/plugins/start/mini.nvim/lua/mini/cursorword.lua b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/cursorword.lua new file mode 100755 index 0000000..2231d1a --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/cursorword.lua @@ -0,0 +1,282 @@ +-- MIT License Copyright (c) 2021 Evgeni Chasnovski + +-- Documentation ============================================================== +--- 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 ||: 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('') +--- 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()') +---@tag mini.cursorword +---@tag MiniCursorword + +-- Module definition ========================================================== +local MiniCursorword = {} +local H = {} + +--- Module setup +--- +---@param config table Module config table. See |MiniCursorword.config|. +--- +---@usage `require('mini.cursorword').setup({})` (replace `{}` with your `config` table) +MiniCursorword.setup = function(config) + -- Export module + _G.MiniCursorword = MiniCursorword + + -- Setup config + config = H.setup_config(config) + + -- Apply config + H.apply_config(config) + + -- Module behavior + vim.api.nvim_exec( + [[augroup MiniCursorword + au! + au CursorMoved * lua MiniCursorword.auto_highlight() + au InsertEnter,TermEnter,QuitPre * lua MiniCursorword.auto_unhighlight() + + au FileType TelescopePrompt let b:minicursorword_disable=v:true + augroup END]], + false + ) + + if vim.fn.exists('##ModeChanged') == 1 then + vim.api.nvim_exec( + -- Call `auto_highlight` on mode change to respect `minicursorword_disable` + [[augroup MiniCursorword + au ModeChanged *:[^i] lua MiniCursorword.auto_highlight() + augroup END]], + false + ) + end + + -- Create highlighting + vim.api.nvim_exec( + [[hi default MiniCursorword cterm=underline gui=underline + hi default link MiniCursorwordCurrent MiniCursorword]], + false + ) +end + +--- Module config +--- +--- Default values: +---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section) +MiniCursorword.config = { + -- Delay (in ms) between when cursor moved and when highlighting appeared + delay = 100, +} +--minidoc_afterlines_end + +-- Module functionality ======================================================= +--- 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_highlight = function() + -- Stop any possible previous delayed highlighting + H.timer:stop() + + -- Stop highlighting immediately if module is disabled when cursor is not on + -- 'keyword' + if H.is_disabled() or not H.is_cursor_on_keyword() then + H.unhighlight() + return + end + + -- Get current information + local win_id = vim.api.nvim_get_current_win() + local win_match = H.window_matches[win_id] or {} + local curword = H.get_cursor_word() + + -- Only immediately update highlighting of current word under cursor if + -- currently highlighted word equals one under cursor + if win_match.word == curword then + H.unhighlight(true) + H.highlight(true) + return + end + + -- Stop highlighting previous match (if it exists) + H.unhighlight() + + -- Delay highlighting + H.timer:start( + H.get_config().delay, + 0, + vim.schedule_wrap(function() + -- Ensure that always only one word is highlighted + H.unhighlight() + H.highlight() + end) + ) +end + +--- Auto unhighlight word under cursor +--- +--- Designed to be used with |autocmd|. No need to use it directly, everything +--- is setup in |MiniCursorword.setup|. +MiniCursorword.auto_unhighlight = function() + -- Stop any possible previous delayed highlighting + H.timer:stop() + H.unhighlight() +end + +-- Helper data ================================================================ +-- Module default config +H.default_config = MiniCursorword.config + +-- Delay timer +H.timer = vim.loop.new_timer() + +-- Information about last match highlighting (stored *per window*): +-- - Key: windows' unique buffer identifiers. +-- - Value: table with: +-- - `id` field for match id (from `vim.fn.matchadd()`). +-- - `word` field for matched word. +H.window_matches = {} + +-- Helper functionality ======================================================= +-- Settings ------------------------------------------------------------------- +H.setup_config = function(config) + -- General idea: if some table elements are not present in user-supplied + -- `config`, take them from default config + vim.validate({ config = { config, 'table', true } }) + config = vim.tbl_deep_extend('force', H.default_config, config or {}) + + vim.validate({ delay = { config.delay, 'number' } }) + + return config +end + +H.apply_config = function(config) MiniCursorword.config = config end + +H.is_disabled = function() return vim.g.minicursorword_disable == true or vim.b.minicursorword_disable == true end + +H.get_config = function(config) + return vim.tbl_deep_extend('force', MiniCursorword.config, vim.b.minicursorword_config or {}, config or {}) +end + +-- Highlighting --------------------------------------------------------------- +---@param only_current boolean Whether to forcefuly highlight only current word +--- under cursor. +---@private +H.highlight = function(only_current) + -- A modified version of https://stackoverflow.com/a/25233145 + -- Using `matchadd()` instead of a simpler `:match` to tweak priority of + -- 'current word' highlighting: with `:match` it is higher than for + -- `incsearch` which is not convenient. + local win_id = vim.api.nvim_get_current_win() + if not vim.api.nvim_win_is_valid(win_id) then return end + + H.window_matches[win_id] = H.window_matches[win_id] or {} + + -- Add match highlight for current word under cursor with low priority + local match_id_current = vim.fn.matchadd('MiniCursorwordCurrent', [[\k*\%#\k*]], -1) + H.window_matches[win_id].id_current = match_id_current + + -- Don't add main match id if not needed or if one is already present + if only_current or H.window_matches[win_id].id ~= nil then return end + + -- Make highlighting for cursor word with pattern being 'very nomagic' ('\V') + -- and matching whole word ('\<' and '\>') + local curword = H.get_cursor_word() + local curpattern = string.format([[\V\<%s\>]], curword) + + -- Add match highlight with even lower priority for current word to be on top + local match_id = vim.fn.matchadd('MiniCursorword', curpattern, -2) + + -- Store information about highlight + H.window_matches[win_id].id = match_id + H.window_matches[win_id].word = curword +end + +---@param only_current boolean Whether to remove highlighting only of current +--- word under cursor. +---@private +H.unhighlight = function(only_current) + -- Don't do anything if there is no valid information to act upon + local win_id = vim.api.nvim_get_current_win() + local win_match = H.window_matches[win_id] + if not vim.api.nvim_win_is_valid(win_id) or win_match == nil then return end + + -- Use `pcall` because there is an error if match id is not present. It can + -- happen if something else called `clearmatches`. + pcall(vim.fn.matchdelete, win_match.id_current) + H.window_matches[win_id].id_current = nil + + if not only_current then + pcall(vim.fn.matchdelete, win_match.id) + H.window_matches[win_id] = nil + end +end + +H.is_cursor_on_keyword = function() + local col = vim.fn.col('.') + local curchar = vim.api.nvim_get_current_line():sub(col, col) + + -- Use `pcall()` to catch `E5108` (can happen in binary files, see #112) + local ok, match_res = pcall(vim.fn.match, curchar, '[[:keyword:]]') + return ok and match_res >= 0 +end + +H.get_cursor_word = function() return vim.fn.escape(vim.fn.expand(''), [[\/]]) end + +return MiniCursorword diff --git a/dotfiles/pack/plugins/start/mini.nvim/lua/mini/doc.lua b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/doc.lua new file mode 100755 index 0000000..6765a82 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/doc.lua @@ -0,0 +1,1289 @@ +-- MIT License Copyright (c) 2022 Evgeni Chasnovski + +-- Documentation ============================================================== +--- 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. +---@tag mini.doc +---@tag MiniDoc + +--- 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|). +---@tag MiniDoc-data-structures + +-- Module definition ========================================================== +local MiniDoc = {} +local H = {} + +--- Module setup +--- +---@param config table Module config table. See |MiniDoc.config|. +--- +---@usage `require('mini.doc').setup({})` (replace `{}` with your `config` table) +MiniDoc.setup = function(config) + -- Export module + _G.MiniDoc = MiniDoc + + -- Setup config + config = H.setup_config(config) + + -- Apply config + H.apply_config(config) +end + +--- Module config +--- +--- Default values: +---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section) +---@text # 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.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 + --minidoc_replace_start block_pre = --, + block_pre = function(b) + -- Infer metadata based on afterlines + if b:has_lines() and #b.info.afterlines > 0 then H.infer_header(b) end + end, + --minidoc_replace_end + + -- Applied to section before anything else + --minidoc_replace_start section_pre = --, + section_pre = function(s) H.alias_replace(s) end, + --minidoc_replace_end + + -- Applied if section has specified captured id + sections = { + --minidoc_replace_start ['@alias'] = --, + ['@alias'] = function(s) + H.alias_register(s) + -- NOTE: don't use `s.parent:remove(s.parent_index)` here because it + -- disrupts iteration over block's section during hook application + -- (skips next section). + s:clear_lines() + end, + --minidoc_replace_end + --minidoc_replace_start ['@class'] = --, + ['@class'] = function(s) + H.enclose_var_name(s) + H.add_section_heading(s, 'Class') + end, + --minidoc_replace_end + --minidoc_replace_start ['@diagnostic'] = --, + ['@diagnostic'] = function(s) s:clear_lines() end, + --minidoc_replace_end + -- For most typical usage see |MiniDoc.afterlines_to_code| + --minidoc_replace_start ['@eval'] = --, + ['@eval'] = function(s) + local src = table.concat(s, '\n') + local is_loaded, code = pcall(function() return assert(loadstring(src)) end) + local output + if is_loaded then + MiniDoc.current.eval_section = s + output = code() + MiniDoc.current.eval_section = nil + else + output = 'MINIDOC ERROR. Parsing Lua code gave the following error:\n' .. code + end + + s:clear_lines() + + if output == nil then return end + if type(output) == 'string' then output = vim.split(output, '\n') end + if type(output) ~= 'table' then + s[1] = 'MINIDOC ERROR. Returned value should be `nil`, `string`, or `table`.' + return + end + for _, x in ipairs(output) do + s:insert(x) + end + end, + --minidoc_replace_end + --minidoc_replace_start ['@field'] = --, + ['@field'] = function(s) + H.mark_optional(s) + H.enclose_var_name(s) + H.enclose_type(s, '`%(%1%)`', s[1]:find('%s')) + end, + --minidoc_replace_end + --minidoc_replace_start ['@overload'] = --, + ['@overload'] = function(s) + H.enclose_type(s, '`%1`', 1) + H.add_section_heading(s, 'Overload') + end, + --minidoc_replace_end + --minidoc_replace_start ['@param'] = --, + ['@param'] = function(s) + H.mark_optional(s) + H.enclose_var_name(s) + H.enclose_type(s, '`%(%1%)`', s[1]:find('%s')) + end, + --minidoc_replace_end + --minidoc_replace_start ['@private'] = --, + ['@private'] = function(s) s.parent:clear_lines() end, + --minidoc_replace_end + --minidoc_replace_start ['@return'] = --, + ['@return'] = function(s) + H.mark_optional(s) + H.enclose_type(s, '`%(%1%)`', 1) + H.add_section_heading(s, 'Return') + end, + --minidoc_replace_end + --minidoc_replace_start ['@seealso'] = --, + ['@seealso'] = function(s) H.add_section_heading(s, 'See also') end, + --minidoc_replace_end + --minidoc_replace_start ['@signature'] = --, + ['@signature'] = function(s) + for i, _ in ipairs(s) do + -- Add extra formatting to make it stand out + s[i] = H.format_signature(s[i]) + + -- Align accounting for concealed characters + s[i] = H.align_text(s[i], 78, 'center') + end + end, + --minidoc_replace_end + --minidoc_replace_start ['@tag'] = --, + ['@tag'] = function(s) + for i, _ in ipairs(s) do + -- Enclose every word in `*` + s[i] = s[i]:gsub('(%S+)', '%*%1%*') + + -- Align to right edge accounting for concealed characters + s[i] = H.align_text(s[i], 78, 'right') + end + end, + --minidoc_replace_end + --minidoc_replace_start ['@text'] = --, + ['@text'] = function() end, + --minidoc_replace_end + --minidoc_replace_start ['@toc'] = --, + ['@toc'] = function(s) s:clear_lines() end, + --minidoc_replace_end + --minidoc_replace_start ['@toc_entry'] = --, + ['@toc_entry'] = function(s) H.toc_register(s) end, + --minidoc_replace_end + --minidoc_replace_start ['@type'] = --, + ['@type'] = function(s) + H.enclose_type(s, '`%(%1%)`', 1) + H.add_section_heading(s, 'Type') + end, + --minidoc_replace_end + --minidoc_replace_start ['@usage'] = --, + ['@usage'] = function(s) H.add_section_heading(s, 'Usage') end, + --minidoc_replace_end + }, + + -- Applied to section after all previous steps + --minidoc_replace_start section_post = --, + section_post = function(s) end, + --minidoc_replace_end + + -- Applied to block after all previous steps + --minidoc_replace_start block_post = --, + block_post = function(b) + if not b:has_lines() then return end + + local found_param, found_field = false, false + local n_tag_sections = 0 + H.apply_recursively(function(x) + if not (type(x) == 'table' and x.type == 'section') then return end + + -- Add headings before first occurence of a section which type usually + -- appear several times + if not found_param and x.info.id == '@param' then + H.add_section_heading(x, 'Parameters') + found_param = true + end + if not found_field and x.info.id == '@field' then + H.add_section_heading(x, 'Fields') + found_field = true + end + + if x.info.id == '@tag' then + x.parent:remove(x.parent_index) + n_tag_sections = n_tag_sections + 1 + x.parent:insert(n_tag_sections, x) + end + end, b) + + b:insert(1, H.as_struct({ string.rep('-', 78) }, 'section')) + b:insert(H.as_struct({ '' }, 'section')) + end, + --minidoc_replace_end + + -- Applied to file after all previous steps + --minidoc_replace_start file = --, + file = function(f) + if not f:has_lines() then return end + + f:insert(1, H.as_struct({ H.as_struct({ string.rep('=', 78) }, 'section') }, 'block')) + f:insert(H.as_struct({ H.as_struct({ '' }, 'section') }, 'block')) + end, + --minidoc_replace_end + + -- Applied to doc after all previous steps + --minidoc_replace_start doc = --, + doc = function(d) + -- Render table of contents + H.apply_recursively(function(x) + if not (type(x) == 'table' and x.type == 'section' and x.info.id == '@toc') then return end + H.toc_insert(x) + end, d) + + -- Insert modeline + d:insert( + H.as_struct( + { H.as_struct({ H.as_struct({ ' vim:tw=78:ts=8:noet:ft=help:norl:' }, 'section') }, 'block') }, + 'file' + ) + ) + end, + --minidoc_replace_end + + -- Applied to after output help file is written. Takes doc as argument. + --minidoc_replace_start write_post = --, + write_post = function(d) + local output = d.info.output + + -- Generate help tags for directory of output file + vim.cmd('helptags ' .. vim.fn.fnamemodify(output, ':h')) + + -- Reload buffer with output file (helps during writing annotations) + local output_path = H.full_path(output) + for _, buf_id in ipairs(vim.api.nvim_list_bufs()) do + local buf_path = H.full_path(vim.api.nvim_buf_get_name(buf_id)) + if buf_path == output_path then + vim.api.nvim_buf_call(buf_id, function() vim.cmd('noautocmd silent edit | set ft=help') end) + end + end + + -- Notify + local msg = ('Help file %s is successfully generated (%s).'):format( + vim.inspect(output), + vim.fn.strftime('%Y-%m-%d %H:%M:%S') + ) + H.message(msg) + end, + --minidoc_replace_end + }, + + -- 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', +} +--minidoc_afterlines_end + +-- Module data ================================================================ +--- 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.current = { aliases = {}, toc = {} } + +--- 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 '`()`' (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.default_hooks = MiniDoc.config.hooks + +-- Module functionality ======================================================= +--- 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 +--- ` 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). +--- +---@param 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. +---@param output string Path for output help file. Default: +--- `doc/.txt` (designed to be used for generating help +--- file for plugin). +---@param 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.generate = function(input, output, config) + -- Try sourcing project specific script first + local success = H.execute_project_script(input, output, config) + if success then return H.generate_recent_output end + + input = input or H.default_input() + output = output or H.default_output() + config = H.get_config(config) + + -- Prepare table for current information + MiniDoc.current = {} + + -- Parse input files + local doc = H.new_struct('doc', { input = input, output = output, config = config }) + for _, path in ipairs(input) do + local lines = H.file_read(path) + local block_arr = H.lines_to_block_arr(lines, config) + local file = H.as_struct(block_arr, 'file', { path = path }) + + doc:insert(file) + end + + -- Apply hooks + H.apply_structure_hooks(doc, config.hooks) + + -- Gather string lines in depth-first fashion + local help_lines = H.collect_strings(doc) + + -- Write helpfile + H.file_write(output, help_lines) + + -- Execute post-write hook + config.hooks.write_post(doc) + + -- Clear current information + MiniDoc.current = {} + + -- Stash output to allow returning value even when called from script + H.generate_recent_output = doc + + return doc +end + +--- 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 ` and `--minidoc_replace_end` +--- denote lines between them which should be replaced with ``. +--- 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 = -- +--- 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 = -- +--- } +--- < +---@param 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. +MiniDoc.afterlines_to_code = function(struct) + if not (type(struct) == 'table' and (struct.type == 'section' or struct.type == 'block')) then + H.message('Input to `MiniDoc.afterlines_to_code()` should be either section or block.') + return + end + + if struct.type == 'section' then struct = struct.parent end + local src = table.concat(struct.info.afterlines, '\n') + + -- Process directives + -- Try to extract afterlines + src = src:match('^(.-)\n%s*%-%-minidoc_afterlines_end') or src + + -- Make replacements + src = src:gsub('%-%-minidoc_replace_start ?(.-)\n.-%-%-minidoc_replace_end', '%1') + + -- Convert to a standalone code. NOTE: indent is needed because of how `>` + -- and `<` work (any line starting in column 1 stops code block). + src = H.ensure_indent(src, 2) + return '>\n' .. src .. '\n<' +end + +-- Helper data ================================================================ +-- Module default config +H.default_config = MiniDoc.config + +-- Alias registry. Keys are alias name, values - single string of alias +-- description with '\n' separating output lines. +H.alias_registry = {} + +--stylua: ignore start +H.pattern_sets = { + -- Patterns for working with afterlines. At the moment deliberately crafted + -- to work only on first line without indent. + + -- Determine if line is a function definition. Captures function name and + -- arguments. For reference see '2.5.9 – Function Definitions' in Lua manual. + afterline_fundef = { + '^function%s+(%S-)(%b())', -- Regular definition + '^local%s+function%s+(%S-)(%b())', -- Local definition + '^(%S+)%s*=%s*function(%b())', -- Regular assignment + '^local%s+(%S+)%s*=%s*function(%b())', -- Local assignment + }, + + -- Determine if line is a general assignment + afterline_assign = { + '^(%S-)%s*=', -- General assignment + '^local%s+(%S-)%s*=', -- Local assignment + }, + + -- Patterns to work with type descriptions + -- (see https://github.com/sumneko/lua-language-server/wiki/EmmyLua-Annotations#types-and-type) + types = { + 'table%b<>', + 'fun%b(): %S+', 'fun%b()', + 'nil', 'any', 'boolean', 'string', 'number', 'integer', 'function', 'table', 'thread', 'userdata', 'lightuserdata', + '%.%.%.' + }, +} +--stylua: ignore end + +-- Helper functionality ======================================================= +-- Settings ------------------------------------------------------------------- +H.setup_config = function(config) + -- General idea: if some table elements are not present in user-supplied + -- `config`, take them from default config + vim.validate({ config = { config, 'table', true } }) + config = vim.tbl_deep_extend('force', H.default_config, config or {}) + + -- Validate per nesting level to produce correct error message + vim.validate({ + annotation_extractor = { config.annotation_extractor, 'function' }, + default_section_id = { config.default_section_id, 'string' }, + hooks = { config.hooks, 'table' }, + script_path = { config.script_path, 'string' }, + }) + + vim.validate({ + ['hooks.block_pre'] = { config.hooks.block_pre, 'function' }, + ['hooks.section_pre'] = { config.hooks.section_pre, 'function' }, + ['hooks.sections'] = { config.hooks.sections, 'table' }, + ['hooks.section_post'] = { config.hooks.section_post, 'function' }, + ['hooks.block_post'] = { config.hooks.block_post, 'function' }, + ['hooks.file'] = { config.hooks.file, 'function' }, + ['hooks.doc'] = { config.hooks.doc, 'function' }, + ['hooks.write_post'] = { config.hooks.write_post, 'function' }, + }) + + vim.validate({ + ['hooks.sections.@alias'] = { config.hooks.sections['@alias'], 'function' }, + ['hooks.sections.@class'] = { config.hooks.sections['@class'], 'function' }, + ['hooks.sections.@diagnostic'] = { config.hooks.sections['@diagnostic'], 'function' }, + ['hooks.sections.@eval'] = { config.hooks.sections['@eval'], 'function' }, + ['hooks.sections.@field'] = { config.hooks.sections['@field'], 'function' }, + ['hooks.sections.@overload'] = { config.hooks.sections['@overload'], 'function' }, + ['hooks.sections.@param'] = { config.hooks.sections['@param'], 'function' }, + ['hooks.sections.@private'] = { config.hooks.sections['@private'], 'function' }, + ['hooks.sections.@return'] = { config.hooks.sections['@return'], 'function' }, + ['hooks.sections.@seealso'] = { config.hooks.sections['@seealso'], 'function' }, + ['hooks.sections.@signature'] = { config.hooks.sections['@signature'], 'function' }, + ['hooks.sections.@tag'] = { config.hooks.sections['@tag'], 'function' }, + ['hooks.sections.@text'] = { config.hooks.sections['@text'], 'function' }, + ['hooks.sections.@toc'] = { config.hooks.sections['@toc'], 'function' }, + ['hooks.sections.@toc_entry'] = { config.hooks.sections['@toc_entry'], 'function' }, + ['hooks.sections.@type'] = { config.hooks.sections['@type'], 'function' }, + ['hooks.sections.@usage'] = { config.hooks.sections['@usage'], 'function' }, + }) + + return config +end + +H.apply_config = function(config) MiniDoc.config = config end + +H.is_disabled = function() return vim.g.minidoc_disable == true or vim.b.minidoc_disable == true end + +H.get_config = + function(config) return vim.tbl_deep_extend('force', MiniDoc.config, vim.b.minidoc_config or {}, config or {}) end + +-- Work with project specific script ========================================== +H.execute_project_script = function(input, output, config) + -- Don't process script if there are more than one active `generate` calls + if H.generate_is_active then return end + + -- Don't process script if at least one argument is not default + if not (input == nil and output == nil and config == nil) then return end + + -- Store information + local global_config_cache = vim.deepcopy(MiniDoc.config) + local local_config_cache = vim.b.minidoc_config + + -- Pass information to a possible `generate()` call inside script + H.generate_is_active = true + H.generate_recent_output = nil + + -- Execute script + local success = pcall(vim.cmd, 'luafile ' .. H.get_config(config).script_path) + + -- Restore information + MiniDoc.config = global_config_cache + vim.b.minidoc_config = local_config_cache + H.generate_is_active = nil + + return success +end + +-- Default documentation targets ---------------------------------------------- +H.default_input = function() + -- Search in current and recursively in other directories for files with + -- 'lua' extension + local res = {} + for _, dir_glob in ipairs({ '.', 'lua/**', 'after/**', 'colors/**' }) do + local files = vim.fn.globpath(dir_glob, '*.lua', false, true) + + -- Use full paths + files = vim.tbl_map(function(x) return vim.fn.fnamemodify(x, ':p') end, files) + + -- Put 'init.lua' first among files from same directory + table.sort(files, function(a, b) + if vim.fn.fnamemodify(a, ':h') == vim.fn.fnamemodify(b, ':h') then + if vim.fn.fnamemodify(a, ':t') == 'init.lua' then return true end + if vim.fn.fnamemodify(b, ':t') == 'init.lua' then return false end + end + + return a < b + end) + table.insert(res, files) + end + + return vim.tbl_flatten(res) +end + +H.default_output = function() + local cur_dir = vim.fn.fnamemodify(vim.loop.cwd(), ':t:r') + return ('doc/%s.txt'):format(cur_dir) +end + +-- Parsing -------------------------------------------------------------------- +H.lines_to_block_arr = function(lines, config) + local matched_prev, matched_cur + + local res = {} + local block_raw = { annotation = {}, section_id = {}, afterlines = {}, line_begin = 1 } + + for i, l in ipairs(lines) do + local from, to, section_id = config.annotation_extractor(l) + matched_prev, matched_cur = matched_cur, from ~= nil + + if matched_cur then + if not matched_prev then + -- Finish current block + block_raw.line_end = i - 1 + table.insert(res, H.raw_block_to_block(block_raw, config)) + + -- Start new block + block_raw = { annotation = {}, section_id = {}, afterlines = {}, line_begin = i } + end + + -- Add annotation line without matched annotation pattern + table.insert(block_raw.annotation, ('%s%s'):format(l:sub(0, from - 1), l:sub(to + 1))) + + -- Add section id (it is empty string in case of no section id capture) + table.insert(block_raw.section_id, section_id or '') + else + -- Add afterline + table.insert(block_raw.afterlines, l) + end + end + block_raw.line_end = #lines + table.insert(res, H.raw_block_to_block(block_raw, config)) + + return res +end + +-- Raw block structure is an intermediate step added for convenience. It is +-- a table with the following keys: +-- - `annotation` - lines (after removing matched annotation pattern) that were +-- parsed as annotation. +-- - `section_id` - array with length equal to `annotation` length with strings +-- captured as section id. Empty string of no section id was captured. +-- - Everything else is used as block info (like `afterlines`, etc.). +H.raw_block_to_block = function(block_raw, config) + if #block_raw.annotation == 0 and #block_raw.afterlines == 0 then return nil end + + local block = H.new_struct('block', { + afterlines = block_raw.afterlines, + line_begin = block_raw.line_begin, + line_end = block_raw.line_end, + }) + local block_begin = block.info.line_begin + + -- Parse raw block annotation lines from top to bottom. New section starts + -- when section id is detected in that line. + local section_cur = H.new_struct('section', { id = config.default_section_id, line_begin = block_begin }) + + for i, annotation_line in ipairs(block_raw.annotation) do + local id = block_raw.section_id[i] + if id ~= '' then + -- Finish current section + if #section_cur > 0 then + section_cur.info.line_end = block_begin + i - 2 + block:insert(section_cur) + end + + -- Start new section + section_cur = H.new_struct('section', { id = id, line_begin = block_begin + i - 1 }) + end + + section_cur:insert(annotation_line) + end + + if #section_cur > 0 then + section_cur.info.line_end = block_begin + #block_raw.annotation - 1 + block:insert(section_cur) + end + + return block +end + +-- Hooks ---------------------------------------------------------------------- +H.apply_structure_hooks = function(doc, hooks) + for _, file in ipairs(doc) do + for _, block in ipairs(file) do + hooks.block_pre(block) + + for _, section in ipairs(block) do + hooks.section_pre(section) + + local hook = hooks.sections[section.info.id] + if hook ~= nil then hook(section) end + + hooks.section_post(section) + end + + hooks.block_post(block) + end + + hooks.file(file) + end + + hooks.doc(doc) +end + +H.alias_register = function(s) + if #s == 0 then return end + + -- Remove first word (with bits of surrounding whitespace) while capturing it + local alias_name + s[1] = s[1]:gsub('%s*(%S+) ?', function(x) + alias_name = x + return '' + end, 1) + if alias_name == nil then return end + + MiniDoc.current.aliases = MiniDoc.current.aliases or {} + MiniDoc.current.aliases[alias_name] = table.concat(s, '\n') +end + +H.alias_replace = function(s) + if MiniDoc.current.aliases == nil then return end + + for i, _ in ipairs(s) do + for alias_name, alias_desc in pairs(MiniDoc.current.aliases) do + -- Escape special characters. This is done here and not while registering + -- alias to allow user to refer to aliases by its original name. + -- Store escaped words in separate variables because `vim.pesc()` returns + -- two values which might conflict if outputs are used as arguments. + local name_escaped = vim.pesc(alias_name) + local desc_escaped = vim.pesc(alias_desc) + s[i] = s[i]:gsub(name_escaped, desc_escaped) + end + end +end + +H.toc_register = function(s) + MiniDoc.current.toc = MiniDoc.current.toc or {} + table.insert(MiniDoc.current.toc, s) +end + +H.toc_insert = function(s) + if MiniDoc.current.toc == nil then return end + + -- Render table of contents + local toc_lines = {} + for _, toc_entry in ipairs(MiniDoc.current.toc) do + local _, tag_section = toc_entry.parent:has_descendant( + function(x) return type(x) == 'table' and x.type == 'section' and x.info.id == '@tag' end + ) + tag_section = tag_section or {} + + local lines = {} + for i = 1, math.max(#toc_entry, #tag_section) do + local left = toc_entry[i] or '' + -- Use tag refernce instead of tag enclosure + local right = vim.trim((tag_section[i] or ''):gsub('%*', '|')) + -- Add visual line only at first entry (while not adding trailing space) + local filler = i == 1 and '.' or (right == '' and '' or ' ') + -- Make padding of 2 spaces at both left and right + local n_filler = math.max(74 - H.visual_text_width(left) - H.visual_text_width(right), 3) + table.insert(lines, (' %s%s%s'):format(left, filler:rep(n_filler), right)) + end + + table.insert(toc_lines, lines) + + -- Don't show `toc_entry` lines in output + toc_entry:clear_lines() + end + + for _, l in ipairs(vim.tbl_flatten(toc_lines)) do + s:insert(l) + end +end + +H.add_section_heading = function(s, heading) + if #s == 0 or s.type ~= 'section' then return end + + -- Add heading + s:insert(1, ('%s~'):format(heading)) +end + +H.mark_optional = function(s) + -- Treat question mark at end of first word as "optional" indicator. See: + -- https://github.com/sumneko/lua-language-server/wiki/EmmyLua-Annotations#optional-params + s[1] = s[1]:gsub('^(%s-%S-)%?', '%1 `(optional)`', 1) +end + +H.enclose_var_name = function(s) + if #s == 0 or s.type ~= 'section' then return end + + s[1] = s[1]:gsub('(%S+)', '{%1}', 1) +end + +---@param init number Start of searching for first "type-like" string. It is +--- needed to not detect type early. Like in `@param a_function function`. +---@private +H.enclose_type = function(s, enclosure, init) + if #s == 0 or s.type ~= 'section' then return end + enclosure = enclosure or '`%(%1%)`' + init = init or 1 + + local cur_type = H.match_first_pattern(s[1], H.pattern_sets['types'], init) + if #cur_type == 0 then return end + + -- Add `%S*` to front and back of found pattern to support their combination + -- with `|`. Also allows using `[]` and `?` prefixes. + local type_pattern = ('(%%S*%s%%S*)'):format(vim.pesc(cur_type[1])) + + -- Avoid replacing possible match before `init` + local l_start = s[1]:sub(1, init - 1) + local l_end = s[1]:sub(init):gsub(type_pattern, enclosure, 1) + s[1] = ('%s%s'):format(l_start, l_end) +end + +-- Infer data from afterlines ------------------------------------------------- +H.infer_header = function(b) + local has_signature = b:has_descendant( + function(x) return type(x) == 'table' and x.type == 'section' and x.info.id == '@signature' end + ) + local has_tag = b:has_descendant( + function(x) return type(x) == 'table' and x.type == 'section' and x.info.id == '@tag' end + ) + + if has_signature and has_tag then return end + + local l_all = table.concat(b.info.afterlines, ' ') + local tag, signature + + -- Try function definition + local fun_capture = H.match_first_pattern(l_all, H.pattern_sets['afterline_fundef']) + if #fun_capture > 0 then + tag = tag or ('%s()'):format(fun_capture[1]) + signature = signature or ('%s%s'):format(fun_capture[1], fun_capture[2]) + end + + -- Try general assignment + local assign_capture = H.match_first_pattern(l_all, H.pattern_sets['afterline_assign']) + if #assign_capture > 0 then + tag = tag or assign_capture[1] + signature = signature or assign_capture[1] + end + + if tag ~= nil then + -- First insert signature (so that it will appear after tag section) + if not has_signature then b:insert(1, H.as_struct({ signature }, 'section', { id = '@signature' })) end + + -- Insert tag + if not has_tag then b:insert(1, H.as_struct({ tag }, 'section', { id = '@tag' })) end + end +end + +H.format_signature = function(line) + -- Try capture function signature + local name, args = line:match('(%S-)(%b())') + -- Otherwise pick first word + name = name or line:match('(%S+)') + + if not name then return '' end + + -- Tidy arguments + if args and args ~= '()' then + local arg_parts = vim.split(args:sub(2, -2), ',') + local arg_list = {} + for _, a in ipairs(arg_parts) do + -- Enclose argument in `{}` while controlling whitespace + table.insert(arg_list, ('{%s}'):format(vim.trim(a))) + end + args = ('(%s)'):format(table.concat(arg_list, ', ')) + end + + return ('`%s`%s'):format(name, args or '') +end + +-- Work with structures ------------------------------------------------------- +-- Constructor +H.new_struct = function(struct_type, info) + local output = { + info = info or {}, + type = struct_type, + } + + output.insert = function(self, index, child) + -- Allow both `x:insert(child)` and `x:insert(1, child)` + if child == nil then + child, index = index, #self + 1 + end + + if type(child) == 'table' then + child.parent = self + child.parent_index = index + end + + table.insert(self, index, child) + + H.sync_parent_index(self) + end + + output.remove = function(self, index) + index = index or #self + table.remove(self, index) + + H.sync_parent_index(self) + end + + output.has_descendant = function(self, predicate) + local bool_res, descendant = false, nil + H.apply_recursively(function(x) + if not bool_res and predicate(x) then + bool_res = true + descendant = x + end + end, self) + return bool_res, descendant + end + + output.has_lines = function(self) + return self:has_descendant(function(x) return type(x) == 'string' end) + end + + output.clear_lines = function(self) + for i, x in ipairs(self) do + if type(x) == 'string' then + self[i] = nil + else + x:clear_lines() + end + end + end + + return output +end + +H.sync_parent_index = function(x) + for i, _ in ipairs(x) do + if type(x[i]) == 'table' then x[i].parent_index = i end + end + return x +end + +-- Converter (this ensures that children have proper parent-related data) +H.as_struct = function(array, struct_type, info) + -- Make default info `info` for cases when structure is created manually + local default_info = ({ + section = { id = '@text', line_begin = -1, line_end = -1 }, + block = { afterlines = {}, line_begin = -1, line_end = -1 }, + file = { path = '' }, + doc = { input = {}, output = '', config = H.get_config() }, + })[struct_type] + info = vim.tbl_deep_extend('force', default_info, info or {}) + + local res = H.new_struct(struct_type, info) + for _, x in ipairs(array) do + res:insert(x) + end + return res +end + +-- Work with text ------------------------------------------------------------- +H.ensure_indent = function(text, n_indent_target) + local lines = vim.split(text, '\n') + local n_indent, n_indent_cur = math.huge, math.huge + + -- Find number of characters in indent + for _, l in ipairs(lines) do + -- Update lines indent: minimum of all indents except empty lines + if n_indent > 0 then + _, n_indent_cur = l:find('^%s*') + -- Condition "current n-indent equals line length" detects empty line + if (n_indent_cur < n_indent) and (n_indent_cur < l:len()) then n_indent = n_indent_cur end + end + end + + -- Ensure indent + local indent = string.rep(' ', n_indent_target) + for i, l in ipairs(lines) do + if l ~= '' then lines[i] = indent .. l:sub(n_indent + 1) end + end + + return table.concat(lines, '\n') +end + +H.align_text = function(text, width, direction) + if type(text) ~= 'string' then return end + text = vim.trim(text) + width = width or 78 + direction = direction or 'left' + + -- Don't do anything if aligning left or line is a whitespace + if direction == 'left' or text:find('^%s*$') then return text end + + local n_left = math.max(0, 78 - H.visual_text_width(text)) + if direction == 'center' then n_left = math.floor(0.5 * n_left) end + + return (' '):rep(n_left) .. text +end + +H.visual_text_width = function(text) + -- Ignore concealed characters (usually "invisible" in 'help' filetype) + local _, n_concealed_chars = text:gsub('([*|`])', '%1') + return vim.fn.strdisplaywidth(text) - n_concealed_chars +end + +--- Return earliest match among many patterns +--- +--- Logic here is to test among several patterns. If several got a match, +--- return one with earliest match. +--- +---@private +H.match_first_pattern = function(text, pattern_set, init) + local start_tbl = vim.tbl_map(function(pattern) return text:find(pattern, init) or math.huge end, pattern_set) + + local min_start, min_id = math.huge, nil + for id, st in ipairs(start_tbl) do + if st < min_start then + min_start, min_id = st, id + end + end + + if min_id == nil then return {} end + return { text:match(pattern_set[min_id], init) } +end + +-- Utilities ------------------------------------------------------------------ +H.apply_recursively = function(f, x) + f(x) + + if type(x) == 'table' then + for _, t in ipairs(x) do + H.apply_recursively(f, t) + end + end +end + +H.collect_strings = function(x) + local res = {} + H.apply_recursively(function(y) + if type(y) == 'string' then + -- Allow `\n` in strings + table.insert(res, vim.split(y, '\n')) + end + end, x) + -- Flatten to only have strings and not table of strings (from `vim.split`) + return vim.tbl_flatten(res) +end + +H.file_read = function(path) + local file = assert(io.open(path)) + local contents = file:read('*all') + file:close() + + return vim.split(contents, '\n') +end + +H.file_write = function(path, lines) + -- Ensure target directory exists + local dir = vim.fn.fnamemodify(path, ':h') + vim.fn.mkdir(dir, 'p') + + -- Write to file + vim.fn.writefile(lines, path, 'b') +end + +H.full_path = function(path) return vim.fn.resolve(vim.fn.fnamemodify(path, ':p')) end + +H.message = function(msg) vim.cmd('echomsg ' .. vim.inspect('(mini.doc) ' .. msg)) end + +return MiniDoc diff --git a/dotfiles/pack/plugins/start/mini.nvim/lua/mini/fuzzy.lua b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/fuzzy.lua new file mode 100755 index 0000000..bbc1113 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/fuzzy.lua @@ -0,0 +1,360 @@ +-- MIT License Copyright (c) 2021 Evgeni Chasnovski + +-- Documentation ============================================================== +--- 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. +---@tag mini.fuzzy +---@tag MiniFuzzy + +--- # 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`. +---@tag MiniFuzzy-algorithm + +-- Module definition ========================================================== +local MiniFuzzy = {} +local H = {} + +--- Module setup +--- +---@param config table Module config table. See |MiniFuzzy.config|. +--- +---@usage `require('mini.fuzzy').setup({})` (replace `{}` with your `config` table) +MiniFuzzy.setup = function(config) + -- Export module + _G.MiniFuzzy = MiniFuzzy + + -- Setup config + config = H.setup_config(config) + + -- Apply config + H.apply_config(config) +end + +--- Module config +--- +--- Default values: +---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section) +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, +} +--minidoc_afterlines_end + +-- Module functionality ======================================================= +--- 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. +--- +---@param word string Input word (usually user input). +---@param candidate string Target word (usually with which matching is done). +--- +---@return table Table with matching information (see function's description). +MiniFuzzy.match = function(word, candidate) + -- Use 'smart case' + candidate = (word == word:lower()) and candidate:lower() or candidate + + local positions = H.find_best_positions(H.string_to_letters(word), candidate) + return { positions = positions, score = H.score_positions(positions) } +end + +--- 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). +--- +---@param word string String which will be searched. +---@param 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.filtersort = function(word, candidate_array) + -- Use 'smart case'. Create new array to preserve input for later filtering + local cand_array + if word == word:lower() then + cand_array = vim.tbl_map(string.lower, candidate_array) + else + cand_array = candidate_array + end + + local filter_ids = H.make_filter_indexes(word, cand_array) + table.sort(filter_ids, H.compare_filter_indexes) + + return H.filter_by_indexes(candidate_array, filter_ids) +end + +--- Fuzzy matching for `lsp_completion.process_items` of |MiniCompletion.config| +--- +---@param items table Lua array with LSP 'textDocument/completion' response items. +---@param base string Word to complete. +MiniFuzzy.process_lsp_items = function(items, base) + -- Extract completion words from items + local words = vim.tbl_map(function(x) + if type(x.textEdit) == 'table' and type(x.textEdit.newText) == 'string' then return x.textEdit.newText end + if type(x.insertText) == 'string' then return x.insertText end + if type(x.label) == 'string' then return x.label end + return '' + end, items) + + -- Fuzzy match + local _, match_inds = MiniFuzzy.filtersort(base, words) + return vim.tbl_map(function(i) return items[i] end, match_inds) +end + +--- 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. +--- +---@param opts table Options (currently not used). +--- +---@usage > +--- require('telescope').setup({ +--- defaults = { +--- generic_sorter = require('mini.fuzzy').get_telescope_sorter +--- } +--- }) +MiniFuzzy.get_telescope_sorter = function(opts) + opts = opts or {} + + return require('telescope.sorters').Sorter:new({ + start = function(self, prompt) + -- Cache prompt's letters + self.letters = H.string_to_letters(prompt) + + -- Use 'smart case': insensitive if `prompt` is lowercase + self.case_sensitive = prompt ~= prompt:lower() + end, + + -- @param self + -- @param prompt (which is the text on the line) + -- @param line (entry.ordinal) + -- @param entry (the whole entry) + scoring_function = function(self, _, line, _) + if #self.letters == 0 then return 1 end + line = self.case_sensitive and line or line:lower() + local positions = H.find_best_positions(self.letters, line) + return H.score_positions(positions) + end, + + -- Currently there doesn't seem to be a proper way to cache matched + -- positions from inside of `scoring_function` (see `highlighter` code of + -- `get_fzy_sorter`'s output). Besides, it seems that `display` and `line` + -- arguments might be different. So, extra calls to `match` are made. + highlighter = function(self, _, display) + if #self.letters == 0 or #display == 0 then return {} end + display = self.case_sensitive and display or display:lower() + return H.find_best_positions(self.letters, display) + end, + }) +end + +-- Helper data ================================================================ +-- Module default config +H.default_config = MiniFuzzy.config + +-- Helper functionality ======================================================= +-- Settings ------------------------------------------------------------------- +H.setup_config = function(config) + -- General idea: if some table elements are not present in user-supplied + -- `config`, take them from default config + vim.validate({ config = { config, 'table', true } }) + config = vim.tbl_deep_extend('force', H.default_config, config or {}) + + vim.validate({ + cutoff = { + config.cutoff, + function(x) return type(x) == 'number' and x >= 1 end, + 'number not less than 1', + }, + }) + + return config +end + +H.apply_config = function(config) MiniFuzzy.config = config end + +H.is_disabled = function() return vim.g.minifuzzy_disable == true or vim.b.minifuzzy_disable == true end + +H.get_config = + function(config) return vim.tbl_deep_extend('force', MiniFuzzy.config, vim.b.minifuzzy_config or {}, config or {}) end + +-- Fuzzy matching ------------------------------------------------------------- +---@param letters table Array of letters from input word +---@param candidate string String of interest +--- +---@return table Table with matched positions (in `candidate`) if there is a +--- match, `nil` otherwise. +---@private +H.find_best_positions = function(letters, candidate) + local n_candidate, n_letters = #candidate, #letters + if n_letters == 0 or n_candidate < n_letters then return nil end + + -- Search forward to find matching positions with left-most last letter match + local pos_last = 0 + for let_i = 1, #letters do + pos_last = candidate:find(letters[let_i], pos_last + 1) + if not pos_last then break end + end + + -- Candidate is matched only if word's last letter is found + if not pos_last then return nil end + + -- If there is only one letter, it is already the best match (there will not + -- be better width and it has lowest first match) + if n_letters == 1 then return { pos_last } end + + -- Compute best match positions by iteratively checking all possible last + -- letter matches (at and after initial one). At end of each iteration + -- `best_pos_last` holds best match for last letter among all previously + -- checked such matches. + local best_pos_last, best_width = pos_last, math.huge + local rev_candidate = candidate:reverse() + + local cutoff = H.get_config().cutoff + while pos_last do + -- Simulate computing best match positions ending exactly at `pos_last` by + -- going backwards from current last letter match. This works because it + -- minimizes width which is the only way to find match with lower score. + -- Not actually creating table with positions and then directly computing + -- score increases speed by up to 40% (on small frequent input word with + -- relatively wide candidate, such as file paths of nested directories). + local rev_first = n_candidate - pos_last + 1 + for i = #letters - 1, 1, -1 do + rev_first = rev_candidate:find(letters[i], rev_first + 1) + end + local first = n_candidate - rev_first + 1 + local width = math.min(pos_last - first + 1, cutoff) + + -- Using strict sign is crucial because when two last letter matches result + -- into positions with similar width, the one which was created earlier + -- (i.e. with smaller last letter match) will have smaller first letter + -- match (hence better score). + if width < best_width then + best_pos_last, best_width = pos_last, width + end + + -- Advance iteration + pos_last = candidate:find(letters[n_letters], pos_last + 1) + end + + -- Actually compute best matched positions from best last letter match + local best_positions = { best_pos_last } + local rev_pos = n_candidate - best_pos_last + 1 + for i = #letters - 1, 1, -1 do + rev_pos = rev_candidate:find(letters[i], rev_pos + 1) + -- For relatively small number of letters (around 10, which is main use + -- case) inserting to front seems to have better performance than + -- inserting at end and then reversing. + table.insert(best_positions, 1, n_candidate - rev_pos + 1) + end + + return best_positions +end + +-- Compute score of matched positions. Smaller values indicate better match +-- (i.e. like distance). Reasoning behind the score is for it to produce the +-- same ordering as with sequential comparison of match's width and first +-- position. So it shouldn't really be perceived as linear distance (difference +-- between scores don't really matter, only their comparison with each other). +-- +-- Reasoning behind comparison logic (based on 'time' input): +-- - '_time' is better than 't_ime' (width is smaller). +-- - 'time_aa' is better than 'aa_time' (same width, first match is smaller). +-- +-- Returns -1 if `positions` is `nil` or empty. +H.score_positions = function(positions) + if not positions or #positions == 0 then return -1 end + local first, last = positions[1], positions[#positions] + local cutoff = H.get_config().cutoff + return cutoff * math.min(last - first + 1, cutoff) + math.min(first, cutoff) +end + +H.make_filter_indexes = function(word, candidate_array) + -- Precompute a table of word's letters + local letters = H.string_to_letters(word) + + local res = {} + for i, cand in ipairs(candidate_array) do + local positions = H.find_best_positions(letters, cand) + if positions then table.insert(res, { index = i, score = H.score_positions(positions) }) end + end + + return res +end + +H.compare_filter_indexes = function(a, b) + if a.score < b.score then return true end + + if a.score == b.score then + -- Make sorting stable by preserving index order + return a.index < b.index + end + + return false +end + +H.filter_by_indexes = function(candidate_array, ids) + local res, res_ids = {}, {} + for _, id in pairs(ids) do + table.insert(res, candidate_array[id.index]) + table.insert(res_ids, id.index) + end + + return res, res_ids +end + +-- Utilities ------------------------------------------------------------------ +H.string_to_letters = function(s) return vim.tbl_map(vim.pesc, vim.split(s, '')) end + +return MiniFuzzy diff --git a/dotfiles/pack/plugins/start/mini.nvim/lua/mini/indentscope.lua b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/indentscope.lua new file mode 100755 index 0000000..3ad1e3a --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/indentscope.lua @@ -0,0 +1,1116 @@ +-- MIT License Copyright (c) 2022 Evgeni Chasnovski + +-- Documentation ============================================================== +--- 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. +---@tag mini.indentscope +---@tag MiniIndentscope + +--- 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. +---@tag MiniIndentscope-drawing + +-- Notes about implementation: +-- - Tried and rejected features/optimizations: +-- - Gap at cursor. Intended to always show cursor at normal state. It +-- might be more visually pleasing and more convenient when start typing +-- over indicator. Couldn't properly do that because couldn't find an +-- appropriate (fast, non-blocking, without much code complexity, +-- low-flickering) way to do that. There was an idea of making draw +-- function not draw at cursor and update only cursor gap when it was +-- enough, but there was slight flickering and too much code complexity. +-- - Draw only inside current window view (from top visible line to bottom +-- one). Would decrease workload. Couldn't properly do that because there +-- is early screen redraw after `WinScrolled` which introduced flickering +-- when scrolling (so it was `WinScrolled` -> redraw with current +-- extmarks -> redraw with new extmarks). +-- - Manual tests to check proper drawing (not sure how to autotest this): +-- - Moving cursor faster than debounce delay should not initiate drawing. +-- - Extmark on cursor line should show right after debounce delay. Other +-- steps (if present) should use animation function. +-- - Usual typing on new line without decreasing indent should immediately +-- update scope without animation (although it is a different scope). +-- - Moving cursor within same scope when it is already drawing shouldn't +-- stop drawing. +-- - Fast consecutive scrolling within big scope (try `` and ``) +-- shouldn't cause flicker. +-- - Manual tests for textobjects and motions: +-- - Dot-repeat in operator-pending mode should work (like `dii` or `d[i` +-- and then `.` in other place). +-- - Different options should be properly handled (like using body line +-- when border line is not present). + +-- Module definition ========================================================== +local MiniIndentscope = {} +local H = {} + +--- Module setup +--- +---@param config table Module config table. See |MiniIndentscope.config|. +--- +---@usage `require('mini.indentscope').setup({})` (replace `{}` with your `config` table) +MiniIndentscope.setup = function(config) + -- Export module + _G.MiniIndentscope = MiniIndentscope + + -- Setup config + config = H.setup_config(config) + + -- Apply config + H.apply_config(config) + + -- Module behavior + vim.api.nvim_exec( + [[augroup MiniIndentscope + au! + au CursorMoved,CursorMovedI * lua MiniIndentscope.auto_draw({ lazy = true }) + au TextChanged,TextChangedI,TextChangedP,WinScrolled * lua MiniIndentscope.auto_draw() + augroup END]], + false + ) + + if vim.fn.exists('##ModeChanged') == 1 then + vim.api.nvim_exec( + -- Call `auto_draw` on mode change to respect `miniindentscope_disable` + [[augroup MiniIndentscope + au ModeChanged *:* lua MiniIndentscope.auto_draw({ lazy = true }) + augroup END]], + false + ) + end + + -- Create highlighting + vim.api.nvim_exec( + [[hi default link MiniIndentscopeSymbol Delimiter + hi MiniIndentscopePrefix guifg=NONE guibg=NONE gui=nocombine]], + false + ) +end + +--- Module config +--- +--- Default values: +---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section) +---@text # 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.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')`. + --minidoc_replace_start animation = --, + animation = function(s, n) return 20 end, + --minidoc_replace_end + }, + + -- 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 = '╎', +} +--minidoc_afterlines_end + +-- Module functionality ======================================================= +--- 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. +--- +---@param line number Input line number (starts from 1). Can be modified to a +--- neighbor if `try_as_border` is `true`. Default: cursor line. +---@param 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. +---@param opts table Options to override global or buffer local ones (see +--- |MiniIndentscope.config|). +--- +---@return table Table with scope information: +--- - - table with (top line of scope, inclusive), +--- (bottom line of scope, inclusive), and (minimum indent withing +--- scope) keys. Line numbers start at 1. +--- - - table with (line of top border, might be `nil`), +--- (line of bottom border, might be `nil`), and (indent +--- of border) keys. Line numbers start at 1. +--- - - identifier of current buffer. +--- - - table with (reference line), (reference +--- column), and ("indent at column") keys. +MiniIndentscope.get_scope = function(line, col, opts) + opts = H.get_config({ options = opts }).options + + -- Compute default `line` and\or `col` + if not (line and col) then + local curpos = vim.fn.getcurpos() + + line = line or curpos[2] + line = opts.try_as_border and H.border_correctors[opts.border](line, opts) or line + + -- Use `curpos[5]` (`curswant`, see `:h getcurpos()`) to account for blank + -- and empty lines. + col = col or (opts.indent_at_cursor and curpos[5] or math.huge) + end + + -- Compute "indent at column" + local line_indent = H.get_line_indent(line, opts) + local indent = math.min(col, line_indent) + + -- Make early return + local body = { indent = indent } + if indent <= 0 then + body.top, body.bottom, body.indent = 1, vim.fn.line('$'), line_indent + else + local up_min_indent, down_min_indent + body.top, up_min_indent = H.cast_ray(line, indent, 'up', opts) + body.bottom, down_min_indent = H.cast_ray(line, indent, 'down', opts) + body.indent = math.min(line_indent, up_min_indent, down_min_indent) + end + + return { + body = body, + border = H.border_from_body[opts.border](body, opts), + buf_id = vim.api.nvim_get_current_buf(), + reference = { line = line, column = col, indent = indent }, + } +end + +--- 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|. +--- +---@param opts table Options. +MiniIndentscope.auto_draw = function(opts) + if H.is_disabled() then + H.undraw_scope() + return + end + + opts = opts or {} + local scope = MiniIndentscope.get_scope() + + -- Make early return if nothing has to be done. Doing this before updating + -- event id allows to not interrupt ongoing animation. + if opts.lazy and H.current.draw_status ~= 'none' and H.scope_is_equal(scope, H.current.scope) then return end + + -- Account for current event + local local_event_id = H.current.event_id + 1 + H.current.event_id = local_event_id + + -- Compute drawing options for current event + local draw_opts = H.make_autodraw_opts(scope) + + -- Allow delay + if draw_opts.delay > 0 then H.undraw_scope(draw_opts) end + + -- Use `defer_fn()` even if `delay` is 0 to draw indicator only after all + -- events are processed (stops flickering) + vim.defer_fn(function() + if H.current.event_id ~= local_event_id then return end + + H.undraw_scope(draw_opts) + + H.current.scope = scope + H.draw_scope(scope, draw_opts) + end, draw_opts.delay) +end + +--- 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. +--- +---@param scope table Scope. Default: output of |MiniIndentscope.get_scope| +--- with default arguments. +---@param opts table Options. Currently supported: +--- - - animation function for drawing. See +--- |MiniIndentscope-drawing| and |MiniIndentscope.gen_animation()|. +MiniIndentscope.draw = function(scope, opts) + scope = scope or MiniIndentscope.get_scope() + local draw_opts = vim.tbl_deep_extend('force', { animation_fun = H.get_config().draw.animation }, opts or {}) + + H.undraw_scope() + + H.current.scope = scope + H.draw_scope(scope, draw_opts) +end + +--- Undraw currently visible scope manually +MiniIndentscope.undraw = function() H.undraw_scope() end + +--- 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. +--- +---@param easing string One of supported easing types. +---@param opts table Options that control progression. Possible keys: +--- - `(number)` - duration (in ms) of a unit. Default: 20. +--- - `(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' })` +--- +---@seealso |MiniIndentscope-drawing| for more information about how drawing is done. +MiniIndentscope.gen_animation = function(easing, opts) + --stylua: ignore start + if easing == 'none' then + return function() return 0 end + end + + opts = vim.tbl_deep_extend('force', { duration = 20, unit = 'step' }, opts or {}) + if not vim.tbl_contains({'total', 'step'}, opts.unit) then + H.message([[`opts.unit` should be one of 'step' or 'total'. Using 'step'.]]) + opts.unit = 'step' + end + + local easing_calls = { + linear = {impl = H.animation_arithmetic_powers, args = {0, 'in', opts}}, + quadraticIn = {impl = H.animation_arithmetic_powers, args = {1, 'in', opts}}, + quadraticOut = {impl = H.animation_arithmetic_powers, args = {1, 'out', opts}}, + quadraticInOut = {impl = H.animation_arithmetic_powers, args = {1, 'in-out', opts}}, + cubicIn = {impl = H.animation_arithmetic_powers, args = {2, 'in', opts}}, + cubicOut = {impl = H.animation_arithmetic_powers, args = {2, 'out', opts}}, + cubicInOut = {impl = H.animation_arithmetic_powers, args = {2, 'in-out', opts}}, + quarticIn = {impl = H.animation_arithmetic_powers, args = {3, 'in', opts}}, + quarticOut = {impl = H.animation_arithmetic_powers, args = {3, 'out', opts}}, + quarticInOut = {impl = H.animation_arithmetic_powers, args = {3, 'in-out', opts}}, + exponentialIn = {impl = H.animation_geometrical_powers, args = {'in', opts}}, + exponentialOut = {impl = H.animation_geometrical_powers, args = {'out', opts}}, + exponentialInOut = {impl = H.animation_geometrical_powers, args = {'in-out', opts}}, + } + local allowed_easing_types = vim.tbl_keys(easing_calls) + table.sort(allowed_easing_types) + + if not vim.tbl_contains(allowed_easing_types, easing) then + H.message(('`easing` should be one of: %s.'):format(table.concat(allowed_easing_types, ', '))) + return + end + + local parts = easing_calls[easing] + return parts.impl(unpack(parts.args)) + --stylua: ignore end +end + +--- Move cursor within scope +--- +--- Cursor is placed on a first non-blank character of target line. +--- +---@param side string One of "top" or "bottom". +---@param use_border boolean Whether to move to border or withing scope's body. +--- If particular border is absent, body is used. +---@param scope table Scope to use. Default: output of |MiniIndentscope.get_scope()|. +MiniIndentscope.move_cursor = function(side, use_border, scope) + scope = scope or MiniIndentscope.get_scope() + + -- This defaults to body's side if it is not present in border + local target_line = use_border and scope.border[side] or scope.body[side] + target_line = math.min(math.max(target_line, 1), vim.fn.line('$')) + + vim.api.nvim_win_set_cursor(0, { target_line, 0 }) + -- Move to first non-blank character to allow chaining scopes + vim.cmd('normal! ^') +end + +--- 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). +--- +---@param side string One of "top" or "bottom". +---@param add_to_jumplist boolean Whether to add movement to jump list. It is +--- `true` only for Normal mode mappings. +MiniIndentscope.operator = function(side, add_to_jumplist) + local scope = MiniIndentscope.get_scope() + + -- Don't support scope that can't be shown + if H.scope_get_draw_indent(scope) < 0 then return end + + -- Add movement to jump list. Needs remembering `count1` before that because + -- it seems to reset it to 1. + local count = vim.v.count1 + if add_to_jumplist then vim.cmd('normal! m`') end + + -- Make sequence of jumps + for _ = 1, count do + MiniIndentscope.move_cursor(side, true, scope) + -- Use `try_as_border = false` to enable chaining + scope = MiniIndentscope.get_scope(nil, nil, { try_as_border = false }) + + -- Don't support scope that can't be shown + if H.scope_get_draw_indent(scope) < 0 then return end + end +end + +--- 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). +--- +---@param use_border boolean Whether to include border in textobject. When +--- `true` and `try_as_border` option is `false`, allows "chaining" calls for +--- incremental selection. +MiniIndentscope.textobject = function(use_border) + local scope = MiniIndentscope.get_scope() + + -- Don't support scope that can't be shown + if H.scope_get_draw_indent(scope) < 0 then return end + + -- Allow chaining only if using border + local count = use_border and vim.v.count1 or 1 + + -- Make sequence of incremental selections + for _ = 1, count do + -- Try finish cursor on border + local start, finish = 'top', 'bottom' + if use_border and scope.border.bottom == nil then + start, finish = 'bottom', 'top' + end + + H.exit_visual_mode() + MiniIndentscope.move_cursor(start, use_border, scope) + vim.cmd('normal! V') + MiniIndentscope.move_cursor(finish, use_border, scope) + + -- Use `try_as_border = false` to enable chaining + scope = MiniIndentscope.get_scope(nil, nil, { try_as_border = false }) + + -- Don't support scope that can't be shown + if H.scope_get_draw_indent(scope) < 0 then return end + end +end + +-- Helper data ================================================================ +-- Module default config +H.default_config = MiniIndentscope.config + +-- Namespace for drawing vertical line +H.ns_id = vim.api.nvim_create_namespace('MiniIndentscope') + +-- Timer for doing animation +H.timer = vim.loop.new_timer() + +-- Table with current relevalnt data: +-- - `event_id` - counter for events. +-- - `scope` - latest drawn scope. +-- - `draw_status` - status of current drawing. +H.current = { event_id = 0, scope = {}, draw_status = 'none' } + +-- Functions to compute indent in ambiguous cases +H.indent_funs = { + ['min'] = function(top_indent, bottom_indent) return math.min(top_indent, bottom_indent) end, + ['max'] = function(top_indent, bottom_indent) return math.max(top_indent, bottom_indent) end, + ['top'] = function(top_indent, bottom_indent) return top_indent end, + ['bottom'] = function(top_indent, bottom_indent) return bottom_indent end, +} + +-- Functions to compute indent of blank line to satisfy `config.options.border` +H.blank_indent_funs = { + ['none'] = H.indent_funs.min, + ['top'] = H.indent_funs.bottom, + ['bottom'] = H.indent_funs.top, + ['both'] = H.indent_funs.max, +} + +-- Functions to compute border from body +H.border_from_body = { + ['none'] = function(body, opts) return {} end, + ['top'] = function(body, opts) return { top = body.top - 1, indent = H.get_line_indent(body.top - 1, opts) } end, + ['bottom'] = function(body, opts) return { bottom = body.bottom + 1, indent = H.get_line_indent(body.bottom + 1, opts) } end, + ['both'] = function(body, opts) + return { + top = body.top - 1, + bottom = body.bottom + 1, + indent = math.max(H.get_line_indent(body.top - 1, opts), H.get_line_indent(body.bottom + 1, opts)), + } + end, +} + +-- Functions to correct line in case it is a border +H.border_correctors = { + ['none'] = function(line, opts) return line end, + ['top'] = function(line, opts) + local cur_indent, next_indent = H.get_line_indent(line, opts), H.get_line_indent(line + 1, opts) + return (cur_indent < next_indent) and (line + 1) or line + end, + ['bottom'] = function(line, opts) + local prev_indent, cur_indent = H.get_line_indent(line - 1, opts), H.get_line_indent(line, opts) + return (cur_indent < prev_indent) and (line - 1) or line + end, + ['both'] = function(line, opts) + local prev_indent, cur_indent, next_indent = + H.get_line_indent(line - 1, opts), H.get_line_indent(line, opts), H.get_line_indent(line + 1, opts) + + if prev_indent <= cur_indent and next_indent <= cur_indent then return line end + + -- If prev and next indents are equal and bigger than current, prefer next + if prev_indent <= next_indent then return line + 1 end + + return line - 1 + end, +} + +-- Helper functionality ======================================================= +-- Settings ------------------------------------------------------------------- +H.setup_config = function(config) + -- General idea: if some table elements are not present in user-supplied + -- `config`, take them from default config + vim.validate({ config = { config, 'table', true } }) + config = vim.tbl_deep_extend('force', H.default_config, config or {}) + + -- Validate per nesting level to produce correct error message + vim.validate({ + draw = { config.draw, 'table' }, + mappings = { config.mappings, 'table' }, + options = { config.options, 'table' }, + symbol = { config.symbol, 'string' }, + }) + + vim.validate({ + ['draw.delay'] = { config.draw.delay, 'number' }, + ['draw.animation'] = { config.draw.animation, 'function' }, + + ['mappings.object_scope'] = { config.mappings.object_scope, 'string' }, + ['mappings.object_scope_with_border'] = { config.mappings.object_scope_with_border, 'string' }, + ['mappings.goto_top'] = { config.mappings.goto_top, 'string' }, + ['mappings.goto_bottom'] = { config.mappings.goto_bottom, 'string' }, + + ['options.border'] = { config.options.border, 'string' }, + ['options.indent_at_cursor'] = { config.options.indent_at_cursor, 'boolean' }, + ['options.try_as_border'] = { config.options.try_as_border, 'boolean' }, + }) + return config +end + +H.apply_config = function(config) + MiniIndentscope.config = config + local maps = config.mappings + + --stylua: ignore start + H.map('n', maps.goto_top, [[lua MiniIndentscope.operator('top', true)]], { desc = 'Go to indent scope top' }) + H.map('n', maps.goto_bottom, [[lua MiniIndentscope.operator('bottom', true)]], { desc = 'Go to indent scope bottom' }) + + H.map('x', maps.goto_top, [[lua MiniIndentscope.operator('top')]], { desc = 'Go to indent scope top' }) + H.map('x', maps.goto_bottom, [[lua MiniIndentscope.operator('bottom')]], { desc = 'Go to indent scope bottom' }) + H.map('x', maps.object_scope, 'lua MiniIndentscope.textobject(false)', { desc = 'Object scope' }) + H.map('x', maps.object_scope_with_border, 'lua MiniIndentscope.textobject(true)', { desc = 'Object scope with border' }) + + H.map('o', maps.goto_top, [[lua MiniIndentscope.operator('top')]], { desc = 'Go to indent scope top' }) + H.map('o', maps.goto_bottom, [[lua MiniIndentscope.operator('bottom')]], { desc = 'Go to indent scope bottom' }) + H.map('o', maps.object_scope, 'lua MiniIndentscope.textobject(false)', { desc = 'Object scope' }) + H.map('o', maps.object_scope_with_border, 'lua MiniIndentscope.textobject(true)', { desc = 'Object scope with border' }) + --stylua: ignore start +end + +H.is_disabled = function() return vim.g.miniindentscope_disable == true or vim.b.miniindentscope_disable == true end + +H.get_config = function(config) + return vim.tbl_deep_extend('force', MiniIndentscope.config, vim.b.miniindentscope_config or {}, config or {}) +end + +-- Scope ---------------------------------------------------------------------- +-- Line indent: +-- - Equals output of `vim.fn.indent()` in case of non-blank line. +-- - Depends on `MiniIndentscope.config.options.border` in such way so as to +-- ignore blank lines before line not recognized as border. +H.get_line_indent = function(line, opts) + local prev_nonblank = vim.fn.prevnonblank(line) + local res = vim.fn.indent(prev_nonblank) + + -- Compute indent of blank line depending on `options.border` values + if line ~= prev_nonblank then + local next_indent = vim.fn.indent(vim.fn.nextnonblank(line)) + local blank_rule = H.blank_indent_funs[opts.border] + res = blank_rule(res, next_indent) + end + + return res +end + +H.cast_ray = function(line, indent, direction, opts) + local final_line, increment = 1, -1 + if direction == 'down' then + final_line, increment = vim.fn.line('$'), 1 + end + + local min_indent = math.huge + for l = line, final_line, increment do + local new_indent = H.get_line_indent(l + increment, opts) + if new_indent < indent then return l, min_indent end + if new_indent < min_indent then min_indent = new_indent end + end + + return final_line, min_indent +end + +H.scope_get_draw_indent = function(scope) return scope.border.indent or (scope.body.indent - 1) end + +H.scope_is_equal = function(scope_1, scope_2) + if type(scope_1) ~= 'table' or type(scope_2) ~= 'table' then return false end + + return scope_1.buf_id == scope_2.buf_id + and H.scope_get_draw_indent(scope_1) == H.scope_get_draw_indent(scope_2) + and scope_1.body.top == scope_2.body.top + and scope_1.body.bottom == scope_2.body.bottom +end + +H.scope_has_intersect = function(scope_1, scope_2) + if type(scope_1) ~= 'table' or type(scope_2) ~= 'table' then return false end + if (scope_1.buf_id ~= scope_2.buf_id) or (H.scope_get_draw_indent(scope_1) ~= H.scope_get_draw_indent(scope_2)) then + return false + end + + local body_1, body_2 = scope_1.body, scope_2.body + return (body_2.top <= body_1.top and body_1.top <= body_2.bottom) + or (body_1.top <= body_2.top and body_2.top <= body_1.bottom) +end + +-- Indicator ------------------------------------------------------------------ +--- Compute indicator of scope to be displayed +--- +--- Indicator is visual representation of scope in current window view using +--- extmarks. Currently only needed because Neovim can't correctly process +--- horizontal window scroll (Neovim issue: +--- https://github.com/neovim/neovim/issues/14050) +--- +---@return table|nil Table with indicator info or empty one in case indicator +--- shouldn't be drawn. +---@private +H.indicator_compute = function(scope) + scope = scope or H.current.scope + local indent = H.scope_get_draw_indent(scope) + + -- Don't draw indicator that should be outside of screen. This condition is + -- (perpusfully) "responsible" for not drawing indicator spanning whole file. + if indent < 0 then return {} end + + -- Extmarks will be located at column zero but show indented text: + -- - This allows showing line even on empty lines. + -- - Text indentation should depend on current window view because extmarks + -- can't scroll to be past left window side. Sources: + -- - Neovim issue: https://github.com/neovim/neovim/issues/14050 + -- - Used fix: https://github.com/lukas-reineke/indent-blankline.nvim/pull/155 + local leftcol = vim.fn.winsaveview().leftcol + if indent < leftcol then return {} end + + -- Usage separate highlight groups for prefix and symbol allows cursor to be + -- "natural" when on the left of indicator line (like on empty lines) + local virt_text = { { H.get_config().symbol, 'MiniIndentscopeSymbol' } } + local prefix = string.rep(' ', indent - leftcol) + -- Currently Neovim doesn't work when text for extmark is empty string + if prefix:len() > 0 then table.insert(virt_text, 1, { prefix, 'MiniIndentscopePrefix' }) end + + local top = scope.body.top + local bottom = scope.body.bottom + + return { buf_id = vim.api.nvim_get_current_buf(), virt_text = virt_text, top = top, bottom = bottom } +end + +-- Drawing -------------------------------------------------------------------- +H.draw_scope = function(scope, opts) + scope = scope or {} + opts = opts or {} + + local indicator = H.indicator_compute(scope) + + -- Don't draw anything if nothing to be displayed + if indicator.virt_text == nil or #indicator.virt_text == 0 then + H.current.draw_status = 'finished' + return + end + + -- Make drawing function + local draw_fun = H.make_draw_function(indicator, opts) + + -- Perform drawing + H.current.draw_status = 'drawing' + H.draw_indicator_animation(indicator, draw_fun, opts.animation_fun) +end + +H.draw_indicator_animation = function(indicator, draw_fun, animation_fun) + -- Draw from origin (cursor line but wihtin indicator range) + local top, bottom = indicator.top, indicator.bottom + local origin = math.min(math.max(vim.fn.line('.'), top), bottom) + + local step = 0 + local n_steps = math.max(origin - top, bottom - origin) + local wait_time = 0 + + local draw_step + draw_step = vim.schedule_wrap(function() + -- Check for not drawing outside of interval is done inside `draw_fun` + local success = draw_fun(origin - step) + if step > 0 then success = success and draw_fun(origin + step) end + + if not success or step == n_steps then + H.current.draw_status = step == n_steps and 'finished' or H.current.draw_status + H.timer:stop() + return + end + + step = step + 1 + wait_time = wait_time + animation_fun(step, n_steps) + + -- Repeat value of `timer` seems to be rounded down to milliseconds. This + -- means that values less than 1 will lead to timer stop repeating. Instead + -- call next step function directly. + if wait_time < 1 then + H.timer:set_repeat(0) + -- Use `return` to make this proper "tail call" + return draw_step() + else + H.timer:set_repeat(wait_time) + + -- Restart `wait_time` only if it is actually used + wait_time = 0 + + -- Usage of `again()` is needed to overcome the fact that it is called + -- inside callback and to restart initial timer. Mainly this is needed + -- only in case of transition from 'non-repeating' timer to 'repeating' + -- one in case of complex animation functions. See + -- https://docs.libuv.org/en/v1.x/timer.html#api + H.timer:again() + end + end) + + -- Start non-repeating timer without callback execution. This shouldn't be + -- `timer:start(0, 0, draw_step)` because it will execute `draw_step` on the + -- next redraw (flickers on window scroll). + H.timer:start(10000000, 0, draw_step) + + -- Draw step zero (at origin) immediately + draw_step() +end + +H.undraw_scope = function(opts) + opts = opts or {} + + -- Don't operate outside of current event if able to verify + if opts.event_id and opts.event_id ~= H.current.event_id then return end + + pcall(vim.api.nvim_buf_clear_namespace, H.current.scope.buf_id or 0, H.ns_id, 0, -1) + + H.current.draw_status = 'none' + H.current.scope = {} +end + +H.make_autodraw_opts = function(scope) + local config = H.get_config() + local res = { + event_id = H.current.event_id, + type = 'animation', + delay = config.draw.delay, + animation_fun = config.draw.animation, + } + + if H.current.draw_status == 'none' then return res end + + -- Draw immediately scope which intersects (same indent, overlapping ranges) + -- currently drawn or finished. This is more natural when typing text. + if H.scope_has_intersect(scope, H.current.scope) then + res.type = 'immediate' + res.delay = 0 + res.animation_fun = MiniIndentscope.gen_animation('none') + return res + end + + return res +end + +H.make_draw_function = function(indicator, opts) + local extmark_opts = { + hl_mode = 'combine', + priority = 2, + right_gravity = false, + virt_text = indicator.virt_text, + virt_text_pos = 'overlay', + } + + local current_event_id = opts.event_id + + return function(l) + -- Don't draw if outdated + if H.current.event_id ~= current_event_id and current_event_id ~= nil then return false end + + -- Don't draw if disabled + if H.is_disabled() then return false end + + -- Don't put extmark outside of indicator range + if not (indicator.top <= l and l <= indicator.bottom) then return true end + + return pcall(vim.api.nvim_buf_set_extmark, indicator.buf_id, H.ns_id, l - 1, 0, extmark_opts) + end +end + +-- Animations ----------------------------------------------------------------- +--- Imitate common power easing function +--- +--- Every step is preceeded by waiting time decreasing/increasing in power +--- series fashion (`d` is "delta", ensures total duration time): +--- - "in": d*n^p; d*(n-1)^p; ... ; d*2^p; d*1^p +--- - "out": d*1^p; d*2^p; ... ; d*(n-1)^p; d*n^p +--- - "in-out": "in" until 0.5*n, "out" afterwards +--- +--- This way it imitates `power + 1` common easing function because animation +--- progression behaves as sum of `power` elements. +--- +---@param power number Power of series. +---@param type string One of "in", "out", "in-out". +---@param opts table Options from `MiniIndentscope.gen_animation()`. +---@private +H.animation_arithmetic_powers = function(power, type, opts) + -- Sum of first `n_steps` natural numbers raised to `power` + --stylua: ignore start + local arith_power_sum = ({ + [0] = function(n_steps) return n_steps end, + [1] = function(n_steps) return n_steps * (n_steps + 1) / 2 end, + [2] = function(n_steps) return n_steps * (n_steps + 1) * (2 * n_steps + 1) / 6 end, + [3] = function(n_steps) return n_steps ^ 2 * (n_steps + 1) ^ 2 / 4 end, + })[power] + --stylua: ignore end + + -- Function which computes common delta so that overall duration will have + -- desired value (based on supplied `opts`) + local duration_unit, duration_value = opts.unit, opts.duration + local make_delta = function(n_steps, is_in_out) + local total_time = duration_unit == 'total' and duration_value or (duration_value * n_steps) + local total_parts + if is_in_out then + -- Examples: + -- - n_steps=5: 3^d, 2^d, 1^d, 2^d, 3^d + -- - n_steps=6: 3^d, 2^d, 1^d, 1^d, 2^d, 3^d + total_parts = 2 * arith_power_sum(math.ceil(0.5 * n_steps)) - (n_steps % 2 == 1 and 1 or 0) + else + total_parts = arith_power_sum(n_steps) + end + return total_time / total_parts + end + + return ({ + ['in'] = function(s, n) return make_delta(n) * (n - s + 1) ^ power end, + ['out'] = function(s, n) return make_delta(n) * s ^ power end, + ['in-out'] = function(s, n) + local n_half = math.ceil(0.5 * n) + local s_halved + if n % 2 == 0 then + s_halved = s <= n_half and (n_half - s + 1) or (s - n_half) + else + s_halved = s < n_half and (n_half - s + 1) or (s - n_half + 1) + end + return make_delta(n, true) * s_halved ^ power + end, + })[type] +end + +--- Imitate common exponential easing function +--- +--- Every step is preceeded by waiting time decreasing/increasing in geometric +--- progression fashion (`d` is 'delta', ensures total duration time): +--- - 'in': (d-1)*d^(n-1); (d-1)*d^(n-2); ...; (d-1)*d^1; (d-1)*d^0 +--- - 'out': (d-1)*d^0; (d-1)*d^1; ...; (d-1)*d^(n-2); (d-1)*d^(n-1) +--- - 'in-out': 'in' until 0.5*n, 'out' afterwards +--- +---@param type string One of "in", "out", "in-out". +---@param opts table Options from `MiniIndentscope.gen_animation()`. +---@private +H.animation_geometrical_powers = function(type, opts) + -- Function which computes common delta so that overall duration will have + -- desired value (based on supplied `opts`) + local duration_unit, duration_value = opts.unit, opts.duration + local make_delta = function(n_steps, is_in_out) + local total_time = duration_unit == 'step' and (duration_value * n_steps) or duration_value + -- Exact solution to avoid possible (bad) approximation + if n_steps == 1 then return total_time + 1 end + if is_in_out then + local n_half = math.ceil(0.5 * n_steps) + -- Example for n_steps=6: + -- Steps: (d-1)*d^2, (d-1)*d^1, (d-1)*d^0, (d-1)*d^0, (d-1)*d^1, (d-1)*d^2 + -- Sum: 2 * (d - 1) * (d^0 + d^1 + d^2) = 2 * (d^3 - 1) + -- Solution: 2 * (d^3 - 1) = total_time => + -- d = math.pow(0.5 * total_time + 1, 1 / 3) + -- + -- Example for n_steps=5: + -- Steps: (d-1)*d^2, (d-1)*d^1, (d-1)*d^0, (d-1)*d^1, (d-1)*d^2 + -- Sum: 2 * (d - 1) * (d^0 + d^1 + d^2) - (d - 1) = 2 * (d^3 - 1) - (d - 1) + -- Solution: 2 * (d^3 - 1) - (d - 1) = total_time => + -- As there is no general explicit solution, use approximation => + -- (Exact solution without `- (d-1)`): + -- d_0 = math.pow(0.5 * total_time + 1, 1 / 3); + -- (Correction by solving exactly withtou `- (d-1)` for + -- `total_time_corr = total_time + (d_0 - 1)`): + -- d_1 = math.pow(0.5 * total_time_corr + 1, 1 / 3) + if n_steps % 2 == 1 then total_time = total_time + math.pow(0.5 * total_time + 1, 1 / n_half) - 1 end + return math.pow(0.5 * total_time + 1, 1 / n_half) + end + return math.pow(total_time + 1, 1 / n_steps) + end + + return ({ + ['in'] = function(s, n) + local delta = make_delta(n) + return (delta - 1) * delta ^ (n - s) + end, + ['out'] = function(s, n) + local delta = make_delta(n) + return (delta - 1) * delta ^ (s - 1) + end, + ['in-out'] = function(s, n) + local n_half, delta = math.ceil(0.5 * n), make_delta(n, true) + local s_halved + if n % 2 == 0 then + s_halved = s <= n_half and (n_half - s) or (s - n_half - 1) + else + s_halved = s < n_half and (n_half - s) or (s - n_half) + end + return (delta - 1) * delta ^ s_halved + end, + })[type] +end + +-- Utilities ------------------------------------------------------------------ +H.message = function(msg) vim.cmd('echomsg ' .. vim.inspect('(mini.indentscope) ' .. msg)) end + +H.map = function(mode, key, rhs, opts) + if key == '' then return end + + opts = vim.tbl_deep_extend('force', { noremap = true, silent = true }, opts or {}) + + -- Use mapping description only in Neovim>=0.7 + if vim.fn.has('nvim-0.7') == 0 then opts.desc = nil end + + vim.api.nvim_set_keymap(mode, key, rhs, opts) +end + +H.exit_visual_mode = function() + local ctrl_v = vim.api.nvim_replace_termcodes('', true, true, true) + local cur_mode = vim.fn.mode() + if cur_mode == 'v' or cur_mode == 'V' or cur_mode == ctrl_v then vim.cmd('normal! ' .. cur_mode) end +end + +return MiniIndentscope diff --git a/dotfiles/pack/plugins/start/mini.nvim/lua/mini/init.lua b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/init.lua new file mode 100755 index 0000000..da2330e --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/init.lua @@ -0,0 +1,234 @@ +-- MIT License Copyright (c) 2021 Evgeni Chasnovski + +-- Documentation ============================================================== +--- *mini.txt* Collection of minimal, independent and fast Lua modules +--- +--- Author: Evgeni Chasnovski +--- License: MIT +--- +--- |mini.nvim| is a collection of minimal, independent, and fast Lua modules +--- dedicated to improve Neovim (version 0.5 and higher) experience. Each +--- module can be considered as a separate sub-plugin. +--- +--- Table of contents: +--- General overview.................................................|mini.nvim| +--- Disabling recepies.............................|mini.nvim-disabling-recipes| +--- Buffer-local config..........................|mini.nvim-buffer-local-config| +--- Plugin colorschemes.....................................|mini-color-schemes| +--- Extend and create a/i textobjects..................................|mini.ai| +--- Align text interactively........................................|mini.align| +--- Base16 colorscheme creation....................................|mini.base16| +--- Remove buffers..............................................|mini.bufremove| +--- Comment.......................................................|mini.comment| +--- Completion and signature help..............................|mini.completion| +--- Autohighlight word under cursor............................|mini.cursorword| +--- Generate Neovim help files........................................|mini.doc| +--- Fuzzy matching..................................................|mini.fuzzy| +--- Visualize and operate on indent scope.....................|mini.indentscope| +--- Jump forward/backward to a single character......................|mini.jump| +--- Jump within visible lines......................................|mini.jump2d| +--- Miscellaneous functions..........................................|mini.misc| +--- Autopairs.......................................................|mini.pairs| +--- Session management...........................................|mini.sessions| +--- Start screen..................................................|mini.starter| +--- Statusline.................................................|mini.statusline| +--- Surround actions.............................................|mini.surround| +--- Tabline.......................................................|mini.tabline| +--- Test Neovim plugins..............................................|mini.test| +--- Trailspace (highlight and remove)..........................|mini.trailspace| +--- +--- # General principles~ +--- +--- - . 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. +--- - . Modules are independent of each other and can be run +--- without external dependencies. Although some of them may need +--- dependencies for full experience. +--- - . 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". +--- - : +--- - Each module (if needed) should be setup separately with +--- `require().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 (after extending with default values) 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()`). +--- - . Each module can be additionally configured +--- to use certain runtime config settings locally to buffer. See +--- |mini.nvim-buffer-local-config| for more information. +--- - . Each module's core functionality can be disabled globally or +--- buffer-locally by creating appropriate global or buffer-scoped variables +--- equal to |v:true|. See |mini.nvim-disabling-recipes| for common recipes. +--- - . Appearance of module's output is controlled by +--- certain highlight group (see |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 colorschemes. +--- - . 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. +--- +--- # List of modules~ +--- +--- - |MiniAi| - extend and create `a`/`i` textobjects (like in `di(` or +--- `va"`). It enhances some builtin |text-objects| (like |a(|, |a)|, |a'|, +--- and more), creates new ones (like `a*`, `a`, `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. +--- - |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. +--- - |MiniBase16| - fast implementation of base16 theme for manually supplied +--- palette. Supports 30+ plugin integrations. Has unique palette generator +--- which needs only background and foreground colors. +--- - |MiniBufremove| - buffer removing (unshow, delete, wipeout) while saving +--- window layout. +--- - |MiniComment| - fast and familiar per-line code commenting. +--- - |MiniCompletion| - async (with customizable 'debounce' delay) 'two-stage +--- chain completion': first builtin LSP, then configurable fallback. Also +--- has functionality for completion item info and function signature (both +--- in floating window appearing after customizable delay). +--- - |MiniCursorword| - automatic highlighting of word under cursor (displayed +--- after customizable delay). Current word under cursor can be highlighted +--- differently. +--- - |MiniDoc| - generation of help files from EmmyLua-like annotations. +--- Allows flexible customization of output via hook functions. Used for +--- documenting this plugin. +--- - |MiniFuzzy| - functions for fast and simple fuzzy matching. It has +--- not only functions to perform fuzzy matching of one string to others, but +--- also a sorter for |telescope.nvim|. +--- - |MiniIndentscope| - Visualize and operate on indent scope. Supports +--- customization of debounce delay, animation style, and different +--- granularity of options for scope computing algorithm. +--- - |MiniJump| - minimal and fast module for smarter jumping to a single +--- character. +--- - |MiniJump2d| - minimal and fast Lua plugin for jumping (moving cursor) +--- within visible lines via iterative label filtering. Supports custom jump +--- targets (spots), labels, hooks, allowed windows and lines, and more. +--- - |MiniMisc| - collection of miscellaneous useful functions. Like `put()` +--- and `put_text()` which print Lua objects to command line and current +--- buffer respectively. +--- - |MiniPairs| - autopairs plugin which has minimal defaults and +--- functionality to do per-key expression mappings. +--- - |MiniSessions| - session management (read, write, delete) which works +--- using |mksession|. Implements both global (from configured directory) and +--- local (from current directory) sessions. +--- - |MiniStarter| - minimal, 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. +--- - |MiniStatusline| - minimal and fast statusline. Has ability to use custom +--- content supplied with concise function (using module's provided section +--- functions) along with builtin default. For full experience needs [Nerd +--- font](https://www.nerdfonts.com/), +--- [gitsigns.nvim](https://github.com/lewis6991/gitsigns.nvim) plugin, and +--- [nvim-web-devicons](https://github.com/kyazdani42/nvim-web-devicons) +--- plugin (but works without any them). +--- - |MiniSurround| - 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. +--- - |MiniTest| - framework for writing 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. +--- - |MiniTabline| - minimal tabline which always shows listed (see 'buflisted') +--- buffers. Allows showing extra information section in case of multiple vim +--- tabpages. For full experience needs +--- [nvim-web-devicons](https://github.com/kyazdani42/nvim-web-devicons). +--- - |MiniTrailspace| - automatic highlighting of trailing whitespace with +--- functionality to remove it. +---@tag mini.nvim + +--- Common recipes for disabling functionality +--- +--- Each module's core functionality can be disabled globally or buffer-locally +--- by creating appropriate global or buffer-scoped variables equal to +--- |v:true|. Functionality is disabled if at least one of `g:` or `b:` +--- variables is equal to `v:true`. +--- +--- Variable names have the same structure: `{g,b}:mini*_disable` where `*` is +--- module's lowercase name. For example, `g:minicursorword_disable` disables +--- |mini.cursorword| globally and `b:minicursorword_disable` - for +--- corresponding buffer. Note: in this section disabling 'mini.cursorword' is +--- used as example; everything holds for other module variables. +--- +--- Considering high number of different scenarios and customization intentions, +--- writing exact rules for disabling module's functionality is left to user. +--- +--- # Manual disabling~ +--- +--- - Disable globally: +--- Lua - `:lua vim.g.minicursorword_disable=true` +--- Vimscript - `:let g:minicursorword_disable=v:true` +--- - Disable for current buffer: +--- Lua - `:lua vim.b.minicursorword_disable=true` +--- Vimscript - `:let b:minicursorword_disable=v:true` +--- - Toggle (disable if enabled, enable if disabled): +--- Globally - `:lua vim.g.minicursorword_disable = not vim.g.minicursorword_disable` +--- For buffer - `:lua vim.b.minicursorword_disable = not vim.b.minicursorword_disable` +--- +--- # Automated disabling~ +--- +--- - Disable for a certain |filetype| (for example, "markdown"): +--- `autocmd Filetype markdown lua vim.b.minicursorword_disable = true` +--- - Enable only for certain filetypes (for example, "lua" and "python"): +--- `au FileType * if index(['lua', 'python'], &ft) < 0 | let b:minicursorword_disable=v:true | endif` +--- - Disable in Insert mode (use similar pattern for Terminal mode or indeed +--- any other mode change with |ModeChanged| starting from Neovim 0.7.0): +--- `au InsertEnter * lua vim.b.minicursorword_disable = true` +--- `au InsertLeave * lua vim.b.minicursorword_disable = false` +--- - Disable in Terminal buffer: +--- `au TermOpen * lua vim.b.minicursorword_disable = true` +---@tag mini.nvim-disabling-recipes + +--- Buffer local config +--- +--- Each module can be additionally configured locally to buffer by creating +--- appropriate buffer-scoped variable with values you want to override. It +--- will affect only runtime options and not those used once during setup (like +--- `mappings` or `set_vim_settings`). +--- +--- Variable names have the same structure: `b:mini*_config` where `*` is +--- module's lowercase name. For example, `b:minicursorword_config` can store +--- information about how |mini.cursorword| will act inside current buffer. Its +--- value should be a table with same structure as module's `config`. +--- Continuing example, `vim.b.minicursorword_config = { delay = 500 }` will +--- use delay 500 inside current buffer. +--- +--- Considering high number of different scenarios and customization intentions, +--- writing exact rules for module's buffer local configuration is left to +--- user. It is done in similar fashion to |mini.nvim-disabling-recipes|. +--- +--- Note: using function values inside buffer variables requires Neovim>=0.7. +---@tag mini.nvim-buffer-local-config + +vim.notify([[Do not `require('mini')` directly. Setup every module separately.]]) + +return {} diff --git a/dotfiles/pack/plugins/start/mini.nvim/lua/mini/jump.lua b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/jump.lua new file mode 100755 index 0000000..ccb96f6 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/jump.lua @@ -0,0 +1,492 @@ +-- MIT License Copyright (c) 2021 Evgeni Chasnovski, Adam Blažek + +-- Documentation ============================================================== +--- 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. +---@tag mini.jump +---@tag MiniJump + +---@alias __target string The string to jump to. +---@alias __backward boolean Whether to jump backward. +---@alias __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. +---@alias __n_times number Number of times to perform consecutive jumps. + +-- Module definition ========================================================== +local MiniJump = {} +local H = {} + +--- Module setup +--- +---@param config table Module config table. See |MiniJump.config|. +--- +---@usage `require('mini.jump').setup({})` (replace `{}` with your `config` table) +MiniJump.setup = function(config) + -- Export module + _G.MiniJump = MiniJump + + -- Setup config + config = H.setup_config(config) + + -- Apply config + H.apply_config(config) + + -- Module behavior + vim.api.nvim_exec( + [[augroup MiniJump + au! + au CursorMoved * lua MiniJump.on_cursormoved() + au BufLeave,InsertEnter * lua MiniJump.stop_jumping() + augroup END]], + false + ) + + -- Highlight groups + vim.cmd('hi default link MiniJump SpellRare') +end + +--- Module config +--- +--- Default values: +---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section) +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, + }, +} +--minidoc_afterlines_end + +-- Module data ================================================================ +--- 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 +--- +---@field target __target +---@field backward __backward +---@field till __till +---@field n_times __n_times +---@field mode string Mode of latest jump (output of |mode()| with non-zero argument). +---@field jumping boolean Whether module is currently in "jumping mode": usage of +--- |MiniJump.smart_jump| and all mappings won't require target. +---@text +--- Initial values: +---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section) +MiniJump.state = { + target = nil, + backward = false, + till = false, + n_times = 1, + mode = nil, + jumping = false, +} +--minidoc_afterlines_end + +-- Module functionality ======================================================= +--- 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. +--- +---@param target __target +---@param backward __backward +---@param till __till +---@param n_times __n_times +MiniJump.jump = function(target, backward, till, n_times) + if H.is_disabled() then return end + + -- Cache inputs for future use + H.update_state(target, backward, till, n_times) + + if MiniJump.state.target == nil then + H.message('Can not jump because there is no recent `target`.') + return + end + + -- Determine if target is present anywhere in order to correctly enter + -- jumping mode. If not, jumping mode is not possible. + local escaped_target = vim.fn.escape(MiniJump.state.target, [[\]]) + local search_pattern = ([[\V%s]]):format(escaped_target) + local target_is_present = vim.fn.search(search_pattern, 'wn') ~= 0 + if not target_is_present then return end + + -- Construct search and highlight pattern data + local pattern, hl_pattern, flags = H.make_search_data() + + -- Delay highlighting after stopping previous one + local config = H.get_config() + H.timers.highlight:stop() + H.timers.highlight:start( + -- Update highlighting immediately if any highlighting is already present + H.is_highlighting() and 0 or config.delay.highlight, + 0, + vim.schedule_wrap(function() H.highlight(hl_pattern) end) + ) + + -- Start idle timer after stopping previous one + H.timers.idle_stop:stop() + H.timers.idle_stop:start(config.delay.idle_stop, 0, vim.schedule_wrap(function() MiniJump.stop_jumping() end)) + + -- Make jump(s) + H.cache.n_cursor_moved = 0 + MiniJump.state.jumping = true + for _ = 1, MiniJump.state.n_times do + vim.fn.search(pattern, flags) + end + + -- Open enough folds to show jump + vim.cmd('normal! zv') + + -- Track cursor position to account for movement not caught by `CursorMoved` + H.cache.latest_cursor = H.get_cursor_data() +end + +--- 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. +--- +---@param backward __backward +---@param till __till +MiniJump.smart_jump = function(backward, till) + if H.is_disabled() then return end + + -- Jumping should stop after mode change (use `mode(1)` to track 'omap' case) + -- or if cursor has moved after latest jump + local has_changed_mode = MiniJump.state.mode ~= vim.fn.mode(1) + local has_changed_cursor = not vim.deep_equal(H.cache.latest_cursor, H.get_cursor_data()) + if has_changed_mode or has_changed_cursor then MiniJump.stop_jumping() end + + -- Ask for target only when needed + local target + if not MiniJump.state.jumping or MiniJump.state.target == nil then + target = H.get_target() + -- Stop if user supplied invalid target + if target == nil then return end + end + + H.update_state(target, backward, till, vim.v.count1) + + MiniJump.jump() +end + +--- 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. +--- +---@param backward __backward +---@param till __till +MiniJump.expr_jump = function(backward, till) + if H.is_disabled() then return '' end + + -- Always ask for `target` as this will be used only in operator-pending + -- mode. Dot-repeat will be implemented via expression-mapping. + local target = H.get_target() + -- Stop if user supplied invalid target + if target == nil then return end + H.update_state(target, backward, till, vim.v.count1) + + return vim.api.nvim_replace_termcodes('vlua MiniJump.jump()', true, true, true) +end + +--- Stop jumping +--- +--- Removes highlights (if any) and forces the next smart jump to prompt for +--- the target. Automatically called on appropriate Neovim |events|. +MiniJump.stop_jumping = function() + H.timers.highlight:stop() + H.timers.idle_stop:stop() + + MiniJump.state.jumping = false + + H.cache.n_cursor_moved = 0 + H.cache.latest_cursor = nil + + H.unhighlight() +end + +--- Act on |CursorMoved| +MiniJump.on_cursormoved = function() + -- Check if jumping to avoid unnecessary actions on every CursorMoved + if MiniJump.state.jumping then + H.cache.n_cursor_moved = H.cache.n_cursor_moved + 1 + -- Stop jumping only if `CursorMoved` was not a result of smart jump + if H.cache.n_cursor_moved > 1 then MiniJump.stop_jumping() end + end +end + +-- Helper data ================================================================ +-- Module default config +H.default_config = MiniJump.config + +-- Cache for various operations +H.cache = { + -- Counter of number of CursorMoved events + n_cursor_moved = 0, + + -- Latest cursor position data + latest_cursor = nil, +} + +-- Timers for different delay-related functionalities +H.timers = { highlight = vim.loop.new_timer(), idle_stop = vim.loop.new_timer() } + +-- Information about last match highlighting (stored *per window*): +-- - Key: windows' unique buffer identifiers. +-- - Value: table with: +-- - `id` field for match id (from `vim.fn.matchadd()`). +-- - `pattern` field for highlighted pattern. +H.window_matches = {} + +-- Helper functionality ======================================================= +-- Settings ------------------------------------------------------------------- +H.setup_config = function(config) + -- General idea: if some table elements are not present in user-supplied + -- `config`, take them from default config + vim.validate({ config = { config, 'table', true } }) + config = vim.tbl_deep_extend('force', H.default_config, config or {}) + + -- Validate per nesting level to produce correct error message + vim.validate({ + mappings = { config.mappings, 'table' }, + delay = { config.delay, 'table' }, + }) + + vim.validate({ + ['delay.highlight'] = { config.delay.highlight, 'number' }, + ['delay.idle_stop'] = { config.delay.idle_stop, 'number' }, + + ['mappings.forward'] = { config.mappings.forward, 'string' }, + ['mappings.backward'] = { config.mappings.backward, 'string' }, + ['mappings.forward_till'] = { config.mappings.forward_till, 'string' }, + ['mappings.backward_till'] = { config.mappings.backward_till, 'string' }, + ['mappings.repeat_jump'] = { config.mappings.repeat_jump, 'string' }, + }) + + return config +end + +H.apply_config = function(config) + MiniJump.config = config + + --stylua: ignore start + H.map('n', config.mappings.forward, 'lua MiniJump.smart_jump(false, false)', { desc = 'Jump forward' }) + H.map('n', config.mappings.backward, 'lua MiniJump.smart_jump(true, false)', { desc = 'Jump backward' }) + H.map('n', config.mappings.forward_till, 'lua MiniJump.smart_jump(false, true)', { desc = 'Jump forward till' }) + H.map('n', config.mappings.backward_till, 'lua MiniJump.smart_jump(true, true)', { desc = 'Jump backward till' }) + H.map('n', config.mappings.repeat_jump, 'lua MiniJump.jump()', { desc = 'Repeat jump' }) + + H.map('x', config.mappings.forward, 'lua MiniJump.smart_jump(false, false)', { desc = 'Jump forward' }) + H.map('x', config.mappings.backward, 'lua MiniJump.smart_jump(true, false)', { desc = 'Jump backward' }) + H.map('x', config.mappings.forward_till, 'lua MiniJump.smart_jump(false, true)', { desc = 'Jump forward till' }) + H.map('x', config.mappings.backward_till, 'lua MiniJump.smart_jump(true, true)', { desc = 'Jump backward till' }) + H.map('x', config.mappings.repeat_jump, 'lua MiniJump.jump()', { desc = 'Repeat jump' }) + + H.map('o', config.mappings.forward, 'v:lua.MiniJump.expr_jump(v:false, v:false)', { expr = true, desc = 'Jump forward' }) + H.map('o', config.mappings.backward, 'v:lua.MiniJump.expr_jump(v:true, v:false)', { expr = true, desc = 'Jump backward' }) + H.map('o', config.mappings.forward_till, 'v:lua.MiniJump.expr_jump(v:false, v:true)', { expr = true, desc = 'Jump forward till' }) + H.map('o', config.mappings.backward_till, 'v:lua.MiniJump.expr_jump(v:true, v:true)', { expr = true, desc = 'Jump backward till' }) + H.map('o', config.mappings.repeat_jump, 'v:lua.MiniJump.expr_jump()', { expr = true, desc = 'Repeat jump' }) + --stylua: ignore end +end + +H.is_disabled = function() return vim.g.minijump_disable == true or vim.b.minijump_disable == true end + +H.get_config = + function(config) return vim.tbl_deep_extend('force', MiniJump.config, vim.b.minijump_config or {}, config or {}) end + +-- Pattern matching ----------------------------------------------------------- +H.make_search_data = function() + local target = vim.fn.escape(MiniJump.state.target, [[\]]) + local backward, till = MiniJump.state.backward, MiniJump.state.till + + local flags = backward and 'Wb' or 'W' + local pattern, hl_pattern + + if till then + -- General logic: moving pattern should match just before/after target, + -- while highlight pattern should match target for every "movable" place. + -- Also allow checking for "just before/after" across lines by accepting + -- `\n` as possible match. + if backward then + -- NOTE: use `\@<=` instead of `\zs` because it behaves better in case of + -- consecutive matches (like `xxxx` for target `x`) + pattern = target .. [[\@<=\_.]] + hl_pattern = target .. [[\ze\_.]] + else + pattern = [[\_.\ze]] .. target + hl_pattern = [[\_.\@<=]] .. target + end + else + local is_visual = vim.tbl_contains({ 'v', 'V', '\22' }, vim.fn.mode()) + local is_exclusive = vim.o.selection == 'exclusive' + if not backward and is_visual and is_exclusive then + -- Still select target in case of exclusive visual selection + pattern = target .. [[\zs\_.]] + hl_pattern = target .. [[\ze\_.]] + else + pattern = target + hl_pattern = target + end + end + + -- Enable 'very nomagic' mode and possibly case-insensitivity + local ignore_case = vim.o.ignorecase and (not vim.o.smartcase or target == target:lower()) + local prefix = ignore_case and [[\V\c]] or [[\V]] + pattern, hl_pattern = prefix .. pattern, prefix .. hl_pattern + + return pattern, hl_pattern, flags +end + +-- Highlighting --------------------------------------------------------------- +H.highlight = function(pattern) + -- Don't do anything if already highlighting input pattern + if H.is_highlighting(pattern) then return end + + -- Stop highlighting possible previous pattern. Needed to adjust highlighting + -- when inside jumping but a different kind one. Example: first jump with + -- `till = false` and then, without jumping stop, jump to same character with + -- `till = true`. If this character is first on line, highlighting should change + H.unhighlight() + + -- Never highlight in Insert mode + if vim.fn.mode() == 'i' then return end + + local match_id = vim.fn.matchadd('MiniJump', pattern) + H.window_matches[vim.api.nvim_get_current_win()] = { id = match_id, pattern = pattern } +end + +H.unhighlight = function() + -- Remove highlighting from all windows as jumping is intended to work only + -- in current window. This will work also from other (usually popup) window. + for win_id, match_info in pairs(H.window_matches) do + if vim.api.nvim_win_is_valid(win_id) then + -- Use `pcall` because there is an error if match id is not present. It + -- can happen if something else called `clearmatches`. + pcall(vim.fn.matchdelete, match_info.id, win_id) + H.window_matches[win_id] = nil + end + end +end + +---@param pattern string Highlight pattern to check for. If `nil`, checks for +--- any highlighting registered in current window. +---@private +H.is_highlighting = function(pattern) + local win_id = vim.api.nvim_get_current_win() + local match_info = H.window_matches[win_id] + if match_info == nil then return false end + return pattern == nil or match_info.pattern == pattern +end + +-- Utilities ------------------------------------------------------------------ +H.message = function(msg) vim.cmd('echomsg ' .. vim.inspect('(mini.jump) ' .. msg)) end + +H.update_state = function(target, backward, till, n_times) + MiniJump.state.mode = vim.fn.mode(1) + + -- Don't use `? and <1> or <2>` because it doesn't work when `<1>` is `false` + if target ~= nil then MiniJump.state.target = target end + if backward ~= nil then MiniJump.state.backward = backward end + if till ~= nil then MiniJump.state.till = till end + if n_times ~= nil then MiniJump.state.n_times = n_times end +end + +H.get_cursor_data = function() return { vim.api.nvim_get_current_win(), vim.api.nvim_win_get_cursor(0) } end + +H.get_target = function() + local needs_help_msg = true + vim.defer_fn(function() + if not needs_help_msg then return end + H.message('Enter target single character ') + end, 1000) + local ok, char = pcall(vim.fn.getchar) + needs_help_msg = false + + -- Terminate if couldn't get input (like with ) or it is `` + if not ok or char == 27 then return end + + if type(char) == 'number' then char = vim.fn.nr2char(char) end + return char +end + +H.map = function(mode, key, rhs, opts) + if key == '' then return end + + opts = vim.tbl_deep_extend('force', { noremap = true }, opts or {}) + + -- Use mapping description only in Neovim>=0.7 + if vim.fn.has('nvim-0.7') == 0 then opts.desc = nil end + + vim.api.nvim_set_keymap(mode, key, rhs, opts) +end + +return MiniJump diff --git a/dotfiles/pack/plugins/start/mini.nvim/lua/mini/jump2d.lua b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/jump2d.lua new file mode 100755 index 0000000..700d002 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/jump2d.lua @@ -0,0 +1,914 @@ +-- MIT License Copyright (c) 2022 Evgeni Chasnovski + +-- Documentation ============================================================== +--- 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 `` +--- 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. +---@tag mini.jump2d +---@tag MiniJump2d + +-- Module definition ========================================================== +local MiniJump2d = {} +local H = {} + +--- Module setup +--- +---@param config table Module config table. See |MiniJump2d.config|. +--- +---@usage `require('mini.jump2d').setup({})` (replace `{}` with your `config` table) +MiniJump2d.setup = function(config) + -- Export module + _G.MiniJump2d = MiniJump2d + + -- Setup config + config = H.setup_config(config) + + -- Apply config + H.apply_config(config) + + -- Corrections for default `` mapping to not interfer with popular usages + if config.mappings.start_jumping == '' then + vim.api.nvim_exec( + [[augroup MiniJump2d + au! + autocmd BufWinEnter quickfix nnoremap + autocmd CmdwinEnter * nnoremap + augroup END]], + false + ) + end + + -- Create highlighting + local hl_cmd = 'hi default MiniJump2dSpot guifg=white guibg=black gui=bold,nocombine' + if vim.o.background == 'light' then + hl_cmd = 'hi default MiniJump2dSpot guifg=black guibg=white gui=bold,nocombine' + end + vim.cmd(hl_cmd) +end + +--- Module config +--- +--- Default values: +---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section) +---@text # 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.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 = '', + }, +} +--minidoc_afterlines_end + +-- Module functionality ======================================================= +--- 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. +--- +---@param opts table Configuration of jumping, overriding global and buffer +--- local values.config|. Has the same structure as |MiniJump2d.config| +--- without field. Extra allowed fields: +--- - - 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' +--- }) +---< +---@seealso |MiniJump2d.config| +MiniJump2d.start = function(opts) + if H.is_disabled() then return end + + opts = opts or {} + + -- Apply `before_start` before `tbl_deep_extend` to allow it modify options + -- inside it (notably `spotter`). Example: `builtins.single_character`. + local before_start = (opts.hooks or {}).before_start + or ((vim.b.minijump2d_config or {}).hooks or {}).before_start + or MiniJump2d.config.hooks.before_start + if before_start ~= nil then before_start() end + + opts = H.get_config(opts) + opts.spotter = opts.spotter or MiniJump2d.default_spotter + opts.hl_group = opts.hl_group or 'MiniJump2dSpot' + + local spots = H.spots_compute(opts) + spots = H.spots_add_label(spots, opts) + + H.spots_show(spots, opts) + + H.cache.spots = spots + + -- Defer advancing jump to allow drawing before invoking `getcharstr()`. + -- This is much faster than having to call `vim.cmd('redraw')`. + -- Don't do that in Operator-pending mode because it doesn't work otherwise. + if H.is_operator_pending() then + H.advance_jump(opts) + else + vim.defer_fn(function() H.advance_jump(opts) end, 0) + end +end + +--- Stop jumping +MiniJump2d.stop = function() + H.spots_unshow() + H.cache.spots = nil + vim.cmd('redraw') + + if H.cache.is_in_getchar then vim.api.nvim_input('') end +end + +--- Generate spotter for Lua pattern +--- +---@param 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). +---@param 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.gen_pattern_spotter = function(pattern, side) + -- Don't use `%w` to account for multibyte characters + pattern = pattern or '[^%s%p]+' + side = side or 'start' + + -- Process anchored patterns separately because: + -- - `gmatch()` doesn't work if pattern start with `^`. + -- - Manual adding of `()` will conflict with anchors. + local is_anchored = pattern:sub(1, 1) == '^' or pattern:sub(-1, -1) == '$' + if is_anchored then + return function(line_num, args) + local line = vim.fn.getline(line_num) + local s, e, m = line:find(pattern) + return { ({ ['start'] = s, ['end'] = e, ['none'] = m })[side] } + end + end + + -- Handle `side = 'end'` later by appending length of match to match start. + -- This, unlike appending `()` to end of pattern, makes output spot to be + -- inside matched pattern and on its exact right. + -- Having `(%s)` for `side = 'none'` is for compatibility with later `gmatch` + local pattern_template = side == 'none' and '(%s)' or '(()%s)' + pattern = pattern_template:format(pattern) + + return function(line_num, args) + local line = vim.fn.getline(line_num) + local res = {} + -- NOTE: maybe a more straightforward approach would be a series of + -- `line:find(original_pattern, init)` with moving `init`, but it has some + -- weird behavior with quantifiers. + -- For example: `string.find(' --', '%s*', 4)` returns `4 3`. + for whole, spot in string.gmatch(line, pattern) do + -- Possibly correct spot to be index of last matched position + if side == 'end' then spot = spot + math.max(whole:len() - 1, 0) end + + -- Ensure that index is strictly within line length (which can be not + -- true in case of weird pattern, like when using frontier `%f[%W]`) + spot = math.min(math.max(spot, 0), line:len()) + + -- Unify how spot is chosen in case of multibyte characters + spot = vim.str_byteindex(line, vim.str_utfindex(line, spot)) + + -- Add spot only if it referces new actually visible column + if spot ~= res[#res] then table.insert(res, spot) end + end + return res + end +end + +--- 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.default_spotter = (function() + local nonblank_start = MiniJump2d.gen_pattern_spotter('%S+', 'start') + local nonblank_end = MiniJump2d.gen_pattern_spotter('%S+', 'end') + -- Use `[^%s%p]` as "alphanumeric" to allow working with multibyte characters + local alphanum_before_punct = MiniJump2d.gen_pattern_spotter('[^%s%p]%p', 'start') + local alphanum_after_punct = MiniJump2d.gen_pattern_spotter('%p[^%s%p]', 'end') + -- NOTE: works only with Latin alphabet + local upper_start = MiniJump2d.gen_pattern_spotter('%u+', 'start') + + return function(line_num, args) + local res_1 = H.merge_unique(nonblank_start(line_num, args), nonblank_end(line_num, args)) + local res_2 = H.merge_unique(alphanum_before_punct(line_num, args), alphanum_after_punct(line_num, args)) + local res = H.merge_unique(res_1, res_2) + return H.merge_unique(res, upper_start(line_num, args)) + end +end)() + +--- 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', '', +--- 'lua MiniJump2d.start(MiniJump2d.builtin_opts.line_start)', {} +--- ) +--- - 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 = {} + +--- Jump with |MiniJump2d.default_spotter()| +--- +--- Defines `spotter`. +MiniJump2d.builtin_opts.default = { spotter = MiniJump2d.default_spotter } + +--- Jump to line start +--- +--- Defines `spotter` and `hooks.after_jump`. +MiniJump2d.builtin_opts.line_start = { + spotter = function(line_num, args) return { 1 } end, + hooks = { + after_jump = function() + -- Move to first non-blank character + vim.cmd('normal! ^') + end, + }, +} + +--- Jump to word start +--- +--- Defines `spotter`. +MiniJump2d.builtin_opts.word_start = { spotter = MiniJump2d.gen_pattern_spotter('[^%s%p]+') } + +-- Produce `opts` which modifies spotter based on user input +local function user_input_opts(input_fun) + local res = { + spotter = function() return {} end, + allowed_lines = { blank = false, fold = false }, + } + + res.hooks = { + before_start = function() + local input = input_fun() + if input == nil then + res.spotter = function() return {} end + else + local pattern = vim.pesc(input) + res.spotter = MiniJump2d.gen_pattern_spotter(pattern) + end + end, + } + + return res +end + +--- Jump to single character taken from user input +--- +--- Defines `spotter`, `allowed_lines.blank`, `allowed_lines.fold`, and +--- `hooks.before_start`. +MiniJump2d.builtin_opts.single_character = user_input_opts( + function() return H.getcharstr('Enter single character to search') end +) + +--- Jump to query taken from user input +--- +--- Defines `spotter`, `allowed_lines.blank`, `allowed_lines.fold`, and +--- `hooks.before_start`. +MiniJump2d.builtin_opts.query = user_input_opts(function() return H.input('Enter query to search') end) + +-- Helper data ================================================================ +-- Module default config +H.default_config = MiniJump2d.config + +-- Namespaces to be used withing module +H.ns_id = { + spots = vim.api.nvim_create_namespace('MiniJump2dSpots'), + input = vim.api.nvim_create_namespace('MiniJump2dInput'), +} + +-- Table with current relevant data: +H.cache = { + -- Array of shown spots + spots = nil, + -- Indicator of whether Neovim is currently in "getchar" mode + is_in_getchar = false, +} + +-- Table with special keys +H.keys = { + esc = vim.api.nvim_replace_termcodes('', true, true, true), + cr = vim.api.nvim_replace_termcodes('', true, true, true), + block_operator_pending = vim.api.nvim_replace_termcodes('no', true, true, true), +} + +-- Helper functionality ======================================================= +-- Settings ------------------------------------------------------------------- +H.setup_config = function(config) + -- General idea: if some table elements are not present in user-supplied + -- `config`, take them from default config + vim.validate({ config = { config, 'table', true } }) + config = vim.tbl_deep_extend('force', H.default_config, config or {}) + + vim.validate({ + spotter = { config.spotter, 'function', true }, + labels = { config.labels, 'string' }, + allowed_lines = { config.allowed_lines, 'table' }, + allowed_windows = { config.allowed_windows, 'table' }, + hooks = { config.hooks, 'table' }, + mappings = { config.mappings, 'table' }, + }) + + vim.validate({ + ['allowed_lines.blank'] = { config.allowed_lines.blank, 'boolean' }, + ['allowed_lines.cursor_before'] = { config.allowed_lines.cursor_before, 'boolean' }, + ['allowed_lines.cursor_at'] = { config.allowed_lines.cursor_at, 'boolean' }, + ['allowed_lines.cursor_after'] = { config.allowed_lines.cursor_after, 'boolean' }, + ['allowed_lines.fold'] = { config.allowed_lines.fold, 'boolean' }, + + ['allowed_windows.current'] = { config.allowed_windows.current, 'boolean' }, + ['allowed_windows.not_current'] = { config.allowed_windows.not_current, 'boolean' }, + + ['hooks.before_start'] = { config.hooks.before_start, 'function', true }, + ['hooks.after_jump'] = { config.hooks.after_jump, 'function', true }, + + ['mappings.start_jumping'] = { config.mappings.start_jumping, 'string' }, + }) + return config +end + +H.apply_config = function(config) + MiniJump2d.config = config + + -- Apply mappings + local keymap = config.mappings.start_jumping + H.map('n', keymap, 'lua MiniJump2d.start()', { desc = 'Start 2d jumping' }) + H.map('x', keymap, 'lua MiniJump2d.start()', { desc = 'Start 2d jumping' }) + H.map('o', keymap, 'lua MiniJump2d.start()', { desc = 'Start 2d jumping' }) +end + +H.is_disabled = function() return vim.g.minijump2d_disable == true or vim.b.minijump2d_disable == true end + +H.get_config = function(config) + return vim.tbl_deep_extend('force', MiniJump2d.config, vim.b.minijump2d_config or {}, config or {}) +end + +-- Jump spots ----------------------------------------------------------------- +H.spots_compute = function(opts) + local win_id_init = vim.api.nvim_get_current_win() + local win_id_arr = vim.tbl_filter(function(win_id) + if win_id == win_id_init then return opts.allowed_windows.current end + return opts.allowed_windows.not_current + end, H.tabpage_list_wins(0)) + + local res = {} + for _, win_id in ipairs(win_id_arr) do + vim.api.nvim_win_call(win_id, function() + local cursor_pos = vim.api.nvim_win_get_cursor(win_id) + local spotter_args = { win_id = win_id, win_id_init = win_id_init } + local buf_id = vim.api.nvim_win_get_buf(win_id) + + -- Use all currently visible lines + for i = vim.fn.line('w0'), vim.fn.line('w$') do + local columns = H.spot_find_in_line(i, spotter_args, opts, cursor_pos) + -- Use all returned columns for particular line + for _, col in ipairs(columns) do + table.insert(res, { line = i, column = col, buf_id = buf_id, win_id = win_id }) + end + end + end) + end + return res +end + +H.spots_add_label = function(spots, opts) + local label_tbl = vim.split(opts.labels, '') + + -- Example: with 3 label characters labels should evolve with progressing + -- number of spots like this: 'a', 'ab', 'abc', 'aabc', 'aabbc', 'aabbcc', + -- 'aaabbcc', 'aaabbbcc', 'aaabbbccc', etc. + local n_spots, n_label_chars = #spots, #label_tbl + local base, extra = math.floor(n_spots / n_label_chars), n_spots % n_label_chars + + local label_id, cur_label_count = 1, 0 + local label_max_count = base + (label_id <= extra and 1 or 0) + for _, s in ipairs(spots) do + s.label = label_tbl[label_id] + cur_label_count = cur_label_count + 1 + if cur_label_count >= label_max_count then + label_id, cur_label_count = label_id + 1, 0 + label_max_count = base + (label_id <= extra and 1 or 0) + end + end + + return spots +end + +H.spots_show = function(spots, opts) + spots = spots or H.cache.spots or {} + if #spots == 0 then + H.message('No spots to show.') + return + end + + for _, extmark in ipairs(H.spots_to_extmarks(spots)) do + local extmark_opts = { + hl_mode = 'combine', + -- Use very high priority + priority = 1000, + virt_text = { { extmark.text, opts.hl_group } }, + virt_text_pos = 'overlay', + } + pcall(vim.api.nvim_buf_set_extmark, extmark.buf_id, H.ns_id.spots, extmark.line, extmark.col, extmark_opts) + end + + -- Need to redraw in Operator-pending mode, because otherwise extmarks won't + -- be shown and deferring disables this mode. + if H.is_operator_pending() then vim.cmd('redraw') end +end + +H.spots_unshow = function(spots) + spots = spots or H.cache.spots or {} + + -- Remove spot extmarks from all buffers they are present + local buf_ids = {} + for _, s in ipairs(spots) do + buf_ids[s.buf_id] = true + end + + for _, buf_id in ipairs(vim.tbl_keys(buf_ids)) do + pcall(vim.api.nvim_buf_clear_namespace, buf_id, H.ns_id.spots, 0, -1) + end +end + +--- Convert consecutive spots into single extmark +--- +--- This considerably increases performance in case of many spots. +---@private +H.spots_to_extmarks = function(spots) + if #spots == 0 then return {} end + + local res = {} + + local buf_id, line, col = spots[1].buf_id, spots[1].line - 1, spots[1].column - 1 + local extmark_chars = {} + local cur_col = col + for _, s in ipairs(spots) do + local is_within_same_extmark = s.buf_id == buf_id and s.line == (line + 1) and s.column == (cur_col + 1) + + if not is_within_same_extmark then + table.insert(res, { buf_id = buf_id, col = col, line = line, text = table.concat(extmark_chars) }) + buf_id, line, col = s.buf_id, s.line - 1, s.column - 1 + extmark_chars = {} + end + + table.insert(extmark_chars, s.label) + cur_col = s.column + end + table.insert(res, { buf_id = buf_id, col = col, line = line, text = table.concat(extmark_chars) }) + + return res +end + +H.spot_find_in_line = function(line_num, spotter_args, opts, cursor_pos) + local allowed = opts.allowed_lines + + -- Adjust for cursor line + local cur_line = cursor_pos[1] + if + (not allowed.cursor_before and line_num < cur_line) + or (not allowed.cursor_at and line_num == cur_line) + or (not allowed.cursor_after and line_num > cur_line) + then + return {} + end + + -- Process folds + local fold_indicator = vim.fn.foldclosed(line_num) + if fold_indicator ~= -1 then return (allowed.fold and fold_indicator == line_num) and { 1 } or {} end + + -- Process blank lines + if vim.fn.prevnonblank(line_num) ~= line_num then return allowed.blank and { 1 } or {} end + + -- Finally apply spotter + return opts.spotter(line_num, spotter_args) +end + +-- Jump state ----------------------------------------------------------------- +H.advance_jump = function(opts) + local label_tbl = vim.split(opts.labels, '') + + local spots = H.cache.spots + + if type(spots) ~= 'table' or #spots < 1 then + H.spots_unshow(spots) + H.cache.spots = nil + return + end + + local key = H.getcharstr('Enter encoding symbol to advance jump') + + if vim.tbl_contains(label_tbl, key) then + H.spots_unshow(spots) + spots = vim.tbl_filter(function(x) return x.label == key end, spots) + + if #spots > 1 then + spots = H.spots_add_label(spots, opts) + H.spots_show(spots, opts) + H.cache.spots = spots + + -- Defer advancing jump to allow drawing before invoking `getcharstr()`. + -- This is much faster than having to call `vim.cmd('redraw')`. Don't do that + -- in Operator-pending mode because it doesn't work otherwise. + if H.is_operator_pending() then + H.advance_jump(opts) + else + vim.defer_fn(function() H.advance_jump(opts) end, 0) + return + end + end + end + + if #spots == 1 or key == H.keys.cr then + -- Add to jumplist + vim.cmd('normal! m`') + + local first_spot = spots[1] + vim.api.nvim_set_current_win(first_spot.win_id) + vim.api.nvim_win_set_cursor(first_spot.win_id, { first_spot.line, first_spot.column - 1 }) + + -- Possibly unfold to see cursor + vim.cmd('normal! zv') + + if opts.hooks.after_jump ~= nil then opts.hooks.after_jump() end + end + + MiniJump2d.stop() +end + +-- Utilities ------------------------------------------------------------------ +H.message = function(msg) vim.cmd('echomsg ' .. vim.inspect('(mini.jump2d) ' .. msg)) end + +H.is_operator_pending = + function() return vim.tbl_contains({ 'no', 'noV', H.keys.block_operator_pending }, vim.fn.mode(1)) end + +H.getcharstr = function(msg) + local needs_help_msg = true + if msg ~= nil then vim.defer_fn(function() + if needs_help_msg then H.message(msg) end + end, 1000) end + + -- Use `getchar()` because `getcharstr()` is present only in Neovim>=0.6 + -- Might want to remove if support for Neovim<0.6 is dropped + H.cache.is_in_getchar = true + local ok, char = pcall(vim.fn.getchar) + H.cache.is_in_getchar = false + needs_help_msg = false + + if not ok then return end + + if type(char) == 'number' then char = vim.fn.nr2char(char) end + return char +end + +H.input = function(prompt, text) + -- Distinguish between ``, ``, and first `` + local on_key = vim.on_key or vim.register_keystroke_callback + local was_cancelled = false + on_key(function(key) + if key == H.keys.esc then was_cancelled = true end + end, H.ns_id.input) + + -- Ask for input + local opts = { prompt = '(mini.jump2d) ' .. prompt .. ': ', default = text or '' } + -- Use `pcall` to allow `` to cancel user input + local ok, res = pcall(vim.fn.input, opts) + + -- Stop key listening + on_key(nil, H.ns_id.input) + + if not ok or was_cancelled then return end + return res +end + +--- This ensures order of windows based on their layout +--- +--- This is already done by default in `nvim_tabpage_list_wins()`, but is not +--- documented, so can break any time. +--- +---@private +H.tabpage_list_wins = function(tabpage_id) + local wins = vim.api.nvim_tabpage_list_wins(tabpage_id) + local wins_pos = {} + for _, win_id in ipairs(wins) do + local pos = vim.api.nvim_win_get_position(win_id) + local config = vim.api.nvim_win_get_config(win_id) + wins_pos[win_id] = { row = pos[1], col = pos[2], zindex = config.zindex or 0 } + end + + -- Sort windows by their position: top to bottom, left to right, low to high + table.sort(wins, function(a, b) + -- Put higher window further to have them processed later. This means that + -- in case of same buffer in floating and underlying regular windows, + -- floating will have "the latest" extmarks (think like `MiniMisc.zoom()`). + if wins_pos[a].zindex < wins_pos[b].zindex then return true end + if wins_pos[a].zindex > wins_pos[b].zindex then return false end + + if wins_pos[a].col < wins_pos[b].col then return true end + if wins_pos[a].col > wins_pos[b].col then return false end + + return wins_pos[a].row < wins_pos[b].row + end) + + return wins +end + +H.map = function(mode, key, rhs, opts) + if key == '' then return end + + opts = vim.tbl_deep_extend('force', { noremap = true, silent = true }, opts or {}) + + -- Use mapping description only in Neovim>=0.7 + if vim.fn.has('nvim-0.7') == 0 then opts.desc = nil end + + vim.api.nvim_set_keymap(mode, key, rhs, opts) +end + +H.merge_unique = function(tbl_1, tbl_2) + if not (type(tbl_1) == 'table' and type(tbl_2) == 'table') then return end + + local n_1, n_2 = #tbl_1, #tbl_2 + local res, i, j = {}, 1, 1 + local to_add + while i <= n_1 and j <= n_2 do + if tbl_1[i] < tbl_2[j] then + to_add = tbl_1[i] + i = i + 1 + else + to_add = tbl_2[j] + j = j + 1 + end + if res[#res] ~= to_add then table.insert(res, to_add) end + end + + while i <= n_1 do + to_add = tbl_1[i] + if res[#res] ~= to_add then table.insert(res, to_add) end + i = i + 1 + end + while j <= n_2 do + to_add = tbl_2[j] + if res[#res] ~= to_add then table.insert(res, to_add) end + j = j + 1 + end + + return res +end + +return MiniJump2d diff --git a/dotfiles/pack/plugins/start/mini.nvim/lua/mini/misc.lua b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/misc.lua new file mode 100755 index 0000000..5e5beb4 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/misc.lua @@ -0,0 +1,375 @@ +-- MIT License Copyright (c) 2021 Evgeni Chasnovski + +-- Documentation ============================================================== +--- 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. +---@tag mini.misc +---@tag MiniMisc + +-- Module definition ========================================================== +local MiniMisc = {} +local H = {} + +--- Module setup +--- +---@param config table Module config table. See |MiniMisc.config|. +--- +---@usage `require('mini.misc').setup({})` (replace `{}` with your `config` table) +MiniMisc.setup = function(config) + -- Export module + _G.MiniMisc = MiniMisc + + -- Setup config + config = H.setup_config(config) + + -- Apply config + H.apply_config(config) +end + +--- Module config +--- +--- Default values: +---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section) +MiniMisc.config = { + -- Array of fields to make global (to be used as independent variables) + make_global = { 'put', 'put_text' }, +} +--minidoc_afterlines_end + +-- Module functionality ======================================================= +--- Execute `f` several times and time how long it took +--- +---@param f function Function which execution to benchmark. +---@param n number Number of times to execute `f(...)`. Default: 1. +---@param ... any Arguments when calling `f`. +--- +---@return ... Table with durations (in seconds; up to microseconds) and +--- output of (last) function execution. +MiniMisc.bench_time = function(f, n, ...) + n = n or 1 + local durations, output = {}, nil + for _ = 1, n do + local start_sec, start_usec = vim.loop.gettimeofday() + output = f(...) + local end_sec, end_usec = vim.loop.gettimeofday() + table.insert(durations, (end_sec - start_sec) + 0.000001 * (end_usec - start_usec)) + end + + return durations, output +end + +--- Compute width of gutter (info column on the left of the window) +--- +---@param win_id number Window identifier (see |win_getid()|) for which gutter +--- width is computed. Default: 0 for current. +MiniMisc.get_gutter_width = function(win_id) + win_id = win_id or 0 + + -- Store window metadata + local virtualedit = vim.opt.virtualedit + local curpos = vim.api.nvim_win_get_cursor(win_id) + + -- Move cursor to the last visible column + local last_col = vim.api.nvim_win_call(win_id, function() + vim.opt.virtualedit = 'all' + vim.cmd('normal! g$') + return vim.fn.virtcol('.') + end) + + -- Restore current window metadata + vim.opt.virtualedit = virtualedit + vim.api.nvim_win_set_cursor(win_id, curpos) + + -- Compute result + return vim.api.nvim_win_get_width(win_id) - last_col +end + +--- Print Lua objects in command line +--- +---@param ... any Any number of objects to be printed each on separate line. +MiniMisc.put = function(...) + local objects = {} + -- Not using `{...}` because it removes `nil` input + for i = 1, select('#', ...) do + local v = select(i, ...) + table.insert(objects, vim.inspect(v)) + end + + print(table.concat(objects, '\n')) + + return ... +end + +--- Print Lua objects in current buffer +--- +---@param ... any Any number of objects to be printed each on separate line. +MiniMisc.put_text = function(...) + local objects = {} + -- Not using `{...}` because it removes `nil` input + for i = 1, select('#', ...) do + local v = select(i, ...) + table.insert(objects, vim.inspect(v)) + end + + local lines = vim.split(table.concat(objects, '\n'), '\n') + local lnum = vim.api.nvim_win_get_cursor(0)[1] + vim.fn.append(lnum, lines) + + return ... +end + +--- Resize window to have exact number of editable columns +--- +---@param win_id number Window identifier (see |win_getid()|) to be resized. +--- Default: 0 for current. +---@param 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.resize_window = function(win_id, text_width) + win_id = win_id or 0 + text_width = text_width or H.default_text_width(win_id) + + vim.api.nvim_win_set_width(win_id, text_width + MiniMisc.get_gutter_width(win_id)) +end + +H.default_text_width = function(win_id) + local buf = vim.api.nvim_win_get_buf(win_id) + local textwidth = vim.api.nvim_buf_get_option(buf, 'textwidth') + textwidth = (textwidth == 0) and math.min(vim.o.columns, 79) or textwidth + + local colorcolumn = vim.api.nvim_win_get_option(win_id, 'colorcolumn') + if colorcolumn ~= '' then + local cc = vim.split(colorcolumn, ',')[1] + local is_cc_relative = vim.tbl_contains({ '-', '+' }, cc:sub(1, 1)) + + if is_cc_relative then + return textwidth + tonumber(cc) + else + return tonumber(cc) + end + else + return textwidth + end +end + +--- Compute summary statistics of numerical array +--- +--- This might be useful to compute summary of time benchmarking with +--- |MiniMisc.bench_time|. +--- +---@param t table Array (table suitable for `ipairs`) of numbers. +--- +---@return table Table with summary values under following keys (may be +--- extended in the future): , , , , +--- (number of elements), (sample standard deviation). +MiniMisc.stat_summary = function(t) + if type(t) ~= 'table' then + H.message('Input of `MiniMisc.stat_summary` should be an array of numbers.') + return + end + + -- Welford algorithm of computing variance + -- Source: https://www.johndcook.com/blog/skewness_kurtosis/ + local n = #t + local delta, m1, m2 = 0, 0, 0 + local minimum, maximum = math.huge, -math.huge + for i, x in ipairs(t) do + delta = x - m1 + m1 = m1 + delta / i + m2 = m2 + delta * (x - m1) + + -- Extremums + minimum = x < minimum and x or minimum + maximum = x > maximum and x or maximum + end + + return { + maximum = maximum, + mean = m1, + median = H.compute_median(t), + minimum = minimum, + n = n, + sd = math.sqrt(n > 1 and m2 / (n - 1) or 0), + } +end + +H.compute_median = function(t) + local n = #t + if n == 0 then return 0 end + + local t_sorted = vim.deepcopy(t) + table.sort(t_sorted) + return 0.5 * (t_sorted[math.ceil(0.5 * n)] + t_sorted[math.ceil(0.5 * (n + 1))]) +end + +--- Return "first" elements of table as decided by `pairs` +--- +--- Note: order of elements might vary. +--- +---@param t table Input table. +---@param 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_head = function(t, n) + n = n or 5 + local res, n_res = {}, 0 + for k, val in pairs(t) do + if n_res >= n then return res end + res[k] = val + n_res = n_res + 1 + end + return res +end + +--- 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. +--- +---@param t table Input table. +---@param n number Maximum number of last elements. Default: 5. +--- +---@return table Table with at most `n` last elements of `t` (with same keys). +MiniMisc.tbl_tail = function(t, n) + n = n or 5 + + -- Count number of elements on first pass + local n_all = 0 + for _, _ in pairs(t) do + n_all = n_all + 1 + end + + -- Construct result on second pass + local res = {} + local i, start_i = 0, n_all - n + 1 + for k, val in pairs(t) do + i = i + 1 + if i >= start_i then res[k] = val end + end + return res +end + +--- 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:`). 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. +--- +---@param buf_id number Buffer identifier (see |bufnr()|) in which function +--- will operate. Default: 0 for current. +MiniMisc.use_nested_comments = function(buf_id) + buf_id = buf_id or 0 + + local commentstring = vim.api.nvim_buf_get_option(buf_id, 'commentstring') + if commentstring == '' then return end + + -- Extract raw comment leader from 'commentstring' option + local comment_parts = vim.tbl_filter(function(x) return x ~= '' end, vim.split(commentstring, '%s', true)) + + -- Don't do anything if 'commentstring' is like '/*%s*/' (as in 'json') + if #comment_parts > 1 then return end + + -- Get comment leader by removing whitespace + local leader = vim.trim(comment_parts[1]) + + local comments = vim.api.nvim_buf_get_option(buf_id, 'comments') + local new_comments = string.format('n:%s,%s', leader, comments) + vim.api.nvim_buf_set_option(buf_id, 'comments', new_comments) +end + +--- 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. +--- +---@param buf_id number Buffer identifier (see |bufnr()|) to be zoomed. +--- Default: 0 for current. +---@param config table Optional config for window (as for |nvim_open_win()|). +MiniMisc.zoom = function(buf_id, config) + if H.zoom_winid and vim.api.nvim_win_is_valid(H.zoom_winid) then + vim.api.nvim_win_close(H.zoom_winid, true) + H.zoom_winid = nil + else + buf_id = buf_id or 0 + -- Currently very big `width` and `height` get truncated to maximum allowed + local default_config = { relative = 'editor', row = 0, col = 0, width = 1000, height = 1000 } + config = vim.tbl_deep_extend('force', default_config, config or {}) + H.zoom_winid = vim.api.nvim_open_win(buf_id, true, config) + vim.cmd('normal! zz') + end +end + +-- Helper data ================================================================ +-- Module default config +H.default_config = MiniMisc.config + +-- Window identifier of current zoom (for `zoom()`) +H.zoom_winid = nil + +-- Helper functionality ======================================================= +-- Settings ------------------------------------------------------------------- +H.setup_config = function(config) + -- General idea: if some table elements are not present in user-supplied + -- `config`, take them from default config + vim.validate({ config = { config, 'table', true } }) + config = vim.tbl_deep_extend('force', H.default_config, config or {}) + + vim.validate({ + make_global = { + config.make_global, + function(x) + if type(x) ~= 'table' then return false end + local present_fields = vim.tbl_keys(MiniMisc) + for _, v in pairs(x) do + if not vim.tbl_contains(present_fields, v) then return false end + end + return true + end, + '`make_global` should be a table with `MiniMisc` actual fields', + }, + }) + + return config +end + +H.apply_config = function(config) + MiniMisc.config = config + + for _, v in pairs(config.make_global) do + _G[v] = MiniMisc[v] + end +end + +-- Utilities ------------------------------------------------------------------ +H.message = function(msg) vim.cmd('echomsg ' .. vim.inspect('(mini.misc) ' .. msg)) end + +return MiniMisc diff --git a/dotfiles/pack/plugins/start/mini.nvim/lua/mini/pairs.lua b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/pairs.lua new file mode 100755 index 0000000..29c31f4 --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/pairs.lua @@ -0,0 +1,569 @@ +-- MIT License Copyright (c) 2021 Evgeni Chasnovski + +-- Documentation ============================================================== +--- 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 `` and ``. +--- +--- 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', <*>, )` : make new mapping +--- for '<*>' in current buffer. +--- - `lua MiniPairs.unmap_buf(0, 'i', <*>, )`: unmap key `<*>` while +--- unregistering `` 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 <*> <*>` (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 ``: > +--- 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 `` 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. +---@tag mini.pairs +---@tag MiniPairs + +---@alias __neigh_pattern string Pattern for two neighborhood characters ("\r" line +--- start, "\n" - line end). +---@alias __pair string String with two characters representing pair. +---@alias __unregister_pair string Pair which should be unregistered from both +--- `` and ``. Should be explicitly supplied to avoid confusion. +--- Supply `''` to not unregister pair. + +-- Module definition ========================================================== +local MiniPairs = {} +local H = {} + +--- Module setup +--- +---@param config table Module config table. See |MiniPairs.config|. +--- +---@usage `require('mini.completion').setup({})` (replace `{}` with your `config` table) +MiniPairs.setup = function(config) + -- Export module + _G.MiniPairs = MiniPairs + + -- Setup config + config = H.setup_config(config) + + -- Apply config + H.apply_config(config) + + -- Module behavior + vim.api.nvim_exec( + [[augroup MiniPairs + au! + au FileType TelescopePrompt let b:minipairs_disable=v:true + au FileType fzf let b:minipairs_disable=v:true + augroup END]], + false + ) +end + +--- Module config +--- +--- Default values: +---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section) +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|): + -- - - one of "open", "close", "closeopen". + -- - - two character string for pair to be used. + -- By default pair is not inserted after `\`, quotes are not recognized by + -- ``, `'` 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 } }, + }, +} +--minidoc_afterlines_end + +-- Module functionality ======================================================= +--- 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 `` (in |MiniPairs.bs|) and/or `` (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 `` and ``. +--- For Neovim>=0.7 it also infers mapping description from `pair_info`. +--- +---@param mode string `mode` for |nvim_set_keymap()|. +---@param lhs string `lhs` for |nvim_set_keymap()|. +---@param pair_info table Table with pair information. +---@param opts table Optional table `opts` for |nvim_set_keymap()|. Elements +--- `expr` and `noremap` won't be recognized (`true` by default). +MiniPairs.map = function(mode, lhs, pair_info, opts) + pair_info = H.validate_pair_info(pair_info) + opts = vim.tbl_deep_extend('force', opts or {}, { expr = true, noremap = true }) + opts.desc = H.infer_mapping_description(pair_info) + + H.map(mode, lhs, H.pair_info_to_map_rhs(pair_info), opts) + H.register_pair(pair_info, mode, 'all') + + -- Ensure that `` and `` are mapped for input mode + H.ensure_cr_bs(mode) +end + +--- 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 `` and ``. +--- For Neovim>=0.7 it also infers mapping description from `pair_info`. +--- +---@param buffer number `buffer` for |nvim_buf_set_keymap()|. +---@param mode string `mode` for |nvim_buf_set_keymap()|. +---@param lhs string `lhs` for |nvim_buf_set_keymap()|. +---@param pair_info table Table with pair information. +---@param opts table Optional table `opts` for |nvim_buf_set_keymap()|. +--- Elements `expr` and `noremap` won't be recognized (`true` by default). +MiniPairs.map_buf = function(buffer, mode, lhs, pair_info, opts) + pair_info = H.validate_pair_info(pair_info) + opts = vim.tbl_deep_extend('force', opts or {}, { expr = true, noremap = true }) + if vim.fn.has('nvim-0.7') == 1 then opts.desc = H.infer_mapping_description(pair_info) end + + vim.api.nvim_buf_set_keymap(buffer, mode, lhs, H.pair_info_to_map_rhs(pair_info), opts) + H.register_pair(pair_info, mode, buffer == 0 and vim.api.nvim_get_current_buf() or buffer) + + -- Ensure that `` and `` are mapped for inpu mode + H.ensure_cr_bs(mode) +end + +--- Remove global mapping +--- +--- A wrapper for |nvim_del_keymap()| which registers supplied `pair`. +--- +---@param mode string `mode` for |nvim_del_keymap()|. +---@param lhs string `lhs` for |nvim_del_keymap()|. +---@param pair __unregister_pair +MiniPairs.unmap = function(mode, lhs, pair) + -- `pair` should be supplied explicitly + vim.validate({ pair = { pair, 'string' } }) + + -- Use `pcall` to allow 'deleting' already deleted mapping + pcall(vim.api.nvim_del_keymap, mode, lhs) + if pair == '' then return end + H.unregister_pair(pair, mode, 'all') +end + +--- 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 <*> <*>` (this maps `<*>` key to do the same it does by +--- default). +--- +---@param buffer number `buffer` for |nvim_buf_del_keymap()|. +---@param mode string `mode` for |nvim_buf_del_keymap()|. +---@param lhs string `lhs` for |nvim_buf_del_keymap()|. +---@param pair __unregister_pair +MiniPairs.unmap_buf = function(buffer, mode, lhs, pair) + -- `pair` should be supplied explicitly + vim.validate({ pair = { pair, 'string' } }) + + -- Use `pcall` to allow 'deleting' already deleted mapping + pcall(vim.api.nvim_buf_del_keymap, buffer, mode, lhs) + if pair == '' then return end + H.unregister_pair(pair, mode, buffer == 0 and vim.api.nvim_get_current_buf() or buffer) +end + +--- 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 ||. +--- +--- Used inside |MiniPairs.map| and |MiniPairs.map_buf| for an actual mapping. +--- +---@param pair __pair +---@param neigh_pattern __neigh_pattern +MiniPairs.open = function(pair, neigh_pattern) + if H.is_disabled() or not H.neigh_match(neigh_pattern) then return pair:sub(1, 1) end + + return ('%s%s'):format(pair, H.get_arrow_key('left')) +end + +--- 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 ||) if it is equal to "close" one and inserts it +--- otherwise. +--- +--- Used inside |MiniPairs.map| and |MiniPairs.map_buf| for an actual mapping. +--- +---@param pair __pair +---@param neigh_pattern __neigh_pattern +MiniPairs.close = function(pair, neigh_pattern) + if H.is_disabled() or not H.neigh_match(neigh_pattern) then return pair:sub(2, 2) end + + local close = pair:sub(2, 2) + if H.get_cursor_neigh(1, 1) == close then + return H.get_arrow_key('right') + else + return close + end +end + +--- Process "closeopen" symbols +--- +--- Used as |map-expr| mapping for 'symmetrical' symbols (from pairs '""', +--- '\'\'', '``'). It tries to perform 'closeopen action': move over right +--- character (with ||) 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. +--- +---@param pair __pair +---@param neigh_pattern __neigh_pattern +MiniPairs.closeopen = function(pair, neigh_pattern) + if H.is_disabled() or H.get_cursor_neigh(1, 1) ~= pair:sub(2, 2) then + return MiniPairs.open(pair, neigh_pattern) + else + return H.get_arrow_key('right') + end +end + +--- Process || +--- +--- Used as |map-expr| mapping for ``. It removes whole pair (via +--- ``) 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.bs = function() + local res = H.keys.bs + + local neigh = H.get_cursor_neigh(0, 1) + if not H.is_disabled() and H.is_pair_registered(neigh, vim.fn.mode(), 0, 'bs') then + res = ('%s%s'):format(res, H.keys.del) + end + + return res +end + +--- Process |i_| +--- +--- Used as |map-expr| mapping for `` in insert mode. It puts "close" +--- symbol on next line (via `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|. +MiniPairs.cr = function() + local res = H.keys.cr + + local neigh = H.get_cursor_neigh(0, 1) + if not H.is_disabled() and H.is_pair_registered(neigh, vim.fn.mode(), 0, 'cr') then + res = ('%s%s'):format(res, H.keys.above) + end + + return res +end + +-- Helper data ================================================================ +-- Module default config +H.default_config = MiniPairs.config + +-- Default value of `pair_info` for mapping functions +H.default_pair_info = { neigh_pattern = '..', register = { bs = true, cr = true } } + +-- Pair sets registered *per mode-buffer-key*. Buffer `'all'` contains pairs +-- registered for all buffers. +H.registered_pairs = { + i = { all = { bs = {}, cr = {} } }, + c = { all = { bs = {}, cr = {} } }, + t = { all = { bs = {}, cr = {} } }, +} + +-- Precomputed keys to increase speed +-- stylua: ignore start +local function escape(s) return vim.api.nvim_replace_termcodes(s, true, true, true) end +H.keys = { + above = escape('O'), + bs = escape(''), + cr = escape(''), + del = escape(''), + keep_undo = escape('U'), + -- NOTE: use `get_arrow_key()` instead of `H.keys.left` or `H.keys.right` + left = escape(''), + right = escape('') +} +-- stylua: ignore end + +-- Helper functionality ======================================================= +-- Settings ------------------------------------------------------------------- +H.setup_config = function(config) + -- General idea: if some table elements are not present in user-supplied + -- `config`, take them from default config + vim.validate({ config = { config, 'table', true } }) + config = vim.tbl_deep_extend('force', H.default_config, config or {}) + + -- Validate per nesting level to produce correct error message + vim.validate({ + modes = { config.modes, 'table' }, + mappings = { config.mappings, 'table' }, + }) + + vim.validate({ + ['modes.insert'] = { config.modes.insert, 'boolean' }, + ['modes.command'] = { config.modes.command, 'boolean' }, + ['modes.terminal'] = { config.modes.terminal, 'boolean' }, + }) + + H.validate_pair_info(config.mappings['('], "mappings['(']") + H.validate_pair_info(config.mappings['['], "mappings['[']") + H.validate_pair_info(config.mappings['{'], "mappings['{']") + H.validate_pair_info(config.mappings[')'], "mappings[')']") + H.validate_pair_info(config.mappings[']'], "mappings[']']") + H.validate_pair_info(config.mappings['}'], "mappings['}']") + H.validate_pair_info(config.mappings['"'], "mappings['\"']") + H.validate_pair_info(config.mappings["'"], 'mappings["\'"]') + H.validate_pair_info(config.mappings['`'], "mappings['`']") + + return config +end + +H.apply_config = function(config) + MiniPairs.config = config + + -- Setup mappings in supplied modes + local mode_ids = { insert = 'i', command = 'c', terminal = 't' } + -- Compute in which modes mapping should be set up + local mode_array = {} + for name, to_set in pairs(config.modes) do + if to_set then table.insert(mode_array, mode_ids[name]) end + end + + for _, mode in pairs(mode_array) do + for key, pair_info in pairs(config.mappings) do + -- This also should take care of mapping `` and `` + MiniPairs.map(mode, key, pair_info) + end + end +end + +H.is_disabled = function() return vim.g.minipairs_disable == true or vim.b.minipairs_disable == true end + +-- Pair registration ---------------------------------------------------------- +H.register_pair = function(pair_info, mode, buffer) + -- Process new mode + H.registered_pairs[mode] = H.registered_pairs[mode] or { all = { bs = {}, cr = {} } } + local mode_pairs = H.registered_pairs[mode] + + -- Process new buffer + mode_pairs[buffer] = mode_pairs[buffer] or { bs = {}, cr = {} } + + -- Register pair if it is not already registered + local register, pair = pair_info.register, pair_info.pair + if register.bs and not vim.tbl_contains(mode_pairs[buffer].bs, pair) then + table.insert(mode_pairs[buffer].bs, pair) + end + if register.cr and not vim.tbl_contains(mode_pairs[buffer].cr, pair) then + table.insert(mode_pairs[buffer].cr, pair) + end +end + +H.unregister_pair = function(pair, mode, buffer) + local mode_pairs = H.registered_pairs[mode] + if not (mode_pairs and mode_pairs[buffer]) then return end + + local buf_pairs = mode_pairs[buffer] + for _, key in ipairs({ 'bs', 'cr' }) do + for i, p in ipairs(buf_pairs[key]) do + if p == pair then table.remove(buf_pairs[key], i) end + end + end +end + +H.is_pair_registered = function(pair, mode, buffer, key) + local mode_pairs = H.registered_pairs[mode] + if not mode_pairs then return false end + + if vim.tbl_contains(mode_pairs['all'][key], pair) then return true end + + buffer = buffer == 0 and vim.api.nvim_get_current_buf() or buffer + local buf_pairs = mode_pairs[buffer] + if not buf_pairs then return false end + + return vim.tbl_contains(buf_pairs[key], pair) +end + +H.ensure_cr_bs = function(mode) + local has_any_cr_pair, has_any_bs_pair = false, false + for _, pair_tbl in pairs(H.registered_pairs[mode]) do + has_any_cr_pair = has_any_cr_pair or not vim.tbl_isempty(pair_tbl.cr) + has_any_bs_pair = has_any_bs_pair or not vim.tbl_isempty(pair_tbl.bs) + end + + -- NOTE: this doesn't distinguish between global and buffer mappings. Both + -- `` and `` should work as normal even if no pairs are registered + if has_any_bs_pair then H.map(mode, '', 'v:lua.MiniPairs.bs()', { expr = true, desc = 'MiniPairs ' }) end + if mode == 'i' and has_any_cr_pair then + H.map(mode, '', 'v:lua.MiniPairs.cr()', { expr = true, desc = 'MiniPairs ' }) + end +end + +-- Work with pair_info -------------------------------------------------------- +H.validate_pair_info = function(pair_info, prefix) + prefix = prefix or 'pair_info' + vim.validate({ [prefix] = { pair_info, 'table' } }) + pair_info = vim.tbl_deep_extend('force', H.default_pair_info, pair_info) + + vim.validate({ + [prefix .. '.action'] = { pair_info.action, 'string' }, + [prefix .. '.pair'] = { pair_info.pair, 'string' }, + [prefix .. '.neigh_pattern'] = { pair_info.neigh_pattern, 'string' }, + [prefix .. '.register'] = { pair_info.register, 'table' }, + }) + + vim.validate({ + [prefix .. '.register.bs'] = { pair_info.register.bs, 'boolean' }, + [prefix .. '.register.cr'] = { pair_info.register.cr, 'boolean' }, + }) + + return pair_info +end + +H.pair_info_to_map_rhs = function(pair_info) + return ('v:lua.MiniPairs.%s(%s, %s)'):format( + pair_info.action, + vim.inspect(pair_info.pair), + vim.inspect(pair_info.neigh_pattern) + ) +end + +H.infer_mapping_description = function(pair_info) + local action_name = pair_info.action:sub(1, 1):upper() .. pair_info.action:sub(2) + return ('%s action for %s pair'):format(action_name, vim.inspect(pair_info.pair)) +end + +-- Utilities ------------------------------------------------------------------ +H.get_cursor_neigh = function(start, finish) + local line, col + if vim.fn.mode() == 'c' then + line = vim.fn.getcmdline() + col = vim.fn.getcmdpos() + -- Adjust start and finish because output of `getcmdpos()` starts counting + -- columns from 1 + start = start - 1 + finish = finish - 1 + else + line = vim.api.nvim_get_current_line() + col = vim.api.nvim_win_get_cursor(0)[2] + end + + -- Add '\r' and '\n' to always return 2 characters + return string.sub(('%s%s%s'):format('\r', line, '\n'), col + 1 + start, col + 1 + finish) +end + +H.neigh_match = function(pattern) return (pattern == nil) or (H.get_cursor_neigh(0, 1):find(pattern) ~= nil) end + +H.get_arrow_key = function(key) + if vim.fn.mode() == 'i' then + -- Using left/right keys in insert mode breaks undo sequence and, more + -- importantly, dot-repeat. To avoid this, use 'i_CTRL-G_U' mapping. + return H.keys.keep_undo .. H.keys[key] + else + return H.keys[key] + end +end + +H.map = function(mode, key, rhs, opts) + if key == '' then return end + + opts = vim.tbl_deep_extend('force', { noremap = true }, opts or {}) + + -- Use mapping description only in Neovim>=0.7 + if vim.fn.has('nvim-0.7') == 0 then opts.desc = nil end + + vim.api.nvim_set_keymap(mode, key, rhs, opts) +end + +return MiniPairs diff --git a/dotfiles/pack/plugins/start/mini.nvim/lua/mini/sessions.lua b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/sessions.lua new file mode 100755 index 0000000..588ce1b --- /dev/null +++ b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/sessions.lua @@ -0,0 +1,577 @@ +-- MIT License Copyright (c) 2021 Evgeni Chasnovski + +-- Documentation ============================================================== +--- 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. +---@tag mini.sessions +---@tag MiniSessions + +-- Module definition ========================================================== +local MiniSessions = {} +local H = { path_sep = package.config:sub(1, 1) } + +--- Module setup +--- +---@param config table Module config table. See |MiniSessions.config|. +--- +---@usage `require('mini.sessions').setup({})` (replace `{}` with your `config` table) +MiniSessions.setup = function(config) + -- Export module + _G.MiniSessions = MiniSessions + + -- Setup config + config = H.setup_config(config) + + -- Apply config + H.apply_config(config) + + -- Module behavior + vim.api.nvim_exec( + [[augroup MiniSessions + au! + au VimEnter * ++nested ++once lua MiniSessions.on_vimenter() + augroup END]], + false + ) + + if config.autowrite then + vim.api.nvim_exec( + [[augroup MiniSessions + au VimLeavePre * lua if vim.v.this_session ~= '' then MiniSessions.write(nil, {force = true}) end + augroup END]], + false + ) + end +end + +--- Module config +--- +--- Default values: +---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section) +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) + --minidoc_replace_start directory = --<"session" subdir of user data directory from |stdpath()|>, + directory = ('%s%ssession'):format(vim.fn.stdpath('data'), H.path_sep), + --minidoc_replace_end + + -- 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 }, +} +--minidoc_afterlines_end + +-- Module data ================================================================ +--- Table of detected sessions. Keys represent session name. Values are tables +--- with session information that currently has these fields (but subject to +--- change): +--- - `(number)` modification time (see |getftime|) of session file. +--- - `(string)` name of session (should be equal to table key). +--- - `(string)` full path to session file. +--- - `(string)` type of session ('global' or 'local'). +MiniSessions.detected = {} + +-- Module functionality ======================================================= +--- 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. +--- +---@param 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|). +---@param opts table Table with options. Current allowed keys: +--- - (whether to delete unsaved buffers; default: +--- `MiniSessions.config.force.read`). +--- - (whether to print session path after action; default +--- `MiniSessions.config.verbose.read`). +--- - (a table with

 and  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.read = function(session_name, opts)
+  if H.is_disabled() then return end
+  if vim.tbl_count(MiniSessions.detected) == 0 then
+    H.error('There is no detected sessions. Change configuration and rerun `MiniSessions.setup()`.')
+  end
+
+  if session_name == nil then
+    if MiniSessions.detected[MiniSessions.config.file] ~= nil then
+      session_name = MiniSessions.config.file
+    else
+      session_name = MiniSessions.get_latest()
+    end
+  end
+
+  opts = vim.tbl_deep_extend('force', H.default_opts('read'), opts or {})
+
+  if not H.validate_detected(session_name) then return end
+
+  local data = MiniSessions.detected[session_name]
+
+  -- Possibly check for unsaved listed buffers and do nothing if present
+  if not opts.force then
+    local unsaved_listed_buffers = H.get_unsaved_listed_buffers()
+
+    if #unsaved_listed_buffers > 0 then
+      local buf_list = table.concat(unsaved_listed_buffers, ', ')
+      H.error(('There are unsaved listed buffers: %s.'):format(buf_list))
+    end
+  end
+
+  -- Execute 'pre' hook
+  H.possibly_execute(opts.hooks.pre, data)
+
+  -- Wipeout all buffers
+  vim.cmd('%bwipeout!')
+
+  -- Read session file
+  local session_path = data.path
+  vim.cmd(('source %s'):format(vim.fn.fnameescape(session_path)))
+  vim.v.this_session = session_path
+
+  -- Possibly notify
+  if opts.verbose then H.message(('Read session %s'):format(session_path)) end
+
+  -- Execute 'post' hook
+  H.possibly_execute(opts.hooks.post, data)
+end
+
+--- 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|.
+---
+---@param session_name string Name of session file to write. Default: `nil` for
+---   current session (|v:this_session|).
+---@param opts table Table with options. Current allowed keys:
+---   -  (whether to ignore existence of session file; default:
+---     `MiniSessions.config.force.write`).
+---   -  (whether to print session path after action; default
+---     `MiniSessions.config.verbose.write`).
+---   -  (a table with 
 and  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.write = function(session_name, opts)
+  if H.is_disabled() then return end
+
+  opts = vim.tbl_deep_extend('force', H.default_opts('write'), opts or {})
+
+  local session_path = H.name_to_path(session_name)
+
+  if not opts.force and H.is_readable_file(session_path) then
+    H.error([[Can't write to existing session when `opts.force` is not `true`.]])
+  end
+
+  local data = H.new_session(session_path)
+
+  -- Execute 'pre' hook
+  H.possibly_execute(opts.hooks.pre, data)
+
+  -- Make session file
+  local cmd = ('mksession%s'):format(opts.force and '!' or '')
+  vim.cmd(('%s %s'):format(cmd, vim.fn.fnameescape(session_path)))
+  data.modify_time = vim.fn.getftime(session_path)
+
+  -- Update detected sessions
+  MiniSessions.detected[data.name] = data
+
+  -- Possibly notify
+  if opts.verbose then H.message(('Written session %s'):format(session_path)) end
+
+  -- Execute 'post' hook
+  H.possibly_execute(opts.hooks.post, data)
+end
+
+--- 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|.
+---
+---@param session_name string Name of detected session file to delete. Default:
+---   `nil` for name of current session (taken from |v:this_session|).
+---@param opts table Table with options. Current allowed keys:
+---   -  (whether to allow deletion of current session; default:
+---     `MiniSessions.config.force.delete`).
+---   -  (whether to print session path after action; default
+---     `MiniSessions.config.verbose.delete`).
+---   -  (a table with 
 and  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.delete = function(session_name, opts)
+  if H.is_disabled() then return end
+  if vim.tbl_count(MiniSessions.detected) == 0 then
+    H.error('There is no detected sessions. Change configuration and rerun `MiniSessions.setup()`.')
+  end
+
+  opts = vim.tbl_deep_extend('force', H.default_opts('delete'), opts or {})
+
+  local session_path = H.name_to_path(session_name)
+
+  -- Make sure to delete only detected session (matters for local session)
+  session_name = vim.fn.fnamemodify(session_path, ':t')
+  if not H.validate_detected(session_name) then return end
+  session_path = MiniSessions.detected[session_name].path
+
+  local is_current_session = session_path == vim.v.this_session
+  if not opts.force and is_current_session then
+    H.error([[Can't delete current session when `opts.force` is not `true`.]])
+  end
+
+  local data = MiniSessions.detected[session_name]
+
+  -- Execute 'pre' hook
+  H.possibly_execute(opts.hooks.pre, data)
+
+  -- Delete and update detected sessions
+  vim.fn.delete(session_path)
+  MiniSessions.detected[session_name] = nil
+  if is_current_session then vim.v.this_session = '' end
+
+  -- Possibly notify
+  if opts.verbose then H.message(('Deleted session %s'):format(session_path)) end
+
+  -- Execute 'pre' hook
+  H.possibly_execute(opts.hooks.post, data)
+end
+
+--- 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").
+---
+---@param action string Action to perform. Should be one of "read" (default),
+---   "write", or "delete".
+---@param opts table Options for specified action.
+MiniSessions.select = function(action, opts)
+  if not (type(vim.ui) == 'table' and type(vim.ui.select) == 'function') then
+    H.error('`MiniSessions.select()` requires `vim.ui.select()` function.')
+  end
+
+  action = action or 'read'
+  if not vim.tbl_contains({ 'read', 'write', 'delete' }, action) then
+    H.error("`action` should be one of 'read', 'write', or 'delete'.")
+  end
+
+  -- Ensure consistent order of items
+  local detected = {}
+  for _, session in pairs(MiniSessions.detected) do
+    table.insert(detected, session)
+  end
+  local sort_fun = function(a, b)
+    -- Put local session first, others - increasing alphabetically
+    local a_name = a.type == 'local' and '' or a.name
+    local b_name = b.type == 'local' and '' or b.name
+    return a_name < b_name
+  end
+  table.sort(detected, sort_fun)
+  local detected_names = vim.tbl_map(function(x) return x.name end, detected)
+
+  vim.ui.select(detected_names, {
+    prompt = 'Select session to ' .. action,
+    format_item = function(x) return ('%s (%s)'):format(x, MiniSessions.detected[x].type) end,
+  }, function(item, idx)
+    if item == nil then return end
+    MiniSessions[action](item, opts)
+  end)
+end
+
+--- 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.get_latest = function()
+  if vim.tbl_count(MiniSessions.detected) == 0 then return end
+
+  local latest_time, latest_name = -1, nil
+  for name, data in pairs(MiniSessions.detected) do
+    if data.modify_time > latest_time then
+      latest_time, latest_name = data.modify_time, name
+    end
+  end
+
+  return latest_name
+end
+
+--- Act on |VimEnter|
+MiniSessions.on_vimenter = function()
+  if MiniSessions.config.autoread and not H.is_something_shown() then MiniSessions.read() end
+end
+
+-- Helper data ================================================================
+-- Module default config
+H.default_config = MiniSessions.config
+
+-- Helper functionality =======================================================
+-- Settings -------------------------------------------------------------------
+H.setup_config = function(config)
+  -- General idea: if some table elements are not present in user-supplied
+  -- `config`, take them from default config
+  vim.validate({ config = { config, 'table', true } })
+  config = vim.tbl_deep_extend('force', H.default_config, config or {})
+
+  -- Validate per nesting level to produce correct error message
+  vim.validate({
+    autoread = { config.autoread, 'boolean' },
+    autowrite = { config.autowrite, 'boolean' },
+    directory = { config.directory, 'string' },
+    file = { config.file, 'string' },
+    force = { config.force, 'table' },
+    hooks = { config.hooks, 'table' },
+    verbose = { config.verbose, 'table' },
+  })
+
+  vim.validate({
+    ['force.read'] = { config.force.read, 'boolean' },
+    ['force.write'] = { config.force.write, 'boolean' },
+    ['force.delete'] = { config.force.delete, 'boolean' },
+
+    ['hooks.pre'] = { config.hooks.pre, 'table' },
+    ['hooks.post'] = { config.hooks.post, 'table' },
+
+    ['verbose.read'] = { config.verbose.read, 'boolean' },
+    ['verbose.write'] = { config.verbose.write, 'boolean' },
+    ['verbose.delete'] = { config.verbose.delete, 'boolean' },
+  })
+
+  vim.validate({
+    ['hooks.pre.read'] = { config.hooks.pre.read, 'function', true },
+    ['hooks.pre.write'] = { config.hooks.pre.write, 'function', true },
+    ['hooks.pre.delete'] = { config.hooks.pre.delete, 'function', true },
+
+    ['hooks.post.read'] = { config.hooks.post.read, 'function', true },
+    ['hooks.post.write'] = { config.hooks.post.write, 'function', true },
+    ['hooks.post.delete'] = { config.hooks.post.delete, 'function', true },
+  })
+
+  return config
+end
+
+H.apply_config = function(config)
+  MiniSessions.config = config
+
+  MiniSessions.detected = H.detect_sessions(config)
+end
+
+H.is_disabled = function() return vim.g.minisessions_disable == true or vim.b.minisessions_disable == true end
+
+-- Work with sessions ---------------------------------------------------------
+H.detect_sessions = function(config)
+  local res_global = config.directory == '' and {} or H.detect_sessions_global(config.directory)
+  local res_local = config.file == '' and {} or H.detect_sessions_local(config.file)
+
+  -- If there are both local and global session with same name, prefer local
+  return vim.tbl_deep_extend('force', res_global, res_local)
+end
+
+H.detect_sessions_global = function(global_dir)
+  global_dir = H.full_path(global_dir)
+  if vim.fn.isdirectory(global_dir) ~= 1 then
+    H.message(('%s is not a directory path.'):format(vim.inspect(global_dir)))
+    return {}
+  end
+
+  local globs = vim.fn.globpath(global_dir, '*')
+  if #globs == 0 then return {} end
+
+  local res = {}
+  for _, f in pairs(vim.split(globs, '\n')) do
+    if H.is_readable_file(f) then
+      local s = H.new_session(f, 'global')
+      res[s.name] = s
+    end
+  end
+  return res
+end
+
+H.detect_sessions_local = function(local_file)
+  local f = H.joinpath(vim.fn.getcwd(), local_file)
+
+  if not H.is_readable_file(f) then return {} end
+
+  local res = {}
+  local s = H.new_session(f, 'local')
+  res[s.name] = s
+  return res
+end
+
+H.new_session = function(session_path, session_type)
+  return {
+    modify_time = vim.fn.getftime(session_path),
+    name = vim.fn.fnamemodify(session_path, ':t'),
+    path = H.full_path(session_path),
+    type = session_type or H.get_session_type(session_path),
+  }
+end
+
+H.get_session_type = function(session_path)
+  if MiniSessions.config.directory == '' then return 'local' end
+
+  local session_dir = H.full_path(session_path)
+  local global_dir = H.full_path(MiniSessions.config.directory)
+  return session_dir == global_dir and 'global' or 'local'
+end
+
+H.validate_detected = function(session_name)
+  local is_detected = vim.tbl_contains(vim.tbl_keys(MiniSessions.detected), session_name)
+  if is_detected then return true end
+
+  H.error(('%s is not a name for detected session.'):format(vim.inspect(session_name)))
+end
+
+H.get_unsaved_listed_buffers = function()
+  return vim.tbl_filter(
+    function(buf_id) return vim.api.nvim_buf_get_option(buf_id, 'modified') and vim.api.nvim_buf_get_option(buf_id, 'buflisted') end,
+    vim.api.nvim_list_bufs()
+  )
+end
+
+H.get_current_session_name = function() return vim.fn.fnamemodify(vim.v.this_session, ':t') end
+
+H.name_to_path = function(session_name)
+  if session_name == nil then
+    if vim.v.this_session == '' then H.error('There is no active session. Supply non-nil session name.') end
+    return vim.v.this_session
+  end
+
+  session_name = tostring(session_name)
+  if session_name == '' then H.error('Supply non-empty session name.') end
+
+  local session_dir = (session_name == MiniSessions.config.file) and vim.fn.getcwd() or MiniSessions.config.directory
+  local path = H.joinpath(session_dir, session_name)
+  return H.full_path(path)
+end
+
+-- Utilities ------------------------------------------------------------------
+H.default_opts = function(action)
+  local config = MiniSessions.config
+  return {
+    force = config.force[action],
+    verbose = config.verbose[action],
+    hooks = { pre = config.hooks.pre[action], post = config.hooks.post[action] },
+  }
+end
+
+H.message = function(msg) vim.cmd('echomsg ' .. vim.inspect('(mini.sessions) ' .. msg)) end
+
+H.error = function(msg) error(('(mini.sessions) %s'):format(msg)) end
+
+H.is_readable_file = function(path) return vim.fn.isdirectory(path) ~= 1 and vim.fn.getfperm(path):sub(1, 1) == 'r' end
+
+H.joinpath = function(directory, filename) return ('%s%s%s'):format(directory, H.path_sep, tostring(filename)) end
+
+H.full_path = function(path) return vim.fn.resolve(vim.fn.fnamemodify(path, ':p')) end
+
+H.is_something_shown = function()
+  -- Don't autoread session if Neovim is opened to show something. That is
+  -- when at least one of the following is true:
+  -- - Current buffer has any lines (something opened explicitly).
+  -- NOTE: Usage of `line2byte(line('$') + 1) > 0` seemed to be fine, but it
+  -- doesn't work if some automated changed was made to buffer while leaving it
+  -- empty (returns 2 instead of -1). This was also the reason of not being
+  -- able to test with child Neovim process from 'tests/helpers'.
+  local lines = vim.api.nvim_buf_get_lines(0, 0, -1, true)
+  if #lines > 1 or (#lines == 1 and lines[1]:len() > 0) then return true end
+
+  -- - Several buffers are listed (like session with placeholder buffers). That
+  --   means unlisted buffers (like from `nvim-tree`) don't affect decision.
+  local listed_buffers = vim.tbl_filter(
+    function(buf_id) return vim.fn.buflisted(buf_id) == 1 end,
+    vim.api.nvim_list_bufs()
+  )
+  if #listed_buffers > 1 then return true end
+
+  -- - There are files in arguments (like `nvim foo.txt` with new file).
+  if vim.fn.argc() > 0 then return true end
+
+  return false
+end
+
+H.possibly_execute = function(f, ...)
+  if f == nil then return end
+  return f(...)
+end
+
+return MiniSessions
diff --git a/dotfiles/pack/plugins/start/mini.nvim/lua/mini/starter.lua b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/starter.lua
new file mode 100755
index 0000000..2b9d8e3
--- /dev/null
+++ b/dotfiles/pack/plugins/start/mini.nvim/lua/mini/starter.lua
@@ -0,0 +1,1500 @@
+-- MIT License Copyright (c) 2021 Evgeni Chasnovski
+
+-- Documentation ==============================================================
+--- 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:
+---     -  - function or string for |vim.cmd| which is executed when
+---       item is chosen. Empty string result in placeholder "inactive" item.
+---     -  - string which will be displayed and used for choosing.
+---     - 
- 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. +---@tag mini.starter +---@tag MiniStarter + +--- 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 }, +--- }) +--- < +---@tag MiniStarter-example-config + +--- # 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|. +--- - deletes latest character from query. +--- - /, /, / move current item. +--- - executes action of current item. +--- - closes Starter buffer. +--- - Evaluate current item when appropriate (after `` or when there is a +--- single item and `MiniStarter.config.evaluate_single` is `true`). This +--- executes item's `action`. +---@tag MiniStarter-lifecycle + +---@alias __starter_buf_id number|nil Buffer identifier of a valid Starter buffer. +--- Default: current buffer. +---@alias __section_fun function Function which returns array of items. + +-- Module definition ========================================================== +local MiniStarter = {} +local H = {} + +--- Module setup +--- +---@param config table Module config table. See |MiniStarter.config|. +--- +---@usage `require('mini.starter').setup({})` (replace `{}` with your `config` table) +MiniStarter.setup = function(config) + -- Export module + _G.MiniStarter = MiniStarter + + -- Setup config + config = H.setup_config(config) + + -- Apply config + H.apply_config(config) + + -- Module behavior + vim.api.nvim_exec( + [[augroup MiniStarter + au! + au VimEnter * ++nested ++once lua MiniStarter.on_vimenter() + augroup END]], + false + ) + + -- Create highlighting + vim.api.nvim_exec( + [[hi default link MiniStarterCurrent NONE + hi default link MiniStarterFooter Title + hi default link MiniStarterHeader Title + hi default link MiniStarterInactive Comment + hi default link MiniStarterItem Normal + hi default link MiniStarterItemBullet Delimiter + hi default link MiniStarterItemPrefix WarningMsg + hi default link MiniStarterSection Delimiter + hi default link MiniStarterQuery MoreMsg]], + false + ) +end + +--- Module config +--- +--- Default values: +---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section) +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 , , and
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_-.', +} +--minidoc_afterlines_end + +-- Module functionality ======================================================= +--- Act on |VimEnter|. +MiniStarter.on_vimenter = function() + if MiniStarter.config.autoopen and not H.is_something_shown() then + -- Set indicator used to make different decision on startup + H.is_in_vimenter = true + MiniStarter.open() + end +end + +--- 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 `. +--- +--- Note: to fully use it in autocommand, it is recommended to utilize +--- |autocmd-nested|. Example: +--- `autocmd TabNewEntered * ++nested lua MiniStarter.open()` +--- +---@param buf_id number Identifier of existing valid buffer (see |bufnr()|) to +--- open inside. Default: create a new one. +MiniStarter.open = function(buf_id) + if H.is_disabled() then return end + + -- Ensure proper buffer and open it + if H.is_in_vimenter then + -- Use current buffer as it should be empty and not needed. This also + -- solves the issue of redundant buffer when opening a file from Starter. + buf_id = vim.api.nvim_get_current_buf() + end + + if buf_id == nil or not vim.api.nvim_buf_is_valid(buf_id) then buf_id = vim.api.nvim_create_buf(false, true) end + + -- Create buffer data entry + H.buffer_data[buf_id] = { current_item_id = 1, query = '' } + + -- Ensure that local config in opened Starter buffer is the same as current. + -- This allow more advanced usage of buffer local configuration. + local config_local = vim.b.ministarter_config + vim.api.nvim_set_current_buf(buf_id) + vim.b.ministarter_config = config_local + + -- Setup buffer behavior + H.make_buffer_autocmd(buf_id) + H.apply_buffer_options(buf_id) + H.apply_buffer_mappings(buf_id) + + -- Populate buffer + MiniStarter.refresh() + + -- Issue custom event + vim.cmd('doautocmd User MiniStarterOpened') + + -- Ensure not being in VimEnter + H.is_in_vimenter = false +end + +--- 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. +--- +---@param buf_id __starter_buf_id +MiniStarter.refresh = function(buf_id) + buf_id = buf_id or vim.api.nvim_get_current_buf() + if not H.validate_starter_buf_id(buf_id, 'refresh()') then return end + + local data = H.buffer_data[buf_id] + local config = H.get_config() + + -- Normalize certain config values + data.header = H.normalize_header_footer(config.header or H.default_header) + local items = H.normalize_items(config.items or H.default_items) + data.footer = H.normalize_header_footer(config.footer or H.default_footer) + + -- Evaluate content + local content = H.make_initial_content(data.header, items, data.footer) + local hooks = config.content_hooks or H.default_content_hooks + for _, f in ipairs(hooks) do + content = f(content) + end + data.content = content + + -- Set items. Possibly reset current item id if items have changed. + local old_items = data.items + data.items = MiniStarter.content_to_items(content) + if not vim.deep_equal(data.items, old_items) then data.current_item_id = 1 end + + -- Add content + vim.api.nvim_buf_set_option(buf_id, 'modifiable', true) + vim.api.nvim_buf_set_lines(buf_id, 0, -1, false, MiniStarter.content_to_lines(content)) + vim.api.nvim_buf_set_option(buf_id, 'modifiable', false) + + -- Add highlighting + H.content_highlight(buf_id) + H.items_highlight(buf_id) + + -- -- Always position cursor on current item + H.position_cursor_on_current_item(buf_id) + H.add_hl_current_item(buf_id) + + -- Apply current query (clear command line afterwards) + H.make_query(buf_id) +end + +--- Close Starter buffer +--- +---@param buf_id __starter_buf_id +MiniStarter.close = function(buf_id) + buf_id = buf_id or vim.api.nvim_get_current_buf() + if not H.validate_starter_buf_id(buf_id, 'close()') then return end + + -- Use `pcall` to allow calling for already non-existing buffer + pcall(vim.api.nvim_buf_delete, buf_id, {}) +end + +-- Sections ------------------------------------------------------------------- +--- Table of pre-configured sections +MiniStarter.sections = {} + +--- Section with builtin actions +--- +---@return table Array of items. +MiniStarter.sections.builtin_actions = function() + return { + { name = 'Edit new buffer', action = 'enew', section = 'Builtin actions' }, + { name = 'Quit Neovim', action = 'qall', section = 'Builtin actions' }, + } +end + +--- 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. +--- +---@param n number Number of returned items. Default: 5. +---@param recent boolean Whether to use recent sessions (instead of +--- alphabetically by name). Default: true. +--- +---@return __section_fun +MiniStarter.sections.sessions = function(n, recent) + n = n or 5 + recent = recent == nil and true or recent + + return function() + if _G.MiniSessions == nil then + return { { name = [['mini.sessions' is not set up]], action = '', section = 'Sessions' } } + end + + local items = {} + for session_name, session in pairs(_G.MiniSessions.detected) do + table.insert(items, { + _session = session, + name = ('%s%s'):format(session_name, session.type == 'local' and ' (local)' or ''), + action = ([[lua _G.MiniSessions.read('%s')]]):format(session_name), + section = 'Sessions', + }) + end + + if vim.tbl_count(items) == 0 then + return { { name = [[There are no detected sessions in 'mini.sessions']], action = '', section = 'Sessions' } } + end + + local sort_fun + if recent then + sort_fun = function(a, b) + local a_time = a._session.type == 'local' and math.huge or a._session.modify_time + local b_time = b._session.type == 'local' and math.huge or b._session.modify_time + return a_time > b_time + end + else + sort_fun = function(a, b) + local a_name = a._session.type == 'local' and '' or a.name + local b_name = b._session.type == 'local' and '' or b.name + return a_name < b_name + end + end + table.sort(items, sort_fun) + + -- Take only first `n` elements and remove helper fields + return vim.tbl_map(function(x) + x._session = nil + return x + end, vim.list_slice(items, 1, n)) + end +end + +--- Section with most recently used files +--- +--- Files are taken from |vim.v.oldfiles|. +--- +---@param n number Number of returned items. Default: 5. +---@param current_dir boolean Whether to return files only from current working +--- directory. Default: `false`. +---@param show_path boolean Whether to append file name with its full path. +--- Default: `true`. +--- +---@return __section_fun +MiniStarter.sections.recent_files = function(n, current_dir, show_path) + n = n or 5 + current_dir = current_dir == nil and false or current_dir + show_path = show_path == nil and true or show_path + + if current_dir then vim.cmd('au DirChanged * lua MiniStarter.refresh()') end + + return function() + local section = ('Recent files%s'):format(current_dir and ' (current directory)' or '') + + -- Use only actual readable files + local files = vim.tbl_filter(function(f) return vim.fn.filereadable(f) == 1 end, vim.v.oldfiles or {}) + + if #files == 0 then + return { { name = 'There are no recent files (`v:oldfiles` is empty)', action = '', section = section } } + end + + -- Possibly filter files from current directory + if current_dir then + local cwd = vim.loop.cwd() + local n_cwd = cwd:len() + files = vim.tbl_filter(function(f) return f:sub(1, n_cwd) == cwd end, files) + end + + if #files == 0 then + return { { name = 'There are no recent files in current directory', action = '', section = section } } + end + + -- Create items + local items = {} + local fmodify = vim.fn.fnamemodify + for _, f in ipairs(vim.list_slice(files, 1, n)) do + local path = show_path and (' (%s)'):format(fmodify(f, ':~:.')) or '' + local name = ('%s%s'):format(fmodify(f, ':t'), path) + table.insert(items, { action = ('edit %s'):format(fmodify(f, ':p')), name = name, section = section }) + end + + return items + end +end + +-- stylua: ignore start +--- Section with basic Telescope pickers relevant to start screen +--- +---@return __section_fun +MiniStarter.sections.telescope = function() + return function() + return { + {action = 'Telescope file_browser', name = 'Browser', section = 'Telescope'}, + {action = 'Telescope command_history', name = 'Command history', section = 'Telescope'}, + {action = 'Telescope find_files', name = 'Files', section = 'Telescope'}, + {action = 'Telescope help_tags', name = 'Help tags', section = 'Telescope'}, + {action = 'Telescope live_grep', name = 'Live grep', section = 'Telescope'}, + {action = 'Telescope oldfiles', name = 'Old files', section = 'Telescope'}, + } + end +end +-- stylua: ignore end + +-- Content hooks -------------------------------------------------------------- +--- 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 = {} + +--- 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. +--- +---@param left number Number of empty spaces to add to start of each content +--- line. Default: 0. +---@param top number Number of empty lines to add to start of content. +--- Default: 0. +--- +---@return function Content hook. +MiniStarter.gen_hook.padding = function(left, top) + left = math.max(left or 0, 0) + top = math.max(top or 0, 0) + return function(content) + -- Add left padding + local left_pad = string.rep(' ', left) + for _, line in ipairs(content) do + local is_empty_line = #line == 0 or (#line == 1 and line[1].string == '') + if not is_empty_line then table.insert(line, 1, H.content_unit(left_pad, 'empty', nil)) end + end + + -- Add top padding + local top_lines = {} + for _ = 1, top do + table.insert(top_lines, { H.content_unit('', 'empty', nil) }) + end + content = vim.list_extend(top_lines, content) + + return content + end +end + +--- Hook generator for adding bullet to items +--- +--- Output is a content hook which adds supplied string to be displayed to the +--- left of item. +--- +---@param bullet string String to be placed to the left of item name. +--- Default: "░ ". +---@param 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.adding_bullet = function(bullet, place_cursor) + bullet = bullet or '░ ' + place_cursor = place_cursor == nil and true or place_cursor + return function(content) + local coords = MiniStarter.content_coords(content, 'item') + -- Go backwards to avoid conflict when inserting units + for i = #coords, 1, -1 do + local l_num, u_num = coords[i].line, coords[i].unit + local bullet_unit = { + string = bullet, + type = 'item_bullet', + hl = 'MiniStarterItemBullet', + -- Use `_item` instead of `item` because it is better to be 'private' + _item = content[l_num][u_num].item, + _place_cursor = place_cursor, + } + table.insert(content[l_num], u_num, bullet_unit) + end + + return content + end +end + +--- 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). +--- +---@param grouping string One of "all" (number indexing across all sections) or +--- "section" (letter-number indexing within each section). Default: "all". +---@param 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.indexing = function(grouping, exclude_sections) + grouping = grouping or 'all' + exclude_sections = exclude_sections or {} + local per_section = grouping == 'section' + + return function(content) + local cur_section, n_section, n_item = nil, 0, 0 + local coords = MiniStarter.content_coords(content, 'item') + + for _, c in ipairs(coords) do + local unit = content[c.line][c.unit] + local item = unit.item + + if not vim.tbl_contains(exclude_sections, item.section) then + n_item = n_item + 1 + if cur_section ~= item.section then + cur_section = item.section + -- Cycle through lower case letters + n_section = math.fmod(n_section, 26) + 1 + n_item = per_section and 1 or n_item + end + + local section_index = per_section and string.char(96 + n_section) or '' + unit.string = ('%s%s. %s'):format(section_index, n_item, unit.string) + end + end + + return content + end +end + +--- 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. +--- +---@param horizontal string One of "left", "center", "right". Default: "left". +---@param vertical string One of "top", "center", "bottom". Default: "top". +--- +---@return function Content hook. +MiniStarter.gen_hook.aligning = function(horizontal, vertical) + horizontal = horizontal == nil and 'left' or horizontal + vertical = vertical == nil and 'top' or vertical + + local horiz_coef = ({ left = 0, center = 0.5, right = 1.0 })[horizontal] + local vert_coef = ({ top = 0, center = 0.5, bottom = 1.0 })[vertical] + + return function(content) + local line_strings = MiniStarter.content_to_lines(content) + + -- Align horizontally + -- Don't use `string.len()` to account for multibyte characters + local lines_width = vim.tbl_map(function(l) return vim.fn.strdisplaywidth(l) end, line_strings) + local min_right_space = vim.api.nvim_win_get_width(0) - math.max(unpack(lines_width)) + local left_pad = math.max(math.floor(horiz_coef * min_right_space), 0) + + -- Align vertically + local bottom_space = vim.api.nvim_win_get_height(0) - #line_strings + local top_pad = math.max(math.floor(vert_coef * bottom_space), 0) + + return MiniStarter.gen_hook.padding(left_pad, top_pad)(content) + end +end + +-- Work with content ---------------------------------------------------------- +--- 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. +--- +---@param buf_id __starter_buf_id +MiniStarter.get_content = function(buf_id) + buf_id = buf_id or vim.api.nvim_get_current_buf() + if not H.validate_starter_buf_id(buf_id, 'get_content()', 'error') then return end + + return H.buffer_data[buf_id].content +end + +--- 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. +--- +---@param content table Content "2d array". Default: content of current buffer. +---@param 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 and keys. To retrieve actual unit from coordinate +--- `c`, use `content[c.line][c.unit]`. +MiniStarter.content_coords = function(content, predicate) + content = content or MiniStarter.get_content() + if predicate == nil then predicate = function(unit) return true end end + if type(predicate) == 'string' then + local pred_type = predicate + predicate = function(unit) return unit.type == pred_type end + end + + local res = {} + for l_num, line in ipairs(content) do + for u_num, unit in ipairs(line) do + if predicate(unit) then table.insert(res, { line = l_num, unit = u_num }) end + end + end + return res +end + +-- stylua: ignore start +--- Convert content to buffer lines +--- +--- One buffer line is made by concatenating `string` element of units within +--- same content line. +--- +---@param content table Content "2d array". Default: content of current buffer. +--- +---@return table Array of strings for each buffer line. +MiniStarter.content_to_lines = function(content) + return vim.tbl_map( + function(content_line) + return table.concat( + -- Ensure that each content line is indeed a single buffer line + vim.tbl_map(function(x) return x.string:gsub('\n', ' ') end, content_line), '' + ) + end, + content or MiniStarter.get_content() + ) +end +-- stylua: ignore end + +--- 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|). +--- +---@param content table Content "2d array". Default: content of current buffer. +--- +---@return table Array of items. +MiniStarter.content_to_items = function(content) + content = content or MiniStarter.get_content() + + -- NOTE: this havily utilizes 'modify by reference' nature of Lua tables + local items = {} + for l_num, line in ipairs(content) do + -- Track 0-based starting column of current unit (using byte length) + local start_col = 0 + for _, unit in ipairs(line) do + -- Cursor position is (1, 0)-based + local cursorpos = { l_num, start_col } + + if unit.type == 'item' then + local item = unit.item + -- Take item's name from content string + item.name = unit.string:gsub('\n', ' ') + item._line = l_num - 1 + item._start_col = start_col + item._end_col = start_col + unit.string:len() + -- Don't overwrite possible cursor position from item's bullet + item._cursorpos = item._cursorpos or cursorpos + + table.insert(items, item) + end + + -- Prefer placing cursor at start of item's bullet + if unit.type == 'item_bullet' and unit._place_cursor then + -- Item bullet uses 'private' `_item` element instead of `item` + unit._item._cursorpos = cursorpos + end + + start_col = start_col + unit.string:len() + end + end + + -- Compute length of unique prefix for every item's name (ignoring case) + local strings = vim.tbl_map(function(x) return x.name:lower() end, items) + local nprefix = H.unique_nprefix(strings) + for i, n in ipairs(nprefix) do + items[i]._nprefix = n + end + + return items +end + +-- Other exported functions --------------------------------------------------- +--- Evaluate current item +--- +--- Note that it resets current query before evaluation, as it is rarely needed +--- any more. +--- +---@param buf_id __starter_buf_id +MiniStarter.eval_current_item = function(buf_id) + buf_id = buf_id or vim.api.nvim_get_current_buf() + if not H.validate_starter_buf_id(buf_id, 'eval_current_item()') then return end + + -- Reset query before evaluation without query echo (avoids hit-enter-prompt) + H.make_query(vim.api.nvim_get_current_buf(), '', false) + + local data = H.buffer_data[buf_id] + H.eval_fun_or_string(data.items[data.current_item_id].action, true) +end + +--- Update current item +--- +--- This makes next (with respect to `direction`) active item to be current. +--- +---@param direction string One of "next" or "previous". +---@param buf_id __starter_buf_id +MiniStarter.update_current_item = function(direction, buf_id) + buf_id = buf_id or vim.api.nvim_get_current_buf() + if not H.validate_starter_buf_id(buf_id, 'update_current_item()') then return end + + local data = H.buffer_data[buf_id] + + -- Advance current item + local prev_current = data.current_item_id + data.current_item_id = H.next_active_item_id(buf_id, data.current_item_id, direction) + if data.current_item_id == prev_current then return end + + -- Update cursor position + H.position_cursor_on_current_item(buf_id) + + -- Highlight current item + vim.api.nvim_buf_clear_namespace(buf_id, H.ns.current_item, 0, -1) + H.add_hl_current_item(buf_id) +end + +--- 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. +--- +---@param char string Single character to be added to query. If `nil`, deletes +--- latest character from query. +---@param buf_id __starter_buf_id +MiniStarter.add_to_query = function(char, buf_id) + buf_id = buf_id or vim.api.nvim_get_current_buf() + if not H.validate_starter_buf_id(buf_id, 'add_to_query()') then return end + + local data = H.buffer_data[buf_id] + + local new_query + if char == nil then + new_query = data.query:sub(0, data.query:len() - 1) + else + new_query = ('%s%s'):format(data.query, char) + end + H.make_query(buf_id, new_query) +end + +--- Set current query +--- +---@param 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. +---@param buf_id __starter_buf_id +MiniStarter.set_query = function(query, buf_id) + query = query or '' + if type(query) ~= 'string' then error('`query` should be either `nil` or string.') end + + buf_id = buf_id or vim.api.nvim_get_current_buf() + if not H.validate_starter_buf_id(buf_id, 'add_to_query()') then return end + + H.make_query(buf_id, query) +end + +--- Act on |CursorMoved| by repositioning cursor in fixed place. +MiniStarter.on_cursormoved = function(buf_id) + buf_id = buf_id or vim.api.nvim_get_current_buf() + if not H.validate_starter_buf_id(buf_id, 'on_cursormoved()') then return end + H.position_cursor_on_current_item(buf_id) +end + +-- Helper data ================================================================ +-- Module default config +H.default_config = MiniStarter.config + +-- Default config values +H.default_items = { + function() + if _G.MiniSessions == nil then return {} end + return MiniStarter.sections.sessions(5, true)() + end, + MiniStarter.sections.recent_files(5, false, false), + MiniStarter.sections.builtin_actions(), +} + +H.default_header = function() + local hour = tonumber(vim.fn.strftime('%H')) + -- [04:00, 12:00) - morning, [12:00, 20:00) - day, [20:00, 04:00) - evening + local part_id = math.floor((hour + 4) / 8) + 1 + local day_part = ({ 'evening', 'morning', 'afternoon', 'evening' })[part_id] + local username = vim.loop.os_get_passwd()['username'] or 'USERNAME' + + return ('Good %s, %s'):format(day_part, username) +end + +H.default_footer = [[ +Type query to filter items + deletes latest character from query + resets current query +, , move current item + executes action of current item + closes this buffer]] + +H.default_content_hooks = { MiniStarter.gen_hook.adding_bullet(), MiniStarter.gen_hook.aligning('center', 'center') } + +-- Storage for all Starter buffers. Fields - buffer number. Values - table: +-- - - buffer content (2d array of units) +-- - - identifier of current item +-- -