Full Code of Saghen/blink.nvim for AI

main d1e7c7c45d45 cached
58 files
323.2 KB
90.8k tokens
12 symbols
1 requests
Download .txt
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
   
Download .txt
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
Download .txt
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.

Copied to clipboard!