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 ================================================
# blink.nvim Experimental library of neovim plugins with a focus on performance and simplicity
## 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 { '', 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 { '', 'BlinkTree reveal', desc = 'Reveal current file in tree' }, { 'E', 'BlinkTree toggle', desc = 'Reveal current file in tree' }, { 'e', 'BlinkTree toggle-focus', 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' -- -- 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 `` removes previous user entry. --- Pressing `` or `` 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 ``. --- 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 `` as Normal mode trigger means that --- there should not be another `` 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 --- `:map ` (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 ``. --- - 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 `` 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 `` or ``, stop the process without any action. --- --- - If ``, stop the process and execute current key query, meaning --- emulate (with |nvim_feedkeys()|) user pressing those keys. --- --- - If ``, 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`; `` / `` 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 || mappings in Normal and Visual modes: > --- --- local miniclue = require('mini.clue') --- miniclue.setup({ --- triggers = { --- -- Leader triggers --- { mode = 'n', keys = '' }, --- { mode = 'x', keys = '' }, --- --- -- Built-in completion --- { mode = 'i', keys = '' }, --- --- -- `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 = '' }, --- { mode = 'c', keys = '' }, --- --- -- Window commands --- { mode = 'n', keys = '' }, --- --- -- `z` key --- { mode = 'n', keys = 'z' }, --- { mode = 'x', keys = 'z' }, --- }, --- --- clues = { --- -- Enhance this by adding descriptions for 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 || mappings set up: > --- --- -- Set `` before making any mappings and configuring 'mini.clue' --- vim.g.mapleader = ' ' --- --- local nmap_leader = function(suffix, rhs, desc) --- vim.keymap.set('n', '' .. suffix, rhs, { desc = desc }) --- end --- local xmap_leader = function(suffix, rhs, desc) --- vim.keymap.set('x', '' .. suffix, rhs, { desc = desc }) --- end --- --- nmap_leader('bd', 'lua MiniBufremove.delete()', 'Delete') --- nmap_leader('bw', 'lua MiniBufremove.wipeout()', 'Wipeout') --- --- nmap_leader('lf', 'lua vim.lsp.buf.format()', 'Format') --- xmap_leader('lf', 'lua vim.lsp.buf.format()', 'Format') --- nmap_leader('lr', 'lua vim.lsp.buf.rename()', 'Rename') --- nmap_leader('lR', 'lua vim.lsp.buf.references()', 'References') --- --- --- The following setup will enable || as trigger in Normal and Visual --- modes and add descriptions to mapping groups: > --- --- require('mini.clue').setup({ --- -- Register `` as trigger --- triggers = { --- { mode = 'n', keys = '' }, --- { mode = 'x', keys = '' }, --- }, --- --- -- Add descriptions for mapping groups --- clues = { --- { mode = 'n', keys = 'b', desc = '+Buffers' }, --- { mode = 'n', keys = '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 `` a trigger. Otherwise, key query process won't start. --- triggers = { --- { mode = 'i', keys = '' }, --- }, --- --- -- Register custom clues --- clues = { --- { mode = 'i', keys = '', desc = 'File names' }, --- { mode = 'i', keys = '', desc = 'Whole lines' }, --- { mode = 'i', keys = '', desc = 'Omni completion' }, --- { mode = 'i', keys = '', desc = 'Spelling suggestions' }, --- { mode = 'i', keys = '', 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 `m` to start submode. --- - Press any of `h`/`j`/`k`/`l` to move selection/line. --- - Press `` to stop submode. --- --- The code: > --- --- require('mini.move').setup({ --- mappings = { --- left = 'mh', --- right = 'ml', --- down = 'mj', --- up = 'mk', --- line_left = 'mh', --- line_right = 'ml', --- line_down = 'mj', --- line_up = 'mk', --- }, --- }) --- --- require('mini.clue').setup({ --- triggers = { --- { mode = 'n', keys = 'm' }, --- { mode = 'x', keys = 'm' }, --- }, --- clues = { --- { mode = 'n', keys = 'mh', postkeys = 'm' }, --- { mode = 'n', keys = 'mj', postkeys = 'm' }, --- { mode = 'n', keys = 'mk', postkeys = 'm' }, --- { mode = 'n', keys = 'ml', postkeys = 'm' }, --- { mode = 'x', keys = 'mh', postkeys = 'm' }, --- { mode = 'x', keys = 'mj', postkeys = 'm' }, --- { mode = 'x', keys = 'mk', postkeys = 'm' }, --- { mode = 'x', keys = 'ml', postkeys = '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 `` 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 `` to start key query process. --- - Press keys which move / change focus / resize windows. --- - Press `` to stop submode. --- --- The code: > --- --- local miniclue = require('mini.clue') --- miniclue.setup({ --- triggers = { --- { mode = 'n', keys = '' }, --- }, --- clues = { --- miniclue.gen_clues.windows({ --- submode_move = true, --- submode_navigate = true, --- submode_resize = true, --- }) --- }, --- }) --- --- # Window config ~ --- > --- require('mini.clue').setup({ --- triggers = { { mode = 'n', keys = '' } }, --- --- 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 || 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: --- - `(string)` - single character describing **single** mode short-name of --- key combination as in `nvim_set_keymap()` ('n', 'x', 'i', 'o', 'c', etc.). --- - `(string)` - key combination for which clue will be shown. --- "Human-readable" key names as in |key-notation| (like "", "", --- "", etc.) are allowed. --- - `(string|nil)` - optional key combination description which will --- be shown in clue window. --- - `(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 and --- 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: --- - field can be equal to `"auto"` leading to window width being --- computed automatically based on its content. Default is fixed width of 30. --- - and 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 = '', scroll_up = '', }, } --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 = '' } --- ---@return table Array of clues. MiniClue.gen_clues.builtin_completion = function() --stylua: ignore return { { mode = 'i', keys = '', desc = 'Defined identifiers' }, { mode = 'i', keys = '', desc = 'Scroll up' }, { mode = 'i', keys = '', desc = 'File names' }, { mode = 'i', keys = '', desc = 'Identifiers' }, { mode = 'i', keys = '', desc = 'Identifiers from dictionary' }, { mode = 'i', keys = '', desc = 'Whole lines' }, { mode = 'i', keys = '', desc = 'Next completion' }, { mode = 'i', keys = '', desc = 'Omni completion' }, { mode = 'i', keys = '', desc = 'Previous completion' }, { mode = 'i', keys = '', desc = 'Spelling suggestions' }, { mode = 'i', keys = '', desc = 'Identifiers from thesaurus' }, { mode = 'i', keys = '', desc = 'Scroll down' }, { mode = 'i', keys = '', desc = "With 'completefunc'" }, { mode = 'i', keys = '', desc = 'Like in command line' }, { mode = 'i', keys = '', desc = 'Stop completion' }, { mode = 'i', keys = '', desc = 'Tags' }, { mode = 'i', keys = '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', desc = '`:tjump` to tag under cursor' }, { mode = 'n', keys = 'g', desc = 'Dump a memory profile' }, { mode = 'n', keys = 'g', desc = 'Show information about cursor' }, { mode = 'n', keys = 'g', desc = 'Start Select block mode' }, { mode = 'n', keys = 'g', 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', desc = '`:tjump` to selected tag' }, { mode = 'x', keys = 'g', desc = 'Increment with compound' }, { mode = 'x', keys = 'g', desc = 'Show information about selection' }, { mode = 'x', keys = 'g', 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 = '' } --- { mode = 'c', keys = '' } --- ---@param opts table|nil Options. Possible keys: --- - `(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', ''), { mode = 'i', keys = '', desc = '+Insert literally' }, describe_registers('i', ''), { mode = 'i', keys = '', desc = '+Insert literally + not auto-indent' }, describe_registers('i', ''), { mode = 'i', keys = '', desc = '+Insert + fix indent' }, describe_registers('i', ''), -- Command-line mode describe_registers('c', ''), { mode = 'c', keys = '', desc = '+Insert literally' }, describe_registers('c', ''), { mode = 'c', keys = '', desc = '+Insert literally' }, describe_registers('c', ''), } end --- Generate clues for window commands --- --- Contains clues for the following triggers: > --- --- { mode = 'n', keys = '' } --- --- Note: only non-duplicated commands are included. For full list see |CTRL-W|. --- ---@param opts table|nil Options. Possible keys: --- - `(boolean)` - whether to make move (change layout) --- commands a submode by using `postkeys` field. Default: `false`. --- - `(boolean)` - whether to make navigation (change --- focus) commands a submode by using `postkeys` field. Default: `false`. --- - `(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 = '' end if opts.submode_navigate then postkeys_navigate = '' end if opts.submode_resize then postkeys_resize = '' end --stylua: ignore return { { mode = 'n', keys = '+', desc = 'Increase height', postkeys = postkeys_resize }, { mode = 'n', keys = '-', desc = 'Decrease height', postkeys = postkeys_resize }, { mode = 'n', keys = '<', desc = 'Decrease width', postkeys = postkeys_resize }, { mode = 'n', keys = '>', desc = 'Increase width', postkeys = postkeys_resize }, { mode = 'n', keys = '=', desc = 'Make windows same dimensions' }, { mode = 'n', keys = ']', desc = 'Split + jump to tag' }, { mode = 'n', keys = '^', desc = 'Split + edit alternate file' }, { mode = 'n', keys = '_', desc = 'Set height (def: very high)' }, { mode = 'n', keys = '|', desc = 'Set width (def: very wide)' }, { mode = 'n', keys = '}', desc = 'Show tag in preview' }, { mode = 'n', keys = 'b', desc = 'Focus bottom', postkeys = postkeys_navigate }, { mode = 'n', keys = 'c', desc = 'Close' }, { mode = 'n', keys = 'd', desc = 'Split + jump to definition' }, { mode = 'n', keys = 'F', desc = 'Split + edit file name + jump' }, { mode = 'n', keys = 'f', desc = 'Split + edit file name' }, { mode = 'n', keys = 'g', desc = '+Extra actions' }, { mode = 'n', keys = 'g]', desc = 'Split + list tags' }, { mode = 'n', keys = 'g}', desc = 'Do `:ptjump`' }, { mode = 'n', keys = 'g', desc = 'Split + jump to tag with `:tjump`' }, { mode = 'n', keys = 'g', desc = 'Focus last accessed tab', postkeys = postkeys_navigate }, { mode = 'n', keys = 'gF', desc = 'New tabpage + edit file name + jump' }, { mode = 'n', keys = 'gf', desc = 'New tabpage + edit file name' }, { mode = 'n', keys = 'gT', desc = 'Focus previous tabpage', postkeys = postkeys_navigate }, { mode = 'n', keys = 'gt', desc = 'Focus next tabpage', postkeys = postkeys_navigate }, { mode = 'n', keys = 'H', desc = 'Move to very left', postkeys = postkeys_move }, { mode = 'n', keys = 'h', desc = 'Focus left', postkeys = postkeys_navigate }, { mode = 'n', keys = 'i', desc = 'Split + jump to declaration' }, { mode = 'n', keys = 'J', desc = 'Move to very bottom', postkeys = postkeys_move }, { mode = 'n', keys = 'j', desc = 'Focus down', postkeys = postkeys_navigate }, { mode = 'n', keys = 'K', desc = 'Move to very top', postkeys = postkeys_move }, { mode = 'n', keys = 'k', desc = 'Focus up', postkeys = postkeys_navigate }, { mode = 'n', keys = 'L', desc = 'Move to very right', postkeys = postkeys_move }, { mode = 'n', keys = 'l', desc = 'Focus right', postkeys = postkeys_navigate }, { mode = 'n', keys = 'n', desc = 'Open new' }, { mode = 'n', keys = 'o', desc = 'Close all but current' }, { mode = 'n', keys = 'P', desc = 'Focus preview', postkeys = postkeys_navigate }, { mode = 'n', keys = 'p', desc = 'Focus last accessed', postkeys = postkeys_navigate }, { mode = 'n', keys = 'q', desc = 'Quit current' }, { mode = 'n', keys = 'R', desc = 'Rotate up/left', postkeys = postkeys_move }, { mode = 'n', keys = 'r', desc = 'Rotate down/right', postkeys = postkeys_move }, { mode = 'n', keys = 's', desc = 'Split horizontally' }, { mode = 'n', keys = 'T', desc = 'Create new tabpage + move' }, { mode = 'n', keys = 't', desc = 'Focus top', postkeys = postkeys_navigate }, { mode = 'n', keys = 'v', desc = 'Split vertically' }, { mode = 'n', keys = 'W', desc = 'Focus previous', postkeys = postkeys_navigate }, { mode = 'n', keys = 'w', desc = 'Focus next', postkeys = postkeys_navigate }, { mode = 'n', keys = 'x', desc = 'Exchange windows', postkeys = postkeys_move }, { mode = 'n', keys = '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('', true, true, true), -- cr = vim.api.nvim_replace_termcodes('', true, true, true), exit = vim.api.nvim_replace_termcodes([[]], true, true, true), ctrl_d = vim.api.nvim_replace_termcodes('', true, true, true), ctrl_u = vim.api.nvim_replace_termcodes('', 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 ``. 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 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 ``) 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 `` ('\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 `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 ''. -- - 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 '' moves cursor one space to left (same as `i`). -- 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 ``, ``, 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 `` 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('', '<') return res end else H.keytrans = function(x) local res = x:gsub('', '<') 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 ) or it is `` 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 = { '' }, next_page = { '' }, quit = { 'q', '' }, }, 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', '', activate) map('n', '<2-LeftMouse>', activate) map('n', '', 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 elseif bit.band(status, libgit2.GIT_STATUS.IGNORED) ~= 0 then worktree_status = libgit2.GIT_DELTA.IGNORED elseif bit.band(status, libgit2.GIT_STATUS.CONFLICTED) ~= 0 then worktree_status = libgit2.GIT_DELTA.CONFLICTED end if bit.band(status, libgit2.GIT_STATUS.INDEX_NEW) ~= 0 then index_status = libgit2.GIT_DELTA.ADDED elseif bit.band(status, libgit2.GIT_STATUS.INDEX_MODIFIED) ~= 0 then index_status = libgit2.GIT_DELTA.MODIFIED elseif bit.band(status, libgit2.GIT_STATUS.INDEX_DELETED) ~= 0 then index_status = libgit2.GIT_DELTA.DELETED elseif bit.band(status, libgit2.GIT_STATUS.INDEX_TYPECHANGE) ~= 0 then index_status = libgit2.GIT_DELTA.TYPECHANGE end end return worktree_status, index_status, 0 end -- Reads head and upstream status. ---@return GitStatusHead? ---@return GitStatusUpstream? ---@return GIT_ERROR function Repository:status_head_upstream() local err -- Get Head information local repo_head, repo_head_oid, head_status repo_head, err = self:head() if not repo_head then return nil, nil, err end local repo_head_commit, _ = repo_head:peel_commit() repo_head_oid = repo_head_commit and repo_head_commit:id() or nil head_status = { name = repo_head:shorthand(), oid = repo_head_oid, author = repo_head_commit and repo_head_commit:author() or "", message = repo_head_commit and repo_head_commit:message() or "", is_detached = self:is_head_detached(), namespace = repo_head.namespace, refname = repo_head.name, } -- Get upstream information local repo_upstream, upstream_status repo_upstream, err = repo_head and repo_head:branch_upstream() or nil, 0 if repo_upstream then ---@type number local ahead, behind = 0, 0 local commit_local = repo_head_oid -- local commit_upstream, _ = repo_upstream:target() local commit_upstream, _ = repo_upstream:peel_commit() local commit_upstream_oid = commit_upstream and commit_upstream:id() or nil if commit_upstream_oid and commit_local then local nilable_ahead, nilable_behind, _ = self:ahead_behind(commit_local, commit_upstream_oid) if nilable_ahead ~= nil and nilable_behind ~= nil then ahead, behind = nilable_ahead, nilable_behind end end local remote local remote_name = repo_upstream:remote_name() if remote_name then remote, _ = self:remote_lookup(remote_name) end upstream_status = { name = repo_upstream:shorthand(), oid = commit_upstream_oid, message = commit_upstream and commit_upstream:message() or "", author = commit_upstream and commit_upstream:author() or "", ahead = ahead, behind = behind, remote = remote and remote.name or "", remote_url = remote and remote.url or "", } end return head_status, upstream_status, 0 end -- Parse information from git_status_list and convert to GitStatusItem[] ---@param git_status_list ffi.cdata* git_status_list pointer ---@return GitStatusItem[] local function git_status_list_to_items(git_status_list) local n_entry = tonumber(libgit2.C.git_status_list_entrycount(git_status_list)) or 0 ---@type GitStatusItem[] local status_list = table_new(n_entry, 0) -- Iterate through git status list for i = 0, n_entry - 1 do local entry = libgit2.C.git_status_byindex(git_status_list, i) if entry == nil or entry.status == libgit2.GIT_STATUS.CURRENT then goto git_status_list_continue end ---@type GitStatusItem local status_item = { path = "", worktree_status = libgit2.GIT_DELTA.UNMODIFIED, index_status = libgit2.GIT_DELTA.UNMODIFIED, renamed = false, } ---@type string local old_path, new_path if entry.index_to_workdir ~= nil then old_path = ffi.string(entry.index_to_workdir.old_file.path) new_path = ffi.string(entry.index_to_workdir.new_file.path) status_item.path = old_path status_item.worktree_status = entry.index_to_workdir.status if bit.band(entry.status, libgit2.GIT_STATUS.WT_NEW) ~= 0 then status_item.worktree_status = libgit2.GIT_DELTA.UNTRACKED status_item.index_status = libgit2.GIT_DELTA.UNTRACKED end if bit.band(entry.status, libgit2.GIT_STATUS.WT_RENAMED) ~= 0 then status_item.renamed = true status_item.new_path = new_path end end if entry.head_to_index ~= nil then old_path = ffi.string(entry.head_to_index.old_file.path) new_path = ffi.string(entry.head_to_index.new_file.path) status_item.path = old_path status_item.index_status = entry.head_to_index.status if bit.band(entry.status, libgit2.GIT_STATUS.INDEX_RENAMED) ~= 0 then status_item.renamed = true status_item.new_path = new_path end end status_list[#status_list + 1] = status_item ::git_status_list_continue:: end return status_list end -- Reads the status of the repository and returns a dictionary. -- with file paths as keys and status flags as values. ---@return GitStatusItem[]? status_result git status result. ---@return integer return_code Return code. function Repository:status() ---@type GIT_ERROR local err local opts = libgit2.git_status_options(libgit2.GIT_STATUS_OPTIONS_INIT) -- libgit2.C.git_status_options_init(opts, libgit2.GIT_STATUS_OPTIONS_VERSION) opts[0].show = libgit2.GIT_STATUS_SHOW.INDEX_AND_WORKDIR opts[0].flags = DEFAULT_STATUS_FLAGS local status = libgit2.git_status_list_double_pointer() err = libgit2.C.git_status_list_new(status, self.repo, opts) if err ~= 0 then return nil, err end local status_list = git_status_list_to_items(status[0]) -- free C resources libgit2.C.git_status_list_free(status[0]) return status_list, 0 end ---Get default remote ---@return GitRemote? ---@return GIT_ERROR function Repository:remote_default() local remote, remotes, err remote, err = self:remote_lookup "origin" if err ~= 0 then remotes, err = self:remote_list() if err ~= 0 then return nil, 0 end if remotes and #remotes > 0 then -- get other remote as default if origin is not found remote, err = self:remote_lookup(remotes[1]) else return nil, 0 end end return remote, err end ---Retrieve pushremote for a branch, if not found ---returns remote from config. ---@param name string branch name ---@return string? function Repository:branch_push_remote(name) if name == "" then return nil end local config, _ = self:config() if not config then return nil end local config_prefix = "branch." .. name return config:get_string(config_prefix .. ".pushremote") or config:get_string(config_prefix .. ".remote") end ---Default signature user and now timestamp. ---@return GitSignature? ---@return GIT_ERROR function Repository:signature_default() local git_signature = libgit2.git_signature_double_pointer() local err = libgit2.C.git_signature_default(git_signature, self.repo) if err ~= 0 then return nil, err end return Signature.new(git_signature[0]), 0 end -- Creates tree and parents object used in -- create_commit and create_commmit_object funcionts. ---@param index GitIndex ---@return ffi.cdata*? ---@return ffi.cdata*? ---@return GIT_ERROR function Repository:_create_commit_head_tree_parents(index) local parents = nil -- get head as parent commit local head, err = self:head() if err ~= 0 and err ~= libgit2.GIT_ERROR.GIT_ENOTFOUND and err ~= libgit2.GIT_ERROR.GIT_EUNBORNBRANCH then return nil, parents, err end local parent = nil if head then parent, err = head:peel_commit() if err ~= 0 then return nil, parents, err end if parent then parents = libgit2.git_commit_double_pointer() parents[0] = parent.commit end end local tree_id tree_id, err = index:write_tree() if not tree_id then return nil, nil, err end local tree = libgit2.git_tree_double_pointer() err = libgit2.C.git_tree_lookup(tree, self.repo, tree_id.oid) if err ~= 0 then return nil, nil, err end return tree, parents, 0 end -- Creates new commit in the repository. ---@param index GitIndex ---@param signature GitSignature ---@param message string ---@return GitObjectId? ---@return GIT_ERROR function Repository:create_commit(index, signature, message) local tree, parents, err = self:_create_commit_head_tree_parents(index) if not tree or err ~= 0 then return nil, err end local git_oid = libgit2.git_oid() err = libgit2.C.git_commit_create( git_oid, self.repo, "HEAD", signature.sign, signature.sign, nil, message, tree[0], parents and 1 or 0, parents and ffi.cast(libgit2.git_commit_const_double_pointer, parents) or nil ) libgit2.C.git_tree_free(tree[0]) if err ~= 0 then return nil, err end return ObjectId.borrow(git_oid), 0 end -- Creates new commit object as string. ---@param index GitIndex ---@param signature GitSignature ---@param message string ---@return string? ---@return GIT_ERROR function Repository:create_commit_content(index, signature, message) local tree, parents, err = self:_create_commit_head_tree_parents(index) if not tree or err ~= 0 then return nil, err end local buf = libgit2.git_buf() err = libgit2.C.git_commit_create_buffer( buf, self.repo, signature.sign, signature.sign, nil, message, tree[0], parents and 1 or 0, parents and ffi.cast(libgit2.git_commit_const_double_pointer, parents) or nil ) libgit2.C.git_tree_free(tree[0]) if err ~= 0 then libgit2.C.git_buf_dispose(buf) return nil, err end local commit_str = ffi.string(buf[0].ptr, buf[0].size) libgit2.C.git_buf_dispose(buf) return commit_str, 0 end -- Creates a commit object from the given content and signature ---@param commit_content string Gitcommit object as string ---@param signature string Gitcommit content signature ---@param signature_field string? signature field ---@return GitObjectId? ---@return GIT_ERROR function Repository:create_commit_with_signature(commit_content, signature, signature_field) local git_oid = libgit2.git_oid() local err = libgit2.C.git_commit_create_with_signature(git_oid, self.repo, commit_content, signature, signature_field) if err ~= 0 then return nil, 0 end return ObjectId.borrow(git_oid), 0 end -- Lookups a blob object from a repository. ---@param id GitObjectId ---@return GitBlob? ---@return GIT_ERROR function Repository:blob_lookup(id) local blob = libgit2.git_blob_double_pointer() local err = libgit2.C.git_blob_lookup(blob, self.repo, id.oid) if err ~= 0 then return nil, err end return Blob.new(blob[0]), 0 end -- Rewords HEAD commit. ---@param signature GitSignature ---@param message string ---@return GitObjectId? ---@return GIT_ERROR function Repository:amend_reword(signature, message) return self:amend(nil, signature, message) end ---Extends new index to HEAD commit. ---@param index GitIndex ---@return GitObjectId? ---@return GIT_ERROR function Repository:amend_extend(index) return self:amend(index, nil, nil) end ---@param index GitIndex? ---@param no_tree boolean don't find tree ---@return GitCommit? ---@return ffi.cdata*? tree tree used in ammend ---@return GIT_ERROR function Repository:_amend_commit_head_tree_parents(index, no_tree) -- get head as parent commit local head, head_commit, err head, err = self:head() if not head then return nil, nil, err end head_commit, err = head:peel_commit() if not head_commit then return nil, nil, err end if no_tree then return head_commit, nil, 0 end local tree = nil if index then local tree_id tree_id, err = index:write_tree() if not tree_id then return nil, nil, err end tree = libgit2.git_tree_double_pointer() err = libgit2.C.git_tree_lookup(tree, self.repo, tree_id.oid) if err ~= 0 then return nil, nil, err end else tree = libgit2.git_tree_double_pointer() err = libgit2.C.git_commit_tree(tree, head_commit.commit) if err ~= 0 then return nil, nil, err end end return head_commit, tree, 0 end ---Amends an existing commit by replacing only non-NULL values. ---@param index GitIndex? ---@param signature GitSignature? ---@param message string? ---@return GitObjectId? ---@return GIT_ERROR function Repository:amend(index, signature, message) local no_tree = not (index or signature or message) local head_commit, tree, err = self:_amend_commit_head_tree_parents(index, no_tree) if not head_commit then return nil, err end if no_tree then return head_commit:id(), 0 end local sig = signature and signature.sign or nil local git_oid = libgit2.git_oid() err = libgit2.C.git_commit_amend( git_oid, head_commit.commit, "HEAD", sig, sig, nil, message, tree ~= nil and tree[0] or nil ) if tree ~= nil then libgit2.C.git_tree_free(tree[0]) end if err ~= 0 then return nil, err end return ObjectId.borrow(git_oid), 0 end -- Creates amend commit as string ---@param index GitIndex? ---@param author GitSignature? ---@param committer GitSignature? ---@param message string? ---@return string? git_commit_content ---@return string? git_commit_message ---@return GIT_ERROR err function Repository:amend_commit_content(index, author, committer, message) local head_commit, tree, err = self:_amend_commit_head_tree_parents(index, false) if not head_commit then return nil, nil, err end author = author or head_commit:author_signature() committer = committer or head_commit:committer_signature() message = message or head_commit:message() local parents local nparents = head_commit:nparents() if nparents > 0 then parents = libgit2.git_commit_pointer_array(nparents) for i = 0, nparents - 1 do parents[i] = nil err = libgit2.C.git_commit_parent(parents + i, head_commit.commit, i) end end local buf = libgit2.git_buf() err = libgit2.C.git_commit_create_buffer( buf, self.repo, author.sign, committer.sign, nil, message, tree ~= nil and tree[0] or nil, nparents, ffi.cast(libgit2.git_commit_const_double_pointer, parents) ) if tree ~= nil then libgit2.C.git_tree_free(tree[0]) end if parents ~= nil then for i = 0, nparents - 1 do if parents[i] ~= nil then libgit2.C.git_commit_free(parents[i]) end end end if err ~= 0 then libgit2.C.git_buf_dispose(buf) return nil, nil, err end local commit_str = ffi.string(buf[0].ptr, buf[0].size) libgit2.C.git_buf_dispose(buf) return commit_str, message, 0 end ---Returns a GitRevisionWalker, cached it for the repo if possible. ---@return GitRevisionWalker? ---@return GIT_ERROR function Repository:walker() if self._walker then return self._walker, 0 end local walker = libgit2.git_revwalk_double_pointer() local err = libgit2.C.git_revwalk_new(walker, self.repo) if err ~= 0 then return nil, err end self._walker = RevisionWalker.new(self.repo, walker[0]) return self._walker, 0 end ---Frees a cached GitRevisionWalker function Repository:free_walker() if self._walker then self._walker = nil end end ---Helper function to keep consistency between ---git_status_options and git_diff_options ---@param status_flag GIT_STATUS_OPT ---@return GIT_DIFF diff_flag ---@return GIT_DIFF_FIND find_flag local function git_status_flags_to_diff_flags(status_flag) local diff_flag = libgit2.GIT_DIFF.INCLUDE_TYPECHANGE local find_flag = libgit2.GIT_DIFF_FIND.FIND_FOR_UNTRACKED if bit.band(status_flag, libgit2.GIT_STATUS_OPT.INCLUDE_UNTRACKED) ~= 0 then diff_flag = bit.bor(diff_flag, libgit2.GIT_DIFF.INCLUDE_UNTRACKED) end if bit.band(status_flag, libgit2.GIT_STATUS_OPT.INCLUDE_IGNORED) ~= 0 then diff_flag = bit.bor(diff_flag, libgit2.GIT_DIFF.INCLUDE_IGNORED) end if bit.band(status_flag, libgit2.GIT_STATUS_OPT.INCLUDE_UNMODIFIED) ~= 0 then diff_flag = bit.bor(diff_flag, libgit2.GIT_DIFF.INCLUDE_UNMODIFIED) end if bit.band(status_flag, libgit2.GIT_STATUS_OPT.RECURSE_UNTRACKED_DIRS) ~= 0 then diff_flag = bit.bor(diff_flag, libgit2.GIT_DIFF.RECURSE_UNTRACKED_DIRS, libgit2.GIT_DIFF.SHOW_UNTRACKED_CONTENT) end if bit.band(status_flag, libgit2.GIT_STATUS_OPT.DISABLE_PATHSPEC_MATCH) ~= 0 then diff_flag = bit.bor(diff_flag, libgit2.GIT_DIFF.DISABLE_PATHSPEC_MATCH) end if bit.band(status_flag, libgit2.GIT_STATUS_OPT.RECURSE_IGNORED_DIRS) ~= 0 then diff_flag = bit.bor(diff_flag, libgit2.GIT_DIFF.RECURSE_IGNORED_DIRS) end if bit.band(status_flag, libgit2.GIT_STATUS_OPT.EXCLUDE_SUBMODULES) ~= 0 then diff_flag = bit.bor(diff_flag, libgit2.GIT_DIFF.IGNORE_SUBMODULES) end if bit.band(status_flag, libgit2.GIT_STATUS_OPT.UPDATE_INDEX) ~= 0 then diff_flag = bit.bor(diff_flag, libgit2.GIT_DIFF.UPDATE_INDEX) end if bit.band(status_flag, libgit2.GIT_STATUS_OPT.INCLUDE_UNREADABLE) ~= 0 then diff_flag = bit.bor(diff_flag, libgit2.GIT_DIFF.INCLUDE_UNREADABLE) end if bit.band(status_flag, libgit2.GIT_STATUS_OPT.INCLUDE_UNREADABLE_AS_UNTRACKED) ~= 0 then diff_flag = bit.bor(diff_flag, libgit2.GIT_DIFF.INCLUDE_UNREADABLE_AS_UNTRACKED) end if bit.band(status_flag, libgit2.GIT_STATUS_OPT.RENAMES_FROM_REWRITES) ~= 0 then find_flag = bit.bor( find_flag, libgit2.GIT_DIFF_FIND.FIND_AND_BREAK_REWRITES, libgit2.GIT_DIFF_FIND.FIND_RENAMES_FROM_REWRITES, libgit2.GIT_DIFF_FIND.BREAK_REWRITES_FOR_RENAMES_ONLY ) end return diff_flag, find_flag end -- Gets diff from index to workdir ---@param index GitIndex? Repository index, can be null ---@param paths string[]? Git paths, can be null ---@param reverse? boolean whether to reverse the diff ---@param context_lines integer? number of context lines ---@return GitDiff? ---@return GIT_ERROR function Repository:diff_index_to_workdir(index, paths, reverse, context_lines) return self:diff_helper(true, true, index, paths, reverse, context_lines) end -- Gets diff from head to index ---@param index GitIndex? Repository index, can be null ---@param paths string[]? Git paths, can be null ---@param reverse? boolean whether to reverse the diff ---@param context_lines integer? number of context lines ---@return GitDiff? ---@return GIT_ERROR function Repository:diff_head_to_index(index, paths, reverse, context_lines) return self:diff_helper(false, true, index, paths, reverse, context_lines) end -- Gets diff from head to workdir ---@param index GitIndex? Repository index, can be null ---@param paths string[]? Git paths, can be null ---@param reverse? boolean whether to reverse the diff ---@param context_lines integer? number of context lines ---@return GitDiff? ---@return GIT_ERROR function Repository:diff_head_to_workdir(index, paths, reverse, context_lines) return self:diff_helper(true, false, index, paths, reverse, context_lines) end ---@param include_workdir boolean Whether to do include workd_dir in diff target ---@param include_index boolean Wheter to include index in diff target ---@param index GitIndex? Repository index, can be null ---@param paths string[]? Git paths, can be null ---@param reverse boolean? Reverse diff ---@param context_lines integer? number of context lines ---@return GitDiff? ---@return GIT_ERROR function Repository:diff_helper(include_workdir, include_index, index, paths, reverse, context_lines) local c_paths, err local opts = libgit2.git_diff_options(libgit2.GIT_DIFF_OPTIONS_INIT) local find_opts = libgit2.git_diff_find_options(libgit2.GIT_DIFF_FIND_OPTIONS_INIT) local diff = libgit2.git_diff_double_pointer() opts[0].id_abbrev = 8 opts[0].flags, find_opts[0].flags = git_status_flags_to_diff_flags(DEFAULT_STATUS_FLAGS) if reverse then opts[0].flags = bit.bor(opts[0].flags, libgit2.GIT_DIFF.REVERSE) end if paths and #paths > 0 then c_paths = libgit2.const_char_pointer_array(#paths, paths) opts[0].pathspec.strings = c_paths opts[0].pathspec.count = #paths end if context_lines then opts[0].context_lines = context_lines end if include_workdir and include_index then -- diff workdir to index err = libgit2.C.git_diff_index_to_workdir(diff, self.repo, index and index.index or nil, opts) elseif include_workdir or include_index then -- diff workd_dir to head or index to head local head, head_tree head, err = self:head() -- if there is no HEAD, that's okay - we'll make an empty iterator if err ~= 0 and err ~= libgit2.GIT_ERROR.GIT_ENOTFOUND and err ~= libgit2.GIT_ERROR.GIT_EUNBORNBRANCH then return nil, err end if head then head_tree, err = head:peel(libgit2.GIT_OBJECT.TREE) if err ~= 0 then return nil, err end end if include_index then -- diff index to head err = libgit2.C.git_diff_tree_to_index( diff, self.repo, head_tree and ffi.cast(libgit2.git_tree_pointer, head_tree.obj) or nil, index and index.index or nil, opts ) else -- diff workd_dir to head err = libgit2.C.git_diff_tree_to_workdir( diff, self.repo, head_tree and ffi.cast(libgit2.git_tree_pointer, head_tree.obj) or nil, opts ) end else return nil, 0 end if err ~= 0 then return nil, err end -- call this to detect rename if include_workdir and bit.band(DEFAULT_STATUS_FLAGS, libgit2.GIT_STATUS.WT_RENAMED) ~= 0 or (include_index and bit.band(DEFAULT_STATUS_FLAGS, libgit2.GIT_STATUS.INDEX_RENAMED) ~= 0) then err = libgit2.C.git_diff_find_similar(diff[0], find_opts) if err ~= 0 then libgit2.C.git_diff_free(diff[0]) return nil, err end end return Diff.new(diff[0]), 0 end -- Gets diff from tree to tree ---@param old_tree GitTree? A git_tree object to diff from. ---@param new_tree GitTree? A git_tree object to diff to. ---@param paths string[]? Git paths, can be null ---@param context_lines integer? number of context lines ---@return GitDiff? ---@return GIT_ERROR function Repository:diff_tree_to_tree(old_tree, new_tree, paths, context_lines) local c_paths, err local opts = libgit2.git_diff_options(libgit2.GIT_DIFF_OPTIONS_INIT) local find_opts = libgit2.git_diff_find_options(libgit2.GIT_DIFF_FIND_OPTIONS_INIT) local diff = libgit2.git_diff_double_pointer() opts[0].id_abbrev = 8 opts[0].flags, find_opts[0].flags = git_status_flags_to_diff_flags(DEFAULT_STATUS_FLAGS) if paths and #paths > 0 then c_paths = libgit2.const_char_pointer_array(#paths, paths) opts[0].pathspec.strings = c_paths opts[0].pathspec.count = #paths end if context_lines then opts[0].context_lines = context_lines end err = libgit2.C.git_diff_tree_to_tree( diff, self.repo, old_tree and old_tree.tree or nil, new_tree and new_tree.tree or nil, opts ) if err ~= 0 then return nil, err end return Diff.new(diff[0]), 0 end -- Gets diff from commit to commit. ---@param from_commit GitCommit? A git_tree object to diff from. ---@param to_commit GitCommit? A git_tree object to diff to. ---@param paths string[]? Git paths, can be null ---@param context_lines integer? number of context lines ---@return GitDiff? ---@return GIT_ERROR function Repository:diff_commit_to_commit(from_commit, to_commit, paths, context_lines) local old_tree, new_tree, diff, err if from_commit then old_tree, err = from_commit:tree() if not old_tree then return nil, err end end if to_commit then new_tree, err = to_commit:tree() if not new_tree then return nil, err end end diff, err = self:diff_tree_to_tree(old_tree, new_tree, paths, context_lines) return diff, err end ---Applies a diff into workdir ---@param diff GitDiff ---@return GIT_ERROR function Repository:apply_workdir(diff) return self:apply(diff, true, false) end ---Applies a diff into index ---@param diff GitDiff ---@return GIT_ERROR function Repository:apply_index(diff) return self:apply(diff, false, true) end ---Applies a diff into index and workdir ---@param diff GitDiff ---@return GIT_ERROR function Repository:apply_workdir_index(diff) return self:apply(diff, true, true) end ---Applies a diff into workdir or index ---@param diff GitDiff ---@param workdir boolean Apply to workdir ---@param index boolean Apply to index ---@return GIT_ERROR function Repository:apply(diff, workdir, index) if not (workdir or index) then return 0 end local opts = libgit2.git_apply_options() libgit2.C.git_apply_options_init(opts, libgit2.GIT_APPLY_OPTIONS_VERSION) local location = 0 if workdir then location = bit.bor(location, libgit2.GIT_APPLY_LOCATION.WORKDIR) end if index then location = bit.bor(location, libgit2.GIT_APPLY_LOCATION.INDEX) end return libgit2.C.git_apply(self.repo, diff.diff, location, opts) end ---@param oid GitObjectId ---@return GitTag? ---@return GIT_ERROR function Repository:tag_lookup(oid) local git_tag = libgit2.git_tag_double_pointer() local err = libgit2.C.git_tag_lookup(git_tag, self.repo, oid.oid) if err ~= 0 then return nil, err end return Tag.new(git_tag[0]), 0 end ---Lookup a branch by its name in a repository. ---@param branch_name string branch name ---@param branch_type GIT_BRANCH ---@return GitReference? ---@return GIT_ERROR function Repository:branch_lookup(branch_name, branch_type) local ref = libgit2.git_reference_double_pointer() local err = libgit2.C.git_branch_lookup(ref, self.repo, branch_name, branch_type) if err ~= 0 then return nil, err end return Reference.new(ref[0]), 0 end ---@param remote_name string remote name ---@return string? ---@return GIT_ERROR function Repository:remote_default_branch(remote_name) local remote_head = string.format("refs/remotes/%s/HEAD", remote_name) local ref, err = self:reference_lookup(remote_head) if not ref then return nil, err end local target = ref:symbolic_target() if not target then return nil, err end return target, 0 end ---Initializes a rebase operation ---@param branch GitAnnotatedCommit? ---@param upstream GitAnnotatedCommit? ---@param onto GitAnnotatedCommit? ---@param opts { inmemory: boolean? } ---@return GitRebase? ---@return GIT_ERROR function Repository:rebase_init(branch, upstream, onto, opts) local git_rebase = libgit2.git_rebase_double_pointer() local rebase_opts = libgit2.git_rebase_options(libgit2.GIT_REBASE_OPTIONS_INIT) if opts.inmemory then rebase_opts[0].inmemory = 1 end local err = libgit2.C.git_rebase_init( git_rebase, self.repo, branch and branch.commit, upstream and upstream.commit, onto and onto.commit, rebase_opts ) if err ~= 0 then return nil, 0 end return Rebase.new(git_rebase[0]), 0 end ---Opens an existing rebase. ---@return GitRebase? ---@return GIT_ERROR function Repository:rebase_open() local git_rebase = libgit2.git_rebase_double_pointer() local opts = libgit2.git_rebase_options(libgit2.GIT_REBASE_OPTIONS_INIT) local err = libgit2.C.git_rebase_open(git_rebase, self.repo, opts) if err ~= 0 then return nil, 0 end return Rebase.new(git_rebase[0]), 0 end -- ============================== -- | Repository async functions | -- ============================== -- Gets blame for single file but run in async ---@param callback fun(b: GitBlame?, err: GIT_ERROR) function Repository:blame_file_async(path, opts, callback) local blame_opts, err = init_blame_options(opts) if not blame_opts then return callback(nil, err) end local work_fn = function(lib, repo_ptr, p, opts_ptr) local lg2 = require "blink.tree.git.libgit2" lg2.load_library(lib) local ffi_ = require "ffi" local repo = ffi_.cast(lg2.git_repository_pointer, repo_ptr) local o = ffi_.cast("git_blame_options*", opts_ptr) local git_blame = lg2.git_blame_double_pointer() local e = lg2.C.git_blame_file(git_blame, repo, p, o) if e ~= 0 then return nil, e end return tonumber(ffi_.cast(lg2.pointer_t, git_blame[0])), 0 end local after_work_fn = function(ptr, e) if not ptr or ptr == 0 then return callback(nil, err) end local blame = Blame.new(ffi.cast(libgit2.git_blame_pointer, ptr)) callback(blame, 0) end local work = uv.new_work(work_fn, after_work_fn) work:queue( libgit2_library_path, tonumber(ffi.cast(libgit2.pointer_t, self.repo)), path, tonumber(ffi.cast(libgit2.pointer_t, blame_opts)) ) end -- Reads repo status but in async. ---@param callback fun(result: GitStatusItem[]?, err: GIT_ERROR) function Repository:status_async(callback) ---@param lib string? ---@param repo_ptr integer ---@param flags integer ---@return integer? ---@return GIT_ERROR local function new_git_status_list(lib, repo_ptr, flags) local lg2 = require "blink.tree.git.libgit2" lg2.load_library(lib) local ffi_ = require "ffi" local opts = lg2.git_status_options(lg2.GIT_STATUS_OPTIONS_INIT) opts[0].show = lg2.GIT_STATUS_SHOW.INDEX_AND_WORKDIR opts[0].flags = flags local repo = ffi_.cast(lg2.git_repository_pointer, repo_ptr) local status = lg2.git_status_list_double_pointer() local err = lg2.C.git_status_list_new(status, repo, opts) if err ~= 0 then return nil, err end return tonumber(ffi_.cast(lg2.pointer_t, status[0])), 0 end ---@param git_status_list_ptr integer? ---@param err GIT_ERROR local function handle_git_status_list(git_status_list_ptr, err) if not git_status_list_ptr or git_status_list_ptr == 0 then return callback(nil, err) end local status = ffi.cast(libgit2.git_status_list_pointer, git_status_list_ptr) local status_list = git_status_list_to_items(status) -- free C resources libgit2.C.git_status_list_free(status) callback(status_list, err) end local work = uv.new_work(new_git_status_list, handle_git_status_list) work:queue(libgit2_library_path, tonumber(ffi.cast(libgit2.pointer_t, self.repo)), DEFAULT_STATUS_FLAGS) end -- =================== -- | Utils functions | -- =================== ---Set a library global option ---@param option GIT_OPT ---@param value integer ---@return GIT_ERROR local function libgit2_set_opts(option, value) return libgit2.C.git_libgit2_opts(option, value) end ---@param delta GIT_DELTA ---@return string char Git status char such as M, A, D. local function status_char(delta) local c = libgit2.C.git_diff_status_char(delta) return string.char(c) end ---Same as status_char but replace " " by "-" ---@param delta GIT_DELTA ---@return string Git status char such as M, A, D. local function status_char_dash(delta) if delta == libgit2.GIT_DELTA.CONFLICTED then return "U" end local c = libgit2.C.git_diff_status_char(delta) if c == 32 then return "-" end return string.char(c) end ---@param delta GIT_DELTA ---@return string status status full string such as "UNTRACKED" local function status_string(delta) return GIT_DELTA_STRING[delta + 1] end ---Prettifiy git message ---@param msg string local function message_prettify(msg) local c_buf = libgit2.git_buf() -- local err = libgit2.C.git_buf_grow(c_buf, msg:len() + 1) -- if err ~= 0 then -- return nil, err -- end local err = libgit2.C.git_message_prettify(c_buf, msg, 1, string.byte "#") if err ~= 0 then libgit2.C.git_buf_dispose(c_buf) return nil, err end local prettified = ffi.string(c_buf[0].ptr, c_buf[0].size) libgit2.C.git_buf_dispose(c_buf) return prettified, 0 end -- ================== -- | Git2Module | -- ================== ---@class Git2Module local M = {} -- Inits luajit-git2 lib ---@param path string? optional path to libgit2 lib function M.init(path) libgit2.load_library(path) libgit2_library_path = path if libgit2_init_count == 0 then -- TODO: defer this libgit2_init_count = libgit2.C.git_libgit2_init() end end M.Config = Config M.Diff = Diff M.IndexEntry = IndexEntry M.ObjectId = ObjectId M.Reference = Reference M.Repository = Repository M.GIT_BRANCH = libgit2.GIT_BRANCH M.GIT_CHECKOUT = libgit2.GIT_CHECKOUT M.GIT_DELTA = libgit2.GIT_DELTA M.GIT_ERROR = libgit2.GIT_ERROR M.GIT_INDEX_STAGE = libgit2.GIT_INDEX_STAGE M.GIT_OBJECT = libgit2.GIT_OBJECT M.GIT_OPT = libgit2.GIT_OPT M.GIT_REBASE_NO_OPERATION = libgit2.GIT_REBASE_NO_OPERATION M.GIT_REBASE_OPERATION = libgit2.GIT_REBASE_OPERATION M.GIT_REFERENCE = libgit2.GIT_REFERENCE M.GIT_REFERENCE_NAMESPACE = GIT_REFERENCE_NAMESPACE M.head = Repository.head M.init_blame_options = init_blame_options M.message_prettify = message_prettify M.reference_name_namespace = reference_name_namespace M.reference_name_remote = reference_name_remote M.reference_name_shorthand = reference_name_shorthand M.set_opts = libgit2_set_opts M.status = Repository.status M.status_char = status_char M.status_char_dash = status_char_dash M.status_string = status_string function M.destroy() libgit2_init_count = libgit2.C.git_libgit2_shutdown() end return M ================================================ FILE: lua/blink/tree/git/ignore.lua ================================================ local uv = vim.uv local ignore = {} function ignore.new(path) local self = setmetatable({}, { __index = ignore }) self.path = path self.ignore = {} return self end function ignore:read() local fd = uv.fs_open(self.path, 'r', 438) if not fd then return end local data = uv.fs_read(fd, uv.fs_stat(self.path).size, 0) uv.fs_close(fd) if not data then return end self.ignore = vim.split(data, '\n') end function ignore.rule_to_matcher(rule) local pattern = rule:gsub('\\', '/') if rule:sub(1, 1) == '/' then pattern = '^' .. pattern end if rule:sub(-1, -1) == '/' then pattern = pattern .. '$' end return function(path) path = path:gsub('\\', '/') return vim.startswith(path, pattern) end end function ignore:is_ignored(path) path = path:gsub('\\', '/') for _, pattern in ipairs(self.ignore) do if vim.startswith(path, pattern) then return true end end return false end return ignore ================================================ FILE: lua/blink/tree/git/init.lua ================================================ local Git = {} local function debounce(func, wait) local timer return function(...) local args = { ... } if timer and not timer:is_closing() then timer:stop() end timer = vim.loop.new_timer() timer:start( wait, 0, vim.schedule_wrap(function() timer:stop() if not timer:is_closing() then timer:close() end func(unpack(args)) end) ) end end function Git.new(path, on_change) local self = setmetatable({}, { __index = Git }) self.path = path self.status = {} local git2 = require('blink.tree.git.git2') git2.init() local err self.repository, err = git2.Repository.open(path, false) if err > 0 then print('Failed to open repository: ' .. err) return end local debounced_update_status = debounce(function() self:update_status(on_change) end, 10) -- watch .git for changes self.watch_unsubscribe = require('blink.tree.lib.fs').watch_dir(path .. '/.git', debounced_update_status) -- poll every 5s vim.loop.new_timer():start(5000, 5000, function() debounced_update_status() end) return self end function Git:set_status(path, status) local parts = vim.split(path, '/') local current = self.status for i = 1, #parts do local part = parts[i] if i == #parts then current[part] = status elseif current[part] == nil then current[part] = {} end current = current[part] end end function Git:get_status(path) local parts = vim.split(path, '/') local current = self.status for i = 1, #parts do local part = parts[i] if current[part] == nil then return nil end current = current[part] end return current end function Git:update_status(callback) callback = callback or function() end self.repository:status_async(function(status, err) if err > 0 then print('Failed to get status' .. err) callback() end self.status = {} for _, item in ipairs(status) do local path = self.path .. '/' .. (item.new_path or item.path) self:set_status(path, item) end -- print(vim.inspect(self.status)) if callback then callback() end end) end local function flat_status(node) local leaves = {} local function traverse(current_node) if type(current_node) ~= "table" then return end if current_node.index_status ~= nil then table.insert(leaves, current_node) else for _, child in pairs(current_node) do traverse(child) end end end traverse(node) return leaves end function Git.get_hl_for_status(status, is_dir) local git = require('blink.tree.git.git2') local DELTA = git.GIT_DELTA if is_dir then local flat = flat_status(status) local worktree_statuses = {} local has_staged = false for _, node_status in ipairs(flat) do local idx = node_status.index_status local wt = node_status.worktree_status if idx ~= DELTA.UNMODIFIED and idx ~= DELTA.UNTRACKED then has_staged = true end if not worktree_statuses[wt] then worktree_statuses[wt] = true end end if has_staged then return 'BlinkTreeGitStaged' elseif worktree_statuses[DELTA.MODIFIED] then return 'BlinkTreeGitModified' elseif worktree_statuses[DELTA.RENAMED] then return 'BlinkTreeGitRenamed' elseif worktree_statuses[DELTA.ADDED] then return 'BlinkTreeGitAdded' elseif worktree_statuses[DELTA.CONFLICTED] then return 'BlinkTreeGitConflict' elseif worktree_statuses[DELTA.UNTRACKED] then return 'BlinkTreeGitUntracked' end else local index_status = status.index_status local worktree_status = status.worktree_status if index_status ~= DELTA.UNMODIFIED and index_status ~= DELTA.UNTRACKED then return 'BlinkTreeGitStaged' end if worktree_status == DELTA.MODIFIED then return 'BlinkTreeGitModified' end if worktree_status == DELTA.RENAMED then return 'BlinkTreeGitRenamed' end if worktree_status == DELTA.ADDED then return 'BlinkTreeGitAdded' end if worktree_status == DELTA.CONFLICTED then return 'BlinkTreeGitConflict' end if worktree_status == DELTA.UNTRACKED then return 'BlinkTreeGitUntracked' end end end function Git:destroy() self.watch_unsubscribe() end return Git ================================================ FILE: lua/blink/tree/git/libgit2.lua ================================================ -- Made by SuperBo in Fugit2 -- https://github.com/SuperBo/fugit2.nvim/tree/70662d529fe98790d7b2104b4dd67dd229332194 -- Licensed under MIT local ffi = require('ffi') -- ===================== -- | Libgit2 C section | -- ===================== --- Load libgit2 via ffi ffi.cdef([[ typedef uint64_t git_object_size_t; typedef int64_t git_off_t; typedef int64_t git_time_t; typedef struct git_annotated_commit git_annotated_commit; typedef struct git_blame git_blame; typedef struct git_blob git_blob; typedef struct git_branch_iterator git_branch_iterator; typedef struct git_commit git_commit; typedef struct git_config git_config; typedef struct git_config_iterator git_config_iterator; typedef struct git_diff git_diff; typedef struct git_diff_stats git_diff_stats; typedef struct git_index git_index; typedef struct git_index_conflict_iterator git_index_conflict_iterator; typedef struct git_index_iterator git_index_iterator; typedef struct git_object git_object; typedef struct git_patch git_patch; typedef struct git_rebase git_rebase; typedef struct git_reference git_reference; typedef struct git_remote git_remote; typedef struct git_repository git_repository; typedef struct git_revwalk git_revwalk; typedef struct git_status_list git_status_list; typedef struct git_tag git_tag; typedef struct git_tree git_tree; typedef struct git_tree_entry git_tree_entry; typedef struct git_strarray { char **strings; size_t count; } git_strarray; typedef struct git_strarray_readonly { const char **strings; size_t count; } git_strarray_readonly; typedef struct git_buf { char *ptr; size_t reserved; size_t size; } git_buf; typedef struct git_oid { unsigned char id[20]; } git_oid; typedef struct git_time { git_time_t time; int offset; char sign; } git_time; typedef struct git_signature { char *name; char *email; git_time when; } git_signature; typedef struct git_blame_hunk { size_t lines_in_hunk; git_oid final_commit_id; size_t final_start_line_number; git_signature *final_signature; git_oid orig_commit_id; const char *orig_path; size_t orig_start_line_number; git_signature *orig_signature; char boundary; } git_blame_hunk; typedef struct git_blame_options { unsigned int version; uint32_t flags; uint16_t min_match_characters; git_oid newest_commit; git_oid oldest_commit; size_t min_line; size_t max_line; } git_blame_options; typedef struct git_config_entry { const char *name; const char *value; const char *backend_type; const char *origin_path; unsigned int include_depth; unsigned int level; void (*free)(struct git_config_entry *entry); } git_config_entry; typedef struct git_diff_hunk { int old_start; int old_lines; int new_start; int new_lines; size_t header_len; char header[128]; } git_diff_hunk; typedef struct git_diff_line { char origin; int old_lineno; int new_lineno; int num_lines; size_t content_len; git_off_t content_offset; const char *content; } git_diff_line; typedef struct git_diff_file { git_oid id; const char * path; git_object_size_t size; uint32_t flags; uint16_t mode; uint16_t id_abbrev; } git_diff_file; typedef struct git_diff_delta { int status; uint32_t flags; uint16_t similarity; uint16_t nfiles; git_diff_file old_file; git_diff_file new_file; } git_diff_delta; typedef int (*git_diff_notify_cb)( const git_diff *diff_so_far, const struct git_diff_delta *delta_to_add, const char *matched_pathspec, void *payload ); typedef int (*git_diff_progress_cb)( const git_diff *diff_so_far, const char *old_path, const char *new_path, void *payload ); typedef struct git_diff_options { unsigned int version; uint32_t flags; unsigned int ignore_submodules; git_strarray_readonly pathspec; git_diff_notify_cb notify_cb; git_diff_progress_cb progress_cb; void * payload; uint32_t context_lines; uint32_t interhunk_lines; unsigned int oid_type; uint16_t id_abbrev; int64_t max_size; const char * old_prefix; const char * new_prefix; } git_diff_options ; typedef struct git_diff_similarity_metric { int (*file_signature)( void **out, const git_diff_file *file, const char *fullpath, void *payload ); int (*buffer_signature)( void **out, const git_diff_file *file, const char *buf, size_t buflen, void *payload ); void (*free_signature)(void *sig, void *payload); int (*similarity)(int *score, void *siga, void *sigb, void *payload); void *payload; } git_diff_similarity_metric; typedef struct git_diff_find_options { unsigned int version; uint32_t flags; uint16_t rename_threshold; uint16_t rename_from_rewrite_threshold; uint16_t copy_threshold; uint16_t break_rewrite_threshold; size_t rename_limit; git_diff_similarity_metric *metric; } git_diff_find_options; typedef int (* git_apply_delta_cb)(const git_diff_delta *delta, void *payload); typedef int (* git_apply_hunk_cb)(const git_diff_hunk *hunk, void *payload); typedef struct git_apply_options { unsigned int version; /**< The version */ git_apply_delta_cb delta_cb; git_apply_hunk_cb hunk_cb; void *payload; unsigned int flags; } git_apply_options; typedef int (*git_commit_create_cb)( git_oid *out, const git_signature *author, const git_signature *committer, const char *message_encoding, const char *message, const git_tree *tree, size_t parent_count, const git_commit **parents, void *payload ); typedef int (*git_checkout_notify_cb)( unsigned int why, const char *path, const git_diff_file *baseline, const git_diff_file *target, const git_diff_file *workdir, void *payload ); typedef void (*git_checkout_progress_cb)( const char *path, size_t completed_steps, size_t total_steps, void *payload ); typedef struct git_checkout_perfdata { size_t mkdir_calls; size_t stat_calls; size_t chmod_calls; } git_checkout_perfdata; typedef void (*git_checkout_perfdata_cb)( const git_checkout_perfdata *perfdata, void *payload ); typedef struct git_checkout_options { unsigned int version; unsigned int checkout_strategy; int disable_filters; /**< don't apply filters like CRLF conversion */ unsigned int dir_mode; /**< default is 0755 */ unsigned int file_mode; /**< default is 0644 or 0755 as dictated by blob */ int file_open_flags; /**< default is O_CREAT | O_TRUNC | O_WRONLY */ unsigned int notify_flags; /**< see `git_checkout_notify_t` above */ git_checkout_notify_cb notify_cb; void *notify_payload; git_checkout_progress_cb progress_cb; void *progress_payload; git_strarray_readonly paths; git_tree *baseline; git_index *baseline_index; const char *target_directory; /**< alternative checkout path to workdir */ const char *ancestor_label; /**< the name of the common ancestor side of conflicts */ const char *our_label; /**< the name of the "our" side of conflicts */ const char *their_label; /**< the name of the "their" side of conflicts */ git_checkout_perfdata_cb perfdata_cb; void *perfdata_payload; } git_checkout_options; typedef struct git_merge_options { unsigned int version; uint32_t flags; unsigned int rename_threshold; unsigned int target_limit; git_diff_similarity_metric *metric; unsigned int recursion_limit; const char *default_driver; unsigned int file_favor; uint32_t file_flags; } git_merge_options; typedef struct git_rebase_options { unsigned int version; int quiet; int inmemory; const char *rewrite_notes_ref; git_merge_options merge_options; git_checkout_options checkout_options; git_commit_create_cb commit_create_cb; void *reserved; void *payload; } git_rebase_options; typedef struct git_rebase_operation { unsigned int type; const git_oid id; const char *exec; } git_rebase_operation; typedef struct git_status_entry { unsigned int status; struct git_diff_delta *head_to_index; struct git_diff_delta *index_to_workdir; } git_status_entry; typedef struct git_status_options { unsigned int version; int show; unsigned int flags; git_strarray pathspec; struct git_tree* baseline; uint16_t rename_threshold; } git_status_options; typedef struct { int32_t seconds; /* nsec should not be stored as time_t compatible */ uint32_t nanoseconds; } git_index_time; typedef struct git_index_entry { git_index_time ctime; git_index_time mtime; uint32_t dev; uint32_t ino; uint32_t mode; uint32_t uid; uint32_t gid; uint32_t file_size; git_oid id; uint16_t flags; uint16_t flags_extended; const char *path; } git_index_entry; int git_libgit2_init(); int git_libgit2_shutdown(); int git_libgit2_opts(int option, ...); void git_strarray_dispose(git_strarray *array); int git_buf_grow(git_buf *buffer, size_t target_size); void git_buf_dispose(git_buf *buffer); int git_blame_options_init(git_blame_options *opts, unsigned int version); void git_blame_free(git_blame *blame); int git_blame_buffer(git_blame **out, git_blame *reference, const char *buffer, size_t buffer_len); int git_blame_file(git_blame **out, git_repository *repo, const char *path, git_blame_options *options); const git_blame_hunk * git_blame_get_hunk_byindex(git_blame *blame, uint32_t index); const git_blame_hunk * git_blame_get_hunk_byline(git_blame *blame, size_t lineno); uint32_t git_blame_get_hunk_count(git_blame *blame); int git_blob_lookup(git_blob **blob, git_repository *repo, const git_oid *id); const void * git_blob_rawcontent(const git_blob *blob); int git_blob_is_binary(const git_blob *blob); git_object_size_t git_blob_rawsize(const git_blob *blob); const void * git_blob_rawcontent(const git_blob *blob); void git_blob_free(git_blob *blob); int git_checkout_head(git_repository *repo, const git_checkout_options *opts); int git_checkout_index(git_repository *repo, git_index *index, const git_checkout_options *opts); int git_checkout_tree(git_repository *repo, const git_object *treeish, const git_checkout_options *opts); char * git_oid_tostr(char *out, size_t n, const git_oid *id); char * git_oid_tostr_s(const git_oid *oid); int git_oid_equal(const git_oid *a, const git_oid *b); int git_oid_cpy(git_oid *out, const git_oid *src); int git_oid_fromstr(git_oid *out, const char *str); int git_oid_fromstrp(git_oid *out, const char *str); int git_oid_fromstrn(git_oid *out, const char *str, size_t length); char git_diff_status_char(unsigned int status); int git_message_prettify(git_buf *out, const char *message, int strip_comments, char comment_char); void git_object_free(git_object *object); int git_object_lookup_bypath(git_object **out, const git_object *treeish, const char *path, int type); const git_oid * git_object_id(const git_object *obj); int git_apply_options_init(git_apply_options *opts, unsigned int version); int git_apply(git_repository *repo, git_diff *diff, unsigned int location, const git_apply_options *options); int git_commit_lookup(git_commit **commit, git_repository *repo, const git_oid *id); int git_commit_lookup_prefix(git_commit **commit, git_repository *repo, const git_oid *id, size_t len); void git_commit_free(git_commit *commit); const git_signature * git_commit_author(const git_commit *commit); const git_signature * git_commit_committer(const git_commit *commit); const git_oid * git_commit_id(const git_commit *commit); git_repository * git_commit_owner(const git_commit *commit); const char * git_commit_message(const git_commit *commit); const char * git_commit_message_encoding(const git_commit *commit); const char * git_commit_summary(git_commit *commit); const char * git_commit_body(git_commit *commit); git_time_t git_commit_time(const git_commit *commit); int git_commit_extract_signature(git_buf *signature, git_buf *signed_data, git_repository *repo, git_oid *commit_id, const char *field); unsigned int git_commit_parentcount(const git_commit *commit); int git_commit_parent(git_commit **out, const git_commit *commit, unsigned int n); const git_oid * git_commit_parent_id(const git_commit *commit, unsigned int n); int git_commit_tree(git_tree **tree_out, const git_commit *commit); int git_commit_create_v( git_oid *id, git_repository *repo, const char *update_ref, const git_signature *author, const git_signature *committer, const char *message_encoding, const char *message, const git_tree *tree, size_t parent_count, ... ); int git_commit_create( git_oid *id, git_repository *repo, const char *update_ref, const git_signature *author, const git_signature *committer, const char *message_encoding, const char *message, const git_tree *tree, size_t parent_count, const git_commit **parents ); int git_commit_create_buffer( git_buf *out, git_repository *repo, const git_signature *author, const git_signature *committer, const char *message_encoding, const char *message, const git_tree *tree, size_t parent_count, const git_commit **parents ); int git_commit_create_with_signature( git_oid *out, git_repository *repo, const char *commit_content, const char *signature, const char *signature_field ); int git_commit_amend( git_oid *id, const git_commit *commit_to_amend, const char *update_ref, const git_signature *author, const git_signature *committer, const char *message_encoding, const char *message, const git_tree *tree ); int git_config_open_default(git_config **out); int git_config_open_level(git_config **out, const git_config *parent, int level); void git_config_free(git_config *cfg); int git_config_get_entry(git_config_entry **out, const git_config *cfg, const char *name); int git_config_get_int32(int32_t *out, const git_config *cfg, const char *name); int git_config_get_int64(int64_t *out, const git_config *cfg, const char *name); int git_config_get_bool(int *out, const git_config *cfg, const char *name); int git_config_get_path(git_buf *out, const git_config *cfg, const char *name); int git_config_get_string(const char **out, const git_config *cfg, const char *name); int git_config_get_string_buf(git_buf *out, const git_config *cfg, const char *name); void git_config_entry_free(git_config_entry *entry); int git_config_iterator_new(git_config_iterator **out, const git_config *cfg); int git_config_next(git_config_entry **entry, git_config_iterator *iter); void git_config_iterator_free(git_config_iterator *iter); int git_diff_find_similar(git_diff *diff, const git_diff_find_options *options); int git_diff_index_to_workdir(git_diff **diff, git_repository *repo, git_index *index, const git_diff_options *opts); int git_diff_tree_to_index(git_diff **diff, git_repository *repo, git_tree *old_tree, git_index *index, const git_diff_options *opts); int git_diff_tree_to_workdir(git_diff **diff, git_repository *repo, git_tree *old_tree, const git_diff_options *opts); int git_diff_tree_to_tree(git_diff **diff, git_repository *repo, git_tree *old_tree, git_tree *new_tree, const git_diff_options *opts); int git_diff_to_buf(git_buf *out, git_diff *diff, unsigned int format); int git_diff_from_buffer(git_diff **out, const char *content, size_t content_len); int git_diff_get_stats(git_diff_stats **out, git_diff *diff); const git_diff_delta * git_diff_get_delta(const git_diff *diff, size_t idx); size_t git_diff_num_deltas(const git_diff *diff); void git_diff_free(git_diff *diff); int git_diff_stats_to_buf(git_buf *out, const git_diff_stats *stats, unsigned int format, size_t width); size_t git_diff_stats_files_changed(const git_diff_stats *stats); size_t git_diff_stats_insertions(const git_diff_stats *stats); size_t git_diff_stats_deletions(const git_diff_stats *stats); void git_diff_stats_free(git_diff_stats *stats); int git_patch_from_diff(git_patch **out, git_diff *diff, size_t idx); int git_patch_to_buf(git_buf *out, git_patch *patch); size_t git_patch_num_hunks(const git_patch *patch); int git_patch_get_hunk(const git_diff_hunk **out, size_t *lines_in_hunk, git_patch *patch, size_t hunk_idx); int git_patch_get_line_in_hunk(const git_diff_line **out, git_patch *patch, size_t hunk_idx, size_t line_of_hunk); int git_patch_num_lines_in_hunk(const git_patch *patch, size_t hunk_idx); int git_patch_line_stats(size_t *total_context, size_t *total_additions, size_t *total_deletions, const git_patch *patch); void git_patch_free(git_patch *patch); const char * git_reference_shorthand(const git_reference *ref); const char * git_reference_name(const git_reference *ref); int git_reference_resolve(git_reference **out, const git_reference *ref); void git_reference_free(git_reference *ref); int git_reference_type(const git_reference *ref); const git_oid * git_reference_target(const git_reference *ref); int git_reference_set_target(git_reference **out, git_reference *ref, const git_oid *id, const char *log_message); int git_reference_peel(git_object **out, const git_reference *ref, int type); int git_reference_name_to_id(git_oid *out, git_repository *repo, const char *name); int git_reference_lookup(git_reference **out, git_repository *repo, const char *name); const char * git_reference_symbolic_target(const git_reference *ref); int git_reference_create(git_reference **out, git_repository *repo, const char *name, const git_oid *id, int force, const char *log_message); int git_reference_symbolic_create(git_reference **out, git_repository *repo, const char *name, const char *target, int force, const char *log_message); int git_revwalk_new(git_revwalk **walker, git_repository *repo); int git_revwalk_push(git_revwalk *walk, const git_oid *oid); int git_revwalk_push_head(git_revwalk *walk); int git_revwalk_push_ref(git_revwalk *walk, const char *refname); int git_revwalk_next(git_oid *oid, git_revwalk *walk); int git_revwalk_hide(git_revwalk *walk, const git_oid *oid); void git_revwalk_sorting(git_revwalk *walk, unsigned int sort_mode); void git_revwalk_free(git_revwalk *walk); int git_revwalk_reset(git_revwalk *walker); int git_remote_lookup(git_remote **out, git_repository *repo, const char *name); int git_remote_list(git_strarray *out, git_repository *repo); const char * git_remote_name(const git_remote *remote); const char * git_remote_url(const git_remote *remote); const char * git_remote_pushurl(const git_remote *remote); int git_remote_default_branch(git_buf *out, git_remote *remote); int git_remote_disconnect(git_remote *remote); void git_remote_free(git_remote *remote); int git_branch_iterator_new(git_branch_iterator **out, git_repository *repo, unsigned int list_flags); int git_branch_next(git_reference **out, unsigned int *out_type, git_branch_iterator *iter); void git_branch_iterator_free(git_branch_iterator *iter); int git_branch_upstream(git_reference **out, const git_reference *branch); int git_branch_remote_name(git_buf *out, git_repository *repo, const char *refname); int git_branch_upstream_remote(git_buf *buf, git_repository *repo, const char *refname); int git_branch_upstream_name(git_buf *out, git_repository *repo, const char *refname); int git_branch_lookup(git_reference **out, git_repository *repo, const char *branch_name, unsigned int branch_type); int git_branch_create(git_reference **out, git_repository *repo, const char *branch_name, const git_commit *target, int force); int git_repository_open_ext(git_repository **out, const char *path, unsigned int flags, const char *ceiling_dirs); void git_repository_free(git_repository *repo); const char* git_repository_path(const git_repository *repo); int git_repository_is_empty(git_repository *repo); int git_repository_is_bare(const git_repository *repo); int git_repository_head_detached(git_repository *repo); int git_repository_head(git_reference **out, git_repository *repo); int git_repository_index(git_index **out, git_repository *repo); int git_repository_config(git_config **out, git_repository *repo); int git_repository_set_head(git_repository *repo, const char *refname); int git_repository_set_head_detached(git_repository *repo, const git_oid *committish); void git_index_free(git_index *index); int git_index_read(git_index *index, int force); int git_index_write(git_index *index); int git_index_write_tree(git_oid *out, git_index *index); const char * git_index_path(const git_index *index); int git_index_add_from_buffer(git_index *index, const git_index_entry *entry, const void *buffer, size_t len); int git_index_add_bypath(git_index *index, const char *path); int git_index_remove_bypath(git_index *index, const char *path); int git_index_remove_directory(git_index *index, const char *dir, int stage); size_t git_index_entrycount(const git_index *index); int git_index_has_conflicts(const git_index *index); int git_index_conflict_get(const git_index_entry **ancestor_out, const git_index_entry **our_out, const git_index_entry **their_out, git_index *index, const char *path); const git_index_entry * git_index_get_bypath(git_index *index, const char *path, int stage); int git_status_list_new(git_status_list **out, git_repository *repo, const git_status_options *opts); void git_status_list_free(git_status_list *statuslist); size_t git_status_list_entrycount(git_status_list *statuslist); const git_status_entry* git_status_byindex(git_status_list *statuslist, size_t idx); int git_status_should_ignore(int *ignored, git_repository *repo, const char *path); int git_status_file(unsigned int *status_flags, git_repository *repo, const char *path); int git_tree_lookup(git_tree **out, git_repository *repo, const git_oid *id); void git_tree_free(git_tree *tree); size_t git_tree_entrycount(const git_tree *tree); const git_oid * git_tree_id(const git_tree *tree); int git_tree_entry_bypath(git_tree_entry **out, const git_tree *root, const char *path); const git_tree_entry * git_tree_entry_byname(const git_tree *tree, const char *filename); const git_tree_entry * git_tree_entry_byid(const git_tree *tree, const git_oid *id); const git_tree_entry * git_tree_entry_byindex(const git_tree *tree, size_t idx); const git_oid * git_tree_entry_id(const git_tree_entry *entry); void git_tree_entry_free(git_tree_entry *entry); const char * git_tree_entry_name(const git_tree_entry *entry); int git_tree_entry_type(const git_tree_entry *entry); int git_tree_entry_to_object(git_object **object_out, git_repository *repo, const git_tree_entry *entry); int git_reset_default(git_repository *repo, const git_object *target, const git_strarray_readonly *pathspecs); int git_graph_ahead_behind(size_t *ahead, size_t *behind, git_repository *repo, const git_oid *local, const git_oid *upstream); int git_graph_descendant_of(git_repository *repo, const git_oid *commit, const git_oid *ancestor); int git_signature_default(git_signature **out, git_repository *repo); void git_signature_free(git_signature *sig); int git_tag_lookup(git_tag **out, git_repository *repo, const git_oid *id); const char * git_tag_name(const git_tag *tag); void git_tag_free(git_tag *tag); int git_tag_list(git_strarray *tag_names, git_repository *repo); int git_annotated_commit_from_ref(git_annotated_commit **out, git_repository *repo, const git_reference *ref); int git_annotated_commit_from_revspec(git_annotated_commit **out, git_repository *repo, const char *revspec); const git_oid * git_annotated_commit_id(const git_annotated_commit *commit); const char * git_annotated_commit_ref(const git_annotated_commit *commit); void git_annotated_commit_free(git_annotated_commit *commit); int git_rebase_init(git_rebase **out, git_repository *repo, const git_annotated_commit *branch, const git_annotated_commit *upstream, const git_annotated_commit *onto, const git_rebase_options *opts); int git_rebase_open(git_rebase **out, git_repository *repo, const git_rebase_options *opts); const char * git_rebase_orig_head_name(git_rebase *rebase); const git_oid * git_rebase_orig_head_id(git_rebase *rebase); const char * git_rebase_onto_name(git_rebase *rebase); const git_oid * git_rebase_onto_id(git_rebase *rebase); size_t git_rebase_operation_entrycount(git_rebase *rebase); size_t git_rebase_operation_current(git_rebase *rebase); git_rebase_operation * git_rebase_operation_byindex(git_rebase *rebase, size_t idx); int git_rebase_inmemory_index(git_index **index, git_rebase *rebase); int git_rebase_next(git_rebase_operation **operation, git_rebase *rebase); int git_rebase_commit(git_oid *id, git_rebase *rebase, const git_signature *author, const git_signature *committer, const char *message_encoding, const char *message); int git_rebase_abort(git_rebase *rebase); int git_rebase_finish(git_rebase *rebase, const git_signature *signature); void git_rebase_free(git_rebase *rebase); ]]) ---@class Libgit2Module ---@field C ffi.namespace* local M = {} ---@param path string? M.load_library = function(path) if not M.C then rawset(M, 'C', ffi.load(path or 'libgit2')) end end M.uint32 = ffi.typeof('uint32_t') M.pointer_t = ffi.typeof('intptr_t') M.char_pointer = ffi.typeof('char*') M.char_array = ffi.typeof('char[?]') M.const_char_pointer_array = ffi.typeof('const char *[?]') M.unsigned_int_array = ffi.typeof('unsigned int[?]') M.size_t_array = ffi.typeof('size_t[?]') M.int64_array = ffi.typeof('int64_t[?]') M.int_array = ffi.typeof('int[?]') ---@type ffi.ctype* git_buf[1] M.git_buf = ffi.typeof('git_buf[1]') ---@type ffi.ctype* git_config*[1] M.git_config_double_pointer = ffi.typeof('git_config*[1]') ---@type ffi.ctype* git_config* pointer M.git_config_pointer = ffi.typeof('git_config*') ---@type ffi.ctype* git_config_entry*[1] M.git_config_entry_double_pointer = ffi.typeof('git_config_entry*[1]') ---@type ffi.ctype* git_config_iterator*[1] M.git_config_iterator_double_pointer = ffi.typeof('git_config_iterator*[1]') ---@type ffi.ctype* git_checkout_options[1] M.git_checkout_options = ffi.typeof('git_checkout_options[1]') ---@type ffi.ctype* git_oid[1] M.git_oid = ffi.typeof('git_oid[1]') ---@type ffi.ctype* git_oid* M.git_oid_pointer = ffi.typeof('git_oid*') ---@type ffi.ctype* git_strarray_readonly[1] M.git_strarray_readonly = ffi.typeof('git_strarray_readonly[1]') ---@type ffi.ctype* git_strarray[1] M.git_strarray = ffi.typeof('git_strarray[1]') ---@type ffi.ctype* git_annotated_commit ** M.git_annotated_commit_double_pointer = ffi.typeof('git_annotated_commit*[1]') ---@type ffi.ctype* git_annotated_commit * M.git_annotated_commit_pointer = ffi.typeof('git_annotated_commit*') ---@type ffi.ctype* git_object ** M.git_object_double_pointer = ffi.typeof('git_object*[1]') ---@type ffi.ctype* git_object * M.git_object_pointer = ffi.typeof('git_object*') ---@type ffi.ctype* git_blame ** M.git_blame_double_pointer = ffi.typeof('git_blame*[1]') ---@type ffi.ctype* git_blame* M.git_blame_pointer = ffi.typeof('git_blame*') ---@type ffi.ctype* const git_blame_hunk * M.git_blame_hunk_pointer = ffi.typeof('const git_blame_hunk*') ---@type ffi.ctype* git_blame_options [1] M.git_blame_options = ffi.typeof('git_blame_options[1]') ---@type ffi.ctype* git_commit ** M.git_commit_double_pointer = ffi.typeof('git_commit*[1]') ---@type ffi.ctype* git_commit * M.git_commit_pointer = ffi.typeof('git_commit*') ---@type ffi.ctype* git_commit * array M.git_commit_pointer_array = ffi.typeof('git_commit*[?]') ---@type ffi.ctype* const git_commit ** M.git_commit_const_double_pointer = ffi.typeof('const git_commit**') ---@type ffi.ctype* git_blob ** M.git_blob_double_pointer = ffi.typeof('git_blob*[1]') ---@type ffi.ctype* git_blob * M.git_blob_pointer = ffi.typeof('git_blob*') ---@type ffi.ctype* git_tree ** M.git_tree_double_pointer = ffi.typeof('git_tree*[1]') ---@type ffi.ctype* git_tree * M.git_tree_pointer = ffi.typeof('git_tree*') ---@type ffi.ctype* git_tree_entry*[1] M.git_tree_entry_double_pointer = ffi.typeof('git_tree_entry*[1]') ---@type ffi.ctype* git_tree_entry* M.git_tree_entry_pointer = ffi.typeof('git_tree_entry*') ---@type ffi.ctype* git_rebase_options[1] M.git_rebase_options = ffi.typeof('git_rebase_options[1]') ---@type ffi.ctype* git_apply_options[1] M.git_apply_options = ffi.typeof('git_apply_options[1]') ---@type ffi.ctype* struct git_diff ** M.git_diff_double_pointer = ffi.typeof('git_diff*[1]') ---@type ffi.ctype* struct git_diff * M.git_diff_pointer = ffi.typeof('git_diff*') ---@type ffi.ctype* struct git_diff_options [1] M.git_diff_options = ffi.typeof('git_diff_options[1]') ---@type ffi.ctype* struct git_diff_find_options [1] M.git_diff_find_options = ffi.typeof('git_diff_find_options[1]') ---@type ffi.ctype* struct git_diff_hunk *[1] M.git_diff_hunk_double_pointer = ffi.typeof('const git_diff_hunk*[1]') ---@type ffi.ctype* const struct git_diff_line **out M.git_diff_line_double_pointer = ffi.typeof('const git_diff_line*[1]') ---@type ffi.ctype* struct git_diff_stats *[1] M.git_diff_stats_double_pointer = ffi.typeof('git_diff_stats*[1]') ---@type ffi.ctype* struct git_patch ** M.git_patch_double_pointer = ffi.typeof('git_patch*[1]') ---@type ffi.ctype* struct git_patch * M.git_patch_pointer = ffi.typeof('git_patch*') ---@type ffi.ctype* git_rebase ** M.git_rebase_double_pointer = ffi.typeof('git_rebase*[1]') ---@type ffi.ctype* git_rebase * M.git_rebase_pointer = ffi.typeof('git_rebase*') ---@type ffi.ctype* git_rebase_operation_double_pointer M.git_rebase_operation_double_pointer = ffi.typeof('git_rebase_operation*[1]') ---@type ffi.ctype* git_rebase_operation struct pointer M.git_rebase_operation_pointer = ffi.typeof('git_rebase_operation*') ---@type ffi.ctype* struct git_repository** M.git_repository_double_pointer = ffi.typeof('git_repository*[1]') ---@type ffi.ctype* struct git_repository* M.git_repository_pointer = ffi.typeof('git_repository*') ---@type ffi.ctype* struct git_reference*[1] M.git_reference_double_pointer = ffi.typeof('git_reference*[1]') ---@type ffi.ctype* struct git_reference* M.git_reference_pointer = ffi.typeof('git_reference*') ---@type ffi.ctype* struct git_remote** M.git_remote_double_pointer = ffi.typeof('git_remote*[1]') ---@type ffi.ctype* struct git_remote* M.git_remote_pointer = ffi.typeof('git_remote*') ---@type ffi.ctype* struct git_revwalk** M.git_revwalk_double_pointer = ffi.typeof('git_revwalk*[1]') ---@type ffi.ctype* struct git_revwalk* M.git_revwalk_pointer = ffi.typeof('git_revwalk*') ---@type ffi.ctype* git_signature ** M.git_signature_double_pointer = ffi.typeof('git_signature*[1]') ---@type ffi.ctype* git_signature * M.git_signature_pointer = ffi.typeof('git_signature*') ---@type ffi.ctype* git_status_options[1] M.git_status_options = ffi.typeof('git_status_options[1]') ---@type ffi.ctype* struct git_status_list*[1] M.git_status_list_double_pointer = ffi.typeof('git_status_list*[1]') ---@type ffi.ctype* struct git_status_list* M.git_status_list_pointer = ffi.typeof('git_status_list*') ---@type ffi.ctype* git_tag*[1] M.git_tag_double_pointer = ffi.typeof('git_tag*[1]') ---@type ffi.ctype* git_tag* M.git_tag_pointer = ffi.typeof('git_tag*') ---@type ffi.ctype* git_index** M.git_index_double_pointer = ffi.typeof('git_index*[1]') ---@type ffi.ctype* git_index* M.git_index_pointer = ffi.typeof('git_index*') ---@type ffi.ctype* git_index_iterator** M.git_index_iterator_double_pointer = ffi.typeof('git_index_iterator*[1]') ---@type ffi.ctype* git_index_entry** M.git_index_entry_double_pointer = ffi.typeof('git_index_entry*[1]') ---@type ffi.ctype* git_index_entry pointer array M.git_index_entry_pointer_array = ffi.typeof('const git_index_entry*[?]') ---@type ffi.ctype* git_index_entry pointer M.git_index_entry_pointer = ffi.typeof('const git_index_entry*') ---@type ffi.ctype* git_index_entry[1] M.git_index_entry = ffi.typeof('git_index_entry[1]') ---@type ffi.ctype* struct git_branch_iterator *[1] M.git_branch_iterator_double_pointer = ffi.typeof('git_branch_iterator *[1]') -- ========================== -- | libgit2 struct version | -- ========================== local _UI64_MAX = 0xffffffffffffffffULL M.GIT_APPLY_OPTIONS_VERSION = 1 M.GIT_BLAME_OPTIONS_VERSION = 1 M.GIT_CHECKOUT_OPTIONS_VERSION = 1 M.GIT_DIFF_FIND_OPTIONS_VERSION = 1 M.GIT_DIFF_OPTIONS_VERSION = 1 M.GIT_FETCH_OPTIONS_VERSION = 1 M.GIT_MERGE_OPTIONS_VERSION = 1 M.GIT_PROXY_OPTIONS_VERSION = 1 M.GIT_REBASE_OPTIONS_VERSION = 1 M.GIT_REMOTE_CALLBACKS_VERSION = 1 M.GIT_STATUS_OPTIONS_VERSION = 1 M.GIT_REBASE_NO_OPERATION = _UI64_MAX -- ================ -- | libgit2 enum | -- ================ local POW = { 2, -- 1 << 1 4, -- 1 << 2 8, -- 1 << 3 16, -- 1 << 4 32, -- 1 << 5 64, -- 1 << 6 0x80, -- 1 << 7 0x100, -- 1 << 8 0x200, -- 1 << 9 0x400, -- 1 << 10 0x800, -- 1 << 11 0x1000, -- 1 << 12 0x2000, -- 1 << 13 0x4000, -- 1 << 14 0x8000, -- 1 << 15 0x10000, -- 1 << 16 0x20000, -- 1 << 17 0x40000, -- 1 << 18 0x80000, -- 1 << 19 0x100000, -- 1 << 20 0x200000, -- 1 << 21 0x400000, -- 1 << 22 0x800000, -- 1 << 23 0x1000000, -- 1 << 24 0x2000000, -- 1 << 25 0x4000000, -- 1 << 26 0x8000000, -- 1 << 27 0x10000000, -- 1 << 28 0x20000000, -- 1 << 29 0x40000000, -- 1 << 30 0x80000000, -- 1 << 31 0x100000000, -- 1 << 32 } ---@enum GIT_OPT M.GIT_OPT = { GET_MWINDOW_SIZE = 0, SET_MWINDOW_SIZE = 1, GET_MWINDOW_MAPPED_LIMIT = 2, SET_MWINDOW_MAPPED_LIMIT = 3, GET_SEARCH_PATH = 4, SET_SEARCH_PATH = 5, SET_CACHE_OBJECT_LIMIT = 6, SET_CACHE_MAX_SIZE = 7, ENABLE_CACHING = 8, GET_CACHED_MEMORY = 9, GET_TEMPLATE_PATH = 10, SET_TEMPLATE_PATH = 11, SET_SSL_CERT_LOCATIONS = 12, SET_USER_AGENT = 13, ENABLE_STRICT_OBJECT_CREATION = 14, ENABLE_STRICT_SYMBOLIC_REF_CREATION = 15, SET_SSL_CIPHERS = 16, GET_USER_AGENT = 17, ENABLE_OFS_DELTA = 18, ENABLE_FSYNC_GITDIR = 19, GET_WINDOWS_SHAREMODE = 20, SET_WINDOWS_SHAREMODE = 21, ENABLE_STRICT_HASH_VERIFICATION = 22, SET_ALLOCATOR = 23, ENABLE_UNSAVED_INDEX_SAFETY = 24, GET_PACK_MAX_OBJECTS = 25, SET_PACK_MAX_OBJECTS = 26, DISABLE_PACK_KEEP_FILE_CHECKS = 27, ENABLE_HTTP_EXPECT_CONTINUE = 28, GET_MWINDOW_FILE_LIMIT = 29, SET_MWINDOW_FILE_LIMIT = 30, SET_ODB_PACKED_PRIORITY = 31, SET_ODB_LOOSE_PRIORITY = 32, GET_EXTENSIONS = 33, SET_EXTENSIONS = 34, GET_OWNER_VALIDATION = 35, SET_OWNER_VALIDATION = 36, GET_HOMEDIR = 37, SET_HOMEDIR = 38, SET_SERVER_CONNECT_TIMEOUT = 39, GET_SERVER_CONNECT_TIMEOUT = 40, SET_SERVER_TIMEOUT = 41, GET_SERVER_TIMEOUT = 42, } ---@enum GIT_APPLY_LOCATION M.GIT_APPLY_LOCATION = { WORKDIR = 0, INDEX = 1, BOTH = 2, } M.GIT_BLAME = { NORMAL = 0, TRACK_COPIES_SAME_FILE = 1, -- not yet implemented TRACK_COPIES_SAME_COMMIT_MOVES = 2, -- not yet implemented TRACK_COPIES_SAME_COMMIT_COPIES = 4, -- not yet implemented TRACK_COPIES_ANY_COMMIT_COPIES = 8, -- not yet implemented FIRST_PARENT = POW[4], -- (1<<4) USE_MAILMAP = POW[5], -- (1<<5) IGNORE_WHITESPACE = POW[6], -- (1<<6) } ---@enum GIT_BRANCH M.GIT_BRANCH = { LOCAL = 1, REMOTE = 2, ALL = 3, -- GIT_BRANCH_LOCAL|GIT_BRANCH_REMOTE, } ---@enum GIT_CONFIG_LEVEL M.GIT_CONFIG_LEVEL = { -- System-wide on Windows, for compatibility with portable git PROGRAMDATA = 1, -- System-wide configuration file; /etc/gitconfig on Linux systems */ SYSTEM = 2, -- XDG compatible configuration file; typically ~/.config/git/config */ XDG = 3, -- User-specific configuration file (also called Global configuration -- file); typically ~/.gitconfig GLOBAL = 4, -- Repository specific configuration file; $WORK_DIR/.git/config on -- non-bare repos LOCAL = 5, -- Application specific configuration file; freely defined by applications APP = 6, -- Represents the highest level available config file (i.e. the most -- specific config file available that actually is loaded) HIGHEST_LEVEL = -1, } ---@enum GIT_ERROR M.GIT_ERROR = { GIT_OK = 0, -- No error GIT_ERROR = -1, -- Generic error GIT_ENOTFOUND = -3, -- Requested object could not be found GIT_EEXISTS = -4, -- Object exists preventing operation GIT_EAMBIGUOUS = -5, -- More than one object matches GIT_EBUFS = -6, -- Output buffer too short to hold data --[[ GIT_EUSER is a special error that is never generated by libgit2 code. You can return it from a callback (e.g to stop an iteration) to know that it was generated by the callback and not by libgit2. ]] -- GIT_EUSER = -7, GIT_EBAREREPO = -8, -- Operation not allowed on bare repository GIT_EUNBORNBRANCH = -9, -- HEAD refers to branch with no commits GIT_EUNMERGED = -10, -- Merge in progress prevented operation GIT_ENONFASTFORWARD = -11, -- Reference was not fast-forwardable GIT_EINVALIDSPEC = -12, -- Name/ref spec was not in a valid format GIT_ECONFLICT = -13, -- Checkout conflicts prevented operation GIT_ELOCKED = -14, -- Lock file prevented operation GIT_EMODIFIED = -15, -- Reference value does not match expected GIT_EAUTH = -16, -- Authentication error GIT_ECERTIFICATE = -17, -- Server certificate is invalid GIT_EAPPLIED = -18, -- Patch/merge has already been applied GIT_EPEEL = -19, -- The requested peel operation is not possible GIT_EEOF = -20, -- Unexpected EOF GIT_EINVALID = -21, -- Invalid operation or input GIT_EUNCOMMITTED = -22, -- Uncommitted changes in index prevented operation GIT_EDIRECTORY = -23, -- The operation is not valid for a directory GIT_EMERGECONFLICT = -24, -- A merge conflict exists and cannot continue GIT_PASSTHROUGH = -30, -- A user-configured callback refused to act GIT_ITEROVER = -31, -- Signals end of iteration with iterator GIT_RETRY = -32, -- Internal only GIT_EMISMATCH = -33, -- Hashsum mismatch in object GIT_EINDEXDIRTY = -34, -- Unsaved changes in the index would be overwritten GIT_EAPPLYFAIL = -35, -- Patch application failed GIT_EOWNER = -36, -- The object is not owned by the current user GIT_TIMEOUT = -37, -- The operation timed out } ---@enum GIT_INDEX_STAGE M.GIT_INDEX_STAGE = { ANY = -1, -- Match any index stage. NORMAL = 0, -- A normal staged file in the index. ANCESTOR = 1, -- The ancestor side of a conflict. OURS = 2, -- The "ours" side of a conflict. THEIRS = 3, -- The "theirs" side of a conflict. } ---@enum GIT_REFERENCE M.GIT_REFERENCE = { INVALID = 0, -- Invalid reference DIRECT = 1, -- A reference that points at an object id SYMBOLIC = 2, -- A reference that points at another reference ALL = 3, -- Both GIT_REFERENCE_DIRECT | GIT_REFERENCE_SYMBOLIC } ---@enum GIT_OBJECT M.GIT_OBJECT = { ANY = -2, -- Object can be any of the following. INVALID = -1, -- Object is invalid. COMMIT = 1, -- A commit object. TREE = 2, -- A tree (directory listing) object. BLOB = 3, -- A file revision object. TAG = 4, -- An annotated tag object. OFS_DELTA = 6, -- A delta, base is given by an offset. REF_DELTA = 7, -- A delta, base is given by object id. } ---@enum GIT_SORT M.GIT_SORT = { NONE = 0, -- 0, default method from `git`: reverse chronological order TOPOLOGICAL = 1, -- 1 << 0, Sort the repository contents in topological order TIME = 2, -- 1 << 1, Sort the repository contents by commit time. REVERSE = 4, -- 1 << 2, Iterate through the repository contents in reverse order. } ---@enum GIT_STATUS M.GIT_STATUS = { CURRENT = 0, INDEX_NEW = 1, -- 1u << 0 INDEX_MODIFIED = POW[1], -- 1u << 1 INDEX_DELETED = POW[2], -- 1u << 2 INDEX_RENAMED = POW[3], -- 1u << 3 INDEX_TYPECHANGE = POW[4], -- 1u << 4 WT_NEW = POW[7], -- 1u << 7 WT_MODIFIED = POW[8], -- 1u << 8 WT_DELETED = POW[9], -- 1u << 9 WT_TYPECHANGE = POW[10], -- 1u << 10 WT_RENAMED = POW[11], -- 1u << 11 WT_UNREADABLE = POW[12], -- 1u << 12 IGNORED = POW[14], -- 1u << 14 CONFLICTED = POW[15], -- 1u << 15 } ---@enum GIT_STATUS_SHOW M.GIT_STATUS_SHOW = { INDEX_AND_WORKDIR = 0, INDEX_ONLY = 1, WORKDIR_ONLY = 2, } ---@enum GIT_STATUS_OPT M.GIT_STATUS_OPT = { INCLUDE_UNTRACKED = 1, -- (1u << 0), INCLUDE_IGNORED = POW[1], -- (1u << 1), INCLUDE_UNMODIFIED = POW[2], -- (1u << 2), EXCLUDE_SUBMODULES = POW[3], -- (1u << 3), RECURSE_UNTRACKED_DIRS = POW[4], -- (1u << 4), DISABLE_PATHSPEC_MATCH = POW[5], -- (1u << 5), RECURSE_IGNORED_DIRS = POW[6], -- (1u << 6), RENAMES_HEAD_TO_INDEX = POW[7], -- (1u << 7), RENAMES_INDEX_TO_WORKDIR = POW[8], -- (1u << 8), SORT_CASE_SENSITIVELY = POW[9], -- (1u << 9), SORT_CASE_INSENSITIVELY = POW[10], -- (1u << 10), RENAMES_FROM_REWRITES = POW[11], -- (1u << 11), NO_REFRESH = POW[12], -- (1u << 12), UPDATE_INDEX = POW[13], -- (1u << 13), INCLUDE_UNREADABLE = POW[14], -- (1u << 14), INCLUDE_UNREADABLE_AS_UNTRACKED = POW[15], -- (1u << 15) } ---@enum GIT_DELTA M.GIT_DELTA = { UNMODIFIED = 0, -- no changes ADDED = 1, -- entry does not exist in old version DELETED = 2, -- entry does not exist in new version MODIFIED = 3, -- entry content changed between old and new RENAMED = 4, -- entry was renamed between old and new COPIED = 5, -- entry was copied from another old entry IGNORED = 6, -- entry is ignored item in workdir UNTRACKED = 7, -- entry is untracked item in workdir TYPECHANGE = 8, -- type of entry changed between old and new UNREADABLE = 9, -- entry is unreadable CONFLICTED = 10, -- entry in the index is conflicted } ---@enum GIT_REPOSITORY_OPEN M.GIT_REPOSITORY_OPEN = { NO_SEARCH = 1, -- Unless this flag is set, open will not continue searching across -- filesystem boundaries (i.e. when `st_dev` changes from the `stat` -- system call). For example, searching in a user's home directory at -- "/home/user/source/" will not return "/.git/" as the found repo if -- "/" is a different filesystem than "/home". CROSS_FS = 2, -- Open repository as a bare repo regardless of core.bare config, and -- defer loading config file for faster setup. -- Unlike `git_repository_open_bare`, this can follow gitlinks. BARE = 4, NO_DOTGIT = 8, FROM_ENV = 16, } ---@enum GIT_DIFF_FORMAT M.GIT_DIFF_FORMAT = { PATCH = 1, -- full git diff PATCH_HEADER = 2, -- just the file headers of patch RAW = 3, -- like git diff --raw NAME_ONLY = 4, -- like git diff --name-only NAME_STATUS = 5, -- like git diff --name-status PATCH_ID = 6, -- git diff as used by git patch-id } ---@enum GIT_DIFF M.GIT_DIFF = { NORMAL = 0, --0: Normal diff, the default REVERSE = 1, --(1 << 0): Reverse the sides of the diff INCLUDE_IGNORED = 2, --(1 << 1): Include ignored files in the diff RECURSE_IGNORED_DIRS = 4, --(1 << 2): adds files under the ignored directory as IGNORED entries INCLUDE_UNTRACKED = 8, --(1 << 3): Include untracked files in the diff RECURSE_UNTRACKED_DIRS = 0x10, --(1 << 4): adds files under untracked directories as UNTRACKED entries INCLUDE_UNMODIFIED = 0x20, --(1 << 5): include unmodified files in the diff -- a type change between files will be converted into a -- DELETED record for the old and an ADDED record for the new; this -- options enabled the generation of TYPECHANGE delta records INCLUDE_TYPECHANGE = 0x40, --(1 << 6) -- Even with GIT_DIFF_INCLUDE_TYPECHANGE, blob->tree changes still -- generally show as a DELETED blob. This flag tries to correctly -- label blob->tree transitions as TYPECHANGE records with new_file's -- mode set to tree. Note: the tree SHA will not be available. INCLUDE_TYPECHANGE_TREES = 0x80, --(1 << 7) IGNORE_FILEMODE = 0x100, --(1u << 8): Ignore file mode changes IGNORE_SUBMODULES = 0x200, --(1u << 9): Treat all submodules as unmodified IGNORE_CASE = 0x400, --(1u << 10): case insensitive for filename comparisons INCLUDE_CASECHANGE = 0x800, --(1u << 11): combined with `IGNORE_CASE` to specify that a file that has changed case will be returned as an add/delete pair. -- If the pathspec is set in the diff options, this flags indicates -- that the paths will be treated as literal paths instead of fnmatch patterns. -- Each path in the list must either be a full path to a file or a directory. -- (A trailing slash indicates that the path will _only_ match a directory). -- If a directory is specified, all children will be included. DISABLE_PATHSPEC_MATCH = 0x1000, --(1u << 12) -- Disable updating of the `binary` flag in delta records. This is -- useful when iterating over a diff if you don't need hunk and data -- callbacks and want to avoid having to load file completely. SKIP_BINARY_CHECK = 0x2000, --(1u << 13) -- When diff finds an untracked directory, to match the behavior of -- core Git, it scans the contents for IGNORED and UNTRACKED files. -- If *all* contents are IGNORED, then the directory is IGNORED; if -- any contents are not IGNORED, then the directory is UNTRACKED. -- This is extra work that may not matter in many cases. This flag -- turns off that scan and immediately labels an untracked directory -- as UNTRACKED (changing the behavior to not match core Git). ENABLE_FAST_UNTRACKED_DIRS = 0x4000, --(1u << 14) -- When diff finds a file in the working directory with stat -- information different from the index, but the OID ends up being the -- same, write the correct stat information into the index. -- Note: without this flag, diff will always leave the index untouched. UPDATE_INDEX = 0x8000, --(1u << 15) INCLUDE_UNREADABLE = 0x10000, -- 1u << 16): Include unreadable files in the diff INCLUDE_UNREADABLE_AS_UNTRACKED = 0x20000, --(1u << 17): Include unreadable files as UNTRACKED -- Options controlling how output will be generated -- Use a heuristic that takes indentation and whitespace into account -- which generally can produce better diffs when dealing with ambiguous -- diff hunks. INDENT_HEURISTIC = 0x40000, --(1u << 18) IGNORE_BLANK_LINES = 0x80000, --(1u << 19): Ignore blank lines FORCE_TEXT = 0x100000, --(1u << 20): Treat all files as text, disabling binary attributes & detection FORCE_BINARY = 0x200000, --(1u << 21): Treat all files as binary, disabling text diffs IGNORE_WHITESPACE = 0x400000, --(1u << 22): Ignore all whitespaces IGNORE_WHITESPACE_CHANGE = 0x800000, --(1u << 23): Ignore changes in amount of whitespace IGNORE_WHITESPACE_EOL = 0x1000000, --(1u << 24): Ignore whitespace at end of line -- When generating patch text, include the content of untracked files. -- This automatically turns on GIT_DIFF_INCLUDE_UNTRACKED but -- it does not turn on GIT_DIFF_RECURSE_UNTRACKED_DIRS. -- Add that flag if you want the content of every single UNTRACKED file. SHOW_UNTRACKED_CONTENT = 0x2000000, --(1u << 25) -- When generating output, include the names of unmodified files if -- they are included in the git_diff. Normally these are skipped in -- the formats that list files (e.g. name-only, name-status, raw). -- Even with this, these will not be included in patch format. SHOW_UNMODIFIED = 0x4000000, --(1u << 26) PATIENCE = 0x10000000, --(1u << 28): Use the "patience diff" algorithm MINIMAL = 0x20000000, --(1u << 29): Take extra time to find minimal diff -- Include the necessary deflate / delta information so that `git-apply` -- can apply given diff information to binary files. SHOW_BINARY = 0x40000000, --(1u << 30) } ---@enum GIT_DIFF_FIND M.GIT_DIFF_FIND = { FIND_BY_CONFIG = 0, -- Obey `diff.renames FIND_RENAMES = 1, -- (1u << 0): Look for renames? (`--find-renames`) FIND_RENAMES_FROM_REWRITES = 2, -- (1u << 1) FIND_COPIES = 4, -- (1u << 2) FIND_COPIES_FROM_UNMODIFIED = 8, -- (1u << 3) FIND_REWRITES = POW[4], -- (1u << 4) BREAK_REWRITES = POW[5], -- (1u << 5) FIND_AND_BREAK_REWRITES = 48, -- (GIT_DIFF_FIND_REWRITES | GIT_DIFF_BREAK_REWRITES) FIND_FOR_UNTRACKED = POW[6], -- (1u << 6) FIND_ALL = 0xff, -- (0x0ff) Turn on all finding features. FIND_IGNORE_LEADING_WHITESPACE = 0, FIND_IGNORE_WHITESPACE = POW[12], -- (1u << 12), FIND_DONT_IGNORE_WHITESPACE = POW[13], -- (1u << 13), FIND_EXACT_MATCH_ONLY = POW[14], -- (1u << 14), BREAK_REWRITES_FOR_RENAMES_ONLY = POW[15], -- (1u << 15), FIND_REMOVE_UNMODIFIED = POW[16], -- (1u << 16) } ---@enum GIT_SUBMODULE M.GIT_SUBMODULE = { IGNORE_UNSPECIFIED = -1, -- use the submodule's configuration IGNORE_NONE = 1, -- any change or untracked == dirty IGNORE_UNTRACKED = 2, -- dirty if tracked files change IGNORE_DIRTY = 3, -- only dirty if HEAD moved IGNORE_ALL = 4, -- never dirty } ---@enum GIT_DIFF_LINE M.GIT_DIFF_LINE = { CONTEXT = ' ', ADDITION = '+', DELETION = '-', CONTEXT_EOFNL = '=', --Both files have no LF at end ADD_EOFNL = '>', --Old has no LF at end, new does DEL_EOFNL = '<', --Old has LF at end, new does not FILE_HDR = 'F', HUNK_HDR = 'H', BINARY = 'B', -- For "Binary files x and y differ" } ---@enum GIT_REBASE_OPERATION M.GIT_REBASE_OPERATION = { PICK = 0, REWORD = 1, EDIT = 2, SQUASH = 3, FIXUP = 4, EXEC = 5, } ---@enum GIT_CHECKOUT M.GIT_CHECKOUT = { NONE = 0, -- default is a dry run, no actual updates SAFE = 1, --(1u << 0): Allow safe updates that cannot overwrite uncommitted data. FORCE = 2, --(1u << 1): Allow all updates to force working directory to look like index. RECREATE_MISSING = 4, --(1u << 2): Allow checkout to recreate missing files. ALLOW_CONFLICTS = POW[4], --(1u << 4): Allow checkout to make safe updates even if conflicts are found. REMOVE_UNTRACKED = POW[5], --(1u << 5): Remove untracked files not in index (that are not ignored. REMOVE_IGNORED = POW[6], --(1u << 6): Remove ignored files not in index */ UPDATE_ONLY = POW[7], --(1u << 7) Only update existing files, don't create new ones. DONT_UPDATE_INDEX = POW[8], --(1u << 8) Normally checkout updates index entries as it goes; this stops that. NO_REFRESH = POW[9], --(1u << 9) Don't refresh index/config/etc before doing checkout. SKIP_UNMERGED = POW[10], -- (1u << 10) Allow checkout to skip unmerged files, USE_OURS = POW[11], --(1u << 11) For unmerged files, checkout stage 2 from index, USE_THEIRS = POW[12], -- (1u << 12) For unmerged files, checkout stage 3 from index */ DISABLE_PATHSPEC_MATCH = POW[13], -- (1u << 13) Treat pathspec as simple list of exact match file paths, SKIP_LOCKED_DIRECTORIES = POW[18], --(1u << 18) Ignore directories in use, they will be left empty DONT_OVERWRITE_IGNORED = POW[19], --(1u << 19) Don't overwrite ignored files that exist in the checkout target CONFLICT_STYLE_MERGE = POW[20], --(1u << 20) Write normal merge files for conflicts CONFLICT_STYLE_DIFF3 = POW[21], --(1u << 21) Include common ancestor data in diff3 format files for conflicts DONT_REMOVE_EXISTING = POW[22], --(1u << 22) Don't overwrite existing files or folders DONT_WRITE_INDEX = POW[23], --(1u << 23) Normally checkout writes the index upon completion; this prevents that. DRY_RUN = POW[24], --(1u << 24), CONFLICT_STYLE_ZDIFF3 = POW[25], --(1u << 25) Include common ancestor data in zdiff3 format for conflicts. --(NOT IMPLEMENTED) UPDATE_SUBMODULES = POW[16], --(1u << 16) Recursively checkout submodules with same options UPDATE_SUBMODULES_IF_CHANGED = POW[17], --(1u << 17) Recursively checkout submodules if HEAD moved in super repo } ---@enum GIT_MERGE M.GIT_MERGE = { FIND_RENAMES = 1, --(1 << 0): Detect renames that occur between the common ancestor and the "ours" FAIL_ON_CONFLICT = POW[1], --(1 << 1): If a conflict occurs, exit immediately SKIP_REUC = POW[2], --(1 << 2): Do not write the REUC extension on the generated index NO_RECURSIVE = POW[3], --(1 << 3): This flag provides a similar merge base to `git-merge-resolve`. VIRTUAL_BASE = POW[4], --(1 << 4): Treat this merge as if it is to produce the virtual base of recursive. } -- Inits helper local NULL = ffi.cast('void*', nil) M.GIT_APPLY_OPTIONS_INIT = { { M.GIT_APPLY_OPTIONS_VERSION } } M.GIT_BLAME_OPTIONS_INIT = { { M.GIT_BLAME_OPTIONS_VERSION } } M.GIT_STATUS_OPTIONS_INIT = { { M.GIT_STATUS_OPTIONS_VERSION } } M.GIT_DIFF_OPTIONS_INIT = { { M.GIT_STATUS_OPTIONS_VERSION, 0, M.GIT_SUBMODULE.IGNORE_UNSPECIFIED, { NULL, 0 }, NULL, NULL, NULL, 3, }, } M.GIT_DIFF_FIND_OPTIONS_INIT = { { M.GIT_DIFF_FIND_OPTIONS_VERSION } } M.GIT_CHECKOUT_OPTIONS_INIT = { { M.GIT_CHECKOUT_OPTIONS_VERSION, M.GIT_CHECKOUT.SAFE } } M.GIT_MERGE_OPTIONS_INIT = { { M.GIT_MERGE_OPTIONS_VERSION, M.GIT_MERGE.FIND_RENAMES } } M.GIT_REBASE_OPTIONS_INIT = { { M.GIT_REBASE_OPTIONS_VERSION, 0, 0, NULL, M.GIT_MERGE_OPTIONS_INIT[1], M.GIT_CHECKOUT_OPTIONS_INIT[1], NULL, NULL, }, } return M ================================================ FILE: lua/blink/tree/git/stat.lua ================================================ -- Made by SuperBo in Fugit2 -- https://github.com/SuperBo/fugit2.nvim/tree/70662d529fe98790d7b2104b4dd67dd229332194 -- Licensed under MIT local M = {} local S_IFMT = 0xf000 local S_IFSOCK = 0xc000 local S_IFLNK = 0xa000 local S_IFREG = 0x8000 local S_IFBLK = 0x6000 local S_IFDIR = 0x4000 local S_IFCHR = 0x2000 local S_IFIFO = 0x1000 local S_ISUID = 0x800 local S_ISGID = 0x400 local S_ISVTX = 0x200 M.S_IFMT = S_IFMT M.S_IFLNK = S_IFLNK M.S_IFREG = S_IFREG M.S_IFDIR = S_IFDIR ---@param m integer file mode ---@return boolean function M.S_ISLNK(m) return bit.band(m, S_IFMT) == S_IFLNK end ---@param m integer file mode ---@return boolean function M.S_ISREG(m) return bit.band(m, S_IFMT) == S_IFREG end ---@param m integer file mode ---@return boolean function M.S_ISDIR(m) return bit.band(m, S_IFMT) == S_IFDIR end ---@param m integer file mode ---@return boolean function M.S_ISCHR(m) return bit.band(m, S_IFMT) == S_IFCHR end ---@param m integer file mode ---@return boolean function M.S_ISBLK(m) return bit.band(m, S_IFMT) == S_IFBLK end ---@param m integer file mode ---@return boolean function M.S_ISFIFO(m) return bit.band(m, S_IFMT) == S_IFIFO end ---@param m integer file mode ---@return boolean function M.S_ISSOCK(m) return bit.band(m, S_IFMT) == S_IFSOCK end return M ================================================ FILE: lua/blink/tree/init.lua ================================================ -- todo: symlinks local api = vim.api local M = { inst = nil, } function M.setup(opts) require('blink.tree.config').setup(opts) M.setup_highlights() vim.api.nvim_create_user_command('BlinkTree', function(info) local args = vim.split(info.args, ' ') local command = args[1] or 'toggle' local silent = args[2] == 'silent' if command == 'toggle' then M.toggle() elseif command == 'open' then M.open(silent) elseif command == 'close' then M.close() elseif command == 'toggle-focus' then M.toggle_focus() elseif command == 'focus' then M.focus() elseif command == 'refresh' then M.refresh() elseif command == 'reveal' then M.reveal(silent) end end, { nargs = '*' }) end function M.get_inst() if M.inst == nil then M.inst = require('blink.tree.window').new() end return M.inst end function M.toggle() M.get_inst():toggle() end function M.open(silent) M.get_inst():open(silent) end function M.close() M.get_inst():close() end function M.toggle_focus() M.get_inst():toggle_focus() end function M.focus() M.get_inst():focus() end function M.refresh() M.get_inst():refresh() end function M.reveal(silent) M.get_inst():reveal(silent) end function M.setup_highlights() api.nvim_set_hl(0, 'BlinkTreeNormal', { link = 'Normal', default = true }) api.nvim_set_hl(0, 'BlinkTreeNormalNC', { link = 'NormalNC', default = true }) api.nvim_set_hl(0, 'BlinkTreeSignColumn', { link = 'SignColumn', default = true }) api.nvim_set_hl(0, 'BlinkTreeCursorLine', { link = 'CursorLine', default = true }) api.nvim_set_hl(0, 'BlinkTreeFloatBorder', { link = 'FloatBorder', default = true }) api.nvim_set_hl(0, 'BlinkTreeStatusLine', { link = 'StatusLine', default = true }) api.nvim_set_hl(0, 'BlinkTreeStatusLineNC', { link = 'StatusLineNC', default = true }) api.nvim_set_hl(0, 'BlinkTreeVertSplit', { link = 'VertSplit', default = true }) end return M ================================================ FILE: lua/blink/tree/lib/fs.lua ================================================ -- todo: manage number of open files to ensure we don't go over limit -- likely via a queue of some sort local sep = '/' local uv = vim.uv local config = { hide_dotfiles = true, hide = { ['.cache'] = true }, never_show = { ['.git'] = true, ['node_modules'] = true }, } local FS = {} function FS.create_file(root_dir, path) -- Split the path into parts local parts = {} for part in string.gmatch(path, '([^/]+)') do table.insert(parts, part) end -- Ensure no invalid parts for _, part in ipairs(parts) do if part == '.' or part == '..' or part == '' then error('Invalid path: contains "." or ".." or ""') end end -- Construct the full path local full_path = root_dir for i, part in ipairs(parts) do full_path = full_path .. sep .. part local part_type = i == #parts and not vim.endswith(path, sep) and 'file' or 'directory' local stat = uv.fs_stat(full_path) local exists = stat ~= nil and stat.type == part_type if exists then if part_type == 'file' then error('File already exists: ' .. full_path) end goto continue end -- Create the file if part_type == 'file' then local fd = uv.fs_open(full_path, 'w', 438) -- 438 is 0666 in octal if fd then uv.fs_close(fd) else error('Failed to create file: ' .. full_path) end -- Create the directory else local success, err = pcall(uv.fs_mkdir, full_path, 493) -- 493 is 0755 in octal if not success then error('Failed to create directory: ' .. full_path .. ' (' .. err .. ')') end end ::continue:: end return full_path end function FS.rename(old_path, new_path) local success, err = pcall(vim.lsp.util.rename, old_path, new_path) if not success then error('Failed to rename: ' .. old_path .. ' -> ' .. new_path .. ' (' .. err .. ')') end end function FS.copy_file(old_path, new_path) local success, err = pcall(uv.fs_copyfile, old_path, new_path) if not success then error('Failed to copy: ' .. old_path .. ' -> ' .. new_path .. ' (' .. err .. ')') end end function FS.read_file(path) local fd = uv.fs_open(path, 'r', 438) -- 438 is 0666 in octal if not fd then error('Failed to open file: ' .. path) end local data = uv.fs_read(fd, uv.fs_stat(path).size, 0) uv.fs_close(fd) return data end function FS.read_file_async(path, callback) uv.fs_open(path, 'r', 438, function(open_err, fd) if open_err or fd == nil then return callback(open_err) end local callback_and_close = function(err, data) uv.fs_close(fd, function() end) callback(err, data) end uv.fs_stat(path, function(stat_err, stat) if stat_err or stat == nil then return callback_and_close(stat_err) end uv.fs_read(fd, stat.size, 0, callback_and_close) end) end) end function FS.scan_dir_async(path, callback) local max_entries = 200 -- open directory, return early if failed uv.fs_opendir(path, function(err, handle) if err ~= nil or handle == nil then -- print('Error opening directory: ' .. parent.path .. ' ' .. (err or 'nil')) callback({}) return end local all_entries = {} local function read_dir() uv.fs_readdir(handle, function(err, entries) if err ~= nil or entries == nil then -- print('Error reading directory: ' .. parent.path .. ' ' .. (err or 'nil')) callback({}) return end vim.list_extend(all_entries, entries) if #entries == max_entries then read_dir() else callback(all_entries) end end) end read_dir() end, max_entries) end function FS.watch_dir(path, callback) local handle = uv.new_fs_event() if handle == nil then error('Failed to create fs event handle') end FS.scan_dir_async(path, function(entries) callback(entries) -- begin watching handle:start(path, {}, function() FS.scan_dir_async(path, callback) end) end) -- unsubscribe return function() handle:stop() end end function FS.path_starts_with(path, prefix) path = path[#path] == sep and path or path .. sep prefix = prefix[#prefix] == sep and prefix or prefix .. sep return vim.startswith(path, prefix) end return FS ================================================ FILE: lua/blink/tree/lib/tree.lua ================================================ local allConfig = require('blink.tree.config') local config = { hide_dotfiles = allConfig.hide_dotfiles, hide = allConfig.hide, never_show = allConfig.never_show, } local Tree = {} function Tree.make_root(path) -- replace home with ~ local filename = path local home = vim.env.HOME if home ~= nil and vim.startswith(path, home) then filename = '~' .. string.sub(path, string.len(home) + 1) end local node = Tree.make_node(nil, path, filename, true) node.expanded = true return node end function Tree.make_node(parent, path, filename, is_dir) local node = { parent = parent, children = {}, path = path, filename = filename, is_dir = is_dir, expanded = false, flags = { cut = false, copy = false, }, } return node end function Tree.destroy_node(node) if node.git_repo then node.git_repo.destroy() end if node.children then for _, child in ipairs(node.children) do Tree.destroy_node(child) end end end function Tree.new(filename, is_dir, expanded) return { filename = filename, is_dir = is_dir, expanded = expanded, children = {}, } end -- loops through the nodes and treats the second argument as the source -- of truth, adding and removing nodes as needed, but using the object reference -- from the first list whenever possible function Tree.merge_nodes(old_nodes, nodes) local old_node_count = #old_nodes local changed = false local merged_nodes = {} -- FIXME: probably breaks if theres two files like FILE.md and file.md local old_node_idx = 1 for _, node in ipairs(nodes) do while old_node_idx <= old_node_count do local old_node = old_nodes[old_node_idx] if not old_node.is_dir and node.is_dir then goto continue end if old_node.is_dir == node.is_dir and string.lower(old_node.filename) >= string.lower(node.filename) then goto continue end old_node_idx = old_node_idx + 1 changed = true end ::continue:: if old_node_idx <= old_node_count and old_nodes[old_node_idx].filename == node.filename then table.insert(merged_nodes, old_nodes[old_node_idx]) old_node_idx = old_node_idx + 1 Tree.destroy_node(node) -- didn't include new node so we destroy it else table.insert(merged_nodes, node) changed = true end end -- didn't include all the previous nodes if old_node_idx < old_node_count then changed = true end -- destroy old nodes that weren't included -- TODO: probably slow for i = old_node_idx, old_node_count do local old_node = old_nodes[i] if not vim.tbl_contains(merged_nodes, old_node) then Tree.destroy_node(old_node) end end return merged_nodes, changed end function Tree.make_children(parent, entries) local children = {} -- scan directory and build nodes for _, entry in ipairs(entries) do local path = parent.path .. '/' .. entry.name local node = Tree.make_node(parent, path, entry.name, entry.type == 'directory') -- TODO: move to renderer, differentiate between hidden and never_show local should_show = (not vim.tbl_contains(config.hide, node.filename)) and (not vim.tbl_contains(config.never_show, node.filename)) if should_show then table.insert(children, node) end end -- sort by name (insensitive) and then by type (dir first) table.sort(children, function(a, b) if a.is_dir == b.is_dir then return string.lower(a.filename) < string.lower(b.filename) end return a.is_dir end) return children end function Tree.build_tree(parent, on_initial, on_change) local fs = require('blink.tree.lib.fs') local git = require('blink.tree.git') if not parent.is_dir or not parent.expanded or parent.watch_unsubscribe then on_initial(parent, false) return end local is_initial = true parent.watch_unsubscribe = fs.watch_dir(parent.path, function(entries) -- check if this is a git repo if not parent.git_repo then for _, entry in ipairs(entries) do if entry.type == 'directory' and entry.name == '.git' then parent.git_repo = git.new(parent.path, on_change) break end end end local cb = is_initial and on_initial or on_change is_initial = false -- update the children local children, changed = Tree.merge_nodes(parent.children, Tree.make_children(parent, entries)) parent.children = children -- scan child directories and hook up the on_change cb local pending_scans = 0 for _, child in ipairs(children) do -- scan child directories if child.is_dir and child.expanded then pending_scans = pending_scans + 1 Tree.build_tree(child, function(_, child_changed) changed = changed or child_changed pending_scans = pending_scans - 1 if pending_scans == 0 then cb(parent, changed) end end, on_change) end end -- no child directories if pending_scans == 0 then cb(parent, changed) end end) end function Tree.clear_watch(node) Tree.traverse(node, function() if node.watch_unsubscribe then node.watch_unsubscribe() node.watch_unsubscribe = nil end end) end function Tree.get_repo(node) local repo = node.git_repo local parent = node.parent while parent ~= nil and repo == nil do repo = parent.git_repo parent = parent.parent end return repo end function Tree.traverse(node, cb) cb(node) for _, child in ipairs(node.children) do Tree.traverse(child, cb) end end return Tree ================================================ FILE: lua/blink/tree/lib/utils.lua ================================================ local api = vim.api local Utils = {} function Utils.pick_or_create_non_special_window() local wins = api.nvim_list_wins() table.insert(wins, 1, api.nvim_get_current_win()) local ignore_list = { 'blink-tree', 'Trouble', 'qf', 'edgy' } local ignore = {} for _, ft in ipairs(ignore_list) do ignore[ft] = true end -- pick the first non-special window for _, win in ipairs(wins) do local is_relative = api.nvim_win_get_config(win).relative ~= '' local buf = api.nvim_win_get_buf(win) local options = vim.bo[buf] if not is_relative and not ignore[options.filetype] and not ignore[options.buftype] then return win end end -- create a new window if all are special return api.nvim_open_win(0, false, { vertical = true, split = 'right', }) end --- Debounces a function on the trailing edge. Automatically --- `schedule_wrap()`s. --- --- @param fn (function) Function to debounce --- @param timeout (number) Timeout in ms --- @returns (function, timer) Debounced function and timer. Remember to call --- `timer:close()` at the end or you will leak memory! function Utils.debounce(fn, timeout) local timer = vim.loop.new_timer() local wrapped_fn function wrapped_fn(...) local argv = { ... } local argc = select('#', ...) timer:start(timeout, 0, function() pcall(vim.schedule_wrap(fn), unpack(argv, 1, argc)) end) end return wrapped_fn, timer end return Utils ================================================ FILE: lua/blink/tree/lib/uv.lua ================================================ local uv = vim.loop local UV = {} function UV.exec_async(opts, callback) callback = callback or function() end local stdout = uv.new_pipe() uv.spawn( opts.command[1], { args = vim.list_slice(opts.command, 2), cwd = opts.cwd, stdio = { nil, stdout, nil } }, function(code) if code ~= 0 then -- TODO: log something? return callback(code, '') end local buffer = '' uv.read_start(stdout, function(err, data) assert(not err, err) if data then buffer = buffer .. data return end uv.read_stop(stdout) stdout:close() callback(code, buffer) end) end ) end return UV ================================================ FILE: lua/blink/tree/popup.lua ================================================ local api = vim.api local Popup = {} -- function Popup.new() -- self.winnr = nil -- self.bufnr = nil -- return self -- end -- function Popup:open(opts) if self.winnr ~= nil then return end opts = opts or {} opts.relative = opts.relative or 'cursor' opts.width = opts.width or 40 opts.height = opts.height or 20 opts.row = opts.row or 1 opts.col = opts.col or 1 opts.style = opts.style or 'minimal' opts.border = opts.border or 'single' local initial_text = opts.initial_text or '' opts.initial_text = nil self.bufnr = api.nvim_create_buf(false, true) api.nvim_buf_set_lines(self.bufnr, 0, -1, false, { initial_text }) self.winnr = api.nvim_open_win(self.bufnr, true, opts) api.nvim_win_set_cursor(self.winnr, { 1, #initial_text }) -- move cursor to the end end function Popup.new_input(opts, callback) local self = setmetatable({}, { __index = Popup }) opts = opts or {} opts.width = opts.width or 40 opts.height = opts.height or 1 self:open(opts) -- enter insert mode api.nvim_feedkeys('a', 'n', true) local has_run = false local on_submit = function() if has_run then return end has_run = true local line = api.nvim_get_current_line() self:close() callback(line) end local on_abort = function() if has_run then return end has_run = true self:close() callback(nil) end -- listen for close events api.nvim_buf_set_keymap(self.bufnr, 'i', '', '', { callback = on_submit, }) api.nvim_buf_set_keymap(self.bufnr, 'i', '', '', { callback = on_abort, }) api.nvim_buf_set_keymap(self.bufnr, 'i', '', '', { callback = on_abort, }) api.nvim_create_autocmd('WinLeave', { callback = on_abort, once = true, }) end function Popup:close() if api.nvim_win_is_valid(self.winnr) then vim.api.nvim_win_close(self.winnr, true) self.winnr = nil end if api.nvim_buf_is_valid(self.bufnr) then vim.api.nvim_buf_delete(self.bufnr, { force = true }) self.bufnr = nil end end return Popup ================================================ FILE: lua/blink/tree/renderer.lua ================================================ local api = vim.api local lib_tree = require('blink.tree.lib.tree') local ns = api.nvim_create_namespace('blink_tree') local Renderer = {} function Renderer:render_node(line_number, node, indent) local prefix = ' ' .. string.rep(' ', indent) local icon, highlight = self:get_icon(node) local name = node.filename -- keep track of which node is associated with which line local nodes_by_lines = {} nodes_by_lines[line_number] = node local icon_spaces = ' ' local line = prefix .. icon_spaces .. name local line_index = line_number - 1 api.nvim_buf_set_lines(self.bufnr, line_index, line_index + 1, false, { line }) -- update render state for decorations -- todo: calculate this on demand instead of storing it node.render_state = { length = #line, indent = indent, icon = { hl = highlight, idx = #prefix, icon = icon }, } if node.expanded then for _, child in ipairs(node.children) do local child_nodes_by_lines, new_line_number = self:render_node(line_number + 1, child, indent + 1) line_number = new_line_number for child_line_number, child_node in pairs(child_nodes_by_lines) do nodes_by_lines[child_line_number] = child_node end end end return nodes_by_lines, line_number end function Renderer.new(bufnr) local self = setmetatable({}, { __index = Renderer }) self.nodes_by_lines = nil self.bufnr = bufnr local modified = {} -- Draws the indent and icon api.nvim_set_decoration_provider(ns, { on_win = function(_, curr_winid, currr_bufnr) local should_render = curr_winid == self.winnr and currr_bufnr == self.bufnr if not should_render then return false end modified = self.get_modified_buffers() return curr_winid == self.winnr and currr_bufnr == self.bufnr end, on_line = function(_, _, _, line_number) if self.nodes_by_lines == nil then return end local node = self.nodes_by_lines[line_number + 1] local next_node = self.nodes_by_lines[line_number + 2] if node == nil then return end local render_state = node.render_state if render_state == nil then return end -- Indents and Icon local indent = render_state.indent local indent_str = '' while indent > 0 do if indent == render_state.indent then indent_str = indent_str .. ' ' elseif (next_node == nil or next_node.render_state.indent < render_state.indent) and indent == 1 then indent_str = indent_str .. '└ ' else indent_str = indent_str .. '│ ' end indent = indent - 1 end api.nvim_buf_set_extmark(bufnr, ns, line_number, render_state.icon.idx, { virt_text = { { indent_str, 'BlinkTreeIndent' }, { render_state.icon.icon, render_state.icon.hl } }, virt_text_win_col = 1, virt_text_pos = 'overlay', hl_mode = 'combine', ephemeral = true, }) -- Buffer modified if modified[node.path] then api.nvim_buf_set_extmark(bufnr, ns, line_number, 2, { virt_text = { { ' ● ', 'BlinkTreeModified' } }, virt_text_pos = 'right_align', hl_mode = 'combine', priority = 1, ephemeral = true, }) end -- Git Status local repo = lib_tree.get_repo(node) if repo ~= nil and line_number > 0 then local status = repo:get_status(node.path) if status ~= nil then local hl = repo.get_hl_for_status(status, node.is_dir) if hl ~= nil then api.nvim_buf_set_extmark(bufnr, ns, line_number, 0, { end_col = render_state.length, hl_group = hl, hl_eol = true, ephemeral = true, }) end end end -- Cut / Copy if node.flags.cut then api.nvim_buf_set_extmark(bufnr, ns, line_number, 0, { virt_text = { { '[cut]', 'BlinkTreeFlagCut' } }, hl_mode = 'combine', priority = 1, ephemeral = true, }) elseif node.flags.copy then api.nvim_buf_set_extmark(bufnr, ns, line_number, 0, { virt_text = { { '[copy]', 'BlinkTreeFlagCopy' } }, hl_mode = 'combine', priority = 1, ephemeral = true, }) end end, }) return self end function Renderer.get_modified_buffers() local modified = {} for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do if vim.api.nvim_buf_is_loaded(bufnr) and vim.api.nvim_buf_get_option(bufnr, 'modified') then local path = vim.api.nvim_buf_get_name(bufnr) modified[path] = true end end return modified end function Renderer:render_window(winnr, root) self.winnr = winnr api.nvim_set_option_value('modifiable', true, { buf = self.bufnr }) -- render local nodes_by_lines, last_line_number = self:render_node(1, root, 0) self.nodes_by_lines = nodes_by_lines -- clear any extra lines from the last render api.nvim_buf_set_lines(self.bufnr, last_line_number, api.nvim_buf_line_count(self.bufnr), false, {}) api.nvim_set_option_value('modifiable', false, { buf = self.bufnr }) return nodes_by_lines end -- redraw the entire window with the decoration provider function Renderer:redraw() api.nvim__redraw({ win = self.winnr, valid = false }) end function Renderer:get_icon(node) if node.is_dir then return node.expanded and '' or '', 'Blue' else local devicons = require('nvim-web-devicons') local icon, color = devicons.get_icon(node.filename, vim.fn.fnamemodify(node.path, ':e'), { default = true }) return icon, color end end function Renderer:get_hovered_node() if self.nodes_by_lines == nil then return end local cursor = api.nvim_win_get_cursor(self.winnr) return self.nodes_by_lines[cursor[1]] end function Renderer:select_node(node) if self.nodes_by_lines == nil then return end for line_number, n in pairs(self.nodes_by_lines) do if n == node then api.nvim_win_set_cursor(self.winnr, { line_number, 0 }) return end end end function Renderer:select_path(path) if self.nodes_by_lines == nil then return end for line_number, n in pairs(self.nodes_by_lines) do if n.path == path then api.nvim_win_set_cursor(self.winnr, { line_number, 0 }) return end end end return Renderer ================================================ FILE: lua/blink/tree/tree.lua ================================================ local fs = require('blink.tree.lib.fs') local lib_tree = require('blink.tree.lib.tree') local Tree = {} function Tree.new(path, on_changed) local self = setmetatable({}, { __index = Tree }) self.path = path self.root = lib_tree.make_root(path) self.on_changed = on_changed -- immediately build lib_tree.build_tree(self.root, function(tree) self.root = tree self.on_changed() end, function() self.on_changed() end) return self end -------------------- --- Public API function Tree:collapse(node) if not node.expanded then return end node.expanded = false lib_tree.clear_watch(node) self.on_changed() end function Tree:expand(node, callback) if node.expanded then return callback() end node.expanded = true local on_initial = function() self.on_changed(callback) end local on_change = function() self.on_changed() end lib_tree.build_tree(node, on_initial, on_change) end function Tree:expand_path(path, callback) callback = callback or function() end if not fs.path_starts_with(path, self.root.path) then return callback('Path is not contained in tree', nil) end local function expand_and_recurse(node, cb) for _, child in ipairs(node.children) do if fs.path_starts_with(path, child.path) then local continue = function() -- final child if child.path == path then return cb(nil, child) end -- or recurse expand_and_recurse(child, cb) end -- already expanded, continue if child.expanded then return continue() end -- otherwise, expand the node but don't render child.expanded = true return lib_tree.build_tree(child, continue, function() self.on_changed() end) end end return cb('Path not found', nil) end expand_and_recurse(self.root, function(err, node) self.on_changed(function() callback(err, node) end) end) end function Tree:find_node_by_path(path, parent) parent = parent or self.root if parent.path == path then return parent end for _, child in ipairs(parent.children) do if fs.path_starts_with(path, child.path) then return self:find_node_by_path(path, child) end end return nil end function Tree:destroy() lib_tree.traverse(self.root, function(node) if node.watch_unsubscribe then node.watch_unsubscribe() end if node.git_repo then node.git_repo:destroy() end end) end return Tree ================================================ FILE: lua/blink/tree/window.lua ================================================ local api = vim.api local Window = {} function Window.new() local self = setmetatable({}, { __index = Window }) self.winnr = -1 self.bufnr = -1 self.tree = require('blink.tree.tree').new(vim.fn.getcwd(), function(callback) self:render(callback) end) self.augroup = api.nvim_create_augroup('BlinkTreeWindow', { clear = true }) api.nvim_create_autocmd('VimLeavePre', { group = self.augroup, callback = function() if self.tree ~= nil then self.tree:destroy() self.tree = nil end end, }) api.nvim_create_autocmd('WinEnter', { group = self.augroup, callback = function() local current_win = api.nvim_get_current_win() if current_win == self.winnr then api.nvim_feedkeys(api.nvim_replace_termcodes('', true, false, true), 'n', false) end end, }) -- only allow the cursor to be on the first column which will always be empty -- avoiding issues with cursorword plugins api.nvim_create_autocmd('CursorMoved', { group = self.augroup, callback = function() if self.winnr == api.nvim_get_current_win() then local cursor = api.nvim_win_get_cursor(self.winnr) api.nvim_win_set_cursor(self.winnr, { cursor[1], 0 }) end end, }) -- recreate the tree on dir change api.nvim_create_autocmd('DirChanged', { group = self.augroup, callback = function() if not self.renderer then return end self.tree:destroy() self.tree = require('blink.tree.tree').new(vim.fn.getcwd(), function() self:render() end) end, }) -- set buffer options api.nvim_create_autocmd('BufEnter', { group = self.augroup, callback = function() if vim.bo.filetype ~= 'blink-tree' then return end -- set local window options vim.cmd('setlocal winfixwidth') vim.cmd('setlocal cursorline') vim.cmd('setlocal cursorlineopt=line') vim.cmd('setlocal signcolumn=no') vim.cmd('setlocal nowrap') vim.cmd('setlocal nolist nospell nonumber norelativenumber') vim.cmd( 'setlocal winhighlight=Normal:BlinkTreeNormal,NormalNC:BlinkTreeNormalNC,SignColumn:BlinkTreeSignColumn,CursorLine:BlinkTreeCursorLine,FloatBorder:BlinkTreeFloatBorder,StatusLine:BlinkTreeStatusLine,StatusLineNC:BlinkTreeStatusLineNC,VertSplit:BlinkTreeVertSplit,EndOfBuffer:BlinkTreeEndOfBuffer' ) end, }) -- hide the cursor when window is focused -- todo: should use winenter and winleave instead? local prev_cursor local prev_blend api.nvim_create_autocmd('BufEnter', { group = self.augroup, callback = function() if vim.bo.filetype == 'blink-tree' and prev_cursor == nil then prev_cursor = api.nvim_get_option_value('guicursor', {}) api.nvim_set_option_value('guicursor', 'n:block-Cursor', {}) 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 = self.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, }) -- prevent buffer from being changed -- api.nvim_create_autocmd('BufEnter', { -- callback = function() -- -- ignore if not in tree window -- if self.winnr ~= api.nvim_get_current_win() or not api.nvim_win_is_valid(self.winnr) then return end -- if self.bufnr == api.nvim_get_current_buf() or not api.nvim_buf_is_valid(self.bufnr) then return end -- local bufnr = api.nvim_get_current_buf() -- -- -- restore tree buffer to tree window -- api.nvim_win_set_buf(self.winnr, self.bufnr) -- -- -- move new buffer to a non-tree window -- local winnr = require('blink.tree.lib.utils').pick_or_create_non_special_window() -- api.nvim_set_current_win(winnr) -- api.nvim_win_set_buf(winnr, bufnr) -- end, -- }) return self end function Window:refresh() -- todo: end function Window:ensure_buffer() -- TODO: should check if buffer is valid and cleanup previous if api.nvim_buf_is_valid(self.bufnr) then return end self.bufnr = api.nvim_create_buf(false, true) api.nvim_set_option_value('buftype', 'nofile', { buf = self.bufnr }) api.nvim_set_option_value('filetype', 'blink-tree', { buf = self.bufnr }) api.nvim_set_option_value('buflisted', false, { buf = self.bufnr }) api.nvim_set_option_value('modifiable', false, { buf = self.bufnr }) api.nvim_set_option_value('swapfile', false, { buf = self.bufnr }) self.renderer = require('blink.tree.renderer').new(self.bufnr) require('blink.tree.binds').attach_to_instance(self) end function Window:render(callback) vim.schedule(function() if not self:is_open() then return end self.nodes_by_line = self.renderer:render_window(self.winnr, self.tree.root) if callback then callback() end end) end function Window:open(silent, callback) self:ensure_buffer() if self:is_open() then if callback then callback() end return end self.winnr = api.nvim_open_win(self.bufnr, not silent, { win = -1, vertical = true, split = 'left', width = 40, }) -- HACK: we manually trigger this because neovim-session disable autocmds -- during startup -- todo: only force the autocmds if autocmds are disabled local prev_win = vim.api.nvim_get_current_win() if silent then vim.api.nvim_set_current_win(self.winnr) end vim.cmd('do BufEnter') if silent then vim.schedule(function() vim.cmd('do BufLeave') vim.api.nvim_set_current_win(prev_win) end) end self:render(callback) end function Window:close() if not self:is_open() then return end -- if we're the last window, just replace the current buffer with a new buffer if api.nvim_tabpage_list_wins(0)[1] == self.winnr and #api.nvim_list_wins() == 1 then return vim.cmd('enew') end -- otherwise close the window -- todo: destroy renderer? api.nvim_win_close(self.winnr, true) api.nvim_buf_delete(self.bufnr, { force = true }) self.winnr = -1 end function Window:toggle() if self:is_open() then self:close() else self:open() end end function Window:toggle_focus() if not self:is_open() then return self:open() end local win = api.nvim_get_current_win() if win == self.winnr then vim.cmd('wincmd p') else api.nvim_set_current_win(self.winnr) end end function Window:focus() if not self:is_open() then return self:open() end api.nvim_set_current_win(self.winnr) end function Window:is_open() return api.nvim_win_is_valid(self.winnr) and api.nvim_win_get_buf(self.winnr) == self.bufnr and api.nvim_buf_is_valid(self.bufnr) end function Window:reveal(silent) local current_buf_path = vim.fn.expand(vim.api.nvim_buf_get_name(0)) if current_buf_path == '' then return end self:open(silent, function() self.tree:expand_path(current_buf_path, function() self.renderer:select_path(current_buf_path) end) if not silent then self:focus() end end) end return Window ================================================ FILE: scripts/dual_log.sh ================================================ # Written by echasnovski in mini.nvim # https://github.com/echasnovski/mini.nvim/blob/82584a42c636efd11781211da1396f4c1f5e7877/scripts/dual_log.sh # Check standalone repos result local_repos="$( ls -d dual/repos/*/ )" for repo in $local_repos; do printf "\n\033[1m$( basename $repo )\033[0m\n" cd $repo > /dev/null git log origin/main..main --abbrev-commit --format=oneline cd - > /dev/null done ================================================ FILE: scripts/dual_push.sh ================================================ # Written by echasnovski in mini.nvim # https://github.com/echasnovski/mini.nvim/blob/82584a42c636efd11781211da1396f4c1f5e7877/scripts/dual_push.sh # Push standalone repos result local_repos="$( ls -d dual/repos/*/ )" for repo in $local_repos; do printf "\n\033[1mPushing $( basename $repo )\033[0m\n" cd $repo > /dev/null # Push only if there is something to push (saves time) if [ $( git rev-parse main ) != $( git rev-parse origin/main ) ] then git push origin main fi cd - > /dev/null done echo '' ================================================ FILE: scripts/dual_sync.sh ================================================ # Written by echasnovski in mini.nvim # https://github.com/echasnovski/mini.nvim/blob/82584a42c636efd11781211da1396f4c1f5e7877/scripts/dual_sync.sh # Perform local sync of standalone repositories repos_dir=dual/repos patches_dir=dual/patches mkdir -p $repos_dir mkdir -p $patches_dir sync_module () { # First argument is a string with module name. Others - extra paths to track # for module. module=$1 shift repo="$( realpath $repos_dir/blink.$module )" patch="$( realpath $patches_dir/blink.$module.patch )" printf "\n\033[1mblink.$module\033[0m\n" # Possibly pull repository if [[ ! -d $repo ]] then printf "Pulling\n" github_repo="blink.$module" git clone --filter=blob:none https://github.com/saghen/$github_repo.git $repo else printf "No pulling (already present)\n" fi # Make patch with commits from 'sync' branch to current HEAD which affect # files related to the module printf "Making patch\n" git format-patch sync..HEAD --output $patch -- \ lua/blink/$module \ readmes/$module \ .gitignore \ .stylua.toml \ LICENSE \ "$@" # Do nothing if patch is empty if [[ ! -s $patch ]] then rm $patch printf "Patch is empty\n" return fi # Tweak patch: # - Move 'readmes/xxx/*' to the top level. This should modify only patch # metadata, and not text (assuming it uses 'readmes/mini-xxx.md' on # purpose; as in "use [this link](https://.../readmes/mini-xxx.md)"). # - move 'scripts/xxx/*' to the top level # TODO: handle relative links sed -i 's|readmes/[^/]*/\(.*\)|\1|g' $patch sed -i 's|scripts/[^/]*/\(.*\)|\1|g' $patch # Apply patch printf "Applying patch\n" cd $repo git am $patch cd - > /dev/null } sync_module "chartoggle" sync_module "clue" sync_module "cmp" Cargo.toml Cargo.lock flake.nix flake.lock build.rs sync_module "indent" sync_module "select" sync_module "tree" ================================================ FILE: src/job/default.rs ================================================ use crate::options::JobStartOptions; use portable_pty::{native_pty_system, CommandBuilder, PtySize, PtySystem}; use std::sync::atomic::AtomicUsize; struct Job { id: usize, pid: Option, pty: Option, stdin: Option, stdout: Option, stderr: Option, options: JobStartOptions, } impl Job { fn new(id: usize, cmd: &Vec, options: JobStartOptions) -> Self {} } ================================================ FILE: src/job/mod.rs ================================================ pub mod default; pub mod options; pub mod pty; ================================================ FILE: src/job/options.rs ================================================ use mlua::prelude::*; use std::collections::HashMap; #[derive(Clone)] pub struct JobStartOptions { pub cwd: String, pub env: HashMap, pub clear_env: bool, pub stdin: bool, pub stdout_buffered: bool, pub stderr_buffered: bool, pub on_stderr: Option, pub on_stdout: Option, pub on_exit: Option, // Use msgpack-rpc to communicate with the job over stdio // on_stdout is ignored // on_stderr can still be used pub rpc: bool, // Detach the job process, it will not be killed when neovim exits pub detach: bool, // Connect the job to a new pseudo terminal, and its stream to the master file descriptor // on_stdout recieves all output // on_stderr is ignored pub pty: bool, // Width of the pty terminal pub width: Option, // Height of the pty terminal pub height: Option, } impl FromLua for JobStartOptions { fn from_lua(value: LuaValue, _lua: &'_ Lua) -> LuaResult { if let Some(tab) = value.as_table() { let cwd: String = tab.get("cwd").unwrap_or( std::env::current_dir() .unwrap() .to_str() .unwrap() .to_string(), ); let env: HashMap = tab.get("env").unwrap_or_default(); let clear_env: bool = tab.get("clear_env").unwrap_or(false); let stdin: bool = tab.get("stdin").unwrap_or(true); let stdout_buffered: bool = tab.get("stdout_buffered").unwrap_or(false); let stderr_buffered: bool = tab.get("stderr_buffered").unwrap_or(false); let on_stderr: Option = tab.get("on_stderr")?; let on_stdout: Option = tab.get("on_stdout")?; let on_exit: Option = tab.get("on_exit")?; let rpc: bool = tab.get("rpc").unwrap_or(false); let detach: bool = tab.get("detach").unwrap_or(false); let pty: bool = tab.get("pty").unwrap_or(false); let width: Option = tab.get("width")?; let height: Option = tab.get("height")?; Ok(JobStartOptions { cwd, env, clear_env, stdin, stdout_buffered, stderr_buffered, on_stderr, on_stdout, on_exit, rpc, detach, pty, width, height, }) } else { Err(mlua::Error::FromLuaConversionError { from: "LuaValue", to: "JobStartOptions".to_string(), message: None, }) } } } ================================================ FILE: src/job/pty.rs ================================================ use crate::options::JobStartOptions; use anyhow::Result; use portable_pty::{Child, CommandBuilder, PtyPair, PtySize, native_pty_system}; struct JobPty { id: usize, pid: Option, child: Box, pair: PtyPair, stdin: Box, stdout: Box, options: JobStartOptions, } impl JobPty { fn new(id: usize, cmd: &Vec, options: JobStartOptions) -> Result { // Use the native pty implementation for the system let pty_system = native_pty_system(); // Create a new pty let pair = pty_system.openpty(PtySize { rows: options.height.unwrap_or(24) as u16, cols: options.width.unwrap_or(80) as u16, // Not all systems support pixel_width, pixel_height, // but it is good practice to set it to something // that matches the size of the selected font. That // is more complex than can be shown here in this // brief example though! pixel_width: 0, pixel_height: 0, })?; // Build the command let mut cmd = CommandBuilder::from_argv(cmd.iter().map(|s| s.into()).collect()); cmd.cwd(&options.cwd); if options.clear_env { cmd.env_clear(); } for (key, value) in options.env.iter() { cmd.env(key, value); } let child = pair.slave.spawn_command(cmd)?; let pid = child.process_id(); let stdin = pair.master.take_writer()?; let stdout = pair.master.try_clone_reader()?; Ok(JobPty { id, pid, child, pair, stdin, stdout, options, }) } fn send(&mut self, data: &[u8]) -> Result<()> { self.stdin.write_all(data)?; Ok(()) } fn pid(&self) -> u32 { self.pid.unwrap() } fn stop(&mut self) { // TODO: send SIGTERM, and then SIGKILL after timeout self.child.kill().unwrap(); } fn poll(&mut self) -> Result { self.poll_stdout()?; self.poll_exit() } fn poll_stdout(&mut self) -> Result { if let Some(on_stdout) = &self.options.on_stdout { if self.options.stdout_buffered { let mut buf = vec![]; self.stdout.read_to_end(&mut buf)?; on_stdout.call::<()>(buf).unwrap(); } let mut buf = [0; 2048]; while let Ok(bytes_read) = self.stdout.read(&mut buf) { if bytes_read > 0 { on_stdout.call::<()>(&buf[..bytes_read]).unwrap(); } return Ok(true); } } Ok(false) } fn poll_exit(&mut self) -> Result { let status = self.child.try_wait()?; if let Some(status) = &status { if let Some(on_exit) = &self.options.on_exit { on_exit.call::<()>(status.exit_code()).unwrap(); } } return Ok(status.is_some()); } } ================================================ FILE: src/job/trait.rs ================================================ ================================================ FILE: src/lib.rs ================================================ // mod job; // use crate::job::*; // // static ID_COUNTER: AtomicUsize = AtomicUsize::new(0); // static JOBS: LazyLock>> = LazyLock::new(|| Mutex::new(HashMap::new())); // // fn start_job(cmd: Vec, options: JobStartOptions) -> usize { // let job = Job::new(cmd, options); // JOBS.lock().unwrap().insert(job.id, job); // job.id // } // // fn job_pid(id: usize) -> Option { // let job = JOBS.lock().unwrap().get(&id); // let job = job.ok_or(mlua::Error::RuntimeError("Job not found".to_string()))?; // job.pid() // } // // fn stop_job(id: usize) { // let job = JOBS.lock().unwrap().remove(&id); // if let Some(job) = job { // job.stop(); // } // } // NOTE: skip_memory_check greatly improves performance // https://github.com/mlua-rs/mlua/issues/318 // #[mlua::lua_module(skip_memory_check)] // fn blink_job_internal(lua: &Lua) -> LuaResult { // let exports = lua.create_table()?; // exports.set("start", lua.create_function(start_job)?)?; // Ok(exports) // }