Showing preview only (341K chars total). Download the full file or copy to clipboard to get everything.
Repository: Saghen/blink.nvim
Branch: main
Commit: d1e7c7c45d45
Files: 58
Total size: 323.2 KB
Directory structure:
gitextract_6oebunai/
├── .cargo/
│ └── config.toml
├── .gitignore
├── .stylua.toml
├── Cargo.toml
├── LICENSE
├── README.md
├── flake.nix
├── lua/
│ └── blink/
│ ├── chartoggle/
│ │ ├── config.lua
│ │ └── init.lua
│ ├── clue/
│ │ └── init.lua
│ ├── config.lua
│ ├── dashboard/
│ │ └── init.lua
│ ├── init.lua
│ ├── render/
│ │ └── types.lua
│ ├── select/
│ │ ├── config.lua
│ │ ├── init.lua
│ │ ├── providers/
│ │ │ ├── buffers.lua
│ │ │ ├── code-actions.lua
│ │ │ ├── diagnostics.lua
│ │ │ ├── lsp/
│ │ │ │ ├── definitions.lua
│ │ │ │ ├── references.lua
│ │ │ │ └── symbols.lua
│ │ │ ├── recent-commands.lua
│ │ │ ├── recent-searches.lua
│ │ │ ├── smart-open.lua
│ │ │ └── yank-history.lua
│ │ ├── renderer.lua
│ │ ├── types.lua
│ │ └── window.lua
│ └── tree/
│ ├── binds/
│ │ ├── activate.lua
│ │ ├── basic.lua
│ │ ├── expand.lua
│ │ ├── init.lua
│ │ └── move.lua
│ ├── config.lua
│ ├── git/
│ │ ├── git2.lua
│ │ ├── ignore.lua
│ │ ├── init.lua
│ │ ├── libgit2.lua
│ │ └── stat.lua
│ ├── init.lua
│ ├── lib/
│ │ ├── fs.lua
│ │ ├── tree.lua
│ │ ├── utils.lua
│ │ └── uv.lua
│ ├── popup.lua
│ ├── renderer.lua
│ ├── tree.lua
│ └── window.lua
├── scripts/
│ ├── dual_log.sh
│ ├── dual_push.sh
│ └── dual_sync.sh
└── src/
├── job/
│ ├── default.rs
│ ├── mod.rs
│ ├── options.rs
│ ├── pty.rs
│ └── trait.rs
└── lib.rs
================================================
FILE CONTENTS
================================================
================================================
FILE: .cargo/config.toml
================================================
[target.x86_64-apple-darwin]
rustflags = [
"-C", "link-arg=-undefined",
"-C", "link-arg=dynamic_lookup",
]
[target.aarch64-apple-darwin]
rustflags = [
"-C", "link-arg=-undefined",
"-C", "link-arg=dynamic_lookup",
]
================================================
FILE: .gitignore
================================================
.archive.lua
dual/
result
.direnv
.devenv
/target
================================================
FILE: .stylua.toml
================================================
column_width = 120
line_endings = "Unix"
indent_type = "Spaces"
indent_width = 2
quote_style = "AutoPreferSingle"
call_parentheses = "Always"
collapse_simple_statement = "Always"
================================================
FILE: Cargo.toml
================================================
[package]
name = "blink_delimiters"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
anyhow = "1.0.95"
nvim-oxi = { version = "0.5.1", features = ["neovim-0-10"] }
portable-pty = "0.9.0"
serde = "1.0.216"
lazy_static = "1.5.0"
logos = "0.15.0"
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2024 Liam Dyer
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.
================================================
FILE: README.md
================================================
<div align="center">
# blink.nvim
Experimental library of neovim plugins with a focus on performance and simplicity
</div>
## Modules
| status | module | description |
|--------|---------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| stable | [blink.chartoggle](/readmes/chartoggle/README.md) | Toggles a character at the end of the current line |
| stable | [blink.cmp](https://github.com/saghen/blink.cmp) | Performant autocompletion plugin, inspired by [nvim-cmp](https://github.com/hrsh7th/nvim-cmp) |
| alpha | [blink.pairs](https://github.com/saghen/blink.pairs) | Rainbow highlighting and intelligent auto-pairs |
| stable | [blink.indent](https://github.com/saghen/blink.indent) | Indent guides with scope on every keystroke |
| WIP | [blink.select](/readmes/select/README.md) | Generic selection UI with built-in providers |
| alpha | [blink.tree](/readmes/tree/README.md) | Tree plugin with async io and FFI git, similar to [neo-tree](https://github.com/nvim-neo-tree/neo-tree.nvim) but eventually to be rewritten to be like oil.nvim |
## Installation
`lazy.nvim`
```lua
{
'saghen/blink.nvim',
build = 'cargo build --release', -- for delimiters
keys = {
-- chartoggle
{
'<C-;>',
function()
require('blink.chartoggle').toggle_char_eol(';')
end,
mode = { 'n', 'v' },
desc = 'Toggle ; at eol',
},
{
',',
function()
require('blink.chartoggle').toggle_char_eol(',')
end,
mode = { 'n', 'v' },
desc = 'Toggle , at eol',
},
-- tree
{ '<C-e>', '<cmd>BlinkTree reveal<cr>', desc = 'Reveal current file in tree' },
{ '<leader>E', '<cmd>BlinkTree toggle<cr>', desc = 'Reveal current file in tree' },
{ '<leader>e', '<cmd>BlinkTree toggle-focus<cr>', desc = 'Toggle file tree focus' },
},
-- all modules handle lazy loading internally
lazy = false,
opts = {
chartoggle = { enabled = true },
tree = { enabled = true }
}
}
================================================
FILE: flake.nix
================================================
{
description = "Set of simple, performant neovim plugins";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
fenix.url = "github:nix-community/fenix";
fenix.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = inputs@{ flake-parts, nixpkgs, fenix, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
systems = [
"x86_64-linux"
"i686-linux"
"x86_64-darwin"
"aarch64-linux"
"aarch64-darwin"
];
perSystem = { config, self', inputs', pkgs, system, lib, ... }: {
# define the packages provided by this flake
packages = {
blink-nvim = pkgs.vimUtils.buildVimPlugin {
pname = "blink-nvim";
version = "2024-11-11";
src = ./.;
meta = {
description = "Set of simple, performant neovim plugins";
homepage = "https://github.com/saghen/blink.nvim";
license = lib.licenses.mit;
maintainers = with lib.maintainers; [ redxtech ];
};
};
default = self'.packages.blink-nvim;
};
};
};
}
================================================
FILE: lua/blink/chartoggle/config.lua
================================================
local M = {}
M.default = {
delimiters = { ',', ';' },
}
function M.setup(opts) M.config = vim.tbl_deep_extend('force', M.default, opts or {}) end
return setmetatable(M, { __index = function(_, k) return M.config[k] end })
================================================
FILE: lua/blink/chartoggle/init.lua
================================================
local M = {}
function M.setup(opts) require('blink.chartoggle.config').setup(opts) end
-- implementation from https://github.com/saifulapm/chartoggle.nvim
-- todo: make a blink plugin with config, delimiters per language, filetype blocklist
function M.toggle_char_eol(character)
local api = vim.api
local delimiters = require('blink.chartoggle.config').delimiters
local mode = api.nvim_get_mode().mode
local is_visual = mode == 'v' or mode == 'V' or mode == '\22' -- <C-v>
-- have to exit visual mode for the marks to update
if is_visual then vim.fn.feedkeys(':', 'nx') end
local start_line = is_visual and vim.fn.getpos("'<")[2] or api.nvim_win_get_cursor(0)[1]
local end_line = is_visual and vim.fn.getpos("'>")[2] or start_line
for line_idx = start_line, end_line do
local line = api.nvim_buf_get_lines(0, line_idx - 1, line_idx, false)[1]
local last_char = line:sub(-1)
if last_char == character then
api.nvim_buf_set_lines(0, line_idx - 1, line_idx, false, { line:sub(1, #line - 1) })
elseif vim.tbl_contains(delimiters, last_char) then
api.nvim_buf_set_lines(0, line_idx - 1, line_idx, false, { line:sub(1, #line - 1) .. character })
else
api.nvim_buf_set_lines(0, line_idx - 1, line_idx, false, { line .. character })
end
end
end
return M
================================================
FILE: lua/blink/clue/init.lua
================================================
--- NOTE: This is just mini.clue with support for backspace
---
--- *mini.clue* Show next key clues
--- *MiniClue*
---
--- MIT License Copyright (c) 2023 Evgeni Chasnovski
---
--- ==============================================================================
---
--- Features:
--- - Implement custom key query process to reach target key combination:
--- - Starts after customizable opt-in triggers (mode + keys).
---
--- - Each key press narrows down set of possible targets.
--- Pressing `<BS>` removes previous user entry.
--- Pressing `<Esc>` or `<C-c>` leads to an early stop.
--- Doesn't depend on 'timeoutlen' and has basic support for 'langmap'.
---
--- - Ends when there is at most one target left or user pressed `<CR>`.
--- Results into emulating pressing all query keys plus possible postkeys.
---
--- - Show window (after configurable delay) with clues. It lists available
--- next keys along with their descriptions (auto generated from descriptions
--- present keymaps and user-supplied clues; preferring the former).
---
--- - Configurable "postkeys" for key combinations - keys which will be emulated
--- after combination is reached during key query process.
---
--- - Provide customizable sets of clues for common built-in keys/concepts:
--- - `g` key.
--- - `z` key.
--- - Window commands.
--- - Built-in completion.
--- - Marks.
--- - Registers.
---
--- - Lua functions to disable/enable triggers globally or per buffer.
---
--- For more details see:
--- - |MiniClue-key-query-process|.
--- - |MiniClue-examples|.
--- - |MiniClue.config|.
--- - |MiniClue.gen_clues|.
---
--- Notes:
--- - Works on all supported versions but using Neovim>=0.9 is recommended.
---
--- - There is no functionality to create mappings while defining clues.
--- This is done to clearly separate these two different actions.
--- The best suggested practice is to manually create mappings with
--- descriptions (`desc` field in options), as they will be automatically
--- used inside clue window.
---
--- - Triggers are implemented as special buffer-local mappings. This leads to
--- several caveats:
--- - They will override same regular buffer-local mappings and have
--- precedence over global one.
---
--- Example: having set `<C-w>` as Normal mode trigger means that
--- there should not be another `<C-w>` mapping.
---
--- - They need to be the latest created buffer-local mappings or they will
--- not function properly. Most common indicator of this is that some
--- mapping starts to work only after clue window is shown.
---
--- Example: `g` is set as Normal mode trigger, but `gcc` from |mini.comment|
--- doesn't work right away. This is probably because there are some
--- other buffer-local mappings starting with `g` which were created after
--- mapping for `g` trigger. Most common places for this are in LSP server's
--- `on_attach` or during tree-sitter start in buffer.
---
--- To check if trigger is the most recent buffer-local mapping, execute
--- `:<mode-char>map <trigger-keys>` (like `:nmap g` for previous example).
--- Mapping for trigger should be the first listed.
---
--- This module makes the best effort to work out of the box and cover
--- most common cases, but it is not full proof. The solution here is to
--- ensure that triggers are created after making all buffer-local mappings:
--- run either |MiniClue.setup()| or |MiniClue.ensure_buf_triggers()|.
---
--- - Descriptions from existing mappings take precedence over user-supplied
--- clues. This is to ensure that information shown in clue window is as
--- relevant as possible. To add/customize description of an already existing
--- mapping, use |MiniClue.set_mapping_desc()|.
---
--- - Due to technical difficulties, there is no full proof support for
--- Operator-pending mode triggers (like `a`/`i` from |mini.ai|):
--- - Doesn't work as part of a command in "temporary Normal mode" (like
--- after |i_CTRL-O|) due to implementation difficulties.
--- - Can have unexpected behavior with custom operators.
---
--- - Has (mostly solved) issues with macros:
--- - All triggers are disabled during macro recording due to technical
--- reasons.
--- - The `@` and `Q` keys are specially mapped inside |MiniClue.setup()|
--- to temporarily disable triggers.
---
--- # Setup ~
---
--- This module needs a setup with `require('mini.clue').setup({})` (replace
--- `{}` with your `config` table). It will create global Lua table `MiniClue`
--- which you can use for scripting or manually (with `:lua MiniClue.*`).
---
--- Config table **needs to have triggers configured**, none is set up by default.
---
--- See |MiniClue.config| for available config settings.
---
--- You can override runtime config settings (like clues or window options)
--- locally to a buffer inside `vim.b.miniclue_config` which should have same
--- structure as `MiniClue.config`. See |mini.nvim-buffer-local-config| for
--- more details.
---
--- # Comparisons ~
---
--- - 'folke/which-key.nvim':
--- - Both have the same main goal: show available next keys along with
--- their customizable descriptions.
--- - Has different UI and content layout.
--- - Allows creating mappings inside its configuration, while this module
--- doesn't have this by design (to clearly separate two different tasks).
--- - Doesn't allow creating submodes, while this module does (via `postkeys`).
---
--- - 'anuvyklack/hydra.nvim':
--- - Both allow creating submodes: state which starts at certain key
--- combination; treats some keys differently; ends after `<Esc>`.
--- - Doesn't show information about available next keys (outside of
--- submodes), while that is this module's main goal.
---
--- # Highlight groups ~
---
--- * `MiniClueBorder` - window border.
--- * `MiniClueDescGroup` - group description in clue window.
--- * `MiniClueDescSingle` - single target description in clue window.
--- * `MiniClueNextKey` - next key label in clue window.
--- * `MiniClueNextKeyWithPostkeys` - next key label with postkeys in clue window.
--- * `MiniClueSeparator` - separator in clue window.
--- * `MiniClueTitle` - window title.
---
--- To change any highlight group, modify it directly with |:highlight|.
---
--- # Disabling ~
---
--- To disable creating triggers, set `vim.g.miniclue_disable` (globally) or
--- `vim.b.miniclue_disable` (for a buffer) to `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.
--- # Key query process ~
---
--- ## General info ~
---
--- This module implements custom key query process imitating a usual built-in
--- mechanism of user pressing keys in order to execute a mapping. General idea
--- is the same: narrow down key combinations until the target is reached.
---
--- Main goals of its existence are:
---
--- - Allow reaching certain mappings be independent of 'timeoutlen'. That is,
--- there is no fixed timeout after which currently typed keys are executed.
---
--- - Enable automated showing of next key clues after user-supplied delay
--- (also independent of 'timeoutlen').
---
--- - Allow emulating configurable key presses after certain key combination is
--- reached. This granular control allows creating so called "submodes".
--- See more at |MiniClue-examples-submodes|.
---
--- This process is primarily designed for nested `<Leader>` mappings in Normal
--- mode but works in all other main modes: Visual, Insert, Operator-pending
--- (with caveats; no full proof guarantees), Command-line, Terminal.
---
--- ## Lifecycle ~
---
--- - Key query process starts when user types a trigger: certain keys in certain
--- mode. Those keys are put into key query as a single user input. All possible
--- mode key combinations are filtered to ones starting with the trigger keys.
---
--- Note: trigger is implemented as a regular mapping, so if it has at least
--- two keys, they should be pressed within 'timeoutlen' milliseconds.
---
--- - Wait (indefinitely) for user to press a key. Advance depending on the key:
---
--- - Special key:
---
--- - If `<Esc>` or `<C-c>`, stop the process without any action.
---
--- - If `<CR>`, stop the process and execute current key query, meaning
--- emulate (with |nvim_feedkeys()|) user pressing those keys.
---
--- - If `<BS>`, remove previous user input from the query. If query becomes
--- empty, stop the process without any action.
---
--- - If a key for scrolling clue window (`scroll_down` / `scroll_up`
--- in `config.window`; `<C-d>` / `<C-u>` by default), scroll clue window
--- and wait for the next user key.
--- Note: if clue window is not shown, treated as a not special key.
---
--- - Not special key. Add key to the query while filtering all available
--- key combinations to start with the current key query. Advance:
---
--- - If there is a single available key combination matching current
--- key query, execute it.
---
--- - If there is no key combinations starting with the current query,
--- execute it. This, for instance, allows a seamless execution of
--- operators in presence of a longer key combinations. Example: with
--- `g` as trigger in Normal mode and available mappings `gc` / `gcc`
--- (like from |mini.comment|), this allows typing `gcip` to comment
--- current paragraph, although there are no key combinations
--- starting with `gci`.
---
--- - Otherwise wait for the new user key press.
---
--- ## Clue window ~
---
--- After initiating key query process and after each key press, a timer is
--- started to show a clue window: floating window with information about
--- available next keys along with their descriptions. Note: if window is
--- already shown, its content is updated right away.
---
--- Clues can have these types:
---
--- - "Terminal next key": when pressed, will lead to query execution.
---
--- - "Terminal next key with postkeys": when pressed, will lead to query
--- execution plus some configured postkeys.
---
--- - "Group next key": when pressed, will narrow down available key combinations
--- and wait for another key press. Note: can have configured description
--- (inside `config.clues`) or it will be auto generated based on the number of
--- available key combinations.
---@tag MiniClue-key-query-process
--- # Full starter example ~
---
--- If not sure where to start, try this example with all provided clues from
--- this module plus all |<Leader>| mappings in Normal and Visual modes: >
---
--- local miniclue = require('mini.clue')
--- miniclue.setup({
--- triggers = {
--- -- Leader triggers
--- { mode = 'n', keys = '<Leader>' },
--- { mode = 'x', keys = '<Leader>' },
---
--- -- Built-in completion
--- { mode = 'i', keys = '<C-x>' },
---
--- -- `g` key
--- { mode = 'n', keys = 'g' },
--- { mode = 'x', keys = 'g' },
---
--- -- Marks
--- { mode = 'n', keys = "'" },
--- { mode = 'n', keys = '`' },
--- { mode = 'x', keys = "'" },
--- { mode = 'x', keys = '`' },
---
--- -- Registers
--- { mode = 'n', keys = '"' },
--- { mode = 'x', keys = '"' },
--- { mode = 'i', keys = '<C-r>' },
--- { mode = 'c', keys = '<C-r>' },
---
--- -- Window commands
--- { mode = 'n', keys = '<C-w>' },
---
--- -- `z` key
--- { mode = 'n', keys = 'z' },
--- { mode = 'x', keys = 'z' },
--- },
---
--- clues = {
--- -- Enhance this by adding descriptions for <Leader> mapping groups
--- miniclue.gen_clues.builtin_completion(),
--- miniclue.gen_clues.g(),
--- miniclue.gen_clues.marks(),
--- miniclue.gen_clues.registers(),
--- miniclue.gen_clues.windows(),
--- miniclue.gen_clues.z(),
--- },
--- })
---
--- # Leader clues ~
---
--- Assume there are these |<Leader>| mappings set up: >
---
--- -- Set `<Leader>` before making any mappings and configuring 'mini.clue'
--- vim.g.mapleader = ' '
---
--- local nmap_leader = function(suffix, rhs, desc)
--- vim.keymap.set('n', '<Leader>' .. suffix, rhs, { desc = desc })
--- end
--- local xmap_leader = function(suffix, rhs, desc)
--- vim.keymap.set('x', '<Leader>' .. suffix, rhs, { desc = desc })
--- end
---
--- nmap_leader('bd', '<Cmd>lua MiniBufremove.delete()<CR>', 'Delete')
--- nmap_leader('bw', '<Cmd>lua MiniBufremove.wipeout()<CR>', 'Wipeout')
---
--- nmap_leader('lf', '<Cmd>lua vim.lsp.buf.format()<CR>', 'Format')
--- xmap_leader('lf', '<Cmd>lua vim.lsp.buf.format()<CR>', 'Format')
--- nmap_leader('lr', '<Cmd>lua vim.lsp.buf.rename()<CR>', 'Rename')
--- nmap_leader('lR', '<Cmd>lua vim.lsp.buf.references()<CR>', 'References')
---
---
--- The following setup will enable |<Leader>| as trigger in Normal and Visual
--- modes and add descriptions to mapping groups: >
---
--- require('mini.clue').setup({
--- -- Register `<Leader>` as trigger
--- triggers = {
--- { mode = 'n', keys = '<Leader>' },
--- { mode = 'x', keys = '<Leader>' },
--- },
---
--- -- Add descriptions for mapping groups
--- clues = {
--- { mode = 'n', keys = '<Leader>b', desc = '+Buffers' },
--- { mode = 'n', keys = '<Leader>l', desc = '+LSP' },
--- },
--- })
---
--- # Clues without mappings ~
---
--- Clues can be shown not only for actually present mappings. This is helpful for
--- showing clues for built-in key combinations. Here is an example of clues for
--- a subset of built-in completion (see |MiniClue.gen_clues.builtin_completion()|
--- to generate clues for all available completion sources): >
---
--- require('mini.clue').setup({
--- -- Make `<C-x>` a trigger. Otherwise, key query process won't start.
--- triggers = {
--- { mode = 'i', keys = '<C-x>' },
--- },
---
--- -- Register custom clues
--- clues = {
--- { mode = 'i', keys = '<C-x><C-f>', desc = 'File names' },
--- { mode = 'i', keys = '<C-x><C-l>', desc = 'Whole lines' },
--- { mode = 'i', keys = '<C-x><C-o>', desc = 'Omni completion' },
--- { mode = 'i', keys = '<C-x><C-s>', desc = 'Spelling suggestions' },
--- { mode = 'i', keys = '<C-x><C-u>', desc = "With 'completefunc'" },
--- }
--- })
--- <
--- *MiniClue-examples-submodes*
--- # Submodes ~
---
--- Submode is a state initiated after pressing certain key combination ("prefix")
--- during which some keys are interpreted differently.
---
--- In this module submode can be implemented following these steps:
---
--- - Create mappings for each key inside submode. Left hand side of mappings
--- should consist from prefix followed by the key.
---
--- - Create clue for each key inside submode with `postkeys` value equal to
--- prefix. It would mean that after executing particular key combination from
--- this submode, pressing its prefix will be automatically emulated (leading
--- back to being inside submode).
---
--- - Register submode prefix (or some of its starting part) as trigger.
---
--- ## Submode examples ~
---
--- - Submode for moving with |mini.move|:
--- - Press `<Leader>m` to start submode.
--- - Press any of `h`/`j`/`k`/`l` to move selection/line.
--- - Press `<Esc>` to stop submode.
---
--- The code: >
---
--- require('mini.move').setup({
--- mappings = {
--- left = '<Leader>mh',
--- right = '<Leader>ml',
--- down = '<Leader>mj',
--- up = '<Leader>mk',
--- line_left = '<Leader>mh',
--- line_right = '<Leader>ml',
--- line_down = '<Leader>mj',
--- line_up = '<Leader>mk',
--- },
--- })
---
--- require('mini.clue').setup({
--- triggers = {
--- { mode = 'n', keys = '<Leader>m' },
--- { mode = 'x', keys = '<Leader>m' },
--- },
--- clues = {
--- { mode = 'n', keys = '<Leader>mh', postkeys = '<Leader>m' },
--- { mode = 'n', keys = '<Leader>mj', postkeys = '<Leader>m' },
--- { mode = 'n', keys = '<Leader>mk', postkeys = '<Leader>m' },
--- { mode = 'n', keys = '<Leader>ml', postkeys = '<Leader>m' },
--- { mode = 'x', keys = '<Leader>mh', postkeys = '<Leader>m' },
--- { mode = 'x', keys = '<Leader>mj', postkeys = '<Leader>m' },
--- { mode = 'x', keys = '<Leader>mk', postkeys = '<Leader>m' },
--- { mode = 'x', keys = '<Leader>ml', postkeys = '<Leader>m' },
--- },
--- })
---
--- - Submode for iterating buffers and windows with |mini.bracketed|:
--- - Press `[` or `]` to start key query process for certain direction.
--- - Press `b` / `w` to iterate buffers/windows until reach target one.
--- - Press `<Esc>` to stop submode.
---
--- The code: >
---
--- require('mini.bracketed').setup()
---
--- require('mini.clue').setup({
--- triggers = {
--- { mode = 'n', keys = ']' },
--- { mode = 'n', keys = '[' },
--- },
--- clues = {
--- { mode = 'n', keys = ']b', postkeys = ']' },
--- { mode = 'n', keys = ']w', postkeys = ']' },
---
--- { mode = 'n', keys = '[b', postkeys = '[' },
--- { mode = 'n', keys = '[w', postkeys = '[' },
--- },
--- })
---
--- - Submode for window commands using |MiniClue.gen_clues.windows()|:
--- - Press `<C-w>` to start key query process.
--- - Press keys which move / change focus / resize windows.
--- - Press `<Esc>` to stop submode.
---
--- The code: >
---
--- local miniclue = require('mini.clue')
--- miniclue.setup({
--- triggers = {
--- { mode = 'n', keys = '<C-w>' },
--- },
--- clues = {
--- miniclue.gen_clues.windows({
--- submode_move = true,
--- submode_navigate = true,
--- submode_resize = true,
--- })
--- },
--- })
---
--- # Window config ~
--- >
--- require('mini.clue').setup({
--- triggers = { { mode = 'n', keys = '<Leader>' } },
---
--- window = {
--- -- Show window immediately
--- delay = 0,
---
--- config = {
--- -- Compute window width automatically
--- width = 'auto',
---
--- -- Use double-line border
--- border = 'double',
--- },
--- },
--- })
---@tag MiniClue-examples
---@diagnostic disable:undefined-field
---@diagnostic disable:discard-returns
---@diagnostic disable:unused-local
---@diagnostic disable:cast-local-type
-- Module definition ==========================================================
local MiniClue = {}
local H = {}
--- Module setup
---
---@param config table|nil Module config table. See |MiniClue.config|.
---
---@usage `require('mini.clue').setup({})` (replace `{}` with your `config` table).
--- **Needs to have triggers configured**.
MiniClue.setup = function(config)
-- Export module
_G.MiniClue = MiniClue
-- Setup config
config = H.setup_config(config)
-- Apply config
H.apply_config(config)
-- Define behavior
H.create_autocommands(config)
-- Create default highlighting
H.create_default_hl()
end
--stylua: ignore
--- Module config
---
--- Default values:
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
---@text # General info ~
---
--- - To use |<Leader>| as part of the config (either as trigger or inside clues),
--- set it prior to running |MiniClue.setup()|.
---
--- - See |MiniClue-examples| for examples.
---
--- # Clues ~
---
--- `config.clues` is an array with extra information about key combinations.
--- Each element can be one of:
--- - Clue table.
--- - Array (possibly nested) of clue tables.
--- - Callable (function) returning either of the previous two.
---
--- A clue table is a table with the following fields:
--- - <mode> `(string)` - single character describing **single** mode short-name of
--- key combination as in `nvim_set_keymap()` ('n', 'x', 'i', 'o', 'c', etc.).
--- - <keys> `(string)` - key combination for which clue will be shown.
--- "Human-readable" key names as in |key-notation| (like "<Leader>", "<Space>",
--- "<Tab>", etc.) are allowed.
--- - <desc> `(string|nil)` - optional key combination description which will
--- be shown in clue window.
--- - <postkeys> `(string|nil)` - optional postkeys which will be executed
--- automatically after `keys`. Allows creation of submodes
--- (see |MiniClue-examples-submodes|).
---
--- Notes:
--- - Postkeys are literal simulation of keypresses with |nvim_feedkeys()|.
---
--- - Suggested approach to configuring clues is to create mappings with `desc`
--- field while supplying to `config.clues` only elements describing groups,
--- postkeys, and built-in mappings.
---
--- # Triggers ~
---
--- `config.triggers` is an array with information when |MiniClue-key-query-process|
--- should start. Each element is a trigger table with the fields <mode> and
--- <keys> which are treated the same as in clue table.
---
--- # Window ~
---
--- `config.window` defines behavior of clue window.
---
--- `config.window.delay` is a number of milliseconds after which clue window will
--- appear. Can be 0 to show immediately.
---
--- `config.window.config` is a table defining floating window characteristics
--- or a callable returning such table (will be called with identifier of
--- window's buffer already showing all clues). It should have the same
--- structure as in |nvim_open_win()| with the following enhancements:
--- - <width> field can be equal to `"auto"` leading to window width being
--- computed automatically based on its content. Default is fixed width of 30.
--- - <row> and <col> can be equal to `"auto"` in which case they will be
--- computed to "stick" to set anchor ("SE" by default; see |nvim_open_win()|).
--- This allows changing corner in which window is shown: >
---
--- -- Pick one anchor
--- local anchor = 'NW' -- top-left
--- local anchor = 'NE' -- top-right
--- local anchor = 'SW' -- bottom-left
--- local anchor = 'SE' -- bottom-right
---
--- require('mini.clue').setup({
--- window = {
--- config = { anchor = anchor, row = 'auto', col = 'auto' },
--- },
--- })
---
--- `config.window.scroll_down` / `config.window.scroll_up` are strings defining
--- keys which will scroll clue window down / up which is useful in case not
--- all clues fit in current window height. Set to empty string `''` to disable
--- either of them.
MiniClue.config = {
-- Array of extra clues to show
clues = {},
-- Array of opt-in triggers which start custom key query process.
-- **Needs to have something in order to show clues**.
triggers = {},
-- Clue window settings
window = {
-- Floating window config
config = {},
-- Delay before showing clue window
delay = 1000,
-- Keys to scroll inside the clue window
scroll_down = '<C-d>',
scroll_up = '<C-u>',
},
}
--minidoc_afterlines_end
--- Enable triggers in all listed buffers
MiniClue.enable_all_triggers = function()
for _, buf_id in ipairs(vim.api.nvim_list_bufs()) do
-- Map only inside valid listed buffers
if vim.fn.buflisted(buf_id) == 1 then H.map_buf_triggers(buf_id) end
end
end
--- Enable triggers in buffer
---
---@param buf_id number|nil Buffer identifier. Default: current buffer.
MiniClue.enable_buf_triggers = function(buf_id)
buf_id = (buf_id == nil or buf_id == 0) and vim.api.nvim_get_current_buf() or buf_id
if not H.is_valid_buf(buf_id) then H.error('`buf_id` should be a valid buffer identifier.') end
H.map_buf_triggers(buf_id)
end
--- Disable triggers in all buffers
MiniClue.disable_all_triggers = function()
for _, buf_id in ipairs(vim.api.nvim_list_bufs()) do
H.unmap_buf_triggers(buf_id)
end
end
--- Disable triggers in buffer
---
---@param buf_id number|nil Buffer identifier. Default: current buffer.
MiniClue.disable_buf_triggers = function(buf_id)
buf_id = (buf_id == nil or buf_id == 0) and vim.api.nvim_get_current_buf() or buf_id
if not H.is_valid_buf(buf_id) then H.error('`buf_id` should be a valid buffer identifier.') end
H.unmap_buf_triggers(buf_id)
end
--- Ensure all triggers are valid
MiniClue.ensure_all_triggers = function()
MiniClue.disable_all_triggers()
MiniClue.enable_all_triggers()
end
--- Ensure buffer triggers are valid
---
---@param buf_id number|nil Buffer identifier. Default: current buffer.
MiniClue.ensure_buf_triggers = function(buf_id)
MiniClue.disable_buf_triggers(buf_id)
MiniClue.enable_buf_triggers(buf_id)
end
--- Update description of an existing mapping
---
--- Notes:
--- - Uses buffer-local mapping in case there are both global and buffer-local
--- mappings with same mode and LHS. Similar to |maparg()|.
--- - Requires Neovim>=0.8.
---
---@param mode string Mapping mode (as in `maparg()`).
---@param lhs string Mapping left hand side (as `name` in `maparg()`).
---@param desc string New description to set.
MiniClue.set_mapping_desc = function(mode, lhs, desc)
if vim.fn.has('nvim-0.8') == 0 then H.error('`set_mapping_desc()` requires Neovim>=0.8.') end
if type(mode) ~= 'string' then H.error('`mode` should be string.') end
if type(lhs) ~= 'string' then H.error('`lhs` should be string.') end
if type(desc) ~= 'string' then H.error('`desc` should be string.') end
local ok_get, map_data = pcall(vim.fn.maparg, lhs, mode, false, true)
if not ok_get or vim.tbl_count(map_data) == 0 then
local msg = string.format('No mapping found for mode %s and LHS %s.', vim.inspect(mode), vim.inspect(lhs))
H.error(msg)
end
map_data.desc = desc
local ok_set = pcall(vim.fn.mapset, mode, false, map_data)
if not ok_set then H.error(vim.inspect(desc) .. ' is not a valid description.') end
end
--- Generate pre-configured clues
---
--- This is a table with function elements. Call to actually get array of clues.
MiniClue.gen_clues = {}
--- Generate clues for built-in completion
---
--- Contains clues for the following triggers: >
---
--- { mode = 'i', keys = '<C-x>' }
---
---@return table Array of clues.
MiniClue.gen_clues.builtin_completion = function()
--stylua: ignore
return {
{ mode = 'i', keys = '<C-x><C-d>', desc = 'Defined identifiers' },
{ mode = 'i', keys = '<C-x><C-e>', desc = 'Scroll up' },
{ mode = 'i', keys = '<C-x><C-f>', desc = 'File names' },
{ mode = 'i', keys = '<C-x><C-i>', desc = 'Identifiers' },
{ mode = 'i', keys = '<C-x><C-k>', desc = 'Identifiers from dictionary' },
{ mode = 'i', keys = '<C-x><C-l>', desc = 'Whole lines' },
{ mode = 'i', keys = '<C-x><C-n>', desc = 'Next completion' },
{ mode = 'i', keys = '<C-x><C-o>', desc = 'Omni completion' },
{ mode = 'i', keys = '<C-x><C-p>', desc = 'Previous completion' },
{ mode = 'i', keys = '<C-x><C-s>', desc = 'Spelling suggestions' },
{ mode = 'i', keys = '<C-x><C-t>', desc = 'Identifiers from thesaurus' },
{ mode = 'i', keys = '<C-x><C-y>', desc = 'Scroll down' },
{ mode = 'i', keys = '<C-x><C-u>', desc = "With 'completefunc'" },
{ mode = 'i', keys = '<C-x><C-v>', desc = 'Like in command line' },
{ mode = 'i', keys = '<C-x><C-z>', desc = 'Stop completion' },
{ mode = 'i', keys = '<C-x><C-]>', desc = 'Tags' },
{ mode = 'i', keys = '<C-x>s', desc = 'Spelling suggestions' },
}
end
--- Generate clues for `g` key
---
--- Contains clues for the following triggers: >
---
--- { mode = 'n', keys = 'g' }
--- { mode = 'x', keys = 'g' }
---
---@return table Array of clues.
MiniClue.gen_clues.g = function()
--stylua: ignore
return {
{ mode = 'n', keys = 'g0', desc = 'Go to leftmost visible column' },
{ mode = 'n', keys = 'g8', desc = 'Print hex value of char under cursor' },
{ mode = 'n', keys = 'ga', desc = 'Print ascii value' },
{ mode = 'n', keys = 'gD', desc = 'Go to definition in file' },
{ mode = 'n', keys = 'gd', desc = 'Go to definition in function' },
{ mode = 'n', keys = 'gE', desc = 'Go backwards to end of previous WORD' },
{ mode = 'n', keys = 'ge', desc = 'Go backwards to end of previous word' },
{ mode = 'n', keys = 'gF', desc = 'Edit file under cursor + jump line' },
{ mode = 'n', keys = 'gf', desc = 'Edit file under cursor' },
{ mode = 'n', keys = 'gg', desc = 'Go to line (def: first)' },
{ mode = 'n', keys = 'gH', desc = 'Start Select line mode' },
{ mode = 'n', keys = 'gh', desc = 'Start Select mode' },
{ mode = 'n', keys = 'gI', desc = 'Start Insert at column 1' },
{ mode = 'n', keys = 'gi', desc = 'Start Insert where it stopped' },
{ mode = 'n', keys = 'gJ', desc = 'Join lines without extra spaces' },
{ mode = 'n', keys = 'gj', desc = 'Go down by screen lines' },
{ mode = 'n', keys = 'gk', desc = 'Go up by screen lines' },
{ mode = 'n', keys = 'gM', desc = 'Go to middle of text line' },
{ mode = 'n', keys = 'gm', desc = 'Go to middle of screen line' },
{ mode = 'n', keys = 'gN', desc = 'Select previous search match' },
{ mode = 'n', keys = 'gn', desc = 'Select next search match' },
{ mode = 'n', keys = 'go', desc = 'Go to byte' },
{ mode = 'n', keys = 'gP', desc = 'Put text before cursor + stay after it' },
{ mode = 'n', keys = 'gp', desc = 'Put text after cursor + stay after it' },
{ mode = 'n', keys = 'gQ', desc = 'Switch to "Ex" mode' },
{ mode = 'n', keys = 'gq', desc = 'Format text (operator)' },
{ mode = 'n', keys = 'gR', desc = 'Enter Virtual Replace mode' },
{ mode = 'n', keys = 'gr', desc = 'Virtual replace with character' },
{ mode = 'n', keys = 'gs', desc = 'Sleep' },
{ mode = 'n', keys = 'gT', desc = 'Go to previous tabpage' },
{ mode = 'n', keys = 'gt', desc = 'Go to next tabpage' },
{ mode = 'n', keys = 'gU', desc = 'Make uppercase (operator)' },
{ mode = 'n', keys = 'gu', desc = 'Make lowercase (operator)' },
{ mode = 'n', keys = 'gV', desc = 'Avoid reselect' },
{ mode = 'n', keys = 'gv', desc = 'Reselect previous Visual area' },
{ mode = 'n', keys = 'gw', desc = 'Format text + keep cursor (operator)' },
{ mode = 'n', keys = 'gx', desc = 'Execute app for file under cursor' },
{ mode = 'n', keys = 'g<C-]>', desc = '`:tjump` to tag under cursor' },
{ mode = 'n', keys = 'g<C-a>', desc = 'Dump a memory profile' },
{ mode = 'n', keys = 'g<C-g>', desc = 'Show information about cursor' },
{ mode = 'n', keys = 'g<C-h>', desc = 'Start Select block mode' },
{ mode = 'n', keys = 'g<Tab>', desc = 'Go to last accessed tabpage' },
{ mode = 'n', keys = "g'", desc = "Jump to mark (don't affect jumplist)" },
{ mode = 'n', keys = 'g#', desc = 'Search backwards word under cursor' },
{ mode = 'n', keys = 'g$', desc = 'Go to rightmost visible column' },
{ mode = 'n', keys = 'g%', desc = 'Cycle through matching groups' },
{ mode = 'n', keys = 'g&', desc = 'Repeat last `:s` on all lines' },
{ mode = 'n', keys = 'g*', desc = 'Search word under cursor' },
{ mode = 'n', keys = 'g+', desc = 'Go to newer text state' },
{ mode = 'n', keys = 'g,', desc = 'Go to newer position in change list' },
{ mode = 'n', keys = 'g-', desc = 'Go to older text state' },
{ mode = 'n', keys = 'g;', desc = 'Go to older position in change list' },
{ mode = 'n', keys = 'g<', desc = 'Display previous command output' },
{ mode = 'n', keys = 'g?', desc = 'Rot13 encode (operator)' },
{ mode = 'n', keys = 'g@', desc = "Call 'operatorfunc' (operator)" },
{ mode = 'n', keys = 'g]', desc = '`:tselect` tag under cursor' },
{ mode = 'n', keys = 'g^', desc = 'Go to leftmost visible non-whitespace' },
{ mode = 'n', keys = 'g_', desc = 'Go to lower line' },
{ mode = 'n', keys = 'g`', desc = "Jump to mark (don't affect jumplist)" },
{ mode = 'n', keys = 'g~', desc = 'Swap case (operator)' },
{ mode = 'x', keys = 'gf', desc = 'Edit selected file' },
{ mode = 'x', keys = 'gJ', desc = 'Join selected lines without extra spaces' },
{ mode = 'x', keys = 'gq', desc = 'Format selection' },
{ mode = 'x', keys = 'gV', desc = 'Avoid reselect' },
{ mode = 'x', keys = 'gw', desc = 'Format selection + keep cursor' },
{ mode = 'x', keys = 'g<C-]>', desc = '`:tjump` to selected tag' },
{ mode = 'x', keys = 'g<C-a>', desc = 'Increment with compound' },
{ mode = 'x', keys = 'g<C-g>', desc = 'Show information about selection' },
{ mode = 'x', keys = 'g<C-x>', desc = 'Decrement with compound' },
{ mode = 'x', keys = 'g]', desc = '`:tselect` selected tag' },
{ mode = 'x', keys = 'g?', desc = 'Rot13 encode selection' },
}
end
--- Generate clues for marks
---
--- Contains clues for the following triggers: >
---
--- { mode = 'n', keys = "'" }
--- { mode = 'n', keys = "g'" }
--- { mode = 'n', keys = '`' }
--- { mode = 'n', keys = 'g`' }
--- { mode = 'x', keys = "'" }
--- { mode = 'x', keys = "g'" }
--- { mode = 'x', keys = '`' }
--- { mode = 'x', keys = 'g`' }
---
--- Note: if you use "g" as trigger (like to enable |MiniClue.gen_clues.g()|),
--- don't add "g'" and "g`" as triggers: they already will be taken into account.
---
---@return table Array of clues.
---
---@seealso |mark-motions|
MiniClue.gen_clues.marks = function()
local describe_marks = function(mode, prefix)
local make_clue = function(register, desc) return { mode = mode, keys = prefix .. register, desc = desc } end
return {
make_clue('^', 'Latest insert position'),
make_clue('.', 'Latest change'),
make_clue('"', 'Latest exited position'),
make_clue("'", 'Line before jump'),
make_clue('`', 'Position before jump'),
make_clue('[', 'Start of latest changed or yanked text'),
make_clue(']', 'End of latest changed or yanked text'),
make_clue('(', 'Start of sentence'),
make_clue(')', 'End of sentence'),
make_clue('{', 'Start of paragraph'),
make_clue('}', 'End of paragraph'),
make_clue('<', 'Start of latest visual selection'),
make_clue('>', 'End of latest visual selection'),
}
end
--stylua: ignore
return {
-- Normal mode
describe_marks('n', "'"),
describe_marks('n', "g'"),
describe_marks('n', "`"),
describe_marks('n', "g`"),
-- Visual mode
describe_marks('x', "'"),
describe_marks('x', "g'"),
describe_marks('x', "`"),
describe_marks('x', "g`"),
}
end
--- Generate clues for registers
---
--- Contains clues for the following triggers: >
---
--- { mode = 'n', keys = '"' }
--- { mode = 'x', keys = '"' }
--- { mode = 'i', keys = '<C-r>' }
--- { mode = 'c', keys = '<C-r>' }
---
---@param opts table|nil Options. Possible keys:
--- - <show_contents> `(boolean)` - whether to show contents of all possible
--- registers. If `false`, only description of special registers is shown.
--- Default: `false`.
---
---@return table Array of clues.
---
---@seealso |registers|
MiniClue.gen_clues.registers = function(opts)
opts = vim.tbl_deep_extend('force', { show_contents = false }, opts or {})
local describe_registers
if opts.show_contents then
describe_registers = H.make_clues_with_register_contents
else
describe_registers = function(mode, prefix)
local make_clue = function(register, desc) return { mode = mode, keys = prefix .. register, desc = desc } end
return {
make_clue('0', 'Latest yank'),
make_clue('1', 'Latest big delete'),
make_clue('"', 'Default register'),
make_clue('#', 'Alternate buffer'),
make_clue('%', 'Name of the current file'),
make_clue('*', 'Selection clipboard'),
make_clue('+', 'System clipboard'),
make_clue('-', 'Latest small delete'),
make_clue('.', 'Latest inserted text'),
make_clue('/', 'Latest search pattern'),
make_clue(':', 'Latest executed command'),
make_clue('=', 'Result of expression'),
make_clue('_', 'Black hole'),
}
end
end
--stylua: ignore
return {
-- Normal mode
describe_registers('n', '"'),
-- Visual mode
describe_registers('x', '"'),
-- Insert mode
describe_registers('i', '<C-r>'),
{ mode = 'i', keys = '<C-r><C-r>', desc = '+Insert literally' },
describe_registers('i', '<C-r><C-r>'),
{ mode = 'i', keys = '<C-r><C-o>', desc = '+Insert literally + not auto-indent' },
describe_registers('i', '<C-r><C-o>'),
{ mode = 'i', keys = '<C-r><C-p>', desc = '+Insert + fix indent' },
describe_registers('i', '<C-r><C-p>'),
-- Command-line mode
describe_registers('c', '<C-r>'),
{ mode = 'c', keys = '<C-r><C-r>', desc = '+Insert literally' },
describe_registers('c', '<C-r><C-r>'),
{ mode = 'c', keys = '<C-r><C-o>', desc = '+Insert literally' },
describe_registers('c', '<C-r><C-o>'),
}
end
--- Generate clues for window commands
---
--- Contains clues for the following triggers: >
---
--- { mode = 'n', keys = '<C-w>' }
---
--- Note: only non-duplicated commands are included. For full list see |CTRL-W|.
---
---@param opts table|nil Options. Possible keys:
--- - <submode_move> `(boolean)` - whether to make move (change layout)
--- commands a submode by using `postkeys` field. Default: `false`.
--- - <submode_navigate> `(boolean)` - whether to make navigation (change
--- focus) commands a submode by using `postkeys` field. Default: `false`.
--- - <submode_resize> `(boolean)` - whether to make resize (change size)
--- commands a submode by using `postkeys` field. Default: `false`.
---
---@return table Array of clues.
MiniClue.gen_clues.windows = function(opts)
local default_opts = { submode_navigate = false, submode_move = false, submode_resize = false }
opts = vim.tbl_deep_extend('force', default_opts, opts or {})
local postkeys_move, postkeys_navigate, postkeys_resize = nil, nil, nil
if opts.submode_move then postkeys_move = '<C-w>' end
if opts.submode_navigate then postkeys_navigate = '<C-w>' end
if opts.submode_resize then postkeys_resize = '<C-w>' end
--stylua: ignore
return {
{ mode = 'n', keys = '<C-w>+', desc = 'Increase height', postkeys = postkeys_resize },
{ mode = 'n', keys = '<C-w>-', desc = 'Decrease height', postkeys = postkeys_resize },
{ mode = 'n', keys = '<C-w><', desc = 'Decrease width', postkeys = postkeys_resize },
{ mode = 'n', keys = '<C-w>>', desc = 'Increase width', postkeys = postkeys_resize },
{ mode = 'n', keys = '<C-w>=', desc = 'Make windows same dimensions' },
{ mode = 'n', keys = '<C-w>]', desc = 'Split + jump to tag' },
{ mode = 'n', keys = '<C-w>^', desc = 'Split + edit alternate file' },
{ mode = 'n', keys = '<C-w>_', desc = 'Set height (def: very high)' },
{ mode = 'n', keys = '<C-w>|', desc = 'Set width (def: very wide)' },
{ mode = 'n', keys = '<C-w>}', desc = 'Show tag in preview' },
{ mode = 'n', keys = '<C-w>b', desc = 'Focus bottom', postkeys = postkeys_navigate },
{ mode = 'n', keys = '<C-w>c', desc = 'Close' },
{ mode = 'n', keys = '<C-w>d', desc = 'Split + jump to definition' },
{ mode = 'n', keys = '<C-w>F', desc = 'Split + edit file name + jump' },
{ mode = 'n', keys = '<C-w>f', desc = 'Split + edit file name' },
{ mode = 'n', keys = '<C-w>g', desc = '+Extra actions' },
{ mode = 'n', keys = '<C-w>g]', desc = 'Split + list tags' },
{ mode = 'n', keys = '<C-w>g}', desc = 'Do `:ptjump`' },
{ mode = 'n', keys = '<C-w>g<C-]>', desc = 'Split + jump to tag with `:tjump`' },
{ mode = 'n', keys = '<C-w>g<Tab>', desc = 'Focus last accessed tab', postkeys = postkeys_navigate },
{ mode = 'n', keys = '<C-w>gF', desc = 'New tabpage + edit file name + jump' },
{ mode = 'n', keys = '<C-w>gf', desc = 'New tabpage + edit file name' },
{ mode = 'n', keys = '<C-w>gT', desc = 'Focus previous tabpage', postkeys = postkeys_navigate },
{ mode = 'n', keys = '<C-w>gt', desc = 'Focus next tabpage', postkeys = postkeys_navigate },
{ mode = 'n', keys = '<C-w>H', desc = 'Move to very left', postkeys = postkeys_move },
{ mode = 'n', keys = '<C-w>h', desc = 'Focus left', postkeys = postkeys_navigate },
{ mode = 'n', keys = '<C-w>i', desc = 'Split + jump to declaration' },
{ mode = 'n', keys = '<C-w>J', desc = 'Move to very bottom', postkeys = postkeys_move },
{ mode = 'n', keys = '<C-w>j', desc = 'Focus down', postkeys = postkeys_navigate },
{ mode = 'n', keys = '<C-w>K', desc = 'Move to very top', postkeys = postkeys_move },
{ mode = 'n', keys = '<C-w>k', desc = 'Focus up', postkeys = postkeys_navigate },
{ mode = 'n', keys = '<C-w>L', desc = 'Move to very right', postkeys = postkeys_move },
{ mode = 'n', keys = '<C-w>l', desc = 'Focus right', postkeys = postkeys_navigate },
{ mode = 'n', keys = '<C-w>n', desc = 'Open new' },
{ mode = 'n', keys = '<C-w>o', desc = 'Close all but current' },
{ mode = 'n', keys = '<C-w>P', desc = 'Focus preview', postkeys = postkeys_navigate },
{ mode = 'n', keys = '<C-w>p', desc = 'Focus last accessed', postkeys = postkeys_navigate },
{ mode = 'n', keys = '<C-w>q', desc = 'Quit current' },
{ mode = 'n', keys = '<C-w>R', desc = 'Rotate up/left', postkeys = postkeys_move },
{ mode = 'n', keys = '<C-w>r', desc = 'Rotate down/right', postkeys = postkeys_move },
{ mode = 'n', keys = '<C-w>s', desc = 'Split horizontally' },
{ mode = 'n', keys = '<C-w>T', desc = 'Create new tabpage + move' },
{ mode = 'n', keys = '<C-w>t', desc = 'Focus top', postkeys = postkeys_navigate },
{ mode = 'n', keys = '<C-w>v', desc = 'Split vertically' },
{ mode = 'n', keys = '<C-w>W', desc = 'Focus previous', postkeys = postkeys_navigate },
{ mode = 'n', keys = '<C-w>w', desc = 'Focus next', postkeys = postkeys_navigate },
{ mode = 'n', keys = '<C-w>x', desc = 'Exchange windows', postkeys = postkeys_move },
{ mode = 'n', keys = '<C-w>z', desc = 'Close preview' },
}
end
--- Generate clues for `z` key
---
--- Contains clues for the following triggers: >
---
--- { mode = 'n', keys = 'z' }
--- { mode = 'x', keys = 'z' }
---
---@return table Array of clues.
MiniClue.gen_clues.z = function()
--stylua: ignore
return {
{ mode = 'n', keys = 'zA', desc = 'Toggle folds recursively' },
{ mode = 'n', keys = 'za', desc = 'Toggle fold' },
{ mode = 'n', keys = 'zb', desc = 'Redraw at bottom' },
{ mode = 'n', keys = 'zC', desc = 'Close folds recursively' },
{ mode = 'n', keys = 'zc', desc = 'Close fold' },
{ mode = 'n', keys = 'zD', desc = 'Delete folds recursively' },
{ mode = 'n', keys = 'zd', desc = 'Delete fold' },
{ mode = 'n', keys = 'zE', desc = 'Eliminate all folds' },
{ mode = 'n', keys = 'ze', desc = 'Scroll to cursor on right screen side' },
{ mode = 'n', keys = 'zF', desc = 'Create fold' },
{ mode = 'n', keys = 'zf', desc = 'Create fold (operator)' },
{ mode = 'n', keys = 'zG', desc = 'Temporarily mark as correctly spelled' },
{ mode = 'n', keys = 'zg', desc = 'Permanently mark as correctly spelled' },
{ mode = 'n', keys = 'zH', desc = 'Scroll left half screen' },
{ mode = 'n', keys = 'zh', desc = 'Scroll left' },
{ mode = 'n', keys = 'zi', desc = "Toggle 'foldenable'" },
{ mode = 'n', keys = 'zj', desc = 'Move to start of next fold' },
{ mode = 'n', keys = 'zk', desc = 'Move to end of previous fold' },
{ mode = 'n', keys = 'zL', desc = 'Scroll right half screen' },
{ mode = 'n', keys = 'zl', desc = 'Scroll right' },
{ mode = 'n', keys = 'zM', desc = 'Close all folds' },
{ mode = 'n', keys = 'zm', desc = 'Fold more' },
{ mode = 'n', keys = 'zN', desc = "Set 'foldenable'" },
{ mode = 'n', keys = 'zn', desc = "Reset 'foldenable'" },
{ mode = 'n', keys = 'zO', desc = 'Open folds recursively' },
{ mode = 'n', keys = 'zo', desc = 'Open fold' },
{ mode = 'n', keys = 'zP', desc = 'Paste without trailspace' },
{ mode = 'n', keys = 'zp', desc = 'Paste without trailspace' },
{ mode = 'n', keys = 'zR', desc = 'Open all folds' },
{ mode = 'n', keys = 'zr', desc = 'Fold less' },
{ mode = 'n', keys = 'zs', desc = 'Scroll to cursor on left screen side' },
{ mode = 'n', keys = 'zt', desc = 'Redraw at top' },
{ mode = 'n', keys = 'zu', desc = '+Undo spelling commands' },
{ mode = 'n', keys = 'zug', desc = 'Undo `zg`' },
{ mode = 'n', keys = 'zuG', desc = 'Undo `zG`' },
{ mode = 'n', keys = 'zuw', desc = 'Undo `zw`' },
{ mode = 'n', keys = 'zuW', desc = 'Undo `zW`' },
{ mode = 'n', keys = 'zv', desc = 'Open enough folds' },
{ mode = 'n', keys = 'zW', desc = 'Temporarily mark as incorrectly spelled' },
{ mode = 'n', keys = 'zw', desc = 'Permanently mark as incorrectly spelled' },
{ mode = 'n', keys = 'zX', desc = 'Update folds' },
{ mode = 'n', keys = 'zx', desc = 'Update folds + open enough folds' },
{ mode = 'n', keys = 'zy', desc = 'Yank without trailing spaces (operator)' },
{ mode = 'n', keys = 'zz', desc = 'Redraw at center' },
{ mode = 'n', keys = 'z+', desc = 'Redraw under bottom at top' },
{ mode = 'n', keys = 'z-', desc = 'Redraw at bottom + cursor on first non-blank' },
{ mode = 'n', keys = 'z.', desc = 'Redraw at center + cursor on first non-blank' },
{ mode = 'n', keys = 'z=', desc = 'Show spelling suggestions' },
{ mode = 'n', keys = 'z^', desc = 'Redraw above top at bottom' },
{ mode = 'x', keys = 'zf', desc = 'Create fold from selection' },
}
end
-- Helper data ================================================================
-- Module default config
H.default_config = vim.deepcopy(MiniClue.config)
-- Namespaces
H.ns_id = {
highlight = vim.api.nvim_create_namespace('MiniClueHighlight'),
}
-- State of user input
H.state = {
trigger = nil,
-- Array of raw keys
query = {},
clues = {},
timer = vim.loop.new_timer(),
buf_id = nil,
win_id = nil,
is_after_postkeys = false,
}
-- Default window config
H.default_win_config = {
anchor = 'SE',
border = 'single',
focusable = false,
relative = 'editor',
style = 'minimal',
width = 30,
zindex = 99,
}
-- Precomputed raw keys
H.keys = {
-- bs = vim.api.nvim_replace_termcodes('<BS>', true, true, true),
-- cr = vim.api.nvim_replace_termcodes('<CR>', true, true, true),
exit = vim.api.nvim_replace_termcodes([[<C-\><C-n>]], true, true, true),
ctrl_d = vim.api.nvim_replace_termcodes('<C-d>', true, true, true),
ctrl_u = vim.api.nvim_replace_termcodes('<C-u>', true, true, true),
}
-- Timers
H.timers = {
getcharstr = vim.loop.new_timer(),
}
-- Undo command which depends on Neovim version
H.undo_autocommand = 'au ModeChanged * ++once undo' .. (vim.fn.has('nvim-0.8') == 1 and '!' or '')
-- 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', vim.deepcopy(H.default_config), config or {})
vim.validate({
clues = { config.clues, 'table' },
triggers = { config.triggers, 'table' },
window = { config.window, 'table' },
})
local is_table_or_callable = function(x) return type(x) == 'table' or vim.is_callable(x) end
vim.validate({
['window.delay'] = { config.window.delay, 'number' },
['window.config'] = { config.window.config, is_table_or_callable, 'table or callable' },
['window.scroll_down'] = { config.window.scroll_down, 'string' },
['window.scroll_up'] = { config.window.scroll_up, 'string' },
})
return config
end
H.apply_config = function(config)
MiniClue.config = config
-- Create trigger keymaps for all existing buffers
MiniClue.enable_all_triggers()
-- Tweak macro execution
local macro_keymap_opts = { nowait = true, desc = "Execute macro without 'mini.clue' triggers" }
local exec_macro = function(keys)
local register = H.getcharstr()
if register == nil then return end
MiniClue.disable_all_triggers()
vim.schedule(MiniClue.enable_all_triggers)
pcall(vim.api.nvim_feedkeys, vim.v.count1 .. '@' .. register, 'nx', false)
end
vim.keymap.set('n', '@', exec_macro, macro_keymap_opts)
local exec_latest_macro = function(keys)
MiniClue.disable_all_triggers()
vim.schedule(MiniClue.enable_all_triggers)
vim.api.nvim_feedkeys(vim.v.count1 .. 'Q', 'nx', false)
end
vim.keymap.set('n', 'Q', exec_latest_macro, macro_keymap_opts)
end
H.is_disabled = function(buf_id)
local buf_disable = H.get_buf_var(buf_id, 'miniclue_disable')
return vim.g.miniclue_disable == true or buf_disable == true
end
H.create_autocommands = function(config)
local augroup = vim.api.nvim_create_augroup('MiniClue', {})
local au = function(event, pattern, callback, desc)
vim.api.nvim_create_autocmd(event, { group = augroup, pattern = pattern, callback = callback, desc = desc })
end
-- Ensure buffer-local mappings for triggers are the latest ones to fully
-- utilize `<nowait>`. Use `vim.schedule_wrap` to allow other events to
-- create `vim.b.miniclue_config` and `vim.b.miniclue_disable`.
local ensure_triggers = vim.schedule_wrap(function(data)
if not H.is_valid_buf(data.buf) then return end
MiniClue.ensure_buf_triggers(data.buf)
end)
-- - Respect `LspAttach` as it is a common source of buffer-local mappings
local events = vim.fn.has('nvim-0.8') == 1 and { 'BufAdd', 'LspAttach' } or { 'BufAdd' }
au(events, '*', ensure_triggers, 'Ensure buffer-local trigger keymaps')
-- Disable all triggers when recording macro as they interfere with what is
-- actually recorded
au('RecordingEnter', '*', MiniClue.disable_all_triggers, 'Disable all triggers')
au('RecordingLeave', '*', MiniClue.enable_all_triggers, 'Enable all triggers')
au('VimResized', '*', H.window_update, 'Update window on resize')
end
--stylua: ignore
H.create_default_hl = function()
local hi = function(name, opts)
opts.default = true
vim.api.nvim_set_hl(0, name, opts)
end
hi('MiniClueBorder', { link = 'FloatBorder' })
hi('MiniClueDescGroup', { link = 'DiagnosticFloatingWarn' })
hi('MiniClueDescSingle', { link = 'NormalFloat' })
hi('MiniClueNextKey', { link = 'DiagnosticFloatingHint' })
hi('MiniClueNextKeyWithPostkeys', { link = 'DiagnosticFloatingError' })
hi('MiniClueSeparator', { link = 'DiagnosticFloatingInfo' })
hi('MiniClueTitle', { link = 'FloatTitle' })
end
H.get_config = function(config, buf_id)
config = config or {}
local buf_config = H.get_buf_var(buf_id, 'miniclue_config') or {}
local global_config = MiniClue.config
-- Manually reconstruct to allow array elements to be concatenated
local res = {
clues = H.list_concat(global_config.clues, buf_config.clues, config.clues),
triggers = H.list_concat(global_config.triggers, buf_config.triggers, config.triggers),
window = vim.tbl_deep_extend('force', global_config.window, buf_config.window or {}, config.window or {}),
}
return res
end
H.get_buf_var = function(buf_id, name)
buf_id = buf_id or vim.api.nvim_get_current_buf()
if not H.is_valid_buf(buf_id) then return nil end
return vim.b[buf_id][name]
end
-- Triggers -------------------------------------------------------------------
H.map_buf_triggers = function(buf_id)
if not H.is_valid_buf(buf_id) or H.is_disabled(buf_id) then return end
for _, trigger in ipairs(H.get_config(nil, buf_id).triggers) do
H.map_trigger(buf_id, trigger)
end
end
H.unmap_buf_triggers = function(buf_id)
if not H.is_valid_buf(buf_id) or H.is_disabled(buf_id) then return end
for _, trigger in ipairs(H.get_config(nil, buf_id).triggers) do
H.unmap_trigger(buf_id, trigger)
end
end
H.map_trigger = function(buf_id, trigger)
if not H.is_valid_buf(buf_id) then return end
-- Compute mapping RHS
trigger.keys = H.replace_termcodes(trigger.keys)
local keys_trans = H.keytrans(trigger.keys)
local rhs = function()
-- Don't act if for some reason entered the same trigger during state exec
local is_in_exec = type(H.exec_trigger) == 'table'
and H.exec_trigger.mode == trigger.mode
and H.exec_trigger.keys == trigger.keys
if is_in_exec then
H.exec_trigger = nil
return
end
-- Start user query
H.state_set(trigger, { trigger.keys })
-- Do not advance if no other clues to query. NOTE: it is `<= 1` and not
-- `<= 0` because the "init query" mapping should match.
if vim.tbl_count(H.state.clues) <= 1 then return H.state_exec() end
H.state_advance()
end
-- Use buffer-local mappings and `nowait` to make it a primary source of
-- keymap execution
local desc = string.format('Query keys after "%s"', keys_trans)
local opts = { buffer = buf_id, nowait = true, desc = desc }
-- Create mapping. Use translated variant to make it work with <F*> keys.
vim.keymap.set(trigger.mode, keys_trans, rhs, opts)
end
H.unmap_trigger = function(buf_id, trigger)
if not H.is_valid_buf(buf_id) then return end
pcall(vim.keymap.del, trigger.mode, H.keytrans(trigger.keys), { buffer = buf_id })
end
-- State ----------------------------------------------------------------------
H.state_advance = function(opts)
opts = opts or {}
local config_window = H.get_config().window
-- Show clues: delay (debounce) first show; update immediately if shown or
-- after postkeys (for visual feedback that extra key is needed to stop)
H.state.timer:stop()
local show_immediately = H.is_valid_win(H.state.win_id) or H.state.is_after_postkeys
local delay = show_immediately and 0 or config_window.delay
H.state.timer:start(delay, 0, function() H.window_update(opts.same_content) end)
-- Reset postkeys right now to not flicker when trying to close window during
-- "not querying" check
H.state.is_after_postkeys = false
-- Query user for new key
local key = H.getcharstr()
-- Handle key
if key == nil then return H.state_reset() end
if key == H.keys.cr then return H.state_exec() end
local is_window_shown = H.is_valid_win(H.state.win_id)
local is_scroll_down = key == H.replace_termcodes(config_window.scroll_down)
local is_scroll_up = key == H.replace_termcodes(config_window.scroll_up)
if is_window_shown and (is_scroll_down or is_scroll_up) then
H.window_scroll(is_scroll_down)
return H.state_advance({ same_content = true })
end
if key == H.keys.bs then
H.state_pop()
else
H.state_push(key)
end
-- Advance state
-- - Execute if reached single target keymap
if H.state_is_at_target() then return H.state_exec() end
-- - Reset if there are no keys (like after `<BS>`)
if #H.state.query == 0 then return H.state_reset() end
-- - Query user for more information if there is not enough
-- NOTE: still advance even if there is single clue because it is still not
-- a target but can be one.
if vim.tbl_count(H.state.clues) >= 1 then return H.state_advance() end
-- - Fall back for executing what user typed
H.state_exec()
end
H.state_set = function(trigger, query)
H.state.trigger = trigger
H.state.query = query
H.state.clues = H.clues_filter(H.clues_get_all(trigger.mode), query)
end
H.state_reset = function(keep_window)
H.state.trigger = nil
H.state.query = {}
H.state.clues = {}
H.state.is_after_postkeys = false
H.state.timer:stop()
if not keep_window then H.window_close() end
end
H.state_exec = function()
-- Compute keys to type
local keys_to_type = H.compute_exec_keys()
-- Add extra (redundant) safety flag to try to avoid infinite recursion
local trigger, clue = H.state.trigger, H.state_get_query_clue()
H.exec_trigger = trigger
vim.schedule(function() H.exec_trigger = nil end)
-- Reset state
local has_postkeys = (clue or {}).postkeys ~= nil
H.state_reset(has_postkeys)
-- Disable trigger !!!VERY IMPORTANT!!!
-- This is a workaround against infinite recursion (like if `g` is trigger
-- then typing `gg`/`g~` would introduce infinite recursion).
local buf_id = vim.api.nvim_get_current_buf()
H.unmap_trigger(buf_id, trigger)
-- Execute keys. The `i` flag is used to fully support Operator-pending mode.
-- Flag `t` imitates keys as if user typed, which is reasonable but has small
-- downside with edge cases of 'langmap' (like ':\;;\;:') as it "inverts" key
-- meaning second time (at least in Normal mode).
vim.api.nvim_feedkeys(keys_to_type, 'mit', false)
-- Enable trigger back after it can no longer harm
vim.schedule(function() H.map_trigger(buf_id, trigger) end)
-- Apply postkeys (in scheduled fashion)
if has_postkeys then H.state_apply_postkeys(clue.postkeys) end
end
H.state_push = function(keys)
table.insert(H.state.query, keys)
H.state.clues = H.clues_filter(H.state.clues, H.state.query)
end
H.state_pop = function()
H.state.query[#H.state.query] = nil
H.state.clues = H.clues_filter(H.clues_get_all(H.state.trigger.mode), H.state.query)
end
H.state_apply_postkeys = vim.schedule_wrap(function(postkeys)
-- Register that possible future querying is a result of postkeys.
-- This enables (keep) showing window immediately.
H.state.is_after_postkeys = true
-- Use `nvim_feedkeys()` because using `state_set()` and
-- `state_advance()` directly does not work: it doesn't guarantee to be
-- executed **after** keys from `nvim_feedkeys()`.
vim.api.nvim_feedkeys(postkeys, 'mit', false)
-- Defer check of whether postkeys resulted into window.
-- Could not find proper way to check this which guarantees to be executed
-- after `nvim_feedkeys()` takes effect **end** doesn't result into flicker
-- when consecutively applying "submode" keys.
vim.defer_fn(function()
if #H.state.query == 0 then H.window_close() end
end, 50)
end)
H.state_is_at_target = function()
return vim.tbl_count(H.state.clues) == 1 and H.state.clues[H.query_to_keys(H.state.query)] ~= nil
end
H.state_get_query_clue = function()
local keys = H.query_to_keys(H.state.query)
return H.state.clues[keys]
end
H.compute_exec_keys = function()
local keys_count = vim.v.count > 0 and vim.v.count or ''
local keys_query = H.query_to_keys(H.state.query)
local res = keys_count .. keys_query
local cur_mode = vim.fn.mode(1)
-- Using `feedkeys()` inside Operator-pending mode leads to its cancel into
-- Normal/Insert mode so extra work should be done to rebuild all keys
if vim.startswith(cur_mode, 'no') then
local operator_tweak = H.operator_tweaks[vim.v.operator] or function(x) return x end
res = operator_tweak(vim.v.operator .. H.get_forced_submode() .. res)
elseif not vim.startswith(cur_mode, 'i') and H.get_default_register() ~= vim.v.register then
-- Force non-default register but not in Insert mode
res = '"' .. vim.v.register .. res
end
-- `feedkeys()` inside "temporary" Normal mode is executed **after** it is
-- already back from Normal mode. Go into it again with `<C-o>` ('\15').
-- NOTE: This only works when Normal mode trigger is triggered in
-- "temporary" Normal mode. Still doesn't work when Operator-pending mode is
-- triggered afterwards (like in `<C-o>gUiw` with 'i' as trigger).
if cur_mode:find('^ni') ~= nil then res = '\15' .. res end
return res
end
-- Some operators needs special tweaking due to their nature:
-- - Some operators perform on register. Solution: add register explicitly.
-- - Some operators end up changing mode which affects `feedkeys()`.
-- Solution: explicitly exit to Normal mode with '<C-\><C-n>'.
-- - Some operators still perform some redundant operation before `feedkeys()`
-- takes effect. Solution: add one-shot autocommand undoing that.
H.operator_tweaks = {
['c'] = function(keys)
-- Doing '<C-\><C-n>' moves cursor one space to left (same as `i<Esc>`).
-- Solution: add one-shot autocommand correcting cursor position.
vim.cmd('au InsertLeave * ++once normal! l')
return H.keys.exit .. '"' .. vim.v.register .. keys
end,
['d'] = function(keys) return '"' .. vim.v.register .. keys end,
['y'] = function(keys) return '"' .. vim.v.register .. keys end,
['~'] = function(keys)
if vim.fn.col('.') == 1 then vim.cmd(H.undo_autocommand) end
return keys
end,
['g~'] = function(keys)
if vim.fn.col('.') == 1 then vim.cmd(H.undo_autocommand) end
return keys
end,
['g?'] = function(keys)
if vim.fn.col('.') == 1 then vim.cmd(H.undo_autocommand) end
return keys
end,
['!'] = function(keys) return H.keys.exit .. keys end,
['>'] = function(keys)
vim.cmd(H.undo_autocommand)
return keys
end,
['<'] = function(keys)
vim.cmd(H.undo_autocommand)
return keys
end,
['g@'] = function(keys)
-- Cancelling in-process `g@` operator seems to be particularly hard.
-- Not even sure why specifically this combination works, but having `x`
-- flag in `feedkeys()` is crucial.
vim.api.nvim_feedkeys(H.keys.exit, 'nx', false)
return H.keys.exit .. keys
end,
}
H.query_to_keys = function(query) return table.concat(query, '') end
H.query_to_title = function(query) return H.keytrans(H.query_to_keys(query)) end
-- Window ---------------------------------------------------------------------
H.window_update = vim.schedule_wrap(function(same_content)
-- Make sure that outdated windows are not shown
if #H.state.query == 0 then return H.window_close() end
local win_id = H.state.win_id
-- Close window if it is not in current tabpage (as only window is tracked)
local is_different_tabpage = H.is_valid_win(win_id)
and vim.api.nvim_win_get_tabpage(win_id) ~= vim.api.nvim_get_current_tabpage()
if is_different_tabpage then H.window_close() end
-- Create-update buffer showing clues
if not same_content then H.state.buf_id = H.buffer_update() end
-- Create-update window showing buffer
local win_config = H.window_get_config()
if not H.is_valid_win(win_id) then
win_config.noautocmd = true
win_id = H.window_open(win_config)
H.state.win_id = win_id
else
vim.api.nvim_win_set_config(win_id, win_config)
vim.wo[win_id].list = true
end
-- Make scroll not persist. NOTE: Don't use 'normal! gg' inside target window
-- as it resets `v:count` and `v:register` which results into invalid keys
-- reproduction in Operator-pending mode.
if not same_content then vim.api.nvim_win_set_cursor(win_id, { 1, 0 }) end
-- Add redraw because Neovim won't do it when `getcharstr()` is active
vim.cmd('redraw')
end)
H.window_scroll = function(is_scroll_down)
local scroll_key = is_scroll_down and H.keys.ctrl_d or H.keys.ctrl_u
local f = function()
local cache_scroll, bot_line, n_lines = vim.wo.scroll, vim.fn.line('w$'), vim.api.nvim_buf_line_count(0)
-- Do not scroll past the end of buffer
local scroll_count = is_scroll_down and math.min(cache_scroll, n_lines - bot_line) or cache_scroll
if scroll_count > 0 then pcall(vim.cmd, 'normal! ' .. scroll_count .. scroll_key) end
vim.wo.scroll = cache_scroll
end
vim.api.nvim_win_call(H.state.win_id, f)
end
H.window_open = function(config)
local win_id = vim.api.nvim_open_win(H.state.buf_id, false, config)
vim.wo[win_id].foldenable = false
vim.wo[win_id].wrap = false
vim.wo[win_id].list = true
vim.wo[win_id].listchars = 'extends:…'
-- Neovim=0.7 doesn't support invalid highlight groups in 'winhighlight'
local win_hl = 'FloatBorder:MiniClueBorder' .. (vim.fn.has('nvim-0.8') == 1 and ',FloatTitle:MiniClueTitle' or '')
vim.wo[win_id].winhighlight = win_hl
return win_id
end
H.window_close = function()
-- Closing floating window when Command-line window is active is not allowed
-- on Neovim<0.10. Make sure it is closed after leaving it.
-- See https://github.com/neovim/neovim/issues/24452
local win_id = H.state.win_id
if vim.fn.has('nvim-0.10') == 0 and vim.fn.getcmdwintype() ~= '' then
vim.api.nvim_create_autocmd(
'CmdwinLeave',
{ once = true, callback = function() pcall(vim.api.nvim_win_close, win_id, true) end }
)
return
else
pcall(vim.api.nvim_win_close, win_id, true)
end
H.state.win_id = nil
end
H.window_get_config = function()
local has_statusline = vim.o.laststatus > 0
local has_tabline = vim.o.showtabline == 2 or (vim.o.showtabline == 1 and #vim.api.nvim_list_tabpages() > 1)
-- Remove 2 from maximum height to account for top and bottom borders
local max_height = vim.o.lines - vim.o.cmdheight - (has_tabline and 1 or 0) - (has_statusline and 1 or 0) - 2
local buf_id = H.state.buf_id
local cur_config_fields = {
row = vim.o.lines - vim.o.cmdheight - (has_statusline and 1 or 0),
col = vim.o.columns,
height = math.min(vim.api.nvim_buf_line_count(buf_id), max_height),
title = H.query_to_title(H.state.query),
}
local user_config = H.expand_callable(H.get_config().window.config, buf_id) or {}
local res = vim.tbl_deep_extend('force', H.default_win_config, cur_config_fields, user_config)
-- Tweak "auto" fields
if res.width == 'auto' then res.width = H.buffer_get_width() + 1 end
res.width = math.min(res.width, vim.o.columns)
if res.row == 'auto' then
local is_on_top = res.anchor == 'NW' or res.anchor == 'NE'
res.row = is_on_top and (has_tabline and 1 or 0) or cur_config_fields.row
end
if res.col == 'auto' then
local is_on_left = res.anchor == 'NW' or res.anchor == 'SW'
res.col = is_on_left and 0 or cur_config_fields.col
end
-- Ensure it works on Neovim<0.9
if vim.fn.has('nvim-0.9') == 0 then res.title = nil end
return res
end
-- Buffer ---------------------------------------------------------------------
H.buffer_update = function()
local buf_id = H.state.buf_id
if not H.is_valid_buf(buf_id) then buf_id = vim.api.nvim_create_buf(false, true) end
-- Compute content data
local keys = H.query_to_keys(H.state.query)
local content = H.clues_to_buffer_content(H.state.clues, keys)
-- Add lines
local lines = {}
for _, line_content in ipairs(content) do
table.insert(lines, string.format(' %s │ %s', line_content.next_key, line_content.desc))
end
vim.api.nvim_buf_set_lines(buf_id, 0, -1, false, lines)
-- Add highlighting
local ns_id = H.ns_id.highlight
vim.api.nvim_buf_clear_namespace(buf_id, ns_id, 0, -1)
local set_hl = function(hl_group, line_from, col_from, line_to, col_to)
local opts = { end_row = line_to, end_col = col_to, hl_group = hl_group, hl_eol = true }
vim.api.nvim_buf_set_extmark(buf_id, ns_id, line_from, col_from, opts)
end
for i, line_content in ipairs(content) do
local sep_start = line_content.next_key:len() + 3
local next_key_hl_group = line_content.has_postkeys and 'MiniClueNextKeyWithPostkeys' or 'MiniClueNextKey'
set_hl(next_key_hl_group, i - 1, 0, i - 1, sep_start - 1)
-- NOTE: Separator '│' is 3 bytes long
set_hl('MiniClueSeparator', i - 1, sep_start - 1, i - 1, sep_start + 2)
local desc_hl_group = line_content.is_group and 'MiniClueDescGroup' or 'MiniClueDescSingle'
set_hl(desc_hl_group, i - 1, sep_start + 2, i, 0)
end
return buf_id
end
H.buffer_get_width = function()
if not H.is_valid_buf(H.state.buf_id) then return end
local lines = vim.api.nvim_buf_get_lines(H.state.buf_id, 0, -1, false)
local res = 0
for _, l in ipairs(lines) do
res = math.max(res, vim.fn.strdisplaywidth(l))
end
return res
end
-- Clues ----------------------------------------------------------------------
H.clues_get_all = function(mode)
local res = {}
-- Order of clue precedence: config clues < buffer mappings < global mappings
local config_clues = H.clues_normalize(H.get_config().clues) or {}
local mode_clues = vim.tbl_filter(function(x) return x.mode == mode end, config_clues)
for _, clue in ipairs(mode_clues) do
local lhsraw = H.replace_termcodes(clue.keys)
local res_data = res[lhsraw] or {}
-- - Allow callable clue description
local desc = H.expand_callable(clue.desc)
-- - Fall back to possibly already present fields to allow partial
-- overwrite in later clues. Like to add `postkeys` and inherit `desc`.
res_data.desc = desc or res_data.desc
res_data.postkeys = H.replace_termcodes(clue.postkeys) or res_data.postkeys
res[lhsraw] = res_data
end
for _, map_data in ipairs(vim.api.nvim_get_keymap(mode)) do
local lhsraw = H.replace_termcodes(map_data.lhs)
local res_data = res[lhsraw] or {}
res_data.desc = map_data.desc or ''
res[lhsraw] = res_data
end
for _, map_data in ipairs(vim.api.nvim_buf_get_keymap(0, mode)) do
local lhsraw = H.replace_termcodes(map_data.lhs)
local res_data = res[lhsraw] or {}
res_data.desc = map_data.desc or ''
res[lhsraw] = res_data
end
return res
end
H.clues_normalize = function(clues)
local res = {}
local process
process = function(x)
x = H.expand_callable(x)
if H.is_clue(x) then return table.insert(res, x) end
if not H.islist(x) then return nil end
for _, y in ipairs(x) do
process(y)
end
end
process(clues)
return res
end
H.clues_filter = function(clues, query)
local keys = H.query_to_keys(query)
for clue_keys, _ in pairs(clues) do
if not vim.startswith(clue_keys, keys) then clues[clue_keys] = nil end
end
return clues
end
H.clues_to_buffer_content = function(clues, keys)
-- Use translated keys to properly handle cases like `<Del>`, `<End>`, etc.
keys = H.keytrans(keys)
-- Gather clue data
local keys_len = keys:len()
local keys_pattern = string.format('^%s(.+)$', vim.pesc(keys))
local next_key_data, next_key_max_width = {}, 0
for clue_keys, clue_data in pairs(clues) do
local left, _, rest_keys = H.keytrans(clue_keys):find(keys_pattern)
-- Add non-trivial next key data only if clue matches current keys plus
-- something more
if left ~= nil then
local next_key = H.clues_get_first_key(rest_keys)
-- Update description data
local data = next_key_data[next_key] or {}
data.n_choices = (data.n_choices or 0) + 1
-- - Add description directly if it is group clue with description or
-- a non-group clue
if next_key == rest_keys then
data.desc = clue_data.desc or ''
data.has_postkeys = clue_data.postkeys ~= nil
end
next_key_data[next_key] = data
-- Update width data
local next_key_width = vim.fn.strchars(next_key)
data.next_key_width = next_key_width
next_key_max_width = math.max(next_key_max_width, next_key_width)
end
end
-- Convert to array sorted by keys and finalize content
local next_keys_extra = vim.tbl_map(
function(x) return { key = x, keytype = H.clues_get_next_key_type(x) } end,
vim.tbl_keys(next_key_data)
)
table.sort(next_keys_extra, H.clues_compare_next_key)
local next_keys = vim.tbl_map(function(x) return x.key end, next_keys_extra)
local res = {}
for _, key in ipairs(next_keys) do
local data = next_key_data[key]
local is_group = data.n_choices > 1
local desc = data.desc or string.format('+%d choice%s', data.n_choices, is_group and 's' or '')
local next_key = key .. string.rep(' ', next_key_max_width - data.next_key_width)
table.insert(res, { next_key = next_key, desc = desc, is_group = is_group, has_postkeys = data.has_postkeys })
end
return res
end
H.clues_get_first_key = function(keys)
-- `keys` are assumed to be translated
-- Special keys
local special = keys:match('^(%b<>)')
if special ~= nil then return special end
-- <
if keys:find('^<') ~= nil then return '<' end
-- Other characters
return vim.fn.strcharpart(keys, 0, 1)
end
H.clues_get_next_key_type = function(x)
if x:find('^%w$') ~= nil then return 'alphanum' end
if x:find('^<.*>$') ~= nil then return 'mod' end
return 'other'
end
H.clues_compare_next_key = function(a, b)
local a_type, b_type = a.keytype, b.keytype
if a_type == b_type then
local cmp = vim.stricmp(a.key, b.key)
return cmp == -1 or (cmp == 0 and a.key < b.key)
end
if a_type == 'alphanum' then return true end
if b_type == 'alphanum' then return false end
if a_type == 'mod' then return true end
if b_type == 'mod' then return false end
end
-- Clue generators ------------------------------------------------------------
H.make_clues_with_register_contents = function(mode, prefix)
local make_register_desc = function(register)
return function()
local ok, value = pcall(vim.fn.getreg, register, 1)
if not ok or value == '' then return nil end
return vim.inspect(value)
end
end
local all_registers = vim.split('0123456789abcdefghijklmnopqrstuvwxyz*+"-:.%/#', '')
local res = {}
for _, register in ipairs(all_registers) do
table.insert(res, { mode = mode, keys = prefix .. register, desc = make_register_desc(register) })
end
table.insert(res, { mode = mode, keys = prefix .. '=', desc = 'Result of expression' })
return res
end
-- Predicates -----------------------------------------------------------------
H.is_trigger = function(x) return type(x) == 'table' and type(x.mode) == 'string' and type(x.keys) == 'string' end
H.is_clue = function(x)
if type(x) ~= 'table' then return false end
local mandatory = type(x.mode) == 'string' and type(x.keys) == 'string'
local extra = (x.desc == nil or type(x.desc) == 'string' or vim.is_callable(x.desc))
and (x.postkeys == nil or type(x.postkeys) == 'string')
return mandatory and extra
end
H.is_array_of = function(x, predicate)
if not H.islist(x) then return false end
for _, v in ipairs(x) do
if not predicate(v) then return false end
end
return true
end
-- Utilities ------------------------------------------------------------------
H.error = function(msg) error(string.format('(mini.clue) %s', msg), 0) end
H.map = function(mode, lhs, rhs, opts)
if lhs == '' then return end
opts = vim.tbl_deep_extend('force', { silent = true }, opts or {})
vim.keymap.set(mode, lhs, rhs, opts)
end
H.replace_termcodes = function(x)
if x == nil then return nil end
-- Use `keytrans` prior replacing termcodes to work correctly on already
-- replaced variant of `<F*>` keys
return vim.api.nvim_replace_termcodes(H.keytrans(x), true, true, true)
end
-- TODO: Remove after compatibility with Neovim=0.7 is dropped
if vim.fn.has('nvim-0.8') == 1 then
H.keytrans = function(x)
local res = vim.fn.keytrans(x):gsub('<lt>', '<')
return res
end
else
H.keytrans = function(x)
local res = x:gsub('<lt>', '<')
return res
end
end
H.get_forced_submode = function()
local mode = vim.fn.mode(1)
if not mode:sub(1, 2) == 'no' then return '' end
return mode:sub(3)
end
H.get_default_register = function()
local clipboard = vim.o.clipboard
if clipboard:find('unnamedplus') ~= nil then return '+' end
if clipboard:find('unnamed') ~= nil then return '*' end
return '"'
end
H.is_valid_buf = function(buf_id) return type(buf_id) == 'number' and vim.api.nvim_buf_is_valid(buf_id) end
H.is_valid_win = function(win_id) return type(win_id) == 'number' and vim.api.nvim_win_is_valid(win_id) end
H.expand_callable = function(x, ...)
if vim.is_callable(x) then return x(...) end
return x
end
H.redraw_scheduled = vim.schedule_wrap(function() vim.cmd('redraw') end)
H.getcharstr = function()
-- Ensure redraws still happen
H.timers.getcharstr:start(0, 50, H.redraw_scheduled)
local ok, char = pcall(vim.fn.getcharstr)
H.timers.getcharstr:stop()
-- Terminate if couldn't get input (like with <C-c>) or it is `<Esc>`
if not ok or char == '\27' or char == '' then return end
return H.get_langmap()[char] or char
end
H.get_langmap = function()
if vim.o.langmap == '' then return {} end
-- Get langmap parts by splitting at "," not preceded by "\"
local langmap_parts = vim.fn.split(vim.o.langmap, '[^\\\\]\\zs,')
-- Process each langmap part
local res = {}
for _, part in ipairs(langmap_parts) do
H.process_langmap_part(res, part)
end
return res
end
H.process_langmap_part = function(res, part)
local semicolon_byte_ind = vim.fn.match(part, '[^\\\\]\\zs;') + 1
-- Part is without ';', like 'aAbB'
if semicolon_byte_ind == 0 then
-- Drop backslash escapes
part = part:gsub('\\([^\\])', '%1')
for i = 1, vim.fn.strchars(part), 2 do
-- `strcharpart()` has 0-based indexes
local from, to = vim.fn.strcharpart(part, i - 1, 1), vim.fn.strcharpart(part, i, 1)
if from ~= '' and to ~= '' then res[from] = to end
end
return
end
-- Part is with ';', like 'ab;AB'
-- - Drop backslash escape
local left = part:sub(1, semicolon_byte_ind - 1):gsub('\\([^\\])', '%1')
local right = part:sub(semicolon_byte_ind + 1):gsub('\\([^\\])', '%1')
for i = 1, vim.fn.strchars(left) do
local from, to = vim.fn.strcharpart(left, i - 1, 1), vim.fn.strcharpart(right, i - 1, 1)
if from ~= '' and to ~= '' then res[from] = to end
end
end
H.list_concat = function(...)
local res = {}
for i = 1, select('#', ...) do
for _, x in ipairs(select(i, ...) or {}) do
table.insert(res, x)
end
end
return res
end
-- TODO: Remove after compatibility with Neovim=0.9 is dropped
H.islist = vim.fn.has('nvim-0.10') == 1 and vim.islist or vim.tbl_islist
return MiniClue
================================================
FILE: lua/blink/config.lua
================================================
local M = {}
M.default = {
chartoggle = {
enabled = false,
delimiters = { ',', ';' },
},
clue = {
enabled = false,
},
select = {
enabled = false,
},
tree = {
enabled = false,
},
}
function M.setup(opts) M.config = vim.tbl_deep_extend('force', M.default, opts or {}) end
return setmetatable(M, { __index = function(_, k) return M.config[k] end })
================================================
FILE: lua/blink/dashboard/init.lua
================================================
local api = vim.api
local Dashboard = {}
local function create_buf()
local bufnr = api.nvim_create_buf(false, true)
local opts = {
['bufhidden'] = 'wipe',
['colorcolumn'] = '',
['foldcolumn'] = '0',
['matchpairs'] = '',
['buflisted'] = false,
['cursorcolumn'] = false,
['cursorline'] = false,
['list'] = false,
['number'] = false,
['relativenumber'] = false,
['spell'] = false,
['swapfile'] = false,
['readonly'] = false,
['filetype'] = 'dashboard',
['wrap'] = false,
['signcolumn'] = 'no',
['winbar'] = '',
['stc'] = '',
}
for opt, val in pairs(opts) do
api.nvim_set_option_value(opt, val, { buf = bufnr })
end
return bufnr
end
function Dashboard.setup()
-- should we show the dashboard?
if vim.fn.argc() == 0 and api.nvim_buf_get_name(0) == '' and vim.g.read_from_stdin == nil then return end
local bufnr = create_buf()
local winid = api.nvim_get_current_win()
api.nvim_win_set_buf(winid, bufnr)
local center_line = function(line)
local width = api.nvim_win_get_width(0)
local line_width = string.len(line)
local padding = math.floor((width - line_width) / 2)
return string.rep(' ', padding) .. line
end
local lines = {
'',
center_line('Welcome to Tuque'),
}
local centered_lines = {}
for _, line in ipairs(lines) do
table.insert(centered_lines, center_line(line))
end
api.nvim_buf_set_lines(bufnr, 0, -1, false, centered_lines)
end
return Dashboard
================================================
FILE: lua/blink/init.lua
================================================
local M = {}
function M.setup(opts)
local config = require('blink.config')
config.setup(opts)
if config.chartoggle.enabled then require('blink.chartoggle').setup(config.chartoggle) end
if config.clue.enabled then require('blink.clue').setup(config.clue) end
if config.indent and config.indent.enabled then
vim.notify(
'blink.nvim: indent.enabled has been replaced by a separate blink.indent repo. See https://github.com/saghen/blink.indent',
vim.log.levels.WARN
)
end
if config.select.enabled then require('blink.select').setup(config.select) end
if config.tree.enabled then require('blink.tree').setup(config.tree) end
end
return M
================================================
FILE: lua/blink/render/types.lua
================================================
--- 0-1 is interpretted as percentage of parent and whole numbers are the number of columns.
--- When an array, the minimum will be taken in all cases except max_width and max_height.
--- @alias Length number | number[]
---
--- @class Component
--- @field align? 'left' | 'center' | 'right'
--- Number of spaces or string of characters to pad between components
--- @field gap? number | string
--- @field space? 'between' | 'around' | 'evenly'
--- @field direction? 'horizontal' | 'vertical'
--- @field size? 'expand' | 'shrink'
--- @field overflow? 'ellipsis'
--- @field max_width? Length
--- @field min_width? Length
--- @field max_height? Length
--- @field min_height? Length
--- @field width? Length
--- @field height? Length
--- @field padding? Length
--- @field margin? Length
--- @field children Component[]
--- @param comp Component
function get(comp) local yo = comp.width end
return Component
================================================
FILE: lua/blink/select/config.lua
================================================
--- @class SelectMapping
--- @field selection string[]
--- @field quit string[]
--- @field next_page string[]
--- @field prev_page string[]
---
--- @class SelectWindowConfig
--- @field min_width number[]
--- @field max_width number[]
--- @field border 'single' | 'double' | 'rounded' | string[]
--- @field wrap boolean
--- @class SelectConfig
--- @field mapping SelectMapping
--- @field window SelectWindowConfig
local config = {
mapping = {
selection = { 'h', 'j', 'k', 'l', 'a', 's', 'd', 'f' },
-- remember selection also uses the capital variants so dont interfere
prev_page = { '<C-h>' },
next_page = { '<C-l>' },
quit = { 'q', '<Esc>' },
},
window = {
min_width = { 20, 0.2 }, -- greater of 20 columns and 20% of current window width
max_width = { 120, 0.8 }, -- lesser of 120 columns and 80% of current window width
border = 'rounded',
wrap = false,
group_size = 4,
},
}
function config.setup(opts) config = vim.tbl_deep_extend('force', config, opts or {}) end
return setmetatable({}, { __index = function(_, k) return config[k] end })
================================================
FILE: lua/blink/select/init.lua
================================================
local select = {}
--- @param opts SelectConfig
function select.setup(opts)
require('blink.select.config').setup(opts)
require('blink.select.providers.yank-history') -- requires autocmds setup early
end
function select.show(provider) require('blink.select.window').show(require('blink.select.providers.' .. provider)) end
return select
================================================
FILE: lua/blink/select/providers/buffers.lua
================================================
--- @class SelectProvider
local buffers = {
name = 'Buffers',
}
function buffers.get_items(opts, cb)
local idx = 1
local bufs = vim.api.nvim_list_bufs()
bufs = vim.tbl_filter(function(bufnr) return vim.api.nvim_get_option_value('buflisted', { buf = bufnr }) end, bufs)
-- Sort buffers by last used time
table.sort(bufs, function(a, b) return vim.fn.getbufinfo(a)[1].lastused > vim.fn.getbufinfo(b)[1].lastused end)
local devicons = require('nvim-web-devicons')
cb({
page_count = math.ceil(#bufs / opts.page_size),
next_page = function(page_cb)
local items = {}
while idx <= #bufs and #items < opts.page_size do
local bufnr = bufs[idx]
if vim.api.nvim_buf_is_valid(bufnr) then
local buf_path = vim.api.nvim_buf_get_name(bufnr)
local dirname = vim.fn.fnamemodify(buf_path, ':~:.:h')
local dirname_component = { dirname, highlight = 'Comment' }
local filename = vim.fn.fnamemodify(buf_path, ':t')
if filename == '' then filename = '[No Name]' end
local diagnostic_level = nil
for _, diagnostic in ipairs(vim.diagnostic.get(bufnr)) do
diagnostic_level = math.min(diagnostic_level or 999, diagnostic.severity)
end
local filename_hl = diagnostic_level == vim.diagnostic.severity.HINT and 'DiagnosticHint'
or diagnostic_level == vim.diagnostic.severity.INFO and 'DiagnosticInfo'
or diagnostic_level == vim.diagnostic.severity.WARN and 'DiagnosticWarn'
or diagnostic_level == vim.diagnostic.severity.ERROR and 'DiagnosticError'
or 'Normal'
local filename_component = { filename, highlight = filename_hl }
-- Modified icon
local modified = vim.bo[bufnr].modified
local modified_component = modified and { ' ● ', highlight = 'BufferCurrentMod' } or ''
local icon, icon_hl = devicons.get_icon(filename)
local icon_component = icon and { ' ' .. icon .. ' ', highlight = icon_hl } or ''
table.insert(items, {
data = { bufnr = bufnr },
fragments = {
modified_component,
icon_component,
' ',
filename_component,
' ',
dirname_component,
' ',
},
})
end
idx = idx + 1
end
page_cb(items)
end,
})
end
function buffers.select(item) vim.api.nvim_set_current_buf(item.data.bufnr) end
return buffers
================================================
FILE: lua/blink/select/providers/code-actions.lua
================================================
--- @class SelectProvider
local code_actions = {
name = 'Code Actions',
}
function code_actions.get_items(opts, cb)
local idx = 1
local actions = {}
-- Get available code actions
local results = vim.lsp.buf_request_sync(0, 'textDocument/codeAction', vim.lsp.util.make_range_params(), 1000)
for _, result in pairs(results or {}) do
if result and result.result then vim.tbl_extend('force', actions, result.result) end
end
cb({
page_count = math.ceil(#actions / opts.page_size),
next_page = function(page_cb)
--- @type RenderFragment[]
local items = {}
while idx <= #actions and #items < opts.page_size do
local action = actions[idx]
-- Action title
local title_component = { action.title, highlight = 'Function' }
-- Action kind (if available)
--- @type string | RenderFragment
local kind_component = ''
if action.kind then kind_component = { ' [' .. action.kind .. ']', highlight = 'Comment' } end
-- Action source (if available)
--- @type string | RenderFragment
local source_component = ''
if action.source then source_component = { ' from ' .. action.source, highlight = 'Comment' } end
table.insert(items, {
data = action,
fragments = {
' ',
title_component,
kind_component,
source_component,
},
})
idx = idx + 1
end
page_cb(items)
end,
})
end
function code_actions.select(item)
local action = item.data
if action.edit or type(action.command) == 'table' then
if action.edit then vim.lsp.util.apply_workspace_edit(action.edit, 'UTF-8') end
if type(action.command) == 'table' then vim.lsp.buf.execute_command(action.command) end
else
vim.lsp.buf.execute_command(action)
end
end
return code_actions
================================================
FILE: lua/blink/select/providers/diagnostics.lua
================================================
--- @class SelectProvider
local diagnostics = {
name = 'Diagnostics',
}
function diagnostics.get_items(opts, cb)
local idx = 1
local items = {}
local all_diagnostics = vim.diagnostic.get(opts.bufnr, { severity = { min = vim.diagnostic.severity.HINT } })
-- sort by line number
table.sort(all_diagnostics, function(a, b) return a.lnum < b.lnum end)
cb({
page_count = math.ceil(#all_diagnostics / opts.page_size),
next_page = function(page_cb)
while idx <= #all_diagnostics and #items < opts.page_size do
local diag = all_diagnostics[idx]
local bufnr = diag.bufnr
if bufnr == nil then break end
-- Get the diagnostic symbol
local symbol = '●'
if diag.severity == vim.diagnostic.severity.ERROR then
symbol = ' '
elseif diag.severity == vim.diagnostic.severity.WARN then
symbol = ' '
elseif diag.severity == vim.diagnostic.severity.INFO then
symbol = ' '
elseif diag.severity == vim.diagnostic.severity.HINT then
symbol = ' '
end
-- Get the diagnostic highlight
local highlight = 'DiagnosticHint'
if diag.severity == vim.diagnostic.severity.ERROR then
highlight = 'DiagnosticError'
elseif diag.severity == vim.diagnostic.severity.WARN then
highlight = 'DiagnosticWarn'
elseif diag.severity == vim.diagnostic.severity.INFO then
highlight = 'DiagnosticInfo'
end
-- Truncate the message if it's too long
local short_message = vim.split(diag.message, '\n')[1]
if #short_message > 80 then short_message = short_message:sub(1, 77) .. '...' end
table.insert(items, {
data = { bufnr = bufnr, lnum = diag.lnum, col = diag.col },
fragments = {
{ symbol .. ' ', highlight = highlight },
{ tostring(diag.lnum) .. ':', highlight = 'Comment' },
{ tostring(diag.col) .. ' ', highlight = 'Comment' },
{ short_message, highlight = highlight },
},
})
idx = idx + 1
end
page_cb(items)
end,
})
end
function diagnostics.select(item)
vim.api.nvim_set_current_buf(item.data.bufnr)
vim.api.nvim_win_set_cursor(0, { item.data.lnum + 1, item.data.col })
vim.diagnostic.open_float()
end
return diagnostics
================================================
FILE: lua/blink/select/providers/lsp/definitions.lua
================================================
================================================
FILE: lua/blink/select/providers/lsp/references.lua
================================================
================================================
FILE: lua/blink/select/providers/lsp/symbols.lua
================================================
local symbols = {}
local function symbols.get_items(opts)
local symbols = {}
local params = vim.lsp.util.make_position_params(opts.winnr)
================================================
FILE: lua/blink/select/providers/recent-commands.lua
================================================
================================================
FILE: lua/blink/select/providers/recent-searches.lua
================================================
--- @class SelectProvider
local recent_searches = {
name = 'Recent Searches',
}
local function reverse(tab)
for i = 1, math.floor(#tab / 2), 1 do
tab[i], tab[#tab - i + 1] = tab[#tab - i + 1], tab[i]
end
return tab
end
function recent_searches.get_items(opts, cb)
local idx = 1
local search_history = vim.fn.searchcount().total > 0 and vim.fn.execute('history search') or ''
local searches = vim.split(search_history, '\n')
-- Remove the header line
table.remove(searches, 1)
table.remove(searches, 1)
reverse(searches)
cb({
page_count = math.ceil(#searches / opts.page_size),
next_page = function(page_cb)
local items = {}
while idx <= #searches and #items < opts.page_size do
local search = searches[idx]
if search ~= '' then
local search_text = search:match('%s+%d+%s+(.+)$')
if search_text then
table.insert(items, {
data = { search = search_text },
fragments = {
{ ' ', highlight = 'Normal' },
{ search_text, highlight = 'String' },
},
})
end
end
idx = idx + 1
end
page_cb(items)
end,
})
end
function recent_searches.select(item)
vim.fn.setreg('/', item.data.search)
vim.cmd('normal! n')
end
return recent_searches
================================================
FILE: lua/blink/select/providers/smart-open.lua
================================================
local DbClient = require('telescope._extensions.smart_open.dbclient')
local config = require('smart-open').config
local get_buffer_list = require('telescope._extensions.smart_open.buffers')
local weights = require('telescope._extensions.smart_open.weights')
local get_finder = require('telescope._extensions.smart_open.finder.finder')
local format_filepath = require('telescope._extensions.smart_open.display.format_filepath')
--- @class SelectProvider
local smart_open = {
name = 'Smart Open',
db = DbClient:new({ path = config.db_filename }),
history = require('telescope._extensions.smart_open.history'),
}
function smart_open.get_items(opts, callback)
local context = {
cwd = vim.fn.getcwd(),
current_buffer = vim.api.nvim_buf_get_name(opts.bufnr),
-- might be wrong if the select buffer is already open
alternate_buffer = opts.alternate_bufnr > 0 and vim.api.nvim_buf_get_name(opts.alternate_bufnr) or '',
open_buffers = get_buffer_list(),
weights = smart_open.db:get_weights(weights.default_weights),
path_display = true,
}
local finder_opts = {
cwd = context.cwd,
cwd_only = config.cwd_only,
ignore_patterns = config.ignore_patterns,
show_scores = config.show_scores,
match_algorithm = config.match_algorithm,
filename_first = true,
}
local finder = get_finder(smart_open.history, finder_opts, context)
local seen_paths = {}
local items = {}
finder('', function(entry)
if seen_paths[entry.path] then return end
seen_paths[entry.path] = true
local symbol = entry.scores.alt > 0 and config.open_buffer_indicators.previous
or entry.buf and config.open_buffer_indicators.others
or ' '
local icon, icon_hl = require('nvim-web-devicons').get_icon(entry.path, nil, { default = true })
local result, hl_group = format_filepath(entry.path, entry.virtual_name, finder_opts, 60)
local filename = result:sub(1, hl_group[1][1])
local directory = result:sub(hl_group[1][1] + 1)
table.insert(items, {
data = entry,
fragments = {
{ symbol .. ' ' },
{ icon .. ' ', highlight = icon_hl },
{ filename },
{ directory, highlight = 'Directory' },
},
})
end, function()
local page_count = math.ceil(#items / opts.page_size)
local page = 0
callback({
next_page = function(cb)
-- no more pages
if page >= page_count then return cb({}) end
local start_idx = (page * opts.page_size + 1)
local end_idx = (page + 1) * opts.page_size
local page_items = vim.list_slice(items, start_idx, end_idx)
page = page + 1
cb(page_items)
end,
page_count = page_count,
})
end)
end
function smart_open.select(item)
if item.data.bufnr ~= nil and vim.api.nvim_buf_is_valid(item.data.bufnr) then
vim.api.nvim_set_current_buf(item.data.bufnr)
else
vim.cmd('edit ' .. item.data.path)
end
vim.defer_fn(function() smart_open.history:record_usage(item.data.path, false) end, 10)
end
return smart_open
================================================
FILE: lua/blink/select/providers/yank-history.lua
================================================
--- @class SelectProvider
local yank_history = {
name = 'Yank History',
}
-- Initialize yank history table
local history = {}
local max_history = 50 -- Maximum number of items to keep in history
-- Function to add item to yank history
local function add_to_history(text)
-- Remove duplicate if exists
for i, item in ipairs(history) do
if item.text == text then
table.remove(history, i)
break
end
end
-- Add new item to the beginning
table.insert(history, 1, { text = text, timestamp = os.time() })
-- Trim history if it exceeds max_history
if #history > max_history then table.remove(history) end
end
-- Set up autocmd to track yanks
vim.api.nvim_create_autocmd('TextYankPost', {
callback = function()
local text = vim.fn.getreg('"')
add_to_history(text)
end,
})
function yank_history.get_items(opts, cb)
local idx = 1
cb({
page_count = math.ceil(#history / opts.page_size),
next_page = function(page_cb)
local items = {}
while idx <= #history and #items < opts.page_size do
local item = history[idx]
local text = item.text:gsub('[\n\r]', ' ') -- Replace newlines with spaces
local truncated_text = #text > 50 and text:sub(1, 47) .. '...' or text
local time_diff = os.difftime(os.time(), item.timestamp)
local time_str = time_diff < 60 and 'just now'
or time_diff < 3600 and string.format('%d min ago', math.floor(time_diff / 60))
or time_diff < 86400 and string.format('%d hours ago', math.floor(time_diff / 3600))
or string.format('%d days ago', math.floor(time_diff / 86400))
table.insert(items, {
data = { text = item.text },
fragments = {
{ truncated_text, highlight = 'Normal' },
' ',
{ time_str, highlight = 'Comment' },
},
})
idx = idx + 1
end
page_cb(items)
end,
})
end
function yank_history.select(item)
vim.fn.setreg('"', item.data.text)
vim.api.nvim_put(vim.split(item.data.text, '\n'), '', true, true)
end
function yank_history.alt_select(item)
vim.fn.setreg('"', item.data.text)
vim.api.nvim_put(vim.split(item.data.text, '\n'), '', false, true)
end
return yank_history
================================================
FILE: lua/blink/select/renderer.lua
================================================
local api = vim.api
local config = require('blink.select.config')
local renderer = {}
function renderer.new(bufnr)
local self = setmetatable({}, { __index = renderer })
self.bufnr = bufnr
return self
end
--- @param fragments RenderFragment[]
function renderer:draw_line(fragments, line_number)
-- render text
local texts = {}
for _, fragment in ipairs(fragments) do
table.insert(texts, type(fragment) == 'string' and fragment or fragment[1])
end
api.nvim_buf_set_lines(self.bufnr, line_number, line_number + 1, false, { table.concat(texts) })
-- render highlights
local char = 0
for fragment_idx, fragment in ipairs(fragments) do
if fragment.highlight ~= nil then
api.nvim_buf_add_highlight(self.bufnr, 0, fragment.highlight, line_number, char, char + #texts[fragment_idx])
end
char = char + #texts[fragment_idx]
end
end
function renderer.draw(self, items) end
================================================
FILE: lua/blink/select/types.lua
================================================
--- @class RenderFragment
--- @field [1] string
--- @field highlight? string
---
--- @class SelectItem
--- @field fragments (RenderFragment | string)[]
--- @field data any
---
--- @class GetItemsOptions
--- @field winnr number
--- @field bufnr number
--- @field alternate_bufnr number
--- @field page_size number
---
--- @class GetItemsResponse
--- @field next_page fun(cb: fun(items: SelectItem[])): nil
--- @field page_count number | nil
---
--- @class SelectProvider
--- @field name string Human readable name of the provider
--- @field get_items fun(opts: GetItemsOptions, cb: fun(response: GetItemsResponse)): nil
--- @field select fun(item: SelectItem): any
--- @field alt_select? fun(item: SelectItem): any
================================================
FILE: lua/blink/select/window.lua
================================================
local window = {
bufnr = -1,
winnr = -1,
}
local api = vim.api
local augroup = vim.api.nvim_create_augroup('BlinkSelectWindow', { clear = true })
local config = require('blink.select.config')
local function center_text(text, width)
local padding = math.floor(width / 2 - vim.fn.strdisplaywidth(text) / 2)
return string.rep(' ', padding) .. text
end
-- hide the cursor when window is focused
local prev_cursor
local prev_blend
api.nvim_create_autocmd('BufEnter', {
group = augroup,
callback = function()
if vim.bo.filetype == 'blink-select' and prev_cursor == nil then
prev_cursor = api.nvim_get_option_value('guicursor', {})
api.nvim_set_option_value('guicursor', 'a:Cursor/lCursor', {})
local cursor_hl = api.nvim_get_hl(0, { name = 'Cursor' })
prev_blend = cursor_hl.blend
api.nvim_set_hl(0, 'Cursor', vim.tbl_extend('force', cursor_hl, { blend = 100 }))
end
end,
})
api.nvim_create_autocmd('BufLeave', {
group = augroup,
callback = function()
if prev_cursor ~= nil then
api.nvim_set_option_value('guicursor', prev_cursor, {})
prev_cursor = nil
local cursor_hl = api.nvim_get_hl(0, { name = 'Cursor' })
api.nvim_set_hl(0, 'Cursor', vim.tbl_extend('force', cursor_hl, { blend = prev_blend or 0 }))
prev_blend = nil
end
end,
})
local function width_clamp(desired_width)
local max_width_columns = config.window.max_width[1]
local max_width_percent = config.window.max_width[2]
local max_width = math.floor(math.min(max_width_columns, vim.o.columns * max_width_percent))
local min_width_columns = config.window.min_width[1]
local min_width_percent = config.window.min_width[2]
local min_width = math.ceil(math.max(min_width_columns, vim.o.columns * min_width_percent))
return math.max(math.min(desired_width, max_width), min_width)
end
function window.show(provider)
window.prev_bufnr = vim.api.nvim_get_current_buf()
window.provider = provider
provider.get_items({
page_size = #config.mapping.selection,
bufnr = window.prev_bufnr,
alternate_bufnr = vim.fn.bufnr('#'),
}, function(result)
window.provider_next_page = result.next_page
window.page_count = result.page_count
window.pages = {}
window.page_idx = 0
vim.schedule(function()
window.open()
window.next_page()
end)
end)
end
function window.get_buf()
if window.bufnr ~= nil and api.nvim_buf_is_valid(window.bufnr) then return window.bufnr end
window.bufnr = api.nvim_create_buf(false, true)
api.nvim_set_option_value('buftype', 'nofile', { buf = window.bufnr })
api.nvim_set_option_value('filetype', 'blink-select', { buf = window.bufnr })
api.nvim_set_option_value('swapfile', false, { buf = window.bufnr })
api.nvim_set_option_value('modifiable', false, { buf = window.bufnr })
window.setup_mapping(window.bufnr)
return window.bufnr
end
function window.open()
if window.is_open() then return window.winnr end
-- open window
window.winnr = api.nvim_open_win(window.get_buf(), true, {
relative = 'editor',
width = 30,
height = 10,
row = 0,
col = 0,
style = 'minimal',
-- border = { ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' },
border = 'single',
title = ' ' .. window.provider.name .. ' ',
title_pos = 'center',
})
api.nvim_set_option_value(
'winhighlight',
'Normal:Normal,FloatBorder:Normal,FloatTitle:Normal',
{ win = window.winnr }
)
api.nvim_set_option_value('wrap', config.window.wrap, { win = window.winnr })
api.nvim_create_autocmd('WinLeave', {
group = augroup,
callback = function()
if window.winnr == vim.api.nvim_get_current_win() then window.close() end
end,
})
return window.winnr
end
function window.is_open() return window.winnr ~= nil and api.nvim_win_is_valid(window.winnr) end
function window.setup_mapping(bufnr)
local mapping = config.mapping
local map = function(lhs, rhs)
api.nvim_buf_set_keymap(bufnr, 'n', lhs, '', {
callback = rhs,
nowait = true,
})
end
for idx, key in ipairs(mapping.selection) do
if key:upper() == key then error('Selection keys must be lowercase') end
map(key, function() window.select(idx) end)
map(key:upper(), function() window.alt_select(idx) end)
end
for _, key in ipairs(mapping.quit) do
map(key, window.close)
end
for _, key in ipairs(mapping.next_page) do
map(key, window.next_page)
end
for _, key in ipairs(mapping.prev_page) do
map(key, window.prev_page)
end
end
function window.render(items)
api.nvim_set_option_value('modifiable', true, { buf = window.bufnr })
local bufnr = window.get_buf()
api.nvim_buf_set_lines(bufnr, 0, -1, false, { '' })
for line_number, item in ipairs(items) do
-- add the key to the fragments to render
local key = config.mapping.selection[line_number]
local fragments = { { ' ' .. key .. ' ', highlight = 'Primary' } }
for _, fragment in ipairs(item.fragments) do
table.insert(fragments, fragment)
end
-- render text
local texts = {}
for _, fragment in ipairs(fragments) do
table.insert(texts, type(fragment) == 'string' and fragment or fragment[1])
end
api.nvim_buf_set_lines(bufnr, line_number, line_number + 1, false, { table.concat(texts) })
-- render highlights
local char = 0
for fragment_idx, fragment in ipairs(fragments) do
if fragment.highlight ~= nil then
api.nvim_buf_add_highlight(bufnr, 0, fragment.highlight, line_number, char, char + #texts[fragment_idx])
end
char = char + #texts[fragment_idx]
end
end
-- set window width to fit longest line
local max_width = 0
for _, line in ipairs(api.nvim_buf_get_lines(bufnr, 0, -1, false)) do
max_width = math.max(max_width, #line)
end
local width = width_clamp(max_width)
api.nvim_win_set_width(window.winnr, width)
-- add the group separators
for i = 1, math.floor(#items / config.window.group_size) - (math.fmod(#items, config.window.group_size) == 0 and 1 or 0) do
local line = center_text(string.rep('─', math.fmod(width, 2) == 1 and 5 or 4), width)
local line_number = i * config.window.group_size + i
api.nvim_buf_set_lines(bufnr, line_number, line_number, false, { line })
end
-- add the bottom line with next/prev page mapping, current page, page count
local page_info = center_text(string.format('Page %d/%d', window.page_idx, window.page_count), width)
api.nvim_buf_set_lines(bufnr, -1, -1, false, { '', page_info })
-- set window height to fit number of lines
api.nvim_win_set_height(window.winnr, vim.api.nvim_buf_line_count(bufnr))
-- center window on screen
local win_width = api.nvim_win_get_width(window.winnr)
local win_height = api.nvim_win_get_height(window.winnr)
local screen_width = vim.o.columns
local screen_height = vim.o.lines - vim.o.cmdheight
local row = math.floor((screen_height - win_height) / 2)
local col = math.floor((screen_width - win_width) / 2)
api.nvim_win_set_config(window.winnr, { relative = 'editor', row = row, col = col })
api.nvim_set_option_value('modifiable', false, { buf = window.bufnr })
end
function window.close() api.nvim_win_close(0, false) end
function window.select(idx)
window.close()
window.provider.select(window.pages[window.page_idx][idx])
end
function window.alt_select(idx)
window.close()
local select_func = window.provider.alt_select or window.provider.select
select_func(window.pages[window.page_idx][idx])
end
function window.next_page()
if window.page_idx + 1 > #window.pages then
return window.provider_next_page(function(next_page)
-- no data, close if we're on the first page and notify user
if #next_page == 0 then
if window.page_idx == 0 then
vim.notify('No data from provider', vim.log.levels.INFO)
window.close()
end
return
end
table.insert(window.pages, next_page)
window.page_idx = window.page_idx + 1
window.render(window.pages[window.page_idx])
end)
end
window.page_idx = window.page_idx + 1
window.render(window.pages[window.page_idx])
end
function window.prev_page()
if window.page_idx - 1 < 1 then return end
window.page_idx = window.page_idx - 1
window.render(window.pages[window.page_idx])
end
return window
================================================
FILE: lua/blink/tree/binds/activate.lua
================================================
local api = vim.api
local function activate(hovered_node, inst)
if hovered_node == nil then return end
-- todo: use existing buffer if available
-- dir: toggled expanded
if hovered_node.is_dir then
if hovered_node.expanded then
inst.tree:collapse(hovered_node)
else
inst.tree:expand(hovered_node)
end
-- file: open
else
local winnr = require('blink.tree.lib.utils').pick_or_create_non_special_window()
api.nvim_set_current_win(winnr)
local bufnr = vim.fn.bufnr(hovered_node.path)
if bufnr ~= -1 then
api.nvim_set_current_buf(bufnr)
else
vim.cmd('edit ' .. hovered_node.path)
end
end
end
return activate
================================================
FILE: lua/blink/tree/binds/basic.lua
================================================
local Basic = {}
function Basic.new_file(hovered_node, inst)
while hovered_node ~= nil and hovered_node.is_dir == false do
hovered_node = hovered_node.parent
end
if hovered_node == nil then return end
local popup = require('blink.tree.popup')
local fs = require('blink.tree.lib.fs')
popup.new_input({ title = 'New File (append / for dir)', title_pos = 'center' }, function(input)
if input == nil then return end
local final_path = fs.create_file(hovered_node.path, input)
inst.tree:expand_path(final_path, function() inst.renderer:select_path(final_path) end)
end)
end
function Basic.delete_file(hovered_node)
local uv = require('blink.tree.lib.uv')
uv.exec_async({ command = { 'trash', hovered_node.path } }, function(code)
if code ~= 0 then print('Failed to delete: ' .. hovered_node.path) end
end)
end
function Basic.rename_file(hovered_node, inst)
local popup = require('blink.tree.popup')
local fs = require('blink.tree.lib.fs')
if hovered_node == inst.tree.root then vim.print('Cannot rename root') end
popup.new_input({ title = 'Rename', title_pos = 'center', initial_text = hovered_node.filename }, function(input)
if input == nil then return end
local new_path = hovered_node.parent.path .. '/' .. input
-- FIXME: would break if they rename the top level dir
fs.rename(hovered_node.path, new_path)
inst.tree:expand_path(new_path, function() inst.renderer:select_path(new_path) end)
end)
end
return Basic
================================================
FILE: lua/blink/tree/binds/expand.lua
================================================
local function expand(hovered_node, inst)
if hovered_node == nil then return end
if hovered_node.is_dir then
if hovered_node.expanded then
inst.tree:collapse(hovered_node)
else
inst.tree:expand(hovered_node)
end
else
return
end
end
return expand
================================================
FILE: lua/blink/tree/binds/init.lua
================================================
local api = vim.api
local Binds = {}
function Binds.attach_to_instance(inst)
local function map(mode, lhs, callback, opts)
opts = opts or {}
opts.callback = function() callback(inst.renderer:get_hovered_node(), inst) end
api.nvim_buf_set_keymap(inst.bufnr, mode, lhs, '', opts)
end
map('n', 'q', function() inst:close() end)
map('n', 'R', function() inst.tree:refresh() end)
local activate = require('blink.tree.binds.activate')
local expand = require('blink.tree.binds.expand')
map('n', '<CR>', activate)
map('n', '<2-LeftMouse>', activate)
map('n', '<Tab>', expand)
local basic = require('blink.tree.binds.basic')
map('n', 'a', basic.new_file)
map('n', 'd', basic.delete_file)
map('n', 'r', basic.rename_file)
local move = require('blink.tree.binds.move')
map('n', 'x', move.cut)
map('n', 'y', move.copy)
map('n', 'p', move.paste)
end
return Binds
================================================
FILE: lua/blink/tree/binds/move.lua
================================================
local Move = {}
function Move.cut(node, inst)
node.flags.copy = false
node.flags.cut = not node.flags.cut
inst.renderer:redraw()
end
function Move.copy(node, inst)
node.flags.cut = false
node.flags.copy = not node.flags.copy
inst.renderer:redraw()
end
function Move.paste(hovered_node, inst)
while hovered_node ~= nil and hovered_node.is_dir == false do
hovered_node = hovered_node.parent
end
if hovered_node == nil then return end
local lib_tree = require('blink.tree.lib.tree')
local cut_nodes = {}
local copied_nodes = {}
lib_tree.traverse(inst.tree.root, function(node)
if node.flags.cut then
table.insert(cut_nodes, node)
elseif node.flags.copy then
table.insert(copied_nodes, node)
end
end)
local fs = require('blink.tree.lib.fs')
for _, cut_node in ipairs(cut_nodes) do
fs.rename(cut_node.path, hovered_node.path .. '/' .. cut_node.filename)
cut_node.flags.cut = false
end
for _, copied_node in ipairs(copied_nodes) do
fs.copy_file(copied_node.path, hovered_node.path .. '/' .. copied_node.filename)
copied_node.flags.copy = false
end
inst.renderer:redraw()
end
return Move
================================================
FILE: lua/blink/tree/config.lua
================================================
local M = {}
M.default = {
hidden_by_default = true,
hide_dotfiles = false,
hide = {
'.direnv',
'.devenv',
},
never_show = {
'.git',
'.cache',
'node_modules',
},
}
function M.setup(opts)
opts = vim.tbl_deep_extend('force', M.default, opts or {})
M.config = opts
end
return setmetatable(M, { __index = function(_, k) return M.config[k] end })
================================================
FILE: lua/blink/tree/git/git2.lua
================================================
-- Made by SuperBo in Fugit2
-- https://github.com/SuperBo/fugit2.nvim/tree/70662d529fe98790d7b2104b4dd67dd229332194
-- Licensed under MIT
local ffi = require "ffi"
local libgit2 = require "blink.tree.git.libgit2"
local stat = require "blink.tree.git.stat"
local table_new = require "table.new"
local uv = vim.uv or vim.loop
--- Libgit2 init counter
local libgit2_init_count = 0
---@type string?
local libgit2_library_path
-- ========================
-- | Libgit2 Enum section |
-- ========================
local GIT_REFERENCE_STRING = {
"INVALID",
"DIRECT",
"SYMBOLIC",
"DIRECT/SYMBOLIC",
}
---@enum GIT_REFERENCE_NAMESPACE
local GIT_REFERENCE_NAMESPACE = {
NONE = 0, -- Normal ref, no namespace
BRANCH = 1, -- Reference is in Branch namespace
TAG = 2, -- Reference is in Tag namespace
REMOTE = 3, -- Reference is in Remote namespace
NOTE = 4, -- Reference is in Note namespace
}
local GIT_REFERENCE_PREFIX = {
[GIT_REFERENCE_NAMESPACE.BRANCH] = string.len "refs/heads/" + 1,
[GIT_REFERENCE_NAMESPACE.TAG] = string.len "refs/tags/" + 1,
[GIT_REFERENCE_NAMESPACE.REMOTE] = string.len "refs/remotes/" + 1,
[GIT_REFERENCE_NAMESPACE.NOTE] = string.len "refs/notes/" + 1,
}
local GIT_DELTA_STRING = {
"UNMODIFIED",
"ADDED",
"DELETED",
"MODIFIED",
"RENAMED",
"COPIED",
"IGNORED",
"UNTRACKED",
"TYPECHANGE",
"UNREADABLE",
"CONFLICTED",
}
-- ===================
-- | Macro functions |
-- ===================
---@param mode integer
---@return boolean
local function GIT_PERMS_IS_EXEC(mode)
return bit.band(mode, 64) ~= 0
end
---@param mode integer
---@return integer
local function GIT_PERMS_CANONICAL(mode)
return GIT_PERMS_IS_EXEC(mode) and 493 or 420
end
---@param mode integer
---@return integer
local function GIT_PERMS_FOR_WRITE(mode)
return GIT_PERMS_IS_EXEC(mode) and 511 or 438
end
-- =====================
-- | Class definitions |
-- =====================
---@class GitConfig
---@field config ffi.cdata* libgit2 struct git_config*
local Config = {}
Config.__index = Config
---@class GitConfigEntry
---@field name string
---@field value string
---@field include_depth integer
---@field level GIT_CONFIG_LEVEL
---@class GitRepository
---@field repo ffi.cdata* libgit2 struct git_repository*
---@field path string git repository path
local Repository = {}
Repository.__index = Repository
---@class GitObject
---@field obj ffi.cdata* libgit2 struct git_object*
local Object = {}
Object.__index = Object
---@class GitObjectId
---@field oid ffi.cdata* libgit2 git_oid struct
local ObjectId = {}
ObjectId.__index = ObjectId
---@class GitBlob
---@field blob ffi.cdata* libgit2 struct git_blob**
local Blob = {}
Blob.__index = Blob
---@class GitTree
---@field tree ffi.cdata* libgit2.git_tree_pointer
local Tree = {}
Tree.__index = Tree
---@class GitTreeEntry
---@field entry ffi.cdata* libgit2 git_tree_entry*
local TreeEntry = {}
TreeEntry.__index = TreeEntry
---@class GitBlame
---@field blame ffi.cdata* libgit2 git_blame *
local Blame = {}
Blame.__index = Blame
---@class GitBlameHunk
---@field hunk ffi.cdata* libgit2 git_blame_hunk *
---@field num_lines integer
---@field final_start_line_number integer
---@field orig_start_line_number integer
---@field boundary boolean true iff the hunk has been tracked to a boundary commit.
local BlameHunk = {}
BlameHunk.__index = BlameHunk
---@class GitBlameOptions
---@field first_parent boolean? reachable following only the first parents.
---@field use_mailmap boolean? use mailmap file to map author and committer names and email.
---@field ignore_whitespace boolean? ignore whitespace differences
---@field min_match_characters integer? default value is 20
---@field newest_commit GitObjectId? id of the newest commit to consider.
---@field oldest_commit GitObjectId? id of the oldest commit to consider.
---@field min_line integer? The first line in the file to blame, 1-th based.
---@field max_line integer? last line in the file to blame. The default is the last line of the file.
---@class GitCommit
---@field commit ffi.cdata* libgit2 git_commit pointer
local Commit = {}
Commit.__index = Commit
---@class GitAnnotatedCommit
---@field commit ffi.cdata* libgit2 git_annotated_commit pointer
local AnnotatedCommit = {}
AnnotatedCommit.__index = AnnotatedCommit
---@class GitTag
---@field tag ffi.cdata* libgit2 git_tag pointer
---@field name string Git Tag name
local Tag = {}
Tag.__index = Tag
---@class GitReference
---@field ref ffi.cdata* libgit2 git_reference type
---@field name string Reference Refs full name
---@field type GIT_REFERENCE Reference type
---@field namespace GIT_REFERENCE_NAMESPACE Reference namespace if available
local Reference = {}
Reference.__index = Reference
---@class GitIndexEntry
---@field entry ffi.cdata* libgit2 git_index_entry *
local IndexEntry = {}
IndexEntry.__index = IndexEntry
---@class GitIndex
---@field index ffi.cdata* libgit2 struct git_index*[1]
local Index = {}
Index.__index = Index
---@class GitRemote
---@field remote ffi.cdata* libgit2 struct git_remote*[1]
---@field name string
---@field url string
---@field push_url string?
local Remote = {}
Remote.__index = Remote
---@class GitRevisionWalker
---@field repo ffi.cdata* libgit2 struct git_repository*
---@field revwalk ffi.cdata* libgit2 struct git_revwalk*[1]
local RevisionWalker = {}
RevisionWalker.__index = RevisionWalker
---@class GitSignature
---@field sign ffi.cdata* libgit2.git_signature_pointer
local Signature = {}
Signature.__index = Signature
---@class GitPatch
---@field patch ffi.cdata* libgit2 git_patch*
local Patch = {}
Patch.__index = Patch
---@class GitDiff
---@field diff ffi.cdata* libgit2 git_diff*
local Diff = {}
Diff.__index = Diff
---@class GitDiffHunk
---@field num_lines integer
---@field old_start integer
---@field old_lines integer
---@field new_start integer
---@field new_lines integer
---@field header string
local DiffHunk = {}
---@class GitDiffLine
---@field origin string
---@field old_lineno integer
---@field new_lineno integer
---@field num_lines integer
---@field content string
---@class GitRebase
---@field rebase ffi.cdata* libgit2 git_rebase* pointer
local Rebase = {}
Rebase.__index = Rebase
---@class GitRebaseOperation
---@field operation ffi.cdata* libgit2 git_rebase_operation pointer
local RebaseOperation = {}
RebaseOperation.__index = RebaseOperation
-- ========================
-- | Git config functions |
-- ========================
---Inits git config
function Config.new(git_config)
local config = { config = libgit2.git_config_pointer(git_config) }
setmetatable(config, Config)
ffi.gc(config.config, libgit2.C.git_config_free)
return config
end
---Open the global, XDG and system configuration files
---@return GitConfig?
---@return GIT_ERROR
function Config.open_default()
local git_config = libgit2.git_config_double_pointer()
local err = libgit2.C.git_config_open_default(git_config)
if err ~= 0 then
return nil, err
end
return Config.new(git_config[0]), 0
end
---Build a single-level focused config object from a multi-level one
---@param level GIT_CONFIG_LEVEL
---@return GitConfig?
---@return GIT_ERROR
function Config:open_level(level)
local git_config = libgit2.git_config_double_pointer()
local err = libgit2.C.git_config_open_level(git_config, self.config, level)
if err ~= 0 then
return nil, err
end
return Config.new(git_config[0]), 0
end
---Get the value of a long integer config variable.
---@param name string config name
---@return integer?
---@return GIT_ERROR
function Config:get_int(name)
local out = libgit2.int64_array(1)
local err = libgit2.C.git_config_get_int64(out, self.config, name)
if err ~= 0 then
return nil, 0
end
return tonumber(out[0]), 0
end
---Get the value of a boolean config variable.
---@param name string config name
---@return boolean?
---@return GIT_ERROR
function Config:get_bool(name)
local out = libgit2.int_array(1)
local err = libgit2.C.git_config_get_bool(out, self.config, name)
if err ~= 0 then
return nil, 0
end
return (out ~= 0), 0
end
---Get the value of a string config variable.
---@param name string config name
---@return string?
---@return GIT_ERROR
function Config:get_string(name)
local buf = libgit2.git_buf()
local err = libgit2.C.git_config_get_string_buf(buf, self.config, name)
if err ~= 0 then
libgit2.C.git_buf_dispose(buf)
return nil, err
end
local str = ffi.string(buf[0].ptr, buf[0].size)
libgit2.C.git_buf_dispose(buf)
return str, 0
end
---Get all entries
---@return GitConfigEntry[]?
---@return GIT_ERROR
function Config:entries()
local iter = libgit2.git_config_iterator_double_pointer()
local err = libgit2.C.git_config_iterator_new(iter, self.config)
if err ~= 0 then
return nil, err
end
local git_config_entry = libgit2.git_config_entry_double_pointer()
local entries = {}
while libgit2.C.git_config_next(git_config_entry, iter[0]) == 0 do
---@type GitConfigEntry
local entry = {
name = ffi.string(git_config_entry[0].name),
value = ffi.string(git_config_entry[0].value),
include_depth = tonumber(git_config_entry[0].include_depth) or -1,
level = tonumber(git_config_entry[0].level) or -1,
}
entries[#entries + 1] = entry
-- libgit2.C.git_config_entry_free(git_config_entry[0])
end
libgit2.C.git_config_iterator_free(iter[0])
return entries, 0
end
-- ========================
-- | Git Object functions |
-- ========================
---@param git_object ffi.cdata* libgit2.git_object_pointer, own cdata.
---@return GitObject
function Object.new(git_object)
local object = { obj = libgit2.git_object_pointer(git_object) }
setmetatable(object, Object)
ffi.gc(object.obj, libgit2.C.git_object_free)
return object
end
function Object.borrow(git_object)
local object = { obj = libgit2.git_object_pointer(git_object) }
setmetatable(object, Object)
return object
end
-- Get the id (SHA1) of a repository object.
---@return GitObjectId
function Object:id()
local oid = libgit2.C.git_object_id(self.obj[0])
return ObjectId.borrow(oid)
end
-- Cast GitObject to GitBlob, the reference still be owned by main GitObject.
---@return GitBlob
function Object:as_blob()
return Blob.borrow(ffi.cast(libgit2.git_blob_pointer, self.obj))
end
-- Lookups an object that represents a tree entry from this treeish object.
---@param path string relative path from the root object to the desired object.
---@param object_type GIT_OBJECT type of object desired.
---@return GitObject?
---@return GIT_ERROR
function Object:lookup_by_path(path, object_type)
local obj_out = libgit2.git_object_double_pointer()
local err = libgit2.C.git_object_lookup_bypath(obj_out, self.obj, path, object_type)
if err ~= 0 then
return nil, err
end
return Object.new(obj_out[0]), 0
end
-- ======================
-- | ObjectId functions |
-- ======================
---Creates new libgit2 oid, then copy value from old oid.
---@param oid GitObjectId
---@return GitObjectId?
---@return GIT_ERROR
function ObjectId.from(oid)
local git_object_id = libgit2.git_oid()
local err = libgit2.C.git_oid_cpy(git_object_id, oid.oid)
if err ~= 0 then
return nil, err
end
return ObjectId.borrow(git_object_id), 0
end
---Creates new libgit2 oid, then copy value from old oid.
---@param git_oid ffi.cdata*
---@return GitObjectId? Objectid
---@return GIT_ERROR
function ObjectId.from_git_oid(git_oid)
local git_object_id = libgit2.git_oid()
local err = libgit2.C.git_oid_cpy(git_object_id, git_oid)
if err ~= 0 then
return nil, err
end
return ObjectId.borrow(git_object_id), 0
end
-- Creates new ObjectId from string
---@param oid_str string oid hex string
---@return GitObjectId?
---@return GIT_ERROR
function ObjectId.from_string(oid_str)
local git_object_id = libgit2.git_oid()
local err = libgit2.C.git_oid_fromstrn(git_object_id, oid_str, oid_str:len())
if err ~= 0 then
return nil, err
end
return ObjectId.borrow(git_object_id), 0
end
---@param oid ffi.cdata* libgit2 git_oid*, borrow data
function ObjectId.borrow(oid)
local object_id = { oid = ffi.cast(libgit2.git_oid_pointer, oid) }
setmetatable(object_id, ObjectId)
return object_id
end
---Creates a new ObjectId with the same value of the old one.
---@return GitObjectId?
---@return GIT_ERROR
function ObjectId:clone()
return ObjectId.from(self)
end
---Sets this oid the same value as the given oid
---@param oid GitObjectId
---@return GIT_ERROR
function ObjectId:copy_from(oid)
return libgit2.C.git_oid_cpy(self.oid, oid.oid)
end
---Copies this oid to target oid.
---@param oid GitObjectId
---@return GIT_ERROR
function ObjectId:copy_to(oid)
return libgit2.C.git_oid_cpy(oid.oid, self.oid)
end
---@param n integer? number of git id
---@return string
function ObjectId:tostring(n)
if not n or n < 0 or n > 40 then
n = 40
end
local c_buf = libgit2.char_array(n + 1)
libgit2.C.git_oid_tostr(c_buf, n + 1, self.oid)
return ffi.string(c_buf, n)
end
---@param oid_str string hex formatted object id.
---@return boolean
function ObjectId:streq(oid_str)
return (libgit2.C.git_oid_streq(self.oid, oid_str) == 0)
end
---@return string
function ObjectId:__tostring()
return self:tostring(8)
end
---@param a GitObjectId
---@param b GitObjectId | string
---@return boolean
function ObjectId.__eq(a, b)
if type(b) == "string" then
return (libgit2.C.git_oid_streq(a.oid, b) == 0)
end
return (libgit2.C.git_oid_equal(a.oid, b.oid) ~= 0)
end
-- ===================
-- | Git Blob object |
-- ===================
---@param git_blob ffi.cdata* libgit2.git_blob_pointer, own cdata
---@return GitBlob
function Blob.new(git_blob)
local blob = { blob = libgit2.git_blob_pointer(git_blob) }
setmetatable(blob, Blob)
ffi.gc(blob.blob, libgit2.C.git_blob_free)
return blob
end
---@param git_blob ffi.cdata* libgit2.git_blob_pointer, doesn't own data
function Blob.borrow(git_blob)
local blob = { blob = libgit2.git_blob_pointer(git_blob) }
setmetatable(blob, Blob)
return blob
end
---@return GitObjectId
function Blob:id()
local oid = libgit2.C.git_blob_id(self.blob)
return ObjectId.borrow(oid)
end
---@return boolean
function Blob:is_binary()
local ret = libgit2.C.git_blob_is_binary(self.blob)
return ret == 1
end
---Gets a raw content of a blob.
---@return string
function Blob:content()
local content = libgit2.C.git_blob_rawcontent(self.blob)
local len = libgit2.C.git_blob_rawsize(self.blob)
return ffi.string(content, len)
end
-- ============================
-- | Git Tree Entry functions |
-- ============================
---@param git_entry ffi.cdata* libgit2.git_tree_entry_pointer, own cdata
---@return GitTreeEntry
function TreeEntry.new(git_entry)
local tree_entry = { entry = libgit2.git_tree_entry_pointer(git_entry) }
setmetatable(tree_entry, TreeEntry)
ffi.gc(tree_entry.entry, libgit2.C.git_tree_entry_free)
return tree_entry
end
---@param entry ffi.cdata* libgit2.git_tree_entry_pointer, just borrow data, didn't own
---@return GitTreeEntry
function TreeEntry.borrow(entry)
local git_tree_entry = { entry = entry }
setmetatable(git_tree_entry, TreeEntry)
return git_tree_entry
end
---Gets the id of the object pointed by the entry
---@return GitObjectId
function TreeEntry:id()
local oid = libgit2.C.git_tree_entry_id(self.entry)
return ObjectId.borrow(oid)
end
---Gets the filename of a tree entry.
---@return string
function TreeEntry:name()
local c_name = libgit2.C.git_tree_entry_name(self.entry)
return ffi.string(c_name)
end
---@return GIT_OBJECT
function TreeEntry:type()
return libgit2.C.git_tree_entry_type(self.entry)
end
---@param repo GitRepository
---@return GitObject?
---@return GIT_ERROR
function TreeEntry:to_object(repo)
local git_object = libgit2.git_object_double_pointer()
local err = libgit2.C.git_tree_entry_to_object(git_object, repo.repo, self.entry)
if err ~= 0 then
return nil, err
end
return Object.new(git_object[0]), 0
end
-- ===================
-- | Git Tree object |
-- ===================
---@param git_tree ffi.cdata* libgit2.git_tree_pointer, own cdata
---@return GitTree
function Tree.new(git_tree)
local tree = { tree = libgit2.git_tree_pointer(git_tree) }
setmetatable(tree, Tree)
ffi.gc(tree.tree, libgit2.C.git_tree_free)
return tree
end
-- Cast Tree to GitObject, the reference still be owned by main Tree.
---@return GitObject
function Tree:as_object()
return Object.borrow(ffi.cast(libgit2.git_object_pointer, self.tree))
end
-- Gets the id of a tree.
function Tree:id()
local oid = libgit2.C.git_tree_id(self.tree)
return ObjectId.borrow(oid)
end
function Tree:nentries()
return libgit2.C.git_tree_entrycount(self.tree)
end
---Retrieves a tree entry contained in a tree
---or in any of its subtrees, given its relative path.
---@param path string
---@return GitTreeEntry?
---@return GIT_ERROR
function Tree:entry_bypath(path)
local entry = libgit2.git_tree_entry_double_pointer()
local err = libgit2.C.git_tree_entry_bypath(entry, self.tree, path)
if err ~= 0 then
return nil, err
end
return TreeEntry.new(entry[0]), 0
end
---Lookup a tree entry by its filename
---@param filename string
---@return GitTreeEntry?
function Tree:entry_byname(filename)
local entry = libgit2.C.git_tree_entry_byname(self.tree, filename)
if entry == nil then
return nil
end
return TreeEntry.borrow(entry)
end
---Lookup a tree entry by SHA value.
---@param id GitObjectId
---@return GitTreeEntry?
function Tree:entry_byid(id)
local entry = libgit2.C.git_tree_entry_byid(self.tree, id.oid)
if entry == nil then
return nil
end
return TreeEntry.borrow(entry)
end
-- ==================
-- | Git Tag object |
-- ==================
---@param git_tag ffi.cdata* libgit2.git_tag_pointer, own cdata
---@return GitTag
function Tag.new(git_tag)
local tag = { tag = libgit2.git_tag_pointer(git_tag) }
setmetatable(tag, Tag)
tag.name = ffi.string(libgit2.C.git_tag_name(git_tag))
ffi.gc(tag.tag, libgit2.C.git_tag_free)
return tag
end
---Get the name of a tag
function Tag:__tostring()
return self.name
end
-- =============================
-- | AnnotatedCommit functions |
-- =============================
---Init GitAnnotatedCommit
---@param git_commit ffi.cdata* libgit2.git_annotated_commit_pointer, this owns cdata.
---@return GitAnnotatedCommit
function AnnotatedCommit.new(git_commit)
local commit = { commit = libgit2.git_annotated_commit_pointer(git_commit) }
setmetatable(commit, AnnotatedCommit)
ffi.gc(commit.commit, libgit2.C.git_annotated_commit_free)
return commit
end
-- =================
-- | Blame methods |
-- =================
-- Inits GitBlame object
function Blame.new(git_blame)
local blame = { blame = libgit2.git_blame_pointer(git_blame) }
setmetatable(blame, Blame)
ffi.gc(blame.blame, libgit2.C.git_blame_free)
return blame
end
-- Gets blame data for a file that has been modified in memory
-- Self is the pre-calculated blame for the in-odb history of the file.
---@param buf string
---@return GitBlame?
---@return GIT_ERROR
function Blame:blame_buffer(buf)
local git_blame = libgit2.git_blame_double_pointer()
local err = libgit2.C.git_blame_buffer(git_blame, self.blame, buf, buf:len())
if err ~= 0 then
return nil, err
end
return Blame.new(git_blame[0]), 0
end
---@return integer
function Blame:nhunks()
local count = libgit2.C.git_blame_get_hunk_count(self.blame)
return tonumber(count) or 0
end
---@param i integer hunk index
---@return GitBlameHunk?
function Blame:hunk(i)
local blame = libgit2.C.git_blame_get_hunk_byindex(self.blame, i)
return blame ~= nil and BlameHunk.borrow(blame) or nil
end
---@param line integer line number, 1-th based index
---@return GitBlameHunk?
function Blame:hunk_byline(line)
local blame = libgit2.C.git_blame_get_hunk_byline(self.blame, line)
return blame ~= nil and BlameHunk.borrow(blame) or nil
end
-- =====================
-- | BlameHunk methods |
-- =====================
---@return GitBlameHunk
function BlameHunk.borrow(git_blame_hunk)
---@type GitBlameHunk
local hunk = {
hunk = libgit2.git_blame_hunk_pointer(git_blame_hunk),
num_lines = tonumber(git_blame_hunk["lines_in_hunk"]) or 0,
final_start_line_number = tonumber(git_blame_hunk["final_start_line_number"]) or 0,
orig_start_line_number = tonumber(git_blame_hunk["orig_start_line_number"]) or 0,
boundary = (git_blame_hunk["boundary"] == 1),
}
setmetatable(hunk, BlameHunk)
return hunk
end
---@return GitSignature?
function BlameHunk:final_signature()
local sig = self.hunk["final_signature"]
return sig ~= nil and Signature.borrow(sig) or nil
end
---@return GitSignature?
function BlameHunk:orig_signature()
local sig = self.hunk["orig_signature"]
return sig ~= nil and Signature.borrow(sig) or nil
end
---@return GitObjectId
function BlameHunk:final_commit_id()
return ObjectId.borrow(self.hunk["final_commit_id"])
end
---@return GitObjectId
function BlameHunk:orig_commit_id()
return ObjectId.borrow(self.hunk["orig_commit_id"])
end
---@return string
function BlameHunk:orig_path()
return ffi.string(self.hunk["orig_path"])
end
-- ====================
-- | Commit functions |
-- ====================
-- Init GitCommit.
---@param git_commit ffi.cdata* libgit2.git_commit_pointer, this owns the data.
---@return GitCommit
function Commit.new(git_commit)
local commit = { commit = libgit2.git_commit_pointer(git_commit) }
setmetatable(commit, Commit)
-- ffi garbage collector
ffi.gc(commit.commit, libgit2.C.git_commit_free)
return commit
end
-- Gets the id of a commit.
---@return GitObjectId
function Commit:id()
local git_oid = libgit2.C.git_commit_id(self.commit)
return ObjectId.borrow(git_oid)
end
---@return osdate
function Commit:time()
local time = tonumber(libgit2.C.git_commit_time(self.commit))
return os.date("*t", time) --[[@as osdate>]]
end
-- Gets GitCommit messages.
---@return string message
function Commit:message()
local c_char = libgit2.C.git_commit_message(self.commit)
return vim.trim(ffi.string(c_char))
end
-- Gets the short "summary" of the git commit message.
---@return string summary
function Commit:summary()
local c_char = libgit2.C.git_commit_summary(self.commit)
return ffi.string(c_char)
end
-- Gets the body of the git commit message.
---@return string body
function Commit:body()
local c_char = libgit2.C.git_commit_body(self.commit)
if c_char == nil then
return ""
end
return ffi.string(c_char)
end
---@return string
function Commit:author()
local author = libgit2.C.git_commit_author(self.commit)
return ffi.string(author.name)
end
---@return string
function Commit:committer()
local committer = libgit2.C.git_commit_committer(self.commit)
return ffi.string(committer.name)
end
---@return GitSignature
function Commit:author_signature()
local author = libgit2.C.git_commit_author(self.commit)
return Signature.borrow(ffi.cast(libgit2.git_signature_pointer, author))
end
---@return GitSignature
function Commit:committer_signature()
local committer = libgit2.C.git_commit_committer(self.commit)
return Signature.borrow(ffi.cast(libgit2.git_signature_pointer, committer))
end
-- Gets the number of parents of this commit
---@return integer parentcount
function Commit:nparents()
return libgit2.C.git_commit_parentcount(self.commit)
end
-- Gets the specified parent of the commit.
---@param i integer Parent index (0-based)
---@return GitCommit?
---@return GIT_ERROR
function Commit:parent(i)
local c_commit = libgit2.git_commit_double_pointer()
local err = libgit2.C.git_commit_parent(c_commit, self.commit, i)
if err ~= 0 then
return nil, err
end
return Commit.new(c_commit[0]), 0
end
-- Gets the oids of all parents
---@return GitObjectId[]
function Commit:parent_oids()
local nparents = self:nparents()
local parents = table_new(nparents, 0)
if nparents < 1 then
return parents
end
for i = 0, nparents - 1 do
local oid = libgit2.C.git_commit_parent_id(self.commit, i)
parents[i + 1] = ObjectId.borrow(oid)
end
return parents
end
-- Gets the tree pointed to by a commit.
---@return GitTree?
---@return GIT_ERROR
function Commit:tree()
local git_tree = libgit2.git_tree_double_pointer()
local err = libgit2.C.git_commit_tree(git_tree, self.commit)
if err ~= 0 then
return nil, err
end
return Tree.new(git_tree[0]), 0
end
-- =======================
-- | Reference functions |
-- =======================
---@param refname string
---@return GIT_REFERENCE_NAMESPACE
local function reference_name_namespace(refname)
if vim.startswith(refname, "refs/") then
local namespace = string.sub(refname, string.len "refs/" + 1)
if vim.startswith(namespace, "heads/") then
return GIT_REFERENCE_NAMESPACE.BRANCH
elseif vim.startswith(namespace, "tags/") then
return GIT_REFERENCE_NAMESPACE.TAG
elseif vim.startswith(namespace, "remotes/") then
return GIT_REFERENCE_NAMESPACE.REMOTE
elseif vim.startswith(namespace, "notes/") then
return GIT_REFERENCE_NAMESPACE.NOTE
end
end
return GIT_REFERENCE_NAMESPACE.NONE
end
---@param refname string full refname
---@return string
local function reference_name_shorthand(refname)
local namespace = reference_name_namespace(refname)
if namespace ~= GIT_REFERENCE_NAMESPACE.NONE then
return refname:sub(GIT_REFERENCE_PREFIX[namespace])
end
return refname
end
---@param refname string
---@return string?
local function reference_name_remote(refname)
return refname:match "refs/remotes/(%a+)/"
end
---Creates new Reference object
---@param git_reference ffi.cdata* libgit2.git_reference_pointer, own cdata
---@return GitReference
function Reference.new(git_reference)
local ref = {
ref = libgit2.git_reference_pointer(git_reference),
namespace = GIT_REFERENCE_NAMESPACE.NONE,
}
setmetatable(ref, Reference)
local c_name = libgit2.C.git_reference_name(ref.ref)
ref.name = ffi.string(c_name)
ref.namespace = reference_name_namespace(ref.name)
ref.type = libgit2.C.git_reference_type(ref.ref)
-- ffi garbage collector
ffi.gc(ref.ref, libgit2.C.git_reference_free)
return ref
end
function Reference:__tostring()
return string.format("Git Ref (%s): %s", GIT_REFERENCE_STRING[self.type + 1], self.name)
end
-- Transforms the reference name into a name "human-readable" version.
---@return string # Shorthand for ref
function Reference:shorthand()
local c_name = libgit2.C.git_reference_shorthand(self.ref)
return ffi.string(c_name)
end
-- Gets target for a GitReference
---@return GitObjectId?
---@return GIT_ERROR
function Reference:target()
if self.type == libgit2.GIT_REFERENCE.SYMBOLIC then
local resolved = libgit2.git_reference_double_pointer()
local err = libgit2.C.git_reference_resolve(resolved, self.ref)
if err ~= 0 then
return nil, err
end
local oid = libgit2.C.git_reference_target(resolved)
libgit2.C.git_reference_free(resolved)
return ObjectId.borrow(oid), 0
elseif self.type ~= 0 then
local oid = libgit2.C.git_reference_target(self.ref)
return ObjectId.borrow(oid), 0
end
return nil, 0
end
---Conditionally creates a new reference
---with the same name as the given reference.
---@param oid GitObjectId
---@param message string
---@return GitReference?
---@return GIT_ERROR
function Reference:set_target(oid, message)
local ref = libgit2.git_reference_double_pointer()
local err = libgit2.C.git_reference_set_target(ref, self.ref, oid.oid, message)
if err ~= 0 then
return nil, err
end
return Reference.new(ref[0]), 0
end
---Resolves a symbolic reference to a direct reference.
---@return GitReference? ref git reference
---@return GIT_ERROR err error code
function Reference:resolve()
local ref = libgit2.git_reference_double_pointer()
local err = libgit2.C.git_reference_resolve(ref, self.ref)
if err ~= 0 then
return nil, err
end
return Reference.new(ref[0]), 0
end
---Recursively peel reference until object of the specified type is found.
---@param type GIT_OBJECT
---@return GitObject?
---@return integer Git Error code
function Reference:peel(type)
local c_object = libgit2.git_object_double_pointer()
local err = libgit2.C.git_reference_peel(c_object, self.ref, type)
if err ~= 0 then
return nil, err
end
return Object.new(c_object[0]), 0
end
-- Recursively peel reference until commit object is found.
---@return GitCommit?
---@return GIT_ERROR err libgit2 Error code
function Reference:peel_commit()
local c_object = libgit2.git_object_double_pointer()
local err = libgit2.C.git_reference_peel(c_object, self.ref, libgit2.GIT_OBJECT.COMMIT)
if err ~= 0 then
return nil, err
end
return Commit.new(ffi.cast(libgit2.git_commit_pointer, c_object[0])), 0
end
---Recursively peel reference until tree object is found.
---@return GitTree?
---@return GIT_ERROR err libgit2 Error code
function Reference:peel_tree()
local c_object = libgit2.git_object_double_pointer()
local err = libgit2.C.git_reference_peel(c_object, self.ref, libgit2.GIT_OBJECT.TREE)
if err ~= 0 then
return nil, err
end
return Tree.new(ffi.cast(libgit2.git_tree_pointer, c_object[0])), 0
end
---Gets upstream for a branch.
---@return GitReference? Reference git upstream reference
---@return GIT_ERROR
function Reference:branch_upstream()
if self.namespace ~= GIT_REFERENCE_NAMESPACE.BRANCH then
return nil, 0
end
local c_ref = libgit2.git_reference_double_pointer()
local err = libgit2.C.git_branch_upstream(c_ref, self.ref)
if err ~= 0 then
return nil, err
end
return Reference.new(c_ref[0]), 0
end
---Retrieves the upstream remote name of a remote_reference.
---@return string?
function Reference:remote_name()
if self.namespace == GIT_REFERENCE_NAMESPACE.REMOTE then
return self.name:match("remotes/([^/]+)/", 6)
end
end
---Get full name to the reference pointed to by a symbolic reference.
---@return string?
function Reference:symbolic_target()
if bit.band(self.type, libgit2.GIT_REFERENCE.SYMBOLIC) ~= 0 then
return ffi.string(libgit2.C.git_reference_symbolic_target(self.ref))
end
end
-- ============================
-- | RevisionWalker functions |
-- ============================
-- Inits new GitRevisionWalker object.
---@param repo ffi.cdata* libgit2.git_respository_pointer, don't own data
---@param revwalk ffi.cdata* libgit2.git_revwalk_pointer, own cdata
---@return GitRevisionWalker
function RevisionWalker.new(repo, revwalk)
local git_walker = {
repo = libgit2.git_repository_pointer(repo),
revwalk = libgit2.git_revwalk_pointer(revwalk),
}
setmetatable(git_walker, RevisionWalker)
ffi.gc(git_walker.revwalk, libgit2.C.git_revwalk_free)
return git_walker
end
---@return GIT_ERROR
function RevisionWalker:reset()
return libgit2.C.git_revwalk_reset(self.revwalk)
end
---@param topo boolean sort in topo order
---@param time boolean sort by time
---@param reverse boolean reverse
---@return GIT_ERROR
function RevisionWalker:sort(topo, time, reverse)
if not (topo or time or reverse) then
return 0
end
local mode = 0ULL
if topo then
mode = bit.bor(mode, libgit2.GIT_SORT.TOPOLOGICAL)
end
if time then
mode = bit.bor(mode, libgit2.GIT_SORT.TIME)
end
if reverse then
mode = bit.bor(mode, libgit2.GIT_SORT.REVERSE)
end
return libgit2.C.git_revwalk_sorting(self.revwalk, mode)
end
---@param oid GitObjectId
---@return GIT_ERROR
function RevisionWalker:push(oid)
return libgit2.C.git_revwalk_push(self.revwalk, oid.oid)
end
---@return GIT_ERROR
function RevisionWalker:push_head()
return libgit2.C.git_revwalk_push_head(self.revwalk)
end
---Push matching references
---@param glob string
---@return GIT_ERROR
function RevisionWalker:push_glob(glob)
return libgit2.C.git_revwalk_push_glob(self.revwalk, glob)
end
---Push the OID pointed to by a reference
---@param refname string
---@return GIT_ERROR
function RevisionWalker:push_ref(refname)
return libgit2.C.git_revwalk_push_ref(self.revwalk, refname)
end
---@param oid GitObjectId
---@return GIT_ERROR
function RevisionWalker:hide(oid)
return libgit2.C.git_revwalk_hide(self.revwalk, oid.oid)
end
---Gets next oid, commit in the walker
---@return GitObjectId?
---@return GitCommit?
---@return GIT_ERROR err error codd
function RevisionWalker:next()
local git_oid = libgit2.git_oid()
local err = libgit2.C.git_revwalk_next(git_oid, self.revwalk)
if err ~= 0 then
return nil, nil, err
end
local c_commit = libgit2.git_commit_double_pointer()
err = libgit2.C.git_commit_lookup(c_commit, self.repo, git_oid)
if err ~= 0 then
return nil, nil, err
end
return ObjectId.borrow(git_oid), Commit.new(c_commit[0]), 0
end
---Iterates through git_oid revisions.
---@return fun(): GitObjectId?, GitCommit?
function RevisionWalker:iter()
local git_oid = libgit2.git_oid()
return function()
local err = libgit2.C.git_revwalk_next(git_oid, self.revwalk)
if err ~= 0 then
return nil, nil
end
local c_commit = libgit2.git_commit_double_pointer()
err = libgit2.C.git_commit_lookup(c_commit, self.repo, git_oid)
if err ~= 0 then
return nil, nil
end
return ObjectId.borrow(git_oid), Commit.new(c_commit[0])
end
end
-- ====================
-- | Remote functions |
-- ====================
-- Inits new GitRemote object.
---@param git_remote ffi.cdata* libgit2.git_remote_pointer, own data
---@return GitRemote
function Remote.new(git_remote)
local remote = { remote = libgit2.git_remote_pointer(git_remote) }
setmetatable(remote, Remote)
remote.name = ffi.string(libgit2.C.git_remote_name(remote.remote))
remote.url = ffi.string(libgit2.C.git_remote_url(remote.remote))
local push_url = libgit2.C.git_remote_pushurl(remote.remote)
if push_url ~= nil then
remote.push_url = ffi.string(push_url)
end
ffi.gc(remote.remote, libgit2.C.git_remote_free)
return remote
end
-- =======================
-- | Signature functions |
-- =======================
---@param git_signature ffi.cdata* libgit2.git_signature_pointer, own data
function Signature.new(git_signature)
local signature = { sign = libgit2.git_signature_pointer(git_signature) }
setmetatable(signature, Signature)
ffi.gc(signature.sign, libgit2.C.git_signature_free)
return signature
end
function Signature.borrow(git_signature)
local signature = { sign = libgit2.git_signature_pointer(git_signature) }
setmetatable(signature, Signature)
return signature
end
---@return string
function Signature:name()
return ffi.string(self.sign["name"])
end
---@return string
function Signature:email()
return ffi.string(self.sign["email"])
end
function Signature:__tostring()
return string.format("%s <%s>", self:name(), self:email())
end
-- ===========================
-- | GitIndexEntry functions |
-- ===========================
-- Inits new GitIndexEntry object.
---@param git_index_entry ffi.cdata* libgit2.git_index_entry_pointer, don't own data
---@return GitIndexEntry
function IndexEntry.borrow(git_index_entry)
local entry = { entry = libgit2.git_index_entry_pointer(git_index_entry) }
setmetatable(entry, IndexEntry)
return entry
end
---@return integer
local function git_index_create_mode(mode)
if stat.S_ISLNK(mode) then
return stat.S_IFLNK
end
local link_or_dir = bit.bor(stat.S_IFLNK, stat.S_IFDIR)
if stat.S_ISDIR(mode) or bit.band(mode, stat.S_IFMT) == link_or_dir then
return link_or_dir
end
return bit.bor(stat.S_IFREG, GIT_PERMS_CANONICAL(mode))
end
---@param fs_stat uv.fs_stat.result
---@param path string file path
---@param distrust boolean index distrust mode
---@return GitIndexEntry
function IndexEntry.from_stat(fs_stat, path, distrust)
local git_index_entry = libgit2.git_index_entry()
local entry = git_index_entry[0]
entry.ctime.seconds = fs_stat.ctime.sec
entry.ctime.nanoseconds = fs_stat.ctime.nsec
entry.mtime.seconds = fs_stat.mtime.sec
entry.mtime.nanoseconds = fs_stat.mtime.nsec
entry.dev = fs_stat.rdev
entry.ino = fs_stat.ino
if distrust and stat.S_ISREG(fs_stat.mode) then
entry.mode = git_index_create_mode(438) -- 0666
else
entry.mode = git_index_create_mode(fs_stat.mode)
end
entry.uid = fs_stat.uid
entry.gid = fs_stat.gid
entry.file_size = fs_stat.size
entry.path = path
return IndexEntry.borrow(git_index_entry)
end
-- Whether the given index entry is a conflict.
---@return boolean is_conflict
function IndexEntry:is_conflict()
local ret = libgit2.C.git_index_entry_is_conflict(self.entry)
return ret == 1
end
-- Get file_size of IndexEntry.
---@return integer
function IndexEntry:file_size()
return tonumber(self.entry["file_size"]) or -1
end
-- Gets Git oid of IndexEntry.
---@return GitObjectId
function IndexEntry:id()
return ObjectId.borrow(self.entry["id"])
end
-- Get path of IndexEntry.
---@return string
function IndexEntry:path()
return ffi.string(self.entry["path"])
end
-- Get flags of IndexEntry.
---@return integer
function IndexEntry:flags()
return self.entry["flags"]
end
-- ===================
-- | Index functions |
-- ===================
-- Inits new GitIndex object.
---@param git_index ffi.cdata* libgit2.git_index_pointer, own cdata
---@return GitIndex
function Index.new(git_index)
local index = { index = libgit2.git_index_pointer(git_index) }
setmetatable(index, Index)
ffi.gc(index.index, libgit2.C.git_index_free)
return index
end
-- Gets the count of entries currently in the index
---@return integer
function Index:nentries()
local entrycount = libgit2.C.git_index_entrycount(self.index)
return math.floor(tonumber(entrycount) or -1)
end
-- Updates the contents of an existing index object.
---@param force boolean Performs hard read or not?
---@return GIT_ERROR
function Index:read(force)
return libgit2.C.git_index_read(self.index, force and 1 or 0)
end
-- Writes index from memory to file.
---@return GIT_ERROR
function Index:write()
return libgit2.C.git_index_write(self.index)
end
-- Write the index as a tree
---@return GitObjectId?
---@return GIT_ERROR
function Index:write_tree()
local tree_oid = libgit2.git_oid()
local err = libgit2.C.git_index_write_tree(tree_oid, self.index)
if err ~= 0 then
return nil, err
end
return ObjectId.borrow(tree_oid), 0
end
-- Get the full path to the index file on disk.
---@return string? path to index file or NULL for in-memory index
function Index:path()
local path = libgit2.C.git_index_path(self.index)
if path ~= nil then
return ffi.string(path)
end
return nil
end
-- Checks index in-memory or not
---return boolean inmemory Index in memory
function Index:in_memory()
local path = libgit2.C.git_index_path(self.index)
return path == nil
end
-- Adds path to index.
---@param path string File path to be added.
---@return GIT_ERROR
function Index:add_bypath(path)
return libgit2.C.git_index_add_bypath(self.index, path)
end
-- Adds or update an index entry from a buffer in memory
---@param entry GitIndexEntry
---@param buffer string String buffer
---@return GIT_ERROR
function Index:add_from_buffer(entry, buffer)
return libgit2.C.git_index_add_from_buffer(self.index, entry.entry, buffer, buffer:len())
end
-- Removes path from index.
---@param path string File path to be removed.
---@return GIT_ERROR
function Index:remove_bypath(path)
return libgit2.C.git_index_remove_bypath(self.index, path)
end
-- Determine if the index contains entries representing file conflicts.
---@return boolean has_conflicts
function Index:has_conflicts()
return (libgit2.C.git_index_has_conflicts(self.index) > 0)
end
-- Gets index entry by path
---@param path string
---@param stage_number GIT_INDEX_STAGE
---@return GitIndexEntry?
function Index:get_bypath(path, stage_number)
local entry = libgit2.C.git_index_get_bypath(self.index, path, stage_number)
if entry == nil then
return nil
end
return IndexEntry.borrow(entry)
end
-- Iterates through entries in the index.
---@return (fun(): GitIndexEntry?)?
function Index:iter()
local entry = libgit2.git_index_entry_double_pointer()
local iterator = libgit2.git_index_iterator_double_pointer()
local err = libgit2.C.git_index_iterator_new(iterator, self.index)
if err ~= 0 then
return nil
end
return function()
err = libgit2.C.git_index_iterator_next(entry, iterator[0])
if err ~= 0 then
libgit2.C.git_index_iterator_free(iterator[0])
return nil
end
return IndexEntry.borrow(entry[0])
end
end
-- Gets conflicst entries
---@param path string path to get conflict
---@return GitIndexEntry? ancestor
---@return GitIndexEntry? our side
---@return GitIndexEntry? their side
---@return GIT_ERROR
function Index:get_conflict(path)
local entries = libgit2.git_index_entry_pointer_array(3)
local err = libgit2.C.git_index_conflict_get(entries, entries + 1, entries + 2, self.index, path)
if err ~= 0 then
return nil, nil, nil, err
end
local ancestor_entry = IndexEntry.borrow(entries[0])
local our_entry = IndexEntry.borrow(entries[1])
local their_entry = IndexEntry.borrow(entries[2])
return ancestor_entry, our_entry, their_entry, 0
end
-- ==================
-- | Diff functions |
-- ==================
---Create new GitDiff object
---@param git_diff ffi.cdata* libgit2.git_diff_pointer, own cdata
---@return GitDiff
function Diff.new(git_diff)
local diff = { diff = libgit2.git_diff_pointer(git_diff) }
setmetatable(diff, Diff)
ffi.gc(diff.diff, libgit2.C.git_diff_free)
return diff
end
---@parm diff_str string
---@return GitDiff?
---@return GIT_ERROR
function Diff.from_buffer(diff_str)
local diff = libgit2.git_diff_double_pointer()
local err = libgit2.C.git_diff_from_buffer(diff, diff_str, diff_str:len())
if err ~= 0 then
return nil, err
end
return Diff.new(diff[0]), 0
end
---@param format GIT_DIFF_FORMAT
---@return string
function Diff:tostring(format)
local buf = libgit2.git_buf()
local err = libgit2.C.git_diff_to_buf(buf, self.diff, format)
if err ~= 0 then
libgit2.C.git_buf_dispose(buf)
return ""
end
local diff = ffi.string(buf[0].ptr, buf[0].size)
libgit2.C.git_buf_dispose(buf)
return diff
end
function Diff:__tostring()
return self:tostring(libgit2.GIT_DIFF_FORMAT.PATCH)
end
---Gets accumulate diff statistics for all patches.
---@return GitDiffStats?
---@return GIT_ERROR
function Diff:stats()
local diff_stats = libgit2.git_diff_stats_double_pointer()
local err = libgit2.C.git_diff_get_stats(diff_stats, self.diff)
if err ~= 0 then
return nil, err
end
---@type GitDiffStats
local stats = {
changed = tonumber(libgit2.C.git_diff_stats_files_changed(diff_stats[0])) or 0,
insertions = tonumber(libgit2.C.git_diff_stats_insertions(diff_stats[0])) or 0,
deletions = tonumber(libgit2.C.git_diff_stats_deletions(diff_stats[0])) or 0,
}
libgit2.C.git_diff_stats_free(diff_stats[0])
return stats, 0
end
---Gets patches from diff as a list
---@param sort_case_sensitive boolean
---@return GitDiffPatchItem[]
---@return GIT_ERROR
function Diff:patches(sort_case_sensitive)
local num_deltas = tonumber(libgit2.C.git_diff_num_deltas(self.diff)) or 0
local patches = table_new(num_deltas, 0)
local err = 0
for i = 0, num_deltas - 1 do
local delta = libgit2.C.git_diff_get_delta(self.diff, i)
local c_patch = libgit2.git_patch_double_pointer()
err = libgit2.C.git_patch_from_diff(c_patch, self.diff, i)
if err ~= 0 then
break
end
local patch = Patch.new(c_patch[0])
---@type GitDiffPatchItem
local patch_item = {
status = delta.status,
path = ffi.string(delta.old_file.path),
new_path = ffi.string(delta.new_file.path),
num_hunks = patch:nhunks(),
patch = patch,
}
table.insert(patches, patch_item)
end
if #patches > 0 and sort_case_sensitive then
-- sort patches by name
table.sort(patches, function(a, b)
return a.path < b.path
end)
end
return patches, err
end
-- ===================
-- | Patch functions |
-- ===================
---Creates new GitPatch object
---@param git_patch ffi.cdata* libgit2.git_patch_pointer, own cdata
---@return GitPatch
function Patch.new(git_patch)
local patch = { patch = libgit2.git_patch_pointer(git_patch) }
setmetatable(patch, Patch)
ffi.gc(patch.patch, libgit2.C.git_patch_free)
return patch
end
---Gets the content of a patch as a single diff text.
---@return string
function Patch:__tostring()
local buf = libgit2.git_buf()
local err = libgit2.C.git_patch_to_buf(buf, self.patch)
if err ~= 0 then
libgit2.C.git_buf_dispose(buf)
return ""
end
local patch = ffi.string(buf[0].ptr, buf[0].size)
libgit2.C.git_buf_dispose(buf)
return patch
end
---@return GitDiffStats?
---@return GIT_ERROR
function Patch:stats()
local number = libgit2.size_t_array(2)
local err = libgit2.C.git_patch_line_stats(nil, number, number + 1, self.patch)
if err ~= 0 then
return nil, err
end
return {
changed = 1,
insertions = tonumber(number[0]),
deletions = tonumber(number[1]),
}, 0
end
---Gets the number of hunks in a patch
---@return integer
function Patch:nhunks()
return tonumber(libgit2.C.git_patch_num_hunks(self.patch)) or 0
end
---@param idx integer Hunk index 0-based
---@return GitDiffHunk?
---@return GIT_ERROR
function Patch:hunk(idx)
local num_lines = libgit2.size_t_array(1)
local hunk = libgit2.git_diff_hunk_double_pointer()
local err = libgit2.C.git_patch_get_hunk(hunk, num_lines, self.patch, idx)
if err ~= 0 then
return nil, err
end
---@type GitDiffHunk
local diff_hunk = {
num_lines = tonumber(num_lines[0]) or 0,
old_start = hunk[0].old_start,
old_lines = hunk[0].old_lines,
new_start = hunk[0].new_start,
new_lines = hunk[0].new_lines,
header = ffi.string(hunk[0].header, hunk[0].header_len),
}
return diff_hunk, 0
end
---@param hunk_idx integer Hunk index 0-based
---@param line_idx integer Line index in hunk, 0-based
---@return GitDiffLine?
---@return GIT_ERROR
function Patch:hunk_line(hunk_idx, line_idx)
local diff_line = libgit2.git_diff_line_double_pointer()
local err = libgit2.C.git_patch_get_line_in_hunk(diff_line, self.patch, hunk_idx, line_idx)
if err ~= 0 then
return nil, err
end
---@type GitDiffLine
local ret = {
origin = string.char(diff_line[0].origin),
old_lineno = diff_line[0].old_lineno,
new_lineno = diff_line[0].new_lineno,
num_lines = diff_line[0].num_lines,
content = ffi.string(diff_line[0].content, diff_line[0].content_len),
}
return ret, 0
end
---Gets the number of lines in a hunk.
---@param i integer hunk index 0-th based.
---@return integer num_lines number of lines in i-th hunk.
function Patch:hunk_num_lines(i)
return libgit2.C.git_patch_num_lines_in_hunk(self.patch, i)
end
--- ============================
-- | RebaseOperation functions |
-- =============================
---Borrow new RebaseOperation
---@param operation_ptr ffi.cdata* libgit2 git_rebase_operation pointer
---@return GitRebaseOperation
function RebaseOperation.borrow(operation_ptr)
local op = {
operation = libgit2.git_rebase_operation_pointer(operation_ptr),
}
setmetatable(op, RebaseOperation)
return op
end
---Gets type of a rebase operation
---@return GIT_REBASE_OPERATION
function RebaseOperation:type()
return self.operation["type"]
end
---Changes type of a rebase operation.
---@param type GIT_REBASE_OPERATION
function RebaseOperation:set_type(type)
self.operation["type"] = type
end
---Gets rebase operation exec.
---@return string
function RebaseOperation:exec()
local str_ptr = self.operation["exec"]
return str_ptr ~= nil and ffi.string(str_ptr) or ""
end
---Sets rebase operation exec string
---@param exec string?
function RebaseOperation:set_exec(exec)
self.operation["exec"] = exec
end
---Gets ObjectId of rebase operation.
---@return GitObjectId
function RebaseOperation:id()
return ObjectId.borrow(ffi.cast(libgit2.git_oid_pointer, self.operation["id"]))
end
---Copies another git_oid to this RebaseOperation oid.
---@param oid GitObjectId
---@return GIT_ERROR err 0 on success or error code
function RebaseOperation:set_id(oid)
local op_id_ptr = ffi.cast(libgit2.git_oid_pointer, self.operation["id"])
return libgit2.C.git_oid_cpy(op_id_ptr, oid.oid)
end
-- ====================
-- | Rebase functions |
-- ====================
---Init new Gitrebase
---@param git_rebase_ptr ffi.cdata* libgit2 git_rebase*, own cdata
---@return GitRebase
function Rebase.new(git_rebase_ptr)
local rebase = { rebase = libgit2.git_rebase_pointer(git_rebase_ptr) }
setmetatable(rebase, Rebase)
ffi.gc(rebase.rebase, libgit2.C.git_rebase_free)
return rebase
end
---Pretty print current rebase operation
function Rebase:__tostring()
local str = "Rebase "
local onto_id = libgit2.C.git_rebase_onto_id(self.rebase)
local org_id = libgit2.C.git_rebase_orig_head_id(self.rebase)
local org_str = libgit2.C.git_oid_tostr_s(org_id)
if org_str ~= nil then
str = str .. string.sub(ffi.string(org_str), 1, 8)
end
local onto_str = libgit2.C.git_oid_tostr_s(onto_id)
if onto_str ~= nil then
str = str .. " onto " .. string.sub(ffi.string(onto_str), 1, 8)
end
return str
end
---Performs the next rebase operation and returns the information about it.
---@return GitRebaseOperation?
---@return GIT_ERROR
function Rebase:next()
local operation = libgit2.git_rebase_operation_double_pointer()
local err = libgit2.C.git_rebase_next(operation, self.rebase)
if err ~= 0 then
return nil, err
end
return RebaseOperation.borrow(operation[0]), 0
end
---Gets the count of rebase operations that are to be applied.
---@return integer
function Rebase:noperations()
return tonumber(libgit2.C.git_rebase_operation_entrycount(self.rebase)) or -1
end
---Gets the index of the rebase operation that is currently being applied.
---@return integer index If the first operation has not yet been applied, returns GIT_REBASE_NO_OPERATION
function Rebase:operation_current()
return libgit2.C.git_rebase_operation_current(self.rebase)
end
---Gets the rebase operation specified by the given index.
---@return GitRebaseOperation? The rebase operation or NULL if `idx` was out of bounds.
function Rebase:operation_byindex(idx)
local operation = libgit2.C.git_rebase_operation_byindex(self.rebase, idx)
if operation == nil then
return nil
end
return RebaseOperation.borrow(operation)
end
---Gets the onto ref name for merge rebases.
---@return string
function Rebase:onto_name()
local name_ptr = libgit2.C.git_rebase_onto_name(self.rebase)
if name_ptr == nil then
return ""
end
return ffi.string(name_ptr)
end
---Gets the onto id for merge rebases.
function Rebase:onto_id()
local oid_ptr = libgit2.C.git_rebase_onto_id(self.rebase)
return ObjectId.borrow(oid_ptr)
end
---Gets the original HEAD ref name for merge rebases.
---@return string
function Rebase:orig_head_name()
local name_ptr = libgit2.C.git_rebase_orig_head_name(self.rebase)
if name_ptr == nil then
return ""
end
return ffi.string(name_ptr)
end
---Gets the original HEAD id for merge rebases.
---@return GitObjectId
function Rebase:orig_head_id()
local oid_ptr = libgit2.C.git_rebase_orig_head_id(self.rebase)
return ObjectId.borrow(oid_ptr)
end
---Aborts a rebase that is currently in progress,
---resetting the repository and working directory to their state before rebase began.
---@return GIT_ERROR
function Rebase:abort()
return libgit2.C.git_rebase_abort(self.rebase)
end
---Commits the current patch. You must have resolved any conflicts.
---@param author GitSignature? The author of the updated commit, or NULL to keep the author from the original commit
---@param commiter GitSignature The committer of the rebase
---@param message string? The message for this commit, or NULL to use the message from the original commit.
---@return GitObjectId?
---@return GIT_ERROR err Zero on success, GIT_EUNMERGED if there are unmerged changes in the index, GIT_EAPPLIED if the current commit has already been applied to the upstream and there is nothing to commit, -1 on failure.
function Rebase:commit(author, commiter, message)
local new_oid = libgit2.git_oid()
local err =
libgit2.C.git_rebase_commit(new_oid, self.rebase, author and author.sign or nil, commiter.sign, "UTF-8", message)
if err ~= 0 then
return nil, err
end
return ObjectId.borrow(new_oid), 0
end
---Finishes a rebase that is currently in progress once all patches have been applied.
---@param signature GitSignature
---@return GIT_ERROR err Zero on success; -1 on error
function Rebase:finish(signature)
return libgit2.C.git_rebase_finish(self.rebase, signature.sign)
end
---Gets the index produced by the last operation,
---which is the result of git_rebase_next and which will be committed
---by the next invocation of git_rebase_commit
---@return GitIndex? The result index of the last operation.
---@return GIT_ERROR
function Rebase:inmemory_index()
local index = libgit2.git_index_double_pointer()
local err = libgit2.C.git_rebase_inmemory_index(index, self.rebase)
if err ~= 0 then
return nil, err
end
return Index.new(index[0]), 0
end
-- ========================
-- | Repository functions |
-- ========================
---@class GitStatusItem
---@field path string File path
---@field new_path string? New file path in case of rename
---@field worktree_status GIT_DELTA Git status in worktree to index
---@field index_status GIT_DELTA Git status in index to head
---@field renamed boolean Extra flag to indicate whether item is renamed
---@class GitStatusUpstream
---@field name string
---@field oid GitObjectId?
---@field message string
---@field author string
---@field ahead integer
---@field behind integer
---@field remote string
---@field remote_url string
---@class GitStatusHead
---@field name string
---@field oid GitObjectId?
---@field message string
---@field author string
---@field is_detached boolean
---@field namespace GIT_REFERENCE_NAMESPACE
---@field refname string
---@class GitStatusResult
---@field head GitStatusHead?
---@field upstream GitStatusUpstream?
---@field status GitStatusItem[]
---@class GitBranch
---@field name string
---@field shorthand string
---@field type GIT_BRANCH
---@alias GitDiffStats {changed: integer, insertions: integer, deletions: integer} Diff stats
---@alias GitDiffPatchItem {status: GIT_DELTA, path: string, new_path:string, num_hunks: integer, patch: GitPatch}
local DEFAULT_STATUS_FLAGS = bit.bor(
libgit2.GIT_STATUS_OPT.INCLUDE_UNTRACKED,
libgit2.GIT_STATUS_OPT.RENAMES_HEAD_TO_INDEX,
libgit2.GIT_STATUS_OPT.RENAMES_INDEX_TO_WORKDIR,
libgit2.GIT_STATUS_OPT.RECURSE_UNTRACKED_DIRS,
libgit2.GIT_STATUS_OPT.SORT_CASE_SENSITIVELY
)
---Inits new Repository object
---@param git_repository ffi.cdata* libgit2.git_repository_pointer, own cdata
---@return GitRepository
function Repository.new(git_repository)
local repo = { repo = libgit2.git_repository_pointer(git_repository) }
setmetatable(repo, Repository)
local c_path = libgit2.C.git_repository_path(repo.repo)
repo.path = ffi.string(c_path)
ffi.gc(repo.repo, libgit2.C.git_repository_free)
return repo
end
-- New Repository object, only borrow cdata
---@param git_repo ffi.cdata* libgit2.git_repository_pointer, don't own cdata
---@return GitRepository
function Repository.borrow(git_repo)
local repo = { repo = libgit2.git_repository_pointer(git_repo) }
setmetatable(repo, Repository)
local c_path = libgit2.C.git_repository_path(repo.repo)
repo.path = ffi.string(c_path)
return repo
end
function Repository:__tostring()
return string.format("Git Repository: %s", self.path)
end
---Opens Git repository
---@param path string Path to repository
---@param search boolean Whether to search parent directories.
---@return GitRepository?
---@return GIT_ERROR
function Repository.open(path, search)
local git_repo = libgit2.git_repository_double_pointer()
local open_flag = 0
if not search then
open_flag = bit.bor(open_flag, libgit2.GIT_REPOSITORY_OPEN.NO_SEARCH)
end
local err = libgit2.C.git_repository_open_ext(git_repo, path, open_flag, nil)
if err ~= 0 then
return nil, err
end
return Repository.new(git_repo[0]), 0
end
---Checks a Repository is empty or not
---@return boolean is_empty Whether this git repo is empty
function Repository:is_empty()
local ret = libgit2.C.git_repository_is_empty(self.repo)
if ret == 1 then
return true
elseif ret == 0 then
return false
else
error("Repository is corrupted, code" .. ret)
end
end
-- Checks a Repository is bare or not
---@return boolean is_bare Whether this git repo is bare repository
function Repository:is_bare()
local ret = libgit2.C.git_repository_is_bare(self.repo)
return ret == 1
end
-- Checks a Repository HEAD is detached or not
---@return boolean is_head_detached Whether this git repo head detached
function Repository:is_head_detached()
local ret = libgit2.C.git_repository_head_detached(self.repo)
return ret == 1
end
---Get the path of this repository
function Repository:repo_path()
return ffi.string(libgit2.C.git_repository_path(self.repo))
end
---Get the configuration file for this repository
---@return GitConfig?
---@return GIT_ERROR
function Repository:config()
local git_config = libgit2.git_config_double_pointer()
local err = libgit2.C.git_repository_config(git_config, self.repo)
if err ~= 0 then
return nil, err
end
return Config.new(git_config[0]), 0
end
-- Creates a git_annotated_commit from the given reference.
---@param ref GitReference
---@return GitAnnotatedCommit?
---@return GIT_ERROR
function Repository:annotated_commit_from_ref(ref)
local git_commit = libgit2.git_annotated_commit_double_pointer()
local err = libgit2.C.git_annotated_commit_from_ref(git_commit, self.repo, ref.ref)
if err ~= 0 then
return nil, 0
end
return AnnotatedCommit.new(git_commit[0]), 0
end
-- Creates a git_annotated_commit from a revision string.
---@param revspec string
---@return GitAnnotatedCommit?
---@return GIT_ERROR
function Repository:annotated_commit_from_revspec(revspec)
local git_commit = libgit2.git_annotated_commit_double_pointer()
local err = libgit2.C.git_annotated_commit_from_revspec(git_commit, self.repo, revspec)
if err ~= 0 then
return nil, 0
end
return AnnotatedCommit.new(git_commit[0]), 0
end
---@param opts GitBlameOptions
---@return ffi.cdata*? blame_opts libgit2 git_blame_options[1]
---@return GIT_ERROR
local function init_blame_options(opts)
local blame_opts = libgit2.git_blame_options()
local err = libgit2.C.git_blame_options_init(blame_opts, libgit2.GIT_BLAME_OPTIONS_VERSION)
if err ~= 0 then
return nil, err
end
local flags = 0
if opts.first_parent then
flags = bit.bor(flags, libgit2.GIT_BLAME.FIRST_PARENT)
end
if opts.use_mailmap then
flags = bit.bor(flags, libgit2.GIT_BLAME.USE_MAILMAP)
end
if opts.ignore_whitespace then
flags = bit.bor(flags, libgit2.GIT_BLAME.IGNORE_WHITESPACE)
end
blame_opts[0].flags = flags
if opts.min_match_characters then
blame_opts[0].min_match_characters = opts.min_match_characters
end
if opts.newest_commit then
err = libgit2.C.git_oid_cpy(blame_opts[0].newest_commit, opts.newest_commit.oid)
if err ~= 0 then
return nil, err
end
end
if opts.oldest_commit then
err = libgit2.C.git_oid_cpy(blame_opts[0].oldest_commit, opts.oldest_commit.oid)
if err ~= 0 then
return nil, err
end
end
if opts.min_line then
blame_opts[0].min_line = opts.min_line
end
if opts.max_line then
blame_opts[0].max_line = opts.max_line
end
return blame_opts, 0
end
-- Gets the blame for a single file.
---@param path string git path
---@param opts GitBlameOptions
---@return GitBlame? GitBlame info
---@return GIT_ERROR err error code
function Repository:blame_file(path, opts)
local blame_opts, err = init_blame_options(opts)
if not blame_opts then
return nil, err
end
local git_blame = libgit2.git_blame_double_pointer()
err = libgit2.C.git_blame_file(git_blame, self.repo, path, blame_opts)
if err ~= 0 then
return nil, err
end
return Blame.new(git_blame[0]), 0
end
---Retrieves reference pointed at by HEAD.
---@return GitReference?
---@return GIT_ERROR
function Repository:head()
local c_ref = libgit2.git_reference_double_pointer()
local err = libgit2.C.git_repository_head(c_ref, self.repo)
if err ~= 0 then
return nil, err
end
return Reference.new(c_ref[0]), 0
end
---@return GitCommit?
---@return GIT_ERROR
function Repository:head_commit()
local c_ref = libgit2.git_reference_double_pointer()
local err = libgit2.C.git_repository_head(c_ref, self.repo)
if err ~= 0 then
return nil, err
end
local git_object = libgit2.git_object_double_pointer()
err = libgit2.C.git_reference_peel(git_object, c_ref[0], libgit2.GIT_OBJECT.COMMIT)
libgit2.C.git_reference_free(c_ref[0])
if err ~= 0 then
return nil, err
end
return Commit.new(ffi.cast(libgit2.git_commit_pointer, git_object[0])), 0
end
-- Makes the repository HEAD point to the specified reference.
---@param refname string
---@return GIT_ERROR
function Repository:set_head(refname)
local err = libgit2.C.git_repository_set_head(self.repo, refname)
return err
end
-- Makes the repository HEAD directly point to the Commit.
---@param oid GitObjectId
---@return GIT_ERROR
function Repository:set_head_detached(oid)
local err = libgit2.C.git_repository_set_head_detached(self.repo, oid.oid)
return err
end
-- Gets GitCommit signature
---@param oid GitObjectId
---@param field string? GPG sign field
function Repository:commit_signature(oid, field)
local buf_signature = libgit2.git_buf()
local buf_signed_data = libgit2.git_buf()
local err = libgit2.C.git_commit_extract_signature(buf_signature, buf_signed_data, self.repo, oid.oid, field)
if err ~= 0 then
return nil, nil, 0
end
local signature = ffi.string(buf_signature[0].ptr, buf_signature[0].size)
local signed_data = ffi.string(buf_signed_data[0].ptr, buf_signed_data[0].size)
libgit2.C.git_buf_dispose(buf_signature[0])
libgit2.C.git_buf_dispose(buf_signed_data[0])
return signature, signed_data, 0
end
---@return GitTree?
---@return GIT_ERROR
function Repository:head_tree()
local c_ref = libgit2.git_reference_double_pointer()
local err = libgit2.C.git_repository_head(c_ref, self.repo)
if err ~= 0 then
return nil, err
end
local git_object = libgit2.git_object_double_pointer()
err = libgit2.C.git_reference_peel(git_object, c_ref[0], libgit2.GIT_OBJECT.TREE)
libgit2.C.git_reference_free(c_ref[0])
if err ~= 0 then
return nil, err
end
return Tree.new(ffi.cast(libgit2.git_tree_pointer, git_object[0])), 0
end
-- Creates a new branch pointing at a target commit
---@param name string
---@param target GitCommit
---@param force boolean
---@return GitReference? Reference to created branch
---@return GIT_ERROR
function Repository:create_branch(name, target, force)
local git_ref = libgit2.git_reference_double_pointer()
local is_force = force and 1 or 0
local err = libgit2.C.git_branch_create(git_ref, self.repo, name, target.commit, is_force)
if err ~= 0 then
return nil, err
end
return Reference.new(git_ref[0]), 0
end
-- Listings branches of a repo.
---@param locals boolean Includes local branches.
---@param remotes boolean Include remote branches.
---@return GitBranch[]?
---@return GIT_ERROR
function Repository:branches(locals, remotes)
if not locals and not remotes then
return {}, 0
end
local branch_flags = 0
if locals then
branch_flags = libgit2.GIT_BRANCH.LOCAL
end
if remotes then
branch_flags = bit.bor(branch_flags, libgit2.GIT_BRANCH.REMOTE)
end
local c_branch_iter = libgit2.git_branch_iterator_double_pointer()
local err = libgit2.C.git_branch_iterator_new(c_branch_iter, self.repo, branch_flags)
if err ~= 0 then
return nil, err
end
---@type GitBranch[]
local branches = {}
local c_ref = libgit2.git_reference_double_pointer()
local c_branch_type = libgit2.unsigned_int_array(1)
while libgit2.C.git_branch_next(c_ref, c_branch_type, c_branch_iter[0]) == 0 do
---@type GitBranch
local br = {
name = ffi.string(libgit2.C.git_reference_name(c_ref[0])),
shorthand = ffi.string(libgit2.C.git_reference_shorthand(c_ref[0])),
type = math.floor(tonumber(c_branch_type[0]) or 0),
}
table.insert(branches, br)
libgit2.C.git_reference_free(c_ref[0])
end
libgit2.C.git_branch_iterator_free(c_branch_iter[0])
return branches, 0
end
-- Listings tags of a repo.
---@return string[]?
---@return GIT_ERROR
function Repository:tag_list()
local tag_names = libgit2.git_strarray()
local err = libgit2.C.git_tag_list(tag_names, self.repo)
if err ~= 0 then
return nil, err
end
local ntags = tonumber(tag_names[0].count) or 0
local tags = table_new(ntags, 0)
for i = 0, ntags - 1 do
tags[i + 1] = ffi.string(tag_names[0].strings[i])
end
libgit2.C.git_strarray_dispose(tag_names)
return tags, 0
end
-- Calculates ahead and behind information.
---@param local_commit GitObjectId The commit which is considered the local or current state.
---@param upstream_commit GitObjectId The commit which is considered upstream.
---@return number? ahead Unique ahead commits.
---@return number? behind Unique behind commits.
---@return GIT_ERROR err Error code.
function Repository:ahead_behind(local_commit, upstream_commit)
local c_ahead = libgit2.size_t_array(2)
local err = libgit2.C.git_graph_ahead_behind(c_ahead, c_ahead + 1, self.repo, local_commit.oid, upstream_commit.oid)
if err ~= 0 then
return nil, nil, err
end
return tonumber(c_ahead[0]), tonumber(c_ahead[1]), 0
end
---Lookup a reference by name in a repository
---@param refname string Long name for the reference (e.g. HEAD, refs/heads/master, refs/tags/v0.1.0).
---@return GitReference?
---@return GIT_ERROR
function Repository:reference_lookup(refname)
local ref = libgit2.git_reference_double_pointer()
local err = libgit2.C.git_reference_lookup(ref, self.repo, refname)
if err ~= 0 then
return nil, 0
end
return Reference.new(ref[0]), 0
end
---Lookup a reference by name and resolve immediately to OID.
---@param refname string Long name for the reference (e.g. HEAD, refs/heads/master, refs/tags/v0.1.0).
---@return GitObjectId?
---@return GIT_ERROR
function Repository:reference_name_to_id(refname)
local oid = libgit2.git_oid()
local err = libgit2.C.git_reference_name_to_id(oid, self.repo, refname)
if err ~= 0 then
return nil, err
end
return ObjectId.borrow(oid), 0
end
---Creates a new direct reference.
---@param name string name of the reference.
---@param oid GitObjectId object id pointed to by the reference.
---@param is_force boolean is force.
---@param log_message string line long message to be appended to the reflog.
---@return GitReference? ref direct reference
---@return GIT_ERROR err error code
function Repository:create_reference(name, oid, is_force, log_message)
local git_ref = libgit2.git_reference_double_pointer()
local force = is_force and 1 or 0
local err = libgit2.C.git_reference_create(git_ref, self.repo, name, oid.oid, force, log_message)
if err ~= 0 then
return nil, err
end
return Reference.new(git_ref[0]), 0
end
---Gets commit from a reference.
---@param oid GitObjectId
---@return GitCommit?
---@return GIT_ERROR
function Repository:commit_lookup(oid)
local c_commit = libgit2.git_commit_double_pointer()
local err = libgit2.C.git_commit_lookup(c_commit, self.repo, oid.oid)
if err ~= 0 then
return nil, err
end
return Commit.new(c_commit[0]), 0
end
-- Gets repository index.
---@return GitIndex?
---@return GIT_ERROR
function Repository:index()
local c_index = libgit2.git_index_double_pointer()
local err = libgit2.C.git_repository_index(c_index, self.repo)
if err ~= 0 then
return nil, err
end
return Index.new(c_index[0]), 0
end
-- Updates some entries in the index from the target commit tree.
---@param paths string[]
---@return GIT_ERROR
function Repository:reset_default(paths)
local head, ret = self:head()
if head == nil then
return ret
else
local commit, err = head:peel(libgit2.GIT_OBJECT.COMMIT)
if commit == nil then
return err
elseif #paths > 0 then
local c_paths = libgit2.const_char_pointer_array(#paths, paths)
local strarray = libgit2.git_strarray_readonly()
-- for i, p in ipairs(paths) do
-- c_paths[i-1] = p
-- end
strarray[0].strings = c_paths
strarray[0].count = #paths
return libgit2.C.git_reset_default(self.repo, commit.obj, strarray)
end
return 0
end
end
---@param strategy GIT_CHECKOUT? The default strategy is SAFE | RECREATE_MISSING.
---@param paths string[]? file paths to be checkout
---@return ffi.cdata* opts GIT_CHECKOUT_OPTION
---@return ffi.cdata* c_paths GIT_STR_ARRAY
local function prepare_checkout_opts(strategy, paths)
local c_paths
local opts = libgit2.git_checkout_options(libgit2.GIT_CHECKOUT_OPTIONS_INIT)
if strategy ~= nil then
opts[0].checkout_strategy = strategy
else
opts[0].checkout_strategy = bit.bor(libgit2.GIT_CHECKOUT.SAFE, libgit2.GIT_CHECKOUT.RECREATE_MISSING)
end
if paths and #paths > 0 then
c_paths = libgit2.const_char_pointer_array(#paths, paths)
opts[0].paths.strings = c_paths
opts[0].paths.count = #paths
end
return opts, c_paths
end
-- Updates files in the working tree to match the content of the index.
---@param index GitIndex? Repository index, can be null
---@param strategy GIT_CHECKOUT?
---@param paths string[]? file paths to be checkout
---@return GIT_ERROR
function Repository:checkout_index(index, strategy, paths)
local opts, c_paths = prepare_checkout_opts(strategy, paths)
local err = libgit2.C.git_checkout_index(self.repo, index and index.index or nil, opts)
return err
end
-- Updates files in the index and the working tree to match
-- the content of the commit pointed at by HEAD.
---@param strategy GIT_CHECKOUT?
---@param paths string[]? files paths to be checkout
---@return GIT_ERROR
function Repository:checkout_head(strategy, paths)
local opts, c_paths = prepare_checkout_opts(strategy, paths)
local err = libgit2.C.git_checkout_head(self.repo, opts)
return err
end
-- Updates files in the index and working tree to match
-- the content of the tree pointed at by the treeish.
---@param tree GitObject Tree like object
---@param strategy GIT_CHECKOUT?
---@param paths string[]? files paths to be checkout
---@return GIT_ERROR
function Repository:checkout_tree(tree, strategy, paths)
local opts, c_paths = prepare_checkout_opts(strategy, paths)
local err = libgit2.C.git_checkout_tree(self.repo, tree.obj, opts)
return err
end
-- Checkout the given reference using the given strategy, and update the HEAD.
-- The default strategy is SAFE | RECREATE_MISSING.
-- If no reference is given, checkout from the index.
---@param refname string
---@param strategy GIT_CHECKOUT?
---@param paths string[]? files paths to be checkout
---@return GIT_ERROR
function Repository:checkout(refname, strategy, paths)
-- Case 1: Checkout index
if not refname then
return self:checkout_index(nil, strategy, paths)
end
-- Case 2: Checkout head
if refname == "HEAD" then
return self:checkout_head(strategy, paths)
end
-- Case 3: Reference name
local ref, err = self:reference_lookup(refname)
if not ref then
return err
end
local tree
tree, err = ref:peel_tree()
if not tree then
return err
end
err = self:checkout_tree(tree:as_object(), strategy, paths)
if err ~= 0 then
return err
end
if not paths or #paths == 0 then
return self:set_head(refname)
end
return 0
end
-- Finds the remote name of a remote-tracking branch.
---@param ref string Ref name
---@return string? remote Git remote name
---@return GIT_ERROR
function Repository:branch_remote_name(ref)
local c_buf = libgit2.git_buf()
local err = libgit2.C.git_branch_remote_name(c_buf, self.repo, ref)
if err ~= 0 then
libgit2.C.git_buf_dispose(c_buf)
return nil, err
end
local remote = ffi.string(c_buf[0].ptr, c_buf[0].size)
libgit2.C.git_buf_dispose(c_buf)
return remote, 0
end
---Retrieves the remote of upstream of a local branch.
---@param ref string Ref name
---@return string? remote Git remote name
---@return GIT_ERROR
function Repository:branch_upstream_remote_name(ref)
local c_buf = libgit2.git_buf()
local err = libgit2.C.git_branch_upstream_remote(c_buf, self.repo, ref)
if err ~= 0 then
libgit2.C.git_buf_dispose(c_buf)
return nil, err
end
local remote = ffi.string(c_buf[0].ptr, c_buf[0].size)
libgit2.C.git_buf_dispose(c_buf)
return remote, 0
end
---@param refname string refname
---@return string?
---@return GIT_ERROR
function Repository:branch_upstream_name(refname)
local git_buf = libgit2.git_buf()
local err = libgit2.C.git_branch_upstream_name(git_buf, self.repo, refname)
if err ~= 0 then
libgit2.C.git_buf_dispose(git_buf)
return nil, err
end
local name = ffi.string(git_buf[0].ptr, git_buf[0].size)
libgit2.C.git_buf_dispose(git_buf)
return name, 0
end
-- Gets the information for a particular remote.
---@param remote string
---@return GitRemote?
---@return GIT_ERROR
function Repository:remote_lookup(remote)
local c_remote = libgit2.git_remote_double_pointer()
local err = libgit2.C.git_remote_lookup(c_remote, self.repo, remote)
if err ~= 0 then
return nil, err
end
return Remote.new(c_remote[0]), 0
end
---Gets a list of the configured remotes for a repo
---@return string[]?
---@return GIT_ERROR
function Repository:remote_list()
local strarr = libgit2.git_strarray()
local err = libgit2.C.git_remote_list(strarr, self.repo)
if err ~= 0 then
return nil, err
end
local num_remotes = tonumber(strarr[0].count) or 0
local remotes = table_new(num_remotes, 0)
for i = 0, num_remotes - 1 do
remotes[i + 1] = ffi.string(strarr[0].strings[i])
end
libgit2.C.git_strarray_dispose(strarr)
return remotes, 0
end
---Reads status of a given file path.
---this can't detect a rename.
---@param path string Git file path.
---@return GIT_DELTA worktree_status Git Status in worktree.
---@return GIT_DELTA index_status Git Status in index.
---@return GIT_ERROR return_code Git return code.
function Repository:status_file(path)
local worktree_status, index_status = libgit2.GIT_DELTA.UNMODIFIED, libgit2.GIT_DELTA.UNMODIFIED
local c_status = libgit2.unsigned_int_array(1)
local err = libgit2.C.git_status_file(c_status, self.repo, path)
if err ~= 0 then
return worktree_status, index_status, err
end
local status = tonumber(c_status[0])
if status ~= nil then
if bit.band(status, libgit2.GIT_STATUS.WT_NEW) ~= 0 then
worktree_status = libgit2.GIT_DELTA.UNTRACKED
index_status = libgit2.GIT_DELTA.UNTRACKED
elseif bit.band(status, libgit2.GIT_STATUS.WT_MODIFIED) ~= 0 then
worktree_status = libgit2.GIT_DELTA.MODIFIED
elseif bit.band(status, libgit2.GIT_STATUS.WT_DELETED) ~= 0 then
worktree_status = libgit2.GIT_DELTA.DELETED
elseif bit.band(status, libgit2.GIT_STATUS.WT_TYPECHANGE) ~= 0 then
worktree_status = libgit2.GIT_DELTA.TYPECHANGE
elseif bit.band(status, libgit2.GIT_STATUS.WT_UNREADABLE) ~= 0 then
worktree_status = libgit2.GIT_DELTA.UNREADABLE
gitextract_6oebunai/
├── .cargo/
│ └── config.toml
├── .gitignore
├── .stylua.toml
├── Cargo.toml
├── LICENSE
├── README.md
├── flake.nix
├── lua/
│ └── blink/
│ ├── chartoggle/
│ │ ├── config.lua
│ │ └── init.lua
│ ├── clue/
│ │ └── init.lua
│ ├── config.lua
│ ├── dashboard/
│ │ └── init.lua
│ ├── init.lua
│ ├── render/
│ │ └── types.lua
│ ├── select/
│ │ ├── config.lua
│ │ ├── init.lua
│ │ ├── providers/
│ │ │ ├── buffers.lua
│ │ │ ├── code-actions.lua
│ │ │ ├── diagnostics.lua
│ │ │ ├── lsp/
│ │ │ │ ├── definitions.lua
│ │ │ │ ├── references.lua
│ │ │ │ └── symbols.lua
│ │ │ ├── recent-commands.lua
│ │ │ ├── recent-searches.lua
│ │ │ ├── smart-open.lua
│ │ │ └── yank-history.lua
│ │ ├── renderer.lua
│ │ ├── types.lua
│ │ └── window.lua
│ └── tree/
│ ├── binds/
│ │ ├── activate.lua
│ │ ├── basic.lua
│ │ ├── expand.lua
│ │ ├── init.lua
│ │ └── move.lua
│ ├── config.lua
│ ├── git/
│ │ ├── git2.lua
│ │ ├── ignore.lua
│ │ ├── init.lua
│ │ ├── libgit2.lua
│ │ └── stat.lua
│ ├── init.lua
│ ├── lib/
│ │ ├── fs.lua
│ │ ├── tree.lua
│ │ ├── utils.lua
│ │ └── uv.lua
│ ├── popup.lua
│ ├── renderer.lua
│ ├── tree.lua
│ └── window.lua
├── scripts/
│ ├── dual_log.sh
│ ├── dual_push.sh
│ └── dual_sync.sh
└── src/
├── job/
│ ├── default.rs
│ ├── mod.rs
│ ├── options.rs
│ ├── pty.rs
│ └── trait.rs
└── lib.rs
SYMBOL INDEX (12 symbols across 3 files)
FILE: src/job/default.rs
type Job (line 5) | struct Job {
method new (line 18) | fn new(id: usize, cmd: &Vec<String>, options: JobStartOptions) -> Self {}
FILE: src/job/options.rs
type JobStartOptions (line 5) | pub struct JobStartOptions {
method from_lua (line 36) | fn from_lua(value: LuaValue, _lua: &'_ Lua) -> LuaResult<Self> {
FILE: src/job/pty.rs
type JobPty (line 5) | struct JobPty {
method new (line 18) | fn new(id: usize, cmd: &Vec<String>, options: JobStartOptions) -> Resu...
method send (line 63) | fn send(&mut self, data: &[u8]) -> Result<()> {
method pid (line 68) | fn pid(&self) -> u32 {
method stop (line 72) | fn stop(&mut self) {
method poll (line 77) | fn poll(&mut self) -> Result<bool> {
method poll_stdout (line 82) | fn poll_stdout(&mut self) -> Result<bool> {
method poll_exit (line 101) | fn poll_exit(&mut self) -> Result<bool> {
Condensed preview — 58 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (345K chars).
[
{
"path": ".cargo/config.toml",
"chars": 224,
"preview": "[target.x86_64-apple-darwin]\nrustflags = [\n \"-C\", \"link-arg=-undefined\",\n \"-C\", \"link-arg=dynamic_lookup\",\n]\n\n[target."
},
{
"path": ".gitignore",
"chars": 52,
"preview": ".archive.lua\ndual/\nresult\n.direnv\n.devenv\n\n/target\n\n"
},
{
"path": ".stylua.toml",
"chars": 179,
"preview": "column_width = 120\nline_endings = \"Unix\"\nindent_type = \"Spaces\"\nindent_width = 2\nquote_style = \"AutoPreferSingle\"\ncall_p"
},
{
"path": "Cargo.toml",
"chars": 277,
"preview": "[package]\nname = \"blink_delimiters\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[lib]\ncrate-type = [\"cdylib\"]\n\n[dependencies]\nan"
},
{
"path": "LICENSE",
"chars": 1066,
"preview": "MIT License\n\nCopyright (c) 2024 Liam Dyer\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\n"
},
{
"path": "README.md",
"chars": 2818,
"preview": "<div align=\"center\">\n\n# blink.nvim\n\nExperimental library of neovim plugins with a focus on performance and simplicity\n\n<"
},
{
"path": "flake.nix",
"chars": 1204,
"preview": "{\n description = \"Set of simple, performant neovim plugins\";\n\n inputs = {\n nixpkgs.url = \"github:nixos/nixpkgs/nixp"
},
{
"path": "lua/blink/chartoggle/config.lua",
"chars": 227,
"preview": "local M = {}\n\nM.default = {\n delimiters = { ',', ';' },\n}\n\nfunction M.setup(opts) M.config = vim.tbl_deep_extend('force"
},
{
"path": "lua/blink/chartoggle/init.lua",
"chars": 1316,
"preview": "local M = {}\n\nfunction M.setup(opts) require('blink.chartoggle.config').setup(opts) end\n\n-- implementation from https://"
},
{
"path": "lua/blink/clue/init.lua",
"chars": 78561,
"preview": "--- NOTE: This is just mini.clue with support for backspace\n---\n--- *mini.clue* Show next key clues\n--- *MiniClue*\n---\n-"
},
{
"path": "lua/blink/config.lua",
"chars": 385,
"preview": "local M = {}\n\nM.default = {\n chartoggle = {\n enabled = false,\n delimiters = { ',', ';' },\n },\n clue = {\n ena"
},
{
"path": "lua/blink/dashboard/init.lua",
"chars": 1504,
"preview": "local api = vim.api\nlocal Dashboard = {}\n\nlocal function create_buf()\n local bufnr = api.nvim_create_buf(false, true)\n\n"
},
{
"path": "lua/blink/init.lua",
"chars": 672,
"preview": "local M = {}\n\nfunction M.setup(opts)\n local config = require('blink.config')\n config.setup(opts)\n\n if config.chartogg"
},
{
"path": "lua/blink/render/types.lua",
"chars": 905,
"preview": "--- 0-1 is interpretted as percentage of parent and whole numbers are the number of columns.\n--- When an array, the mini"
},
{
"path": "lua/blink/select/config.lua",
"chars": 1094,
"preview": "--- @class SelectMapping\n--- @field selection string[]\n--- @field quit string[]\n--- @field next_page string[]\n--- @field"
},
{
"path": "lua/blink/select/init.lua",
"chars": 342,
"preview": "local select = {}\n\n--- @param opts SelectConfig\nfunction select.setup(opts)\n require('blink.select.config').setup(opts)"
},
{
"path": "lua/blink/select/providers/buffers.lua",
"chars": 2536,
"preview": "--- @class SelectProvider\nlocal buffers = {\n name = 'Buffers',\n}\n\nfunction buffers.get_items(opts, cb)\n local idx = 1\n"
},
{
"path": "lua/blink/select/providers/code-actions.lua",
"chars": 1885,
"preview": "--- @class SelectProvider\nlocal code_actions = {\n name = 'Code Actions',\n}\n\nfunction code_actions.get_items(opts, cb)\n "
},
{
"path": "lua/blink/select/providers/diagnostics.lua",
"chars": 2365,
"preview": "--- @class SelectProvider\nlocal diagnostics = {\n name = 'Diagnostics',\n}\n\nfunction diagnostics.get_items(opts, cb)\n lo"
},
{
"path": "lua/blink/select/providers/lsp/definitions.lua",
"chars": 0,
"preview": ""
},
{
"path": "lua/blink/select/providers/lsp/references.lua",
"chars": 0,
"preview": ""
},
{
"path": "lua/blink/select/providers/lsp/symbols.lua",
"chars": 143,
"preview": "local symbols = {}\n\nlocal function symbols.get_items(opts)\n local symbols = {}\n local params = vim.lsp.util.make_posit"
},
{
"path": "lua/blink/select/providers/recent-commands.lua",
"chars": 0,
"preview": ""
},
{
"path": "lua/blink/select/providers/recent-searches.lua",
"chars": 1356,
"preview": "--- @class SelectProvider\nlocal recent_searches = {\n name = 'Recent Searches',\n}\n\nlocal function reverse(tab)\n for i ="
},
{
"path": "lua/blink/select/providers/smart-open.lua",
"chars": 3046,
"preview": "local DbClient = require('telescope._extensions.smart_open.dbclient')\nlocal config = require('smart-open').config\nlocal "
},
{
"path": "lua/blink/select/providers/yank-history.lua",
"chars": 2256,
"preview": "--- @class SelectProvider\nlocal yank_history = {\n name = 'Yank History',\n}\n\n-- Initialize yank history table\nlocal hist"
},
{
"path": "lua/blink/select/renderer.lua",
"chars": 912,
"preview": "local api = vim.api\nlocal config = require('blink.select.config')\nlocal renderer = {}\n\nfunction renderer.new(bufnr)\n lo"
},
{
"path": "lua/blink/select/types.lua",
"chars": 714,
"preview": "--- @class RenderFragment\n--- @field [1] string\n--- @field highlight? string\n---\n--- @class SelectItem\n--- @field fragme"
},
{
"path": "lua/blink/select/window.lua",
"chars": 8344,
"preview": "local window = {\n bufnr = -1,\n winnr = -1,\n}\n\nlocal api = vim.api\nlocal augroup = vim.api.nvim_create_augroup('BlinkSe"
},
{
"path": "lua/blink/tree/binds/activate.lua",
"chars": 681,
"preview": "local api = vim.api\n\nlocal function activate(hovered_node, inst)\n if hovered_node == nil then return end\n\n -- todo: us"
},
{
"path": "lua/blink/tree/binds/basic.lua",
"chars": 1491,
"preview": "local Basic = {}\n\nfunction Basic.new_file(hovered_node, inst)\n while hovered_node ~= nil and hovered_node.is_dir == fal"
},
{
"path": "lua/blink/tree/binds/expand.lua",
"chars": 284,
"preview": "local function expand(hovered_node, inst)\n if hovered_node == nil then return end\n\n if hovered_node.is_dir then\n if"
},
{
"path": "lua/blink/tree/binds/init.lua",
"chars": 903,
"preview": "local api = vim.api\n\nlocal Binds = {}\n\nfunction Binds.attach_to_instance(inst)\n local function map(mode, lhs, callback,"
},
{
"path": "lua/blink/tree/binds/move.lua",
"chars": 1172,
"preview": "local Move = {}\n\nfunction Move.cut(node, inst)\n node.flags.copy = false\n node.flags.cut = not node.flags.cut\n inst.re"
},
{
"path": "lua/blink/tree/config.lua",
"chars": 381,
"preview": "local M = {}\n\nM.default = {\n hidden_by_default = true,\n hide_dotfiles = false,\n hide = {\n '.direnv',\n '.devenv'"
},
{
"path": "lua/blink/tree/git/git2.lua",
"chars": 109095,
"preview": "-- Made by SuperBo in Fugit2\n-- https://github.com/SuperBo/fugit2.nvim/tree/70662d529fe98790d7b2104b4dd67dd229332194\n-- "
},
{
"path": "lua/blink/tree/git/ignore.lua",
"chars": 939,
"preview": "local uv = vim.uv\nlocal ignore = {}\n\nfunction ignore.new(path)\n local self = setmetatable({}, { __index = ignore })\n s"
},
{
"path": "lua/blink/tree/git/init.lua",
"chars": 4200,
"preview": "local Git = {}\n\nlocal function debounce(func, wait)\n local timer\n return function(...)\n local args = { ... }\n if"
},
{
"path": "lua/blink/tree/git/libgit2.lua",
"chars": 53598,
"preview": "-- Made by SuperBo in Fugit2\n-- https://github.com/SuperBo/fugit2.nvim/tree/70662d529fe98790d7b2104b4dd67dd229332194\n-- "
},
{
"path": "lua/blink/tree/git/stat.lua",
"chars": 1294,
"preview": "-- Made by SuperBo in Fugit2\n-- https://github.com/SuperBo/fugit2.nvim/tree/70662d529fe98790d7b2104b4dd67dd229332194\n-- "
},
{
"path": "lua/blink/tree/init.lua",
"chars": 1950,
"preview": "-- todo: symlinks\nlocal api = vim.api\nlocal M = {\n inst = nil,\n}\n\nfunction M.setup(opts)\n require('blink.tree.config')"
},
{
"path": "lua/blink/tree/lib/fs.lua",
"chars": 4229,
"preview": "-- todo: manage number of open files to ensure we don't go over limit\n-- likely via a queue of some sort\n\nlocal sep = '/"
},
{
"path": "lua/blink/tree/lib/tree.lua",
"chars": 5548,
"preview": "local allConfig = require('blink.tree.config')\nlocal config = {\n hide_dotfiles = allConfig.hide_dotfiles,\n hide = allC"
},
{
"path": "lua/blink/tree/lib/utils.lua",
"chars": 1431,
"preview": "local api = vim.api\nlocal Utils = {}\n\nfunction Utils.pick_or_create_non_special_window()\n local wins = api.nvim_list_wi"
},
{
"path": "lua/blink/tree/lib/uv.lua",
"chars": 698,
"preview": "local uv = vim.loop\nlocal UV = {}\n\nfunction UV.exec_async(opts, callback)\n callback = callback or function() end\n loca"
},
{
"path": "lua/blink/tree/popup.lua",
"chars": 2050,
"preview": "local api = vim.api\n\nlocal Popup = {}\n\n-- function Popup.new()\n-- self.winnr = nil\n-- self.bufnr = nil\n-- return s"
},
{
"path": "lua/blink/tree/renderer.lua",
"chars": 6376,
"preview": "local api = vim.api\nlocal lib_tree = require('blink.tree.lib.tree')\n\nlocal ns = api.nvim_create_namespace('blink_tree')\n"
},
{
"path": "lua/blink/tree/tree.lua",
"chars": 2413,
"preview": "local fs = require('blink.tree.lib.fs')\nlocal lib_tree = require('blink.tree.lib.tree')\nlocal Tree = {}\n\nfunction Tree.n"
},
{
"path": "lua/blink/tree/window.lua",
"chars": 7374,
"preview": "local api = vim.api\n\nlocal Window = {}\n\nfunction Window.new()\n local self = setmetatable({}, { __index = Window })\n se"
},
{
"path": "scripts/dual_log.sh",
"chars": 406,
"preview": "# Written by echasnovski in mini.nvim\n# https://github.com/echasnovski/mini.nvim/blob/82584a42c636efd11781211da1396f4c1f"
},
{
"path": "scripts/dual_push.sh",
"chars": 523,
"preview": "# Written by echasnovski in mini.nvim\n# https://github.com/echasnovski/mini.nvim/blob/82584a42c636efd11781211da1396f4c1f"
},
{
"path": "scripts/dual_sync.sh",
"chars": 1912,
"preview": "# Written by echasnovski in mini.nvim\n# https://github.com/echasnovski/mini.nvim/blob/82584a42c636efd11781211da1396f4c1f"
},
{
"path": "src/job/default.rs",
"chars": 502,
"preview": "use crate::options::JobStartOptions;\nuse portable_pty::{native_pty_system, CommandBuilder, PtySize, PtySystem};\nuse std:"
},
{
"path": "src/job/mod.rs",
"chars": 47,
"preview": "pub mod default;\npub mod options;\npub mod pty;\n"
},
{
"path": "src/job/options.rs",
"chars": 2852,
"preview": "use mlua::prelude::*;\nuse std::collections::HashMap;\n\n#[derive(Clone)]\npub struct JobStartOptions {\n pub cwd: String,"
},
{
"path": "src/job/pty.rs",
"chars": 3122,
"preview": "use crate::options::JobStartOptions;\nuse anyhow::Result;\nuse portable_pty::{Child, CommandBuilder, PtyPair, PtySize, nat"
},
{
"path": "src/job/trait.rs",
"chars": 0,
"preview": ""
},
{
"path": "src/lib.rs",
"chars": 1064,
"preview": "// mod job;\n// use crate::job::*;\n//\n// static ID_COUNTER: AtomicUsize = AtomicUsize::new(0);\n// static JOBS: LazyLock<M"
}
]
About this extraction
This page contains the full source code of the Saghen/blink.nvim GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 58 files (323.2 KB), approximately 90.8k tokens, and a symbol index with 12 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.