Update generated nvim config
This commit is contained in:
1
config/neovim/store/lazy-plugins/diffview.nvim/.github/FUNDING.yml
vendored
Normal file
1
config/neovim/store/lazy-plugins/diffview.nvim/.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
github: [sindrets]
|
||||
138
config/neovim/store/lazy-plugins/diffview.nvim/.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
138
config/neovim/store/lazy-plugins/diffview.nvim/.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@ -0,0 +1,138 @@
|
||||
name: Bug report
|
||||
description: Report a problem with diffview.nvim
|
||||
title: "[Bug] "
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Before reporting: search [existing issues](https://github.com/sindrets/diffview.nvim/issues) and make sure that diffview.nvim is updated to the latest version.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Description"
|
||||
description: "A description of the problem you're facing."
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Expected behavior"
|
||||
description: "A description of the behavior you expected:"
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Actual behavior"
|
||||
description: "Observed behavior (may optionally include images, or videos)."
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Steps to reproduce"
|
||||
description: "Steps to reproduce the issue, preferably using the minimal config provided below."
|
||||
placeholder: |
|
||||
1. `nvim --clean -u mini.lua`
|
||||
2. ...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Health check"
|
||||
value: |
|
||||
<details>
|
||||
<summary>Output of <code>:checkhealth diffview</code></summary>
|
||||
|
||||
```
|
||||
#######################
|
||||
### PUT OUTPUT HERE ###
|
||||
#######################
|
||||
```
|
||||
|
||||
</details>
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Log info"
|
||||
description: "Include relevant info from `:DiffviewLog`. Look at the time stamps of the log messages to determine what is relevant. Please do not include the entire log."
|
||||
value: |
|
||||
<details>
|
||||
<summary>Relevant info from <code>:DiffviewLog</code></summary>
|
||||
|
||||
```
|
||||
############################
|
||||
### PUT LOG CONTENT HERE ###
|
||||
############################
|
||||
```
|
||||
|
||||
</details>
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Neovim version"
|
||||
description: "Output of `nvim --version`"
|
||||
render: markdown
|
||||
placeholder: |
|
||||
NVIM v0.9.0
|
||||
Build type: RelWithDebInfo
|
||||
LuaJIT 2.1.0-beta3
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: "Operating system and version"
|
||||
description: "On \\*nix systems you can use the output of `uname -srom`"
|
||||
placeholder: "Linux 6.3.1-arch2-1 x86_64 GNU/Linux"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Minimal config"
|
||||
description: "If possible, please provide a **minimal** configuration necessary to reproduce the issue. Save this as `mini.lua`. If *absolutely* necessary, add plugins and config options from your `init.lua` at the indicated lines."
|
||||
render: Lua
|
||||
value: |
|
||||
-- #######################################
|
||||
-- ### USAGE: nvim --clean -u mini.lua ###
|
||||
-- #######################################
|
||||
|
||||
local root = vim.fn.stdpath("run") .. "/nvim/diffview.nvim"
|
||||
local plugin_dir = root .. "/plugins"
|
||||
vim.fn.mkdir(plugin_dir, "p")
|
||||
|
||||
for _, name in ipairs({ "config", "data", "state", "cache" }) do
|
||||
vim.env[("XDG_%s_HOME"):format(name:upper())] = root .. "/" .. name
|
||||
end
|
||||
|
||||
local plugins = {
|
||||
{ "nvim-web-devicons", url = "https://github.com/nvim-tree/nvim-web-devicons.git" },
|
||||
{ "diffview.nvim", url = "https://github.com/sindrets/diffview.nvim.git" },
|
||||
-- ##################################################################
|
||||
-- ### ADD PLUGINS THAT ARE _NECESSARY_ FOR REPRODUCING THE ISSUE ###
|
||||
-- ##################################################################
|
||||
}
|
||||
|
||||
for _, spec in ipairs(plugins) do
|
||||
local install_path = plugin_dir .. "/" .. spec[1]
|
||||
if vim.fn.isdirectory(install_path) ~= 1 then
|
||||
if spec.url then
|
||||
print(string.format("Installing '%s'...", spec[1]))
|
||||
vim.fn.system({ "git", "clone", "--depth=1", spec.url, install_path })
|
||||
end
|
||||
end
|
||||
vim.opt.runtimepath:append(spec.path or install_path)
|
||||
end
|
||||
|
||||
require("diffview").setup({
|
||||
-- ##############################################################################
|
||||
-- ### ADD DIFFVIEW.NVIM CONFIG THAT IS _NECESSARY_ FOR REPRODUCING THE ISSUE ###
|
||||
-- ##############################################################################
|
||||
})
|
||||
|
||||
vim.opt.termguicolors = true
|
||||
vim.cmd("colorscheme " .. (vim.fn.has("nvim-0.8") == 1 and "habamax" or "slate"))
|
||||
|
||||
-- ############################################################################
|
||||
-- ### ADD INIT.LUA SETTINGS THAT ARE _NECESSARY_ FOR REPRODUCING THE ISSUE ###
|
||||
-- ############################################################################
|
||||
|
||||
print("Ready!")
|
||||
validations:
|
||||
required: false
|
||||
1
config/neovim/store/lazy-plugins/diffview.nvim/.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
config/neovim/store/lazy-plugins/diffview.nvim/.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
blank_issues_enabled: true
|
||||
3
config/neovim/store/lazy-plugins/diffview.nvim/.gitignore
vendored
Normal file
3
config/neovim/store/lazy-plugins/diffview.nvim/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/doc/tags
|
||||
/.tests/
|
||||
/.dev/
|
||||
@ -0,0 +1,4 @@
|
||||
-- Global objects defined by C code.
|
||||
read_globals = {
|
||||
"vim"
|
||||
}
|
||||
15
config/neovim/store/lazy-plugins/diffview.nvim/.luarc.json
Normal file
15
config/neovim/store/lazy-plugins/diffview.nvim/.luarc.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json",
|
||||
"Lua.diagnostics.disable": [
|
||||
"assign-type-mismatch",
|
||||
"cast-type-mismatch",
|
||||
"missing-fields"
|
||||
],
|
||||
"runtime.version": "LuaJIT",
|
||||
"workspace.checkThirdParty": false,
|
||||
"workspace.library": [
|
||||
"./lua",
|
||||
"$VIMRUNTIME/lua",
|
||||
".dev/lua/nvim"
|
||||
]
|
||||
}
|
||||
16
config/neovim/store/lazy-plugins/diffview.nvim/LICENSE
Normal file
16
config/neovim/store/lazy-plugins/diffview.nvim/LICENSE
Normal file
@ -0,0 +1,16 @@
|
||||
Diffview.nvim is a tool for Neovim that adds an interface that enables
|
||||
easy review of changes made in any Git revision.
|
||||
Copyright (C) 2021 Sindre T. Strøm
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
33
config/neovim/store/lazy-plugins/diffview.nvim/Makefile
Normal file
33
config/neovim/store/lazy-plugins/diffview.nvim/Makefile
Normal file
@ -0,0 +1,33 @@
|
||||
.PHONY: all
|
||||
all: dev test
|
||||
|
||||
TEST_PATH := $(if $(TEST_PATH),$(TEST_PATH),lua/diffview/tests/)
|
||||
export TEST_PATH
|
||||
|
||||
# Usage:
|
||||
# Run all tests:
|
||||
# $ make test
|
||||
#
|
||||
# Run tests for a specific path:
|
||||
# $ TEST_PATH=tests/some/path make test
|
||||
.PHONY: test
|
||||
test:
|
||||
nvim --headless -i NONE -n -u scripts/test_init.lua -c \
|
||||
"PlenaryBustedDirectory $(TEST_PATH) { minimal_init = './scripts/test_init.lua' }"
|
||||
|
||||
.PHONY: dev
|
||||
dev: .dev/lua/nvim
|
||||
|
||||
.dev/lua/nvim:
|
||||
mkdir -p "$@"
|
||||
git clone --filter=blob:none https://github.com/folke/neodev.nvim.git "$@/repo"
|
||||
cd "$@/repo" && git -c advice.detachedHead=false checkout ce9a2e8eaba5649b553529c5498acb43a6c317cd
|
||||
cp "$@/repo/types/nightly/uv.lua" \
|
||||
"$@/repo/types/nightly/cmd.lua" \
|
||||
"$@/repo/types/nightly/alias.lua" \
|
||||
"$@/"
|
||||
rm -rf "$@/repo"
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf .tests .dev
|
||||
524
config/neovim/store/lazy-plugins/diffview.nvim/README.md
Normal file
524
config/neovim/store/lazy-plugins/diffview.nvim/README.md
Normal file
@ -0,0 +1,524 @@
|
||||
# Diffview.nvim
|
||||
|
||||
Single tabpage interface for easily cycling through diffs for all modified files
|
||||
for any git rev.
|
||||
|
||||

|
||||
|
||||
|
||||
## Introduction
|
||||
|
||||
Vim's diff mode is pretty good, but there is no convenient way to quickly bring
|
||||
up all modified files in a diffsplit. This plugin aims to provide a simple,
|
||||
unified, single tabpage interface that lets you easily review all changed files
|
||||
for any git rev.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Git ≥ 2.31.0 (for Git support)
|
||||
- Mercurial ≥ 5.4.0 (for Mercurial support)
|
||||
- Neovim ≥ 0.7.0 (with LuaJIT)
|
||||
- [nvim-web-devicons](https://github.com/nvim-tree/nvim-web-devicons) (optional) For file icons
|
||||
|
||||
## Installation
|
||||
|
||||
Install the plugin with your package manager of choice.
|
||||
|
||||
```vim
|
||||
" Plug
|
||||
Plug 'sindrets/diffview.nvim'
|
||||
```
|
||||
|
||||
```lua
|
||||
-- Packer
|
||||
use "sindrets/diffview.nvim"
|
||||
```
|
||||
|
||||
## Merge Tool
|
||||
|
||||

|
||||
|
||||
Opening a diff view during a merge or a rebase will list the conflicted files in
|
||||
their own section. When opening a conflicted file, it will open in a 3-way diff
|
||||
allowing you to resolve the merge conflicts with the context of the target
|
||||
branch's version, as well as the version from the branch which is being merged.
|
||||
|
||||
The 3-way diff is only the default layout for merge conflicts. There are
|
||||
multiple variations on this layout, a 4-way diff layout, and a single window
|
||||
layout available.
|
||||
|
||||
In addition to the normal `:h copy-diffs` mappings, there are default mappings
|
||||
provided for jumping between conflict markers, obtaining a hunk directly from
|
||||
any of the diff buffers, and accepting any one, all, or none of the versions of
|
||||
a file given by a conflict region.
|
||||
|
||||
For more information on the merge tool, mappings, layouts and how to
|
||||
configure them, see:
|
||||
|
||||
- `:h diffview-merge-tool`
|
||||
- `:h diffview-config-view.x.layout`
|
||||
|
||||
## File History
|
||||
|
||||

|
||||
|
||||
The file history view allows you to list all the commits that affected a given
|
||||
set of paths, and view the changes made in a diff split. This is a porcelain
|
||||
interface for git-log, and supports a good number of its options. Things like:
|
||||
|
||||
- Filtering commits by grepping commit messages and commit authors.
|
||||
- Tracing the line evolution of a given set of line ranges for multiple files.
|
||||
- Only listing changes for a specific commit range, branch, or tag.
|
||||
- Following file changes through renames.
|
||||
|
||||
Get started by opening file history for:
|
||||
|
||||
- The current branch: `:DiffviewFileHistory`
|
||||
- The current file: `:DiffviewFileHistory %`
|
||||
|
||||
For more info, see `:h :DiffviewFileHistory`.
|
||||
|
||||
## Usage
|
||||
|
||||
### `:DiffviewOpen [git rev] [options] [ -- {paths...}]`
|
||||
|
||||
Calling `:DiffviewOpen` with no args opens a new Diffview that compares against
|
||||
the current index. You can also provide any valid git rev to view only changes
|
||||
for that rev.
|
||||
|
||||
Examples:
|
||||
|
||||
- `:DiffviewOpen`
|
||||
- `:DiffviewOpen HEAD~2`
|
||||
- `:DiffviewOpen HEAD~4..HEAD~2`
|
||||
- `:DiffviewOpen d4a7b0d`
|
||||
- `:DiffviewOpen d4a7b0d^!`
|
||||
- `:DiffviewOpen d4a7b0d..519b30e`
|
||||
- `:DiffviewOpen origin/main...HEAD`
|
||||
|
||||
You can also provide additional paths to narrow down what files are shown:
|
||||
|
||||
- `:DiffviewOpen HEAD~2 -- lua/diffview plugin`
|
||||
|
||||
For information about additional `[options]`, visit the
|
||||
[documentation](https://github.com/sindrets/diffview.nvim/blob/main/doc/diffview.txt).
|
||||
|
||||
Additional commands for convenience:
|
||||
|
||||
- `:DiffviewClose`: Close the current diffview. You can also use `:tabclose`.
|
||||
- `:DiffviewToggleFiles`: Toggle the file panel.
|
||||
- `:DiffviewFocusFiles`: Bring focus to the file panel.
|
||||
- `:DiffviewRefresh`: Update stats and entries in the file list of the current
|
||||
Diffview.
|
||||
|
||||
With a Diffview open and the default key bindings, you can cycle through changed
|
||||
files with `<tab>` and `<s-tab>` (see configuration to change the key bindings).
|
||||
|
||||
#### Staging
|
||||
|
||||
You can stage individual hunks by editing any buffer that represents the index
|
||||
(after running `:DiffviewOpen` with no `[git-rev]` the entries under "Changes"
|
||||
will have the index buffer on the left side, and the entries under "Staged
|
||||
changes" will have it on the right side). Once you write to an index buffer the
|
||||
index will be updated.
|
||||
|
||||
### `:[range]DiffviewFileHistory [paths] [options]`
|
||||
|
||||
Opens a new file history view that lists all commits that affected the given
|
||||
paths. This is a porcelain interface for git-log. Both `[paths]` and
|
||||
`[options]` may be specified in any order, even interchangeably.
|
||||
|
||||
If no `[paths]` are given, defaults to the top-level of the working tree. The
|
||||
top-level will be inferred from the current buffer when possible, otherwise the
|
||||
cwd is used. Multiple `[paths]` may be provided and git pathspec is supported.
|
||||
|
||||
If `[range]` is given, the file history view will trace the line evolution of the
|
||||
given range in the current file (for more info, see the `-L` flag in the docs).
|
||||
|
||||
Examples:
|
||||
|
||||
- `:DiffviewFileHistory`
|
||||
- `:DiffviewFileHistory %`
|
||||
- `:DiffviewFileHistory path/to/some/file.txt`
|
||||
- `:DiffviewFileHistory path/to/some/directory`
|
||||
- `:DiffviewFileHistory include/this and/this :!but/not/this`
|
||||
- `:DiffviewFileHistory --range=origin..HEAD`
|
||||
- `:DiffviewFileHistory --range=feat/example-branch`
|
||||
- `:'<,'>DiffviewFileHistory`
|
||||
|
||||
> [!IMPORTANT]
|
||||
> ### Familiarize Yourself With `:h diff-mode`
|
||||
>
|
||||
> This plugin assumes you're familiar with all the features already provided by
|
||||
> nvim's builtin diff-mode. These features include:
|
||||
>
|
||||
> - Jumping between hunks (`:h jumpto-diffs`).
|
||||
> - Applying the changes of a diff hunk from any of the diffed buffers
|
||||
> (`:h copy-diffs`).
|
||||
> - And more...
|
||||
>
|
||||
> Read the help page for more info.
|
||||
|
||||
---
|
||||
|
||||
<br>
|
||||
|
||||
> [!NOTE]
|
||||
> Additionally check out [USAGE](USAGE.md) for examples of some more specific
|
||||
> use-cases.
|
||||
|
||||
<br>
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
<p>
|
||||
<details>
|
||||
<summary style='cursor: pointer'><b>Example config with default values</b></summary>
|
||||
|
||||
```lua
|
||||
-- Lua
|
||||
local actions = require("diffview.actions")
|
||||
|
||||
require("diffview").setup({
|
||||
diff_binaries = false, -- Show diffs for binaries
|
||||
enhanced_diff_hl = false, -- See |diffview-config-enhanced_diff_hl|
|
||||
git_cmd = { "git" }, -- The git executable followed by default args.
|
||||
hg_cmd = { "hg" }, -- The hg executable followed by default args.
|
||||
use_icons = true, -- Requires nvim-web-devicons
|
||||
show_help_hints = true, -- Show hints for how to open the help panel
|
||||
watch_index = true, -- Update views and index buffers when the git index changes.
|
||||
icons = { -- Only applies when use_icons is true.
|
||||
folder_closed = "",
|
||||
folder_open = "",
|
||||
},
|
||||
signs = {
|
||||
fold_closed = "",
|
||||
fold_open = "",
|
||||
done = "✓",
|
||||
},
|
||||
view = {
|
||||
-- Configure the layout and behavior of different types of views.
|
||||
-- Available layouts:
|
||||
-- 'diff1_plain'
|
||||
-- |'diff2_horizontal'
|
||||
-- |'diff2_vertical'
|
||||
-- |'diff3_horizontal'
|
||||
-- |'diff3_vertical'
|
||||
-- |'diff3_mixed'
|
||||
-- |'diff4_mixed'
|
||||
-- For more info, see |diffview-config-view.x.layout|.
|
||||
default = {
|
||||
-- Config for changed files, and staged files in diff views.
|
||||
layout = "diff2_horizontal",
|
||||
disable_diagnostics = false, -- Temporarily disable diagnostics for diff buffers while in the view.
|
||||
winbar_info = false, -- See |diffview-config-view.x.winbar_info|
|
||||
},
|
||||
merge_tool = {
|
||||
-- Config for conflicted files in diff views during a merge or rebase.
|
||||
layout = "diff3_horizontal",
|
||||
disable_diagnostics = true, -- Temporarily disable diagnostics for diff buffers while in the view.
|
||||
winbar_info = true, -- See |diffview-config-view.x.winbar_info|
|
||||
},
|
||||
file_history = {
|
||||
-- Config for changed files in file history views.
|
||||
layout = "diff2_horizontal",
|
||||
disable_diagnostics = false, -- Temporarily disable diagnostics for diff buffers while in the view.
|
||||
winbar_info = false, -- See |diffview-config-view.x.winbar_info|
|
||||
},
|
||||
},
|
||||
file_panel = {
|
||||
listing_style = "tree", -- One of 'list' or 'tree'
|
||||
tree_options = { -- Only applies when listing_style is 'tree'
|
||||
flatten_dirs = true, -- Flatten dirs that only contain one single dir
|
||||
folder_statuses = "only_folded", -- One of 'never', 'only_folded' or 'always'.
|
||||
},
|
||||
win_config = { -- See |diffview-config-win_config|
|
||||
position = "left",
|
||||
width = 35,
|
||||
win_opts = {},
|
||||
},
|
||||
},
|
||||
file_history_panel = {
|
||||
log_options = { -- See |diffview-config-log_options|
|
||||
git = {
|
||||
single_file = {
|
||||
diff_merges = "combined",
|
||||
},
|
||||
multi_file = {
|
||||
diff_merges = "first-parent",
|
||||
},
|
||||
},
|
||||
hg = {
|
||||
single_file = {},
|
||||
multi_file = {},
|
||||
},
|
||||
},
|
||||
win_config = { -- See |diffview-config-win_config|
|
||||
position = "bottom",
|
||||
height = 16,
|
||||
win_opts = {},
|
||||
},
|
||||
},
|
||||
commit_log_panel = {
|
||||
win_config = {}, -- See |diffview-config-win_config|
|
||||
},
|
||||
default_args = { -- Default args prepended to the arg-list for the listed commands
|
||||
DiffviewOpen = {},
|
||||
DiffviewFileHistory = {},
|
||||
},
|
||||
hooks = {}, -- See |diffview-config-hooks|
|
||||
keymaps = {
|
||||
disable_defaults = false, -- Disable the default keymaps
|
||||
view = {
|
||||
-- The `view` bindings are active in the diff buffers, only when the current
|
||||
-- tabpage is a Diffview.
|
||||
{ "n", "<tab>", actions.select_next_entry, { desc = "Open the diff for the next file" } },
|
||||
{ "n", "<s-tab>", actions.select_prev_entry, { desc = "Open the diff for the previous file" } },
|
||||
{ "n", "gf", actions.goto_file_edit, { desc = "Open the file in the previous tabpage" } },
|
||||
{ "n", "<C-w><C-f>", actions.goto_file_split, { desc = "Open the file in a new split" } },
|
||||
{ "n", "<C-w>gf", actions.goto_file_tab, { desc = "Open the file in a new tabpage" } },
|
||||
{ "n", "<leader>e", actions.focus_files, { desc = "Bring focus to the file panel" } },
|
||||
{ "n", "<leader>b", actions.toggle_files, { desc = "Toggle the file panel." } },
|
||||
{ "n", "g<C-x>", actions.cycle_layout, { desc = "Cycle through available layouts." } },
|
||||
{ "n", "[x", actions.prev_conflict, { desc = "In the merge-tool: jump to the previous conflict" } },
|
||||
{ "n", "]x", actions.next_conflict, { desc = "In the merge-tool: jump to the next conflict" } },
|
||||
{ "n", "<leader>co", actions.conflict_choose("ours"), { desc = "Choose the OURS version of a conflict" } },
|
||||
{ "n", "<leader>ct", actions.conflict_choose("theirs"), { desc = "Choose the THEIRS version of a conflict" } },
|
||||
{ "n", "<leader>cb", actions.conflict_choose("base"), { desc = "Choose the BASE version of a conflict" } },
|
||||
{ "n", "<leader>ca", actions.conflict_choose("all"), { desc = "Choose all the versions of a conflict" } },
|
||||
{ "n", "dx", actions.conflict_choose("none"), { desc = "Delete the conflict region" } },
|
||||
{ "n", "<leader>cO", actions.conflict_choose_all("ours"), { desc = "Choose the OURS version of a conflict for the whole file" } },
|
||||
{ "n", "<leader>cT", actions.conflict_choose_all("theirs"), { desc = "Choose the THEIRS version of a conflict for the whole file" } },
|
||||
{ "n", "<leader>cB", actions.conflict_choose_all("base"), { desc = "Choose the BASE version of a conflict for the whole file" } },
|
||||
{ "n", "<leader>cA", actions.conflict_choose_all("all"), { desc = "Choose all the versions of a conflict for the whole file" } },
|
||||
{ "n", "dX", actions.conflict_choose_all("none"), { desc = "Delete the conflict region for the whole file" } },
|
||||
},
|
||||
diff1 = {
|
||||
-- Mappings in single window diff layouts
|
||||
{ "n", "g?", actions.help({ "view", "diff1" }), { desc = "Open the help panel" } },
|
||||
},
|
||||
diff2 = {
|
||||
-- Mappings in 2-way diff layouts
|
||||
{ "n", "g?", actions.help({ "view", "diff2" }), { desc = "Open the help panel" } },
|
||||
},
|
||||
diff3 = {
|
||||
-- Mappings in 3-way diff layouts
|
||||
{ { "n", "x" }, "2do", actions.diffget("ours"), { desc = "Obtain the diff hunk from the OURS version of the file" } },
|
||||
{ { "n", "x" }, "3do", actions.diffget("theirs"), { desc = "Obtain the diff hunk from the THEIRS version of the file" } },
|
||||
{ "n", "g?", actions.help({ "view", "diff3" }), { desc = "Open the help panel" } },
|
||||
},
|
||||
diff4 = {
|
||||
-- Mappings in 4-way diff layouts
|
||||
{ { "n", "x" }, "1do", actions.diffget("base"), { desc = "Obtain the diff hunk from the BASE version of the file" } },
|
||||
{ { "n", "x" }, "2do", actions.diffget("ours"), { desc = "Obtain the diff hunk from the OURS version of the file" } },
|
||||
{ { "n", "x" }, "3do", actions.diffget("theirs"), { desc = "Obtain the diff hunk from the THEIRS version of the file" } },
|
||||
{ "n", "g?", actions.help({ "view", "diff4" }), { desc = "Open the help panel" } },
|
||||
},
|
||||
file_panel = {
|
||||
{ "n", "j", actions.next_entry, { desc = "Bring the cursor to the next file entry" } },
|
||||
{ "n", "<down>", actions.next_entry, { desc = "Bring the cursor to the next file entry" } },
|
||||
{ "n", "k", actions.prev_entry, { desc = "Bring the cursor to the previous file entry" } },
|
||||
{ "n", "<up>", actions.prev_entry, { desc = "Bring the cursor to the previous file entry" } },
|
||||
{ "n", "<cr>", actions.select_entry, { desc = "Open the diff for the selected entry" } },
|
||||
{ "n", "o", actions.select_entry, { desc = "Open the diff for the selected entry" } },
|
||||
{ "n", "l", actions.select_entry, { desc = "Open the diff for the selected entry" } },
|
||||
{ "n", "<2-LeftMouse>", actions.select_entry, { desc = "Open the diff for the selected entry" } },
|
||||
{ "n", "-", actions.toggle_stage_entry, { desc = "Stage / unstage the selected entry" } },
|
||||
{ "n", "s", actions.toggle_stage_entry, { desc = "Stage / unstage the selected entry" } },
|
||||
{ "n", "S", actions.stage_all, { desc = "Stage all entries" } },
|
||||
{ "n", "U", actions.unstage_all, { desc = "Unstage all entries" } },
|
||||
{ "n", "X", actions.restore_entry, { desc = "Restore entry to the state on the left side" } },
|
||||
{ "n", "L", actions.open_commit_log, { desc = "Open the commit log panel" } },
|
||||
{ "n", "zo", actions.open_fold, { desc = "Expand fold" } },
|
||||
{ "n", "h", actions.close_fold, { desc = "Collapse fold" } },
|
||||
{ "n", "zc", actions.close_fold, { desc = "Collapse fold" } },
|
||||
{ "n", "za", actions.toggle_fold, { desc = "Toggle fold" } },
|
||||
{ "n", "zR", actions.open_all_folds, { desc = "Expand all folds" } },
|
||||
{ "n", "zM", actions.close_all_folds, { desc = "Collapse all folds" } },
|
||||
{ "n", "<c-b>", actions.scroll_view(-0.25), { desc = "Scroll the view up" } },
|
||||
{ "n", "<c-f>", actions.scroll_view(0.25), { desc = "Scroll the view down" } },
|
||||
{ "n", "<tab>", actions.select_next_entry, { desc = "Open the diff for the next file" } },
|
||||
{ "n", "<s-tab>", actions.select_prev_entry, { desc = "Open the diff for the previous file" } },
|
||||
{ "n", "gf", actions.goto_file_edit, { desc = "Open the file in the previous tabpage" } },
|
||||
{ "n", "<C-w><C-f>", actions.goto_file_split, { desc = "Open the file in a new split" } },
|
||||
{ "n", "<C-w>gf", actions.goto_file_tab, { desc = "Open the file in a new tabpage" } },
|
||||
{ "n", "i", actions.listing_style, { desc = "Toggle between 'list' and 'tree' views" } },
|
||||
{ "n", "f", actions.toggle_flatten_dirs, { desc = "Flatten empty subdirectories in tree listing style" } },
|
||||
{ "n", "R", actions.refresh_files, { desc = "Update stats and entries in the file list" } },
|
||||
{ "n", "<leader>e", actions.focus_files, { desc = "Bring focus to the file panel" } },
|
||||
{ "n", "<leader>b", actions.toggle_files, { desc = "Toggle the file panel" } },
|
||||
{ "n", "g<C-x>", actions.cycle_layout, { desc = "Cycle available layouts" } },
|
||||
{ "n", "[x", actions.prev_conflict, { desc = "Go to the previous conflict" } },
|
||||
{ "n", "]x", actions.next_conflict, { desc = "Go to the next conflict" } },
|
||||
{ "n", "g?", actions.help("file_panel"), { desc = "Open the help panel" } },
|
||||
{ "n", "<leader>cO", actions.conflict_choose_all("ours"), { desc = "Choose the OURS version of a conflict for the whole file" } },
|
||||
{ "n", "<leader>cT", actions.conflict_choose_all("theirs"), { desc = "Choose the THEIRS version of a conflict for the whole file" } },
|
||||
{ "n", "<leader>cB", actions.conflict_choose_all("base"), { desc = "Choose the BASE version of a conflict for the whole file" } },
|
||||
{ "n", "<leader>cA", actions.conflict_choose_all("all"), { desc = "Choose all the versions of a conflict for the whole file" } },
|
||||
{ "n", "dX", actions.conflict_choose_all("none"), { desc = "Delete the conflict region for the whole file" } },
|
||||
},
|
||||
file_history_panel = {
|
||||
{ "n", "g!", actions.options, { desc = "Open the option panel" } },
|
||||
{ "n", "<C-A-d>", actions.open_in_diffview, { desc = "Open the entry under the cursor in a diffview" } },
|
||||
{ "n", "y", actions.copy_hash, { desc = "Copy the commit hash of the entry under the cursor" } },
|
||||
{ "n", "L", actions.open_commit_log, { desc = "Show commit details" } },
|
||||
{ "n", "X", actions.restore_entry, { desc = "Restore file to the state from the selected entry" } },
|
||||
{ "n", "zr", actions.open_fold, { desc = "Expand fold" } },
|
||||
{ "n", "zo", actions.open_fold, { desc = "Expand fold" } },
|
||||
{ "n", "zm", actions.close_fold, { desc = "Collapse fold" } },
|
||||
{ "n", "zc", actions.close_fold, { desc = "Collapse fold" } },
|
||||
{ "n", "h", actions.close_fold, { desc = "Collapse fold" } },
|
||||
{ "n", "za", actions.toggle_fold, { desc = "Toggle fold" } },
|
||||
{ "n", "zR", actions.open_all_folds, { desc = "Expand all folds" } },
|
||||
{ "n", "zM", actions.close_all_folds, { desc = "Collapse all folds" } },
|
||||
{ "n", "j", actions.next_entry, { desc = "Bring the cursor to the next file entry" } },
|
||||
{ "n", "<down>", actions.next_entry, { desc = "Bring the cursor to the next file entry" } },
|
||||
{ "n", "k", actions.prev_entry, { desc = "Bring the cursor to the previous file entry" } },
|
||||
{ "n", "<up>", actions.prev_entry, { desc = "Bring the cursor to the previous file entry" } },
|
||||
{ "n", "<cr>", actions.select_entry, { desc = "Open the diff for the selected entry" } },
|
||||
{ "n", "o", actions.select_entry, { desc = "Open the diff for the selected entry" } },
|
||||
{ "n", "l", actions.select_entry, { desc = "Open the diff for the selected entry" } },
|
||||
{ "n", "<2-LeftMouse>", actions.select_entry, { desc = "Open the diff for the selected entry" } },
|
||||
{ "n", "<c-b>", actions.scroll_view(-0.25), { desc = "Scroll the view up" } },
|
||||
{ "n", "<c-f>", actions.scroll_view(0.25), { desc = "Scroll the view down" } },
|
||||
{ "n", "<tab>", actions.select_next_entry, { desc = "Open the diff for the next file" } },
|
||||
{ "n", "<s-tab>", actions.select_prev_entry, { desc = "Open the diff for the previous file" } },
|
||||
{ "n", "gf", actions.goto_file_edit, { desc = "Open the file in the previous tabpage" } },
|
||||
{ "n", "<C-w><C-f>", actions.goto_file_split, { desc = "Open the file in a new split" } },
|
||||
{ "n", "<C-w>gf", actions.goto_file_tab, { desc = "Open the file in a new tabpage" } },
|
||||
{ "n", "<leader>e", actions.focus_files, { desc = "Bring focus to the file panel" } },
|
||||
{ "n", "<leader>b", actions.toggle_files, { desc = "Toggle the file panel" } },
|
||||
{ "n", "g<C-x>", actions.cycle_layout, { desc = "Cycle available layouts" } },
|
||||
{ "n", "g?", actions.help("file_history_panel"), { desc = "Open the help panel" } },
|
||||
},
|
||||
option_panel = {
|
||||
{ "n", "<tab>", actions.select_entry, { desc = "Change the current option" } },
|
||||
{ "n", "q", actions.close, { desc = "Close the panel" } },
|
||||
{ "n", "g?", actions.help("option_panel"), { desc = "Open the help panel" } },
|
||||
},
|
||||
help_panel = {
|
||||
{ "n", "q", actions.close, { desc = "Close help menu" } },
|
||||
{ "n", "<esc>", actions.close, { desc = "Close help menu" } },
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
</details>
|
||||
</p>
|
||||
|
||||
### Hooks
|
||||
|
||||
The `hooks` table allows you to define callbacks for various events emitted from
|
||||
Diffview. The available hooks are documented in detail in
|
||||
`:h diffview-config-hooks`. The hook events are also available as User
|
||||
autocommands. See `:h diffview-user-autocmds` for more details.
|
||||
|
||||
Examples:
|
||||
|
||||
```lua
|
||||
hooks = {
|
||||
diff_buf_read = function(bufnr)
|
||||
-- Change local options in diff buffers
|
||||
vim.opt_local.wrap = false
|
||||
vim.opt_local.list = false
|
||||
vim.opt_local.colorcolumn = { 80 }
|
||||
end,
|
||||
view_opened = function(view)
|
||||
print(
|
||||
("A new %s was opened on tab page %d!")
|
||||
:format(view.class:name(), view.tabpage)
|
||||
)
|
||||
end,
|
||||
}
|
||||
```
|
||||
|
||||
### Keymaps
|
||||
|
||||
The keymaps config is structured as a table with sub-tables for various
|
||||
different contexts where mappings can be declared. In these sub-tables
|
||||
key-value pairs are treated as the `{lhs}` and `{rhs}` of a normal mode
|
||||
mapping. These mappings all use the `:map-arguments` `silent`, `nowait`, and
|
||||
`noremap`. The implementation uses `vim.keymap.set()`, so the `{rhs}` can be
|
||||
either a vim command in the form of a string, or it can be a lua function:
|
||||
|
||||
```lua
|
||||
view = {
|
||||
-- Vim command:
|
||||
["a"] = "<Cmd>echom 'foo'<CR>",
|
||||
-- Lua function:
|
||||
["b"] = function() print("bar") end,
|
||||
}
|
||||
```
|
||||
|
||||
For more control (i.e. mappings for other modes), you can also define index
|
||||
values as list-like tables containing the arguments for `vim.keymap.set()`.
|
||||
This way you can also change all the `:map-arguments` with the only exception
|
||||
being the `buffer` field, as this will be overridden with the target buffer
|
||||
number:
|
||||
|
||||
```lua
|
||||
view = {
|
||||
-- Normal and visual mode mapping to vim command:
|
||||
{ { "n", "v" }, "<leader>a", "<Cmd>echom 'foo'<CR>", { silent = true } },
|
||||
-- Visual mode mapping to lua function:
|
||||
{ "v", "<leader>b", function() print("bar") end, { nowait = true } },
|
||||
}
|
||||
```
|
||||
|
||||
To disable any single mapping without disabling them all, set its `{rhs}` to
|
||||
`false`:
|
||||
|
||||
```lua
|
||||
view = {
|
||||
-- Disable the default normal mode mapping for `<tab>`:
|
||||
["<tab>"] = false,
|
||||
-- Disable the default visual mode mapping for `gf`:
|
||||
{ "x", "gf", false },
|
||||
}
|
||||
```
|
||||
|
||||
Most of the mapped file panel actions also work from the view if they are added
|
||||
to the view maps (and vice versa). The exception is for actions that only
|
||||
really make sense specifically in the file panel, such as `next_entry`,
|
||||
`prev_entry`. Actions such as `toggle_stage_entry` and `restore_entry` work
|
||||
just fine from the view. When invoked from the view, these will target the file
|
||||
currently open in the view rather than the file under the cursor in the file
|
||||
panel.
|
||||
|
||||
**For more details on how to set mappings for other modes, actions, and more see:**
|
||||
- `:h diffview-config-keymaps`
|
||||
- `:h diffview-actions`
|
||||
|
||||
## Restoring Files
|
||||
|
||||
If the right side of the diff is showing the local state of a file, you can
|
||||
restore the file to the state from the left side of the diff (key binding `X`
|
||||
from the file panel by default). The current state of the file is stored in the
|
||||
git object database, and a command is echoed that shows how to undo the change.
|
||||
|
||||
## Tips and FAQ
|
||||
|
||||
- **Hide untracked files:**
|
||||
- `DiffviewOpen -uno`
|
||||
- **Exclude certain paths:**
|
||||
- `DiffviewOpen -- :!exclude/this :!and/this`
|
||||
- **Run as if git was started in a specific directory:**
|
||||
- `DiffviewOpen -C/foo/bar/baz`
|
||||
- **Diff the index against a git rev:**
|
||||
- `DiffviewOpen HEAD~2 --cached`
|
||||
- Defaults to `HEAD` if no rev is given.
|
||||
- **Q: How do I get the diagonal lines in place of deleted lines in
|
||||
diff-mode?**
|
||||
- A: Change your `:h 'fillchars'`:
|
||||
- (vimscript): `set fillchars+=diff:╱`
|
||||
- (Lua): `vim.opt.fillchars:append { diff = "╱" }`
|
||||
- Note: whether or not the diagonal lines will line up nicely will depend on
|
||||
your terminal emulator. The terminal used in the screenshots is Kitty.
|
||||
- **Q: How do I jump between hunks in the diff?**
|
||||
- A: Use `[c` and `]c`
|
||||
- `:h jumpto-diffs`
|
||||
|
||||
<!-- vim: set tw=80 -->
|
||||
287
config/neovim/store/lazy-plugins/diffview.nvim/USAGE.md
Normal file
287
config/neovim/store/lazy-plugins/diffview.nvim/USAGE.md
Normal file
@ -0,0 +1,287 @@
|
||||
# Usage
|
||||
|
||||
This document contains a few guides for solving common problems, and describes
|
||||
some more specific use-cases.
|
||||
|
||||
## Review a PR
|
||||
|
||||
### Comparing All the Changes
|
||||
|
||||
First, checkout the branch locally. If it's a GitHub PR, you can use
|
||||
[`gh`](https://github.com/cli/cli) to do this:
|
||||
|
||||
```console
|
||||
$ gh pr checkout {PR_ID}
|
||||
```
|
||||
|
||||
Now, run a symmetric diff against the base branch:
|
||||
|
||||
```vim
|
||||
:DiffviewOpen origin/HEAD...HEAD --imply-local
|
||||
```
|
||||
|
||||
The symmetric difference rev range (triple dot) will here compare the changes on
|
||||
the current branch (the PR branch) against its merge-base in `origin/HEAD`. This
|
||||
is different from comparing directly against `origin/HEAD` if the branches have
|
||||
diverged, and is usually what you want when comparing changes in a PR. For more
|
||||
info see the section on "SPECIFYING REVISIONS" in `man git-rev-parse(1)`.
|
||||
|
||||
The `--imply-local` flag option will here make diffview.nvim show the working
|
||||
tree versions[^1] of the changed files on the right side of the diff. This means
|
||||
that if you have tools such as LSP set up, it will work for all the diff buffers
|
||||
on the right side, giving you access to LSP features - such as diagnostics and
|
||||
references - that can be useful while reviewing changes.
|
||||
|
||||
If you want the plugin to always use this option, you can add it to your default
|
||||
args:
|
||||
|
||||
```lua
|
||||
default_args = {
|
||||
DiffviewOpen = { "--imply-local" },
|
||||
}
|
||||
```
|
||||
|
||||
From the file panel you can press `L` to open the commit log for all the
|
||||
changes. This lets you check the full commit messages for all the commits
|
||||
involved.
|
||||
|
||||

|
||||
|
||||
> NOTE: If Git complains `fatal: Not a valid object name origin/HEAD` then it
|
||||
> likely means that your local origin remote doesn't have a default branch
|
||||
> configured. You can have Git automatically query your remote and detect its
|
||||
> default branch like this:
|
||||
>
|
||||
> ```sh
|
||||
> git remote set-head -a origin
|
||||
> ```
|
||||
|
||||
[^1]: The files as they currently exist on disk.
|
||||
|
||||
### Comparing Changes From the Individual PR Commits
|
||||
|
||||
If you're reviewing a big PR composed of many commits, you might prefer to
|
||||
review the changes introduced in each of those commits individually. To do
|
||||
this, you can use `:DiffviewFileHistory`:
|
||||
|
||||
```vim
|
||||
:DiffviewFileHistory --range=origin/HEAD...HEAD --right-only --no-merges
|
||||
```
|
||||
|
||||
Here we are again using a symmetric difference range. However, symdiff ranges
|
||||
have different behavior between `git-diff` and `git-log`. Whereas in `git-diff`
|
||||
it compares against the merge-base, here it will select only the commits that
|
||||
are reachable from _either_ `origin/HEAD` _or_ `HEAD`, but not from both (in
|
||||
other words, it's actually performing a symmetric difference here).
|
||||
|
||||
We then use the cherry-pick option `--right-only` to limit the commits to only
|
||||
those on the right side of the symmetric difference. Finally `--no-merges`
|
||||
filters out merge commits. We are left with a list of all the non-merge commits
|
||||
from the PR branch.
|
||||
|
||||

|
||||
|
||||
## Inspecting Diffs for Stashes
|
||||
|
||||
The latest Git stash is always stored in the reference `refs/stash`. We can
|
||||
find all the stashes by traversing the reflog for this reference. This can be
|
||||
achieved with the flag option `--walk-reflogs` (or it's short form `-g`). The
|
||||
following command will list all stashes in the file history panel:
|
||||
|
||||
```vim
|
||||
:DiffviewFileHistory -g --range=stash
|
||||
```
|
||||
|
||||
## Committing
|
||||
|
||||
Creating commits from within nvim is a solved problem, and as such diffview.nvim
|
||||
does not reinvent the wheel here. Here are a few different ways in which you can
|
||||
create a new commit from within the editor:
|
||||
|
||||
### Use a Git Wrapper Plugin (Recommended)
|
||||
|
||||
Diffview.nvim _is not_, and _does not try to be_ a complete git wrapper. As
|
||||
such, there are a number of features offered by such plugins that won't ever be
|
||||
implemented here, because they are deemed out-of-scope. It's therefore
|
||||
recommended to use some form of a Git wrapper plugin in order to get a more
|
||||
complete integration of Git's features into your editor. Here are a few options:
|
||||
|
||||
- [`vim-fugitive`](https://github.com/tpope/vim-fugitive)
|
||||
- [`neogit`](https://github.com/TimUntersberger/neogit)
|
||||
- [`lazygit.nvim`](https://github.com/kdheepak/lazygit.nvim)
|
||||
|
||||
Example creating some `vim-fugitive` mappings for committing staged changes
|
||||
from the file panel:
|
||||
|
||||
```lua
|
||||
keymaps = {
|
||||
file_panel = {
|
||||
{
|
||||
"n", "cc",
|
||||
"<Cmd>Git commit <bar> wincmd J<CR>",
|
||||
{ desc = "Commit staged changes" },
|
||||
},
|
||||
{
|
||||
"n", "ca",
|
||||
"<Cmd>Git commit --amend <bar> wincmd J<CR>",
|
||||
{ desc = "Amend the last commit" },
|
||||
},
|
||||
{
|
||||
"n", "c<space>",
|
||||
":Git commit ",
|
||||
{ desc = "Populate command line with \":Git commit \"" },
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Use [`neovim-remote`](https://github.com/mhinz/neovim-remote)
|
||||
|
||||
`neovim-remote` is a more complete version of the builtin `:h clientserver`.
|
||||
Notably it implements all the `:h clientserver-missing` functionality. Hopefully
|
||||
this functionality will be implemented in core at some point in the future. But
|
||||
until then this separate application is needed to get the `--{...}-wait`
|
||||
variants of the remote options.
|
||||
|
||||
With the remote installed you can simply configure your `$GIT_EDITOR`
|
||||
environment variable from within the editor such that when the Git editor is
|
||||
invoked, it will open in a new split inside the current editor session. This
|
||||
avoids the problem of spawning nested nvim instances every time an `$EDITOR` is
|
||||
invoked. Put this somewhere in your `init.lua`:
|
||||
|
||||
```lua
|
||||
if vim.fn.executable("nvr") == 1 then
|
||||
local nvr = "nvr --servername " .. vim.v.servername .. " "
|
||||
vim.env.GIT_EDITOR = nvr .. "-cc split +'setl bh=delete' --remote-wait"
|
||||
vim.env.EDITOR = nvr .. "-l --remote" -- (Optional)
|
||||
vim.env.VISUAL = nvr .. "-l --remote" -- (Optional)
|
||||
end
|
||||
```
|
||||
|
||||
Example creating some mappings for committing staged changes from the file
|
||||
panel, that will trigger `nvr`:
|
||||
|
||||
```lua
|
||||
keymaps = {
|
||||
file_panel = {
|
||||
{
|
||||
"n", "cc",
|
||||
[[<Cmd>call jobstart(["git", "commit"]) | au BufWinEnter * ++once wincmd J<CR>]],
|
||||
{ desc = "Commit staged changes" },
|
||||
},
|
||||
{
|
||||
"n", "ca",
|
||||
[[<Cmd>call jobstart(["git", "commit", "--amend"]) | au BufWinEnter * ++once wincmd J<CR>]],
|
||||
{ desc = "Amend the last commit" },
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Use `:terminal`
|
||||
|
||||
The `:h :terminal` command allows you to run interactive terminal jobs. However,
|
||||
unlike the [previously discussed](#use-neovim-remote) `neovim-remote` solution
|
||||
this will spawn nested instances of nvim.
|
||||
|
||||
Example creating some `:terminal` mappings for committing staged changes from
|
||||
the file panel:
|
||||
|
||||
```lua
|
||||
keymaps = {
|
||||
file_panel = {
|
||||
{
|
||||
"n", "cc",
|
||||
"<Cmd>sp <bar> wincmd J <bar> term git commit<CR>",
|
||||
{ desc = "Commit staged changes" },
|
||||
},
|
||||
{
|
||||
"n", "ca",
|
||||
"<Cmd>sp <bar> wincmd J <bar> term git commit -amend<CR>",
|
||||
{ desc = "Amend the last commit" },
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Use `vim.ui.input()`
|
||||
|
||||
This example shows how to use the Neovim builtin `vim.ui.input()` to create a
|
||||
simple input prompt, and create a new commit with the user's given message.
|
||||
|
||||

|
||||
|
||||
For Neovim ≥ v0.10:
|
||||
|
||||
```lua
|
||||
keymaps = {
|
||||
file_panel = {
|
||||
{
|
||||
"n", "cc",
|
||||
function()
|
||||
vim.ui.input({ prompt = "Commit message: " }, function(msg)
|
||||
if not msg then return end
|
||||
local results = vim.system({ "git", "commit", "-m", msg }, { text = true }):wait()
|
||||
|
||||
if results.code ~= 0 then
|
||||
vim.notify(
|
||||
"Commit failed with the message: \n"
|
||||
.. vim.trim(results.stdout .. "\n" .. results.stderr),
|
||||
vim.log.levels.ERROR,
|
||||
{ title = "Commit" }
|
||||
)
|
||||
else
|
||||
vim.notify(results.stdout, vim.log.levels.INFO, { title = "Commit" })
|
||||
end
|
||||
end)
|
||||
end,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary><b>For Neovim ≤ v0.10, use this function in place of <code>vim.system()</code></b></summary>
|
||||
|
||||
```lua
|
||||
--- @class system.Results
|
||||
--- @field code integer
|
||||
--- @field stdout string
|
||||
--- @field stderr string
|
||||
|
||||
--- @param cmd string|string[]
|
||||
--- @return system.Results
|
||||
local function system(cmd)
|
||||
local results = {}
|
||||
|
||||
local function callback(_, data, event)
|
||||
if event == "exit" then results.code = data
|
||||
elseif event == "stdout" or event == "stderr" then
|
||||
results[event] = table.concat(data, "\n")
|
||||
end
|
||||
end
|
||||
|
||||
vim.fn.jobwait({
|
||||
vim.fn.jobstart(cmd, {
|
||||
on_exit = callback,
|
||||
on_stdout = callback,
|
||||
on_stderr = callback,
|
||||
stdout_buffered = true,
|
||||
stderr_buffered = true,
|
||||
})
|
||||
})
|
||||
|
||||
return results
|
||||
end
|
||||
```
|
||||
</details>
|
||||
|
||||
### Use `:!cmd`
|
||||
|
||||
If you only ever write simple commit messages you could make use of `:h !cmd`:
|
||||
|
||||
```vim
|
||||
:!git commit -m 'some commit message'
|
||||
```
|
||||
|
||||
<!-- vim: set tw=80 -->
|
||||
1662
config/neovim/store/lazy-plugins/diffview.nvim/doc/diffview.txt
Normal file
1662
config/neovim/store/lazy-plugins/diffview.nvim/doc/diffview.txt
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,217 @@
|
||||
================================================================================
|
||||
*diffview.changelog*
|
||||
|
||||
CHANGELOG
|
||||
|
||||
NOTE: This changelog only encompasses breaking changes.
|
||||
|
||||
*diffview.changelog-271*
|
||||
|
||||
PR: https://github.com/sindrets/diffview.nvim/pull/271
|
||||
|
||||
The config for log options has changed. In preparation of adding support of
|
||||
other VCS, the table is now divided into sub-tables per VCS type. This allows
|
||||
you to define different default log options for different VCS tools. To update
|
||||
your config, just move all your current log options into the new table key
|
||||
`git`:
|
||||
|
||||
Before: ~
|
||||
>
|
||||
require("diffview").setup({
|
||||
-- ...
|
||||
file_history_panel = {
|
||||
log_options = {
|
||||
single_file = {
|
||||
max_count = 512,
|
||||
follow = true,
|
||||
},
|
||||
multi_file = {
|
||||
max_count = 128,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
<
|
||||
|
||||
After: ~
|
||||
>
|
||||
require("diffview").setup({
|
||||
-- ...
|
||||
file_history_panel = {
|
||||
log_options = {
|
||||
git = {
|
||||
single_file = {
|
||||
max_count = 512,
|
||||
follow = true,
|
||||
},
|
||||
multi_file = {
|
||||
max_count = 128,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
<
|
||||
*diffview.changelog-190*
|
||||
|
||||
PR: https://github.com/sindrets/diffview.nvim/pull/190
|
||||
|
||||
This PR involves a major refactor of the layout system. The changes are made
|
||||
in preparation of the planned merge-tool, which is going to involve 3-way
|
||||
diffs, and possibly also 4-way diffs. Different entries in the same view may
|
||||
now use completely different window layouts. Thus the action `view_windo` has
|
||||
changed to reflect these changes. See |diffview-actions-view_windo| for more
|
||||
details on the new usage.
|
||||
|
||||
*diffview.changelog-169*
|
||||
|
||||
PR: https://github.com/sindrets/diffview.nvim/pull/169
|
||||
|
||||
The file history option panel is now able to accept multiple values separated
|
||||
by whitespace. This means that if you want to specify values with whitespace,
|
||||
you need to quote the value, or escape the whitespace with a backslash (`\`).
|
||||
|
||||
*diffview.changelog-151*
|
||||
|
||||
PR: https://github.com/sindrets/diffview.nvim/pull/151
|
||||
|
||||
The config for log options has changed. The table is now divided into the
|
||||
sub-tables `single_file`, and `multi_file`. This allows you to define
|
||||
different default log options for history targeting singular files, and
|
||||
history targeting multiple paths, and/or directories. To update your config,
|
||||
just move all your log options into the new table keys `single_file` and
|
||||
`multi_file`:
|
||||
|
||||
Before: ~
|
||||
>
|
||||
require("diffview").setup({
|
||||
-- ...
|
||||
file_history_panel = {
|
||||
log_options = {
|
||||
max_count = 512,
|
||||
follow = true,
|
||||
},
|
||||
},
|
||||
})
|
||||
<
|
||||
|
||||
After: ~
|
||||
>
|
||||
require("diffview").setup({
|
||||
-- ...
|
||||
file_history_panel = {
|
||||
log_options = {
|
||||
single_file = {
|
||||
max_count = 512,
|
||||
follow = true,
|
||||
},
|
||||
multi_file = {
|
||||
max_count = 128,
|
||||
-- follow = false -- `follow` only applies to single-file history
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
<
|
||||
|
||||
You only need to define the options you want to change from the defaults. To
|
||||
find all the available log options, see |diffview.git.LogOptions|.
|
||||
|
||||
Calling `:DiffviewFileHistory` with no args would previously target the file
|
||||
in the current buffer. This has now been changed to instead target the
|
||||
top-level of the working tree. This was changed because with how it worked
|
||||
before, there was effectively no way to get the file history equivalent of
|
||||
running `git log` with no path args. If your cwd was some subdirectory of the
|
||||
working tree, and you wanted the full file history of the tree, you would have
|
||||
to manually type out the path to the top-level. On the contrary, getting the
|
||||
history for the current file is always as simple as just using `%`, which
|
||||
expands to the current file name.
|
||||
|
||||
To get the file history for the current file like before, simply run: >
|
||||
|
||||
:DiffviewFileHistory %
|
||||
<
|
||||
|
||||
*diffview.changelog-137*
|
||||
|
||||
PR: https://github.com/sindrets/diffview.nvim/pull/137
|
||||
|
||||
The minimum required version has been bumped to Neovim 0.7.0, as the plugin
|
||||
now uses some of the API functions provided in this release.
|
||||
|
||||
*diffview.changelog-136*
|
||||
|
||||
PR: https://github.com/sindrets/diffview.nvim/pull/136
|
||||
|
||||
This PR refactors the internal representation of a panel (the various
|
||||
interactive windows used in the plugin). The way panels are configured has
|
||||
been changed and extended in a manner that is incompatible with the way it was
|
||||
done before. To update your config, just move all the window related options
|
||||
into a new table key `win_config`:
|
||||
|
||||
Before: ~
|
||||
>
|
||||
require("diffview").setup({
|
||||
-- ...
|
||||
file_panel = {
|
||||
position = "left",
|
||||
width = 35,
|
||||
height = 16,
|
||||
-- (Other options...)
|
||||
},
|
||||
})
|
||||
<
|
||||
After: ~
|
||||
>
|
||||
require("diffview").setup({
|
||||
-- ...
|
||||
file_panel = {
|
||||
win_config = {
|
||||
position = "left",
|
||||
width = 35,
|
||||
height = 16,
|
||||
},
|
||||
-- (Other options...)
|
||||
},
|
||||
})
|
||||
<
|
||||
This goes for both the `file_panel` and the `file_history_panel` config. To
|
||||
see all the available options for `win_config`, see
|
||||
|diffview-config-win_config|.
|
||||
|
||||
*diffview.changelog-93*
|
||||
|
||||
PR: https://github.com/sindrets/diffview.nvim/pull/93
|
||||
|
||||
The plugin will from here on out require `plenary.nvim`:
|
||||
https://github.com/nvim-lua/plenary.nvim
|
||||
|
||||
I'm using plenary for it's async utilities as well as job management. To
|
||||
update, just make sure plenary is loaded before diffview. Examples:
|
||||
|
||||
Packer:~
|
||||
`use { 'sindrets/diffview.nvim', requires = 'nvim-lua/plenary.nvim' }`
|
||||
|
||||
Plug:~
|
||||
`Plug 'nvim-lua/plenary.nvim'`
|
||||
`Plug 'sindrets/diffview.nvim'`
|
||||
|
||||
*diffview.changelog-64*
|
||||
|
||||
PR: https://github.com/sindrets/diffview.nvim/pull/64
|
||||
|
||||
This PR introduces some small breaking changes in the config, and for plugins
|
||||
integrating diffview.nvim.
|
||||
|
||||
The `use_icons` config table key has been moved out of the `file_panel` table.
|
||||
This has been done because `use_icons` now applies to other contexts than just
|
||||
the file panel. The correct way to configure this now is to set `use_icons`
|
||||
somewhere from the top level of the config table.
|
||||
|
||||
For plugins integrating diffview.nvim:
|
||||
Several of the git utilities have been refactored into their own namespace
|
||||
(`lua/diffview/git/`). I (STS) felt this was necessary due to the growing
|
||||
scope of the plugin. Most notably this means that the `Rev` class now resides
|
||||
in `lua/diffview/git/rev.lua`.
|
||||
|
||||
vim:tw=78:ts=8:ft=help:norl:
|
||||
@ -0,0 +1,230 @@
|
||||
DEFAULT CONFIG *diffview.defaults*
|
||||
|
||||
>lua
|
||||
local actions = require("diffview.actions")
|
||||
require("diffview").setup({
|
||||
diff_binaries = false, -- Show diffs for binaries
|
||||
enhanced_diff_hl = false, -- See |diffview-config-enhanced_diff_hl|
|
||||
git_cmd = { "git" }, -- The git executable followed by default args.
|
||||
hg_cmd = { "hg" }, -- The hg executable followed by default args.
|
||||
use_icons = true, -- Requires nvim-web-devicons
|
||||
show_help_hints = true, -- Show hints for how to open the help panel
|
||||
watch_index = true, -- Update views and index buffers when the git index changes.
|
||||
icons = { -- Only applies when use_icons is true.
|
||||
folder_closed = "",
|
||||
folder_open = "",
|
||||
},
|
||||
signs = {
|
||||
fold_closed = "",
|
||||
fold_open = "",
|
||||
done = "✓",
|
||||
},
|
||||
view = {
|
||||
-- Configure the layout and behavior of different types of views.
|
||||
-- Available layouts:
|
||||
-- 'diff1_plain'
|
||||
-- |'diff2_horizontal'
|
||||
-- |'diff2_vertical'
|
||||
-- |'diff3_horizontal'
|
||||
-- |'diff3_vertical'
|
||||
-- |'diff3_mixed'
|
||||
-- |'diff4_mixed'
|
||||
-- For more info, see |diffview-config-view.x.layout|.
|
||||
default = {
|
||||
-- Config for changed files, and staged files in diff views.
|
||||
layout = "diff2_horizontal",
|
||||
disable_diagnostics = false, -- Temporarily disable diagnostics for diff buffers while in the view.
|
||||
winbar_info = false, -- See |diffview-config-view.x.winbar_info|
|
||||
},
|
||||
merge_tool = {
|
||||
-- Config for conflicted files in diff views during a merge or rebase.
|
||||
layout = "diff3_horizontal",
|
||||
disable_diagnostics = true, -- Temporarily disable diagnostics for diff buffers while in the view.
|
||||
winbar_info = true, -- See |diffview-config-view.x.winbar_info|
|
||||
},
|
||||
file_history = {
|
||||
-- Config for changed files in file history views.
|
||||
layout = "diff2_horizontal",
|
||||
disable_diagnostics = false, -- Temporarily disable diagnostics for diff buffers while in the view.
|
||||
winbar_info = false, -- See |diffview-config-view.x.winbar_info|
|
||||
},
|
||||
},
|
||||
file_panel = {
|
||||
listing_style = "tree", -- One of 'list' or 'tree'
|
||||
tree_options = { -- Only applies when listing_style is 'tree'
|
||||
flatten_dirs = true, -- Flatten dirs that only contain one single dir
|
||||
folder_statuses = "only_folded", -- One of 'never', 'only_folded' or 'always'.
|
||||
},
|
||||
win_config = { -- See |diffview-config-win_config|
|
||||
position = "left",
|
||||
width = 35,
|
||||
win_opts = {},
|
||||
},
|
||||
},
|
||||
file_history_panel = {
|
||||
log_options = { -- See |diffview-config-log_options|
|
||||
git = {
|
||||
single_file = {
|
||||
diff_merges = "combined",
|
||||
},
|
||||
multi_file = {
|
||||
diff_merges = "first-parent",
|
||||
},
|
||||
},
|
||||
hg = {
|
||||
single_file = {},
|
||||
multi_file = {},
|
||||
},
|
||||
},
|
||||
win_config = { -- See |diffview-config-win_config|
|
||||
position = "bottom",
|
||||
height = 16,
|
||||
win_opts = {},
|
||||
},
|
||||
},
|
||||
commit_log_panel = {
|
||||
win_config = {}, -- See |diffview-config-win_config|
|
||||
},
|
||||
default_args = { -- Default args prepended to the arg-list for the listed commands
|
||||
DiffviewOpen = {},
|
||||
DiffviewFileHistory = {},
|
||||
},
|
||||
hooks = {}, -- See |diffview-config-hooks|
|
||||
keymaps = {
|
||||
disable_defaults = false, -- Disable the default keymaps
|
||||
view = {
|
||||
-- The `view` bindings are active in the diff buffers, only when the current
|
||||
-- tabpage is a Diffview.
|
||||
{ "n", "<tab>", actions.select_next_entry, { desc = "Open the diff for the next file" } },
|
||||
{ "n", "<s-tab>", actions.select_prev_entry, { desc = "Open the diff for the previous file" } },
|
||||
{ "n", "gf", actions.goto_file_edit, { desc = "Open the file in the previous tabpage" } },
|
||||
{ "n", "<C-w><C-f>", actions.goto_file_split, { desc = "Open the file in a new split" } },
|
||||
{ "n", "<C-w>gf", actions.goto_file_tab, { desc = "Open the file in a new tabpage" } },
|
||||
{ "n", "<leader>e", actions.focus_files, { desc = "Bring focus to the file panel" } },
|
||||
{ "n", "<leader>b", actions.toggle_files, { desc = "Toggle the file panel." } },
|
||||
{ "n", "g<C-x>", actions.cycle_layout, { desc = "Cycle through available layouts." } },
|
||||
{ "n", "[x", actions.prev_conflict, { desc = "In the merge-tool: jump to the previous conflict" } },
|
||||
{ "n", "]x", actions.next_conflict, { desc = "In the merge-tool: jump to the next conflict" } },
|
||||
{ "n", "<leader>co", actions.conflict_choose("ours"), { desc = "Choose the OURS version of a conflict" } },
|
||||
{ "n", "<leader>ct", actions.conflict_choose("theirs"), { desc = "Choose the THEIRS version of a conflict" } },
|
||||
{ "n", "<leader>cb", actions.conflict_choose("base"), { desc = "Choose the BASE version of a conflict" } },
|
||||
{ "n", "<leader>ca", actions.conflict_choose("all"), { desc = "Choose all the versions of a conflict" } },
|
||||
{ "n", "dx", actions.conflict_choose("none"), { desc = "Delete the conflict region" } },
|
||||
{ "n", "<leader>cO", actions.conflict_choose_all("ours"), { desc = "Choose the OURS version of a conflict for the whole file" } },
|
||||
{ "n", "<leader>cT", actions.conflict_choose_all("theirs"), { desc = "Choose the THEIRS version of a conflict for the whole file" } },
|
||||
{ "n", "<leader>cB", actions.conflict_choose_all("base"), { desc = "Choose the BASE version of a conflict for the whole file" } },
|
||||
{ "n", "<leader>cA", actions.conflict_choose_all("all"), { desc = "Choose all the versions of a conflict for the whole file" } },
|
||||
{ "n", "dX", actions.conflict_choose_all("none"), { desc = "Delete the conflict region for the whole file" } },
|
||||
},
|
||||
diff1 = {
|
||||
-- Mappings in single window diff layouts
|
||||
{ "n", "g?", actions.help({ "view", "diff1" }), { desc = "Open the help panel" } },
|
||||
},
|
||||
diff2 = {
|
||||
-- Mappings in 2-way diff layouts
|
||||
{ "n", "g?", actions.help({ "view", "diff2" }), { desc = "Open the help panel" } },
|
||||
},
|
||||
diff3 = {
|
||||
-- Mappings in 3-way diff layouts
|
||||
{ { "n", "x" }, "2do", actions.diffget("ours"), { desc = "Obtain the diff hunk from the OURS version of the file" } },
|
||||
{ { "n", "x" }, "3do", actions.diffget("theirs"), { desc = "Obtain the diff hunk from the THEIRS version of the file" } },
|
||||
{ "n", "g?", actions.help({ "view", "diff3" }), { desc = "Open the help panel" } },
|
||||
},
|
||||
diff4 = {
|
||||
-- Mappings in 4-way diff layouts
|
||||
{ { "n", "x" }, "1do", actions.diffget("base"), { desc = "Obtain the diff hunk from the BASE version of the file" } },
|
||||
{ { "n", "x" }, "2do", actions.diffget("ours"), { desc = "Obtain the diff hunk from the OURS version of the file" } },
|
||||
{ { "n", "x" }, "3do", actions.diffget("theirs"), { desc = "Obtain the diff hunk from the THEIRS version of the file" } },
|
||||
{ "n", "g?", actions.help({ "view", "diff4" }), { desc = "Open the help panel" } },
|
||||
},
|
||||
file_panel = {
|
||||
{ "n", "j", actions.next_entry, { desc = "Bring the cursor to the next file entry" } },
|
||||
{ "n", "<down>", actions.next_entry, { desc = "Bring the cursor to the next file entry" } },
|
||||
{ "n", "k", actions.prev_entry, { desc = "Bring the cursor to the previous file entry" } },
|
||||
{ "n", "<up>", actions.prev_entry, { desc = "Bring the cursor to the previous file entry" } },
|
||||
{ "n", "<cr>", actions.select_entry, { desc = "Open the diff for the selected entry" } },
|
||||
{ "n", "o", actions.select_entry, { desc = "Open the diff for the selected entry" } },
|
||||
{ "n", "l", actions.select_entry, { desc = "Open the diff for the selected entry" } },
|
||||
{ "n", "<2-LeftMouse>", actions.select_entry, { desc = "Open the diff for the selected entry" } },
|
||||
{ "n", "-", actions.toggle_stage_entry, { desc = "Stage / unstage the selected entry" } },
|
||||
{ "n", "s", actions.toggle_stage_entry, { desc = "Stage / unstage the selected entry" } },
|
||||
{ "n", "S", actions.stage_all, { desc = "Stage all entries" } },
|
||||
{ "n", "U", actions.unstage_all, { desc = "Unstage all entries" } },
|
||||
{ "n", "X", actions.restore_entry, { desc = "Restore entry to the state on the left side" } },
|
||||
{ "n", "L", actions.open_commit_log, { desc = "Open the commit log panel" } },
|
||||
{ "n", "zo", actions.open_fold, { desc = "Expand fold" } },
|
||||
{ "n", "h", actions.close_fold, { desc = "Collapse fold" } },
|
||||
{ "n", "zc", actions.close_fold, { desc = "Collapse fold" } },
|
||||
{ "n", "za", actions.toggle_fold, { desc = "Toggle fold" } },
|
||||
{ "n", "zR", actions.open_all_folds, { desc = "Expand all folds" } },
|
||||
{ "n", "zM", actions.close_all_folds, { desc = "Collapse all folds" } },
|
||||
{ "n", "<c-b>", actions.scroll_view(-0.25), { desc = "Scroll the view up" } },
|
||||
{ "n", "<c-f>", actions.scroll_view(0.25), { desc = "Scroll the view down" } },
|
||||
{ "n", "<tab>", actions.select_next_entry, { desc = "Open the diff for the next file" } },
|
||||
{ "n", "<s-tab>", actions.select_prev_entry, { desc = "Open the diff for the previous file" } },
|
||||
{ "n", "gf", actions.goto_file_edit, { desc = "Open the file in the previous tabpage" } },
|
||||
{ "n", "<C-w><C-f>", actions.goto_file_split, { desc = "Open the file in a new split" } },
|
||||
{ "n", "<C-w>gf", actions.goto_file_tab, { desc = "Open the file in a new tabpage" } },
|
||||
{ "n", "i", actions.listing_style, { desc = "Toggle between 'list' and 'tree' views" } },
|
||||
{ "n", "f", actions.toggle_flatten_dirs, { desc = "Flatten empty subdirectories in tree listing style" } },
|
||||
{ "n", "R", actions.refresh_files, { desc = "Update stats and entries in the file list" } },
|
||||
{ "n", "<leader>e", actions.focus_files, { desc = "Bring focus to the file panel" } },
|
||||
{ "n", "<leader>b", actions.toggle_files, { desc = "Toggle the file panel" } },
|
||||
{ "n", "g<C-x>", actions.cycle_layout, { desc = "Cycle available layouts" } },
|
||||
{ "n", "[x", actions.prev_conflict, { desc = "Go to the previous conflict" } },
|
||||
{ "n", "]x", actions.next_conflict, { desc = "Go to the next conflict" } },
|
||||
{ "n", "g?", actions.help("file_panel"), { desc = "Open the help panel" } },
|
||||
{ "n", "<leader>cO", actions.conflict_choose_all("ours"), { desc = "Choose the OURS version of a conflict for the whole file" } },
|
||||
{ "n", "<leader>cT", actions.conflict_choose_all("theirs"), { desc = "Choose the THEIRS version of a conflict for the whole file" } },
|
||||
{ "n", "<leader>cB", actions.conflict_choose_all("base"), { desc = "Choose the BASE version of a conflict for the whole file" } },
|
||||
{ "n", "<leader>cA", actions.conflict_choose_all("all"), { desc = "Choose all the versions of a conflict for the whole file" } },
|
||||
{ "n", "dX", actions.conflict_choose_all("none"), { desc = "Delete the conflict region for the whole file" } },
|
||||
},
|
||||
file_history_panel = {
|
||||
{ "n", "g!", actions.options, { desc = "Open the option panel" } },
|
||||
{ "n", "<C-A-d>", actions.open_in_diffview, { desc = "Open the entry under the cursor in a diffview" } },
|
||||
{ "n", "y", actions.copy_hash, { desc = "Copy the commit hash of the entry under the cursor" } },
|
||||
{ "n", "L", actions.open_commit_log, { desc = "Show commit details" } },
|
||||
{ "n", "X", actions.restore_entry, { desc = "Restore file to the state from the selected entry" } },
|
||||
{ "n", "zr", actions.open_fold, { desc = "Expand fold" } },
|
||||
{ "n", "zo", actions.open_fold, { desc = "Expand fold" } },
|
||||
{ "n", "zm", actions.close_fold, { desc = "Collapse fold" } },
|
||||
{ "n", "zc", actions.close_fold, { desc = "Collapse fold" } },
|
||||
{ "n", "h", actions.close_fold, { desc = "Collapse fold" } },
|
||||
{ "n", "za", actions.toggle_fold, { desc = "Toggle fold" } },
|
||||
{ "n", "zR", actions.open_all_folds, { desc = "Expand all folds" } },
|
||||
{ "n", "zM", actions.close_all_folds, { desc = "Collapse all folds" } },
|
||||
{ "n", "j", actions.next_entry, { desc = "Bring the cursor to the next file entry" } },
|
||||
{ "n", "<down>", actions.next_entry, { desc = "Bring the cursor to the next file entry" } },
|
||||
{ "n", "k", actions.prev_entry, { desc = "Bring the cursor to the previous file entry" } },
|
||||
{ "n", "<up>", actions.prev_entry, { desc = "Bring the cursor to the previous file entry" } },
|
||||
{ "n", "<cr>", actions.select_entry, { desc = "Open the diff for the selected entry" } },
|
||||
{ "n", "o", actions.select_entry, { desc = "Open the diff for the selected entry" } },
|
||||
{ "n", "l", actions.select_entry, { desc = "Open the diff for the selected entry" } },
|
||||
{ "n", "<2-LeftMouse>", actions.select_entry, { desc = "Open the diff for the selected entry" } },
|
||||
{ "n", "<c-b>", actions.scroll_view(-0.25), { desc = "Scroll the view up" } },
|
||||
{ "n", "<c-f>", actions.scroll_view(0.25), { desc = "Scroll the view down" } },
|
||||
{ "n", "<tab>", actions.select_next_entry, { desc = "Open the diff for the next file" } },
|
||||
{ "n", "<s-tab>", actions.select_prev_entry, { desc = "Open the diff for the previous file" } },
|
||||
{ "n", "gf", actions.goto_file_edit, { desc = "Open the file in the previous tabpage" } },
|
||||
{ "n", "<C-w><C-f>", actions.goto_file_split, { desc = "Open the file in a new split" } },
|
||||
{ "n", "<C-w>gf", actions.goto_file_tab, { desc = "Open the file in a new tabpage" } },
|
||||
{ "n", "<leader>e", actions.focus_files, { desc = "Bring focus to the file panel" } },
|
||||
{ "n", "<leader>b", actions.toggle_files, { desc = "Toggle the file panel" } },
|
||||
{ "n", "g<C-x>", actions.cycle_layout, { desc = "Cycle available layouts" } },
|
||||
{ "n", "g?", actions.help("file_history_panel"), { desc = "Open the help panel" } },
|
||||
},
|
||||
option_panel = {
|
||||
{ "n", "<tab>", actions.select_entry, { desc = "Change the current option" } },
|
||||
{ "n", "q", actions.close, { desc = "Close the panel" } },
|
||||
{ "n", "g?", actions.help("option_panel"), { desc = "Open the help panel" } },
|
||||
},
|
||||
help_panel = {
|
||||
{ "n", "q", actions.close, { desc = "Close help menu" } },
|
||||
{ "n", "<esc>", actions.close, { desc = "Close help menu" } },
|
||||
},
|
||||
},
|
||||
})
|
||||
<
|
||||
|
||||
vim:ft=help:norl:
|
||||
114
config/neovim/store/lazy-plugins/diffview.nvim/doc/tags
Normal file
114
config/neovim/store/lazy-plugins/diffview.nvim/doc/tags
Normal file
@ -0,0 +1,114 @@
|
||||
:DiffviewClose diffview.txt /*:DiffviewClose*
|
||||
:DiffviewFileHistory diffview.txt /*:DiffviewFileHistory*
|
||||
:DiffviewFocusFiles diffview.txt /*:DiffviewFocusFiles*
|
||||
:DiffviewLog diffview.txt /*:DiffviewLog*
|
||||
:DiffviewOpen diffview.txt /*:DiffviewOpen*
|
||||
:DiffviewRefresh diffview.txt /*:DiffviewRefresh*
|
||||
:DiffviewToggleFiles diffview.txt /*:DiffviewToggleFiles*
|
||||
diffview diffview.txt /*diffview*
|
||||
diffview-actions diffview.txt /*diffview-actions*
|
||||
diffview-actions-close diffview.txt /*diffview-actions-close*
|
||||
diffview-actions-close_all_folds diffview.txt /*diffview-actions-close_all_folds*
|
||||
diffview-actions-close_fold diffview.txt /*diffview-actions-close_fold*
|
||||
diffview-actions-conflict_choose diffview.txt /*diffview-actions-conflict_choose*
|
||||
diffview-actions-conflict_choose_all diffview.txt /*diffview-actions-conflict_choose_all*
|
||||
diffview-actions-copy_hash diffview.txt /*diffview-actions-copy_hash*
|
||||
diffview-actions-diffget diffview.txt /*diffview-actions-diffget*
|
||||
diffview-actions-focus_entry diffview.txt /*diffview-actions-focus_entry*
|
||||
diffview-actions-focus_files diffview.txt /*diffview-actions-focus_files*
|
||||
diffview-actions-goto_file diffview.txt /*diffview-actions-goto_file*
|
||||
diffview-actions-goto_file_edit diffview.txt /*diffview-actions-goto_file_edit*
|
||||
diffview-actions-goto_file_split diffview.txt /*diffview-actions-goto_file_split*
|
||||
diffview-actions-goto_file_tab diffview.txt /*diffview-actions-goto_file_tab*
|
||||
diffview-actions-jumpto_conflict diffview.txt /*diffview-actions-jumpto_conflict*
|
||||
diffview-actions-listing_style diffview.txt /*diffview-actions-listing_style*
|
||||
diffview-actions-next_conflict diffview.txt /*diffview-actions-next_conflict*
|
||||
diffview-actions-next_entry diffview.txt /*diffview-actions-next_entry*
|
||||
diffview-actions-open_all_folds diffview.txt /*diffview-actions-open_all_folds*
|
||||
diffview-actions-open_commit_log diffview.txt /*diffview-actions-open_commit_log*
|
||||
diffview-actions-open_fold diffview.txt /*diffview-actions-open_fold*
|
||||
diffview-actions-open_in_diffview diffview.txt /*diffview-actions-open_in_diffview*
|
||||
diffview-actions-options diffview.txt /*diffview-actions-options*
|
||||
diffview-actions-prev_conflict diffview.txt /*diffview-actions-prev_conflict*
|
||||
diffview-actions-prev_entry diffview.txt /*diffview-actions-prev_entry*
|
||||
diffview-actions-refresh_files diffview.txt /*diffview-actions-refresh_files*
|
||||
diffview-actions-restore_entry diffview.txt /*diffview-actions-restore_entry*
|
||||
diffview-actions-scroll_view diffview.txt /*diffview-actions-scroll_view*
|
||||
diffview-actions-select_entry diffview.txt /*diffview-actions-select_entry*
|
||||
diffview-actions-select_next_entry diffview.txt /*diffview-actions-select_next_entry*
|
||||
diffview-actions-select_prev_entry diffview.txt /*diffview-actions-select_prev_entry*
|
||||
diffview-actions-stage_all diffview.txt /*diffview-actions-stage_all*
|
||||
diffview-actions-toggle_files diffview.txt /*diffview-actions-toggle_files*
|
||||
diffview-actions-toggle_flatten_dirs diffview.txt /*diffview-actions-toggle_flatten_dirs*
|
||||
diffview-actions-toggle_fold diffview.txt /*diffview-actions-toggle_fold*
|
||||
diffview-actions-toggle_stage_entry diffview.txt /*diffview-actions-toggle_stage_entry*
|
||||
diffview-actions-unstage_all diffview.txt /*diffview-actions-unstage_all*
|
||||
diffview-actions-view_windo diffview.txt /*diffview-actions-view_windo*
|
||||
diffview-api diffview.txt /*diffview-api*
|
||||
diffview-available-actions diffview.txt /*diffview-available-actions*
|
||||
diffview-commands diffview.txt /*diffview-commands*
|
||||
diffview-config diffview.txt /*diffview-config*
|
||||
diffview-config-default_args diffview.txt /*diffview-config-default_args*
|
||||
diffview-config-enhanced_diff_hl diffview.txt /*diffview-config-enhanced_diff_hl*
|
||||
diffview-config-git_cmd diffview.txt /*diffview-config-git_cmd*
|
||||
diffview-config-hg_cmd diffview.txt /*diffview-config-hg_cmd*
|
||||
diffview-config-hooks diffview.txt /*diffview-config-hooks*
|
||||
diffview-config-keymaps diffview.txt /*diffview-config-keymaps*
|
||||
diffview-config-log_options diffview.txt /*diffview-config-log_options*
|
||||
diffview-config-view.x.disable_diagnostics diffview.txt /*diffview-config-view.x.disable_diagnostics*
|
||||
diffview-config-view.x.layout diffview.txt /*diffview-config-view.x.layout*
|
||||
diffview-config-view.x.winbar_info diffview.txt /*diffview-config-view.x.winbar_info*
|
||||
diffview-config-win_config diffview.txt /*diffview-config-win_config*
|
||||
diffview-conflict-example diffview.txt /*diffview-conflict-example*
|
||||
diffview-conflict-versions diffview.txt /*diffview-conflict-versions*
|
||||
diffview-file-inference diffview.txt /*diffview-file-inference*
|
||||
diffview-inspect-stash diffview.txt /*diffview-inspect-stash*
|
||||
diffview-layouts diffview.txt /*diffview-layouts*
|
||||
diffview-maps diffview.txt /*diffview-maps*
|
||||
diffview-maps-close_all_folds diffview.txt /*diffview-maps-close_all_folds*
|
||||
diffview-maps-copy_hash diffview.txt /*diffview-maps-copy_hash*
|
||||
diffview-maps-file-history-option-panel diffview.txt /*diffview-maps-file-history-option-panel*
|
||||
diffview-maps-file-history-panel diffview.txt /*diffview-maps-file-history-panel*
|
||||
diffview-maps-file-panel diffview.txt /*diffview-maps-file-panel*
|
||||
diffview-maps-focus_files diffview.txt /*diffview-maps-focus_files*
|
||||
diffview-maps-goto_file_edit diffview.txt /*diffview-maps-goto_file_edit*
|
||||
diffview-maps-goto_file_split diffview.txt /*diffview-maps-goto_file_split*
|
||||
diffview-maps-goto_file_tab diffview.txt /*diffview-maps-goto_file_tab*
|
||||
diffview-maps-next_entry diffview.txt /*diffview-maps-next_entry*
|
||||
diffview-maps-open_all_folds diffview.txt /*diffview-maps-open_all_folds*
|
||||
diffview-maps-open_in_diffview diffview.txt /*diffview-maps-open_in_diffview*
|
||||
diffview-maps-options diffview.txt /*diffview-maps-options*
|
||||
diffview-maps-prev_entry diffview.txt /*diffview-maps-prev_entry*
|
||||
diffview-maps-refresh_files diffview.txt /*diffview-maps-refresh_files*
|
||||
diffview-maps-restore_entry diffview.txt /*diffview-maps-restore_entry*
|
||||
diffview-maps-select_entry diffview.txt /*diffview-maps-select_entry*
|
||||
diffview-maps-select_next_entry diffview.txt /*diffview-maps-select_next_entry*
|
||||
diffview-maps-select_prev_entry diffview.txt /*diffview-maps-select_prev_entry*
|
||||
diffview-maps-stage_all diffview.txt /*diffview-maps-stage_all*
|
||||
diffview-maps-toggle_files diffview.txt /*diffview-maps-toggle_files*
|
||||
diffview-maps-toggle_stage_entry diffview.txt /*diffview-maps-toggle_stage_entry*
|
||||
diffview-maps-unstage_all diffview.txt /*diffview-maps-unstage_all*
|
||||
diffview-maps-view diffview.txt /*diffview-maps-view*
|
||||
diffview-merge-tool diffview.txt /*diffview-merge-tool*
|
||||
diffview-staging diffview.txt /*diffview-staging*
|
||||
diffview-unused-actions diffview.txt /*diffview-unused-actions*
|
||||
diffview-usage diffview.txt /*diffview-usage*
|
||||
diffview-user-autocmds diffview.txt /*diffview-user-autocmds*
|
||||
diffview.ConflictCount diffview.txt /*diffview.ConflictCount*
|
||||
diffview.api.views.diff.diff_view.CDiffView diffview.txt /*diffview.api.views.diff.diff_view.CDiffView*
|
||||
diffview.api.views.diff.diff_view.FileData diffview.txt /*diffview.api.views.diff.diff_view.FileData*
|
||||
diffview.changelog diffview_changelog.txt /*diffview.changelog*
|
||||
diffview.changelog-136 diffview_changelog.txt /*diffview.changelog-136*
|
||||
diffview.changelog-137 diffview_changelog.txt /*diffview.changelog-137*
|
||||
diffview.changelog-151 diffview_changelog.txt /*diffview.changelog-151*
|
||||
diffview.changelog-169 diffview_changelog.txt /*diffview.changelog-169*
|
||||
diffview.changelog-190 diffview_changelog.txt /*diffview.changelog-190*
|
||||
diffview.changelog-271 diffview_changelog.txt /*diffview.changelog-271*
|
||||
diffview.changelog-64 diffview_changelog.txt /*diffview.changelog-64*
|
||||
diffview.changelog-93 diffview_changelog.txt /*diffview.changelog-93*
|
||||
diffview.defaults diffview_defaults.txt /*diffview.defaults*
|
||||
diffview.git.FileDict diffview.txt /*diffview.git.FileDict*
|
||||
diffview.git.LogOptions diffview.txt /*diffview.git.LogOptions*
|
||||
diffview.nvim diffview.txt /*diffview.nvim*
|
||||
diffview.txt diffview.txt /*diffview.txt*
|
||||
diffview.views.file_entry.GitStats diffview.txt /*diffview.views.file_entry.GitStats*
|
||||
@ -0,0 +1,652 @@
|
||||
require("diffview.bootstrap")
|
||||
|
||||
local async = require("diffview.async")
|
||||
local lazy = require("diffview.lazy")
|
||||
|
||||
local DiffView = lazy.access("diffview.scene.views.diff.diff_view", "DiffView") ---@type DiffView|LazyModule
|
||||
local FileHistoryView = lazy.access("diffview.scene.views.file_history.file_history_view", "FileHistoryView") ---@type FileHistoryView|LazyModule
|
||||
local HelpPanel = lazy.access("diffview.ui.panels.help_panel", "HelpPanel") ---@type HelpPanel|LazyModule
|
||||
local StandardView = lazy.access("diffview.scene.views.standard.standard_view", "StandardView") ---@type StandardView|LazyModule
|
||||
local lib = lazy.require("diffview.lib") ---@module "diffview.lib"
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
local vcs_utils = lazy.require("diffview.vcs.utils") ---@module "diffview.vcs.utils"
|
||||
|
||||
local Diff1 = lazy.access("diffview.scene.layouts.diff_1", "Diff1") ---@type Diff1|LazyModule
|
||||
local Diff2Hor = lazy.access("diffview.scene.layouts.diff_2_hor", "Diff2Hor") ---@type Diff2Hor|LazyModule
|
||||
local Diff2Ver = lazy.access("diffview.scene.layouts.diff_2_ver", "Diff2Ver") ---@type Diff2Ver|LazyModule
|
||||
local Diff3 = lazy.access("diffview.scene.layouts.diff_3", "Diff3") ---@type Diff3|LazyModule
|
||||
local Diff3Hor = lazy.access("diffview.scene.layouts.diff_3_hor", "Diff3Hor") ---@type Diff3Hor|LazyModule
|
||||
local Diff3Ver = lazy.access("diffview.scene.layouts.diff_3_ver", "Diff3Ver") ---@type Diff3Hor|LazyModule
|
||||
local Diff3Mixed = lazy.access("diffview.scene.layouts.diff_3_mixed", "Diff3Mixed") ---@type Diff3Mixed|LazyModule
|
||||
local Diff4 = lazy.access("diffview.scene.layouts.diff_4", "Diff4") ---@type Diff4|LazyModule
|
||||
local Diff4Mixed = lazy.access("diffview.scene.layouts.diff_4_mixed", "Diff4Mixed") ---@type Diff4Mixed|LazyModule
|
||||
|
||||
local api = vim.api
|
||||
local await = async.await
|
||||
local pl = lazy.access(utils, "path") ---@type PathLib
|
||||
|
||||
local M = setmetatable({}, {
|
||||
__index = function(_, k)
|
||||
utils.err((
|
||||
"The action '%s' does not exist! "
|
||||
.. "See ':h diffview-available-actions' for an overview of available actions."
|
||||
):format(k))
|
||||
end
|
||||
})
|
||||
|
||||
M.compat = {}
|
||||
|
||||
---@return FileEntry?
|
||||
---@return integer[]? cursor
|
||||
local function prepare_goto_file()
|
||||
local view = lib.get_current_view()
|
||||
|
||||
if view and not (view:instanceof(DiffView.__get()) or view:instanceof(FileHistoryView.__get())) then
|
||||
return
|
||||
end
|
||||
|
||||
---@cast view DiffView|FileHistoryView
|
||||
|
||||
local file = view:infer_cur_file()
|
||||
if file then
|
||||
---@cast file FileEntry
|
||||
-- Ensure file exists
|
||||
if not pl:readable(file.absolute_path) then
|
||||
utils.err(
|
||||
string.format(
|
||||
"File does not exist on disk: '%s'",
|
||||
pl:relative(file.absolute_path, ".")
|
||||
)
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
local cursor
|
||||
local cur_file = view.cur_entry
|
||||
if file == cur_file then
|
||||
local win = view.cur_layout:get_main_win()
|
||||
cursor = api.nvim_win_get_cursor(win.id)
|
||||
end
|
||||
|
||||
return file, cursor
|
||||
end
|
||||
end
|
||||
|
||||
function M.goto_file()
|
||||
local file, cursor = prepare_goto_file()
|
||||
|
||||
if file then
|
||||
local target_tab = lib.get_prev_non_view_tabpage()
|
||||
|
||||
if target_tab then
|
||||
api.nvim_set_current_tabpage(target_tab)
|
||||
file.layout:restore_winopts()
|
||||
vim.cmd("sp " .. vim.fn.fnameescape(file.absolute_path))
|
||||
else
|
||||
vim.cmd("tabnew")
|
||||
local temp_bufnr = api.nvim_get_current_buf()
|
||||
file.layout:restore_winopts()
|
||||
vim.cmd("keepalt edit " .. vim.fn.fnameescape(file.absolute_path))
|
||||
|
||||
if temp_bufnr ~= api.nvim_get_current_buf() then
|
||||
api.nvim_buf_delete(temp_bufnr, { force = true })
|
||||
end
|
||||
end
|
||||
|
||||
if cursor then
|
||||
utils.set_cursor(0, unpack(cursor))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.goto_file_edit()
|
||||
local file, cursor = prepare_goto_file()
|
||||
|
||||
if file then
|
||||
local target_tab = lib.get_prev_non_view_tabpage()
|
||||
|
||||
if target_tab then
|
||||
api.nvim_set_current_tabpage(target_tab)
|
||||
file.layout:restore_winopts()
|
||||
vim.cmd("edit " .. vim.fn.fnameescape(file.absolute_path))
|
||||
else
|
||||
vim.cmd("tabnew")
|
||||
local temp_bufnr = api.nvim_get_current_buf()
|
||||
file.layout:restore_winopts()
|
||||
vim.cmd("keepalt edit " .. vim.fn.fnameescape(file.absolute_path))
|
||||
|
||||
if temp_bufnr ~= api.nvim_get_current_buf() then
|
||||
api.nvim_buf_delete(temp_bufnr, { force = true })
|
||||
end
|
||||
end
|
||||
|
||||
if cursor then
|
||||
utils.set_cursor(0, unpack(cursor))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.goto_file_split()
|
||||
local file, cursor = prepare_goto_file()
|
||||
|
||||
if file then
|
||||
vim.cmd("new")
|
||||
local temp_bufnr = api.nvim_get_current_buf()
|
||||
file.layout:restore_winopts()
|
||||
vim.cmd("keepalt edit " .. vim.fn.fnameescape(file.absolute_path))
|
||||
|
||||
if temp_bufnr ~= api.nvim_get_current_buf() then
|
||||
api.nvim_buf_delete(temp_bufnr, { force = true })
|
||||
end
|
||||
|
||||
if cursor then
|
||||
utils.set_cursor(0, unpack(cursor))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.goto_file_tab()
|
||||
local file, cursor = prepare_goto_file()
|
||||
|
||||
if file then
|
||||
vim.cmd("tabnew")
|
||||
local temp_bufnr = api.nvim_get_current_buf()
|
||||
file.layout:restore_winopts()
|
||||
vim.cmd("keepalt edit " .. vim.fn.fnameescape(file.absolute_path))
|
||||
|
||||
if temp_bufnr ~= api.nvim_get_current_buf() then
|
||||
api.nvim_buf_delete(temp_bufnr, { force = true })
|
||||
end
|
||||
|
||||
if cursor then
|
||||
utils.set_cursor(0, unpack(cursor))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@class diffview.ConflictCount
|
||||
---@field total integer
|
||||
---@field current integer
|
||||
---@field cur_conflict? ConflictRegion
|
||||
---@field conflicts ConflictRegion[]
|
||||
|
||||
---@param num integer
|
||||
---@param use_delta? boolean
|
||||
---@return diffview.ConflictCount?
|
||||
function M.jumpto_conflict(num, use_delta)
|
||||
local view = lib.get_current_view()
|
||||
|
||||
if view and view:instanceof(StandardView.__get()) then
|
||||
---@cast view StandardView
|
||||
local main = view.cur_layout:get_main_win()
|
||||
local curfile = main.file
|
||||
|
||||
if main:is_valid() and curfile:is_valid() then
|
||||
local next_idx
|
||||
local conflicts, cur, cur_idx = vcs_utils.parse_conflicts(
|
||||
api.nvim_buf_get_lines(curfile.bufnr, 0, -1, false),
|
||||
main.id
|
||||
)
|
||||
|
||||
if #conflicts > 0 then
|
||||
if not use_delta then
|
||||
next_idx = utils.clamp(num, 1, #conflicts)
|
||||
else
|
||||
local delta = num
|
||||
|
||||
if not cur and delta < 0 and cur_idx <= #conflicts then
|
||||
delta = delta + 1
|
||||
end
|
||||
|
||||
if (delta < 0 and cur_idx < 1) or (delta > 0 and cur_idx > #conflicts) then
|
||||
cur_idx = utils.clamp(cur_idx, 1, #conflicts)
|
||||
end
|
||||
|
||||
next_idx = (cur_idx + delta - 1) % #conflicts + 1
|
||||
end
|
||||
|
||||
local next_conflict = conflicts[next_idx]
|
||||
local curwin = api.nvim_get_current_win()
|
||||
|
||||
api.nvim_win_call(main.id, function()
|
||||
api.nvim_win_set_cursor(main.id, { next_conflict.first, 0 })
|
||||
if curwin ~= main.id then view.cur_layout:sync_scroll() end
|
||||
end)
|
||||
|
||||
api.nvim_echo({{ ("Conflict [%d/%d]"):format(next_idx, #conflicts) }}, false, {})
|
||||
|
||||
return {
|
||||
total = #conflicts,
|
||||
current = next_idx,
|
||||
cur_conflict = next_conflict,
|
||||
conflicts = conflicts,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Jump to the next merge conflict marker.
|
||||
---@return diffview.ConflictCount?
|
||||
function M.next_conflict()
|
||||
return M.jumpto_conflict(1, true)
|
||||
end
|
||||
|
||||
---Jump to the previous merge conflict marker.
|
||||
---@return diffview.ConflictCount?
|
||||
function M.prev_conflict()
|
||||
return M.jumpto_conflict(-1, true)
|
||||
end
|
||||
|
||||
---Execute `cmd` for each target window in the current view. If no targets
|
||||
---are given, all windows are targeted.
|
||||
---@param cmd string|function The vim cmd to execute, or a function.
|
||||
---@return function action
|
||||
function M.view_windo(cmd)
|
||||
local fun
|
||||
|
||||
if type(cmd) == "string" then
|
||||
fun = function(_, _) vim.cmd(cmd) end
|
||||
else
|
||||
fun = cmd
|
||||
end
|
||||
|
||||
return function()
|
||||
local view = lib.get_current_view()
|
||||
|
||||
if view and view:instanceof(StandardView.__get()) then
|
||||
---@cast view StandardView
|
||||
|
||||
for _, symbol in ipairs({ "a", "b", "c", "d" }) do
|
||||
local win = view.cur_layout[symbol] --[[@as Window? ]]
|
||||
|
||||
if win then
|
||||
api.nvim_win_call(win.id, function()
|
||||
fun(view.cur_layout.name, symbol)
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param distance number Either an exact number of lines, or a fraction of the window height.
|
||||
---@return function
|
||||
function M.scroll_view(distance)
|
||||
local scroll_opr = distance < 0 and [[\<c-y>]] or [[\<c-e>]]
|
||||
local scroll_cmd
|
||||
|
||||
if distance % 1 == 0 then
|
||||
scroll_cmd = ([[exe "norm! %d%s"]]):format(distance, scroll_opr)
|
||||
else
|
||||
scroll_cmd = ([[exe "norm! " . float2nr(winheight(0) * %f) . "%s"]])
|
||||
:format(math.abs(distance), scroll_opr)
|
||||
end
|
||||
|
||||
return function()
|
||||
local view = lib.get_current_view()
|
||||
|
||||
if view and view:instanceof(StandardView.__get()) then
|
||||
---@cast view StandardView
|
||||
local max = -1
|
||||
local target
|
||||
|
||||
for _, win in ipairs(view.cur_layout.windows) do
|
||||
local height = utils.win_content_height(win.id)
|
||||
if height > max then
|
||||
max = height
|
||||
target = win.id
|
||||
end
|
||||
end
|
||||
|
||||
if target then
|
||||
api.nvim_win_call(target, function()
|
||||
vim.cmd(scroll_cmd)
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param kind "ours"|"theirs"|"base"|"local"
|
||||
local function diff_copy_target(kind)
|
||||
local view = lib.get_current_view() --[[@as DiffView|FileHistoryView ]]
|
||||
local file = view.cur_entry
|
||||
|
||||
if file then
|
||||
local layout = file.layout
|
||||
local bufnr
|
||||
|
||||
if layout:instanceof(Diff3.__get()) then
|
||||
---@cast layout Diff3
|
||||
if kind == "ours" then
|
||||
bufnr = layout.a.file.bufnr
|
||||
elseif kind == "theirs" then
|
||||
bufnr = layout.c.file.bufnr
|
||||
elseif kind == "local" then
|
||||
bufnr = layout.b.file.bufnr
|
||||
end
|
||||
elseif layout:instanceof(Diff4.__get()) then
|
||||
---@cast layout Diff4
|
||||
if kind == "ours" then
|
||||
bufnr = layout.a.file.bufnr
|
||||
elseif kind == "theirs" then
|
||||
bufnr = layout.c.file.bufnr
|
||||
elseif kind == "base" then
|
||||
bufnr = layout.d.file.bufnr
|
||||
elseif kind == "local" then
|
||||
bufnr = layout.b.file.bufnr
|
||||
end
|
||||
end
|
||||
|
||||
if bufnr then return bufnr end
|
||||
end
|
||||
end
|
||||
|
||||
---@param view DiffView
|
||||
---@param target "ours"|"theirs"|"base"|"all"|"none"
|
||||
local function resolve_all_conflicts(view, target)
|
||||
local main = view.cur_layout:get_main_win()
|
||||
local curfile = main.file
|
||||
|
||||
if main:is_valid() and curfile:is_valid() then
|
||||
local lines = api.nvim_buf_get_lines(curfile.bufnr, 0, -1, false)
|
||||
local conflicts = vcs_utils.parse_conflicts(lines, main.id)
|
||||
|
||||
if next(conflicts) then
|
||||
local content
|
||||
local offset = 0
|
||||
local first, last
|
||||
|
||||
for _, cur_conflict in ipairs(conflicts) do
|
||||
-- add offset to line numbers
|
||||
first = cur_conflict.first + offset
|
||||
last = cur_conflict.last + offset
|
||||
|
||||
if target == "ours" then content = cur_conflict.ours.content
|
||||
elseif target == "theirs" then content = cur_conflict.theirs.content
|
||||
elseif target == "base" then content = cur_conflict.base.content
|
||||
elseif target == "all" then
|
||||
content = utils.vec_join(
|
||||
cur_conflict.ours.content,
|
||||
cur_conflict.base.content,
|
||||
cur_conflict.theirs.content
|
||||
)
|
||||
end
|
||||
|
||||
content = content or {}
|
||||
api.nvim_buf_set_lines(curfile.bufnr, first - 1, last, false, content)
|
||||
offset = offset + (#content - (last - first) - 1)
|
||||
end
|
||||
|
||||
utils.set_cursor(main.id, unpack({
|
||||
(content and #content or 0) + first - 1,
|
||||
content and content[1] and #content[#content] or 0
|
||||
}))
|
||||
|
||||
view.cur_layout:sync_scroll()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param target "ours"|"theirs"|"base"|"all"|"none"
|
||||
function M.conflict_choose_all(target)
|
||||
return async.void(function()
|
||||
local view = lib.get_current_view() --[[@as DiffView ]]
|
||||
|
||||
if (view and view:instanceof(DiffView.__get())) then
|
||||
---@cast view DiffView
|
||||
|
||||
if view.panel:is_focused() then
|
||||
local item = view:infer_cur_file(false) ---@cast item -DirData
|
||||
if not item then return end
|
||||
|
||||
if not item.active then
|
||||
-- Open the entry
|
||||
await(view:set_file(item))
|
||||
end
|
||||
end
|
||||
|
||||
resolve_all_conflicts(view, target)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---@param target "ours"|"theirs"|"base"|"all"|"none"
|
||||
function M.conflict_choose(target)
|
||||
return function()
|
||||
local view = lib.get_current_view()
|
||||
|
||||
if view and view:instanceof(StandardView.__get()) then
|
||||
---@cast view StandardView
|
||||
local main = view.cur_layout:get_main_win()
|
||||
local curfile = main.file
|
||||
|
||||
if main:is_valid() and curfile:is_valid() then
|
||||
local _, cur = vcs_utils.parse_conflicts(
|
||||
api.nvim_buf_get_lines(curfile.bufnr, 0, -1, false),
|
||||
main.id
|
||||
)
|
||||
|
||||
if cur then
|
||||
local content
|
||||
|
||||
if target == "ours" then content = cur.ours.content
|
||||
elseif target == "theirs" then content = cur.theirs.content
|
||||
elseif target == "base" then content = cur.base.content
|
||||
elseif target == "all" then
|
||||
content = utils.vec_join(
|
||||
cur.ours.content,
|
||||
cur.base.content,
|
||||
cur.theirs.content
|
||||
)
|
||||
end
|
||||
|
||||
api.nvim_buf_set_lines(curfile.bufnr, cur.first - 1, cur.last, false, content or {})
|
||||
|
||||
utils.set_cursor(main.id, unpack({
|
||||
(content and #content or 0) + cur.first - 1,
|
||||
content and content[1] and #content[#content] or 0
|
||||
}))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param target "ours"|"theirs"|"base"|"local"
|
||||
function M.diffget(target)
|
||||
return function()
|
||||
local bufnr = diff_copy_target(target)
|
||||
|
||||
if bufnr and api.nvim_buf_is_valid(bufnr) then
|
||||
local range
|
||||
|
||||
if api.nvim_get_mode().mode:match("^[vV]") then
|
||||
range = ("%d,%d"):format(unpack(utils.vec_sort({
|
||||
vim.fn.line("."),
|
||||
vim.fn.line("v")
|
||||
})))
|
||||
end
|
||||
|
||||
vim.cmd(("%sdiffget %d"):format(range or "", bufnr))
|
||||
|
||||
if range then
|
||||
api.nvim_feedkeys(utils.t("<esc>"), "n", false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param target "ours"|"theirs"|"base"|"local"
|
||||
function M.diffput(target)
|
||||
return function()
|
||||
local bufnr = diff_copy_target(target)
|
||||
|
||||
if bufnr and api.nvim_buf_is_valid(bufnr) then
|
||||
vim.cmd("diffput " .. bufnr)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.cycle_layout()
|
||||
local layout_cycles = {
|
||||
standard = {
|
||||
Diff2Hor.__get(),
|
||||
Diff2Ver.__get(),
|
||||
},
|
||||
merge_tool = {
|
||||
Diff3Hor.__get(),
|
||||
Diff3Ver.__get(),
|
||||
Diff3Mixed.__get(),
|
||||
Diff4Mixed.__get(),
|
||||
Diff1.__get(),
|
||||
}
|
||||
}
|
||||
|
||||
local view = lib.get_current_view()
|
||||
|
||||
if not view then return end
|
||||
|
||||
local layouts, files, cur_file
|
||||
|
||||
if view:instanceof(FileHistoryView.__get()) then
|
||||
---@cast view FileHistoryView
|
||||
layouts = layout_cycles.standard
|
||||
files = view.panel:list_files()
|
||||
cur_file = view:cur_file()
|
||||
elseif view:instanceof(DiffView.__get()) then
|
||||
---@cast view DiffView
|
||||
cur_file = view.cur_entry
|
||||
|
||||
if cur_file then
|
||||
layouts = cur_file.kind == "conflicting"
|
||||
and layout_cycles.merge_tool
|
||||
or layout_cycles.standard
|
||||
files = cur_file.kind == "conflicting"
|
||||
and view.files.conflicting
|
||||
or utils.vec_join(view.panel.files.working, view.panel.files.staged)
|
||||
end
|
||||
else
|
||||
return
|
||||
end
|
||||
|
||||
for _, entry in ipairs(files) do
|
||||
local cur_layout = entry.layout
|
||||
local next_layout = layouts[utils.vec_indexof(layouts, cur_layout.class) % #layouts + 1]
|
||||
entry:convert_layout(next_layout)
|
||||
end
|
||||
|
||||
if cur_file then
|
||||
local main = view.cur_layout:get_main_win()
|
||||
local pos = api.nvim_win_get_cursor(main.id)
|
||||
local was_focused = view.cur_layout:is_focused()
|
||||
|
||||
cur_file.layout.emitter:once("files_opened", function()
|
||||
utils.set_cursor(main.id, unpack(pos))
|
||||
if not was_focused then view.cur_layout:sync_scroll() end
|
||||
end)
|
||||
|
||||
view:set_file(cur_file, false)
|
||||
main = view.cur_layout:get_main_win()
|
||||
|
||||
if was_focused then main:focus() end
|
||||
end
|
||||
end
|
||||
|
||||
---@param keymap_groups string|string[]
|
||||
function M.help(keymap_groups)
|
||||
keymap_groups = type(keymap_groups) == "table" and keymap_groups or { keymap_groups }
|
||||
|
||||
return function()
|
||||
local view = lib.get_current_view()
|
||||
|
||||
if view then
|
||||
local help_panel = HelpPanel(view, keymap_groups) --[[@as HelpPanel ]]
|
||||
help_panel:focus()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
do
|
||||
M.compat.fold_cmds = {}
|
||||
|
||||
-- For file entries that use custom folds with `foldmethod=manual` we need to
|
||||
-- replicate fold commands in all diff windows, as folds are only
|
||||
-- synchronized between diff windows when `foldmethod=diff`.
|
||||
local function compat_fold(fold_cmd)
|
||||
return function()
|
||||
if vim.wo.foldmethod ~= "manual" then
|
||||
local ok, msg = pcall(vim.cmd, "norm! " .. fold_cmd)
|
||||
if not ok and msg then
|
||||
api.nvim_err_writeln(msg)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local view = lib.get_current_view()
|
||||
|
||||
if view and view:instanceof(StandardView.__get()) then
|
||||
---@cast view StandardView
|
||||
local err
|
||||
|
||||
for _, win in ipairs(view.cur_layout.windows) do
|
||||
api.nvim_win_call(win.id, function()
|
||||
local ok, msg = pcall(vim.cmd, "norm! " .. fold_cmd)
|
||||
if not ok then err = msg end
|
||||
end)
|
||||
end
|
||||
|
||||
if err then api.nvim_err_writeln(err) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for _, fold_cmd in ipairs({
|
||||
"za", "zA", "ze", "zE", "zo", "zc", "zO", "zC", "zr", "zm", "zR", "zM",
|
||||
"zv", "zx", "zX", "zn", "zN", "zi",
|
||||
}) do
|
||||
table.insert(M.compat.fold_cmds, {
|
||||
"n",
|
||||
fold_cmd,
|
||||
compat_fold(fold_cmd),
|
||||
{ desc = "diffview_ignore" },
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
local action_names = {
|
||||
"close",
|
||||
"close_all_folds",
|
||||
"close_fold",
|
||||
"copy_hash",
|
||||
"focus_entry",
|
||||
"focus_files",
|
||||
"listing_style",
|
||||
"next_entry",
|
||||
"open_all_folds",
|
||||
"open_commit_log",
|
||||
"open_fold",
|
||||
"open_in_diffview",
|
||||
"options",
|
||||
"prev_entry",
|
||||
"refresh_files",
|
||||
"restore_entry",
|
||||
"select_entry",
|
||||
"select_next_entry",
|
||||
"select_prev_entry",
|
||||
"stage_all",
|
||||
"toggle_files",
|
||||
"toggle_flatten_dirs",
|
||||
"toggle_fold",
|
||||
"toggle_stage_entry",
|
||||
"unstage_all",
|
||||
}
|
||||
|
||||
for _, name in ipairs(action_names) do
|
||||
M[name] = function()
|
||||
require("diffview").emit(name)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,179 @@
|
||||
local async = require("diffview.async")
|
||||
local lazy = require("diffview.lazy")
|
||||
|
||||
local DiffView = lazy.access("diffview.scene.views.diff.diff_view", "DiffView") ---@type DiffView|LazyModule
|
||||
local FileEntry = lazy.access("diffview.scene.file_entry", "FileEntry") ---@type FileEntry|LazyModule
|
||||
local FilePanel = lazy.access("diffview.scene.views.diff.file_panel", "FilePanel") ---@type FilePanel|LazyModule
|
||||
local Rev = lazy.access("diffview.vcs.adapters.git.rev", "GitRev") ---@type GitRev|LazyModule
|
||||
local RevType = lazy.access("diffview.vcs.rev", "RevType") ---@type RevType|LazyModule
|
||||
local vcs_utils = lazy.require("diffview.vcs") ---@module "diffview.vcs"
|
||||
local oop = lazy.require("diffview.oop") ---@module "diffview.oop"
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
|
||||
local logger = DiffviewGlobal.logger
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class FileData
|
||||
---@field path string Path relative to git root.
|
||||
---@field oldpath string|nil If the file has been renamed, this should be the old path, oterhwise nil.
|
||||
---@field status string Git status symbol.
|
||||
---@field stats GitStats|nil
|
||||
---@field left_null boolean Indicates that the left buffer should be represented by the null buffer.
|
||||
---@field right_null boolean Indicates that the right buffer should be represented by the null buffer.
|
||||
---@field selected boolean|nil Indicates that this should be the initially selected file.
|
||||
|
||||
---@class CDiffView : DiffView
|
||||
---@field files any
|
||||
---@field fetch_files function A function that should return an updated list of files.
|
||||
---@field get_file_data function A function that is called with parameters `path: string` and `split: string`, and should return a list of lines that should make up the buffer.
|
||||
local CDiffView = oop.create_class("CDiffView", DiffView.__get())
|
||||
|
||||
---CDiffView constructor.
|
||||
---@param opt any
|
||||
function CDiffView:init(opt)
|
||||
logger:info("[api] Creating a new Custom DiffView.")
|
||||
self.valid = false
|
||||
|
||||
local err, adapter = vcs_utils.get_adapter({ top_indicators = { opt.git_root } })
|
||||
|
||||
if err then
|
||||
utils.err(
|
||||
("Failed to create an adapter for the repository: %s")
|
||||
:format(utils.str_quote(opt.git_root))
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
---@cast adapter -?
|
||||
|
||||
-- Fix malformed revs
|
||||
for _, v in ipairs({ "left", "right" }) do
|
||||
local rev = opt[v]
|
||||
if not rev or not rev.type then
|
||||
opt[v] = Rev(RevType.STAGE, 0)
|
||||
end
|
||||
end
|
||||
|
||||
self.fetch_files = opt.update_files
|
||||
self.get_file_data = opt.get_file_data
|
||||
|
||||
self:super(vim.tbl_extend("force", opt, {
|
||||
adapter = adapter,
|
||||
panel = FilePanel(
|
||||
adapter,
|
||||
self.files,
|
||||
self.path_args,
|
||||
self.rev_arg or adapter:rev_to_pretty_string(opt.left, opt.right)
|
||||
),
|
||||
}))
|
||||
|
||||
if type(opt.files) == "table" and not vim.tbl_isempty(opt.files) then
|
||||
local files = self:create_file_entries(opt.files)
|
||||
|
||||
for kind, entries in pairs(files) do
|
||||
for _, entry in ipairs(entries) do
|
||||
table.insert(self.files[kind], entry)
|
||||
end
|
||||
end
|
||||
self.files:update_file_trees()
|
||||
|
||||
if self.panel.cur_file then
|
||||
vim.schedule(function()
|
||||
self:set_file(self.panel.cur_file, false, true)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
self.valid = true
|
||||
end
|
||||
|
||||
---@override
|
||||
CDiffView.get_updated_files = async.wrap(function(self, callback)
|
||||
local err
|
||||
|
||||
repeat
|
||||
local ok, new_files = pcall(self.fetch_files, self)
|
||||
|
||||
if not ok or type(new_files) ~= "table" then
|
||||
err = { "Integrating plugin failed to provide file data!" }
|
||||
break
|
||||
end
|
||||
|
||||
---@diagnostic disable-next-line: redefined-local
|
||||
local ok, entries = pcall(self.create_file_entries, self, new_files)
|
||||
|
||||
if not ok then
|
||||
err = { "Integrating plugin provided malformed file data!" }
|
||||
break
|
||||
end
|
||||
|
||||
callback(nil, entries)
|
||||
return
|
||||
until true
|
||||
|
||||
utils.err(err, true)
|
||||
logger:error(table.concat(err, "\n"))
|
||||
callback(err, nil)
|
||||
end)
|
||||
|
||||
function CDiffView:create_file_entries(files)
|
||||
local entries = {}
|
||||
|
||||
local sections = {
|
||||
{ kind = "conflicting", files = files.conflicting or {} },
|
||||
{ kind = "working", files = files.working or {}, left = self.left, right = self.right },
|
||||
{
|
||||
kind = "staged",
|
||||
files = files.staged or {},
|
||||
left = self.adapter:head_rev(),
|
||||
right = Rev(RevType.STAGE, 0),
|
||||
},
|
||||
}
|
||||
|
||||
for _, v in ipairs(sections) do
|
||||
entries[v.kind] = {}
|
||||
|
||||
for _, file_data in ipairs(v.files) do
|
||||
if v.kind == "conflicting" then
|
||||
table.insert(entries[v.kind], FileEntry.with_layout(CDiffView.get_default_merge_layout(), {
|
||||
adapter = self.adapter,
|
||||
path = file_data.path,
|
||||
oldpath = file_data.oldpath,
|
||||
status = "U",
|
||||
kind = "conflicting",
|
||||
revs = {
|
||||
a = Rev(RevType.STAGE, 2),
|
||||
b = Rev(RevType.LOCAL),
|
||||
c = Rev(RevType.STAGE, 3),
|
||||
d = Rev(RevType.STAGE, 1),
|
||||
},
|
||||
}))
|
||||
else
|
||||
table.insert(entries[v.kind], FileEntry.with_layout(CDiffView.get_default_layout(), {
|
||||
adapter = self.adapter,
|
||||
path = file_data.path,
|
||||
oldpath = file_data.oldpath,
|
||||
status = file_data.status,
|
||||
stats = file_data.stats,
|
||||
kind = v.kind,
|
||||
revs = {
|
||||
a = v.left,
|
||||
b = v.right,
|
||||
},
|
||||
get_data = self.get_file_data,
|
||||
--FIXME: left_null, right_null
|
||||
}))
|
||||
end
|
||||
|
||||
if file_data.selected then
|
||||
self.panel:set_cur_file(entries[v.kind][#entries[v.kind]])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return entries
|
||||
end
|
||||
|
||||
M.CDiffView = CDiffView
|
||||
return M
|
||||
@ -0,0 +1,432 @@
|
||||
local lazy = require("diffview.lazy")
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
|
||||
local M = {}
|
||||
|
||||
local short_flag_pat = { "^[-+](%a)=?(.*)" }
|
||||
local long_flag_pat = { "^%-%-(%a[%a%d-]*)=?(.*)", "^%+%+(%a[%a%d-]*)=?(.*)" }
|
||||
|
||||
---@class ArgObject : diffview.Object
|
||||
---@field flags table<string, string[]>
|
||||
---@field args string[]
|
||||
---@field post_args string[]
|
||||
local ArgObject = oop.create_class("ArgObject")
|
||||
|
||||
---ArgObject constructor.
|
||||
---@param flags table<string, string>
|
||||
---@param args string[]
|
||||
function ArgObject:init(flags, args, post_args)
|
||||
self.flags = flags
|
||||
self.args = args
|
||||
self.post_args = post_args
|
||||
end
|
||||
|
||||
---@class ArgObject.GetFlagSpec
|
||||
---@field plain boolean Never cast string values to booleans.
|
||||
---@field expect_list boolean Return a list of all defined values for the given flag.
|
||||
---@field expect_string boolean Inferred boolean values are changed to be empty strings.
|
||||
---@field no_empty boolean Return nil if the value is an empty string. Implies `expect_string`.
|
||||
---@field expand boolean Expand wildcards and special keywords (`:h expand()`).
|
||||
|
||||
---Get a flag value.
|
||||
---@param names string|string[] Flag synonyms
|
||||
---@param opt? ArgObject.GetFlagSpec
|
||||
---@return string[]|string|boolean
|
||||
function ArgObject:get_flag(names, opt)
|
||||
opt = opt or {}
|
||||
if opt.no_empty then
|
||||
opt.expect_string = true
|
||||
end
|
||||
|
||||
if type(names) ~= "table" then
|
||||
names = { names }
|
||||
end
|
||||
|
||||
local values = {}
|
||||
for _, name in ipairs(names) do
|
||||
if self.flags[name] then
|
||||
utils.vec_push(values, unpack(self.flags[name]))
|
||||
end
|
||||
end
|
||||
|
||||
values = utils.tbl_fmap(values, function(v)
|
||||
if opt.expect_string and v == "true" then
|
||||
-- Undo inferred boolean values
|
||||
if opt.no_empty then
|
||||
return nil
|
||||
end
|
||||
v = ""
|
||||
elseif not opt.plain and (v == "true" or v == "false") then
|
||||
-- Cast to boolean
|
||||
v = v == "true"
|
||||
end
|
||||
|
||||
if opt.expand then
|
||||
v = vim.fn.expand(v)
|
||||
end
|
||||
|
||||
return v
|
||||
end)
|
||||
|
||||
-- If a list isn't expected: return the last defined value for this flag.
|
||||
return opt.expect_list and values or values[#values]
|
||||
end
|
||||
|
||||
---@class FlagValueMap : diffview.Object
|
||||
---@field map table<string, string[]>
|
||||
local FlagValueMap = oop.create_class("FlagValueMap")
|
||||
|
||||
---FlagValueMap constructor
|
||||
function FlagValueMap:init()
|
||||
self.map = {}
|
||||
end
|
||||
|
||||
---@param flag_synonyms string[]
|
||||
---@param producer? string[]|fun(name_lead: string, arg_lead: string): string[]
|
||||
function FlagValueMap:put(flag_synonyms, producer)
|
||||
for _, flag in ipairs(flag_synonyms) do
|
||||
local char = flag:sub(1, 1)
|
||||
if char ~= "-" and char ~= "+" then
|
||||
if #flag > 1 then
|
||||
flag = "--" .. flag
|
||||
else
|
||||
flag = "-" .. flag
|
||||
end
|
||||
end
|
||||
self.map[flag] = producer or { "true", "false" }
|
||||
self.map[#self.map + 1] = flag
|
||||
end
|
||||
end
|
||||
|
||||
---Get list of possible values for a given flag.
|
||||
---@param flag_name string
|
||||
---@return string[]
|
||||
function FlagValueMap:get(flag_name)
|
||||
local char = flag_name:sub(1, 1)
|
||||
if char ~= "-" and char ~= "+" then
|
||||
if #flag_name > 1 then
|
||||
flag_name = "--" .. flag_name
|
||||
else
|
||||
flag_name = "-" .. flag_name
|
||||
end
|
||||
end
|
||||
|
||||
if type(self.map[flag_name]) == "function" then
|
||||
local is_short = utils.str_match(flag_name, short_flag_pat) ~= nil
|
||||
return self.map[flag_name](flag_name .. (not is_short and "=" or ""), "")
|
||||
end
|
||||
|
||||
return self.map[flag_name]
|
||||
end
|
||||
|
||||
---Get a list of all flag names.
|
||||
---@return string[]
|
||||
function FlagValueMap:get_all_names()
|
||||
return utils.vec_slice(self.map)
|
||||
end
|
||||
|
||||
---@param arg_lead string
|
||||
---@return string[]?
|
||||
function FlagValueMap:get_completion(arg_lead)
|
||||
arg_lead = arg_lead or ""
|
||||
local name
|
||||
local is_short = utils.str_match(arg_lead, short_flag_pat) ~= nil
|
||||
|
||||
if is_short then
|
||||
name = arg_lead:sub(1, 2)
|
||||
arg_lead = arg_lead:match("..=?(.*)") or ""
|
||||
else
|
||||
name = arg_lead:gsub("=.*", "")
|
||||
arg_lead = arg_lead:match("=(.*)") or ""
|
||||
end
|
||||
|
||||
local name_lead = name .. (not is_short and "=" or "")
|
||||
local values = self.map[name]
|
||||
if type(values) == "function" then
|
||||
values = values(name_lead, arg_lead)
|
||||
end
|
||||
if not values then
|
||||
return nil
|
||||
end
|
||||
|
||||
local items = {}
|
||||
for _, v in ipairs(values) do
|
||||
local e_lead, _ = vim.pesc(arg_lead)
|
||||
if v:match(e_lead) then
|
||||
items[#items + 1] = name_lead .. v
|
||||
end
|
||||
end
|
||||
|
||||
return items
|
||||
end
|
||||
|
||||
---Parse args and create an ArgObject.
|
||||
---@param args string[]
|
||||
---@return ArgObject
|
||||
function M.parse(args)
|
||||
local flags = {}
|
||||
local pre_args = {}
|
||||
local post_args = {}
|
||||
|
||||
for i, arg in ipairs(args) do
|
||||
if arg == "--" then
|
||||
for j = i + 1, #args do
|
||||
table.insert(post_args, args[j])
|
||||
end
|
||||
break
|
||||
end
|
||||
|
||||
local flag, value
|
||||
flag, value = utils.str_match(arg, short_flag_pat)
|
||||
if flag then
|
||||
value = (value == "") and "true" or value
|
||||
|
||||
if not flags[flag] then
|
||||
flags[flag] = {}
|
||||
end
|
||||
|
||||
table.insert(flags[flag], value)
|
||||
goto continue
|
||||
end
|
||||
|
||||
flag, value = utils.str_match(arg, long_flag_pat)
|
||||
if flag then
|
||||
value = (value == "") and "true" or value
|
||||
|
||||
if not flags[flag] then
|
||||
flags[flag] = {}
|
||||
end
|
||||
|
||||
table.insert(flags[flag], value)
|
||||
goto continue
|
||||
end
|
||||
|
||||
table.insert(pre_args, arg)
|
||||
|
||||
::continue::
|
||||
end
|
||||
|
||||
return ArgObject(flags, pre_args, post_args)
|
||||
end
|
||||
|
||||
---Split the line range from an EX command arg.
|
||||
---@param arg string
|
||||
---@return string range, string command
|
||||
function M.split_ex_range(arg)
|
||||
local idx = arg:match(".*()%A")
|
||||
if not idx then
|
||||
return "", arg
|
||||
end
|
||||
|
||||
local slice = arg:sub(idx or 1)
|
||||
idx = slice:match("[^']()%a")
|
||||
|
||||
if idx then
|
||||
return arg:sub(1, (#arg - #slice) + idx - 1), slice:sub(idx)
|
||||
end
|
||||
|
||||
return arg, ""
|
||||
end
|
||||
|
||||
---@class CmdLineContext
|
||||
---@field cmd_line string
|
||||
---@field args string[] # The tokenized list of arguments.
|
||||
---@field raw_args string[] # The unprocessed list of arguments. Contains syntax characters, such as quotes.
|
||||
---@field arg_lead string # The leading part of the current argument.
|
||||
---@field lead_quote string? # If present: the quote character used for the current argument.
|
||||
---@field cur_pos integer # The cursor position in the command line.
|
||||
---@field argidx integer # Index of the current argument.
|
||||
---@field divideridx integer # The index of the end-of-options token. (default: math.huge)
|
||||
---@field range string? # Ex command range.
|
||||
---@field between boolean # The current position is between two arguments.
|
||||
|
||||
---@class arg_parser.scan.Opt
|
||||
---@field cur_pos integer # The current cursor position in the command line.
|
||||
---@field allow_quoted boolean # Everything between a pair of quotes should be treated as part of a single argument. (default: true)
|
||||
---@field allow_ex_range boolean # The command line may contain an EX command range. (default: false)
|
||||
|
||||
---Tokenize a command line string.
|
||||
---@param cmd_line string
|
||||
---@param opt? arg_parser.scan.Opt
|
||||
---@return CmdLineContext
|
||||
function M.scan(cmd_line, opt)
|
||||
opt = vim.tbl_extend("keep", opt or {}, {
|
||||
cur_pos = #cmd_line + 1,
|
||||
allow_quoted = true,
|
||||
allow_ex_range = false,
|
||||
}) --[[@as arg_parser.scan.Opt ]]
|
||||
|
||||
local args = {}
|
||||
local raw_args = {}
|
||||
local arg_lead
|
||||
local divideridx = math.huge
|
||||
local argidx
|
||||
local between = false
|
||||
local cur_quote, lead_quote
|
||||
local arg, raw_arg = "", ""
|
||||
|
||||
local h, i = -1, 1
|
||||
|
||||
while i <= #cmd_line do
|
||||
local char = cmd_line:sub(i, i)
|
||||
|
||||
if not argidx and i > opt.cur_pos then
|
||||
argidx = #args + 1
|
||||
arg_lead = arg
|
||||
lead_quote = cur_quote
|
||||
if h < opt.cur_pos then between = true end
|
||||
end
|
||||
|
||||
if char == "\\" then
|
||||
arg = arg .. char
|
||||
raw_arg = raw_arg .. char
|
||||
if i < #cmd_line then
|
||||
i = i + 1
|
||||
arg = arg .. cmd_line:sub(i, i)
|
||||
raw_arg = raw_arg .. cmd_line:sub(i, i)
|
||||
end
|
||||
h = i
|
||||
elseif cur_quote then
|
||||
if char == cur_quote then
|
||||
cur_quote = nil
|
||||
else
|
||||
arg = arg .. char
|
||||
end
|
||||
raw_arg = raw_arg .. char
|
||||
h = i
|
||||
elseif opt.allow_quoted and (char == [[']] or char == [["]]) then
|
||||
cur_quote = char
|
||||
raw_arg = raw_arg .. char
|
||||
h = i
|
||||
elseif char:match("%s") then
|
||||
if arg ~= "" then
|
||||
table.insert(args, arg)
|
||||
if arg == "--" and i - 1 < #cmd_line then
|
||||
divideridx = #args
|
||||
end
|
||||
end
|
||||
if raw_arg ~= "" then
|
||||
table.insert(raw_args, raw_arg)
|
||||
end
|
||||
arg = ""
|
||||
raw_arg = ""
|
||||
-- Skip whitespace
|
||||
i = i + cmd_line:sub(i, -1):match("^%s+()") - 2
|
||||
else
|
||||
arg = arg .. char
|
||||
raw_arg = raw_arg .. char
|
||||
h = i
|
||||
end
|
||||
|
||||
i = i + 1
|
||||
end
|
||||
|
||||
if #arg > 0 then
|
||||
table.insert(args, arg)
|
||||
table.insert(raw_args, raw_arg)
|
||||
if not arg_lead then
|
||||
arg_lead = arg
|
||||
lead_quote = cur_quote
|
||||
end
|
||||
|
||||
if arg == "--" and cmd_line:sub(#cmd_line, #cmd_line) ~= "-" then
|
||||
divideridx = #args
|
||||
end
|
||||
end
|
||||
|
||||
if not argidx then
|
||||
argidx = #args
|
||||
if cmd_line:sub(#cmd_line, #cmd_line):match("%s") then
|
||||
argidx = argidx + 1
|
||||
end
|
||||
end
|
||||
|
||||
local range
|
||||
|
||||
if #args > 0 then
|
||||
if opt.allow_ex_range then
|
||||
range, args[1] = M.split_ex_range(args[1])
|
||||
_, raw_args[1] = M.split_ex_range(raw_args[1])
|
||||
end
|
||||
|
||||
if args[1] == "" then
|
||||
table.remove(args, 1)
|
||||
table.remove(raw_args, 1)
|
||||
argidx = math.max(argidx - 1, 1)
|
||||
divideridx = math.max(divideridx - 1, 1)
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
cmd_line = cmd_line,
|
||||
args = args,
|
||||
raw_args = raw_args,
|
||||
arg_lead = arg_lead or "",
|
||||
lead_quote = lead_quote,
|
||||
cur_pos = opt.cur_pos,
|
||||
argidx = argidx,
|
||||
divideridx = divideridx,
|
||||
range = range ~= "" and range or nil,
|
||||
between = between,
|
||||
}
|
||||
end
|
||||
|
||||
---Filter completion candidates.
|
||||
---@param arg_lead string
|
||||
---@param candidates string[]
|
||||
---@return string[]
|
||||
function M.filter_candidates(arg_lead, candidates)
|
||||
arg_lead, _ = vim.pesc(arg_lead)
|
||||
|
||||
return vim.tbl_filter(function(item)
|
||||
return item:match(arg_lead)
|
||||
end, candidates)
|
||||
end
|
||||
|
||||
---Process completion candidates.
|
||||
---@param candidates string[]
|
||||
---@param ctx CmdLineContext
|
||||
---@param input_cmp boolean? Completion for |input()|.
|
||||
---@return string[]
|
||||
function M.process_candidates(candidates, ctx, input_cmp)
|
||||
if not candidates then return {} end
|
||||
|
||||
local cmd_lead = ""
|
||||
local ex_lead = (ctx.lead_quote or "") .. ctx.arg_lead
|
||||
|
||||
if ctx.arg_lead and ctx.arg_lead:find("[^\\]%s") then
|
||||
ex_lead = (ctx.lead_quote or "") .. ctx.arg_lead:match(".*[^\\]%s(.*)")
|
||||
end
|
||||
|
||||
if input_cmp then
|
||||
cmd_lead = ctx.cmd_line:sub(1, ctx.cur_pos - #ex_lead)
|
||||
end
|
||||
|
||||
local ret = vim.tbl_map(function(v)
|
||||
if v:match("^" .. vim.pesc(ctx.arg_lead)) then
|
||||
return cmd_lead .. ex_lead .. v:sub(#ctx.arg_lead + 1)
|
||||
elseif input_cmp then
|
||||
return cmd_lead .. v
|
||||
end
|
||||
|
||||
return (ctx.lead_quote or "") .. v
|
||||
end, candidates)
|
||||
|
||||
return M.filter_candidates(cmd_lead .. ex_lead, ret)
|
||||
end
|
||||
|
||||
function M.ambiguous_bool(value, default, truthy, falsy)
|
||||
if vim.tbl_contains(truthy, value) then
|
||||
return true
|
||||
end
|
||||
if vim.tbl_contains(falsy, value) then
|
||||
return false
|
||||
end
|
||||
return default
|
||||
end
|
||||
|
||||
M.ArgObject = ArgObject
|
||||
M.FlagValueMap = FlagValueMap
|
||||
return M
|
||||
@ -0,0 +1,577 @@
|
||||
local ffi = require("diffview.ffi")
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
local fmt = string.format
|
||||
local uv = vim.loop
|
||||
|
||||
local DEFAULT_ERROR = "Unkown error."
|
||||
|
||||
local M = {}
|
||||
|
||||
---@package
|
||||
---@type { [Future]: boolean }
|
||||
M._watching = setmetatable({}, { __mode = "k" })
|
||||
|
||||
---@package
|
||||
---@type { [thread]: Future }
|
||||
M._handles = {}
|
||||
|
||||
---@alias AsyncFunc (fun(...): Future)
|
||||
---@alias AsyncKind "callback"|"void"
|
||||
|
||||
local function dstring(object)
|
||||
if not DiffviewGlobal.logger then return "" end
|
||||
dstring = DiffviewGlobal.logger.dstring
|
||||
return dstring(object)
|
||||
end
|
||||
|
||||
---@param ... any
|
||||
---@return table
|
||||
local function tbl_pack(...)
|
||||
return { n = select("#", ...), ... }
|
||||
end
|
||||
|
||||
---@param t table
|
||||
---@param i? integer
|
||||
---@param j? integer
|
||||
---@return any ...
|
||||
local function tbl_unpack(t, i, j)
|
||||
return unpack(t, i or 1, j or t.n or table.maxn(t))
|
||||
end
|
||||
|
||||
---Returns the current thread or `nil` if it's the main thread.
|
||||
---
|
||||
---NOTE: coroutine.running() was changed between Lua 5.1 and 5.2:
|
||||
--- • 5.1: Returns the running coroutine, or `nil` when called by the main
|
||||
--- thread.
|
||||
--- • 5.2: Returns the running coroutine plus a boolean, true when the running
|
||||
--- coroutine is the main one.
|
||||
---
|
||||
---For LuaJIT, 5.2 behaviour is enabled with LUAJIT_ENABLE_LUA52COMPAT
|
||||
---
|
||||
---We need to handle both.
|
||||
---
|
||||
---Source: https://github.com/lewis6991/async.nvim/blob/bad4edbb2917324cd11662dc0209ce53f6c8bc23/lua/async.lua#L10
|
||||
---@return thread?
|
||||
local function current_thread()
|
||||
local current, ismain = coroutine.running()
|
||||
|
||||
if type(ismain) == "boolean" then
|
||||
return not ismain and current or nil
|
||||
else
|
||||
return current
|
||||
end
|
||||
end
|
||||
|
||||
---@class Waitable : diffview.Object
|
||||
local Waitable = oop.create_class("Waitable")
|
||||
M.Waitable = Waitable
|
||||
|
||||
---@abstract
|
||||
---@return any ... # Any values returned by the waitable
|
||||
function Waitable:await() oop.abstract_stub() end
|
||||
|
||||
---Schedule a callback to be invoked when this waitable has settled.
|
||||
---@param callback function
|
||||
function Waitable:finally(callback)
|
||||
(M.void(function()
|
||||
local ret = tbl_pack(M.await(self))
|
||||
callback(tbl_unpack(ret))
|
||||
end))()
|
||||
end
|
||||
|
||||
---@class Future : Waitable
|
||||
---@operator call : Future
|
||||
---@field package thread thread
|
||||
---@field package listeners Future[]
|
||||
---@field package parent? Future
|
||||
---@field package func? function
|
||||
---@field package return_values? any[]
|
||||
---@field package err? string
|
||||
---@field package kind AsyncKind
|
||||
---@field package started boolean
|
||||
---@field package awaiting_cb boolean
|
||||
---@field package done boolean
|
||||
---@field package has_raised boolean # `true` if this future has raised an error.
|
||||
local Future = oop.create_class("Future", Waitable)
|
||||
|
||||
function Future:init(opt)
|
||||
opt = opt or {}
|
||||
|
||||
if opt.thread then
|
||||
self.thread = opt.thread
|
||||
elseif opt.func then
|
||||
self.thread = coroutine.create(opt.func)
|
||||
else
|
||||
error("Either 'thread' or 'func' must be specified!")
|
||||
end
|
||||
|
||||
M._handles[self.thread] = self
|
||||
self.listeners = {}
|
||||
self.kind = opt.kind
|
||||
self.started = false
|
||||
self.awaiting_cb = false
|
||||
self.done = false
|
||||
self.has_raised = false
|
||||
end
|
||||
|
||||
---@package
|
||||
---@return string
|
||||
function Future:__tostring()
|
||||
return dstring(self.thread)
|
||||
end
|
||||
|
||||
---@package
|
||||
function Future:destroy()
|
||||
M._handles[self.thread] = nil
|
||||
end
|
||||
|
||||
---@package
|
||||
---@param value boolean
|
||||
function Future:set_done(value)
|
||||
self.done = value
|
||||
if self:is_watching() then
|
||||
self:dprint("done was set:", self.done)
|
||||
end
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
function Future:is_done()
|
||||
return not not self.done
|
||||
end
|
||||
|
||||
---@return any ... # If the future has completed, this returns any returned values.
|
||||
function Future:get_returned()
|
||||
if not self.return_values then return end
|
||||
return unpack(self.return_values, 2, table.maxn(self.return_values))
|
||||
end
|
||||
|
||||
---@package
|
||||
---@param ... any
|
||||
function Future:dprint(...)
|
||||
if not DiffviewGlobal.logger then return end
|
||||
if DiffviewGlobal.debug_level >= 10 or M._watching[self] then
|
||||
local t = { self, "::", ... }
|
||||
for i = 1, table.maxn(t) do t[i] = dstring(t[i]) end
|
||||
DiffviewGlobal.logger:debug(table.concat(t, " "))
|
||||
end
|
||||
end
|
||||
|
||||
---@package
|
||||
---@param ... any
|
||||
function Future:dprintf(...)
|
||||
self:dprint(fmt(...))
|
||||
end
|
||||
|
||||
---Start logging debug info about this future.
|
||||
function Future:watch()
|
||||
M._watching[self] = true
|
||||
end
|
||||
|
||||
---Stop logging debug info about this future.
|
||||
function Future:unwatch()
|
||||
M._watching[self] = nil
|
||||
end
|
||||
|
||||
---@package
|
||||
---@return boolean
|
||||
function Future:is_watching()
|
||||
return not not M._watching[self]
|
||||
end
|
||||
|
||||
---@package
|
||||
---@param force? boolean
|
||||
function Future:raise(force)
|
||||
if self.has_raised and not force then return end
|
||||
self.has_raised = true
|
||||
error(self.err)
|
||||
end
|
||||
|
||||
---@package
|
||||
function Future:step(...)
|
||||
self:dprint("step")
|
||||
local ret = { coroutine.resume(self.thread, ...) }
|
||||
local ok = ret[1]
|
||||
|
||||
if not ok then
|
||||
local err = ret[2] or DEFAULT_ERROR
|
||||
local func_info
|
||||
|
||||
if self.func then
|
||||
func_info = debug.getinfo(self.func, "uS")
|
||||
end
|
||||
|
||||
local msg = fmt(
|
||||
"The coroutine failed with this message: \n"
|
||||
.. "\tcontext: cur_thread=%s co_thread=%s %s\n%s",
|
||||
dstring(current_thread() or "main"),
|
||||
dstring(self.thread),
|
||||
func_info and fmt("co_func=%s:%d", func_info.short_src, func_info.linedefined) or "",
|
||||
debug.traceback(self.thread, err)
|
||||
)
|
||||
self:set_done(true)
|
||||
self:notify_all(false, msg)
|
||||
self:destroy()
|
||||
self:raise()
|
||||
return
|
||||
end
|
||||
|
||||
if coroutine.status(self.thread) == "dead" then
|
||||
self:dprint("handle dead")
|
||||
self:set_done(true)
|
||||
self:notify_all(true, unpack(ret, 2, table.maxn(ret)))
|
||||
self:destroy()
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
---@package
|
||||
---@param ok boolean
|
||||
---@param ... any
|
||||
function Future:notify_all(ok, ...)
|
||||
local ret_values = tbl_pack(ok, ...)
|
||||
|
||||
if not ok then
|
||||
self.err = ret_values[2] or DEFAULT_ERROR
|
||||
end
|
||||
|
||||
local seen = {}
|
||||
|
||||
while next(self.listeners) do
|
||||
local handle = table.remove(self.listeners, #self.listeners) --[[@as Future ]]
|
||||
|
||||
-- We don't want to trigger multiple steps for a single thread
|
||||
if handle and not seen[handle.thread] then
|
||||
self:dprint("notifying:", handle)
|
||||
seen[handle.thread] = true
|
||||
handle:step(ret_values)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@override
|
||||
---@return any ... # Return values
|
||||
function Future:await()
|
||||
if self.err then
|
||||
self:raise(true)
|
||||
return
|
||||
end
|
||||
|
||||
if self:is_done() then
|
||||
return self:get_returned()
|
||||
end
|
||||
|
||||
local current = current_thread()
|
||||
|
||||
if not current then
|
||||
-- Await called from main thread
|
||||
return self:toplevel_await()
|
||||
end
|
||||
|
||||
local parent_handle = M._handles[current]
|
||||
|
||||
if not parent_handle then
|
||||
-- We're on a thread not managed by us: create a Future wrap around the
|
||||
-- thread
|
||||
self:dprint("creating a wrapper around unmanaged thread")
|
||||
self.parent = Future({
|
||||
thread = current,
|
||||
kind = "void",
|
||||
})
|
||||
else
|
||||
self.parent = parent_handle
|
||||
end
|
||||
|
||||
if current ~= self.thread then
|
||||
-- We want the current thread to be notified when this future is done /
|
||||
-- terminated
|
||||
table.insert(self.listeners, self.parent)
|
||||
end
|
||||
|
||||
self:dprintf("awaiting: yielding=%s listeners=%s", dstring(current), dstring(self.listeners))
|
||||
coroutine.yield()
|
||||
|
||||
local ok
|
||||
|
||||
if not self.return_values then
|
||||
ok = self.err == nil
|
||||
else
|
||||
ok = self.return_values[1]
|
||||
|
||||
if not ok then
|
||||
self.err = self.return_values[2] or DEFAULT_ERROR
|
||||
end
|
||||
end
|
||||
|
||||
if not ok then
|
||||
self:raise(true)
|
||||
return
|
||||
end
|
||||
|
||||
return self:get_returned()
|
||||
end
|
||||
|
||||
---@package
|
||||
---@return any ...
|
||||
function Future:toplevel_await()
|
||||
local ok, status
|
||||
|
||||
while true do
|
||||
ok, status = vim.wait(1000 * 60, function()
|
||||
return coroutine.status(self.thread) == "dead"
|
||||
end, 1)
|
||||
|
||||
-- Respect interrupts
|
||||
if status ~= -1 then break end
|
||||
end
|
||||
|
||||
if not ok then
|
||||
if status == -1 then
|
||||
error("Async task timed out!")
|
||||
elseif status == -2 then
|
||||
error("Async task got interrupted!")
|
||||
end
|
||||
end
|
||||
|
||||
if self.err then
|
||||
self:raise(true)
|
||||
return
|
||||
end
|
||||
|
||||
return self:get_returned()
|
||||
end
|
||||
|
||||
---@class async._run.Opt
|
||||
---@field kind AsyncKind
|
||||
---@field nparams? integer
|
||||
---@field args any[]
|
||||
|
||||
---@package
|
||||
---@param func function
|
||||
---@param opt async._run.Opt
|
||||
function M._run(func, opt)
|
||||
opt = opt or {}
|
||||
|
||||
local handle ---@type Future
|
||||
local use_err_handler = not not current_thread()
|
||||
|
||||
local function wrapped_func(...)
|
||||
if use_err_handler then
|
||||
-- We are not on the main thread: use custom err handler
|
||||
local ok = xpcall(func, function(err)
|
||||
handle.err = debug.traceback(err, 2)
|
||||
end, ...)
|
||||
|
||||
if not ok then
|
||||
handle:dprint("an error was raised: terminating")
|
||||
handle:set_done(true)
|
||||
handle:destroy()
|
||||
error(handle.err, 0)
|
||||
return
|
||||
end
|
||||
else
|
||||
func(...)
|
||||
end
|
||||
|
||||
-- Check if we need to yield until cb. We might not need to if the cb was
|
||||
-- called in a synchronous way.
|
||||
if opt.kind == "callback" and not handle:is_done() then
|
||||
handle.awaiting_cb = true
|
||||
handle:dprintf("yielding for cb: current=%s", dstring(current_thread()))
|
||||
coroutine.yield()
|
||||
handle:dprintf("resuming after cb: current=%s", dstring(current_thread()))
|
||||
end
|
||||
|
||||
handle:set_done(true)
|
||||
end
|
||||
|
||||
if opt.kind == "callback" then
|
||||
local cur_cb = opt.args[opt.nparams]
|
||||
|
||||
local function wrapped_cb(...)
|
||||
handle:set_done(true)
|
||||
handle.return_values = { true, ... }
|
||||
if cur_cb then cur_cb(...) end
|
||||
|
||||
if handle.awaiting_cb then
|
||||
-- The thread was yielding for the callback: resume
|
||||
handle.awaiting_cb = false
|
||||
handle:step()
|
||||
end
|
||||
|
||||
handle:notify_all(true, ...)
|
||||
end
|
||||
|
||||
opt.args[opt.nparams] = wrapped_cb
|
||||
end
|
||||
|
||||
handle = Future({ func = wrapped_func, kind = opt.kind })
|
||||
handle:dprint("created thread")
|
||||
handle.func = func
|
||||
handle.started = true
|
||||
handle:step(tbl_unpack(opt.args))
|
||||
|
||||
return handle
|
||||
end
|
||||
|
||||
---Create an async task for a function with no return values.
|
||||
---@param func function
|
||||
---@return AsyncFunc
|
||||
function M.void(func)
|
||||
return function(...)
|
||||
return M._run(func, {
|
||||
kind = "void",
|
||||
args = { ... },
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
---Create an async task for a callback style function.
|
||||
---@param func function
|
||||
---@param nparams? integer # The number of parameters.
|
||||
---The last parameter in `func` must be the callback. For Lua functions this
|
||||
---can be derived through reflection. If `func` is an FFI procedure then
|
||||
---`nparams` is required.
|
||||
---@return AsyncFunc
|
||||
function M.wrap(func, nparams)
|
||||
if not nparams then
|
||||
local info = debug.getinfo(func, "uS")
|
||||
assert(info.what == "Lua", "Parameter count can only be derived for Lua functions!")
|
||||
nparams = info.nparams
|
||||
end
|
||||
|
||||
return function(...)
|
||||
return M._run(func, {
|
||||
nparams = nparams,
|
||||
kind = "callback",
|
||||
args = { ... },
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
---@param waitable Waitable
|
||||
---@return any ... # Any values returned by the waitable
|
||||
function M.await(waitable)
|
||||
return waitable:await()
|
||||
end
|
||||
|
||||
---Await the async function `x` with the given arguments in protected mode. `x`
|
||||
---may also be a waitable, in which case the subsequent parameters are ignored.
|
||||
---@param x AsyncFunc|Waitable # The async function or waitable.
|
||||
---@param ... any # Arguments to be applied to the `x` if it's a function.
|
||||
---@return boolean ok # `false` if the execution of `x` failed.
|
||||
---@return any result # Either the first returned value from `x` or an error message.
|
||||
---@return any ... # Any subsequent values returned from `x`.
|
||||
function M.pawait(x, ...)
|
||||
local args = tbl_pack(...)
|
||||
return pcall(function()
|
||||
if type(x) == "function" then
|
||||
return M.await(x(tbl_unpack(args)))
|
||||
else
|
||||
return x:await()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- ###############################
|
||||
-- ### VARIOUS ASYNC UTILITIES ###
|
||||
-- ###############################
|
||||
|
||||
local await = M.await
|
||||
|
||||
---Create a synchronous version of an async `void` task. Calling the resulting
|
||||
---function will block until the async task is done.
|
||||
---@param func function
|
||||
function M.sync_void(func)
|
||||
local afunc = M.void(func)
|
||||
|
||||
return function(...)
|
||||
return await(afunc(...))
|
||||
end
|
||||
end
|
||||
|
||||
---Create a synchronous version of an async `wrap` task. Calling the resulting
|
||||
---function will block until the async task is done. Any values that were
|
||||
---passed to the callback will be returned.
|
||||
---@param func function
|
||||
---@param nparams? integer
|
||||
---@return (fun(...): ...)
|
||||
function M.sync_wrap(func, nparams)
|
||||
local afunc = M.wrap(func, nparams)
|
||||
|
||||
return function(...)
|
||||
return await(afunc(...))
|
||||
end
|
||||
end
|
||||
|
||||
---Run the given async tasks concurrently, and then wait for them all to
|
||||
---terminate.
|
||||
---@param tasks (AsyncFunc|Waitable)[]
|
||||
M.join = M.void(function(tasks)
|
||||
---@type Waitable[]
|
||||
local futures = {}
|
||||
|
||||
-- Ensure all async tasks are started
|
||||
for _, cur in ipairs(tasks) do
|
||||
if cur then
|
||||
if type(cur) == "function" then
|
||||
futures[#futures+1] = cur()
|
||||
else
|
||||
---@cast cur Waitable
|
||||
futures[#futures+1] = cur
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Await all futures
|
||||
for _, future in ipairs(futures) do
|
||||
await(future)
|
||||
end
|
||||
end)
|
||||
|
||||
---Run, and await the given async tasks in sequence.
|
||||
---@param tasks (AsyncFunc|Waitable)[]
|
||||
M.chain = M.void(function(tasks)
|
||||
for _, task in ipairs(tasks) do
|
||||
if type(task) == "function" then
|
||||
---@cast task AsyncFunc
|
||||
await(task())
|
||||
else
|
||||
---@cast task Waitable
|
||||
await(task)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
---Async task that resolves after the given `timeout` ms passes.
|
||||
---@param timeout integer # Duration of the timeout (ms)
|
||||
M.timeout = M.wrap(function(timeout, callback)
|
||||
local timer = assert(uv.new_timer())
|
||||
|
||||
timer:start(
|
||||
timeout,
|
||||
0,
|
||||
function()
|
||||
if not timer:is_closing() then timer:close() end
|
||||
callback()
|
||||
end
|
||||
)
|
||||
end)
|
||||
|
||||
---Yield until the Neovim API is available.
|
||||
---@param fast_only? boolean # Only schedule if in an |api-fast| event.
|
||||
--- When this is `true`, the scheduler will resume immediately unless the
|
||||
--- editor is in an |api-fast| event. This means that the API might still be
|
||||
--- limited by other mechanisms (i.e. |textlock|).
|
||||
M.scheduler = M.wrap(function(fast_only, callback)
|
||||
if (fast_only and not vim.in_fast_event()) or not ffi.nvim_is_locked() then
|
||||
callback()
|
||||
return
|
||||
end
|
||||
|
||||
vim.schedule(callback)
|
||||
end)
|
||||
|
||||
M.schedule_now = M.wrap(vim.schedule, 1)
|
||||
|
||||
return M
|
||||
@ -0,0 +1,56 @@
|
||||
if DiffviewGlobal and DiffviewGlobal.bootstrap_done then
|
||||
return DiffviewGlobal.bootstrap_ok
|
||||
end
|
||||
|
||||
local lazy = require("diffview.lazy")
|
||||
|
||||
local EventEmitter = lazy.access("diffview.events", "EventEmitter") ---@type EventEmitter|LazyModule
|
||||
local Logger = lazy.access("diffview.logger", "Logger") ---@type Logger|LazyModule
|
||||
local config = lazy.require("diffview.config") ---@module "diffview.config"
|
||||
local diffview = lazy.require("diffview") ---@module "diffview"
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
|
||||
local uv = vim.loop
|
||||
|
||||
local function err(msg)
|
||||
msg = msg:gsub("'", "''")
|
||||
vim.cmd("echohl Error")
|
||||
vim.cmd(string.format("echom '[diffview.nvim] %s'", msg))
|
||||
vim.cmd("echohl NONE")
|
||||
end
|
||||
|
||||
_G.DiffviewGlobal = {
|
||||
bootstrap_done = true,
|
||||
bootstrap_ok = false,
|
||||
}
|
||||
|
||||
if vim.fn.has("nvim-0.7") ~= 1 then
|
||||
err(
|
||||
"Minimum required version is Neovim 0.7.0! Cannot continue."
|
||||
.. " (See ':h diffview.changelog-137')"
|
||||
)
|
||||
return false
|
||||
end
|
||||
|
||||
_G.DiffviewGlobal = {
|
||||
---Debug Levels:
|
||||
---0: NOTHING
|
||||
---1: NORMAL
|
||||
---5: LOADING
|
||||
---10: RENDERING & ASYNC
|
||||
---@diagnostic disable-next-line: missing-parameter
|
||||
debug_level = tonumber((uv.os_getenv("DEBUG_DIFFVIEW"))) or 0,
|
||||
state = {},
|
||||
bootstrap_done = true,
|
||||
bootstrap_ok = true,
|
||||
}
|
||||
|
||||
DiffviewGlobal.logger = Logger()
|
||||
DiffviewGlobal.emitter = EventEmitter()
|
||||
|
||||
DiffviewGlobal.emitter:on_any(function(e, args)
|
||||
diffview.nore_emit(e.id, utils.tbl_unpack(args))
|
||||
config.user_emitter:nore_emit(e.id, utils.tbl_unpack(args))
|
||||
end)
|
||||
|
||||
return true
|
||||
@ -0,0 +1,660 @@
|
||||
require("diffview.bootstrap")
|
||||
|
||||
---@diagnostic disable: deprecated
|
||||
local EventEmitter = require("diffview.events").EventEmitter
|
||||
local actions = require("diffview.actions")
|
||||
local lazy = require("diffview.lazy")
|
||||
|
||||
local Diff1 = lazy.access("diffview.scene.layouts.diff_1", "Diff1") ---@type Diff1|LazyModule
|
||||
local Diff2 = lazy.access("diffview.scene.layouts.diff_2", "Diff2") ---@type Diff2|LazyModule
|
||||
local Diff2Hor = lazy.access("diffview.scene.layouts.diff_2_hor", "Diff2Hor") ---@type Diff2Hor|LazyModule
|
||||
local Diff2Ver = lazy.access("diffview.scene.layouts.diff_2_ver", "Diff2Ver") ---@type Diff2Ver|LazyModule
|
||||
local Diff3 = lazy.access("diffview.scene.layouts.diff_3", "Diff3") ---@type Diff3|LazyModule
|
||||
local Diff3Hor = lazy.access("diffview.scene.layouts.diff_3_hor", "Diff3Hor") ---@type Diff3Hor|LazyModule
|
||||
local Diff3Mixed = lazy.access("diffview.scene.layouts.diff_3_mixed", "Diff3Mixed") ---@type Diff3Mixed|LazyModule
|
||||
local Diff3Ver = lazy.access("diffview.scene.layouts.diff_3_ver", "Diff3Ver") ---@type Diff3Hor|LazyModule
|
||||
local Diff4 = lazy.access("diffview.scene.layouts.diff_4", "Diff4") ---@type Diff4|LazyModule
|
||||
local Diff4Mixed = lazy.access("diffview.scene.layouts.diff_4_mixed", "Diff4Mixed") ---@type Diff4Mixed|LazyModule
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
|
||||
local M = {}
|
||||
|
||||
local setup_done = false
|
||||
|
||||
---@deprecated
|
||||
function M.diffview_callback(cb_name)
|
||||
if cb_name == "select" then
|
||||
-- Reroute deprecated action
|
||||
return actions.select_entry
|
||||
end
|
||||
return actions[cb_name]
|
||||
end
|
||||
|
||||
---@class ConfigLogOptions
|
||||
---@field single_file LogOptions
|
||||
---@field multi_file LogOptions
|
||||
|
||||
-- stylua: ignore start
|
||||
---@class DiffviewConfig
|
||||
M.defaults = {
|
||||
diff_binaries = false,
|
||||
enhanced_diff_hl = false,
|
||||
git_cmd = { "git" },
|
||||
hg_cmd = { "hg" },
|
||||
use_icons = true,
|
||||
show_help_hints = true,
|
||||
watch_index = true,
|
||||
icons = {
|
||||
folder_closed = "",
|
||||
folder_open = "",
|
||||
},
|
||||
signs = {
|
||||
fold_closed = "",
|
||||
fold_open = "",
|
||||
done = "✓",
|
||||
},
|
||||
view = {
|
||||
default = {
|
||||
layout = "diff2_horizontal",
|
||||
disable_diagnostics = false,
|
||||
winbar_info = false,
|
||||
},
|
||||
merge_tool = {
|
||||
layout = "diff3_horizontal",
|
||||
disable_diagnostics = true,
|
||||
winbar_info = true,
|
||||
},
|
||||
file_history = {
|
||||
layout = "diff2_horizontal",
|
||||
disable_diagnostics = false,
|
||||
winbar_info = false,
|
||||
},
|
||||
},
|
||||
file_panel = {
|
||||
listing_style = "tree",
|
||||
tree_options = {
|
||||
flatten_dirs = true,
|
||||
folder_statuses = "only_folded"
|
||||
},
|
||||
win_config = {
|
||||
position = "left",
|
||||
width = 35,
|
||||
win_opts = {}
|
||||
},
|
||||
},
|
||||
file_history_panel = {
|
||||
log_options = {
|
||||
---@type ConfigLogOptions
|
||||
git = {
|
||||
single_file = {
|
||||
diff_merges = "first-parent",
|
||||
follow = true,
|
||||
},
|
||||
multi_file = {
|
||||
diff_merges = "first-parent",
|
||||
},
|
||||
},
|
||||
---@type ConfigLogOptions
|
||||
hg = {
|
||||
single_file = {},
|
||||
multi_file = {},
|
||||
},
|
||||
},
|
||||
win_config = {
|
||||
position = "bottom",
|
||||
height = 16,
|
||||
win_opts = {}
|
||||
},
|
||||
},
|
||||
commit_log_panel = {
|
||||
win_config = {
|
||||
win_opts = {}
|
||||
},
|
||||
},
|
||||
default_args = {
|
||||
DiffviewOpen = {},
|
||||
DiffviewFileHistory = {},
|
||||
},
|
||||
hooks = {},
|
||||
-- Tabularize formatting pattern: `\v(\"[^"]{-}\",\ze(\s*)actions)|actions\.\w+(\(.{-}\))?,?|\{\ desc\ \=`
|
||||
keymaps = {
|
||||
disable_defaults = false, -- Disable the default keymaps
|
||||
view = {
|
||||
-- The `view` bindings are active in the diff buffers, only when the current
|
||||
-- tabpage is a Diffview.
|
||||
{ "n", "<tab>", actions.select_next_entry, { desc = "Open the diff for the next file" } },
|
||||
{ "n", "<s-tab>", actions.select_prev_entry, { desc = "Open the diff for the previous file" } },
|
||||
{ "n", "gf", actions.goto_file_edit, { desc = "Open the file in the previous tabpage" } },
|
||||
{ "n", "<C-w><C-f>", actions.goto_file_split, { desc = "Open the file in a new split" } },
|
||||
{ "n", "<C-w>gf", actions.goto_file_tab, { desc = "Open the file in a new tabpage" } },
|
||||
{ "n", "<leader>e", actions.focus_files, { desc = "Bring focus to the file panel" } },
|
||||
{ "n", "<leader>b", actions.toggle_files, { desc = "Toggle the file panel." } },
|
||||
{ "n", "g<C-x>", actions.cycle_layout, { desc = "Cycle through available layouts." } },
|
||||
{ "n", "[x", actions.prev_conflict, { desc = "In the merge-tool: jump to the previous conflict" } },
|
||||
{ "n", "]x", actions.next_conflict, { desc = "In the merge-tool: jump to the next conflict" } },
|
||||
{ "n", "<leader>co", actions.conflict_choose("ours"), { desc = "Choose the OURS version of a conflict" } },
|
||||
{ "n", "<leader>ct", actions.conflict_choose("theirs"), { desc = "Choose the THEIRS version of a conflict" } },
|
||||
{ "n", "<leader>cb", actions.conflict_choose("base"), { desc = "Choose the BASE version of a conflict" } },
|
||||
{ "n", "<leader>ca", actions.conflict_choose("all"), { desc = "Choose all the versions of a conflict" } },
|
||||
{ "n", "dx", actions.conflict_choose("none"), { desc = "Delete the conflict region" } },
|
||||
{ "n", "<leader>cO", actions.conflict_choose_all("ours"), { desc = "Choose the OURS version of a conflict for the whole file" } },
|
||||
{ "n", "<leader>cT", actions.conflict_choose_all("theirs"), { desc = "Choose the THEIRS version of a conflict for the whole file" } },
|
||||
{ "n", "<leader>cB", actions.conflict_choose_all("base"), { desc = "Choose the BASE version of a conflict for the whole file" } },
|
||||
{ "n", "<leader>cA", actions.conflict_choose_all("all"), { desc = "Choose all the versions of a conflict for the whole file" } },
|
||||
{ "n", "dX", actions.conflict_choose_all("none"), { desc = "Delete the conflict region for the whole file" } },
|
||||
unpack(actions.compat.fold_cmds),
|
||||
},
|
||||
diff1 = {
|
||||
-- Mappings in single window diff layouts
|
||||
{ "n", "g?", actions.help({ "view", "diff1" }), { desc = "Open the help panel" } },
|
||||
},
|
||||
diff2 = {
|
||||
-- Mappings in 2-way diff layouts
|
||||
{ "n", "g?", actions.help({ "view", "diff2" }), { desc = "Open the help panel" } },
|
||||
},
|
||||
diff3 = {
|
||||
-- Mappings in 3-way diff layouts
|
||||
{ { "n", "x" }, "2do", actions.diffget("ours"), { desc = "Obtain the diff hunk from the OURS version of the file" } },
|
||||
{ { "n", "x" }, "3do", actions.diffget("theirs"), { desc = "Obtain the diff hunk from the THEIRS version of the file" } },
|
||||
{ "n", "g?", actions.help({ "view", "diff3" }), { desc = "Open the help panel" } },
|
||||
},
|
||||
diff4 = {
|
||||
-- Mappings in 4-way diff layouts
|
||||
{ { "n", "x" }, "1do", actions.diffget("base"), { desc = "Obtain the diff hunk from the BASE version of the file" } },
|
||||
{ { "n", "x" }, "2do", actions.diffget("ours"), { desc = "Obtain the diff hunk from the OURS version of the file" } },
|
||||
{ { "n", "x" }, "3do", actions.diffget("theirs"), { desc = "Obtain the diff hunk from the THEIRS version of the file" } },
|
||||
{ "n", "g?", actions.help({ "view", "diff4" }), { desc = "Open the help panel" } },
|
||||
},
|
||||
file_panel = {
|
||||
{ "n", "j", actions.next_entry, { desc = "Bring the cursor to the next file entry" } },
|
||||
{ "n", "<down>", actions.next_entry, { desc = "Bring the cursor to the next file entry" } },
|
||||
{ "n", "k", actions.prev_entry, { desc = "Bring the cursor to the previous file entry" } },
|
||||
{ "n", "<up>", actions.prev_entry, { desc = "Bring the cursor to the previous file entry" } },
|
||||
{ "n", "<cr>", actions.select_entry, { desc = "Open the diff for the selected entry" } },
|
||||
{ "n", "o", actions.select_entry, { desc = "Open the diff for the selected entry" } },
|
||||
{ "n", "l", actions.select_entry, { desc = "Open the diff for the selected entry" } },
|
||||
{ "n", "<2-LeftMouse>", actions.select_entry, { desc = "Open the diff for the selected entry" } },
|
||||
{ "n", "-", actions.toggle_stage_entry, { desc = "Stage / unstage the selected entry" } },
|
||||
{ "n", "s", actions.toggle_stage_entry, { desc = "Stage / unstage the selected entry" } },
|
||||
{ "n", "S", actions.stage_all, { desc = "Stage all entries" } },
|
||||
{ "n", "U", actions.unstage_all, { desc = "Unstage all entries" } },
|
||||
{ "n", "X", actions.restore_entry, { desc = "Restore entry to the state on the left side" } },
|
||||
{ "n", "L", actions.open_commit_log, { desc = "Open the commit log panel" } },
|
||||
{ "n", "zo", actions.open_fold, { desc = "Expand fold" } },
|
||||
{ "n", "h", actions.close_fold, { desc = "Collapse fold" } },
|
||||
{ "n", "zc", actions.close_fold, { desc = "Collapse fold" } },
|
||||
{ "n", "za", actions.toggle_fold, { desc = "Toggle fold" } },
|
||||
{ "n", "zR", actions.open_all_folds, { desc = "Expand all folds" } },
|
||||
{ "n", "zM", actions.close_all_folds, { desc = "Collapse all folds" } },
|
||||
{ "n", "<c-b>", actions.scroll_view(-0.25), { desc = "Scroll the view up" } },
|
||||
{ "n", "<c-f>", actions.scroll_view(0.25), { desc = "Scroll the view down" } },
|
||||
{ "n", "<tab>", actions.select_next_entry, { desc = "Open the diff for the next file" } },
|
||||
{ "n", "<s-tab>", actions.select_prev_entry, { desc = "Open the diff for the previous file" } },
|
||||
{ "n", "gf", actions.goto_file_edit, { desc = "Open the file in the previous tabpage" } },
|
||||
{ "n", "<C-w><C-f>", actions.goto_file_split, { desc = "Open the file in a new split" } },
|
||||
{ "n", "<C-w>gf", actions.goto_file_tab, { desc = "Open the file in a new tabpage" } },
|
||||
{ "n", "i", actions.listing_style, { desc = "Toggle between 'list' and 'tree' views" } },
|
||||
{ "n", "f", actions.toggle_flatten_dirs, { desc = "Flatten empty subdirectories in tree listing style" } },
|
||||
{ "n", "R", actions.refresh_files, { desc = "Update stats and entries in the file list" } },
|
||||
{ "n", "<leader>e", actions.focus_files, { desc = "Bring focus to the file panel" } },
|
||||
{ "n", "<leader>b", actions.toggle_files, { desc = "Toggle the file panel" } },
|
||||
{ "n", "g<C-x>", actions.cycle_layout, { desc = "Cycle available layouts" } },
|
||||
{ "n", "[x", actions.prev_conflict, { desc = "Go to the previous conflict" } },
|
||||
{ "n", "]x", actions.next_conflict, { desc = "Go to the next conflict" } },
|
||||
{ "n", "g?", actions.help("file_panel"), { desc = "Open the help panel" } },
|
||||
{ "n", "<leader>cO", actions.conflict_choose_all("ours"), { desc = "Choose the OURS version of a conflict for the whole file" } },
|
||||
{ "n", "<leader>cT", actions.conflict_choose_all("theirs"), { desc = "Choose the THEIRS version of a conflict for the whole file" } },
|
||||
{ "n", "<leader>cB", actions.conflict_choose_all("base"), { desc = "Choose the BASE version of a conflict for the whole file" } },
|
||||
{ "n", "<leader>cA", actions.conflict_choose_all("all"), { desc = "Choose all the versions of a conflict for the whole file" } },
|
||||
{ "n", "dX", actions.conflict_choose_all("none"), { desc = "Delete the conflict region for the whole file" } },
|
||||
},
|
||||
file_history_panel = {
|
||||
{ "n", "g!", actions.options, { desc = "Open the option panel" } },
|
||||
{ "n", "<C-A-d>", actions.open_in_diffview, { desc = "Open the entry under the cursor in a diffview" } },
|
||||
{ "n", "y", actions.copy_hash, { desc = "Copy the commit hash of the entry under the cursor" } },
|
||||
{ "n", "L", actions.open_commit_log, { desc = "Show commit details" } },
|
||||
{ "n", "X", actions.restore_entry, { desc = "Restore file to the state from the selected entry" } },
|
||||
{ "n", "zo", actions.open_fold, { desc = "Expand fold" } },
|
||||
{ "n", "zc", actions.close_fold, { desc = "Collapse fold" } },
|
||||
{ "n", "h", actions.close_fold, { desc = "Collapse fold" } },
|
||||
{ "n", "za", actions.toggle_fold, { desc = "Toggle fold" } },
|
||||
{ "n", "zR", actions.open_all_folds, { desc = "Expand all folds" } },
|
||||
{ "n", "zM", actions.close_all_folds, { desc = "Collapse all folds" } },
|
||||
{ "n", "j", actions.next_entry, { desc = "Bring the cursor to the next file entry" } },
|
||||
{ "n", "<down>", actions.next_entry, { desc = "Bring the cursor to the next file entry" } },
|
||||
{ "n", "k", actions.prev_entry, { desc = "Bring the cursor to the previous file entry" } },
|
||||
{ "n", "<up>", actions.prev_entry, { desc = "Bring the cursor to the previous file entry" } },
|
||||
{ "n", "<cr>", actions.select_entry, { desc = "Open the diff for the selected entry" } },
|
||||
{ "n", "o", actions.select_entry, { desc = "Open the diff for the selected entry" } },
|
||||
{ "n", "l", actions.select_entry, { desc = "Open the diff for the selected entry" } },
|
||||
{ "n", "<2-LeftMouse>", actions.select_entry, { desc = "Open the diff for the selected entry" } },
|
||||
{ "n", "<c-b>", actions.scroll_view(-0.25), { desc = "Scroll the view up" } },
|
||||
{ "n", "<c-f>", actions.scroll_view(0.25), { desc = "Scroll the view down" } },
|
||||
{ "n", "<tab>", actions.select_next_entry, { desc = "Open the diff for the next file" } },
|
||||
{ "n", "<s-tab>", actions.select_prev_entry, { desc = "Open the diff for the previous file" } },
|
||||
{ "n", "gf", actions.goto_file_edit, { desc = "Open the file in the previous tabpage" } },
|
||||
{ "n", "<C-w><C-f>", actions.goto_file_split, { desc = "Open the file in a new split" } },
|
||||
{ "n", "<C-w>gf", actions.goto_file_tab, { desc = "Open the file in a new tabpage" } },
|
||||
{ "n", "<leader>e", actions.focus_files, { desc = "Bring focus to the file panel" } },
|
||||
{ "n", "<leader>b", actions.toggle_files, { desc = "Toggle the file panel" } },
|
||||
{ "n", "g<C-x>", actions.cycle_layout, { desc = "Cycle available layouts" } },
|
||||
{ "n", "g?", actions.help("file_history_panel"), { desc = "Open the help panel" } },
|
||||
},
|
||||
option_panel = {
|
||||
{ "n", "<tab>", actions.select_entry, { desc = "Change the current option" } },
|
||||
{ "n", "q", actions.close, { desc = "Close the panel" } },
|
||||
{ "n", "g?", actions.help("option_panel"), { desc = "Open the help panel" } },
|
||||
},
|
||||
help_panel = {
|
||||
{ "n", "q", actions.close, { desc = "Close help menu" } },
|
||||
{ "n", "<esc>", actions.close, { desc = "Close help menu" } },
|
||||
},
|
||||
},
|
||||
}
|
||||
-- stylua: ignore end
|
||||
|
||||
---@type EventEmitter
|
||||
M.user_emitter = EventEmitter()
|
||||
M._config = M.defaults
|
||||
|
||||
---@class GitLogOptions
|
||||
---@field follow boolean
|
||||
---@field first_parent boolean
|
||||
---@field show_pulls boolean
|
||||
---@field reflog boolean
|
||||
---@field walk_reflogs boolean
|
||||
---@field all boolean
|
||||
---@field merges boolean
|
||||
---@field no_merges boolean
|
||||
---@field reverse boolean
|
||||
---@field cherry_pick boolean
|
||||
---@field left_only boolean
|
||||
---@field right_only boolean
|
||||
---@field max_count integer
|
||||
---@field L string[]
|
||||
---@field author string
|
||||
---@field grep string
|
||||
---@field G string
|
||||
---@field S string
|
||||
---@field diff_merges string
|
||||
---@field rev_range string
|
||||
---@field base string
|
||||
---@field path_args string[]
|
||||
---@field after string
|
||||
---@field before string
|
||||
|
||||
---@class HgLogOptions
|
||||
---@field follow string
|
||||
---@field limit integer
|
||||
---@field user string
|
||||
---@field no_merges boolean
|
||||
---@field rev string
|
||||
---@field keyword string
|
||||
---@field branch string
|
||||
---@field bookmark string
|
||||
---@field include string
|
||||
---@field exclude string
|
||||
---@field path_args string[]
|
||||
|
||||
---@alias LogOptions GitLogOptions|HgLogOptions
|
||||
|
||||
M.log_option_defaults = {
|
||||
---@type GitLogOptions
|
||||
git = {
|
||||
follow = false,
|
||||
first_parent = false,
|
||||
show_pulls = false,
|
||||
reflog = false,
|
||||
walk_reflogs = false,
|
||||
all = false,
|
||||
merges = false,
|
||||
no_merges = false,
|
||||
reverse = false,
|
||||
cherry_pick = false,
|
||||
left_only = false,
|
||||
right_only = false,
|
||||
rev_range = nil,
|
||||
base = nil,
|
||||
max_count = 256,
|
||||
L = {},
|
||||
diff_merges = nil,
|
||||
author = nil,
|
||||
grep = nil,
|
||||
G = nil,
|
||||
S = nil,
|
||||
path_args = {},
|
||||
},
|
||||
---@type HgLogOptions
|
||||
hg = {
|
||||
limit = 256,
|
||||
user = nil,
|
||||
no_merges = false,
|
||||
rev = nil,
|
||||
keyword = nil,
|
||||
include = nil,
|
||||
exclude = nil,
|
||||
},
|
||||
}
|
||||
|
||||
---@return DiffviewConfig
|
||||
function M.get_config()
|
||||
if not setup_done then
|
||||
M.setup()
|
||||
end
|
||||
|
||||
return M._config
|
||||
end
|
||||
|
||||
---@param single_file boolean
|
||||
---@param t GitLogOptions|HgLogOptions
|
||||
---@param vcs "git"|"hg"
|
||||
---@return GitLogOptions|HgLogOptions
|
||||
function M.get_log_options(single_file, t, vcs)
|
||||
local log_options
|
||||
|
||||
if single_file then
|
||||
log_options = M._config.file_history_panel.log_options[vcs].single_file
|
||||
else
|
||||
log_options = M._config.file_history_panel.log_options[vcs].multi_file
|
||||
end
|
||||
|
||||
if t then
|
||||
log_options = vim.tbl_extend("force", log_options, t)
|
||||
|
||||
for k, _ in pairs(log_options) do
|
||||
if t[k] == "" then
|
||||
log_options[k] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return log_options
|
||||
end
|
||||
|
||||
---@alias LayoutName "diff1_plain"
|
||||
--- | "diff2_horizontal"
|
||||
--- | "diff2_vertical"
|
||||
--- | "diff3_horizontal"
|
||||
--- | "diff3_vertical"
|
||||
--- | "diff3_mixed"
|
||||
--- | "diff4_mixed"
|
||||
|
||||
local layout_map = {
|
||||
diff1_plain = Diff1,
|
||||
diff2_horizontal = Diff2Hor,
|
||||
diff2_vertical = Diff2Ver,
|
||||
diff3_horizontal = Diff3Hor,
|
||||
diff3_vertical = Diff3Ver,
|
||||
diff3_mixed = Diff3Mixed,
|
||||
diff4_mixed = Diff4Mixed,
|
||||
}
|
||||
|
||||
---@param layout_name LayoutName
|
||||
---@return Layout
|
||||
function M.name_to_layout(layout_name)
|
||||
assert(layout_map[layout_name], "Invalid layout name: " .. layout_name)
|
||||
|
||||
return layout_map[layout_name].__get()
|
||||
end
|
||||
|
||||
---@param layout Layout
|
||||
---@return table?
|
||||
function M.get_layout_keymaps(layout)
|
||||
if layout:instanceof(Diff1.__get()) then
|
||||
return M._config.keymaps.diff1
|
||||
elseif layout:instanceof(Diff2.__get()) then
|
||||
return M._config.keymaps.diff2
|
||||
elseif layout:instanceof(Diff3.__get()) then
|
||||
return M._config.keymaps.diff3
|
||||
elseif layout:instanceof(Diff4.__get()) then
|
||||
return M._config.keymaps.diff4
|
||||
end
|
||||
end
|
||||
|
||||
function M.find_option_keymap(t)
|
||||
for _, mapping in ipairs(t) do
|
||||
if mapping[3] and mapping[3] == actions.options then
|
||||
return mapping
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.find_help_keymap(t)
|
||||
for _, mapping in ipairs(t) do
|
||||
if type(mapping[4]) == "table" and mapping[4].desc == "Open the help panel" then
|
||||
return mapping
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param values vector
|
||||
---@param no_quote? boolean
|
||||
---@return string
|
||||
local function fmt_enum(values, no_quote)
|
||||
return table.concat(vim.tbl_map(function(v)
|
||||
return (not no_quote and type(v) == "string") and ("'" .. v .. "'") or v
|
||||
end, values), "|")
|
||||
end
|
||||
|
||||
---@param ... table
|
||||
---@return table
|
||||
function M.extend_keymaps(...)
|
||||
local argc = select("#", ...)
|
||||
local argv = { ... }
|
||||
local contexts = {}
|
||||
|
||||
for i = 1, argc do
|
||||
local cur = argv[i]
|
||||
if type(cur) == "table" then
|
||||
contexts[#contexts + 1] = { subject = cur, expanded = {} }
|
||||
end
|
||||
end
|
||||
|
||||
for _, ctx in ipairs(contexts) do
|
||||
-- Expand the normal mode maps
|
||||
for lhs, rhs in pairs(ctx.subject) do
|
||||
if type(lhs) == "string" then
|
||||
ctx.expanded["n " .. lhs] = {
|
||||
"n",
|
||||
lhs,
|
||||
rhs,
|
||||
{ silent = true, nowait = true },
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
for _, map in ipairs(ctx.subject) do
|
||||
for _, mode in ipairs(type(map[1]) == "table" and map[1] or { map[1] }) do
|
||||
ctx.expanded[mode .. " " .. map[2]] = utils.vec_join(
|
||||
mode,
|
||||
map[2],
|
||||
utils.vec_slice(map, 3)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local merged = vim.tbl_extend("force", unpack(
|
||||
vim.tbl_map(function(v)
|
||||
return v.expanded
|
||||
end, contexts)
|
||||
))
|
||||
|
||||
return vim.tbl_values(merged)
|
||||
end
|
||||
|
||||
function M.setup(user_config)
|
||||
user_config = user_config or {}
|
||||
|
||||
M._config = vim.tbl_deep_extend(
|
||||
"force",
|
||||
utils.tbl_deep_clone(M.defaults),
|
||||
user_config
|
||||
)
|
||||
---@type EventEmitter
|
||||
M.user_emitter = EventEmitter()
|
||||
|
||||
--#region DEPRECATION NOTICES
|
||||
|
||||
if type(M._config.file_panel.use_icons) ~= "nil" then
|
||||
utils.warn("'file_panel.use_icons' has been deprecated. See ':h diffview.changelog-64'.")
|
||||
end
|
||||
|
||||
-- Move old panel preoperties to win_config
|
||||
local old_win_config_spec = { "position", "width", "height" }
|
||||
for _, panel_name in ipairs({ "file_panel", "file_history_panel" }) do
|
||||
local panel_config = M._config[panel_name]
|
||||
---@cast panel_config table
|
||||
local notified = false
|
||||
|
||||
for _, option in ipairs(old_win_config_spec) do
|
||||
if panel_config[option] ~= nil then
|
||||
if not notified then
|
||||
utils.warn(
|
||||
("'%s.{%s}' has been deprecated. See ':h diffview.changelog-136'.")
|
||||
:format(panel_name, fmt_enum(old_win_config_spec, true))
|
||||
)
|
||||
notified = true
|
||||
end
|
||||
panel_config.win_config[option] = panel_config[option]
|
||||
panel_config[option] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Move old keymaps
|
||||
if user_config.key_bindings then
|
||||
M._config.keymaps = vim.tbl_deep_extend("force", M._config.keymaps, user_config.key_bindings)
|
||||
user_config.keymaps = user_config.key_bindings
|
||||
M._config.key_bindings = nil
|
||||
end
|
||||
|
||||
local user_log_options = utils.tbl_access(user_config, "file_history_panel.log_options")
|
||||
if user_log_options then
|
||||
local top_options = {
|
||||
"single_file",
|
||||
"multi_file",
|
||||
}
|
||||
for _, name in ipairs(top_options) do
|
||||
if user_log_options[name] ~= nil then
|
||||
utils.warn("Global config of 'file_panel.log_options' has been deprecated. See ':h diffview.changelog-271'.")
|
||||
end
|
||||
break
|
||||
end
|
||||
|
||||
local option_names = {
|
||||
"max_count",
|
||||
"follow",
|
||||
"all",
|
||||
"merges",
|
||||
"no_merges",
|
||||
"reverse",
|
||||
}
|
||||
for _, name in ipairs(option_names) do
|
||||
if user_log_options[name] ~= nil then
|
||||
utils.warn(
|
||||
("'file_history_panel.log_options.{%s}' has been deprecated. See ':h diffview.changelog-151'.")
|
||||
:format(fmt_enum(option_names, true))
|
||||
)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
if #M._config.git_cmd == 0 then
|
||||
M._config.git_cmd = M.defaults.git_cmd
|
||||
end
|
||||
|
||||
do
|
||||
-- Validate layouts
|
||||
local view = M._config.view
|
||||
local standard_layouts = { "diff2_horizontal", "diff2_vertical", -1 }
|
||||
local merge_layuots = {
|
||||
"diff1_plain",
|
||||
"diff3_horizontal",
|
||||
"diff3_vertical",
|
||||
"diff3_mixed",
|
||||
"diff4_mixed",
|
||||
-1
|
||||
}
|
||||
local valid_layouts = {
|
||||
default = standard_layouts,
|
||||
merge_tool = merge_layuots,
|
||||
file_history = standard_layouts,
|
||||
}
|
||||
|
||||
for _, kind in ipairs(vim.tbl_keys(valid_layouts)) do
|
||||
if not vim.tbl_contains(valid_layouts[kind], view[kind].layout) then
|
||||
utils.err(("Invalid layout name '%s' for 'view.%s'! Must be one of (%s)."):format(
|
||||
view[kind].layout,
|
||||
kind,
|
||||
fmt_enum(valid_layouts[kind])
|
||||
))
|
||||
view[kind].layout = M.defaults.view[kind].layout
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for _, name in ipairs({ "single_file", "multi_file" }) do
|
||||
for _, vcs in ipairs({ "git", "hg" }) do
|
||||
local t = M._config.file_history_panel.log_options[vcs]
|
||||
t[name] = vim.tbl_extend(
|
||||
"force",
|
||||
M.log_option_defaults[vcs],
|
||||
t[name]
|
||||
)
|
||||
for k, _ in pairs(t[name]) do
|
||||
if t[name][k] == "" then
|
||||
t[name][k] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for event, callback in pairs(M._config.hooks) do
|
||||
if type(callback) == "function" then
|
||||
M.user_emitter:on(event, function (_, ...)
|
||||
callback(...)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
if M._config.keymaps.disable_defaults then
|
||||
for name, _ in pairs(M._config.keymaps) do
|
||||
if name ~= "disable_defaults" then
|
||||
M._config.keymaps[name] = utils.tbl_access(user_config, { "keymaps", name }) or {}
|
||||
end
|
||||
end
|
||||
else
|
||||
M._config.keymaps = utils.tbl_clone(M.defaults.keymaps)
|
||||
end
|
||||
|
||||
-- Merge default and user keymaps
|
||||
for name, keymap in pairs(M._config.keymaps) do
|
||||
if type(name) == "string" and type(keymap) == "table" then
|
||||
M._config.keymaps[name] = M.extend_keymaps(
|
||||
keymap,
|
||||
utils.tbl_access(user_config, { "keymaps", name }) or {}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
-- Disable keymaps set to `false`
|
||||
for name, keymaps in pairs(M._config.keymaps) do
|
||||
if type(name) == "string" and type(keymaps) == "table" then
|
||||
for i = #keymaps, 1, -1 do
|
||||
local v = keymaps[i]
|
||||
if type(v) == "table" and not v[3] then
|
||||
table.remove(keymaps, i)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
setup_done = true
|
||||
end
|
||||
|
||||
M.actions = actions
|
||||
return M
|
||||
@ -0,0 +1,294 @@
|
||||
local lazy = require("diffview.lazy")
|
||||
local oop = require("diffview.oop")
|
||||
local async = require("diffview.async")
|
||||
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
|
||||
local await = async.await
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class Condvar : Waitable
|
||||
---@operator call : Condvar
|
||||
local Condvar = oop.create_class("Condvar", async.Waitable)
|
||||
M.Condvar = Condvar
|
||||
|
||||
function Condvar:init()
|
||||
self.handles = {}
|
||||
end
|
||||
|
||||
---@override
|
||||
Condvar.await = async.sync_wrap(function(self, callback)
|
||||
table.insert(self.handles, callback)
|
||||
end, 2)
|
||||
|
||||
function Condvar:notify_all()
|
||||
local len = #self.handles
|
||||
|
||||
for i, cb in ipairs(self.handles) do
|
||||
if i > len then break end
|
||||
cb()
|
||||
end
|
||||
|
||||
if #self.handles > len then
|
||||
self.handles = utils.vec_slice(self.handles, len + 1)
|
||||
else
|
||||
self.handles = {}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
---@class SignalConsumer : Waitable
|
||||
---@operator call : SignalConsumer
|
||||
---@field package parent Signal
|
||||
local SignalConsumer = oop.create_class("SignalConsumer", async.Waitable)
|
||||
|
||||
function SignalConsumer:init(parent)
|
||||
self.parent = parent
|
||||
end
|
||||
|
||||
---@override
|
||||
---@param self SignalConsumer
|
||||
SignalConsumer.await = async.sync_void(function(self)
|
||||
await(self.parent)
|
||||
end)
|
||||
|
||||
---Check if the signal has been emitted.
|
||||
---@return boolean
|
||||
function SignalConsumer:check()
|
||||
return self.parent:check()
|
||||
end
|
||||
|
||||
---Listen for the signal to be emitted. If the signal has already been emitted,
|
||||
---the callback is invoked immediately. The callback can potentially be called
|
||||
---multiple times if the signal is reset between emissions.
|
||||
---@see Signal.reset
|
||||
---@param callback fun(signal: Signal)
|
||||
function SignalConsumer:listen(callback)
|
||||
self.parent:listen(callback)
|
||||
end
|
||||
|
||||
function SignalConsumer:get_name()
|
||||
return self.parent:get_name()
|
||||
end
|
||||
|
||||
|
||||
---@class Signal : SignalConsumer
|
||||
---@operator call : Signal
|
||||
---@field package name string
|
||||
---@field package emitted boolean
|
||||
---@field package cond Condvar
|
||||
---@field package listeners (fun(signal: Signal))[]
|
||||
local Signal = oop.create_class("Signal", async.Waitable)
|
||||
M.Signal = Signal
|
||||
|
||||
function Signal:init(name)
|
||||
self.name = name or "UNNAMED_SIGNAL"
|
||||
self.emitted = false
|
||||
self.cond = Condvar()
|
||||
self.listeners = {}
|
||||
end
|
||||
|
||||
---@override
|
||||
---@param self Signal
|
||||
Signal.await = async.sync_void(function(self)
|
||||
if self.emitted then return end
|
||||
await(self.cond)
|
||||
end)
|
||||
|
||||
---Send the signal.
|
||||
function Signal:send()
|
||||
if self.emitted then return end
|
||||
self.emitted = true
|
||||
|
||||
for _, listener in ipairs(self.listeners) do
|
||||
listener(self)
|
||||
end
|
||||
|
||||
self.cond:notify_all()
|
||||
end
|
||||
|
||||
---Listen for the signal to be emitted. If the signal has already been emitted,
|
||||
---the callback is invoked immediately. The callback can potentially be called
|
||||
---multiple times if the signal is reset between emissions.
|
||||
---@see Signal.reset
|
||||
---@param callback fun(signal: Signal)
|
||||
function Signal:listen(callback)
|
||||
self.listeners[#self.listeners + 1] = callback
|
||||
if self.emitted then callback(self) end
|
||||
end
|
||||
|
||||
---@return SignalConsumer
|
||||
function Signal:new_consumer()
|
||||
return SignalConsumer(self)
|
||||
end
|
||||
|
||||
---Check if the signal has been emitted.
|
||||
---@return boolean
|
||||
function Signal:check()
|
||||
return self.emitted
|
||||
end
|
||||
|
||||
---Reset the signal such that it can be sent again.
|
||||
function Signal:reset()
|
||||
self.emitted = false
|
||||
end
|
||||
|
||||
function Signal:get_name()
|
||||
return self.name
|
||||
end
|
||||
|
||||
|
||||
---@class WorkPool : Waitable
|
||||
---@operator call : WorkPool
|
||||
---@field package workers table<Signal, boolean>
|
||||
local WorkPool = oop.create_class("WorkPool", async.Waitable)
|
||||
M.WorkPool = WorkPool
|
||||
|
||||
function WorkPool:init()
|
||||
self.workers = {}
|
||||
end
|
||||
|
||||
---Check in a worker. Returns a "checkout" signal that must be used to resolve
|
||||
---the work.
|
||||
---@return Signal checkout
|
||||
function WorkPool:check_in()
|
||||
local signal = Signal()
|
||||
self.workers[signal] = true
|
||||
|
||||
signal:listen(function()
|
||||
self.workers[signal] = nil
|
||||
end)
|
||||
|
||||
return signal
|
||||
end
|
||||
|
||||
function WorkPool:size()
|
||||
return #vim.tbl_keys(self.workers)
|
||||
end
|
||||
|
||||
---Wait for all workers to resolve and check out.
|
||||
---@override
|
||||
---@param self WorkPool
|
||||
WorkPool.await = async.sync_void(function(self)
|
||||
local cur = next(self.workers)
|
||||
|
||||
while cur do
|
||||
self.workers[cur] = nil
|
||||
await(cur)
|
||||
cur = next(self.workers)
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
---@class Permit : diffview.Object
|
||||
---@operator call : Permit
|
||||
---@field parent Semaphore
|
||||
local Permit = oop.create_class("Permit")
|
||||
|
||||
function Permit:init(opt)
|
||||
self.parent = opt.parent
|
||||
end
|
||||
|
||||
function Permit:destroy()
|
||||
self.parent = nil
|
||||
end
|
||||
|
||||
---@param self Permit
|
||||
function Permit:forget()
|
||||
if self.parent then
|
||||
local parent = self.parent
|
||||
self:destroy()
|
||||
parent:forget_one()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
---@class Semaphore : diffview.Object
|
||||
---@operator call : Semaphore
|
||||
---@field initial_count integer
|
||||
---@field permit_count integer
|
||||
---@field queue fun(p: Permit)[]
|
||||
local Semaphore = oop.create_class("Semaphore")
|
||||
M.Semaphore = Semaphore
|
||||
|
||||
function Semaphore:init(permit_count)
|
||||
assert(permit_count)
|
||||
self.initial_count = permit_count
|
||||
self.permit_count = permit_count
|
||||
self.queue = {}
|
||||
end
|
||||
|
||||
function Semaphore:forget_one()
|
||||
if self.permit_count == self.initial_count then return end
|
||||
|
||||
if next(self.queue) then
|
||||
local next_contractee = table.remove(self.queue, 1)
|
||||
next_contractee(Permit({ parent = self }))
|
||||
else
|
||||
self.permit_count = self.permit_count + 1
|
||||
end
|
||||
end
|
||||
|
||||
---@param self Semaphore
|
||||
---@param callback fun(permit: Permit)
|
||||
Semaphore.acquire = async.wrap(function(self, callback)
|
||||
if self.permit_count <= 0 then
|
||||
table.insert(self.queue, callback)
|
||||
return
|
||||
end
|
||||
|
||||
self.permit_count = self.permit_count - 1
|
||||
|
||||
return callback(Permit({ parent = self }))
|
||||
end)
|
||||
|
||||
|
||||
---@class CountDownLatch : Waitable
|
||||
---@operator call : CountDownLatch
|
||||
---@field initial_count integer
|
||||
---@field counter integer
|
||||
---@field sem Semaphore
|
||||
---@field condvar Condvar
|
||||
---@field count_down fun(self: CountDownLatch)
|
||||
local CountDownLatch = oop.create_class("CountDownLatch", async.Waitable)
|
||||
M.CountDownLatch = CountDownLatch
|
||||
|
||||
function CountDownLatch:init(count)
|
||||
self.initial_count = count
|
||||
self.counter = count
|
||||
self.sem = Semaphore(1)
|
||||
self.condvar = Condvar()
|
||||
end
|
||||
|
||||
function CountDownLatch:count_down()
|
||||
local permit = await(self.sem:acquire()) --[[@as Permit ]]
|
||||
|
||||
if self.counter == 0 then
|
||||
-- The counter reached 0 while we were waiting for the permit
|
||||
permit:forget()
|
||||
return
|
||||
end
|
||||
|
||||
self.counter = self.counter - 1
|
||||
permit:forget()
|
||||
|
||||
if self.counter == 0 then
|
||||
self.condvar:notify_all()
|
||||
end
|
||||
end
|
||||
|
||||
---@override
|
||||
function CountDownLatch:await()
|
||||
if self.counter == 0 then return end
|
||||
await(self.condvar)
|
||||
end
|
||||
|
||||
function CountDownLatch:reset()
|
||||
local permit = await(self.sem:acquire()) --[[@as Permit ]]
|
||||
self.counter = self.initial_count
|
||||
permit:forget()
|
||||
self.condvar:notify_all()
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,246 @@
|
||||
local async = require("diffview.async")
|
||||
local utils = require("diffview.utils")
|
||||
|
||||
local await = async.await
|
||||
local uv = vim.loop
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class Closeable
|
||||
---@field close fun() # Perform cleanup and release the associated handle.
|
||||
|
||||
---@class ManagedFn : Closeable
|
||||
---@operator call : unknown ...
|
||||
|
||||
---@param ... uv_handle_t
|
||||
function M.try_close(...)
|
||||
local args = { ... }
|
||||
|
||||
for i = 1, select("#", ...) do
|
||||
local handle = args[i]
|
||||
|
||||
if handle and not handle:is_closing() then
|
||||
handle:close()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@return ManagedFn
|
||||
local function wrap(timer, fn)
|
||||
return setmetatable({}, {
|
||||
__call = function(_, ...)
|
||||
fn(...)
|
||||
end,
|
||||
__index = {
|
||||
close = function()
|
||||
timer:stop()
|
||||
M.try_close(timer)
|
||||
end,
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
---Debounces a function on the leading edge.
|
||||
---@param ms integer Timeout in ms
|
||||
---@param fn function Function to debounce
|
||||
---@return ManagedFn # Debounced function.
|
||||
function M.debounce_leading(ms, fn)
|
||||
local timer = assert(uv.new_timer())
|
||||
local lock = false
|
||||
|
||||
return wrap(timer, function(...)
|
||||
timer:start(ms, 0, function()
|
||||
timer:stop()
|
||||
lock = false
|
||||
end)
|
||||
|
||||
if not lock then
|
||||
lock = true
|
||||
fn(...)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---Debounces a function on the trailing edge.
|
||||
---@param ms integer Timeout in ms
|
||||
---@param rush_first boolean If the managed fn is called and it's not recovering from a debounce: call the fn immediately.
|
||||
---@param fn function Function to debounce
|
||||
---@return ManagedFn # Debounced function.
|
||||
function M.debounce_trailing(ms, rush_first, fn)
|
||||
local timer = assert(uv.new_timer())
|
||||
local lock = false
|
||||
local debounced_fn, args
|
||||
|
||||
debounced_fn = wrap(timer, function(...)
|
||||
if not lock and rush_first and args == nil then
|
||||
lock = true
|
||||
fn(...)
|
||||
else
|
||||
args = utils.tbl_pack(...)
|
||||
end
|
||||
|
||||
timer:start(ms, 0, function()
|
||||
lock = false
|
||||
timer:stop()
|
||||
if args then
|
||||
local a = args
|
||||
args = nil
|
||||
fn(utils.tbl_unpack(a))
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
return debounced_fn
|
||||
end
|
||||
|
||||
---Throttles a function on the leading edge.
|
||||
---@param ms integer Timeout in ms
|
||||
---@param fn function Function to throttle
|
||||
---@return ManagedFn # throttled function.
|
||||
function M.throttle_leading(ms, fn)
|
||||
local timer = assert(uv.new_timer())
|
||||
local lock = false
|
||||
|
||||
return wrap(timer, function(...)
|
||||
if not lock then
|
||||
timer:start(ms, 0, function()
|
||||
lock = false
|
||||
timer:stop()
|
||||
end)
|
||||
|
||||
lock = true
|
||||
fn(...)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---Throttles a function on the trailing edge.
|
||||
---@param ms integer Timeout in ms
|
||||
---@param rush_first boolean If the managed fn is called and it's not recovering from a throttle: call the fn immediately.
|
||||
---@param fn function Function to throttle
|
||||
---@return ManagedFn # throttled function.
|
||||
function M.throttle_trailing(ms, rush_first, fn)
|
||||
local timer = assert(uv.new_timer())
|
||||
local lock = false
|
||||
local throttled_fn, args
|
||||
|
||||
throttled_fn = wrap(timer, function(...)
|
||||
if lock or (not rush_first and args == nil) then
|
||||
args = utils.tbl_pack(...)
|
||||
end
|
||||
|
||||
if lock then return end
|
||||
|
||||
lock = true
|
||||
|
||||
if rush_first then
|
||||
fn(...)
|
||||
end
|
||||
|
||||
timer:start(ms, 0, function()
|
||||
lock = false
|
||||
if args then
|
||||
local a = args
|
||||
args = nil
|
||||
if rush_first then
|
||||
throttled_fn(utils.tbl_unpack(a))
|
||||
else
|
||||
fn(utils.tbl_unpack(a))
|
||||
end
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
return throttled_fn
|
||||
end
|
||||
|
||||
---Throttle a function against a target framerate. The function will always be
|
||||
---called when the editor is unlocked and writing to buffers is possible.
|
||||
---@param framerate integer # Target framerate. Set to <= 0 to render whenever the scheduler is ready.
|
||||
---@param fn function
|
||||
function M.throttle_render(framerate, fn)
|
||||
local lock = false
|
||||
local use_framerate = framerate > 0
|
||||
local period = use_framerate and (1000 / framerate) * 1E6 or 0
|
||||
local throttled_fn
|
||||
local args, last
|
||||
|
||||
throttled_fn = async.void(function(...)
|
||||
args = utils.tbl_pack(...)
|
||||
if lock then return end
|
||||
|
||||
lock = true
|
||||
await(async.schedule_now())
|
||||
fn(utils.tbl_unpack(args))
|
||||
args = nil
|
||||
|
||||
if use_framerate then
|
||||
local now = uv.hrtime()
|
||||
|
||||
if last and now - last < period then
|
||||
local wait = period - (now - last)
|
||||
await(async.timeout(wait / 1E6))
|
||||
last = last + period
|
||||
else
|
||||
last = now
|
||||
end
|
||||
end
|
||||
|
||||
lock = false
|
||||
|
||||
if args ~= nil then
|
||||
throttled_fn(utils.tbl_unpack(args))
|
||||
end
|
||||
end)
|
||||
|
||||
return throttled_fn
|
||||
end
|
||||
|
||||
---Repeatedly call `func` with a fixed time delay.
|
||||
---@param func function
|
||||
---@param delay integer # Delay between executions (ms)
|
||||
---@return Closeable
|
||||
function M.set_interval(func, delay)
|
||||
local timer = assert(uv.new_timer())
|
||||
|
||||
local ret = {
|
||||
close = function()
|
||||
timer:stop()
|
||||
M.try_close(timer)
|
||||
end,
|
||||
}
|
||||
|
||||
timer:start(delay, delay, function()
|
||||
local should_close = func()
|
||||
if type(should_close) == "boolean" and should_close then
|
||||
ret.close()
|
||||
end
|
||||
end)
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
---Call `func` after a fixed time delay.
|
||||
---@param func function
|
||||
---@param delay integer # Delay until execution (ms)
|
||||
---@return Closeable
|
||||
function M.set_timeout(func, delay)
|
||||
local timer = assert(uv.new_timer())
|
||||
|
||||
local ret = {
|
||||
close = function()
|
||||
timer:stop()
|
||||
M.try_close(timer)
|
||||
end,
|
||||
}
|
||||
|
||||
timer:start(delay, 0, function()
|
||||
func()
|
||||
ret.close()
|
||||
end)
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@ -0,0 +1,225 @@
|
||||
--[[
|
||||
|
||||
An implementation of Myers' diff algorithm
|
||||
Derived from: https://github.com/Swatinem/diff
|
||||
|
||||
]]
|
||||
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@enum EditToken
|
||||
local EditToken = oop.enum({
|
||||
NOOP = 1,
|
||||
DELETE = 2,
|
||||
INSERT = 3,
|
||||
REPLACE = 4,
|
||||
})
|
||||
|
||||
---@class Diff : diffview.Object
|
||||
---@operator call : Diff
|
||||
---@field a any[]
|
||||
---@field b any[]
|
||||
---@field moda boolean[]
|
||||
---@field modb boolean[]
|
||||
---@field up table<integer, integer>
|
||||
---@field down table<integer, integer>
|
||||
---@field eql_fn function
|
||||
local Diff = oop.create_class("Diff")
|
||||
|
||||
---Diff constructor.
|
||||
---@param a any[]
|
||||
---@param b any[]
|
||||
---@param eql_fn function|nil
|
||||
function Diff:init(a, b, eql_fn)
|
||||
self.a = a
|
||||
self.b = b
|
||||
self.moda = {}
|
||||
self.modb = {}
|
||||
self.up = {}
|
||||
self.down = {}
|
||||
self.eql_fn = eql_fn or function(aa, bb)
|
||||
return aa == bb
|
||||
end
|
||||
|
||||
for i = 1, #a do
|
||||
self.moda[i] = false
|
||||
end
|
||||
for i = 1, #b do
|
||||
self.modb[i] = false
|
||||
end
|
||||
|
||||
self:lcs(1, #self.a + 1, 1, #self.b + 1)
|
||||
end
|
||||
|
||||
---@return EditToken[]
|
||||
function Diff:create_edit_script()
|
||||
local astart = 1
|
||||
local bstart = 1
|
||||
local aend = #self.moda
|
||||
local bend = #self.modb
|
||||
local script = {}
|
||||
|
||||
while astart <= aend or bstart <= bend do
|
||||
if astart <= aend and bstart <= bend then
|
||||
if not self.moda[astart] and not self.modb[bstart] then
|
||||
table.insert(script, EditToken.NOOP)
|
||||
astart = astart + 1
|
||||
bstart = bstart + 1
|
||||
goto continue
|
||||
elseif self.moda[astart] and self.modb[bstart] then
|
||||
table.insert(script, EditToken.REPLACE)
|
||||
astart = astart + 1
|
||||
bstart = bstart + 1
|
||||
goto continue
|
||||
end
|
||||
end
|
||||
|
||||
if astart <= aend and (bstart > bend or self.moda[astart]) then
|
||||
table.insert(script, EditToken.DELETE)
|
||||
astart = astart + 1
|
||||
end
|
||||
|
||||
if bstart <= bend and (astart > aend or self.modb[bstart]) then
|
||||
table.insert(script, EditToken.INSERT)
|
||||
bstart = bstart + 1
|
||||
end
|
||||
|
||||
::continue::
|
||||
end
|
||||
|
||||
return script
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param astart integer
|
||||
---@param aend integer
|
||||
---@param bstart integer
|
||||
---@param bend integer
|
||||
function Diff:lcs(astart, aend, bstart, bend)
|
||||
-- separate common head
|
||||
while astart < aend and bstart < bend and self.eql_fn(self.a[astart], self.b[bstart]) do
|
||||
astart = astart + 1
|
||||
bstart = bstart + 1
|
||||
end
|
||||
|
||||
-- separate common tail
|
||||
while astart < aend and bstart < bend and self.eql_fn(self.a[aend - 1], self.b[bend - 1]) do
|
||||
aend = aend - 1
|
||||
bend = bend - 1
|
||||
end
|
||||
|
||||
if astart == aend then
|
||||
-- only insertions
|
||||
while bstart < bend do
|
||||
self.modb[bstart] = true
|
||||
bstart = bstart + 1
|
||||
end
|
||||
elseif bend == bstart then
|
||||
-- only deletions
|
||||
while astart < aend do
|
||||
self.moda[astart] = true
|
||||
astart = astart + 1
|
||||
end
|
||||
else
|
||||
local snake = self:snake(astart, aend, bstart, bend)
|
||||
self:lcs(astart, snake.x, bstart, snake.y)
|
||||
self:lcs(snake.u, aend, snake.v, bend)
|
||||
end
|
||||
end
|
||||
|
||||
---@class Diff.Snake
|
||||
---@field x integer
|
||||
---@field y integer
|
||||
---@field u integer
|
||||
---@field v integer
|
||||
|
||||
---@private
|
||||
---@param astart integer
|
||||
---@param aend integer
|
||||
---@param bstart integer
|
||||
---@param bend integer
|
||||
---@return Diff.Snake
|
||||
function Diff:snake(astart, aend, bstart, bend)
|
||||
local N = aend - astart
|
||||
local MM = bend - bstart
|
||||
|
||||
local kdown = astart - bstart
|
||||
local kup = aend - bend
|
||||
|
||||
local delta = N - MM
|
||||
local deltaOdd = delta % 2 ~= 0
|
||||
|
||||
self.down[kdown + 1] = astart
|
||||
self.up[kup - 1] = aend
|
||||
|
||||
local Dmax = (N + MM) / 2 + 1
|
||||
|
||||
for D = 0, Dmax do
|
||||
local x, y
|
||||
|
||||
-- Forward path
|
||||
for k = kdown - D, kdown + D, 2 do
|
||||
if k == kdown - D then
|
||||
x = self.down[k + 1] -- down
|
||||
else
|
||||
x = self.down[k - 1] + 1 -- right
|
||||
if k < kdown + D and self.down[k + 1] >= x then
|
||||
x = self.down[k + 1] -- down
|
||||
end
|
||||
end
|
||||
y = x - k
|
||||
|
||||
while x < aend and y < bend and self.eql_fn(self.a[x], self.b[y]) do
|
||||
x = x + 1
|
||||
y = y + 1 -- diagonal
|
||||
end
|
||||
self.down[k] = x
|
||||
|
||||
if deltaOdd and kup - D < k and k < kup + D and self.up[k] <= self.down[k] then
|
||||
return {
|
||||
x = self.down[k],
|
||||
y = self.down[k] - k,
|
||||
u = self.up[k],
|
||||
v = self.up[k] - k,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
-- Reverse path
|
||||
for k = kup - D, kup + D, 2 do
|
||||
if k == kup + D then
|
||||
x = self.up[k - 1] -- up
|
||||
else
|
||||
x = self.up[k + 1] - 1 -- left
|
||||
if k > kup - D and self.up[k - 1] < x then
|
||||
x = self.up[k - 1] -- up
|
||||
end
|
||||
end
|
||||
y = x - k
|
||||
|
||||
while x > astart and y > bstart and self.eql_fn(self.a[x - 1], self.b[y - 1]) do
|
||||
x = x - 1
|
||||
y = y - 1 -- diagonal
|
||||
end
|
||||
self.up[k] = x
|
||||
|
||||
if not deltaOdd and kdown - D <= k and k <= kdown + D and self.up[k] <= self.down[k] then
|
||||
return {
|
||||
x = self.down[k],
|
||||
y = self.down[k] - k,
|
||||
u = self.up[k],
|
||||
v = self.up[k] - k,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
error("Unexpected state!")
|
||||
end
|
||||
|
||||
M.EditToken = EditToken
|
||||
M.Diff = Diff
|
||||
|
||||
return M
|
||||
@ -0,0 +1,237 @@
|
||||
local lazy = require("diffview.lazy")
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
|
||||
local M = {}
|
||||
|
||||
---@enum EventName
|
||||
local EventName = oop.enum({
|
||||
FILES_STAGED = 1,
|
||||
})
|
||||
|
||||
---@alias ListenerType "normal"|"once"|"any"|"any_once"
|
||||
---@alias ListenerCallback (fun(e: Event, ...): boolean?)
|
||||
|
||||
---@class Listener
|
||||
---@field type ListenerType
|
||||
---@field callback ListenerCallback The original callback
|
||||
---@field call function
|
||||
|
||||
---@class Event : diffview.Object
|
||||
---@operator call : Event
|
||||
---@field id any
|
||||
---@field propagate boolean
|
||||
local Event = oop.create_class("Event")
|
||||
|
||||
function Event:init(opt)
|
||||
self.id = opt.id
|
||||
self.propagate = true
|
||||
end
|
||||
|
||||
function Event:stop_propagation()
|
||||
self.propagate = false
|
||||
end
|
||||
|
||||
|
||||
---@class EventEmitter : diffview.Object
|
||||
---@operator call : EventEmitter
|
||||
---@field event_map table<any, Listener[]> # Registered events mapped to subscribed listeners.
|
||||
---@field any_listeners Listener[] # Listeners subscribed to all events.
|
||||
---@field emit_lock table<any, boolean>
|
||||
local EventEmitter = oop.create_class("EventEmitter")
|
||||
|
||||
---EventEmitter constructor.
|
||||
function EventEmitter:init()
|
||||
self.event_map = {}
|
||||
self.any_listeners = {}
|
||||
self.emit_lock = {}
|
||||
end
|
||||
|
||||
---Subscribe to a given event.
|
||||
---@param event_id any Event identifier.
|
||||
---@param callback ListenerCallback
|
||||
function EventEmitter:on(event_id, callback)
|
||||
if not self.event_map[event_id] then
|
||||
self.event_map[event_id] = {}
|
||||
end
|
||||
|
||||
table.insert(self.event_map[event_id], 1, {
|
||||
type = "normal",
|
||||
callback = callback,
|
||||
call = function(event, args)
|
||||
return callback(event, utils.tbl_unpack(args))
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
---Subscribe a one-shot listener to a given event.
|
||||
---@param event_id any Event identifier.
|
||||
---@param callback ListenerCallback
|
||||
function EventEmitter:once(event_id, callback)
|
||||
if not self.event_map[event_id] then
|
||||
self.event_map[event_id] = {}
|
||||
end
|
||||
|
||||
local emitted = false
|
||||
|
||||
table.insert(self.event_map[event_id], 1, {
|
||||
type = "once",
|
||||
callback = callback,
|
||||
call = function(event, args)
|
||||
if not emitted then
|
||||
emitted = true
|
||||
return callback(event, utils.tbl_unpack(args))
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
---Add a new any-listener, subscribed to all events.
|
||||
---@param callback ListenerCallback
|
||||
function EventEmitter:on_any(callback)
|
||||
table.insert(self.any_listeners, 1, {
|
||||
type = "any",
|
||||
callback = callback,
|
||||
call = function(event, args)
|
||||
return callback(event, args)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
---Add a new one-shot any-listener, subscribed to all events.
|
||||
---@param callback ListenerCallback
|
||||
function EventEmitter:once_any(callback)
|
||||
local emitted = false
|
||||
|
||||
table.insert(self.any_listeners, 1, {
|
||||
type = "any_once",
|
||||
callback = callback,
|
||||
call = function(event, args)
|
||||
if not emitted then
|
||||
emitted = true
|
||||
return callback(event, utils.tbl_unpack(args))
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
---Unsubscribe a listener. If no event is given, the listener is unsubscribed
|
||||
---from all events.
|
||||
---@param callback function
|
||||
---@param event_id? any Only unsubscribe listeners from this event.
|
||||
function EventEmitter:off(callback, event_id)
|
||||
---@type Listener[][]
|
||||
local all
|
||||
|
||||
if event_id then
|
||||
all = { self.event_map[event_id] }
|
||||
else
|
||||
all = utils.vec_join(
|
||||
vim.tbl_values(self.event_map),
|
||||
{ self.any_listeners }
|
||||
)
|
||||
end
|
||||
|
||||
for _, listeners in ipairs(all) do
|
||||
local remove = {}
|
||||
|
||||
for i, listener in ipairs(listeners) do
|
||||
if listener.callback == callback then
|
||||
remove[#remove + 1] = i
|
||||
end
|
||||
end
|
||||
|
||||
for i = #remove, 1, -1 do
|
||||
table.remove(listeners, remove[i])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Clear all listeners for a given event. If no event is given: clear all listeners.
|
||||
---@param event_id any?
|
||||
function EventEmitter:clear(event_id)
|
||||
for e, _ in pairs(self.event_map) do
|
||||
if event_id == nil or event_id == e then
|
||||
self.event_map[e] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param listeners Listener[]
|
||||
---@param event Event
|
||||
---@param args table
|
||||
---@return Listener[]
|
||||
local function filter_call(listeners, event, args)
|
||||
listeners = utils.vec_slice(listeners) --[[@as Listener[] ]]
|
||||
local result = {}
|
||||
|
||||
for i = 1, #listeners do
|
||||
local cur = listeners[i]
|
||||
local ret = cur.call(event, args)
|
||||
local discard = (type(ret) == "boolean" and ret)
|
||||
or cur.type == "once"
|
||||
or cur.type == "any_once"
|
||||
|
||||
if not discard then result[#result + 1] = cur end
|
||||
|
||||
if not event.propagate then
|
||||
for j = i + 1, #listeners do result[j] = listeners[j] end
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
---Notify all listeners subscribed to a given event.
|
||||
---@param event_id any Event identifier.
|
||||
---@param ... any Event callback args.
|
||||
function EventEmitter:emit(event_id, ...)
|
||||
if not self.emit_lock[event_id] then
|
||||
local args = utils.tbl_pack(...)
|
||||
local e = Event({ id = event_id })
|
||||
|
||||
if type(self.event_map[event_id]) == "table" then
|
||||
self.event_map[event_id] = filter_call(self.event_map[event_id], e, args)
|
||||
end
|
||||
|
||||
if e.propagate then
|
||||
self.any_listeners = filter_call(self.any_listeners, e, args)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Non-recursively notify all listeners subscribed to a given event.
|
||||
---@param event_id any Event identifier.
|
||||
---@param ... any Event callback args.
|
||||
function EventEmitter:nore_emit(event_id, ...)
|
||||
if not self.emit_lock[event_id] then
|
||||
self.emit_lock[event_id] = true
|
||||
local args = utils.tbl_pack(...)
|
||||
local e = Event({ id = event_id })
|
||||
|
||||
if type(self.event_map[event_id]) == "table" then
|
||||
self.event_map[event_id] = filter_call(self.event_map[event_id], e, args)
|
||||
end
|
||||
|
||||
if e.propagate then
|
||||
self.any_listeners = filter_call(self.any_listeners, e, args)
|
||||
end
|
||||
|
||||
self.emit_lock[event_id] = false
|
||||
end
|
||||
end
|
||||
|
||||
---Get all listeners subscribed to the given event.
|
||||
---@param event_id any Event identifier.
|
||||
---@return Listener[]?
|
||||
function EventEmitter:get(event_id)
|
||||
return self.event_map[event_id]
|
||||
end
|
||||
|
||||
M.EventName = EventName
|
||||
M.Event = Event
|
||||
M.EventEmitter = EventEmitter
|
||||
|
||||
return M
|
||||
@ -0,0 +1,50 @@
|
||||
local ffi = require("ffi")
|
||||
|
||||
local C = ffi.C
|
||||
|
||||
local M = setmetatable({}, { __index = ffi })
|
||||
|
||||
local HAS_NVIM_0_9 = vim.fn.has("nvim-0.9") == 1
|
||||
|
||||
---Check if the |textlock| is active.
|
||||
---@return boolean
|
||||
function M.nvim_is_textlocked()
|
||||
return C.textlock > 0
|
||||
end
|
||||
|
||||
---Check if the nvim API is locked for any reason.
|
||||
---See: |api-fast|, |textlock|
|
||||
---@return boolean
|
||||
function M.nvim_is_locked()
|
||||
if vim.in_fast_event() then return true end
|
||||
|
||||
if HAS_NVIM_0_9 then
|
||||
return C.textlock > 0 or C.allbuf_lock > 0 or C.expr_map_lock > 0
|
||||
end
|
||||
|
||||
return C.textlock > 0 or C.allbuf_lock > 0 or C.ex_normal_lock > 0
|
||||
end
|
||||
|
||||
ffi.cdef([[
|
||||
/// Non-zero when changing text and jumping to another window or editing another buffer is not
|
||||
/// allowed.
|
||||
extern int textlock;
|
||||
|
||||
/// Non-zero when no buffer name can be changed, no buffer can be deleted and
|
||||
/// current directory can't be changed. Used for SwapExists et al.
|
||||
extern int allbuf_lock;
|
||||
]])
|
||||
|
||||
if HAS_NVIM_0_9 then
|
||||
ffi.cdef([[
|
||||
/// Running expr mapping, prevent use of ex_normal() and text changes
|
||||
extern int expr_map_lock;
|
||||
]])
|
||||
else
|
||||
ffi.cdef([[
|
||||
/// prevent use of ex_normal()
|
||||
extern int ex_normal_lock;
|
||||
]])
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,102 @@
|
||||
local health = vim.health or require("health")
|
||||
local fmt = string.format
|
||||
|
||||
-- Polyfill deprecated health api
|
||||
if vim.fn.has("nvim-0.10") ~= 1 then
|
||||
health = {
|
||||
start = health.report_start,
|
||||
ok = health.report_ok,
|
||||
info = health.report_info,
|
||||
warn = health.report_warn,
|
||||
error = health.report_error,
|
||||
}
|
||||
end
|
||||
|
||||
local M = {}
|
||||
|
||||
M.plugin_deps = {
|
||||
{
|
||||
name = "nvim-web-devicons",
|
||||
optional = true,
|
||||
},
|
||||
}
|
||||
|
||||
---@param cmd string|string[]
|
||||
---@return string[] stdout
|
||||
---@return integer code
|
||||
local function system_list(cmd)
|
||||
local out = vim.fn.systemlist(cmd)
|
||||
return out or {}, vim.v.shell_error
|
||||
end
|
||||
|
||||
local function lualib_available(name)
|
||||
local ok, _ = pcall(require, name)
|
||||
return ok
|
||||
end
|
||||
|
||||
function M.check()
|
||||
if vim.fn.has("nvim-0.7") == 0 then
|
||||
health.error("Diffview.nvim requires Neovim 0.7.0+")
|
||||
end
|
||||
|
||||
-- LuaJIT
|
||||
if not _G.jit then
|
||||
health.error("Not running on LuaJIT! Non-JIT Lua runtimes are not officially supported by the plugin. Mileage may vary.")
|
||||
end
|
||||
|
||||
health.start("Checking plugin dependencies")
|
||||
|
||||
local missing_essential = false
|
||||
|
||||
for _, plugin in ipairs(M.plugin_deps) do
|
||||
if lualib_available(plugin.name) then
|
||||
health.ok(plugin.name .. " installed.")
|
||||
else
|
||||
if plugin.optional then
|
||||
health.warn(fmt("Optional dependency '%s' not found.", plugin.name))
|
||||
else
|
||||
missing_essential = true
|
||||
health.error(fmt("Dependency '%s' not found!", plugin.name))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
health.start("Checking VCS tools")
|
||||
|
||||
;(function()
|
||||
if missing_essential then
|
||||
health.warn("Cannot perform checks on external dependencies without all essential plugin dependencies installed!")
|
||||
return
|
||||
end
|
||||
|
||||
health.info("The plugin requires at least one of the supported VCS tools to be valid.")
|
||||
|
||||
local has_valid_adapter = false
|
||||
local adapter_kinds = {
|
||||
{ class = require("diffview.vcs.adapters.git").GitAdapter, name = "Git" },
|
||||
{ class = require("diffview.vcs.adapters.hg").HgAdapter, name = "Mercurial" },
|
||||
}
|
||||
|
||||
for _, kind in ipairs(adapter_kinds) do
|
||||
local bs = kind.class.bootstrap
|
||||
if not bs.done then kind.class.run_bootstrap() end
|
||||
|
||||
if bs.version_string then
|
||||
health.ok(fmt("%s found.", kind.name))
|
||||
end
|
||||
|
||||
if bs.ok then
|
||||
health.ok(fmt("%s is up-to-date. (%s)", kind.name, bs.version_string))
|
||||
has_valid_adapter = true
|
||||
else
|
||||
health.warn(bs.err or (kind.name .. ": Unknown error"))
|
||||
end
|
||||
end
|
||||
|
||||
if not has_valid_adapter then
|
||||
health.error("No valid VCS tool was found!")
|
||||
end
|
||||
end)()
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,497 @@
|
||||
local lazy = require("diffview.lazy")
|
||||
|
||||
local config = lazy.require("diffview.config") ---@module "diffview.config"
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
|
||||
local api = vim.api
|
||||
local web_devicons
|
||||
local icon_cache = {}
|
||||
|
||||
local M = {}
|
||||
|
||||
---@alias hl.HiValue<T> T|"NONE"
|
||||
|
||||
---@class hl.HiSpec
|
||||
---@field fg hl.HiValue<string>
|
||||
---@field bg hl.HiValue<string>
|
||||
---@field sp hl.HiValue<string>
|
||||
---@field style hl.HiValue<string>
|
||||
---@field ctermfg hl.HiValue<integer>
|
||||
---@field ctermbg hl.HiValue<integer>
|
||||
---@field cterm hl.HiValue<string>
|
||||
---@field blend hl.HiValue<integer>
|
||||
---@field default hl.HiValue<boolean> Only set values if the hl group is cleared.
|
||||
---@field link string|-1
|
||||
---@field explicit boolean All undefined fields will be cleared from the hl group.
|
||||
|
||||
---@class hl.HiLinkSpec
|
||||
---@field force boolean
|
||||
---@field default boolean
|
||||
---@field clear boolean
|
||||
|
||||
---@class hl.HlData
|
||||
---@field link string|integer
|
||||
---@field fg integer Foreground color integer
|
||||
---@field bg integer Background color integer
|
||||
---@field sp integer Special color integer
|
||||
---@field x_fg string Foreground color hex string
|
||||
---@field x_bg string Bakground color hex string
|
||||
---@field x_sp string Special color hex string
|
||||
---@field bold boolean
|
||||
---@field italic boolean
|
||||
---@field underline boolean
|
||||
---@field underlineline boolean
|
||||
---@field undercurl boolean
|
||||
---@field underdash boolean
|
||||
---@field underdot boolean
|
||||
---@field strikethrough boolean
|
||||
---@field standout boolean
|
||||
---@field reverse boolean
|
||||
---@field blend integer
|
||||
---@field default boolean
|
||||
|
||||
---@alias hl.HlAttrValue integer|boolean
|
||||
|
||||
local HAS_NVIM_0_8 = vim.fn.has("nvim-0.8") == 1
|
||||
local HAS_NVIM_0_9 = vim.fn.has("nvim-0.9") == 1
|
||||
|
||||
---@enum HlAttribute
|
||||
M.HlAttribute = {
|
||||
fg = 1,
|
||||
bg = 2,
|
||||
sp = 3,
|
||||
x_fg = 4,
|
||||
x_bg = 5,
|
||||
x_sp = 6,
|
||||
bold = 7,
|
||||
italic = 8,
|
||||
underline = 9,
|
||||
underlineline = 10,
|
||||
undercurl = 11,
|
||||
underdash = 12,
|
||||
underdot = 13,
|
||||
strikethrough = 14,
|
||||
standout = 15,
|
||||
reverse = 16,
|
||||
blend = 17,
|
||||
}
|
||||
|
||||
local style_attrs = {
|
||||
"bold",
|
||||
"italic",
|
||||
"underline",
|
||||
"underlineline",
|
||||
"undercurl",
|
||||
"underdash",
|
||||
"underdot",
|
||||
"strikethrough",
|
||||
"standout",
|
||||
"reverse",
|
||||
}
|
||||
|
||||
-- NOTE: Some atrtibutes have been renamed in v0.8.0
|
||||
if HAS_NVIM_0_8 then
|
||||
M.HlAttribute.underdashed = M.HlAttribute.underdash
|
||||
M.HlAttribute.underdash = nil
|
||||
|
||||
M.HlAttribute.underdotted = M.HlAttribute.underdot
|
||||
M.HlAttribute.underdot = nil
|
||||
|
||||
M.HlAttribute.underdouble = M.HlAttribute.underlineline
|
||||
M.HlAttribute.underlineline = nil
|
||||
|
||||
style_attrs = {
|
||||
"bold",
|
||||
"italic",
|
||||
"underline",
|
||||
"underdouble",
|
||||
"undercurl",
|
||||
"underdashed",
|
||||
"underdotted",
|
||||
"strikethrough",
|
||||
"standout",
|
||||
"reverse",
|
||||
}
|
||||
end
|
||||
|
||||
utils.add_reverse_lookup(M.HlAttribute)
|
||||
utils.add_reverse_lookup(style_attrs)
|
||||
local hlattr = M.HlAttribute
|
||||
|
||||
---@param name string Syntax group name.
|
||||
---@param no_trans? boolean Don't translate the syntax group (follow links).
|
||||
---@return hl.HlData?
|
||||
function M.get_hl(name, no_trans)
|
||||
local hl
|
||||
|
||||
if no_trans then
|
||||
if HAS_NVIM_0_9 then
|
||||
hl = api.nvim_get_hl(0, { name = name, link = true })
|
||||
else
|
||||
hl = api.nvim__get_hl_defs(0)[name]
|
||||
end
|
||||
else
|
||||
local id = api.nvim_get_hl_id_by_name(name)
|
||||
|
||||
if id then
|
||||
if HAS_NVIM_0_9 then
|
||||
hl = api.nvim_get_hl(0, { id = id, link = false })
|
||||
else
|
||||
hl = api.nvim_get_hl_by_id(id, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if hl then
|
||||
if not HAS_NVIM_0_9 then
|
||||
-- Handle renames
|
||||
if hl.foreground then hl.fg = hl.foreground; hl.foreground = nil end
|
||||
if hl.background then hl.bg = hl.background; hl.background = nil end
|
||||
if hl.special then hl.sp = hl.special; hl.special = nil end
|
||||
end
|
||||
|
||||
if hl.fg then hl.x_fg = string.format("#%06x", hl.fg) end
|
||||
if hl.bg then hl.x_bg = string.format("#%06x", hl.bg) end
|
||||
if hl.sp then hl.x_sp = string.format("#%06x", hl.sp) end
|
||||
|
||||
return hl
|
||||
end
|
||||
end
|
||||
|
||||
---@param name string Syntax group name.
|
||||
---@param attr HlAttribute|string Attribute kind.
|
||||
---@param no_trans? boolean Don't translate the syntax group (follow links).
|
||||
---@return hl.HlAttrValue?
|
||||
function M.get_hl_attr(name, attr, no_trans)
|
||||
local hl = M.get_hl(name, no_trans)
|
||||
|
||||
if type(attr) == "string" then attr = hlattr[attr] end
|
||||
|
||||
if not (hl and attr) then return end
|
||||
|
||||
return hl[hlattr[attr]]
|
||||
end
|
||||
|
||||
---@param groups string|string[] Syntax group name, or an ordered list of
|
||||
---groups where the first found value will be returned.
|
||||
---@param no_trans? boolean Don't translate the syntax group (follow links).
|
||||
---@return string?
|
||||
function M.get_fg(groups, no_trans)
|
||||
no_trans = not not no_trans
|
||||
|
||||
if type(groups) ~= "table" then groups = { groups } end
|
||||
|
||||
for _, group in ipairs(groups) do
|
||||
local v = M.get_hl_attr(group, hlattr.x_fg, no_trans) --[[@as string? ]]
|
||||
|
||||
if v then return v end
|
||||
end
|
||||
end
|
||||
|
||||
---@param groups string|string[] Syntax group name, or an ordered list of
|
||||
---groups where the first found value will be returned.
|
||||
---@param no_trans? boolean Don't translate the syntax group (follow links).
|
||||
---@return string?
|
||||
function M.get_bg(groups, no_trans)
|
||||
no_trans = not not no_trans
|
||||
|
||||
if type(groups) ~= "table" then groups = { groups } end
|
||||
|
||||
for _, group in ipairs(groups) do
|
||||
local v = M.get_hl_attr(group, hlattr.x_bg, no_trans) --[[@as string? ]]
|
||||
|
||||
if v then return v end
|
||||
end
|
||||
end
|
||||
|
||||
---@param groups string|string[] Syntax group name, or an ordered list of
|
||||
---groups where the first found value will be returned.
|
||||
---@param no_trans? boolean Don't translate the syntax group (follow links).
|
||||
---@return string?
|
||||
function M.get_style(groups, no_trans)
|
||||
no_trans = not not no_trans
|
||||
if type(groups) ~= "table" then groups = { groups } end
|
||||
|
||||
for _, group in ipairs(groups) do
|
||||
local hl = M.get_hl(group, no_trans)
|
||||
|
||||
if hl then
|
||||
local res = {}
|
||||
|
||||
for _, attr in ipairs(style_attrs) do
|
||||
if hl[attr] then table.insert(res, attr)
|
||||
end
|
||||
|
||||
if #res > 0 then
|
||||
return table.concat(res, ",")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param spec hl.HiSpec
|
||||
---@return hl.HlData
|
||||
function M.hi_spec_to_def_map(spec)
|
||||
---@type hl.HlData
|
||||
local res = {}
|
||||
local fields = { "fg", "bg", "sp", "ctermfg", "ctermbg", "default", "link" }
|
||||
|
||||
for _, field in ipairs(fields) do
|
||||
res[field] = spec[field]
|
||||
end
|
||||
|
||||
if spec.style then
|
||||
local spec_attrs = utils.add_reverse_lookup(vim.split(spec.style, ","))
|
||||
|
||||
for _, attr in ipairs(style_attrs) do
|
||||
res[attr] = spec_attrs[attr] ~= nil
|
||||
end
|
||||
end
|
||||
|
||||
return res
|
||||
end
|
||||
|
||||
---@param groups string|string[] Syntax group name or a list of group names.
|
||||
---@param opt hl.HiSpec
|
||||
function M.hi(groups, opt)
|
||||
if type(groups) ~= "table" then groups = { groups } end
|
||||
|
||||
for _, group in ipairs(groups) do
|
||||
local def_spec
|
||||
|
||||
if opt.explicit then
|
||||
def_spec = M.hi_spec_to_def_map(opt)
|
||||
else
|
||||
def_spec = M.hi_spec_to_def_map(
|
||||
vim.tbl_extend("force", M.get_hl(group, true) or {}, opt)
|
||||
)
|
||||
end
|
||||
|
||||
for k, v in pairs(def_spec) do
|
||||
if v == "NONE" then
|
||||
def_spec[k] = nil
|
||||
end
|
||||
end
|
||||
|
||||
if not HAS_NVIM_0_9 and def_spec.link then
|
||||
-- Pre 0.9 `nvim_set_hl()` could not set other attributes in combination
|
||||
-- with `link`. Furthermore, setting non-link attributes would clear the
|
||||
-- link, but this does *not* happen if you set the other attributes first
|
||||
-- (???). However, if the value of `link` is `-1`, the group will be
|
||||
-- cleared regardless (?????).
|
||||
local link = def_spec.link
|
||||
def_spec.link = nil
|
||||
|
||||
if not def_spec.default then
|
||||
api.nvim_set_hl(0, group, def_spec)
|
||||
end
|
||||
|
||||
if link ~= -1 then
|
||||
api.nvim_set_hl(0, group, { link = link, default = def_spec.default })
|
||||
end
|
||||
else
|
||||
api.nvim_set_hl(0, group, def_spec)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param from string|string[] Syntax group name or a list of group names.
|
||||
---@param to? string Syntax group name. (default: `"NONE"`)
|
||||
---@param opt? hl.HiLinkSpec
|
||||
function M.hi_link(from, to, opt)
|
||||
if to and tostring(to):upper() == "NONE" then
|
||||
---@diagnostic disable-next-line: cast-local-type
|
||||
to = -1
|
||||
end
|
||||
|
||||
opt = vim.tbl_extend("keep", opt or {}, {
|
||||
force = true,
|
||||
}) --[[@as hl.HiLinkSpec ]]
|
||||
|
||||
if type(from) ~= "table" then from = { from } end
|
||||
|
||||
for _, f in ipairs(from) do
|
||||
if opt.clear then
|
||||
if not HAS_NVIM_0_9 then
|
||||
-- Pre 0.9 `nvim_set_hl()` did not clear other attributes when `link` was set.
|
||||
api.nvim_set_hl(0, f, {})
|
||||
end
|
||||
|
||||
api.nvim_set_hl(0, f, { default = opt.default, link = to })
|
||||
|
||||
else
|
||||
-- When `clear` is not set; use our `hi()` function such that other
|
||||
-- attributes are not affected.
|
||||
M.hi(f, { default = opt.default, link = to })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Clear highlighting for a given syntax group, or all groups if no group is
|
||||
---given.
|
||||
---@param groups? string|string[]
|
||||
function M.hi_clear(groups)
|
||||
if not groups then
|
||||
vim.cmd("hi clear")
|
||||
return
|
||||
end
|
||||
|
||||
if type(groups) ~= "table" then
|
||||
groups = { groups }
|
||||
end
|
||||
|
||||
for _, g in ipairs(groups) do
|
||||
api.nvim_set_hl(0, g, {})
|
||||
end
|
||||
end
|
||||
|
||||
function M.get_file_icon(name, ext, render_data, line_idx, offset)
|
||||
if not config.get_config().use_icons then return "" end
|
||||
|
||||
if not web_devicons then
|
||||
local ok
|
||||
ok, web_devicons = pcall(require, "nvim-web-devicons")
|
||||
|
||||
if not ok then
|
||||
config.get_config().use_icons = false
|
||||
utils.warn(
|
||||
"nvim-web-devicons is required to use file icons! "
|
||||
.. "Set `use_icons = false` in your config to stop seeing this message."
|
||||
)
|
||||
|
||||
return ""
|
||||
end
|
||||
end
|
||||
|
||||
local icon, hl
|
||||
local icon_key = (name or "") .. "|&|" .. (ext or "")
|
||||
|
||||
if icon_cache[icon_key] then
|
||||
icon, hl = unpack(icon_cache[icon_key])
|
||||
else
|
||||
icon, hl = web_devicons.get_icon(name, ext, { default = true })
|
||||
icon_cache[icon_key] = { icon, hl }
|
||||
end
|
||||
|
||||
if icon then
|
||||
if hl and render_data then
|
||||
render_data:add_hl(hl, line_idx, offset, offset + string.len(icon) + 1)
|
||||
end
|
||||
|
||||
return icon .. " ", hl
|
||||
end
|
||||
|
||||
return ""
|
||||
end
|
||||
|
||||
local git_status_hl_map = {
|
||||
["A"] = "DiffviewStatusAdded",
|
||||
["?"] = "DiffviewStatusUntracked",
|
||||
["M"] = "DiffviewStatusModified",
|
||||
["R"] = "DiffviewStatusRenamed",
|
||||
["C"] = "DiffviewStatusCopied",
|
||||
["T"] = "DiffviewStatusTypeChanged",
|
||||
["U"] = "DiffviewStatusUnmerged",
|
||||
["X"] = "DiffviewStatusUnknown",
|
||||
["D"] = "DiffviewStatusDeleted",
|
||||
["B"] = "DiffviewStatusBroken",
|
||||
["!"] = "DiffviewStatusIgnored",
|
||||
}
|
||||
|
||||
function M.get_git_hl(status)
|
||||
return git_status_hl_map[status]
|
||||
end
|
||||
|
||||
function M.get_colors()
|
||||
return {
|
||||
white = M.get_fg("Normal") or "White",
|
||||
red = M.get_fg("Keyword") or "Red",
|
||||
green = M.get_fg("Character") or "Green",
|
||||
yellow = M.get_fg("PreProc") or "Yellow",
|
||||
blue = M.get_fg("Include") or "Blue",
|
||||
purple = M.get_fg("Define") or "Purple",
|
||||
cyan = M.get_fg("Conditional") or "Cyan",
|
||||
dark_red = M.get_fg("Keyword") or "DarkRed",
|
||||
orange = M.get_fg("Number") or "Orange",
|
||||
}
|
||||
end
|
||||
|
||||
function M.get_hl_groups()
|
||||
local colors = M.get_colors()
|
||||
|
||||
return {
|
||||
FilePanelTitle = { fg = M.get_fg("Label") or colors.blue, style = "bold" },
|
||||
FilePanelCounter = { fg = M.get_fg("Identifier") or colors.purple, style = "bold" },
|
||||
FilePanelFileName = { fg = M.get_fg("Normal") or colors.white },
|
||||
Dim1 = { fg = M.get_fg("Comment") or colors.white },
|
||||
Primary = { fg = M.get_fg("Function") or "Purple" },
|
||||
Secondary = { fg = M.get_fg("String") or "Orange" },
|
||||
}
|
||||
end
|
||||
|
||||
M.hl_links = {
|
||||
Normal = "Normal",
|
||||
NonText = "NonText",
|
||||
CursorLine = "CursorLine",
|
||||
WinSeparator = "WinSeparator",
|
||||
SignColumn = "Normal",
|
||||
StatusLine = "StatusLine",
|
||||
StatusLineNC = "StatusLineNC",
|
||||
EndOfBuffer = "EndOfBuffer",
|
||||
FilePanelRootPath = "DiffviewFilePanelTitle",
|
||||
FilePanelFileName = "Normal",
|
||||
FilePanelSelected = "Type",
|
||||
FilePanelPath = "Comment",
|
||||
FilePanelInsertions = "diffAdded",
|
||||
FilePanelDeletions = "diffRemoved",
|
||||
FilePanelConflicts = "DiagnosticSignWarn",
|
||||
FolderName = "Directory",
|
||||
FolderSign = "PreProc",
|
||||
Hash = "Identifier",
|
||||
Reference = "Function",
|
||||
ReflogSelector = "Special",
|
||||
StatusAdded = "diffAdded",
|
||||
StatusUntracked = "diffAdded",
|
||||
StatusModified = "diffChanged",
|
||||
StatusRenamed = "diffChanged",
|
||||
StatusCopied = "diffChanged",
|
||||
StatusTypeChange = "diffChanged",
|
||||
StatusUnmerged = "diffChanged",
|
||||
StatusUnknown = "diffRemoved",
|
||||
StatusDeleted = "diffRemoved",
|
||||
StatusBroken = "diffRemoved",
|
||||
StatusIgnored = "Comment",
|
||||
DiffAdd = "DiffAdd",
|
||||
DiffDelete = "DiffDelete",
|
||||
DiffChange = "DiffChange",
|
||||
DiffText = "DiffText",
|
||||
}
|
||||
|
||||
function M.update_diff_hl()
|
||||
local fg = M.get_fg("DiffDelete", true) or "NONE"
|
||||
local bg = M.get_bg("DiffDelete", true) or "NONE"
|
||||
local style = M.get_style("DiffDelete", true) or "NONE"
|
||||
|
||||
M.hi("DiffviewDiffAddAsDelete", { fg = fg, bg = bg, style = style })
|
||||
M.hi_link("DiffviewDiffDeleteDim", "Comment", { default = true })
|
||||
|
||||
if config.get_config().enhanced_diff_hl then
|
||||
M.hi_link("DiffviewDiffDelete", "DiffviewDiffDeleteDim")
|
||||
end
|
||||
end
|
||||
|
||||
function M.setup()
|
||||
for name, v in pairs(M.get_hl_groups()) do
|
||||
v = vim.tbl_extend("force", v, { default = true })
|
||||
M.hi("Diffview" .. name, v)
|
||||
end
|
||||
|
||||
for from, to in pairs(M.hl_links) do
|
||||
M.hi_link("Diffview" .. from, to, { default = true })
|
||||
end
|
||||
|
||||
M.update_diff_hl()
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,279 @@
|
||||
if not require("diffview.bootstrap") then
|
||||
return
|
||||
end
|
||||
|
||||
local hl = require("diffview.hl")
|
||||
local lazy = require("diffview.lazy")
|
||||
|
||||
local arg_parser = lazy.require("diffview.arg_parser") ---@module "diffview.arg_parser"
|
||||
local config = lazy.require("diffview.config") ---@module "diffview.config"
|
||||
local lib = lazy.require("diffview.lib") ---@module "diffview.lib"
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
local vcs = lazy.require("diffview.vcs") ---@module "diffview.vcs"
|
||||
|
||||
local api = vim.api
|
||||
local logger = DiffviewGlobal.logger
|
||||
local pl = lazy.access(utils, "path") ---@type PathLib
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.setup(user_config)
|
||||
config.setup(user_config or {})
|
||||
end
|
||||
|
||||
function M.init()
|
||||
-- Fix the strange behavior that "<afile>" expands non-files
|
||||
-- as file name in some cases.
|
||||
--
|
||||
-- Ref:
|
||||
-- * sindrets/diffview.nvim#369
|
||||
-- * neovim/neovim#23943
|
||||
local function get_tabnr(state)
|
||||
if vim.fn.has("nvim-0.9.2") ~= 1 then
|
||||
return tonumber(state.match)
|
||||
else
|
||||
return tonumber(state.file)
|
||||
end
|
||||
end
|
||||
|
||||
local au = api.nvim_create_autocmd
|
||||
|
||||
-- Set up highlighting
|
||||
hl.setup()
|
||||
|
||||
-- Set up autocommands
|
||||
M.augroup = api.nvim_create_augroup("diffview_nvim", {})
|
||||
au("TabEnter", {
|
||||
group = M.augroup,
|
||||
pattern = "*",
|
||||
callback = function(_)
|
||||
M.emit("tab_enter")
|
||||
end,
|
||||
})
|
||||
au("TabLeave", {
|
||||
group = M.augroup,
|
||||
pattern = "*",
|
||||
callback = function(_)
|
||||
M.emit("tab_leave")
|
||||
end,
|
||||
})
|
||||
au("TabClosed", {
|
||||
group = M.augroup,
|
||||
pattern = "*",
|
||||
callback = function(state)
|
||||
M.close(get_tabnr(state))
|
||||
end,
|
||||
})
|
||||
au("BufWritePost", {
|
||||
group = M.augroup,
|
||||
pattern = "*",
|
||||
callback = function(_)
|
||||
M.emit("buf_write_post")
|
||||
end,
|
||||
})
|
||||
au("WinClosed", {
|
||||
group = M.augroup,
|
||||
pattern = "*",
|
||||
callback = function(state)
|
||||
M.emit("win_closed", get_tabnr(state))
|
||||
end,
|
||||
})
|
||||
au("ColorScheme", {
|
||||
group = M.augroup,
|
||||
pattern = "*",
|
||||
callback = function(_)
|
||||
M.update_colors()
|
||||
end,
|
||||
})
|
||||
au("User", {
|
||||
group = M.augroup,
|
||||
pattern = "FugitiveChanged",
|
||||
callback = function(_)
|
||||
M.emit("refresh_files")
|
||||
end,
|
||||
})
|
||||
|
||||
-- Set up user autocommand emitters
|
||||
DiffviewGlobal.emitter:on("view_opened", function(_)
|
||||
api.nvim_exec_autocmds("User", { pattern = "DiffviewViewOpened", modeline = false })
|
||||
end)
|
||||
DiffviewGlobal.emitter:on("view_closed", function(_)
|
||||
api.nvim_exec_autocmds("User", { pattern = "DiffviewViewClosed", modeline = false })
|
||||
end)
|
||||
DiffviewGlobal.emitter:on("view_enter", function(_)
|
||||
api.nvim_exec_autocmds("User", { pattern = "DiffviewViewEnter", modeline = false })
|
||||
end)
|
||||
DiffviewGlobal.emitter:on("view_leave", function(_)
|
||||
api.nvim_exec_autocmds("User", { pattern = "DiffviewViewLeave", modeline = false })
|
||||
end)
|
||||
DiffviewGlobal.emitter:on("view_post_layout", function(_)
|
||||
api.nvim_exec_autocmds("User", { pattern = "DiffviewViewPostLayout", modeline = false })
|
||||
end)
|
||||
DiffviewGlobal.emitter:on("diff_buf_read", function(_)
|
||||
api.nvim_exec_autocmds("User", { pattern = "DiffviewDiffBufRead", modeline = false })
|
||||
end)
|
||||
DiffviewGlobal.emitter:on("diff_buf_win_enter", function(_)
|
||||
api.nvim_exec_autocmds("User", { pattern = "DiffviewDiffBufWinEnter", modeline = false })
|
||||
end)
|
||||
|
||||
-- Set up completion wrapper used by `vim.ui.input()`
|
||||
vim.cmd([[
|
||||
function! Diffview__ui_input_completion(...) abort
|
||||
return luaeval("DiffviewGlobal.state.current_completer(
|
||||
\ unpack(vim.fn.eval('a:000')))")
|
||||
endfunction
|
||||
]])
|
||||
end
|
||||
|
||||
---@param args string[]
|
||||
function M.open(args)
|
||||
local view = lib.diffview_open(args)
|
||||
if view then
|
||||
view:open()
|
||||
end
|
||||
end
|
||||
|
||||
---@param range? { [1]: integer, [2]: integer }
|
||||
---@param args string[]
|
||||
function M.file_history(range, args)
|
||||
local view = lib.file_history(range, args)
|
||||
if view then
|
||||
view:open()
|
||||
end
|
||||
end
|
||||
|
||||
function M.close(tabpage)
|
||||
if tabpage then
|
||||
vim.schedule(function()
|
||||
lib.dispose_stray_views()
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
local view = lib.get_current_view()
|
||||
if view then
|
||||
view:close()
|
||||
lib.dispose_view(view)
|
||||
end
|
||||
end
|
||||
|
||||
function M.completion(_, cmd_line, cur_pos)
|
||||
local ctx = arg_parser.scan(cmd_line, { cur_pos = cur_pos, allow_ex_range = true })
|
||||
local cmd = ctx.args[1]
|
||||
|
||||
if cmd and M.completers[cmd] then
|
||||
return arg_parser.process_candidates(M.completers[cmd](ctx), ctx)
|
||||
end
|
||||
end
|
||||
|
||||
---Create a temporary adapter to get relevant completions
|
||||
---@return VCSAdapter?
|
||||
function M.get_adapter()
|
||||
local cfile = pl:vim_expand("%")
|
||||
local top_indicators = utils.vec_join(
|
||||
vim.bo.buftype == ""
|
||||
and pl:absolute(cfile)
|
||||
or nil,
|
||||
pl:realpath(".")
|
||||
)
|
||||
|
||||
local err, adapter = vcs.get_adapter({ top_indicators = top_indicators })
|
||||
|
||||
if err then
|
||||
logger:warn("[completion] Failed to create adapter: " .. err)
|
||||
end
|
||||
|
||||
return adapter
|
||||
end
|
||||
|
||||
M.completers = {
|
||||
---@param ctx CmdLineContext
|
||||
DiffviewOpen = function(ctx)
|
||||
local has_rev_arg = false
|
||||
local adapter = M.get_adapter()
|
||||
|
||||
for i = 2, math.min(#ctx.args, ctx.divideridx) do
|
||||
if ctx.args[i]:sub(1, 1) ~= "-" and i ~= ctx.argidx then
|
||||
has_rev_arg = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
local candidates = {}
|
||||
|
||||
if ctx.argidx > ctx.divideridx then
|
||||
if adapter then
|
||||
utils.vec_push(candidates, unpack(adapter:path_candidates(ctx.arg_lead)))
|
||||
else
|
||||
utils.vec_push(candidates, unpack(vim.fn.getcompletion(ctx.arg_lead, "file", 0)))
|
||||
end
|
||||
elseif adapter then
|
||||
if not has_rev_arg and ctx.arg_lead:sub(1, 1) ~= "-" then
|
||||
utils.vec_push(candidates, unpack(adapter.comp.open:get_all_names()))
|
||||
utils.vec_push(candidates, unpack(adapter:rev_candidates(ctx.arg_lead, {
|
||||
accept_range = true,
|
||||
})))
|
||||
else
|
||||
utils.vec_push(candidates, unpack(
|
||||
adapter.comp.open:get_completion(ctx.arg_lead)
|
||||
or adapter.comp.open:get_all_names()
|
||||
))
|
||||
end
|
||||
end
|
||||
|
||||
return candidates
|
||||
end,
|
||||
---@param ctx CmdLineContext
|
||||
DiffviewFileHistory = function(ctx)
|
||||
local adapter = M.get_adapter()
|
||||
local candidates = {}
|
||||
|
||||
if adapter then
|
||||
utils.vec_push(candidates, unpack(
|
||||
adapter.comp.file_history:get_completion(ctx.arg_lead)
|
||||
or adapter.comp.file_history:get_all_names()
|
||||
))
|
||||
utils.vec_push(candidates, unpack(adapter:path_candidates(ctx.arg_lead)))
|
||||
else
|
||||
utils.vec_push(candidates, unpack(vim.fn.getcompletion(ctx.arg_lead, "file", 0)))
|
||||
end
|
||||
|
||||
return candidates
|
||||
end,
|
||||
}
|
||||
|
||||
function M.update_colors()
|
||||
hl.setup()
|
||||
lib.update_colors()
|
||||
end
|
||||
|
||||
local function _emit(no_recursion, event_name, ...)
|
||||
local view = lib.get_current_view()
|
||||
|
||||
if view and not view.closing:check() then
|
||||
local that = view.emitter
|
||||
local fn = no_recursion and that.nore_emit or that.emit
|
||||
fn(that, event_name, ...)
|
||||
|
||||
that = DiffviewGlobal.emitter
|
||||
fn = no_recursion and that.nore_emit or that.emit
|
||||
|
||||
if event_name == "tab_enter" then
|
||||
fn(that, "view_enter", view)
|
||||
elseif event_name == "tab_leave" then
|
||||
fn(that, "view_leave", view)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.emit(event_name, ...)
|
||||
_emit(false, event_name, ...)
|
||||
end
|
||||
|
||||
function M.nore_emit(event_name, ...)
|
||||
_emit(true, event_name, ...)
|
||||
end
|
||||
|
||||
M.init()
|
||||
|
||||
return M
|
||||
@ -0,0 +1,546 @@
|
||||
---@diagnostic disable: invisible
|
||||
local oop = require("diffview.oop")
|
||||
local async = require("diffview.async")
|
||||
local lazy = require("diffview.lazy")
|
||||
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
|
||||
local await = async.await
|
||||
local fmt = string.format
|
||||
local logger = DiffviewGlobal.logger
|
||||
local uv = vim.loop
|
||||
|
||||
local M = {}
|
||||
|
||||
---@alias diffview.Job.OnOutCallback fun(err?: string, line: string, j: diffview.Job)
|
||||
---@alias diffview.Job.OnExitCallback fun(j: diffview.Job, success: boolean, err?: string)
|
||||
---@alias diffview.Job.OnRetryCallback fun(j: diffview.Job)
|
||||
---@alias diffview.Job.FailCond fun(j: diffview.Job): boolean, string
|
||||
|
||||
---@alias StdioKind "in"|"out"|"err"
|
||||
|
||||
---@class diffview.Job : Waitable
|
||||
---@operator call: diffview.Job
|
||||
---@field command string
|
||||
---@field args string[]
|
||||
---@field cwd string
|
||||
---@field retry integer
|
||||
---@field check_status diffview.Job.FailCond
|
||||
---@field log_opt Logger.log_job.Opt
|
||||
---@field writer string|string[]
|
||||
---@field env string[]
|
||||
---@field stdout string[]
|
||||
---@field stderr string[]
|
||||
---@field handle uv_process_t
|
||||
---@field pid integer
|
||||
---@field code integer
|
||||
---@field signal integer
|
||||
---@field p_out uv_pipe_t
|
||||
---@field p_err uv_pipe_t
|
||||
---@field p_in? uv_pipe_t
|
||||
---@field buffered_std boolean
|
||||
---@field on_stdout_listeners diffview.Job.OnOutCallback[]
|
||||
---@field on_stderr_listeners diffview.Job.OnOutCallback[]
|
||||
---@field on_exit_listeners diffview.Job.OnExitCallback[]
|
||||
---@field on_retry_listeners diffview.Job.OnRetryCallback[]
|
||||
---@field _started boolean
|
||||
---@field _done boolean
|
||||
---@field _retry_count integer
|
||||
local Job = oop.create_class("Job", async.Waitable)
|
||||
|
||||
local function prepare_env(env)
|
||||
local ret = {}
|
||||
|
||||
for k, v in pairs(env) do
|
||||
table.insert(ret, k .. "=" .. v)
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
---Predefined fail conditions.
|
||||
Job.FAIL_COND = {
|
||||
---Fail on all non-zero exit codes.
|
||||
---@param j diffview.Job
|
||||
non_zero = function(j)
|
||||
return j.code == 0, fmt("Job exited with a non-zero exit code: %d", j.code)
|
||||
end,
|
||||
---Fail if there's no data in stdout.
|
||||
---@param j diffview.Job
|
||||
on_empty = function(j)
|
||||
local msg = fmt("Job expected output, but returned nothing! Code: %d", j.code)
|
||||
local n = #j.stdout
|
||||
if n == 0 or (n == 1 and j.stdout[1] == "") then return false, msg end
|
||||
return true
|
||||
end,
|
||||
}
|
||||
|
||||
function Job:init(opt)
|
||||
self.command = opt.command
|
||||
self.args = opt.args
|
||||
self.cwd = opt.cwd
|
||||
self.env = opt.env and prepare_env(opt.env) or prepare_env(uv.os_environ())
|
||||
self.retry = opt.retry or 0
|
||||
self.writer = opt.writer
|
||||
self.buffered_std = utils.sate(opt.buffered_std, true)
|
||||
self.on_stdout_listeners = {}
|
||||
self.on_stderr_listeners = {}
|
||||
self.on_exit_listeners = {}
|
||||
self.on_retry_listeners = {}
|
||||
self._started = false
|
||||
self._done = false
|
||||
self._retry_count = 0
|
||||
|
||||
self.log_opt = vim.tbl_extend("keep", opt.log_opt or {}, {
|
||||
func = "debug",
|
||||
no_stdout = true,
|
||||
debuginfo = debug.getinfo(3, "Sl"),
|
||||
})
|
||||
|
||||
if opt.fail_cond then
|
||||
if type(opt.fail_cond) == "string" then
|
||||
self.check_status = Job.FAIL_COND[opt.fail_cond]
|
||||
assert(self.check_status, fmt("Unknown fail condition: '%s'", opt.fail_cond))
|
||||
elseif type(opt.fail_cond) == "function" then
|
||||
self.check_status = opt.fail_cond
|
||||
else
|
||||
error("Invalid fail condition: " .. vim.inspect(opt.fail_cond))
|
||||
end
|
||||
else
|
||||
self.check_status = Job.FAIL_COND.non_zero
|
||||
end
|
||||
|
||||
if opt.on_stdout then self:on_stdout(opt.on_stdout) end
|
||||
if opt.on_stderr then self:on_stderr(opt.on_stderr) end
|
||||
if opt.on_exit then self:on_exit(opt.on_exit) end
|
||||
if opt.on_retry then self:on_retry(opt.on_retry) end
|
||||
end
|
||||
|
||||
---@param ... uv_handle_t
|
||||
local function try_close(...)
|
||||
local args = { ... }
|
||||
|
||||
for i = 1, select("#", ...) do
|
||||
local handle = args[i]
|
||||
if handle and not handle:is_closing() then
|
||||
handle:close()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param chunks string[]
|
||||
---@return string[] lines
|
||||
local function process_chunks(chunks)
|
||||
local data = table.concat(chunks)
|
||||
|
||||
if data == "" then
|
||||
return {}
|
||||
end
|
||||
|
||||
local has_eof = data:sub(-1) == "\n"
|
||||
local ret = vim.split(data, "\r?\n")
|
||||
if has_eof then ret[#ret] = nil end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param pipe uv_pipe_t
|
||||
---@param out string[]
|
||||
---@param err? string
|
||||
---@param data? string
|
||||
function Job:buffered_reader(pipe, out, err, data)
|
||||
if err then
|
||||
logger:error("[Job:buffered_reader()] " .. err)
|
||||
end
|
||||
|
||||
if data then
|
||||
out[#out + 1] = data
|
||||
else
|
||||
try_close(pipe)
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param pipe uv_pipe_t
|
||||
---@param out string[]
|
||||
---@param line_listeners? diffview.Job.OnOutCallback[]
|
||||
function Job:line_reader(pipe, out, line_listeners)
|
||||
local line_buffer
|
||||
|
||||
---@param err? string
|
||||
---@param data? string
|
||||
return function (err, data)
|
||||
if err then
|
||||
logger:error("[Job:line_reader()] " .. err)
|
||||
end
|
||||
|
||||
if data then
|
||||
local has_eol = data:sub(-1) == "\n"
|
||||
local lines = vim.split(data, "\r?\n")
|
||||
|
||||
lines[1] = (line_buffer or "") .. lines[1]
|
||||
line_buffer = nil
|
||||
|
||||
for i, line in ipairs(lines) do
|
||||
if not has_eol and i == #lines then
|
||||
line_buffer = line
|
||||
else
|
||||
out[#out+1] = line
|
||||
|
||||
if line_listeners then
|
||||
for _, listener in ipairs(line_listeners) do
|
||||
listener(nil, line, self)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
if line_buffer then
|
||||
out[#out+1] = line_buffer
|
||||
|
||||
if line_listeners then
|
||||
for _, listener in ipairs(line_listeners) do
|
||||
listener(nil, line_buffer, self)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
try_close(pipe)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param pipe uv_pipe_t
|
||||
---@param out string[]
|
||||
---@param kind StdioKind
|
||||
function Job:handle_reader(pipe, out, kind)
|
||||
if self.buffered_std then
|
||||
pipe:read_start(utils.bind(self.buffered_reader, self, pipe, out))
|
||||
else
|
||||
local listeners = ({
|
||||
out = self.on_stdout_listeners,
|
||||
err = self.on_stderr_listeners,
|
||||
})[kind] or {}
|
||||
pipe:read_start(self:line_reader(pipe, out, listeners))
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param pipe uv_pipe_t
|
||||
---@param data string|string[]
|
||||
function Job:handle_writer(pipe, data)
|
||||
if type(data) == "string" then
|
||||
if data:sub(-1) ~= "\n" then data = data .. "\n" end
|
||||
pipe:write(data, function(err)
|
||||
if err then
|
||||
logger:error("[Job:handle_writer()] " .. err)
|
||||
end
|
||||
|
||||
try_close(pipe)
|
||||
end)
|
||||
|
||||
else
|
||||
---@cast data string[]
|
||||
local c = #data
|
||||
|
||||
for i, s in ipairs(data) do
|
||||
if i ~= c then
|
||||
pipe:write(s .. "\n")
|
||||
else
|
||||
pipe:write(s .. "\n", function(err)
|
||||
if err then
|
||||
logger:error("[Job:handle_writer()] " .. err)
|
||||
end
|
||||
|
||||
try_close(pipe)
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
function Job:reset()
|
||||
try_close(self.handle, self.p_out, self.p_err, self.p_in)
|
||||
|
||||
self.handle = nil
|
||||
self.p_out = nil
|
||||
self.p_err = nil
|
||||
self.p_in = nil
|
||||
|
||||
self.stdout = {}
|
||||
self.stderr = {}
|
||||
self.pid = nil
|
||||
self.code = nil
|
||||
self.signal = nil
|
||||
self._started = false
|
||||
self._done = false
|
||||
end
|
||||
|
||||
---@param self diffview.Job
|
||||
---@param callback fun(success: boolean, err?: string)
|
||||
Job.start = async.wrap(function(self, callback)
|
||||
self:reset()
|
||||
|
||||
self.p_out = uv.new_pipe(false)
|
||||
self.p_err = uv.new_pipe(false)
|
||||
|
||||
assert(self.p_out and self.p_err, "Failed to create pipes!")
|
||||
|
||||
if self.writer then
|
||||
self.p_in = uv.new_pipe(false)
|
||||
assert(self.p_in, "Failed to create pipes!")
|
||||
end
|
||||
|
||||
self._started = true
|
||||
|
||||
local handle, pid
|
||||
|
||||
handle, pid = uv.spawn(self.command, {
|
||||
args = self.args,
|
||||
stdio = { self.p_in, self.p_out, self.p_err },
|
||||
cwd = self.cwd,
|
||||
env = self.env,
|
||||
hide = true,
|
||||
},
|
||||
function(code, signal)
|
||||
---@cast handle -?
|
||||
handle:close()
|
||||
self.p_out:read_stop()
|
||||
self.p_err:read_stop()
|
||||
|
||||
if not self.code then self.code = code end
|
||||
if not self.signal then self.signal = signal end
|
||||
|
||||
try_close(self.p_out, self.p_err, self.p_in)
|
||||
|
||||
if self.buffered_std then
|
||||
self.stdout = process_chunks(self.stdout)
|
||||
self.stderr = process_chunks(self.stderr)
|
||||
end
|
||||
|
||||
---@type boolean, string?
|
||||
local ok, err = self:is_success()
|
||||
local log = not self.log_opt.silent and logger or logger.mock --[[@as Logger ]]
|
||||
|
||||
if not ok then
|
||||
log:error(err)
|
||||
log:log_job(self, { func = "error", no_stdout = true, debuginfo = self.log_opt.debuginfo })
|
||||
|
||||
if self.retry > 0 then
|
||||
if self._retry_count < self.retry then
|
||||
self:do_retry(callback)
|
||||
return
|
||||
else
|
||||
log:error("All retries failed!")
|
||||
end
|
||||
end
|
||||
else
|
||||
if self._retry_count > 0 then
|
||||
log:info("Retry was successful!")
|
||||
end
|
||||
|
||||
log:log_job(self, self.log_opt)
|
||||
end
|
||||
|
||||
self._retry_count = 0
|
||||
self._done = true
|
||||
|
||||
for _, listener in ipairs(self.on_exit_listeners) do
|
||||
listener(self, ok, err)
|
||||
end
|
||||
|
||||
callback(ok, err)
|
||||
end)
|
||||
|
||||
if not handle then
|
||||
try_close(self.p_out, self.p_err, self.p_in)
|
||||
error("Failed to spawn job!")
|
||||
end
|
||||
|
||||
self.handle = handle
|
||||
self.pid = pid
|
||||
|
||||
self:handle_reader(self.p_out, self.stdout, "out")
|
||||
self:handle_reader(self.p_err, self.stderr, "err")
|
||||
|
||||
if self.p_in then
|
||||
self:handle_writer(self.p_in, self.writer)
|
||||
end
|
||||
end)
|
||||
|
||||
---@private
|
||||
---@param self diffview.Job
|
||||
---@param callback function
|
||||
Job.do_retry = async.void(function(self, callback)
|
||||
self._retry_count = self._retry_count + 1
|
||||
|
||||
if not self.log_opt.silent then
|
||||
logger:fmt_warn("(%d/%d) Retrying job...", self._retry_count, self.retry)
|
||||
end
|
||||
|
||||
await(async.timeout(1))
|
||||
|
||||
for _, listener in ipairs(self.on_retry_listeners) do
|
||||
listener(self)
|
||||
end
|
||||
|
||||
self:start(callback)
|
||||
end)
|
||||
|
||||
---@param self diffview.Job
|
||||
---@param timeout? integer # Max duration (ms) (default: 30_000)
|
||||
---@return boolean success
|
||||
---@return string? err
|
||||
function Job:sync(timeout)
|
||||
if not self:is_started() then
|
||||
self:start()
|
||||
end
|
||||
|
||||
await(async.scheduler())
|
||||
|
||||
if self:is_done() then
|
||||
return self:is_success()
|
||||
end
|
||||
|
||||
local ok, status = vim.wait(timeout or (30 * 1000), function()
|
||||
return self:is_done()
|
||||
end, 1)
|
||||
|
||||
await(async.scheduler())
|
||||
|
||||
if not ok then
|
||||
if status == -1 then
|
||||
error("Synchronous job timed out!")
|
||||
elseif status == -2 then
|
||||
error("Synchronous job got interrupted!")
|
||||
end
|
||||
|
||||
return false, "Unexpected state"
|
||||
end
|
||||
|
||||
return self:is_success()
|
||||
end
|
||||
|
||||
---@param code integer
|
||||
---@param signal? integer|uv.aliases.signals # (default: "sigterm")
|
||||
---@return 0? success
|
||||
---@return string? err_name
|
||||
---@return string? err_msg
|
||||
function Job:kill(code, signal)
|
||||
if not self.handle then return 0 end
|
||||
|
||||
if not self.handle:is_closing() then
|
||||
self.code = code
|
||||
self.signal = signal
|
||||
return self.handle:kill(signal or "sigterm")
|
||||
end
|
||||
|
||||
return 0
|
||||
end
|
||||
|
||||
---@override
|
||||
---@param self diffview.Job
|
||||
---@param callback fun(success: boolean, err?: string)
|
||||
Job.await = async.sync_wrap(function(self, callback)
|
||||
if self:is_done() then
|
||||
callback(self:is_success())
|
||||
elseif self:is_running() then
|
||||
self:on_exit(function(_, ...) callback(...) end)
|
||||
else
|
||||
callback(await(self:start()))
|
||||
end
|
||||
end)
|
||||
|
||||
---@param jobs diffview.Job[]
|
||||
function Job.start_all(jobs)
|
||||
for _, job in ipairs(jobs) do
|
||||
job:start()
|
||||
end
|
||||
end
|
||||
|
||||
---@param jobs diffview.Job[]
|
||||
---@param callback fun(success: boolean, errors?: string[])
|
||||
Job.join = async.wrap(function(jobs, callback)
|
||||
-- Start by ensuring all jobs are running
|
||||
for _, job in ipairs(jobs) do
|
||||
if not job:is_started() then
|
||||
job:start()
|
||||
end
|
||||
end
|
||||
|
||||
local success, errors = true, {}
|
||||
|
||||
for _, job in ipairs(jobs) do
|
||||
local ok, err = await(job)
|
||||
if not ok then
|
||||
success = false
|
||||
errors[#errors + 1] = err
|
||||
end
|
||||
end
|
||||
|
||||
callback(success, not success and errors or nil)
|
||||
end)
|
||||
|
||||
---@param jobs diffview.Job[]
|
||||
Job.chain = async.void(function(jobs)
|
||||
for _, job in ipairs(jobs) do
|
||||
await(job)
|
||||
end
|
||||
end)
|
||||
|
||||
---Subscribe to stdout data. Only used if `buffered_std=false`.
|
||||
---@param callback diffview.Job.OnOutCallback
|
||||
function Job:on_stdout(callback)
|
||||
table.insert(self.on_stdout_listeners, callback)
|
||||
|
||||
if not self:is_started() then
|
||||
self.buffered_std = false
|
||||
end
|
||||
end
|
||||
|
||||
---Subscribe to stderr data. Only used if `buffered_std=false`.
|
||||
---@param callback diffview.Job.OnOutCallback
|
||||
function Job:on_stderr(callback)
|
||||
table.insert(self.on_stderr_listeners, callback)
|
||||
|
||||
if not self:is_running() then
|
||||
self.buffered_std = false
|
||||
end
|
||||
end
|
||||
|
||||
---@param callback diffview.Job.OnExitCallback
|
||||
function Job:on_exit(callback)
|
||||
table.insert(self.on_exit_listeners, callback)
|
||||
end
|
||||
|
||||
---@param callback diffview.Job.OnRetryCallback
|
||||
function Job:on_retry(callback)
|
||||
table.insert(self.on_retry_listeners, callback)
|
||||
end
|
||||
|
||||
---@return boolean success
|
||||
---@return string? err
|
||||
function Job:is_success()
|
||||
local ok, err = self:check_status()
|
||||
if not ok then return false, err end
|
||||
return true
|
||||
end
|
||||
|
||||
function Job:is_done()
|
||||
return self._done
|
||||
end
|
||||
|
||||
function Job:is_started()
|
||||
return self._started
|
||||
end
|
||||
|
||||
function Job:is_running()
|
||||
return self:is_started() and not self:is_done()
|
||||
end
|
||||
|
||||
M.Job = Job
|
||||
|
||||
return M
|
||||
@ -0,0 +1,125 @@
|
||||
local fmt = string.format
|
||||
|
||||
local lazy = {}
|
||||
|
||||
---@class LazyModule : { [string] : unknown }
|
||||
---@field __get fun(): unknown Load the module if needed, and return it.
|
||||
---@field __loaded boolean Indicates that the module has been loaded.
|
||||
|
||||
---Create a table the triggers a given handler every time it's accessed or
|
||||
---called, until the handler returns a table. Once the handler has returned a
|
||||
---table, any subsequent accessing of the wrapper will instead access the table
|
||||
---returned from the handler.
|
||||
---@param t any
|
||||
---@param handler fun(t: any): table?
|
||||
---@return LazyModule
|
||||
function lazy.wrap(t, handler)
|
||||
local export
|
||||
|
||||
local ret = {
|
||||
__get = function()
|
||||
if export == nil then
|
||||
---@cast handler function
|
||||
export = handler(t)
|
||||
end
|
||||
|
||||
return export
|
||||
end,
|
||||
__loaded = function()
|
||||
return export ~= nil
|
||||
end,
|
||||
}
|
||||
|
||||
return setmetatable(ret, {
|
||||
__index = function(_, key)
|
||||
if export == nil then ret.__get() end
|
||||
---@cast export table
|
||||
return export[key]
|
||||
end,
|
||||
__newindex = function(_, key, value)
|
||||
if export == nil then ret.__get() end
|
||||
export[key] = value
|
||||
end,
|
||||
__call = function(_, ...)
|
||||
if export == nil then ret.__get() end
|
||||
---@cast export table
|
||||
return export(...)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
---Will only require the module after first either indexing, or calling it.
|
||||
---
|
||||
---You can pass a handler function to process the module in some way before
|
||||
---returning it. This is useful i.e. if you're trying to require the result of
|
||||
---an exported function.
|
||||
---
|
||||
---Example:
|
||||
---
|
||||
---```lua
|
||||
--- local foo = require("bar")
|
||||
--- local foo = lazy.require("bar")
|
||||
---
|
||||
--- local foo = require("bar").baz({ qux = true })
|
||||
--- local foo = lazy.require("bar", function(module)
|
||||
--- return module.baz({ qux = true })
|
||||
--- end)
|
||||
---```
|
||||
---@param require_path string
|
||||
---@param handler? fun(module: any): any
|
||||
---@return LazyModule
|
||||
function lazy.require(require_path, handler)
|
||||
local use_handler = type(handler) == "function"
|
||||
|
||||
return lazy.wrap(require_path, function(s)
|
||||
if use_handler then
|
||||
---@cast handler function
|
||||
return handler(require(s))
|
||||
end
|
||||
return require(s)
|
||||
end)
|
||||
end
|
||||
|
||||
---Lazily access a table value. If `x` is a string, it's treated as a lazy
|
||||
---require.
|
||||
---
|
||||
---Example:
|
||||
---
|
||||
---```lua
|
||||
--- -- table:
|
||||
--- local foo = bar.baz.qux.quux
|
||||
--- local foo = lazy.access(bar, "baz.qux.quux")
|
||||
--- local foo = lazy.access(bar, { "baz", "qux", "quux" })
|
||||
---
|
||||
--- -- require:
|
||||
--- local foo = require("bar").baz.qux.quux
|
||||
--- local foo = lazy.access("bar", "baz.qux.quux")
|
||||
--- local foo = lazy.access("bar", { "baz", "qux", "quux" })
|
||||
---```
|
||||
---@param x table|string Either the table to be accessed, or a module require path.
|
||||
---@param access_path string|string[] Either a `.` separated string of table keys, or a list.
|
||||
---@return LazyModule
|
||||
function lazy.access(x, access_path)
|
||||
local keys = type(access_path) == "table"
|
||||
and access_path
|
||||
or vim.split(access_path --[[@as string ]], ".", { plain = true })
|
||||
|
||||
local handler = function(module)
|
||||
local export = module
|
||||
|
||||
for _, key in ipairs(keys) do
|
||||
export = export[key]
|
||||
assert(export ~= nil, fmt("Failed to lazy-access! No key '%s' in table!", key))
|
||||
end
|
||||
|
||||
return export
|
||||
end
|
||||
|
||||
if type(x) == "string" then
|
||||
return lazy.require(x, handler)
|
||||
else
|
||||
return lazy.wrap(x, handler)
|
||||
end
|
||||
end
|
||||
|
||||
return lazy
|
||||
@ -0,0 +1,233 @@
|
||||
local lazy = require("diffview.lazy")
|
||||
|
||||
local DiffView = lazy.access("diffview.scene.views.diff.diff_view", "DiffView") ---@type DiffView|LazyModule
|
||||
local FileHistoryView = lazy.access("diffview.scene.views.file_history.file_history_view", "FileHistoryView") ---@type FileHistoryView|LazyModule
|
||||
local StandardView = lazy.access("diffview.scene.views.standard.standard_view", "StandardView") ---@type StandardView|LazyModule
|
||||
local arg_parser = lazy.require("diffview.arg_parser") ---@module "diffview.arg_parser"
|
||||
local config = lazy.require("diffview.config") ---@module "diffview.config"
|
||||
local vcs = lazy.require("diffview.vcs") ---@module "diffview.vcs"
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
|
||||
local api = vim.api
|
||||
local logger = DiffviewGlobal.logger
|
||||
|
||||
local M = {}
|
||||
|
||||
---@type View[]
|
||||
M.views = {}
|
||||
|
||||
function M.diffview_open(args)
|
||||
local default_args = config.get_config().default_args.DiffviewOpen
|
||||
local argo = arg_parser.parse(utils.flatten({ default_args, args }))
|
||||
local rev_arg = argo.args[1]
|
||||
|
||||
logger:info("[command call] :DiffviewOpen " .. table.concat(utils.flatten({
|
||||
default_args,
|
||||
args,
|
||||
}), " "))
|
||||
|
||||
local err, adapter = vcs.get_adapter({
|
||||
cmd_ctx = {
|
||||
path_args = argo.post_args,
|
||||
cpath = argo:get_flag("C", { no_empty = true, expand = true }),
|
||||
},
|
||||
})
|
||||
|
||||
if err then
|
||||
utils.err(err)
|
||||
return
|
||||
end
|
||||
|
||||
---@cast adapter -?
|
||||
|
||||
local opts = adapter:diffview_options(argo)
|
||||
|
||||
if opts == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local v = DiffView({
|
||||
adapter = adapter,
|
||||
rev_arg = rev_arg,
|
||||
path_args = adapter.ctx.path_args,
|
||||
left = opts.left,
|
||||
right = opts.right,
|
||||
options = opts.options,
|
||||
})
|
||||
|
||||
if not v:is_valid() then
|
||||
return
|
||||
end
|
||||
|
||||
table.insert(M.views, v)
|
||||
logger:debug("DiffView instantiation successful!")
|
||||
|
||||
return v
|
||||
end
|
||||
|
||||
---@param range? { [1]: integer, [2]: integer }
|
||||
---@param args string[]
|
||||
function M.file_history(range, args)
|
||||
local default_args = config.get_config().default_args.DiffviewFileHistory
|
||||
local argo = arg_parser.parse(utils.flatten({ default_args, args }))
|
||||
|
||||
logger:info("[command call] :DiffviewFileHistory " .. table.concat(utils.flatten({
|
||||
default_args,
|
||||
args,
|
||||
}), " "))
|
||||
|
||||
local err, adapter = vcs.get_adapter({
|
||||
cmd_ctx = {
|
||||
path_args = argo.args,
|
||||
cpath = argo:get_flag("C", { no_empty = true, expand = true }),
|
||||
},
|
||||
})
|
||||
|
||||
if err then
|
||||
utils.err(err)
|
||||
return
|
||||
end
|
||||
|
||||
---@cast adapter -?
|
||||
|
||||
local log_options = adapter:file_history_options(range, adapter.ctx.path_args, argo)
|
||||
|
||||
if log_options == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local v = FileHistoryView({
|
||||
adapter = adapter,
|
||||
log_options = log_options,
|
||||
})
|
||||
|
||||
if not v:is_valid() then
|
||||
return
|
||||
end
|
||||
|
||||
table.insert(M.views, v)
|
||||
logger:debug("FileHistoryView instantiation successful!")
|
||||
|
||||
return v
|
||||
end
|
||||
|
||||
---@param view View
|
||||
function M.add_view(view)
|
||||
table.insert(M.views, view)
|
||||
end
|
||||
|
||||
---@param view View
|
||||
function M.dispose_view(view)
|
||||
for j, v in ipairs(M.views) do
|
||||
if v == view then
|
||||
table.remove(M.views, j)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Close and dispose of views that have no tabpage.
|
||||
function M.dispose_stray_views()
|
||||
local tabpage_map = {}
|
||||
for _, id in ipairs(api.nvim_list_tabpages()) do
|
||||
tabpage_map[id] = true
|
||||
end
|
||||
|
||||
local dispose = {}
|
||||
for _, view in ipairs(M.views) do
|
||||
if not tabpage_map[view.tabpage] then
|
||||
-- Need to schedule here because the tabnr's don't update fast enough.
|
||||
vim.schedule(function()
|
||||
view:close()
|
||||
end)
|
||||
table.insert(dispose, view)
|
||||
end
|
||||
end
|
||||
|
||||
for _, view in ipairs(dispose) do
|
||||
M.dispose_view(view)
|
||||
end
|
||||
end
|
||||
|
||||
---Get the currently open Diffview.
|
||||
---@return View?
|
||||
function M.get_current_view()
|
||||
local tabpage = api.nvim_get_current_tabpage()
|
||||
for _, view in ipairs(M.views) do
|
||||
if view.tabpage == tabpage then
|
||||
return view
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
function M.tabpage_to_view(tabpage)
|
||||
for _, view in ipairs(M.views) do
|
||||
if view.tabpage == tabpage then
|
||||
return view
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Get the first tabpage that is not a view. Tries the previous tabpage first.
|
||||
---If there are no non-view tabpages: returns nil.
|
||||
---@return number|nil
|
||||
function M.get_prev_non_view_tabpage()
|
||||
local tabs = api.nvim_list_tabpages()
|
||||
if #tabs > 1 then
|
||||
local seen = {}
|
||||
for _, view in ipairs(M.views) do
|
||||
seen[view.tabpage] = true
|
||||
end
|
||||
|
||||
local prev_tab = utils.tabnr_to_id(vim.fn.tabpagenr("#")) or -1
|
||||
if api.nvim_tabpage_is_valid(prev_tab) and not seen[prev_tab] then
|
||||
return prev_tab
|
||||
else
|
||||
for _, id in ipairs(tabs) do
|
||||
if not seen[id] then
|
||||
return id
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@param ignore? vcs.File[]
|
||||
---@return boolean
|
||||
function M.is_buf_in_use(bufnr, ignore)
|
||||
local ignore_map = ignore and utils.vec_slice(ignore) or {}
|
||||
utils.add_reverse_lookup(ignore_map)
|
||||
|
||||
for _, view in ipairs(M.views) do
|
||||
if view:instanceof(StandardView.__get()) then
|
||||
---@cast view StandardView
|
||||
|
||||
for _, file in ipairs(view.cur_entry and view.cur_entry.layout:files() or {}) do
|
||||
if file:is_valid() and file.bufnr == bufnr then
|
||||
if not ignore_map[file] then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function M.update_colors()
|
||||
for _, view in ipairs(M.views) do
|
||||
if view:instanceof(StandardView.__get()) then
|
||||
---@cast view StandardView
|
||||
if view.panel:buf_loaded() then
|
||||
view.panel:render()
|
||||
view.panel:redraw()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,411 @@
|
||||
local async = require("diffview.async")
|
||||
local lazy = require("diffview.lazy")
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
local Mock = lazy.access("diffview.mock", "Mock") ---@type Mock|LazyModule
|
||||
local Semaphore = lazy.access("diffview.control", "Semaphore") ---@type Semaphore|LazyModule
|
||||
local loop = lazy.require("diffview.debounce") ---@module "diffview.debounce"
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
|
||||
local api = vim.api
|
||||
local await, pawait = async.await, async.pawait
|
||||
local fmt = string.format
|
||||
local pl = lazy.access(utils, "path") ---@type PathLib
|
||||
local uv = vim.loop
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class Logger.TimeOfDay
|
||||
---@field hours integer
|
||||
---@field mins integer
|
||||
---@field secs integer
|
||||
---@field micros integer
|
||||
---@field tz string
|
||||
---@field timestamp integer
|
||||
|
||||
---@class Logger.Time
|
||||
---@field timestamp integer # Unix time stamp
|
||||
---@field micros integer # Microsecond offset
|
||||
|
||||
---Get high resolution time of day
|
||||
---@param time? Logger.Time
|
||||
---@return Logger.TimeOfDay
|
||||
local function time_of_day(time)
|
||||
local secs, micros
|
||||
|
||||
if time then
|
||||
secs, micros = time.timestamp, (time.micros or 0)
|
||||
else
|
||||
secs, micros = uv.gettimeofday()
|
||||
assert(secs, micros)
|
||||
end
|
||||
|
||||
local tzs = os.date("%z", secs) --[[@as string ]]
|
||||
local sign = tzs:match("[+-]") == "-" and -1 or 1
|
||||
local tz_h, tz_m = tzs:match("[+-]?(%d%d)(%d%d)")
|
||||
tz_h = tz_h * sign
|
||||
tz_m = tz_m * sign
|
||||
|
||||
local ret = {}
|
||||
ret.hours = math.floor(((secs / (60 * 60)) % 24) + tz_h)
|
||||
ret.mins = math.floor(((secs / 60) % 60) + tz_m)
|
||||
ret.secs = (secs % 60)
|
||||
ret.micros = micros
|
||||
ret.tz = tzs
|
||||
ret.timestamp = secs
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
---@alias Logger.LogFunc fun(self: Logger, ...)
|
||||
---@alias Logger.FmtLogFunc fun(self: Logger, formatstring: string, ...)
|
||||
---@alias Logger.LazyLogFunc fun(self: Logger, work: (fun(): ...))
|
||||
|
||||
---@class Logger.Context
|
||||
---@field debuginfo debuginfo
|
||||
---@field time Logger.Time
|
||||
---@field label string
|
||||
|
||||
---@class Logger : diffview.Object
|
||||
---@operator call : Logger
|
||||
---@field private outfile_status Logger.OutfileStatus
|
||||
---@field private level integer # Max level. Messages of higher level will be ignored. NOTE: Higher level -> lower severity.
|
||||
---@field private msg_buffer (string|function)[]
|
||||
---@field private msg_sem Semaphore
|
||||
---@field private batch_interval integer # Minimum time (ms) between each time batched messages are written to the output file.
|
||||
---@field private batch_handle? Closeable
|
||||
---@field private ctx? Logger.Context
|
||||
---@field plugin string
|
||||
---@field outfile string
|
||||
---@field trace Logger.LogFunc
|
||||
---@field debug Logger.LogFunc
|
||||
---@field info Logger.LogFunc
|
||||
---@field warn Logger.LogFunc
|
||||
---@field error Logger.LogFunc
|
||||
---@field fatal Logger.LogFunc
|
||||
---@field fmt_trace Logger.FmtLogFunc
|
||||
---@field fmt_debug Logger.FmtLogFunc
|
||||
---@field fmt_info Logger.FmtLogFunc
|
||||
---@field fmt_warn Logger.FmtLogFunc
|
||||
---@field fmt_error Logger.FmtLogFunc
|
||||
---@field fmt_fatal Logger.FmtLogFunc
|
||||
---@field lazy_trace Logger.LazyLogFunc
|
||||
---@field lazy_debug Logger.LazyLogFunc
|
||||
---@field lazy_info Logger.LazyLogFunc
|
||||
---@field lazy_warn Logger.LazyLogFunc
|
||||
---@field lazy_error Logger.LazyLogFunc
|
||||
---@field lazy_fatal Logger.LazyLogFunc
|
||||
local Logger = oop.create_class("Logger")
|
||||
|
||||
---@enum Logger.OutfileStatus
|
||||
Logger.OutfileStatus = oop.enum({
|
||||
UNKNOWN = 1,
|
||||
READY = 2,
|
||||
ERROR = 3,
|
||||
})
|
||||
|
||||
---@enum Logger.LogLevels
|
||||
Logger.LogLevels = oop.enum({
|
||||
fatal = 1,
|
||||
error = 2,
|
||||
warn = 3,
|
||||
info = 4,
|
||||
debug = 5,
|
||||
trace = 6,
|
||||
})
|
||||
|
||||
Logger.mock = Mock()
|
||||
|
||||
function Logger:init(opt)
|
||||
opt = opt or {}
|
||||
self.plugin = opt.plugin or "diffview"
|
||||
self.outfile = opt.outfile or fmt("%s/%s.log", vim.fn.stdpath("cache"), self.plugin)
|
||||
self.outfile_status = Logger.OutfileStatus.UNKNOWN
|
||||
self.level = DiffviewGlobal.debug_level > 0 and Logger.LogLevels.debug or Logger.LogLevels.info
|
||||
self.msg_buffer = {}
|
||||
self.msg_sem = Semaphore(1)
|
||||
self.batch_interval = opt.batch_interval or 3000
|
||||
|
||||
-- Flush msg buffer before exiting
|
||||
api.nvim_create_autocmd("VimLeavePre", {
|
||||
callback = function()
|
||||
if self.batch_handle then
|
||||
self.batch_handle.close()
|
||||
await(self:flush())
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
---@return Logger.Time
|
||||
function Logger.time_now()
|
||||
local secs, micros = uv.gettimeofday()
|
||||
assert(secs, micros)
|
||||
|
||||
return {
|
||||
timestamp = secs,
|
||||
micros = micros,
|
||||
}
|
||||
end
|
||||
|
||||
---@param num number
|
||||
---@param precision number
|
||||
---@return number
|
||||
local function to_precision(num, precision)
|
||||
if num % 1 == 0 then return num end
|
||||
local pow = math.pow(10, precision)
|
||||
return math.floor(num * pow) / pow
|
||||
end
|
||||
|
||||
---@param object any
|
||||
---@return string
|
||||
function Logger.dstring(object)
|
||||
local tp = type(object)
|
||||
|
||||
if tp == "thread"
|
||||
or tp == "function"
|
||||
or tp == "userdata"
|
||||
then
|
||||
return fmt("<%s %p>", tp, object)
|
||||
elseif tp == "number" then
|
||||
return tostring(to_precision(object, 3))
|
||||
elseif tp == "table" then
|
||||
local mt = getmetatable(object)
|
||||
|
||||
if mt and mt.__tostring then
|
||||
return tostring(object)
|
||||
elseif utils.islist(object) then
|
||||
if #object == 0 then return "[]" end
|
||||
local s = ""
|
||||
|
||||
for i = 1, table.maxn(object) do
|
||||
if i > 1 then s = s .. ", " end
|
||||
s = s .. Logger.dstring(object[i])
|
||||
end
|
||||
|
||||
return "[ " .. s .. " ]"
|
||||
end
|
||||
|
||||
return vim.inspect(object)
|
||||
end
|
||||
|
||||
return tostring(object)
|
||||
end
|
||||
|
||||
local dstring = Logger.dstring
|
||||
|
||||
local function dvalues(...)
|
||||
local args = { ... }
|
||||
local ret = {}
|
||||
|
||||
for i = 1, select("#", ...) do
|
||||
ret[i] = dstring(args[i])
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param level_name string
|
||||
---@param lazy_eval boolean
|
||||
---@param x function|any
|
||||
---@param ... any
|
||||
function Logger:_log(level_name, lazy_eval, x, ...)
|
||||
local ctx = self.ctx or {}
|
||||
local info = ctx.debuginfo or debug.getinfo(3, "Sl")
|
||||
local lineinfo = info.short_src .. ":" .. info.currentline
|
||||
local time = ctx.time or Logger.time_now()
|
||||
local tod = time_of_day(time)
|
||||
local date = fmt(
|
||||
"%s %02d:%02d:%02d.%03d %s",
|
||||
os.date("%F", time.timestamp),
|
||||
tod.hours,
|
||||
tod.mins,
|
||||
tod.secs,
|
||||
math.floor(tod.micros / 1000),
|
||||
tod.tz
|
||||
)
|
||||
|
||||
if lazy_eval then
|
||||
self:queue_msg(function()
|
||||
return fmt(
|
||||
"[%-6s%s] %s: %s%s\n",
|
||||
level_name:upper(),
|
||||
date,
|
||||
lineinfo,
|
||||
ctx.label and fmt("[%s] ", ctx.label) or "",
|
||||
table.concat(dvalues(x()), " ")
|
||||
)
|
||||
end)
|
||||
else
|
||||
self:queue_msg(
|
||||
fmt(
|
||||
"[%-6s%s] %s: %s%s\n",
|
||||
level_name:upper(),
|
||||
date,
|
||||
lineinfo,
|
||||
ctx.label and fmt("[%s] ", ctx.label) or "",
|
||||
table.concat(dvalues(x, ...), " ")
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
---@diagnostic disable: invisible
|
||||
|
||||
---@private
|
||||
---@param self Logger
|
||||
---@param msg string
|
||||
Logger.queue_msg = async.void(function(self, msg)
|
||||
if self.outfile_status == Logger.OutfileStatus.ERROR then
|
||||
-- We already failed to prepare the log file
|
||||
return
|
||||
elseif self.outfile_status == Logger.OutfileStatus.UNKNOWN then
|
||||
local ok, err = pawait(pl.touch, pl, self.outfile, { parents = true })
|
||||
|
||||
if not ok then
|
||||
error("Failed to prepare log file! Details:\n" .. err)
|
||||
end
|
||||
|
||||
self.outfile_status = Logger.OutfileStatus.READY
|
||||
end
|
||||
|
||||
local permit = await(self.msg_sem:acquire()) --[[@as Permit ]]
|
||||
table.insert(self.msg_buffer, msg)
|
||||
permit:forget()
|
||||
|
||||
if self.batch_handle then return end
|
||||
|
||||
self.batch_handle = loop.set_timeout(
|
||||
async.void(function()
|
||||
await(self:flush())
|
||||
self.batch_handle = nil
|
||||
end),
|
||||
self.batch_interval
|
||||
)
|
||||
end)
|
||||
|
||||
---@private
|
||||
---@param self Logger
|
||||
Logger.flush = async.void(function(self)
|
||||
if next(self.msg_buffer) then
|
||||
local permit = await(self.msg_sem:acquire()) --[[@as Permit ]]
|
||||
|
||||
-- Eval lazy messages
|
||||
for i = 1, #self.msg_buffer do
|
||||
if type(self.msg_buffer[i]) == "function" then
|
||||
self.msg_buffer[i] = self.msg_buffer[i]()
|
||||
end
|
||||
end
|
||||
|
||||
local fd, err = uv.fs_open(self.outfile, "a", tonumber("0644", 8))
|
||||
assert(fd, err)
|
||||
uv.fs_write(fd, table.concat(self.msg_buffer))
|
||||
uv.fs_close(fd)
|
||||
|
||||
self.msg_buffer = {}
|
||||
permit:forget()
|
||||
end
|
||||
end)
|
||||
|
||||
---@param min_level integer
|
||||
---@return Logger
|
||||
function Logger:lvl(min_level)
|
||||
if DiffviewGlobal.debug_level >= min_level then
|
||||
return self
|
||||
end
|
||||
|
||||
return Logger.mock --[[@as Logger ]]
|
||||
end
|
||||
|
||||
---@param ctx Logger.Context
|
||||
function Logger:set_context(ctx)
|
||||
self.ctx = ctx
|
||||
end
|
||||
|
||||
function Logger:clear_context()
|
||||
self.ctx = nil
|
||||
end
|
||||
|
||||
do
|
||||
-- Create methods
|
||||
for level, name in ipairs(Logger.LogLevels --[[@as string[] ]]) do
|
||||
---@param self Logger
|
||||
Logger[name] = function(self, ...)
|
||||
if self.level < level then return end
|
||||
self:_log(name, false, ...)
|
||||
end
|
||||
|
||||
---@param self Logger
|
||||
Logger["fmt_" .. name] = function(self, formatstring, ...)
|
||||
if self.level < level then return end
|
||||
self:_log(name, false, fmt(formatstring, ...))
|
||||
end
|
||||
|
||||
---@param self Logger
|
||||
Logger["lazy_" .. name] = function(self, func)
|
||||
if self.level < level then return end
|
||||
self:_log(name, true, func)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@diagnostic enable: invisible
|
||||
|
||||
---@class Logger.log_job.Opt
|
||||
---@field func function|string
|
||||
---@field label string
|
||||
---@field no_stdout boolean
|
||||
---@field no_stderr boolean
|
||||
---@field silent boolean
|
||||
---@field debug_level integer
|
||||
---@field debuginfo debuginfo
|
||||
|
||||
---@param job diffview.Job
|
||||
---@param opt? Logger.log_job.Opt
|
||||
function Logger:log_job(job, opt)
|
||||
opt = opt or {}
|
||||
|
||||
if opt.silent then return end
|
||||
if opt.debug_level and DiffviewGlobal.debug_level < opt.debug_level then
|
||||
return
|
||||
end
|
||||
|
||||
self:set_context({
|
||||
debuginfo = opt.debuginfo or debug.getinfo(2, "Sl"),
|
||||
time = Logger.time_now(),
|
||||
label = opt.label,
|
||||
})
|
||||
|
||||
local args = vim.tbl_map(function(arg)
|
||||
-- Simple shell escape. NOTE: not valid for windows shell.
|
||||
return fmt("'%s'", arg:gsub("'", [['"'"']]))
|
||||
end, job.args) --[[@as vector ]]
|
||||
|
||||
local log_func = self.debug
|
||||
|
||||
if type(opt.func) == "string" then
|
||||
log_func = self[opt.func]
|
||||
elseif type(opt.func) == "function" then
|
||||
log_func = opt.func --[[@as function ]]
|
||||
end
|
||||
|
||||
log_func(self, fmt("[job-info] Exit code: %s", job.code))
|
||||
log_func(self, fmt(" [cmd] %s %s", job.command, table.concat(args, " ")))
|
||||
|
||||
if job.cwd then
|
||||
log_func(self, fmt(" [cwd] %s", job.cwd))
|
||||
end
|
||||
if not opt.no_stdout and job.stdout[1] then
|
||||
log_func(self, " [stdout] " .. table.concat(job.stdout, "\n"))
|
||||
end
|
||||
if not opt.no_stderr and job.stderr[1] then
|
||||
log_func(self, " [stderr] " .. table.concat(job.stderr, "\n"))
|
||||
end
|
||||
|
||||
self:clear_context()
|
||||
end
|
||||
|
||||
M.Logger = Logger
|
||||
|
||||
return M
|
||||
@ -0,0 +1,41 @@
|
||||
--[[
|
||||
A class for creating mock objects. Accessing any key in the object returns
|
||||
itself. Calling the object does nothing.
|
||||
--]]
|
||||
|
||||
local M = {}
|
||||
local mock_mt = {}
|
||||
|
||||
local function tbl_clone(t)
|
||||
local ret = {}
|
||||
for k, v in pairs(t) do ret[k] = v end
|
||||
return ret
|
||||
end
|
||||
|
||||
---@class Mock
|
||||
---@operator call : Mock
|
||||
local Mock = setmetatable({}, mock_mt)
|
||||
|
||||
function mock_mt.__index(_, key)
|
||||
return mock_mt[key]
|
||||
end
|
||||
|
||||
function mock_mt.__call(_, internals)
|
||||
local mt = {
|
||||
__index = function(self, k)
|
||||
if Mock[k] then
|
||||
return Mock[k]
|
||||
else
|
||||
return self
|
||||
end
|
||||
end,
|
||||
__call = function()
|
||||
return nil
|
||||
end,
|
||||
}
|
||||
local this = setmetatable(tbl_clone(internals or {}), mt)
|
||||
return this
|
||||
end
|
||||
|
||||
M.Mock = Mock
|
||||
return M
|
||||
@ -0,0 +1,257 @@
|
||||
local async = require("diffview.async")
|
||||
local lazy = require("diffview.lazy")
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
local Job = lazy.access("diffview.job", "Job") ---@type diffview.Job|LazyModule
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
|
||||
local await = async.await
|
||||
local fmt = string.format
|
||||
local logger = DiffviewGlobal.logger
|
||||
|
||||
local M = {}
|
||||
|
||||
---@alias MultiJob.OnExitCallback fun(mj: MultiJob, success: boolean, err?: string)
|
||||
---@alias MultiJob.OnRetryCallback fun(mj: MultiJob, jobs: diffview.Job[])
|
||||
---@alias MultiJob.FailCond fun(mj: MultiJob): boolean, diffview.Job[]?, string?
|
||||
|
||||
---@class MultiJob : Waitable
|
||||
---@operator call : MultiJob
|
||||
---@field jobs diffview.Job[]
|
||||
---@field retry integer
|
||||
---@field check_status MultiJob.FailCond
|
||||
---@field on_exit_listeners MultiJob.OnExitCallback[]
|
||||
---@field _started boolean
|
||||
---@field _done boolean
|
||||
local MultiJob = oop.create_class("MultiJob")
|
||||
|
||||
---Predefined fail conditions.
|
||||
MultiJob.FAIL_COND = {
|
||||
---Fail if any of the jobs termintated with a non-zero exit code.
|
||||
---@param mj MultiJob
|
||||
non_zero = function(mj)
|
||||
local failed = {}
|
||||
|
||||
for _, job in ipairs(mj.jobs) do
|
||||
if job.code ~= 0 then
|
||||
failed[#failed + 1] = job
|
||||
end
|
||||
end
|
||||
|
||||
if next(failed) then
|
||||
return false, failed, "Job(s) exited with a non-zero exit code!"
|
||||
end
|
||||
|
||||
return true
|
||||
end,
|
||||
---Fail if any of the jobs had no data in stdout.
|
||||
---@param mj MultiJob
|
||||
on_empty = function(mj)
|
||||
local failed = {}
|
||||
|
||||
for _, job in ipairs(mj.jobs) do
|
||||
if #job.stdout == 1 and job.stdout[1] == ""
|
||||
or #job.stdout == 0
|
||||
then
|
||||
failed[#failed + 1] = job
|
||||
end
|
||||
end
|
||||
|
||||
if next(failed) then
|
||||
return false, failed, "Job(s) expected output, but returned nothing!"
|
||||
end
|
||||
|
||||
return true
|
||||
end,
|
||||
}
|
||||
|
||||
function MultiJob:init(jobs, opt)
|
||||
self.jobs = jobs
|
||||
self.retry = opt.retry or 0
|
||||
self.on_exit_listeners = {}
|
||||
self.on_retry_listeners = {}
|
||||
self._started = false
|
||||
self._done = false
|
||||
|
||||
self.log_opt = vim.tbl_extend("keep", opt.log_opt or {}, {
|
||||
func = "debug",
|
||||
no_stdout = true,
|
||||
debuginfo = debug.getinfo(3, "Sl"),
|
||||
})
|
||||
|
||||
if opt.fail_cond then
|
||||
if type(opt.fail_cond) == "string" then
|
||||
self.check_status = MultiJob.FAIL_COND[opt.fail_cond]
|
||||
assert(self.check_status, fmt("Unknown fail condition: '%s'", opt.fail_cond))
|
||||
elseif type(opt.fail_cond) == "function" then
|
||||
self.check_status = opt.fail_cond
|
||||
else
|
||||
error("Invalid fail condition: " .. vim.inspect(opt.fail_cond))
|
||||
end
|
||||
else
|
||||
self.check_status = MultiJob.FAIL_COND.non_zero
|
||||
end
|
||||
|
||||
if opt.on_exit then self:on_exit(opt.on_exit) end
|
||||
if opt.on_retry then self:on_retry(opt.on_retry) end
|
||||
end
|
||||
|
||||
---@private
|
||||
function MultiJob:reset()
|
||||
self._started = false
|
||||
self._done = false
|
||||
end
|
||||
|
||||
---@param self MultiJob
|
||||
MultiJob.start = async.wrap(function(self, callback)
|
||||
---@diagnostic disable: invisible
|
||||
for _, job in ipairs(self.jobs) do
|
||||
if job:is_running() then
|
||||
error("A job is still running!")
|
||||
end
|
||||
end
|
||||
|
||||
self:reset()
|
||||
|
||||
self._started = true
|
||||
|
||||
local jobs = self.jobs
|
||||
local retry_status
|
||||
|
||||
for i = 1, self.retry + 1 do
|
||||
if i > 1 then
|
||||
for _, listener in ipairs(self.on_retry_listeners) do
|
||||
listener(self, jobs)
|
||||
end
|
||||
end
|
||||
|
||||
Job.start_all(jobs)
|
||||
await(Job.join(jobs))
|
||||
|
||||
local ok, err
|
||||
ok, jobs, err = self:check_status()
|
||||
|
||||
if ok then break end
|
||||
---@cast jobs -?
|
||||
|
||||
if i == self.retry + 1 then
|
||||
retry_status = 1
|
||||
else
|
||||
retry_status = 0
|
||||
|
||||
if not self.log_opt.silent then
|
||||
logger:error(err)
|
||||
|
||||
for _, job in ipairs(jobs) do
|
||||
logger:log_job(job, { func = "error", no_stdout = true })
|
||||
end
|
||||
|
||||
logger:fmt_error("(%d/%d) Retrying failed jobs...", i, self.retry)
|
||||
end
|
||||
|
||||
await(async.timeout(1))
|
||||
end
|
||||
end
|
||||
|
||||
if not self.log_opt.silent then
|
||||
if retry_status == 0 then
|
||||
logger:info("Retry was successful!")
|
||||
elseif retry_status == 1 then
|
||||
logger:error("All retries failed!")
|
||||
end
|
||||
end
|
||||
|
||||
self._done = true
|
||||
local ok, err = self:is_success()
|
||||
|
||||
for _, listener in ipairs(self.on_exit_listeners) do
|
||||
listener(self, ok, err)
|
||||
end
|
||||
|
||||
callback(ok, err)
|
||||
---@diagnostic enable: invisible
|
||||
end)
|
||||
|
||||
---@override
|
||||
---@param self MultiJob
|
||||
---@param callback fun(success: boolean, err?: string)
|
||||
MultiJob.await = async.sync_wrap(function(self, callback)
|
||||
if self:is_done() then
|
||||
callback(self:is_success())
|
||||
elseif self:is_running() then
|
||||
self:on_exit(function(_, ...) callback(...) end)
|
||||
else
|
||||
callback(await(self:start()))
|
||||
end
|
||||
end)
|
||||
|
||||
---@return boolean success
|
||||
---@return string? err
|
||||
function MultiJob:is_success()
|
||||
local ok, _, err = self:check_status()
|
||||
if not ok then return false, err end
|
||||
return true
|
||||
end
|
||||
|
||||
---@param callback MultiJob.OnExitCallback
|
||||
function MultiJob:on_exit(callback)
|
||||
table.insert(self.on_exit_listeners, callback)
|
||||
end
|
||||
|
||||
---@param callback MultiJob.OnRetryCallback
|
||||
function MultiJob:on_retry(callback)
|
||||
table.insert(self.on_retry_listeners, callback)
|
||||
end
|
||||
|
||||
function MultiJob:is_done()
|
||||
return self._done
|
||||
end
|
||||
|
||||
function MultiJob:is_started()
|
||||
return self._started
|
||||
end
|
||||
|
||||
function MultiJob:is_running()
|
||||
return self:is_started() and not self:is_done()
|
||||
end
|
||||
|
||||
---@return string[]
|
||||
function MultiJob:stdout()
|
||||
return utils.flatten(
|
||||
---@param value diffview.Job
|
||||
vim.tbl_map(function(value)
|
||||
return value.stdout
|
||||
end, self.jobs)
|
||||
)
|
||||
end
|
||||
|
||||
---@return string[]
|
||||
function MultiJob:stderr()
|
||||
return utils.flatten(
|
||||
---@param value diffview.Job
|
||||
vim.tbl_map(function(value)
|
||||
return value.stderr
|
||||
end, self.jobs)
|
||||
)
|
||||
end
|
||||
|
||||
---@param code integer
|
||||
---@param signal? integer|uv.aliases.signals # (default: "sigterm")
|
||||
---@return 0|nil success
|
||||
function MultiJob:kill(code, signal)
|
||||
---@type 0?
|
||||
local ret = 0
|
||||
|
||||
for _, job in ipairs(self.jobs) do
|
||||
if job:is_running() then
|
||||
local success = job:kill(code, signal)
|
||||
if not success then ret = nil end
|
||||
end
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
M.MultiJob = MultiJob
|
||||
|
||||
return M
|
||||
@ -0,0 +1,227 @@
|
||||
local lazy = require("diffview.lazy")
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
|
||||
local fmt = string.format
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.abstract_stub()
|
||||
error("Unimplemented abstract method!")
|
||||
end
|
||||
|
||||
---@generic T
|
||||
---@param t T
|
||||
---@return T
|
||||
function M.enum(t)
|
||||
utils.add_reverse_lookup(t)
|
||||
return t
|
||||
end
|
||||
|
||||
---Wrap metatable methods to ensure they're called with the instance as `self`.
|
||||
---@param func function
|
||||
---@param instance table
|
||||
---@return function
|
||||
local function wrap_mt_func(func, instance)
|
||||
return function(_, k)
|
||||
return func(instance, k)
|
||||
end
|
||||
end
|
||||
|
||||
local mt_func_names = {
|
||||
"__index",
|
||||
"__tostring",
|
||||
"__eq",
|
||||
"__add",
|
||||
"__sub",
|
||||
"__mul",
|
||||
"__div",
|
||||
"__mod",
|
||||
"__pow",
|
||||
"__unm",
|
||||
"__len",
|
||||
"__lt",
|
||||
"__le",
|
||||
"__concat",
|
||||
"__newindex",
|
||||
"__call",
|
||||
}
|
||||
|
||||
local function new_instance(class, ...)
|
||||
local inst = { class = class }
|
||||
local mt = { __index = class }
|
||||
|
||||
for _, mt_name in ipairs(mt_func_names) do
|
||||
local class_mt_func = class[mt_name]
|
||||
|
||||
if type(class_mt_func) == "function" then
|
||||
mt[mt_name] = wrap_mt_func(class_mt_func, inst)
|
||||
elseif class_mt_func ~= nil then
|
||||
mt[mt_name] = class_mt_func
|
||||
end
|
||||
end
|
||||
|
||||
local self = setmetatable(inst, mt)
|
||||
self:init(...)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
local function tostring(class)
|
||||
return fmt("<class %s>", class.__name)
|
||||
end
|
||||
|
||||
---@generic T : diffview.Object
|
||||
---@generic U : diffview.Object
|
||||
---@param name string
|
||||
---@param super_class? T
|
||||
---@return U new_class
|
||||
function M.create_class(name, super_class)
|
||||
super_class = super_class or M.Object
|
||||
|
||||
return setmetatable(
|
||||
{
|
||||
__name = name,
|
||||
super_class = super_class,
|
||||
},
|
||||
{
|
||||
__index = super_class,
|
||||
__call = new_instance,
|
||||
__tostring = tostring,
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
local function classm_safeguard(x)
|
||||
assert(x.class == nil, "Class method should not be invoked from an instance!")
|
||||
end
|
||||
|
||||
local function instancem_safeguard(x)
|
||||
assert(type(x.class) == "table", "Instance method must be called from a class instance!")
|
||||
end
|
||||
|
||||
---@class diffview.Object
|
||||
---@field protected __name string
|
||||
---@field private __init_caller? table
|
||||
---@field class table|diffview.Object
|
||||
---@field super_class table|diffview.Object
|
||||
local Object = M.create_class("Object")
|
||||
M.Object = Object
|
||||
|
||||
function Object:__tostring()
|
||||
return fmt("<a %s>", self.class.__name)
|
||||
end
|
||||
|
||||
-- ### CLASS METHODS ###
|
||||
|
||||
---@return string
|
||||
function Object:name()
|
||||
classm_safeguard(self)
|
||||
return self.__name
|
||||
end
|
||||
|
||||
---Check if this class is an ancestor of the given instance. `A` is an ancestor
|
||||
---of `b` if - and only if - `b` is an instance of a subclass of `A`.
|
||||
---@param other any
|
||||
---@return boolean
|
||||
function Object:ancestorof(other)
|
||||
classm_safeguard(self)
|
||||
if not M.is_instance(other) then return false end
|
||||
|
||||
return other:instanceof(self)
|
||||
end
|
||||
|
||||
---@return string
|
||||
function Object:classpath()
|
||||
classm_safeguard(self)
|
||||
local ret = self.__name
|
||||
local cur = self.super_class
|
||||
|
||||
while cur do
|
||||
ret = cur.__name .. "." .. ret
|
||||
cur = cur.super_class
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
-- ### INSTANCE METHODS ###
|
||||
|
||||
---Call constructor.
|
||||
function Object:init(...) end
|
||||
|
||||
---Call super constructor.
|
||||
---@param ... any
|
||||
function Object:super(...)
|
||||
instancem_safeguard(self)
|
||||
local next_super
|
||||
|
||||
-- Keep track of what class is currently calling the constructor such that we
|
||||
-- can avoid loops.
|
||||
if self.__init_caller then
|
||||
next_super = self.__init_caller.super_class
|
||||
else
|
||||
next_super = self.super_class
|
||||
end
|
||||
|
||||
if not next_super then return end
|
||||
|
||||
self.__init_caller = next_super
|
||||
next_super.init(self, ...)
|
||||
self.__init_caller = nil
|
||||
end
|
||||
|
||||
---@param other diffview.Object
|
||||
---@return boolean
|
||||
function Object:instanceof(other)
|
||||
instancem_safeguard(self)
|
||||
local cur = self.class
|
||||
|
||||
while cur do
|
||||
if cur == other then return true end
|
||||
cur = cur.super_class
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
---@param x any
|
||||
---@return boolean
|
||||
function M.is_class(x)
|
||||
if type(x) ~= "table" then return false end
|
||||
return type(rawget(x, "__name")) == "string" and x.instanceof == Object.instanceof
|
||||
end
|
||||
|
||||
---@param x any
|
||||
---@return boolean
|
||||
function M.is_instance(x)
|
||||
if type(x) ~= "table" then return false end
|
||||
return M.is_class(x.class)
|
||||
end
|
||||
|
||||
---@class Symbol
|
||||
---@operator call : Symbol
|
||||
---@field public name? string
|
||||
---@field public id integer
|
||||
---@field private _id_counter integer
|
||||
local Symbol = M.create_class("Symbol")
|
||||
M.Symbol = Symbol
|
||||
|
||||
---@private
|
||||
Symbol._id_counter = 1
|
||||
|
||||
---@param name? string
|
||||
function Symbol:init(name)
|
||||
self.name = name
|
||||
self.id = Symbol._id_counter
|
||||
Symbol._id_counter = Symbol._id_counter + 1
|
||||
end
|
||||
|
||||
function Symbol:__tostring()
|
||||
if self.name then
|
||||
return fmt("<Symbol('%s)>", self.name)
|
||||
else
|
||||
return fmt("<Symbol(#%d)>", self.id)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,662 @@
|
||||
local async = require("diffview.async")
|
||||
local lazy = require("diffview.lazy")
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
|
||||
local await = async.await
|
||||
local fmt = string.format
|
||||
local uv = vim.loop
|
||||
|
||||
local M = {}
|
||||
|
||||
local is_windows = uv.os_uname().version:match("Windows")
|
||||
|
||||
local function handle_uv_err(x, err, err_msg)
|
||||
if not x then
|
||||
error(err .. " " .. err_msg, 2)
|
||||
end
|
||||
|
||||
return x
|
||||
end
|
||||
|
||||
-- Ref: https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats
|
||||
local WINDOWS_PATH_SPECIFIER = {
|
||||
dos_dev = "^[\\/][\\/][.?][\\/]", -- DOS Device path
|
||||
unc = "^[\\/][\\/]", -- UNC path
|
||||
rel_drive = "^[\\/]", -- Relative drive
|
||||
drive = [[^[a-zA-Z]:]],
|
||||
}
|
||||
table.insert(WINDOWS_PATH_SPECIFIER, WINDOWS_PATH_SPECIFIER.dos_dev)
|
||||
table.insert(WINDOWS_PATH_SPECIFIER, WINDOWS_PATH_SPECIFIER.unc)
|
||||
table.insert(WINDOWS_PATH_SPECIFIER, WINDOWS_PATH_SPECIFIER.rel_drive)
|
||||
table.insert(WINDOWS_PATH_SPECIFIER, WINDOWS_PATH_SPECIFIER.drive)
|
||||
|
||||
---@class PathLib
|
||||
---@operator call : PathLib
|
||||
---@field sep "/"|"\\"
|
||||
---@field os "unix"|"windows" Determines the type of paths we're dealing with.
|
||||
---@field cwd string Leave as `nil` to always use current cwd.
|
||||
local PathLib = oop.create_class("PathLib")
|
||||
|
||||
function PathLib:init(o)
|
||||
self.os = o.os or (is_windows and "windows" or "unix")
|
||||
assert(vim.tbl_contains({ "unix", "windows" }, self.os), "Invalid OS type!")
|
||||
self._is_windows = self.os == "windows"
|
||||
self.sep = o.separator or (self._is_windows and "\\" or "/")
|
||||
self.cwd = o.cwd and self:convert(o.cwd) or nil
|
||||
end
|
||||
|
||||
---@private
|
||||
function PathLib:_cwd()
|
||||
return self.cwd or self:convert(uv.cwd())
|
||||
end
|
||||
|
||||
---@private
|
||||
---@return ...
|
||||
function PathLib:_clean(...)
|
||||
local argc = select("#", ...)
|
||||
|
||||
if argc == 1 and select(1, ...) ~= nil then
|
||||
return self:convert(...)
|
||||
end
|
||||
|
||||
local paths = { ... }
|
||||
|
||||
for i = 1, argc do
|
||||
if paths[i] ~= nil then
|
||||
paths[i] = self:convert(paths[i])
|
||||
end
|
||||
end
|
||||
|
||||
return unpack(paths, 1, argc)
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param path string
|
||||
function PathLib:_split_root(path)
|
||||
local root = self:root(path)
|
||||
if not root then return "", path end
|
||||
return root, path:sub(#root + 1)
|
||||
end
|
||||
|
||||
---Check if a given path is a URI.
|
||||
---@param path string
|
||||
---@return boolean
|
||||
function PathLib:is_uri(path)
|
||||
return string.match(path, "^%w+://") ~= nil
|
||||
end
|
||||
|
||||
---Get the URI scheme of a given URI.
|
||||
---@param path string
|
||||
---@return string
|
||||
function PathLib:get_uri_scheme(path)
|
||||
return string.match(path, "^(%w+://)")
|
||||
end
|
||||
|
||||
---Change the path separators in a path. Removes duplicate separators.
|
||||
---@param path string
|
||||
---@param sep? "/"|"\\"
|
||||
---@return string
|
||||
function PathLib:convert(path, sep)
|
||||
sep = sep or self.sep
|
||||
local prefix
|
||||
local p = tostring(path)
|
||||
|
||||
if self:is_uri(path) then
|
||||
sep = "/"
|
||||
prefix, p = path:match("^(%w+://)(.*)")
|
||||
elseif self._is_windows then
|
||||
for _, pat in ipairs(WINDOWS_PATH_SPECIFIER) do
|
||||
prefix = path:match(pat)
|
||||
|
||||
if prefix then
|
||||
prefix = prefix:gsub("[\\/]", sep)
|
||||
p = path:sub(#prefix + 1)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
p, _ = p:gsub("[\\/]+", sep)
|
||||
|
||||
return (prefix or "") .. p
|
||||
end
|
||||
|
||||
---Convert a path to use the appropriate path separators for the current OS.
|
||||
---@param path string
|
||||
---@return string
|
||||
function PathLib:to_os(path)
|
||||
return self:convert(path, self._is_windows and "\\" or "/")
|
||||
end
|
||||
|
||||
---Check if a given path is absolute.
|
||||
---@param path string
|
||||
---@return boolean
|
||||
function PathLib:is_abs(path)
|
||||
path = self:_clean(path)
|
||||
|
||||
if self._is_windows then
|
||||
for _, pat in ipairs(WINDOWS_PATH_SPECIFIER) do
|
||||
if path:match(pat) ~= nil then return true end
|
||||
end
|
||||
|
||||
return false
|
||||
else
|
||||
return path:sub(1, 1) == self.sep
|
||||
end
|
||||
end
|
||||
|
||||
---Get the absolute path of a given path. This is resolved using either the
|
||||
---`cwd` field if it's defined. Otherwise the current cwd is used instead.
|
||||
---@param path string
|
||||
---@param cwd? string
|
||||
---@return string
|
||||
function PathLib:absolute(path, cwd)
|
||||
path, cwd = self:_clean(path, cwd)
|
||||
path = self:expand(path)
|
||||
cwd = cwd or self:_cwd()
|
||||
|
||||
if self:is_uri(path) then
|
||||
return path
|
||||
end
|
||||
|
||||
if self:is_abs(path) then
|
||||
return self:normalize(path, { cwd = cwd, absolute = true })
|
||||
end
|
||||
|
||||
return self:normalize(self:join(cwd, path), { cwd = cwd, absolute = true })
|
||||
end
|
||||
|
||||
---Check if the given path is the root.
|
||||
---@param path string
|
||||
---@return boolean
|
||||
function PathLib:is_root(path)
|
||||
path = self:remove_trailing(self:_clean(path))
|
||||
|
||||
if self:is_abs(path) then
|
||||
if self._is_windows then
|
||||
for _, pat in ipairs(WINDOWS_PATH_SPECIFIER) do
|
||||
local prefix = path:match(pat)
|
||||
if prefix and #path == #prefix then return true end
|
||||
end
|
||||
|
||||
return false
|
||||
else
|
||||
return path == self.sep
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
---Get the root of an absolute path. Returns nil if the path is not absolute.
|
||||
---@param path string
|
||||
---@return string|nil
|
||||
function PathLib:root(path)
|
||||
path = tostring(path)
|
||||
|
||||
if self:is_abs(path) then
|
||||
if self._is_windows then
|
||||
for _, pat in ipairs(WINDOWS_PATH_SPECIFIER) do
|
||||
local root = path:match(pat)
|
||||
if root then return root end
|
||||
end
|
||||
else
|
||||
return self.sep
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@class PathLibNormalizeSpec
|
||||
---@field cwd string
|
||||
---@field absolute boolean
|
||||
|
||||
---Normalize a given path, resolving relative segments.
|
||||
---@param path string
|
||||
---@param opt? PathLibNormalizeSpec
|
||||
---@return string
|
||||
function PathLib:normalize(path, opt)
|
||||
path = self:_clean(path)
|
||||
|
||||
if self:is_uri(path) then
|
||||
return path
|
||||
end
|
||||
|
||||
opt = opt or {}
|
||||
local cwd = opt.cwd and self:_clean(opt.cwd) or self:_cwd()
|
||||
local absolute = vim.F.if_nil(opt.absolute, false)
|
||||
|
||||
local root = self:root(path)
|
||||
if root and self:is_root(path) then
|
||||
return path
|
||||
end
|
||||
|
||||
if not self:is_abs(path) then
|
||||
local relpath = self:relative(path, cwd, true)
|
||||
path = self:add_trailing(cwd) .. relpath
|
||||
end
|
||||
|
||||
local parts = self:explode(path)
|
||||
if root then
|
||||
table.remove(parts, 1)
|
||||
|
||||
if self._is_windows and root == root:match(WINDOWS_PATH_SPECIFIER.rel_drive) then
|
||||
-- Resolve relative drive
|
||||
-- path="/foo/bar/baz", cwd="D:/lorem/ipsum" -> "D:/foo/bar/baz"
|
||||
root = self:root(cwd)
|
||||
end
|
||||
end
|
||||
|
||||
local normal = path
|
||||
if #parts > 1 then
|
||||
local i = 2
|
||||
local upc = 0
|
||||
repeat
|
||||
if parts[i] == "." then
|
||||
table.remove(parts, i)
|
||||
i = i - 1
|
||||
elseif parts[i] == ".." then
|
||||
if i == 1 then
|
||||
upc = upc + 1
|
||||
end
|
||||
table.remove(parts, i)
|
||||
if i > 1 then
|
||||
table.remove(parts, i - 1)
|
||||
i = i - 2
|
||||
else
|
||||
i = i - 1
|
||||
end
|
||||
end
|
||||
|
||||
i = i + 1
|
||||
until i > #parts
|
||||
|
||||
normal = self:join(root, unpack(parts))
|
||||
if not absolute and upc == 0 then
|
||||
normal = self:relative(normal, cwd, true)
|
||||
end
|
||||
end
|
||||
|
||||
return normal == "" and "." or normal
|
||||
end
|
||||
|
||||
---Expand environment variables and `~`.
|
||||
---@param path string
|
||||
---@return string
|
||||
function PathLib:expand(path)
|
||||
local segments = self:explode(path)
|
||||
local idx = 1
|
||||
|
||||
if segments[1] == "~" then
|
||||
segments[1] = uv.os_homedir()
|
||||
idx = 2
|
||||
end
|
||||
|
||||
for i = idx, #segments do
|
||||
local env_var = segments[i]:match("^%$(%S+)$")
|
||||
if env_var then
|
||||
segments[i] = uv.os_getenv(env_var) or env_var
|
||||
end
|
||||
end
|
||||
|
||||
return self:join(unpack(segments))
|
||||
end
|
||||
|
||||
---Joins an ordered list of path segments into a path string.
|
||||
---@vararg ... string|string[] Paths
|
||||
---@return string
|
||||
function PathLib:join(...)
|
||||
local segments = { ... }
|
||||
|
||||
if type(segments[1]) == "table" then
|
||||
segments = segments[1]
|
||||
end
|
||||
|
||||
local ret = ""
|
||||
|
||||
for i = 1, table.maxn(segments) do
|
||||
local cur = segments[i]
|
||||
if cur and cur ~= "" then
|
||||
if #ret > 0 and not ret:sub(-1, -1):match("[\\/]") then
|
||||
ret = ret .. self.sep
|
||||
end
|
||||
ret = ret .. cur
|
||||
end
|
||||
end
|
||||
|
||||
return self:_clean(ret)
|
||||
end
|
||||
|
||||
---Explodes the path into an ordered list of path segments.
|
||||
---@param path string
|
||||
---@return string[]
|
||||
function PathLib:explode(path)
|
||||
path = self:_clean(path)
|
||||
local parts = {}
|
||||
local i = 1
|
||||
|
||||
if self:is_uri(path) then
|
||||
local scheme, p = path:match("^(%w+://)(.*)")
|
||||
parts[i] = scheme
|
||||
path = p
|
||||
i = i + 1
|
||||
end
|
||||
|
||||
local root
|
||||
root, path = self:_split_root(path)
|
||||
|
||||
if root ~= "" then
|
||||
parts[i] = root
|
||||
|
||||
if path:sub(1, 1) == self.sep then
|
||||
path = path:sub(2)
|
||||
end
|
||||
end
|
||||
|
||||
for part in path:gmatch(string.format("([^%s]+)%s?", self.sep, self.sep)) do
|
||||
parts[#parts+1] = part
|
||||
end
|
||||
|
||||
return parts
|
||||
end
|
||||
|
||||
---Add a trailing separator, unless already present.
|
||||
---@param path string
|
||||
---@return string
|
||||
function PathLib:add_trailing(path)
|
||||
local root
|
||||
root, path = self:_split_root(path)
|
||||
|
||||
if #path == 0 then return root .. path end
|
||||
if path:sub(-1) == self.sep then
|
||||
return root .. path
|
||||
end
|
||||
|
||||
return root .. path .. self.sep
|
||||
end
|
||||
|
||||
---Remove any trailing separator, if present.
|
||||
---@param path string
|
||||
---@return string
|
||||
function PathLib:remove_trailing(path)
|
||||
local root
|
||||
root, path = self:_split_root(path)
|
||||
local p, _ = path:gsub(self.sep .. "$", "")
|
||||
|
||||
return root .. p
|
||||
end
|
||||
|
||||
---Get the basename of the given path.
|
||||
---@param path string
|
||||
---@return string
|
||||
function PathLib:basename(path)
|
||||
path = self:remove_trailing(self:_clean(path))
|
||||
local i = path:match("^.*()" .. self.sep)
|
||||
|
||||
if not i then
|
||||
return path
|
||||
end
|
||||
|
||||
return path:sub(i + 1, #path)
|
||||
end
|
||||
|
||||
---Get the extension of the given path.
|
||||
---@param path string
|
||||
---@return string|nil
|
||||
function PathLib:extension(path)
|
||||
path = self:basename(path)
|
||||
|
||||
return path:match(".+%.(.*)")
|
||||
end
|
||||
|
||||
---Get the path to the parent directory of the given path. Returns `nil` if the
|
||||
---path has no parent.
|
||||
---@param path string
|
||||
---@param n? integer Nth parent. (default: 1)
|
||||
---@return string?
|
||||
function PathLib:parent(path, n)
|
||||
if type(n) ~= "number" or n < 1 then
|
||||
n = 1
|
||||
end
|
||||
|
||||
local parts = self:explode(path)
|
||||
local root = self:root(path)
|
||||
|
||||
if root and n == #parts then
|
||||
return root
|
||||
elseif n >= #parts then
|
||||
return
|
||||
end
|
||||
|
||||
return self:join(unpack(parts, 1, #parts - n))
|
||||
end
|
||||
|
||||
---Get a path relative to another path.
|
||||
---@param path string
|
||||
---@param relative_to string
|
||||
---@param no_resolve? boolean Don't normalize paths first.
|
||||
---@return string
|
||||
function PathLib:relative(path, relative_to, no_resolve)
|
||||
path, relative_to = self:_clean(path, relative_to)
|
||||
|
||||
if not no_resolve then
|
||||
local abs = self:is_abs(path)
|
||||
path = self:normalize(path, { absolute = abs })
|
||||
relative_to = self:normalize(relative_to, { absolute = abs })
|
||||
end
|
||||
|
||||
if relative_to == "" then
|
||||
return path
|
||||
elseif relative_to == path then
|
||||
return ""
|
||||
end
|
||||
|
||||
local p, _ = path:gsub("^" .. vim.pesc(self:add_trailing(relative_to)), "")
|
||||
|
||||
return p
|
||||
end
|
||||
|
||||
---Shorten a path by truncating the head.
|
||||
---@param path string
|
||||
---@param max_length integer
|
||||
---@return string
|
||||
function PathLib:truncate(path, max_length)
|
||||
path = self:_clean(path)
|
||||
|
||||
if #path > max_length - 1 then
|
||||
path = path:sub(#path - max_length + 1, #path)
|
||||
local i = path:match("()" .. self.sep)
|
||||
if not i then
|
||||
return "…" .. path
|
||||
end
|
||||
return "…" .. path:sub(i, -1)
|
||||
else
|
||||
return path
|
||||
end
|
||||
end
|
||||
|
||||
---@param path string
|
||||
---@return string|nil
|
||||
function PathLib:realpath(path)
|
||||
local p = uv.fs_realpath(path)
|
||||
|
||||
if p then
|
||||
return self:convert(p)
|
||||
end
|
||||
end
|
||||
|
||||
---@param path string
|
||||
---@return string|nil
|
||||
function PathLib:readlink(path)
|
||||
local p = uv.fs_readlink(path)
|
||||
|
||||
if p then
|
||||
return self:convert(p)
|
||||
end
|
||||
end
|
||||
|
||||
---@param path string
|
||||
---@param nosuf? boolean
|
||||
---@param list falsy
|
||||
---@return string
|
||||
---@overload fun(self: PathLib, path: string, nosuf: boolean, list: true): string[]
|
||||
function PathLib:vim_expand(path, nosuf, list)
|
||||
if list then
|
||||
return vim.tbl_map(function(v)
|
||||
return self:convert(v)
|
||||
end, vim.fn.expand(path, nosuf, list))
|
||||
end
|
||||
|
||||
return self:convert(vim.fn.expand(path, nosuf, list) --[[@as string ]])
|
||||
end
|
||||
|
||||
---@param path string
|
||||
---@return string
|
||||
function PathLib:vim_fnamemodify(path, mods)
|
||||
return self:convert(vim.fn.fnamemodify(path, mods))
|
||||
end
|
||||
|
||||
---@param path string
|
||||
---@return table?
|
||||
function PathLib:stat(path)
|
||||
return uv.fs_stat(path)
|
||||
end
|
||||
|
||||
---@param path string
|
||||
---@return string?
|
||||
function PathLib:type(path)
|
||||
local p = uv.fs_realpath(path)
|
||||
|
||||
if p then
|
||||
local stat = uv.fs_stat(p)
|
||||
if stat then
|
||||
return stat.type
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param path string
|
||||
---@return boolean
|
||||
function PathLib:is_dir(path)
|
||||
return self:type(path) == "directory"
|
||||
end
|
||||
|
||||
---Check for read access to a given path.
|
||||
---@param path string
|
||||
---@return boolean
|
||||
function PathLib:readable(path)
|
||||
local p = uv.fs_realpath(path)
|
||||
|
||||
if p then
|
||||
return not not uv.fs_access(p, "R")
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
---@class PathLib.touch.Opt
|
||||
---@field mode? integer
|
||||
---@field parents? boolean
|
||||
|
||||
---@param self PathLib
|
||||
---@param path string
|
||||
---@param opt PathLib.touch.Opt
|
||||
PathLib.touch = async.void(function(self, path, opt)
|
||||
opt = opt or {}
|
||||
local mode = opt.mode or tonumber("0644", 8)
|
||||
|
||||
path = self:_clean(path)
|
||||
local stat = self:stat(path)
|
||||
|
||||
if stat then
|
||||
-- Path exists: just update utime
|
||||
local time = os.time()
|
||||
handle_uv_err(uv.fs_utime(path, time, time))
|
||||
return
|
||||
end
|
||||
|
||||
if opt.parents then
|
||||
local parent = self:parent(path)
|
||||
|
||||
if parent then
|
||||
await(self:mkdir(self:parent(path), { parents = true }))
|
||||
end
|
||||
end
|
||||
|
||||
local fd = handle_uv_err(uv.fs_open(path, "w", mode))
|
||||
handle_uv_err(uv.fs_close(fd))
|
||||
end)
|
||||
|
||||
---@class PathLib.mkdir.Opt
|
||||
---@field mode? integer
|
||||
---@field parents? boolean
|
||||
|
||||
---@param self PathLib
|
||||
---@param path string
|
||||
---@param opt? table
|
||||
PathLib.mkdir = async.void(function(self, path, opt)
|
||||
opt = opt or {}
|
||||
local mode = opt.mode or tonumber("0700", 8)
|
||||
path = self:absolute(path)
|
||||
|
||||
if not opt.parents then
|
||||
handle_uv_err(uv.fs_mkdir(path, mode))
|
||||
return
|
||||
end
|
||||
|
||||
local cur_path
|
||||
|
||||
for _, part in ipairs(self:explode(path)) do
|
||||
cur_path = cur_path and self:join(cur_path, part) or part
|
||||
local stat = self:stat(cur_path)
|
||||
|
||||
if not stat then
|
||||
handle_uv_err(uv.fs_mkdir(cur_path, mode))
|
||||
else
|
||||
if stat.type ~= "directory" then
|
||||
error(fmt("Cannot create directory '%s': Not a directory", cur_path))
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
---Delete a name and possibly the file it refers to.
|
||||
---@param self PathLib
|
||||
---@param path string
|
||||
---@param callback? function
|
||||
---@diagnostic disable-next-line: unused-local
|
||||
PathLib.unlink = async.wrap(function(self, path, callback)
|
||||
---@cast callback -?
|
||||
uv.fs_unlink(path, function(err, ok)
|
||||
if not ok then
|
||||
error(err)
|
||||
end
|
||||
callback()
|
||||
end)
|
||||
end)
|
||||
|
||||
function PathLib:chain(...)
|
||||
local t = {
|
||||
__result = utils.tbl_pack(...)
|
||||
}
|
||||
|
||||
return setmetatable(t, {
|
||||
__index = function(chain, k)
|
||||
if k == "get" then
|
||||
return function(_)
|
||||
return utils.tbl_unpack(t.__result)
|
||||
end
|
||||
|
||||
else
|
||||
return function(_, ...)
|
||||
t.__result = utils.tbl_pack(self[k](self, utils.tbl_unpack(t.__result), ...))
|
||||
return chain
|
||||
end
|
||||
end
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
M.PathLib = PathLib
|
||||
return M
|
||||
@ -0,0 +1,92 @@
|
||||
local oop = require("diffview.oop")
|
||||
local utils = require("diffview.utils")
|
||||
|
||||
local uv = vim.loop
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class PerfTimer : diffview.Object
|
||||
---@operator call : PerfTimer
|
||||
---@field subject string|nil
|
||||
---@field first integer Start time (ns)
|
||||
---@field last integer Stop time (ns)
|
||||
---@field final_time number Final time (ms)
|
||||
---@field laps number[] List of lap times (ms)
|
||||
local PerfTimer = oop.create_class("PerfTimer")
|
||||
|
||||
---PerfTimer constructor.
|
||||
---@param subject string|nil
|
||||
function PerfTimer:init(subject)
|
||||
self.subject = subject
|
||||
self.laps = {}
|
||||
self.first = uv.hrtime()
|
||||
end
|
||||
|
||||
function PerfTimer:reset()
|
||||
self.laps = {}
|
||||
self.first = uv.hrtime()
|
||||
self.final_time = nil
|
||||
end
|
||||
|
||||
---Record a lap time.
|
||||
---@param subject string|nil
|
||||
function PerfTimer:lap(subject)
|
||||
self.laps[#self.laps + 1] = {
|
||||
subject or #self.laps + 1,
|
||||
(uv.hrtime() - self.first) / 1000000,
|
||||
}
|
||||
end
|
||||
|
||||
---Set final time.
|
||||
---@return number
|
||||
function PerfTimer:time()
|
||||
self.last = uv.hrtime() - self.first
|
||||
self.final_time = self.last / 1000000
|
||||
|
||||
return self.final_time
|
||||
end
|
||||
|
||||
function PerfTimer:__tostring()
|
||||
if not self.final_time then
|
||||
self:time()
|
||||
end
|
||||
|
||||
if #self.laps == 0 then
|
||||
return string.format(
|
||||
"%s %.3f ms",
|
||||
utils.str_right_pad((self.subject or "TIME") .. ":", 24),
|
||||
self.final_time
|
||||
)
|
||||
else
|
||||
local s = (self.subject or "LAPS") .. ":\n"
|
||||
local last = 0
|
||||
|
||||
for _, lap in ipairs(self.laps) do
|
||||
s = s
|
||||
.. string.format(
|
||||
">> %s %.3f ms\t(%.3f ms)\n",
|
||||
utils.str_right_pad(lap[1], 36),
|
||||
lap[2],
|
||||
lap[2] - last
|
||||
)
|
||||
last = lap[2]
|
||||
end
|
||||
|
||||
return s .. string.format("== %s %.3f ms", utils.str_right_pad("FINAL TIME", 36), self.final_time)
|
||||
end
|
||||
end
|
||||
|
||||
---Get the relative performance difference in percent.
|
||||
---@static
|
||||
---@param a PerfTimer
|
||||
---@param b PerfTimer
|
||||
---@return string
|
||||
function PerfTimer.difference(a, b)
|
||||
local delta = (b.final_time - a.final_time) / a.final_time
|
||||
local negative = delta < 0
|
||||
|
||||
return string.format("%s%.2f%%", not negative and "+" or "", delta * 100)
|
||||
end
|
||||
|
||||
M.PerfTimer = PerfTimer
|
||||
return M
|
||||
@ -0,0 +1,532 @@
|
||||
local oop = require("diffview.oop")
|
||||
local utils = require("diffview.utils")
|
||||
local api = vim.api
|
||||
|
||||
local M = {}
|
||||
local uid_counter = 0
|
||||
|
||||
---Duration of the last redraw in ms.
|
||||
M.last_draw_time = 0
|
||||
|
||||
---@class renderer.HlData
|
||||
---@field group string
|
||||
---@field line_idx integer
|
||||
---@field first integer 0 indexed, inclusive
|
||||
---@field last integer Exclusive
|
||||
|
||||
---@class renderer.HlList
|
||||
---@field offset integer
|
||||
---@field [integer] renderer.HlData
|
||||
|
||||
---@class CompStruct
|
||||
---@field _name string
|
||||
---@field comp RenderComponent
|
||||
---@field [integer|string] CompStruct
|
||||
|
||||
---@class CompSchema
|
||||
---@field name? string
|
||||
---@field context? table
|
||||
---@field [integer] CompSchema
|
||||
|
||||
---@class RenderComponent : diffview.Object
|
||||
---@field name string
|
||||
---@field context? table
|
||||
---@field parent RenderComponent
|
||||
---@field lines string[]
|
||||
---@field hl renderer.HlList
|
||||
---@field line_buffer string
|
||||
---@field components RenderComponent[]
|
||||
---@field lstart integer 0 indexed, Inclusive
|
||||
---@field lend integer Exclusive
|
||||
---@field height integer
|
||||
---@field data_root RenderData
|
||||
local RenderComponent = oop.create_class("RenderComponent")
|
||||
|
||||
---RenderComponent constructor.
|
||||
function RenderComponent:init(name)
|
||||
self.name = name or RenderComponent.next_uid()
|
||||
self.lines = {}
|
||||
self.hl = {}
|
||||
self.line_buffer = ""
|
||||
self.components = {}
|
||||
self.lstart = -1
|
||||
self.lend = -1
|
||||
self.height = 0
|
||||
end
|
||||
|
||||
---@param parent RenderComponent
|
||||
---@param comp_struct CompStruct
|
||||
---@param schema CompSchema
|
||||
local function create_subcomponents(parent, comp_struct, schema)
|
||||
for i, v in ipairs(schema) do
|
||||
v.name = v.name or RenderComponent.next_uid()
|
||||
local sub_comp = parent:create_component()
|
||||
---@cast sub_comp RenderComponent
|
||||
sub_comp.name = v.name
|
||||
sub_comp.context = v.context
|
||||
sub_comp.parent = parent
|
||||
comp_struct[i] = {
|
||||
_name = v.name,
|
||||
comp = sub_comp,
|
||||
}
|
||||
comp_struct[v.name] = comp_struct[i]
|
||||
if #v > 0 then
|
||||
create_subcomponents(sub_comp, comp_struct[i], v)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function RenderComponent.next_uid()
|
||||
local uid = "comp_" .. uid_counter
|
||||
uid_counter = uid_counter + 1
|
||||
return uid
|
||||
end
|
||||
|
||||
---Create a new compoenent
|
||||
---@param schema? CompSchema
|
||||
---@return RenderComponent, CompStruct
|
||||
function RenderComponent.create_static_component(schema)
|
||||
local comp_struct
|
||||
---@diagnostic disable-next-line: need-check-nil
|
||||
local new_comp = RenderComponent(schema and schema.name or nil)
|
||||
|
||||
if schema then
|
||||
new_comp.context = schema.context
|
||||
comp_struct = { _name = new_comp.name, comp = new_comp }
|
||||
create_subcomponents(new_comp, comp_struct, schema)
|
||||
end
|
||||
|
||||
return new_comp, comp_struct
|
||||
end
|
||||
|
||||
---Create and add a new component.
|
||||
---@param schema? CompSchema
|
||||
---@overload fun(): RenderComponent
|
||||
---@overload fun(schema: CompSchema): CompStruct
|
||||
function RenderComponent:create_component(schema)
|
||||
local new_comp, comp_struct = RenderComponent.create_static_component(schema)
|
||||
new_comp.data_root = self.data_root
|
||||
self:add_component(new_comp)
|
||||
|
||||
if comp_struct then
|
||||
return comp_struct
|
||||
end
|
||||
|
||||
return new_comp
|
||||
end
|
||||
|
||||
---@param component RenderComponent
|
||||
function RenderComponent:add_component(component)
|
||||
component.parent = self
|
||||
self.components[#self.components + 1] = component
|
||||
end
|
||||
|
||||
---@param component RenderComponent
|
||||
function RenderComponent:remove_component(component)
|
||||
for i, c in ipairs(self.components) do
|
||||
if c == component then
|
||||
table.remove(self.components, i)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
---@param line string?
|
||||
---@param hl_group string?
|
||||
function RenderComponent:add_line(line, hl_group)
|
||||
if line and hl_group then
|
||||
local first = #self.line_buffer
|
||||
self:add_hl(hl_group, #self.lines, first, first + #line)
|
||||
end
|
||||
|
||||
self.lines[#self.lines + 1] = self.line_buffer .. (line or "")
|
||||
self.line_buffer = ""
|
||||
end
|
||||
|
||||
---@param group string
|
||||
---@param line_idx integer
|
||||
---@param first integer
|
||||
---@param last integer
|
||||
function RenderComponent:add_hl(group, line_idx, first, last)
|
||||
self.hl[#self.hl + 1] = {
|
||||
group = group,
|
||||
line_idx = line_idx,
|
||||
first = first,
|
||||
last = last,
|
||||
}
|
||||
end
|
||||
|
||||
---@param text string
|
||||
---@param hl_group string?
|
||||
function RenderComponent:add_text(text, hl_group)
|
||||
if hl_group then
|
||||
local first = #self.line_buffer
|
||||
self:add_hl(hl_group, #self.lines, first, first + #text)
|
||||
end
|
||||
|
||||
self.line_buffer = self.line_buffer .. text
|
||||
end
|
||||
|
||||
---Finalize current line
|
||||
function RenderComponent:ln()
|
||||
self.lines[#self.lines + 1] = self.line_buffer
|
||||
self.line_buffer = ""
|
||||
end
|
||||
|
||||
function RenderComponent:clear()
|
||||
self.lines = {}
|
||||
self.hl = {}
|
||||
self.lstart = -1
|
||||
self.lend = -1
|
||||
self.height = 0
|
||||
for _, c in ipairs(self.components) do
|
||||
c:clear()
|
||||
end
|
||||
end
|
||||
|
||||
function RenderComponent:destroy()
|
||||
self.lines = nil
|
||||
self.hl = nil
|
||||
self.parent = nil
|
||||
self.context = nil
|
||||
self.data_root = nil
|
||||
for _, c in ipairs(self.components) do
|
||||
c:destroy()
|
||||
end
|
||||
self.components = nil
|
||||
end
|
||||
|
||||
function RenderComponent:isleaf()
|
||||
return (not next(self.components))
|
||||
end
|
||||
|
||||
---@param line integer
|
||||
---@return RenderComponent?
|
||||
function RenderComponent:get_comp_on_line(line)
|
||||
line = line - 1
|
||||
local ret
|
||||
|
||||
self:deep_some(function(child)
|
||||
if line >= child.lstart and line < child.lend and child:isleaf() then
|
||||
ret = child
|
||||
return true
|
||||
end
|
||||
end)
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
---@param callback fun(comp: RenderComponent, i: integer, parent: RenderComponent): boolean?
|
||||
function RenderComponent:some(callback)
|
||||
for i, child in ipairs(self.components) do
|
||||
if callback(child, i, self) then
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param callback fun(comp: RenderComponent, i: integer, parent: RenderComponent): boolean?
|
||||
function RenderComponent:deep_some(callback)
|
||||
local function wrap(comp, i, parent)
|
||||
if callback(comp, i, parent) then
|
||||
return true
|
||||
else
|
||||
return comp:some(wrap)
|
||||
end
|
||||
end
|
||||
self:some(wrap)
|
||||
end
|
||||
|
||||
function RenderComponent:leaves()
|
||||
local leaves = {}
|
||||
self:deep_some(function(comp)
|
||||
if #comp.components == 0 then
|
||||
leaves[#leaves + 1] = comp
|
||||
end
|
||||
return false
|
||||
end)
|
||||
|
||||
return leaves
|
||||
end
|
||||
|
||||
function RenderComponent:pretty_print()
|
||||
local keys = { "name", "lstart", "lend" }
|
||||
|
||||
local function recurse(depth, comp)
|
||||
local outer_padding = string.rep(" ", depth * 2)
|
||||
print(outer_padding .. "{")
|
||||
|
||||
local inner_padding = outer_padding .. " "
|
||||
for _, k in ipairs(keys) do
|
||||
print(string.format("%s%s = %s,", inner_padding, k, vim.inspect(comp[k])))
|
||||
end
|
||||
if #comp.lines > 0 then
|
||||
print(string.format("%slines = {", inner_padding))
|
||||
for _, line in ipairs(comp.lines) do
|
||||
print(string.format("%s %s,", inner_padding, vim.inspect(line)))
|
||||
end
|
||||
print(string.format("%s},", inner_padding))
|
||||
end
|
||||
for _, child in ipairs(comp.components) do
|
||||
recurse(depth + 1, child)
|
||||
end
|
||||
|
||||
print(outer_padding .. "},")
|
||||
end
|
||||
|
||||
recurse(0, self)
|
||||
end
|
||||
|
||||
---@class RenderData : diffview.Object
|
||||
---@field lines string[]
|
||||
---@field hl renderer.HlList
|
||||
---@field components RenderComponent[]
|
||||
---@field namespace integer
|
||||
local RenderData = oop.create_class("RenderData")
|
||||
|
||||
---RenderData constructor.
|
||||
function RenderData:init(ns_name)
|
||||
self.lines = {}
|
||||
self.hl = {}
|
||||
self.components = {}
|
||||
self.namespace = api.nvim_create_namespace(ns_name)
|
||||
end
|
||||
|
||||
---Create and add a new component.
|
||||
---@param schema table
|
||||
---@return RenderComponent|CompStruct
|
||||
function RenderData:create_component(schema)
|
||||
local comp_struct
|
||||
local new_comp = RenderComponent(schema and schema.name or nil)
|
||||
new_comp.data_root = self
|
||||
self:add_component(new_comp)
|
||||
|
||||
if schema then
|
||||
new_comp.context = schema.context
|
||||
comp_struct = { _name = new_comp.name, comp = new_comp }
|
||||
create_subcomponents(new_comp, comp_struct, schema)
|
||||
return comp_struct
|
||||
end
|
||||
|
||||
return new_comp
|
||||
end
|
||||
|
||||
---@param component RenderComponent
|
||||
function RenderData:add_component(component)
|
||||
self.components[#self.components + 1] = component
|
||||
end
|
||||
|
||||
---@param component RenderComponent
|
||||
function RenderData:remove_component(component)
|
||||
for i, c in ipairs(self.components) do
|
||||
if c == component then
|
||||
table.remove(self.components, i)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
---@param group string
|
||||
---@param line_idx integer
|
||||
---@param first integer
|
||||
---@param last integer
|
||||
function RenderData:add_hl(group, line_idx, first, last)
|
||||
self.hl[#self.hl + 1] = {
|
||||
group = group,
|
||||
line_idx = line_idx,
|
||||
first = first,
|
||||
last = last,
|
||||
}
|
||||
end
|
||||
|
||||
function RenderData:clear()
|
||||
self.lines = {}
|
||||
self.hl = {}
|
||||
for _, c in ipairs(self.components) do
|
||||
c:clear()
|
||||
end
|
||||
end
|
||||
|
||||
function RenderData:destroy()
|
||||
self.lines = nil
|
||||
self.hl = nil
|
||||
for _, c in ipairs(self.components) do
|
||||
c:destroy()
|
||||
end
|
||||
self.components = {}
|
||||
end
|
||||
|
||||
function M.destroy_comp_struct(schema)
|
||||
schema.comp = nil
|
||||
for k, v in pairs(schema) do
|
||||
if type(v) == "table" then
|
||||
M.destroy_comp_struct(v)
|
||||
schema[k] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Create a function to enable easily constraining the cursor to a given list of
|
||||
---components.
|
||||
---@param components RenderComponent[]
|
||||
function M.create_cursor_constraint(components)
|
||||
local stack = utils.vec_slice(components, 1)
|
||||
utils.merge_sort(stack, function(a, b)
|
||||
return a.lstart <= b.lstart
|
||||
end)
|
||||
|
||||
---Given a cursor delta or target: returns the next valid line index inside a
|
||||
---contraining component. When the cursor is trying to move out of a
|
||||
---constraint, the next component is determined by the direction the cursor is
|
||||
---moving.
|
||||
---@param winid_or_opt number|{from: number, to: number}
|
||||
---@param delta number The amount of change from the current cursor position.
|
||||
---Not needed if the first argument is a table.
|
||||
---@return number
|
||||
return function(winid_or_opt, delta)
|
||||
local line_from, line_to
|
||||
if type(winid_or_opt) == "number" then
|
||||
local cursor = api.nvim_win_get_cursor(winid_or_opt)
|
||||
line_from, line_to = cursor[1] - 1, cursor[1] - 1 + delta
|
||||
else
|
||||
line_from, line_to = winid_or_opt.from - 1, winid_or_opt.to - 1
|
||||
end
|
||||
|
||||
local min, max = math.min(line_from, line_to), math.max(line_from, line_to)
|
||||
local nearest_dist, dist, target = math.huge, nil, {}
|
||||
local top, bot
|
||||
local fstack = {}
|
||||
|
||||
for _, comp in ipairs(stack) do
|
||||
if comp.height > 0 then
|
||||
fstack[#fstack + 1] = comp
|
||||
if min <= comp.lend and max >= comp.lstart then
|
||||
if not top then
|
||||
top = { idx = #fstack, comp = comp }
|
||||
bot = top
|
||||
else
|
||||
bot = { idx = #fstack, comp = comp }
|
||||
end
|
||||
end
|
||||
|
||||
dist = math.min(math.abs(line_to - comp.lstart), math.abs(line_to - comp.lend))
|
||||
if dist < nearest_dist then
|
||||
nearest_dist = dist
|
||||
target = { idx = #fstack, comp = comp }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not top and target.comp then
|
||||
return utils.clamp(line_to + 1, target.comp.lstart + 1, target.comp.lend)
|
||||
elseif top then
|
||||
if line_to < line_from then
|
||||
-- moving up
|
||||
if line_to < top.comp.lstart and top.idx > 1 then
|
||||
target = { idx = top.idx - 1, comp = fstack[top.idx - 1] }
|
||||
else
|
||||
target = top
|
||||
end
|
||||
return utils.clamp(line_to + 1, target.comp.lstart + 1, target.comp.lend)
|
||||
else
|
||||
-- moving down
|
||||
if line_to >= bot.comp.lend and bot.idx < #fstack then
|
||||
target = { idx = bot.idx + 1, comp = fstack[bot.idx + 1] }
|
||||
else
|
||||
target = bot
|
||||
end
|
||||
return utils.clamp(line_to + 1, target.comp.lstart + 1, target.comp.lend)
|
||||
end
|
||||
end
|
||||
|
||||
return line_from
|
||||
end
|
||||
end
|
||||
|
||||
---@param line_idx integer
|
||||
---@param lines string[]
|
||||
---@param hl_data renderer.HlData[]
|
||||
---@param component RenderComponent
|
||||
---@return integer
|
||||
local function process_component(line_idx, lines, hl_data, component)
|
||||
if #component.components > 0 then
|
||||
component.lstart = line_idx
|
||||
for _, c in ipairs(component.components) do
|
||||
line_idx = process_component(line_idx, lines, hl_data, c)
|
||||
end
|
||||
|
||||
component.lend = line_idx
|
||||
component.height = component.lend - component.lstart
|
||||
return line_idx
|
||||
else
|
||||
for _, line in ipairs(component.lines) do
|
||||
lines[#lines + 1] = line
|
||||
end
|
||||
|
||||
component.hl.offset = line_idx
|
||||
hl_data[#hl_data + 1] = component.hl
|
||||
component.height = #component.lines
|
||||
|
||||
if component.height > 0 then
|
||||
component.lstart = line_idx
|
||||
component.lend = line_idx + component.height
|
||||
else
|
||||
component.lstart = line_idx
|
||||
component.lend = line_idx
|
||||
end
|
||||
|
||||
return component.lend
|
||||
end
|
||||
end
|
||||
|
||||
---Render the given render data to the given buffer.
|
||||
---@param bufid integer
|
||||
---@param data RenderData
|
||||
function M.render(bufid, data)
|
||||
if not api.nvim_buf_is_loaded(bufid) then
|
||||
return
|
||||
end
|
||||
|
||||
local last = vim.loop.hrtime()
|
||||
local was_modifiable = api.nvim_buf_get_option(bufid, "modifiable")
|
||||
api.nvim_buf_set_option(bufid, "modifiable", true)
|
||||
|
||||
local lines, hl_data
|
||||
local line_idx = 0
|
||||
if #data.components > 0 then
|
||||
lines = {}
|
||||
hl_data = {}
|
||||
for _, c in ipairs(data.components) do
|
||||
line_idx = process_component(line_idx, lines, hl_data, c)
|
||||
end
|
||||
else
|
||||
lines = data.lines
|
||||
hl_data = { data.hl }
|
||||
end
|
||||
|
||||
api.nvim_buf_set_lines(bufid, 0, -1, false, lines)
|
||||
api.nvim_buf_clear_namespace(bufid, data.namespace, 0, -1)
|
||||
for _, t in ipairs(hl_data) do
|
||||
for _, hl in ipairs(t) do
|
||||
api.nvim_buf_add_highlight(
|
||||
bufid,
|
||||
data.namespace,
|
||||
hl.group,
|
||||
hl.line_idx + (t.offset or 0),
|
||||
hl.first,
|
||||
hl.last
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
api.nvim_buf_set_option(bufid, "modifiable", was_modifiable)
|
||||
M.last_draw_time = (vim.loop.hrtime() - last) / 1000000
|
||||
end
|
||||
|
||||
M.RenderComponent = RenderComponent
|
||||
M.RenderData = RenderData
|
||||
return M
|
||||
@ -0,0 +1,50 @@
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
---@class Scanner : diffview.Object
|
||||
---@operator call : Scanner
|
||||
---@field lines string[]
|
||||
---@field line_idx integer
|
||||
local Scanner = oop.create_class("Scanner")
|
||||
|
||||
---@param source string|string[]
|
||||
function Scanner:init(source)
|
||||
if type(source) == "table" then
|
||||
self.lines = source
|
||||
else
|
||||
self.lines = vim.split(source, "\r?\n")
|
||||
end
|
||||
|
||||
self.line_idx = 0
|
||||
end
|
||||
|
||||
---Peek the nth line after the current line.
|
||||
---@param n? integer # (default: 1)
|
||||
---@return string?
|
||||
function Scanner:peek_line(n)
|
||||
return self.lines[self.line_idx + math.max(1, n or 1)]
|
||||
end
|
||||
|
||||
function Scanner:cur_line()
|
||||
return self.lines[self.line_idx]
|
||||
end
|
||||
|
||||
function Scanner:cur_line_idx()
|
||||
return self.line_idx
|
||||
end
|
||||
|
||||
---Advance the scanner to the next line.
|
||||
---@return string?
|
||||
function Scanner:next_line()
|
||||
self.line_idx = self.line_idx + 1
|
||||
return self.lines[self.line_idx]
|
||||
end
|
||||
|
||||
---Advance the scanner by n lines.
|
||||
---@param n? integer # (default: 1)
|
||||
---@return string?
|
||||
function Scanner:skip_line(n)
|
||||
self.line_idx = self.line_idx + math.max(1, n or 1)
|
||||
return self.lines[self.line_idx]
|
||||
end
|
||||
|
||||
return Scanner
|
||||
@ -0,0 +1,360 @@
|
||||
local lazy = require("diffview.lazy")
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
local Diff1 = lazy.access("diffview.scene.layouts.diff_1", "Diff1") ---@type Diff1|LazyModule
|
||||
local Diff2 = lazy.access("diffview.scene.layouts.diff_2", "Diff2") ---@type Diff2|LazyModule
|
||||
local File = lazy.access("diffview.vcs.file", "File") ---@type vcs.File|LazyModule
|
||||
local RevType = lazy.access("diffview.vcs.rev", "RevType") ---@type RevType|LazyModule
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
|
||||
local api = vim.api
|
||||
local pl = lazy.access(utils, "path") ---@type PathLib
|
||||
|
||||
local M = {}
|
||||
|
||||
local fstat_cache = {}
|
||||
|
||||
---@class GitStats
|
||||
---@field additions integer
|
||||
---@field deletions integer
|
||||
---@field conflicts integer
|
||||
|
||||
---@class RevMap
|
||||
---@field a Rev
|
||||
---@field b Rev
|
||||
---@field c Rev
|
||||
---@field d Rev
|
||||
|
||||
---@class FileEntry : diffview.Object
|
||||
---@field adapter GitAdapter
|
||||
---@field path string
|
||||
---@field oldpath string
|
||||
---@field absolute_path string
|
||||
---@field parent_path string
|
||||
---@field basename string
|
||||
---@field extension string
|
||||
---@field revs RevMap
|
||||
---@field layout Layout
|
||||
---@field status string
|
||||
---@field stats GitStats
|
||||
---@field kind vcs.FileKind
|
||||
---@field commit Commit|nil
|
||||
---@field merge_ctx vcs.MergeContext?
|
||||
---@field active boolean
|
||||
---@field opened boolean
|
||||
local FileEntry = oop.create_class("FileEntry")
|
||||
|
||||
---@class FileEntry.init.Opt
|
||||
---@field adapter GitAdapter
|
||||
---@field path string
|
||||
---@field oldpath string
|
||||
---@field revs RevMap
|
||||
---@field layout Layout
|
||||
---@field status string
|
||||
---@field stats GitStats
|
||||
---@field kind vcs.FileKind
|
||||
---@field commit? Commit
|
||||
---@field merge_ctx? vcs.MergeContext
|
||||
|
||||
---FileEntry constructor
|
||||
---@param opt FileEntry.init.Opt
|
||||
function FileEntry:init(opt)
|
||||
self.adapter = opt.adapter
|
||||
self.path = opt.path
|
||||
self.oldpath = opt.oldpath
|
||||
self.absolute_path = pl:absolute(opt.path, opt.adapter.ctx.toplevel)
|
||||
self.parent_path = pl:parent(opt.path) or ""
|
||||
self.basename = pl:basename(opt.path)
|
||||
self.extension = pl:extension(opt.path)
|
||||
self.revs = opt.revs
|
||||
self.layout = opt.layout
|
||||
self.status = opt.status
|
||||
self.stats = opt.stats
|
||||
self.kind = opt.kind
|
||||
self.commit = opt.commit
|
||||
self.merge_ctx = opt.merge_ctx
|
||||
self.active = false
|
||||
self.opened = false
|
||||
end
|
||||
|
||||
function FileEntry:destroy()
|
||||
for _, f in ipairs(self.layout:files()) do
|
||||
f:destroy()
|
||||
end
|
||||
|
||||
self.layout:destroy()
|
||||
end
|
||||
|
||||
---@param new_head Rev
|
||||
function FileEntry:update_heads(new_head)
|
||||
for _, file in ipairs(self.layout:files()) do
|
||||
if file.rev.track_head then
|
||||
file:dispose_buffer()
|
||||
file.rev = new_head
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param flag boolean
|
||||
function FileEntry:set_active(flag)
|
||||
self.active = flag
|
||||
|
||||
for _, f in ipairs(self.layout:files()) do
|
||||
f.active = flag
|
||||
end
|
||||
end
|
||||
|
||||
---@param target_layout Layout
|
||||
function FileEntry:convert_layout(target_layout)
|
||||
local get_data
|
||||
|
||||
for _, file in ipairs(self.layout:files()) do
|
||||
if file.get_data then
|
||||
get_data = file.get_data
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
local function create_file(rev, symbol)
|
||||
return File({
|
||||
adapter = self.adapter,
|
||||
path = symbol == "a" and self.oldpath or self.path,
|
||||
kind = self.kind,
|
||||
commit = self.commit,
|
||||
get_data = get_data,
|
||||
rev = rev,
|
||||
nulled = select(2, pcall(target_layout.should_null, rev, self.status, symbol)),
|
||||
}) --[[@as vcs.File ]]
|
||||
end
|
||||
|
||||
self.layout = target_layout({
|
||||
a = utils.tbl_access(self.layout, "a.file") or create_file(self.revs.a, "a"),
|
||||
b = utils.tbl_access(self.layout, "b.file") or create_file(self.revs.b, "b"),
|
||||
c = utils.tbl_access(self.layout, "c.file") or create_file(self.revs.c, "c"),
|
||||
d = utils.tbl_access(self.layout, "d.file") or create_file(self.revs.d, "d"),
|
||||
})
|
||||
self:update_merge_context()
|
||||
end
|
||||
|
||||
---@param stat? table
|
||||
function FileEntry:validate_stage_buffers(stat)
|
||||
stat = stat or pl:stat(pl:join(self.adapter.ctx.dir, "index"))
|
||||
local cached_stat = utils.tbl_access(fstat_cache, { self.adapter.ctx.toplevel, "index" })
|
||||
|
||||
if stat and (not cached_stat or cached_stat.mtime < stat.mtime.sec) then
|
||||
for _, f in ipairs(self.layout:files()) do
|
||||
if f.rev.type == RevType.STAGE and f:is_valid() then
|
||||
if f.rev.stage > 0 then
|
||||
-- We only care about stage 0 here
|
||||
f:dispose_buffer()
|
||||
else
|
||||
local is_modified = vim.bo[f.bufnr].modified
|
||||
|
||||
if f.blob_hash then
|
||||
local new_hash = self.adapter:file_blob_hash(f.path)
|
||||
|
||||
if new_hash and new_hash ~= f.blob_hash then
|
||||
if is_modified then
|
||||
utils.warn((
|
||||
"A file was changed in the index since you started editing it!"
|
||||
.. " Be careful not to lose any staged changes when writing to this buffer: %s"
|
||||
):format(api.nvim_buf_get_name(f.bufnr)))
|
||||
else
|
||||
f:dispose_buffer()
|
||||
end
|
||||
end
|
||||
elseif not is_modified then
|
||||
-- Should be very rare that we don't have an index-buffer's blob
|
||||
-- hash. But in that case, we can't warn the user when a file
|
||||
-- changes in the index while they're editing its index buffer.
|
||||
f:dispose_buffer()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Update winbar info
|
||||
---@param ctx? vcs.MergeContext
|
||||
function FileEntry:update_merge_context(ctx)
|
||||
ctx = ctx or self.merge_ctx
|
||||
if ctx then self.merge_ctx = ctx else return end
|
||||
|
||||
local layout = self.layout --[[@as Diff4 ]]
|
||||
|
||||
if layout.a then
|
||||
layout.a.file.winbar = (" OURS (Current changes) %s %s"):format(
|
||||
(ctx.ours.hash):sub(1, 10),
|
||||
ctx.ours.ref_names and ("(" .. ctx.ours.ref_names .. ")") or ""
|
||||
)
|
||||
end
|
||||
|
||||
if layout.b then
|
||||
layout.b.file.winbar = " LOCAL (Working tree)"
|
||||
end
|
||||
|
||||
if layout.c then
|
||||
layout.c.file.winbar = (" THEIRS (Incoming changes) %s %s"):format(
|
||||
(ctx.theirs.hash):sub(1, 10),
|
||||
ctx.theirs.ref_names and ("(" .. ctx.theirs.ref_names .. ")") or ""
|
||||
)
|
||||
end
|
||||
|
||||
if layout.d then
|
||||
layout.d.file.winbar = (" BASE (Common ancestor) %s %s"):format(
|
||||
(ctx.base.hash):sub(1, 10),
|
||||
ctx.base.ref_names and ("(" .. ctx.base.ref_names .. ")") or ""
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
---Derive custom folds from the hunks in a diff patch.
|
||||
---@param diff diff.FileEntry
|
||||
function FileEntry:update_patch_folds(diff)
|
||||
if not self.layout:instanceof(Diff2.__get()) then return end
|
||||
|
||||
local layout = self.layout --[[@as Diff2 ]]
|
||||
local folds = {
|
||||
a = utils.tbl_set(layout.a.file, { "custom_folds" }, { type = "diff_patch" }),
|
||||
b = utils.tbl_set(layout.b.file, { "custom_folds" }, { type = "diff_patch" }),
|
||||
}
|
||||
|
||||
local lcount_a = api.nvim_buf_line_count(layout.a.file.bufnr)
|
||||
local lcount_b = api.nvim_buf_line_count(layout.b.file.bufnr)
|
||||
|
||||
local prev_last_old, prev_last_new = 0, 0
|
||||
|
||||
for i = 1, #diff.hunks + 1 do
|
||||
local hunk = diff.hunks[i]
|
||||
local first_old, last_old, first_new, last_new
|
||||
|
||||
if hunk then
|
||||
first_old = hunk.old_row
|
||||
last_old = first_old + hunk.old_size - 1
|
||||
first_new = hunk.new_row
|
||||
last_new = first_new + hunk.new_size - 1
|
||||
else
|
||||
first_old = lcount_a + 1
|
||||
first_new = lcount_b + 1
|
||||
end
|
||||
|
||||
if first_old - prev_last_old > 1 then
|
||||
local prev_fold = folds.a[#folds.a]
|
||||
|
||||
if prev_fold and (prev_last_old + 1) - prev_fold[2] == 1 then
|
||||
-- This fold is right next to the previous: merge the folds
|
||||
prev_fold[2] = first_old - 1
|
||||
else
|
||||
table.insert(folds.a, { prev_last_old + 1, first_old - 1 })
|
||||
end
|
||||
-- print("old:", folds.a[#folds.a][1], folds.a[#folds.a][2])
|
||||
end
|
||||
|
||||
if first_new - prev_last_new > 1 then
|
||||
local prev_fold = folds.b[#folds.b]
|
||||
|
||||
if prev_fold and (prev_last_new + 1) - prev_fold[2] == 1 then
|
||||
-- This fold is right next to the previous: merge the folds
|
||||
prev_fold[2] = first_new - 1
|
||||
else
|
||||
table.insert(folds.b, { prev_last_new + 1, first_new - 1 })
|
||||
end
|
||||
-- print("new:", folds.b[#folds.b][1], folds.b[#folds.b][2])
|
||||
end
|
||||
|
||||
prev_last_old = last_old
|
||||
prev_last_new = last_new
|
||||
end
|
||||
end
|
||||
|
||||
---Check if the entry has custom diff patch folds.
|
||||
---@return boolean
|
||||
function FileEntry:has_patch_folds()
|
||||
for _, file in ipairs(self.layout:files()) do
|
||||
if not file.custom_folds or file.custom_folds.type ~= "diff_patch" then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
function FileEntry:is_null_entry()
|
||||
return self.path == "null" and self.layout:get_main_win().file == File.NULL_FILE
|
||||
end
|
||||
|
||||
---@static
|
||||
---@param adapter VCSAdapter
|
||||
function FileEntry.update_index_stat(adapter, stat)
|
||||
stat = stat or pl:stat(pl:join(adapter.ctx.toplevel, "index"))
|
||||
|
||||
if stat then
|
||||
if not fstat_cache[adapter.ctx.toplevel] then
|
||||
fstat_cache[adapter.ctx.toplevel] = {}
|
||||
end
|
||||
|
||||
fstat_cache[adapter.ctx.toplevel].index = {
|
||||
mtime = stat.mtime.sec,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
---@class FileEntry.with_layout.Opt : FileEntry.init.Opt
|
||||
---@field nulled boolean
|
||||
---@field get_data git.FileDataProducer?
|
||||
|
||||
---@param layout_class Layout (class)
|
||||
---@param opt FileEntry.with_layout.Opt
|
||||
---@return FileEntry
|
||||
function FileEntry.with_layout(layout_class, opt)
|
||||
local function create_file(rev, symbol)
|
||||
return File({
|
||||
adapter = opt.adapter,
|
||||
path = symbol == "a" and opt.oldpath or opt.path,
|
||||
kind = opt.kind,
|
||||
commit = opt.commit,
|
||||
get_data = opt.get_data,
|
||||
rev = rev,
|
||||
nulled = utils.sate(
|
||||
opt.nulled,
|
||||
select(2, pcall(layout_class.should_null, rev, opt.status, symbol))
|
||||
),
|
||||
}) --[[@as vcs.File ]]
|
||||
end
|
||||
|
||||
return FileEntry({
|
||||
adapter = opt.adapter,
|
||||
path = opt.path,
|
||||
oldpath = opt.oldpath,
|
||||
status = opt.status,
|
||||
stats = opt.stats,
|
||||
kind = opt.kind,
|
||||
commit = opt.commit,
|
||||
revs = opt.revs,
|
||||
layout = layout_class({
|
||||
a = create_file(opt.revs.a, "a"),
|
||||
b = create_file(opt.revs.b, "b"),
|
||||
c = create_file(opt.revs.c, "c"),
|
||||
d = create_file(opt.revs.d, "d"),
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
function FileEntry.new_null_entry(adapter)
|
||||
return FileEntry({
|
||||
adapter = adapter,
|
||||
path = "null",
|
||||
kind = "working",
|
||||
binary = false,
|
||||
nulled = true,
|
||||
layout = Diff1({
|
||||
b = File.NULL_FILE,
|
||||
})
|
||||
})
|
||||
end
|
||||
|
||||
M.FileEntry = FileEntry
|
||||
|
||||
return M
|
||||
@ -0,0 +1,312 @@
|
||||
local async = require("diffview.async")
|
||||
local lazy = require("diffview.lazy")
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
local EventEmitter = lazy.access("diffview.events", "EventEmitter") ---@type EventEmitter|LazyModule
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
|
||||
local api = vim.api
|
||||
local await = async.await
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class Layout : diffview.Object
|
||||
---@field windows Window[]
|
||||
---@field emitter EventEmitter
|
||||
---@field pivot_producer fun(): integer?
|
||||
---@field name string
|
||||
---@field state table
|
||||
local Layout = oop.create_class("Layout")
|
||||
|
||||
function Layout:init(opt)
|
||||
opt = opt or {}
|
||||
self.windows = opt.windows or {}
|
||||
self.emitter = opt.emitter or EventEmitter()
|
||||
self.state = {}
|
||||
end
|
||||
|
||||
---@diagnostic disable: unused-local, missing-return
|
||||
|
||||
---@abstract
|
||||
---@param self Layout
|
||||
---@param pivot? integer The window ID of the window around which the layout will be created.
|
||||
Layout.create = async.void(function(self, pivot) oop.abstract_stub() end)
|
||||
|
||||
---@abstract
|
||||
---@param rev Rev
|
||||
---@param status string Git status symbol.
|
||||
---@param sym string
|
||||
---@return boolean
|
||||
function Layout.should_null(rev, status, sym) oop.abstract_stub() end
|
||||
|
||||
---@abstract
|
||||
---@param self Layout
|
||||
---@param entry FileEntry
|
||||
Layout.use_entry = async.void(function(self, entry) oop.abstract_stub() end)
|
||||
|
||||
---@abstract
|
||||
---@return Window
|
||||
function Layout:get_main_win() oop.abstract_stub() end
|
||||
|
||||
---@diagnostic enable: unused-local, missing-return
|
||||
|
||||
function Layout:destroy()
|
||||
for _, win in ipairs(self.windows) do
|
||||
win:destroy()
|
||||
end
|
||||
end
|
||||
|
||||
function Layout:clone()
|
||||
local clone = self.class({ emitter = self.emitter }) --[[@as Layout ]]
|
||||
|
||||
for i, win in ipairs(self.windows) do
|
||||
clone.windows[i]:set_id(win.id)
|
||||
clone.windows[i]:set_file(win.file)
|
||||
end
|
||||
|
||||
return clone
|
||||
end
|
||||
|
||||
function Layout:create_pre()
|
||||
self.state.save_equalalways = vim.o.equalalways
|
||||
vim.opt.equalalways = true
|
||||
end
|
||||
|
||||
---@param self Layout
|
||||
Layout.create_post = async.void(function(self)
|
||||
await(self:open_files())
|
||||
vim.opt.equalalways = self.state.save_equalalways
|
||||
end)
|
||||
|
||||
---Check if any of the windows in the lauout are focused.
|
||||
---@return boolean
|
||||
function Layout:is_focused()
|
||||
for _, win in ipairs(self.windows) do
|
||||
if win:is_focused() then return true end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
---@param ... Window
|
||||
function Layout:use_windows(...)
|
||||
local wins = { ... }
|
||||
|
||||
for i = 1, select("#", ...) do
|
||||
local win = wins[i]
|
||||
win.parent = self
|
||||
|
||||
if utils.vec_indexof(self.windows, win) == -1 then
|
||||
table.insert(self.windows, win)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Find or create a window that can be used as a pivot during layout
|
||||
---creation.
|
||||
---@return integer winid
|
||||
function Layout:find_pivot()
|
||||
local last_win = api.nvim_get_current_win()
|
||||
|
||||
for _, win in ipairs(self.windows) do
|
||||
if win:is_valid() then
|
||||
local ret
|
||||
|
||||
api.nvim_win_call(win.id, function()
|
||||
vim.cmd("aboveleft vsp")
|
||||
ret = api.nvim_get_current_win()
|
||||
end)
|
||||
|
||||
return ret
|
||||
end
|
||||
end
|
||||
|
||||
if vim.is_callable(self.pivot_producer) then
|
||||
local ret = self.pivot_producer()
|
||||
|
||||
if ret then
|
||||
return ret
|
||||
end
|
||||
end
|
||||
|
||||
vim.cmd("1windo belowright vsp")
|
||||
|
||||
local pivot = api.nvim_get_current_win()
|
||||
|
||||
if api.nvim_win_is_valid(last_win) then
|
||||
api.nvim_set_current_win(last_win)
|
||||
end
|
||||
|
||||
return pivot
|
||||
end
|
||||
|
||||
---@return vcs.File[]
|
||||
function Layout:files()
|
||||
return utils.tbl_fmap(self.windows, function(v)
|
||||
return v.file
|
||||
end)
|
||||
end
|
||||
|
||||
---Check if the buffers for all the files in the layout are loaded.
|
||||
---@return boolean
|
||||
function Layout:is_files_loaded()
|
||||
for _, f in ipairs(self:files()) do
|
||||
if not f:is_valid() then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
---@param self Layout
|
||||
Layout.open_files = async.void(function(self)
|
||||
if #self:files() < #self.windows then
|
||||
self:open_null()
|
||||
self.emitter:emit("files_opened")
|
||||
return
|
||||
end
|
||||
|
||||
vim.cmd("diffoff!")
|
||||
|
||||
if not self:is_files_loaded() then
|
||||
self:open_null()
|
||||
|
||||
-- Wait for all files to be loaded before opening
|
||||
for _, win in ipairs(self.windows) do
|
||||
await(win:load_file())
|
||||
end
|
||||
end
|
||||
|
||||
await(async.scheduler())
|
||||
|
||||
for _, win in ipairs(self.windows) do
|
||||
await(win:open_file())
|
||||
end
|
||||
|
||||
self:sync_scroll()
|
||||
self.emitter:emit("files_opened")
|
||||
end)
|
||||
|
||||
function Layout:open_null()
|
||||
for _, win in ipairs(self.windows) do
|
||||
win:open_null()
|
||||
end
|
||||
end
|
||||
|
||||
---Recover a broken layout.
|
||||
---@param pivot? integer
|
||||
function Layout:recover(pivot)
|
||||
pivot = pivot or self:find_pivot()
|
||||
---@cast pivot -?
|
||||
|
||||
for _, win in ipairs(self.windows) do
|
||||
if win.id ~= pivot then
|
||||
pcall(api.nvim_win_close, win.id, true)
|
||||
end
|
||||
end
|
||||
|
||||
self.windows = {}
|
||||
self:create(pivot)
|
||||
end
|
||||
|
||||
---@alias Layout.State { [Window]: boolean, valid: boolean }
|
||||
|
||||
---Check the validity of all composing layout windows.
|
||||
---@return Layout.State
|
||||
function Layout:validate()
|
||||
if not next(self.windows) then
|
||||
return { valid = false }
|
||||
end
|
||||
|
||||
local state = { valid = true }
|
||||
|
||||
for _, win in ipairs(self.windows) do
|
||||
state[win] = win:is_valid()
|
||||
if not state[win] then
|
||||
state.valid = false
|
||||
end
|
||||
end
|
||||
|
||||
return state
|
||||
end
|
||||
|
||||
---Check the validity if the layout.
|
||||
---@return boolean
|
||||
function Layout:is_valid()
|
||||
return self:validate().valid
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
function Layout:is_nulled()
|
||||
if not self:is_valid() then return false end
|
||||
|
||||
for _, win in ipairs(self.windows) do
|
||||
if not win:is_nulled() then return false end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
---Validate the layout and recover if necessary.
|
||||
function Layout:ensure()
|
||||
local state = self:validate()
|
||||
|
||||
if not state.valid then
|
||||
self:recover()
|
||||
end
|
||||
end
|
||||
|
||||
---Save window local options.
|
||||
function Layout:save_winopts()
|
||||
for _, win in ipairs(self.windows) do
|
||||
win:_save_winopts()
|
||||
end
|
||||
end
|
||||
|
||||
---Restore saved window local options.
|
||||
function Layout:restore_winopts()
|
||||
for _, win in ipairs(self.windows) do
|
||||
win:_restore_winopts()
|
||||
end
|
||||
end
|
||||
|
||||
function Layout:detach_files()
|
||||
for _, win in ipairs(self.windows) do
|
||||
win:detach_file()
|
||||
end
|
||||
end
|
||||
|
||||
---Sync the scrollbind.
|
||||
function Layout:sync_scroll()
|
||||
local curwin = api.nvim_get_current_win()
|
||||
local target, max = nil, 0
|
||||
|
||||
for _, win in ipairs(self.windows) do
|
||||
local lcount = api.nvim_buf_line_count(api.nvim_win_get_buf(win.id))
|
||||
if lcount > max then target, max = win, lcount end
|
||||
end
|
||||
|
||||
local main_win = self:get_main_win()
|
||||
local cursor = api.nvim_win_get_cursor(main_win.id)
|
||||
|
||||
for _, win in ipairs(self.windows) do
|
||||
api.nvim_win_call(win.id, function()
|
||||
if win == target then
|
||||
-- Scroll to trigger the scrollbind and sync the windows. This works more
|
||||
-- consistently than calling `:syncbind`.
|
||||
vim.cmd("norm! " .. api.nvim_replace_termcodes("<c-e><c-y>", true, true, true))
|
||||
end
|
||||
|
||||
if win.id ~= curwin then
|
||||
api.nvim_exec_autocmds("WinLeave", { modeline = false })
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Cursor will sometimes move +- the value of 'scrolloff'
|
||||
api.nvim_win_set_cursor(target.id, cursor)
|
||||
end
|
||||
|
||||
M.Layout = Layout
|
||||
return M
|
||||
@ -0,0 +1,169 @@
|
||||
local async = require("diffview.async")
|
||||
local lazy = require("diffview.lazy")
|
||||
local Layout = require("diffview.scene.layout").Layout
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
local Diff3 = lazy.access("diffview.scene.layouts.diff_3", "Diff3") ---@type Diff3|LazyModule
|
||||
local Diff4 = lazy.access("diffview.scene.layouts.diff_4", "Diff4") ---@type Diff4|LazyModule
|
||||
local File = lazy.access("diffview.vcs.file", "File") ---@type vcs.File|LazyModule
|
||||
local Rev = lazy.access("diffview.vcs.rev", "Rev") ---@type Rev|LazyModule
|
||||
local RevType = lazy.access("diffview.vcs.rev", "RevType") ---@type RevType|LazyModule
|
||||
local Window = lazy.access("diffview.scene.window", "Window") ---@type Window|LazyModule
|
||||
|
||||
local api = vim.api
|
||||
local await = async.await
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class Diff1 : Layout
|
||||
---@field b Window
|
||||
local Diff1 = oop.create_class("Diff1", Layout)
|
||||
|
||||
---@alias Diff1.WindowSymbol "b"
|
||||
|
||||
---@class Diff1.init.Opt
|
||||
---@field b vcs.File
|
||||
---@field winid_b integer
|
||||
|
||||
Diff1.name = "diff1_plain"
|
||||
|
||||
---@param opt Diff1.init.Opt
|
||||
function Diff1:init(opt)
|
||||
self:super()
|
||||
self.b = Window({ file = opt.b, id = opt.winid_b })
|
||||
self:use_windows(self.b)
|
||||
end
|
||||
|
||||
---@override
|
||||
---@param self Diff1
|
||||
---@param pivot integer?
|
||||
Diff1.create = async.void(function(self, pivot)
|
||||
self:create_pre()
|
||||
local curwin
|
||||
|
||||
pivot = pivot or self:find_pivot()
|
||||
assert(api.nvim_win_is_valid(pivot), "Layout creation requires a valid window pivot!")
|
||||
|
||||
for _, win in ipairs(self.windows) do
|
||||
if win.id ~= pivot then
|
||||
win:close(true)
|
||||
end
|
||||
end
|
||||
|
||||
api.nvim_win_call(pivot, function()
|
||||
vim.cmd("aboveleft vsp")
|
||||
curwin = api.nvim_get_current_win()
|
||||
|
||||
if self.b then
|
||||
self.b:set_id(curwin)
|
||||
else
|
||||
self.b = Window({ id = curwin })
|
||||
end
|
||||
end)
|
||||
|
||||
api.nvim_win_close(pivot, true)
|
||||
self.windows = { self.b }
|
||||
await(self:create_post())
|
||||
end)
|
||||
|
||||
---@param file vcs.File
|
||||
function Diff1:set_file_b(file)
|
||||
self.b:set_file(file)
|
||||
file.symbol = "b"
|
||||
end
|
||||
|
||||
---@param self Diff1
|
||||
---@param entry FileEntry
|
||||
Diff1.use_entry = async.void(function(self, entry)
|
||||
local layout = entry.layout --[[@as Diff1 ]]
|
||||
assert(layout:instanceof(Diff1))
|
||||
|
||||
self:set_file_b(layout.b.file)
|
||||
|
||||
if self:is_valid() then
|
||||
await(self:open_files())
|
||||
end
|
||||
end)
|
||||
|
||||
function Diff1:get_main_win()
|
||||
return self.b
|
||||
end
|
||||
|
||||
---@param layout Diff3
|
||||
---@return Diff3
|
||||
function Diff1:to_diff3(layout)
|
||||
assert(layout:instanceof(Diff3.__get()))
|
||||
local main = self:get_main_win().file
|
||||
|
||||
return layout({
|
||||
a = File({
|
||||
adapter = main.adapter,
|
||||
path = main.path,
|
||||
kind = main.kind,
|
||||
commit = main.commit,
|
||||
get_data = main.get_data,
|
||||
rev = Rev(RevType.STAGE, 2),
|
||||
nulled = false, -- FIXME
|
||||
}),
|
||||
b = self.b.file,
|
||||
c = File({
|
||||
adapter = main.adapter,
|
||||
path = main.path,
|
||||
kind = main.kind,
|
||||
commit = main.commit,
|
||||
get_data = main.get_data,
|
||||
rev = Rev(RevType.STAGE, 3),
|
||||
nulled = false, -- FIXME
|
||||
}),
|
||||
})
|
||||
end
|
||||
|
||||
---@param layout Diff4
|
||||
---@return Diff4
|
||||
function Diff1:to_diff4(layout)
|
||||
assert(layout:instanceof(Diff4.__get()))
|
||||
local main = self:get_main_win().file
|
||||
|
||||
return layout({
|
||||
a = File({
|
||||
adapter = main.adapter,
|
||||
path = main.path,
|
||||
kind = main.kind,
|
||||
commit = main.commit,
|
||||
get_data = main.get_data,
|
||||
rev = Rev(RevType.STAGE, 2),
|
||||
nulled = false, -- FIXME
|
||||
}),
|
||||
b = self.b.file,
|
||||
c = File({
|
||||
adapter = main.adapter,
|
||||
path = main.path,
|
||||
kind = main.kind,
|
||||
commit = main.commit,
|
||||
get_data = main.get_data,
|
||||
rev = Rev(RevType.STAGE, 3),
|
||||
nulled = false, -- FIXME
|
||||
}),
|
||||
d = File({
|
||||
adapter = main.adapter,
|
||||
path = main.path,
|
||||
kind = main.kind,
|
||||
commit = main.commit,
|
||||
get_data = main.get_data,
|
||||
rev = Rev(RevType.STAGE, 1),
|
||||
nulled = false, -- FIXME
|
||||
})
|
||||
})
|
||||
end
|
||||
|
||||
---FIXME
|
||||
---@override
|
||||
---@param rev Rev
|
||||
---@param status string Git status symbol.
|
||||
---@param sym Diff1.WindowSymbol
|
||||
function Diff1.should_null(rev, status, sym)
|
||||
return false
|
||||
end
|
||||
|
||||
M.Diff1 = Diff1
|
||||
return M
|
||||
@ -0,0 +1,91 @@
|
||||
local async = require("diffview.async")
|
||||
local RevType = require("diffview.vcs.rev").RevType
|
||||
local Window = require("diffview.scene.window").Window
|
||||
local Layout = require("diffview.scene.layout").Layout
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
local await = async.await
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class Diff2 : Layout
|
||||
---@field a Window
|
||||
---@field b Window
|
||||
local Diff2 = oop.create_class("Diff2", Layout)
|
||||
|
||||
---@alias Diff2.WindowSymbol "a"|"b"
|
||||
|
||||
---@class Diff2.init.Opt
|
||||
---@field a vcs.File
|
||||
---@field b vcs.File
|
||||
---@field winid_a integer
|
||||
---@field winid_b integer
|
||||
|
||||
---@param opt Diff2.init.Opt
|
||||
function Diff2:init(opt)
|
||||
self:super()
|
||||
self.a = Window({ file = opt.a, id = opt.winid_a })
|
||||
self.b = Window({ file = opt.b, id = opt.winid_b })
|
||||
self:use_windows(self.a, self.b)
|
||||
end
|
||||
|
||||
---@param file vcs.File
|
||||
function Diff2:set_file_a(file)
|
||||
self.a:set_file(file)
|
||||
file.symbol = "a"
|
||||
end
|
||||
|
||||
---@param file vcs.File
|
||||
function Diff2:set_file_b(file)
|
||||
self.b:set_file(file)
|
||||
file.symbol = "b"
|
||||
end
|
||||
|
||||
---@param self Diff2
|
||||
---@param entry FileEntry
|
||||
Diff2.use_entry = async.void(function(self, entry)
|
||||
local layout = entry.layout --[[@as Diff2 ]]
|
||||
assert(layout:instanceof(Diff2))
|
||||
|
||||
self:set_file_a(layout.a.file)
|
||||
self:set_file_b(layout.b.file)
|
||||
|
||||
if self:is_valid() then
|
||||
await(self:open_files())
|
||||
end
|
||||
end)
|
||||
|
||||
function Diff2:get_main_win()
|
||||
return self.b
|
||||
end
|
||||
|
||||
---@override
|
||||
---@param rev Rev
|
||||
---@param status string Git status symbol.
|
||||
---@param sym Diff2.WindowSymbol
|
||||
function Diff2.should_null(rev, status, sym)
|
||||
assert(sym == "a" or sym == "b")
|
||||
|
||||
if rev.type == RevType.LOCAL then
|
||||
return status == "D"
|
||||
|
||||
elseif rev.type == RevType.COMMIT then
|
||||
if sym == "a" then
|
||||
return vim.tbl_contains({ "?", "A" }, status)
|
||||
end
|
||||
|
||||
return false
|
||||
|
||||
elseif rev.type == RevType.STAGE then
|
||||
if sym == "a" then
|
||||
return vim.tbl_contains({ "?", "A" }, status)
|
||||
elseif sym == "b" then
|
||||
return status == "D"
|
||||
end
|
||||
end
|
||||
|
||||
error(("Unexpected state! %s, %s, %s"):format(rev, status, sym))
|
||||
end
|
||||
|
||||
M.Diff2 = Diff2
|
||||
return M
|
||||
@ -0,0 +1,71 @@
|
||||
local async = require("diffview.async")
|
||||
local Window = require("diffview.scene.window").Window
|
||||
local Diff2 = require("diffview.scene.layouts.diff_2").Diff2
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
local api = vim.api
|
||||
local await = async.await
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class Diff2Hor : Diff2
|
||||
local Diff2Hor = oop.create_class("Diff2Hor", Diff2)
|
||||
|
||||
Diff2Hor.name = "diff2_horizontal"
|
||||
|
||||
---@class Diff2Hor.init.Opt
|
||||
---@field a vcs.File
|
||||
---@field b vcs.File
|
||||
---@field winid_a integer
|
||||
---@field winid_b integer
|
||||
|
||||
---@param opt Diff2Hor.init.Opt
|
||||
function Diff2Hor:init(opt)
|
||||
self:super(opt)
|
||||
end
|
||||
|
||||
---@override
|
||||
---@param self Diff2Hor
|
||||
---@param pivot integer?
|
||||
Diff2Hor.create = async.void(function(self, pivot)
|
||||
self:create_pre()
|
||||
local curwin
|
||||
|
||||
pivot = pivot or self:find_pivot()
|
||||
assert(api.nvim_win_is_valid(pivot), "Layout creation requires a valid window pivot!")
|
||||
|
||||
for _, win in ipairs(self.windows) do
|
||||
if win.id ~= pivot then
|
||||
win:close(true)
|
||||
end
|
||||
end
|
||||
|
||||
api.nvim_win_call(pivot, function()
|
||||
vim.cmd("aboveleft vsp")
|
||||
curwin = api.nvim_get_current_win()
|
||||
|
||||
if self.a then
|
||||
self.a:set_id(curwin)
|
||||
else
|
||||
self.a = Window({ id = curwin })
|
||||
end
|
||||
end)
|
||||
|
||||
api.nvim_win_call(pivot, function()
|
||||
vim.cmd("aboveleft vsp")
|
||||
curwin = api.nvim_get_current_win()
|
||||
|
||||
if self.b then
|
||||
self.b:set_id(curwin)
|
||||
else
|
||||
self.b = Window({ id = curwin })
|
||||
end
|
||||
end)
|
||||
|
||||
api.nvim_win_close(pivot, true)
|
||||
self.windows = { self.a, self.b }
|
||||
await(self:create_post())
|
||||
end)
|
||||
|
||||
M.Diff2Hor = Diff2Hor
|
||||
return M
|
||||
@ -0,0 +1,73 @@
|
||||
local async = require("diffview.async")
|
||||
local Window = require("diffview.scene.window").Window
|
||||
local Diff2 = require("diffview.scene.layouts.diff_2").Diff2
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
local api = vim.api
|
||||
local await = async.await
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class Diff2Ver : Diff2
|
||||
---@field a Window
|
||||
---@field b Window
|
||||
local Diff2Ver = oop.create_class("Diff2Ver", Diff2)
|
||||
|
||||
Diff2Ver.name = "diff2_vertical"
|
||||
|
||||
---@class Diff2Ver.init.Opt
|
||||
---@field a vcs.File
|
||||
---@field b vcs.File
|
||||
---@field winid_a integer
|
||||
---@field winid_b integer
|
||||
|
||||
---@param opt Diff2Hor.init.Opt
|
||||
function Diff2Ver:init(opt)
|
||||
self:super(opt)
|
||||
end
|
||||
|
||||
---@override
|
||||
---@param self Diff2Ver
|
||||
---@param pivot integer?
|
||||
Diff2Ver.create = async.void(function(self, pivot)
|
||||
self:create_pre()
|
||||
local curwin
|
||||
|
||||
pivot = pivot or self:find_pivot()
|
||||
assert(api.nvim_win_is_valid(pivot), "Layout creation requires a valid window pivot!")
|
||||
|
||||
for _, win in ipairs(self.windows) do
|
||||
if win.id ~= pivot then
|
||||
win:close(true)
|
||||
end
|
||||
end
|
||||
|
||||
api.nvim_win_call(pivot, function()
|
||||
vim.cmd("aboveleft sp")
|
||||
curwin = api.nvim_get_current_win()
|
||||
|
||||
if self.a then
|
||||
self.a:set_id(curwin)
|
||||
else
|
||||
self.a = Window({ id = curwin })
|
||||
end
|
||||
end)
|
||||
|
||||
api.nvim_win_call(pivot, function()
|
||||
vim.cmd("aboveleft sp")
|
||||
curwin = api.nvim_get_current_win()
|
||||
|
||||
if self.b then
|
||||
self.b:set_id(curwin)
|
||||
else
|
||||
self.b = Window({ id = curwin })
|
||||
end
|
||||
end)
|
||||
|
||||
api.nvim_win_close(pivot, true)
|
||||
self.windows = { self.a, self.b }
|
||||
await(self:create_post())
|
||||
end)
|
||||
|
||||
M.Diff2Ver = Diff2Ver
|
||||
return M
|
||||
@ -0,0 +1,119 @@
|
||||
local async = require("diffview.async")
|
||||
local lazy = require("diffview.lazy")
|
||||
local Window = require("diffview.scene.window").Window
|
||||
local Layout = require("diffview.scene.layout").Layout
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
local Diff1 = lazy.access("diffview.scene.layouts.diff_1", "Diff1") ---@type Diff1|LazyModule
|
||||
local Diff4 = lazy.access("diffview.scene.layouts.diff_4", "Diff4") ---@type Diff4|LazyModule
|
||||
local File = lazy.access("diffview.vcs.file", "File") ---@type vcs.File|LazyModule
|
||||
local Rev = lazy.access("diffview.vcs.rev", "Rev") ---@type Rev|LazyModule
|
||||
local RevType = lazy.access("diffview.vcs.rev", "RevType") ---@type RevType|LazyModule
|
||||
|
||||
local await = async.await
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class Diff3 : Layout
|
||||
---@field a Window
|
||||
---@field b Window
|
||||
---@field c Window
|
||||
local Diff3 = oop.create_class("Diff3", Layout)
|
||||
|
||||
---@alias Diff3.WindowSymbol "a"|"b"|"c"
|
||||
|
||||
---@class Diff3.init.Opt
|
||||
---@field a vcs.File
|
||||
---@field b vcs.File
|
||||
---@field c vcs.File
|
||||
---@field winid_a integer
|
||||
---@field winid_b integer
|
||||
---@field winid_c integer
|
||||
|
||||
---@param opt Diff3.init.Opt
|
||||
function Diff3:init(opt)
|
||||
self:super()
|
||||
self.a = Window({ file = opt.a, id = opt.winid_a })
|
||||
self.b = Window({ file = opt.b, id = opt.winid_b })
|
||||
self.c = Window({ file = opt.c, id = opt.winid_c })
|
||||
self:use_windows(self.a, self.b, self.c)
|
||||
end
|
||||
|
||||
---@param file vcs.File
|
||||
function Diff3:set_file_a(file)
|
||||
self.a:set_file(file)
|
||||
file.symbol = "a"
|
||||
end
|
||||
|
||||
---@param file vcs.File
|
||||
function Diff3:set_file_b(file)
|
||||
self.b:set_file(file)
|
||||
file.symbol = "b"
|
||||
end
|
||||
|
||||
---@param file vcs.File
|
||||
function Diff3:set_file_c(file)
|
||||
self.c:set_file(file)
|
||||
file.symbol = "c"
|
||||
end
|
||||
|
||||
---@param self Diff3
|
||||
---@param entry FileEntry
|
||||
Diff3.use_entry = async.void(function(self, entry)
|
||||
local layout = entry.layout --[[@as Diff3 ]]
|
||||
assert(layout:instanceof(Diff3))
|
||||
|
||||
self:set_file_a(layout.a.file)
|
||||
self:set_file_b(layout.b.file)
|
||||
self:set_file_c(layout.c.file)
|
||||
|
||||
if self:is_valid() then
|
||||
await(self:open_files())
|
||||
end
|
||||
end)
|
||||
|
||||
function Diff3:get_main_win()
|
||||
return self.b
|
||||
end
|
||||
|
||||
---@param layout Diff1
|
||||
---@return Diff1
|
||||
function Diff3:to_diff1(layout)
|
||||
assert(layout:instanceof(Diff1.__get()))
|
||||
|
||||
return layout({ a = self:get_main_win().file })
|
||||
end
|
||||
|
||||
---@param layout Diff4
|
||||
---@return Diff4
|
||||
function Diff3:to_diff4(layout)
|
||||
assert(layout:instanceof(Diff4.__get()))
|
||||
local main = self:get_main_win().file
|
||||
|
||||
return layout({
|
||||
a = self.a.file,
|
||||
b = self.b.file,
|
||||
c = self.c.file,
|
||||
d = File({
|
||||
adapter = main.adapter,
|
||||
path = main.path,
|
||||
kind = main.kind,
|
||||
commit = main.commit,
|
||||
get_data = main.get_data,
|
||||
rev = Rev(RevType.STAGE, 1),
|
||||
nulled = false, -- FIXME
|
||||
})
|
||||
})
|
||||
end
|
||||
|
||||
---FIXME
|
||||
---@override
|
||||
---@param rev Rev
|
||||
---@param status string Git status symbol.
|
||||
---@param sym Diff3.WindowSymbol
|
||||
function Diff3.should_null(rev, status, sym)
|
||||
return false
|
||||
end
|
||||
|
||||
M.Diff3 = Diff3
|
||||
return M
|
||||
@ -0,0 +1,78 @@
|
||||
local async = require("diffview.async")
|
||||
local Window = require("diffview.scene.window").Window
|
||||
local Diff3 = require("diffview.scene.layouts.diff_3").Diff3
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
local api = vim.api
|
||||
local await = async.await
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class Diff3Hor : Diff3
|
||||
---@field a Window
|
||||
---@field b Window
|
||||
---@field c Window
|
||||
local Diff3Hor = oop.create_class("Diff3Hor", Diff3)
|
||||
|
||||
Diff3Hor.name = "diff3_horizontal"
|
||||
|
||||
function Diff3Hor:init(opt)
|
||||
self:super(opt)
|
||||
end
|
||||
|
||||
---@override
|
||||
---@param self Diff3Hor
|
||||
---@param pivot integer?
|
||||
Diff3Hor.create = async.void(function(self, pivot)
|
||||
self:create_pre()
|
||||
local curwin
|
||||
|
||||
pivot = pivot or self:find_pivot()
|
||||
assert(api.nvim_win_is_valid(pivot), "Layout creation requires a valid window pivot!")
|
||||
|
||||
for _, win in ipairs(self.windows) do
|
||||
if win.id ~= pivot then
|
||||
win:close(true)
|
||||
end
|
||||
end
|
||||
|
||||
api.nvim_win_call(pivot, function()
|
||||
vim.cmd("aboveleft vsp")
|
||||
curwin = api.nvim_get_current_win()
|
||||
|
||||
if self.a then
|
||||
self.a:set_id(curwin)
|
||||
else
|
||||
self.a = Window({ id = curwin })
|
||||
end
|
||||
end)
|
||||
|
||||
api.nvim_win_call(pivot, function()
|
||||
vim.cmd("aboveleft vsp")
|
||||
curwin = api.nvim_get_current_win()
|
||||
|
||||
if self.b then
|
||||
self.b:set_id(curwin)
|
||||
else
|
||||
self.b = Window({ id = curwin })
|
||||
end
|
||||
end)
|
||||
|
||||
api.nvim_win_call(pivot, function()
|
||||
vim.cmd("aboveleft vsp")
|
||||
curwin = api.nvim_get_current_win()
|
||||
|
||||
if self.c then
|
||||
self.c:set_id(curwin)
|
||||
else
|
||||
self.c = Window({ id = curwin })
|
||||
end
|
||||
end)
|
||||
|
||||
api.nvim_win_close(pivot, true)
|
||||
self.windows = { self.a, self.b, self.c }
|
||||
await(self:create_post())
|
||||
end)
|
||||
|
||||
M.Diff3Hor = Diff3Hor
|
||||
return M
|
||||
@ -0,0 +1,78 @@
|
||||
local async = require("diffview.async")
|
||||
local Window = require("diffview.scene.window").Window
|
||||
local Diff3 = require("diffview.scene.layouts.diff_3").Diff3
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
local api = vim.api
|
||||
local await = async.await
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class Diff3Mixed : Diff3
|
||||
---@field a Window
|
||||
---@field b Window
|
||||
---@field c Window
|
||||
local Diff3Mixed = oop.create_class("Diff3Mixed", Diff3)
|
||||
|
||||
Diff3Mixed.name = "diff3_mixed"
|
||||
|
||||
function Diff3Mixed:init(opt)
|
||||
self:super(opt)
|
||||
end
|
||||
|
||||
---@override
|
||||
---@param self Diff3Mixed
|
||||
---@param pivot integer?
|
||||
Diff3Mixed.create = async.void(function(self, pivot)
|
||||
self:create_pre()
|
||||
local curwin
|
||||
|
||||
pivot = pivot or self:find_pivot()
|
||||
assert(api.nvim_win_is_valid(pivot), "Layout creation requires a valid window pivot!")
|
||||
|
||||
for _, win in ipairs(self.windows) do
|
||||
if win.id ~= pivot then
|
||||
win:close(true)
|
||||
end
|
||||
end
|
||||
|
||||
api.nvim_win_call(pivot, function()
|
||||
vim.cmd("belowright sp")
|
||||
curwin = api.nvim_get_current_win()
|
||||
|
||||
if self.b then
|
||||
self.b:set_id(curwin)
|
||||
else
|
||||
self.b = Window({ id = curwin })
|
||||
end
|
||||
end)
|
||||
|
||||
api.nvim_win_call(pivot, function()
|
||||
vim.cmd("aboveleft vsp")
|
||||
curwin = api.nvim_get_current_win()
|
||||
|
||||
if self.a then
|
||||
self.a:set_id(curwin)
|
||||
else
|
||||
self.a = Window({ id = curwin })
|
||||
end
|
||||
end)
|
||||
|
||||
api.nvim_win_call(pivot, function()
|
||||
vim.cmd("aboveleft vsp")
|
||||
curwin = api.nvim_get_current_win()
|
||||
|
||||
if self.c then
|
||||
self.c:set_id(curwin)
|
||||
else
|
||||
self.c = Window({ id = curwin })
|
||||
end
|
||||
end)
|
||||
|
||||
api.nvim_win_close(pivot, true)
|
||||
self.windows = { self.a, self.b, self.c }
|
||||
await(self:create_post())
|
||||
end)
|
||||
|
||||
M.Diff3Mixed = Diff3Mixed
|
||||
return M
|
||||
@ -0,0 +1,78 @@
|
||||
local async = require("diffview.async")
|
||||
local Window = require("diffview.scene.window").Window
|
||||
local Diff3 = require("diffview.scene.layouts.diff_3").Diff3
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
local api = vim.api
|
||||
local await = async.await
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class Diff3Ver : Diff3
|
||||
---@field a Window
|
||||
---@field b Window
|
||||
---@field c Window
|
||||
local Diff3Ver = oop.create_class("Diff3Ver", Diff3)
|
||||
|
||||
Diff3Ver.name = "diff3_vertical"
|
||||
|
||||
function Diff3Ver:init(opt)
|
||||
self:super(opt)
|
||||
end
|
||||
|
||||
---@override
|
||||
---@param self Diff3Ver
|
||||
---@param pivot integer?
|
||||
Diff3Ver.create = async.void(function(self, pivot)
|
||||
self:create_pre()
|
||||
local curwin
|
||||
|
||||
pivot = pivot or self:find_pivot()
|
||||
assert(api.nvim_win_is_valid(pivot), "Layout creation requires a valid window pivot!")
|
||||
|
||||
for _, win in ipairs(self.windows) do
|
||||
if win.id ~= pivot then
|
||||
win:close(true)
|
||||
end
|
||||
end
|
||||
|
||||
api.nvim_win_call(pivot, function()
|
||||
vim.cmd("aboveleft sp")
|
||||
curwin = api.nvim_get_current_win()
|
||||
|
||||
if self.a then
|
||||
self.a:set_id(curwin)
|
||||
else
|
||||
self.a = Window({ id = curwin })
|
||||
end
|
||||
end)
|
||||
|
||||
api.nvim_win_call(pivot, function()
|
||||
vim.cmd("aboveleft sp")
|
||||
curwin = api.nvim_get_current_win()
|
||||
|
||||
if self.b then
|
||||
self.b:set_id(curwin)
|
||||
else
|
||||
self.b = Window({ id = curwin })
|
||||
end
|
||||
end)
|
||||
|
||||
api.nvim_win_call(pivot, function()
|
||||
vim.cmd("aboveleft sp")
|
||||
curwin = api.nvim_get_current_win()
|
||||
|
||||
if self.c then
|
||||
self.c:set_id(curwin)
|
||||
else
|
||||
self.c = Window({ id = curwin })
|
||||
end
|
||||
end)
|
||||
|
||||
api.nvim_win_close(pivot, true)
|
||||
self.windows = { self.a, self.b, self.c }
|
||||
await(self:create_post())
|
||||
end)
|
||||
|
||||
M.Diff3Ver = Diff3Ver
|
||||
return M
|
||||
@ -0,0 +1,116 @@
|
||||
local async = require("diffview.async")
|
||||
local lazy = require("diffview.lazy")
|
||||
local Window = require("diffview.scene.window").Window
|
||||
local Layout = require("diffview.scene.layout").Layout
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
local Diff1 = lazy.access("diffview.scene.layouts.diff_1", "Diff1") ---@type Diff1|LazyModule
|
||||
local Diff3 = lazy.access("diffview.scene.layouts.diff_3", "Diff3") ---@type Diff3|LazyModule
|
||||
|
||||
local await = async.await
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class Diff4 : Layout
|
||||
---@field a Window
|
||||
---@field b Window
|
||||
---@field c Window
|
||||
---@field d Window
|
||||
local Diff4 = oop.create_class("Diff4", Layout)
|
||||
|
||||
---@alias Diff4.WindowSymbol "a"|"b"|"c"|"d"
|
||||
|
||||
---@class Diff4.init.Opt
|
||||
---@field a vcs.File
|
||||
---@field b vcs.File
|
||||
---@field c vcs.File
|
||||
---@field d vcs.File
|
||||
---@field winid_a integer
|
||||
---@field winid_b integer
|
||||
---@field winid_c integer
|
||||
---@field winid_d integer
|
||||
|
||||
---@param opt Diff4.init.Opt
|
||||
function Diff4:init(opt)
|
||||
self:super()
|
||||
self.a = Window({ file = opt.a, id = opt.winid_a })
|
||||
self.b = Window({ file = opt.b, id = opt.winid_b })
|
||||
self.c = Window({ file = opt.c, id = opt.winid_c })
|
||||
self.d = Window({ file = opt.d, id = opt.winid_d })
|
||||
self:use_windows(self.a, self.b, self.c, self.d)
|
||||
end
|
||||
|
||||
---@param file vcs.File
|
||||
function Diff4:set_file_a(file)
|
||||
self.a:set_file(file)
|
||||
file.symbol = "a"
|
||||
end
|
||||
|
||||
---@param file vcs.File
|
||||
function Diff4:set_file_b(file)
|
||||
self.b:set_file(file)
|
||||
file.symbol = "b"
|
||||
end
|
||||
|
||||
---@param file vcs.File
|
||||
function Diff4:set_file_c(file)
|
||||
self.c:set_file(file)
|
||||
file.symbol = "c"
|
||||
end
|
||||
|
||||
---@param file vcs.File
|
||||
function Diff4:set_file_d(file)
|
||||
self.d:set_file(file)
|
||||
file.symbol = "d"
|
||||
end
|
||||
|
||||
---@param self Diff4
|
||||
---@param entry FileEntry
|
||||
Diff4.use_entry = async.void(function(self, entry)
|
||||
local layout = entry.layout --[[@as Diff4 ]]
|
||||
assert(layout:instanceof(Diff4))
|
||||
|
||||
self:set_file_a(layout.a.file)
|
||||
self:set_file_b(layout.b.file)
|
||||
self:set_file_c(layout.c.file)
|
||||
self:set_file_d(layout.d.file)
|
||||
|
||||
if self:is_valid() then
|
||||
await(self:open_files())
|
||||
end
|
||||
end)
|
||||
|
||||
function Diff4:get_main_win()
|
||||
return self.b
|
||||
end
|
||||
|
||||
---@param layout Diff1
|
||||
---@return Diff1
|
||||
function Diff4:to_diff1(layout)
|
||||
assert(layout:instanceof(Diff1.__get()))
|
||||
|
||||
return layout({ a = self:get_main_win().file })
|
||||
end
|
||||
|
||||
---@param layout Diff3
|
||||
---@return Diff3
|
||||
function Diff4:to_diff3(layout)
|
||||
assert(layout:instanceof(Diff3.__get()))
|
||||
return layout({
|
||||
a = self.a.file,
|
||||
b = self.b.file,
|
||||
c = self.c.file,
|
||||
})
|
||||
end
|
||||
|
||||
---FIXME
|
||||
---@override
|
||||
---@param rev Rev
|
||||
---@param status string Git status symbol.
|
||||
---@param sym Diff4.WindowSymbol
|
||||
function Diff4.should_null(rev, status, sym)
|
||||
return false
|
||||
end
|
||||
|
||||
M.Diff4 = Diff4
|
||||
return M
|
||||
@ -0,0 +1,90 @@
|
||||
local async = require("diffview.async")
|
||||
local Window = require("diffview.scene.window").Window
|
||||
local Diff4 = require("diffview.scene.layouts.diff_4").Diff4
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
local api = vim.api
|
||||
local await = async.await
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class Diff4Mixed : Diff4
|
||||
---@field a Window
|
||||
---@field b Window
|
||||
---@field c Window
|
||||
---@field d Window
|
||||
local Diff4Mixed = oop.create_class("Diff4Mixed", Diff4)
|
||||
|
||||
Diff4Mixed.name = "diff4_mixed"
|
||||
|
||||
function Diff4Mixed:init(opt)
|
||||
self:super(opt)
|
||||
end
|
||||
|
||||
---@override
|
||||
---@param self Diff4Mixed
|
||||
---@param pivot integer?
|
||||
Diff4Mixed.create = async.void(function(self, pivot)
|
||||
self:create_pre()
|
||||
local curwin
|
||||
|
||||
pivot = pivot or self:find_pivot()
|
||||
assert(api.nvim_win_is_valid(pivot), "Layout creation requires a valid window pivot!")
|
||||
|
||||
for _, win in ipairs(self.windows) do
|
||||
if win.id ~= pivot then
|
||||
win:close(true)
|
||||
end
|
||||
end
|
||||
|
||||
api.nvim_win_call(pivot, function()
|
||||
vim.cmd("belowright sp")
|
||||
curwin = api.nvim_get_current_win()
|
||||
|
||||
if self.b then
|
||||
self.b:set_id(curwin)
|
||||
else
|
||||
self.b = Window({ id = curwin })
|
||||
end
|
||||
end)
|
||||
|
||||
api.nvim_win_call(pivot, function()
|
||||
vim.cmd("aboveleft vsp")
|
||||
curwin = api.nvim_get_current_win()
|
||||
|
||||
if self.a then
|
||||
self.a:set_id(curwin)
|
||||
else
|
||||
self.a = Window({ id = curwin })
|
||||
end
|
||||
end)
|
||||
|
||||
api.nvim_win_call(pivot, function()
|
||||
vim.cmd("aboveleft vsp")
|
||||
curwin = api.nvim_get_current_win()
|
||||
|
||||
if self.d then
|
||||
self.d:set_id(curwin)
|
||||
else
|
||||
self.d = Window({ id = curwin })
|
||||
end
|
||||
end)
|
||||
|
||||
api.nvim_win_call(pivot, function()
|
||||
vim.cmd("aboveleft vsp")
|
||||
curwin = api.nvim_get_current_win()
|
||||
|
||||
if self.c then
|
||||
self.c:set_id(curwin)
|
||||
else
|
||||
self.c = Window({ id = curwin })
|
||||
end
|
||||
end)
|
||||
|
||||
api.nvim_win_close(pivot, true)
|
||||
self.windows = { self.a, self.b, self.c, self.d }
|
||||
await(self:create_post())
|
||||
end)
|
||||
|
||||
M.Diff4Mixed = Diff4Mixed
|
||||
return M
|
||||
@ -0,0 +1,166 @@
|
||||
local lazy = require("diffview.lazy")
|
||||
|
||||
local Diff1 = lazy.access("diffview.scene.layouts.diff_1", "Diff1") ---@type Diff1|LazyModule
|
||||
local Diff2Hor = lazy.access("diffview.scene.layouts.diff_2_hor", "Diff2Hor") ---@type Diff2Hor|LazyModule
|
||||
local Diff2Ver = lazy.access("diffview.scene.layouts.diff_2_ver", "Diff2Ver") ---@type Diff2Ver|LazyModule
|
||||
local Diff3Hor = lazy.access("diffview.scene.layouts.diff_3_hor", "Diff3Hor") ---@type Diff3Hor|LazyModule
|
||||
local Diff3Ver = lazy.access("diffview.scene.layouts.diff_3_ver", "Diff3Ver") ---@type Diff3Ver|LazyModule
|
||||
local Diff4Mixed = lazy.access("diffview.scene.layouts.diff_4_mixed", "Diff4Mixed") ---@type Diff4Mixed|LazyModule
|
||||
local EventEmitter = lazy.access("diffview.events", "EventEmitter") ---@type EventEmitter|LazyModule
|
||||
local File = lazy.access("diffview.vcs.file", "File") ---@type vcs.File|LazyModule
|
||||
local Signal = lazy.access("diffview.control", "Signal") ---@type Signal|LazyModule
|
||||
local config = lazy.require("diffview.config") ---@module "diffview.config"
|
||||
local oop = lazy.require("diffview.oop") ---@module "diffview.oop"
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
|
||||
local api = vim.api
|
||||
local M = {}
|
||||
|
||||
---@enum LayoutMode
|
||||
local LayoutMode = oop.enum({
|
||||
HORIZONTAL = 1,
|
||||
VERTICAL = 2,
|
||||
})
|
||||
|
||||
---@class View : diffview.Object
|
||||
---@field tabpage integer
|
||||
---@field emitter EventEmitter
|
||||
---@field default_layout Layout (class)
|
||||
---@field ready boolean
|
||||
---@field closing Signal
|
||||
local View = oop.create_class("View")
|
||||
|
||||
---@diagnostic disable unused-local
|
||||
|
||||
---@abstract
|
||||
function View:init_layout() oop.abstract_stub() end
|
||||
|
||||
---@abstract
|
||||
function View:post_open() oop.abstract_stub() end
|
||||
|
||||
---@diagnostic enable unused-local
|
||||
|
||||
---View constructor
|
||||
function View:init(opt)
|
||||
opt = opt or {}
|
||||
self.emitter = opt.emitter or EventEmitter()
|
||||
self.default_layout = opt.default_layout or View.get_default_layout()
|
||||
self.ready = utils.sate(opt.ready, false)
|
||||
self.closing = utils.sate(opt.closing, Signal())
|
||||
|
||||
local function wrap_event(event)
|
||||
DiffviewGlobal.emitter:on(event, function(_, view, ...)
|
||||
local cur_view = require("diffview.lib").get_current_view()
|
||||
|
||||
if (view and view == self) or (not view and cur_view == self) then
|
||||
self.emitter:emit(event, view, ...)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
wrap_event("view_closed")
|
||||
end
|
||||
|
||||
function View:open()
|
||||
vim.cmd("tab split")
|
||||
self.tabpage = api.nvim_get_current_tabpage()
|
||||
self:init_layout()
|
||||
self:post_open()
|
||||
DiffviewGlobal.emitter:emit("view_opened", self)
|
||||
DiffviewGlobal.emitter:emit("view_enter", self)
|
||||
end
|
||||
|
||||
function View:close()
|
||||
self.closing:send()
|
||||
|
||||
if self.tabpage and api.nvim_tabpage_is_valid(self.tabpage) then
|
||||
DiffviewGlobal.emitter:emit("view_leave", self)
|
||||
|
||||
if #api.nvim_list_tabpages() == 1 then
|
||||
vim.cmd("tabnew")
|
||||
end
|
||||
|
||||
local pagenr = api.nvim_tabpage_get_number(self.tabpage)
|
||||
vim.cmd("tabclose " .. pagenr)
|
||||
end
|
||||
|
||||
DiffviewGlobal.emitter:emit("view_closed", self)
|
||||
end
|
||||
|
||||
function View:is_cur_tabpage()
|
||||
return self.tabpage == api.nvim_get_current_tabpage()
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
local function prefer_horizontal()
|
||||
return vim.tbl_contains(vim.opt.diffopt:get(), "vertical")
|
||||
end
|
||||
|
||||
---@return Diff1
|
||||
function View.get_default_diff1()
|
||||
return Diff1.__get()
|
||||
end
|
||||
|
||||
---@return Diff2
|
||||
function View.get_default_diff2()
|
||||
if prefer_horizontal() then
|
||||
return Diff2Hor.__get()
|
||||
else
|
||||
return Diff2Ver.__get()
|
||||
end
|
||||
end
|
||||
|
||||
---@return Diff3
|
||||
function View.get_default_diff3()
|
||||
if prefer_horizontal() then
|
||||
return Diff3Hor.__get()
|
||||
else
|
||||
return Diff3Ver.__get()
|
||||
end
|
||||
end
|
||||
|
||||
---@return Diff4
|
||||
function View.get_default_diff4()
|
||||
return Diff4Mixed.__get()
|
||||
end
|
||||
|
||||
---@return LayoutName|-1
|
||||
function View.get_default_layout_name()
|
||||
return config.get_config().view.default.layout
|
||||
end
|
||||
|
||||
---@return Layout # (class) The default layout class.
|
||||
function View.get_default_layout()
|
||||
local name = View.get_default_layout_name()
|
||||
|
||||
if name == -1 then
|
||||
return View.get_default_diff2()
|
||||
end
|
||||
|
||||
return config.name_to_layout(name --[[@as string ]])
|
||||
end
|
||||
|
||||
---@return Layout
|
||||
function View.get_default_merge_layout()
|
||||
local name = config.get_config().view.merge_tool.layout
|
||||
|
||||
if name == -1 then
|
||||
return View.get_default_diff3()
|
||||
end
|
||||
|
||||
return config.name_to_layout(name)
|
||||
end
|
||||
|
||||
---@return Diff2
|
||||
function View.get_temp_layout()
|
||||
local layout_class = View.get_default_layout()
|
||||
return layout_class({
|
||||
a = File.NULL_FILE,
|
||||
b = File.NULL_FILE,
|
||||
})
|
||||
end
|
||||
|
||||
M.LayoutMode = LayoutMode
|
||||
M.View = View
|
||||
|
||||
return M
|
||||
@ -0,0 +1,556 @@
|
||||
local async = require("diffview.async")
|
||||
local lazy = require("diffview.lazy")
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
local CommitLogPanel = lazy.access("diffview.ui.panels.commit_log_panel", "CommitLogPanel") ---@type CommitLogPanel|LazyModule
|
||||
local Diff = lazy.access("diffview.diff", "Diff") ---@type Diff|LazyModule
|
||||
local EditToken = lazy.access("diffview.diff", "EditToken") ---@type EditToken|LazyModule
|
||||
local EventName = lazy.access("diffview.events", "EventName") ---@type EventName|LazyModule
|
||||
local FileDict = lazy.access("diffview.vcs.file_dict", "FileDict") ---@type FileDict|LazyModule
|
||||
local FileEntry = lazy.access("diffview.scene.file_entry", "FileEntry") ---@type FileEntry|LazyModule
|
||||
local FilePanel = lazy.access("diffview.scene.views.diff.file_panel", "FilePanel") ---@type FilePanel|LazyModule
|
||||
local PerfTimer = lazy.access("diffview.perf", "PerfTimer") ---@type PerfTimer|LazyModule
|
||||
local RevType = lazy.access("diffview.vcs.rev", "RevType") ---@type RevType|LazyModule
|
||||
local StandardView = lazy.access("diffview.scene.views.standard.standard_view", "StandardView") ---@type StandardView|LazyModule
|
||||
local config = lazy.require("diffview.config") ---@module "diffview.config"
|
||||
local debounce = lazy.require("diffview.debounce") ---@module "diffview.debounce"
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
local vcs_utils = lazy.require("diffview.vcs.utils") ---@module "diffview.vcs.utils"
|
||||
local GitAdapter = lazy.access("diffview.vcs.adapters.git", "GitAdapter") ---@type GitAdapter|LazyModule
|
||||
|
||||
local api = vim.api
|
||||
local await = async.await
|
||||
local fmt = string.format
|
||||
local logger = DiffviewGlobal.logger
|
||||
local pl = lazy.access(utils, "path") ---@type PathLib
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class DiffViewOptions
|
||||
---@field show_untracked? boolean
|
||||
---@field selected_file? string Path to the preferred initially selected file.
|
||||
|
||||
---@class DiffView : StandardView
|
||||
---@operator call : DiffView
|
||||
---@field adapter VCSAdapter
|
||||
---@field rev_arg string
|
||||
---@field path_args string[]
|
||||
---@field left Rev
|
||||
---@field right Rev
|
||||
---@field options DiffViewOptions
|
||||
---@field panel FilePanel
|
||||
---@field commit_log_panel CommitLogPanel
|
||||
---@field files FileDict
|
||||
---@field file_idx integer
|
||||
---@field merge_ctx? vcs.MergeContext
|
||||
---@field initialized boolean
|
||||
---@field valid boolean
|
||||
---@field watcher uv_fs_poll_t # UV fs poll handle.
|
||||
local DiffView = oop.create_class("DiffView", StandardView.__get())
|
||||
|
||||
---DiffView constructor
|
||||
function DiffView:init(opt)
|
||||
self.valid = false
|
||||
self.files = FileDict()
|
||||
self.adapter = opt.adapter
|
||||
self.path_args = opt.path_args
|
||||
self.rev_arg = opt.rev_arg
|
||||
self.left = opt.left
|
||||
self.right = opt.right
|
||||
self.initialized = false
|
||||
self.options = opt.options or {}
|
||||
self.options.selected_file = self.options.selected_file
|
||||
and pl:chain(self.options.selected_file)
|
||||
:absolute()
|
||||
:relative(self.adapter.ctx.toplevel)
|
||||
:get()
|
||||
|
||||
self:super({
|
||||
panel = FilePanel(
|
||||
self.adapter,
|
||||
self.files,
|
||||
self.path_args,
|
||||
self.rev_arg or self.adapter:rev_to_pretty_string(self.left, self.right)
|
||||
),
|
||||
})
|
||||
|
||||
self.attached_bufs = {}
|
||||
self.emitter:on("file_open_post", utils.bind(self.file_open_post, self))
|
||||
self.valid = true
|
||||
end
|
||||
|
||||
function DiffView:post_open()
|
||||
vim.cmd("redraw")
|
||||
|
||||
self.commit_log_panel = CommitLogPanel(self.adapter, {
|
||||
name = fmt("diffview://%s/log/%d/%s", self.adapter.ctx.dir, self.tabpage, "commit_log"),
|
||||
})
|
||||
|
||||
if config.get_config().watch_index and self.adapter:instanceof(GitAdapter.__get()) then
|
||||
self.watcher = vim.loop.new_fs_poll()
|
||||
self.watcher:start(
|
||||
self.adapter.ctx.dir .. "/index",
|
||||
1000,
|
||||
---@diagnostic disable-next-line: unused-local
|
||||
vim.schedule_wrap(function(err, prev, cur)
|
||||
if not err then
|
||||
if self:is_cur_tabpage() then
|
||||
self:update_files()
|
||||
end
|
||||
end
|
||||
end)
|
||||
)
|
||||
end
|
||||
|
||||
self:init_event_listeners()
|
||||
|
||||
vim.schedule(function()
|
||||
self:file_safeguard()
|
||||
if self.files:len() == 0 then
|
||||
self:update_files()
|
||||
end
|
||||
self.ready = true
|
||||
end)
|
||||
end
|
||||
|
||||
---@param e Event
|
||||
---@param new_entry FileEntry
|
||||
---@param old_entry FileEntry
|
||||
---@diagnostic disable-next-line: unused-local
|
||||
function DiffView:file_open_post(e, new_entry, old_entry)
|
||||
if new_entry.layout:is_nulled() then return end
|
||||
if new_entry.kind == "conflicting" then
|
||||
local file = new_entry.layout:get_main_win().file
|
||||
|
||||
local count_conflicts = vim.schedule_wrap(function()
|
||||
local conflicts = vcs_utils.parse_conflicts(api.nvim_buf_get_lines(file.bufnr, 0, -1, false))
|
||||
|
||||
new_entry.stats = new_entry.stats or {}
|
||||
new_entry.stats.conflicts = #conflicts
|
||||
|
||||
self.panel:render()
|
||||
self.panel:redraw()
|
||||
end)
|
||||
|
||||
count_conflicts()
|
||||
|
||||
if file.bufnr and not self.attached_bufs[file.bufnr] then
|
||||
self.attached_bufs[file.bufnr] = true
|
||||
|
||||
local work = debounce.throttle_trailing(
|
||||
1000,
|
||||
true,
|
||||
vim.schedule_wrap(function()
|
||||
if not self:is_cur_tabpage() or self.cur_entry ~= new_entry then
|
||||
self.attached_bufs[file.bufnr] = false
|
||||
return
|
||||
end
|
||||
|
||||
count_conflicts()
|
||||
end)
|
||||
)
|
||||
|
||||
api.nvim_create_autocmd(
|
||||
{ "TextChanged", "TextChangedI" },
|
||||
{
|
||||
buffer = file.bufnr,
|
||||
callback = function()
|
||||
if not self.attached_bufs[file.bufnr] then
|
||||
work:close()
|
||||
return true
|
||||
end
|
||||
|
||||
work()
|
||||
end,
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@override
|
||||
function DiffView:close()
|
||||
if not self.closing:check() then
|
||||
self.closing:send()
|
||||
|
||||
if self.watcher then
|
||||
self.watcher:stop()
|
||||
self.watcher:close()
|
||||
end
|
||||
|
||||
for _, file in self.files:iter() do
|
||||
file:destroy()
|
||||
end
|
||||
|
||||
self.commit_log_panel:destroy()
|
||||
DiffView.super_class.close(self)
|
||||
end
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param self DiffView
|
||||
---@param file FileEntry
|
||||
DiffView._set_file = async.void(function(self, file)
|
||||
self.panel:render()
|
||||
self.panel:redraw()
|
||||
vim.cmd("redraw")
|
||||
|
||||
self.cur_layout:detach_files()
|
||||
local cur_entry = self.cur_entry
|
||||
self.emitter:emit("file_open_pre", file, cur_entry)
|
||||
self.nulled = false
|
||||
|
||||
await(self:use_entry(file))
|
||||
|
||||
self.emitter:emit("file_open_post", file, cur_entry)
|
||||
|
||||
if not self.cur_entry.opened then
|
||||
self.cur_entry.opened = true
|
||||
DiffviewGlobal.emitter:emit("file_open_new", file)
|
||||
end
|
||||
end)
|
||||
|
||||
---Open the next file.
|
||||
---@param highlight? boolean Bring the cursor to the file entry in the panel.
|
||||
---@return FileEntry?
|
||||
function DiffView:next_file(highlight)
|
||||
self:ensure_layout()
|
||||
|
||||
if self:file_safeguard() then return end
|
||||
|
||||
if self.files:len() > 1 or self.nulled then
|
||||
local cur = self.panel:next_file()
|
||||
|
||||
if cur then
|
||||
if highlight or not self.panel:is_focused() then
|
||||
self.panel:highlight_file(cur)
|
||||
end
|
||||
|
||||
self:_set_file(cur)
|
||||
|
||||
return cur
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Open the previous file.
|
||||
---@param highlight? boolean Bring the cursor to the file entry in the panel.
|
||||
---@return FileEntry?
|
||||
function DiffView:prev_file(highlight)
|
||||
self:ensure_layout()
|
||||
|
||||
if self:file_safeguard() then return end
|
||||
|
||||
if self.files:len() > 1 or self.nulled then
|
||||
local cur = self.panel:prev_file()
|
||||
|
||||
if cur then
|
||||
if highlight or not self.panel:is_focused() then
|
||||
self.panel:highlight_file(cur)
|
||||
end
|
||||
|
||||
self:_set_file(cur)
|
||||
|
||||
return cur
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Set the active file.
|
||||
---@param self DiffView
|
||||
---@param file FileEntry
|
||||
---@param focus? boolean Bring focus to the diff buffers.
|
||||
---@param highlight? boolean Bring the cursor to the file entry in the panel.
|
||||
DiffView.set_file = async.void(function(self, file, focus, highlight)
|
||||
---@diagnostic disable: invisible
|
||||
self:ensure_layout()
|
||||
|
||||
if self:file_safeguard() or not file then return end
|
||||
|
||||
for _, f in self.files:iter() do
|
||||
if f == file then
|
||||
self.panel:set_cur_file(file)
|
||||
|
||||
if highlight or not self.panel:is_focused() then
|
||||
self.panel:highlight_file(file)
|
||||
end
|
||||
|
||||
await(self:_set_file(file))
|
||||
|
||||
if focus then
|
||||
api.nvim_set_current_win(self.cur_layout:get_main_win().id)
|
||||
end
|
||||
end
|
||||
end
|
||||
---@diagnostic enable: invisible
|
||||
end)
|
||||
|
||||
---Set the active file.
|
||||
---@param self DiffView
|
||||
---@param path string
|
||||
---@param focus? boolean Bring focus to the diff buffers.
|
||||
---@param highlight? boolean Bring the cursor to the file entry in the panel.
|
||||
DiffView.set_file_by_path = async.void(function(self, path, focus, highlight)
|
||||
---@type FileEntry
|
||||
for _, file in self.files:iter() do
|
||||
if file.path == path then
|
||||
await(self:set_file(file, focus, highlight))
|
||||
return
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
---Get an updated list of files.
|
||||
---@param self DiffView
|
||||
---@param callback fun(err?: string[], files: FileDict)
|
||||
DiffView.get_updated_files = async.wrap(function(self, callback)
|
||||
vcs_utils.diff_file_list(
|
||||
self.adapter,
|
||||
self.left,
|
||||
self.right,
|
||||
self.path_args,
|
||||
self.options,
|
||||
{
|
||||
default_layout = DiffView.get_default_layout(),
|
||||
merge_layout = DiffView.get_default_merge_layout(),
|
||||
},
|
||||
callback
|
||||
)
|
||||
end)
|
||||
|
||||
---Update the file list, including stats and status for all files.
|
||||
DiffView.update_files = debounce.debounce_trailing(
|
||||
100,
|
||||
true,
|
||||
---@param self DiffView
|
||||
---@param callback fun(err?: string[])
|
||||
async.wrap(function(self, callback)
|
||||
await(async.scheduler())
|
||||
|
||||
-- Never update unless the view is in focus
|
||||
if self.tabpage ~= api.nvim_get_current_tabpage() then
|
||||
callback({ "The update was cancelled." })
|
||||
return
|
||||
end
|
||||
|
||||
---@type PerfTimer
|
||||
local perf = PerfTimer("[DiffView] Status Update")
|
||||
self:ensure_layout()
|
||||
|
||||
-- If left is tracking HEAD and right is LOCAL: Update HEAD rev.
|
||||
local new_head
|
||||
if self.left.track_head and self.right.type == RevType.LOCAL then
|
||||
new_head = self.adapter:head_rev()
|
||||
if new_head and self.left.commit ~= new_head.commit then
|
||||
self.left = new_head
|
||||
else
|
||||
new_head = nil
|
||||
end
|
||||
perf:lap("updated head rev")
|
||||
end
|
||||
|
||||
local index_stat = pl:stat(pl:join(self.adapter.ctx.dir, "index"))
|
||||
|
||||
---@type string[]?, FileDict
|
||||
local err, new_files = await(self:get_updated_files())
|
||||
await(async.scheduler())
|
||||
|
||||
if err then
|
||||
utils.err("Failed to update files in a diff view!", true)
|
||||
logger:error("[DiffView] Failed to update files!")
|
||||
callback(err)
|
||||
return
|
||||
end
|
||||
|
||||
-- Stop the update if the view is no longer in focus.
|
||||
if self.tabpage ~= api.nvim_get_current_tabpage() then
|
||||
callback({ "The update was cancelled." })
|
||||
return
|
||||
end
|
||||
|
||||
perf:lap("received new file list")
|
||||
|
||||
local files = {
|
||||
{ cur_files = self.files.conflicting, new_files = new_files.conflicting },
|
||||
{ cur_files = self.files.working, new_files = new_files.working },
|
||||
{ cur_files = self.files.staged, new_files = new_files.staged },
|
||||
}
|
||||
|
||||
for _, v in ipairs(files) do
|
||||
-- We diff the old file list against the new file list in order to find
|
||||
-- the most efficient way to morph the current list into the new. This
|
||||
-- way we avoid having to discard and recreate buffers for files that
|
||||
-- exist in both lists.
|
||||
---@param aa FileEntry
|
||||
---@param bb FileEntry
|
||||
local diff = Diff(v.cur_files, v.new_files, function(aa, bb)
|
||||
return aa.path == bb.path and aa.oldpath == bb.oldpath
|
||||
end)
|
||||
|
||||
local script = diff:create_edit_script()
|
||||
local ai = 1
|
||||
local bi = 1
|
||||
|
||||
for _, opr in ipairs(script) do
|
||||
if opr == EditToken.NOOP then
|
||||
-- Update status and stats
|
||||
local a_stats = v.cur_files[ai].stats
|
||||
local b_stats = v.new_files[bi].stats
|
||||
|
||||
if a_stats then
|
||||
v.cur_files[ai].stats = vim.tbl_extend("force", a_stats, b_stats or {})
|
||||
else
|
||||
v.cur_files[ai].stats = v.new_files[bi].stats
|
||||
end
|
||||
|
||||
v.cur_files[ai].status = v.new_files[bi].status
|
||||
v.cur_files[ai]:validate_stage_buffers(index_stat)
|
||||
|
||||
if new_head then
|
||||
v.cur_files[ai]:update_heads(new_head)
|
||||
end
|
||||
|
||||
ai = ai + 1
|
||||
bi = bi + 1
|
||||
|
||||
elseif opr == EditToken.DELETE then
|
||||
if self.panel.cur_file == v.cur_files[ai] then
|
||||
local file_list = self.panel:ordered_file_list()
|
||||
if file_list[1] == self.panel.cur_file then
|
||||
self.panel:set_cur_file(nil)
|
||||
else
|
||||
self.panel:set_cur_file(self.panel:prev_file())
|
||||
end
|
||||
end
|
||||
|
||||
v.cur_files[ai]:destroy()
|
||||
table.remove(v.cur_files, ai)
|
||||
|
||||
elseif opr == EditToken.INSERT then
|
||||
table.insert(v.cur_files, ai, v.new_files[bi])
|
||||
ai = ai + 1
|
||||
bi = bi + 1
|
||||
|
||||
elseif opr == EditToken.REPLACE then
|
||||
if self.panel.cur_file == v.cur_files[ai] then
|
||||
local file_list = self.panel:ordered_file_list()
|
||||
if file_list[1] == self.panel.cur_file then
|
||||
self.panel:set_cur_file(nil)
|
||||
else
|
||||
self.panel:set_cur_file(self.panel:prev_file())
|
||||
end
|
||||
end
|
||||
|
||||
v.cur_files[ai]:destroy()
|
||||
v.cur_files[ai] = v.new_files[bi]
|
||||
ai = ai + 1
|
||||
bi = bi + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
perf:lap("updated file list")
|
||||
|
||||
self.merge_ctx = next(new_files.conflicting) and self.adapter:get_merge_context() or nil
|
||||
|
||||
if self.merge_ctx then
|
||||
for _, entry in ipairs(self.files.conflicting) do
|
||||
entry:update_merge_context(self.merge_ctx)
|
||||
end
|
||||
end
|
||||
|
||||
FileEntry.update_index_stat(self.adapter, index_stat)
|
||||
self.files:update_file_trees()
|
||||
self.panel:update_components()
|
||||
self.panel:render()
|
||||
self.panel:redraw()
|
||||
perf:lap("panel redrawn")
|
||||
self.panel:reconstrain_cursor()
|
||||
|
||||
if utils.vec_indexof(self.panel:ordered_file_list(), self.panel.cur_file) == -1 then
|
||||
self.panel:set_cur_file(nil)
|
||||
end
|
||||
|
||||
-- Set initially selected file
|
||||
if not self.initialized and self.options.selected_file then
|
||||
for _, file in self.files:iter() do
|
||||
if file.path == self.options.selected_file then
|
||||
self.panel:set_cur_file(file)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
self:set_file(self.panel.cur_file or self.panel:next_file(), false, not self.initialized)
|
||||
|
||||
self.update_needed = false
|
||||
perf:time()
|
||||
logger:lvl(5):debug(perf)
|
||||
logger:fmt_info(
|
||||
"[%s] Completed update for %d files successfully (%.3f ms)",
|
||||
self.class:name(),
|
||||
self.files:len(),
|
||||
perf.final_time
|
||||
)
|
||||
self.emitter:emit("files_updated", self.files)
|
||||
|
||||
callback()
|
||||
end)
|
||||
)
|
||||
|
||||
---Ensures there are files to load, and loads the null buffer otherwise.
|
||||
---@return boolean
|
||||
function DiffView:file_safeguard()
|
||||
if self.files:len() == 0 then
|
||||
local cur = self.panel.cur_file
|
||||
|
||||
if cur then
|
||||
cur.layout:detach_files()
|
||||
end
|
||||
|
||||
self.cur_layout:open_null()
|
||||
self.nulled = true
|
||||
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function DiffView:on_files_staged(callback)
|
||||
self.emitter:on(EventName.FILES_STAGED, callback)
|
||||
end
|
||||
|
||||
function DiffView:init_event_listeners()
|
||||
local listeners = require("diffview.scene.views.diff.listeners")(self)
|
||||
for event, callback in pairs(listeners) do
|
||||
self.emitter:on(event, callback)
|
||||
end
|
||||
end
|
||||
|
||||
---Infer the current selected file. If the file panel is focused: return the
|
||||
---file entry under the cursor. Otherwise return the file open in the view.
|
||||
---Returns nil if no file is open in the view, or there is no entry under the
|
||||
---cursor in the file panel.
|
||||
---@param allow_dir? boolean Allow directory nodes from the file tree.
|
||||
---@return (FileEntry|DirData)?
|
||||
function DiffView:infer_cur_file(allow_dir)
|
||||
if self.panel:is_focused() then
|
||||
---@type any
|
||||
local item = self.panel:get_item_at_cursor()
|
||||
if not item then return end
|
||||
if not allow_dir and type(item.collapsed) == "boolean" then return end
|
||||
|
||||
return item
|
||||
else
|
||||
return self.panel.cur_file
|
||||
end
|
||||
end
|
||||
|
||||
---Check whether or not the instantiation was successful.
|
||||
---@return boolean
|
||||
function DiffView:is_valid()
|
||||
return self.valid
|
||||
end
|
||||
|
||||
M.DiffView = DiffView
|
||||
|
||||
return M
|
||||
@ -0,0 +1,404 @@
|
||||
local config = require("diffview.config")
|
||||
local oop = require("diffview.oop")
|
||||
local renderer = require("diffview.renderer")
|
||||
local utils = require("diffview.utils")
|
||||
local Panel = require("diffview.ui.panel").Panel
|
||||
local api = vim.api
|
||||
local M = {}
|
||||
|
||||
---@class TreeOptions
|
||||
---@field flatten_dirs boolean
|
||||
---@field folder_statuses "never"|"only_folded"|"always"
|
||||
|
||||
---@class FilePanel : Panel
|
||||
---@field adapter VCSAdapter
|
||||
---@field files FileDict
|
||||
---@field path_args string[]
|
||||
---@field rev_pretty_name string|nil
|
||||
---@field cur_file FileEntry
|
||||
---@field listing_style "list"|"tree"
|
||||
---@field tree_options TreeOptions
|
||||
---@field render_data RenderData
|
||||
---@field components CompStruct
|
||||
---@field constrain_cursor function
|
||||
---@field help_mapping string
|
||||
local FilePanel = oop.create_class("FilePanel", Panel)
|
||||
|
||||
FilePanel.winopts = vim.tbl_extend("force", Panel.winopts, {
|
||||
cursorline = true,
|
||||
winhl = {
|
||||
"EndOfBuffer:DiffviewEndOfBuffer",
|
||||
"Normal:DiffviewNormal",
|
||||
"CursorLine:DiffviewCursorLine",
|
||||
"WinSeparator:DiffviewWinSeparator",
|
||||
"SignColumn:DiffviewNormal",
|
||||
"StatusLine:DiffviewStatusLine",
|
||||
"StatusLineNC:DiffviewStatuslineNC",
|
||||
opt = { method = "prepend" },
|
||||
},
|
||||
})
|
||||
|
||||
FilePanel.bufopts = vim.tbl_extend("force", Panel.bufopts, {
|
||||
filetype = "DiffviewFiles",
|
||||
})
|
||||
|
||||
---FilePanel constructor.
|
||||
---@param adapter VCSAdapter
|
||||
---@param files FileEntry[]
|
||||
---@param path_args string[]
|
||||
function FilePanel:init(adapter, files, path_args, rev_pretty_name)
|
||||
local conf = config.get_config()
|
||||
self:super({
|
||||
config = conf.file_panel.win_config,
|
||||
bufname = "DiffviewFilePanel",
|
||||
})
|
||||
self.adapter = adapter
|
||||
self.files = files
|
||||
self.path_args = path_args
|
||||
self.rev_pretty_name = rev_pretty_name
|
||||
self.listing_style = conf.file_panel.listing_style
|
||||
self.tree_options = conf.file_panel.tree_options
|
||||
|
||||
self:on_autocmd("BufNew", {
|
||||
callback = function()
|
||||
self:setup_buffer()
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
---@override
|
||||
function FilePanel:open()
|
||||
FilePanel.super_class.open(self)
|
||||
vim.cmd("wincmd =")
|
||||
end
|
||||
|
||||
function FilePanel:setup_buffer()
|
||||
local conf = config.get_config()
|
||||
|
||||
local default_opt = { silent = true, nowait = true, buffer = self.bufid }
|
||||
for _, mapping in ipairs(conf.keymaps.file_panel) do
|
||||
local opt = vim.tbl_extend("force", default_opt, mapping[4] or {}, { buffer = self.bufid })
|
||||
vim.keymap.set(mapping[1], mapping[2], mapping[3], opt)
|
||||
end
|
||||
|
||||
local help_keymap = config.find_help_keymap(conf.keymaps.file_panel)
|
||||
if help_keymap then self.help_mapping = help_keymap[2] end
|
||||
end
|
||||
|
||||
function FilePanel:update_components()
|
||||
local conflicting_files
|
||||
local working_files
|
||||
local staged_files
|
||||
|
||||
if self.listing_style == "list" then
|
||||
conflicting_files = { name = "files" }
|
||||
working_files = { name = "files" }
|
||||
staged_files = { name = "files" }
|
||||
|
||||
for _, file in ipairs(self.files.conflicting) do
|
||||
table.insert(conflicting_files, {
|
||||
name = "file",
|
||||
context = file,
|
||||
})
|
||||
end
|
||||
|
||||
for _, file in ipairs(self.files.working) do
|
||||
table.insert(working_files, {
|
||||
name = "file",
|
||||
context = file,
|
||||
})
|
||||
end
|
||||
|
||||
for _, file in ipairs(self.files.staged) do
|
||||
table.insert(staged_files, {
|
||||
name = "file",
|
||||
context = file,
|
||||
})
|
||||
end
|
||||
|
||||
elseif self.listing_style == "tree" then
|
||||
self.files.conflicting_tree:update_statuses()
|
||||
self.files.working_tree:update_statuses()
|
||||
self.files.staged_tree:update_statuses()
|
||||
|
||||
conflicting_files = utils.tbl_merge(
|
||||
{ name = "files" },
|
||||
self.files.conflicting_tree:create_comp_schema({
|
||||
flatten_dirs = self.tree_options.flatten_dirs,
|
||||
})
|
||||
)
|
||||
|
||||
working_files = utils.tbl_merge(
|
||||
{ name = "files" },
|
||||
self.files.working_tree:create_comp_schema({
|
||||
flatten_dirs = self.tree_options.flatten_dirs,
|
||||
})
|
||||
)
|
||||
|
||||
staged_files = utils.tbl_merge(
|
||||
{ name = "files" },
|
||||
self.files.staged_tree:create_comp_schema({
|
||||
flatten_dirs = self.tree_options.flatten_dirs,
|
||||
})
|
||||
)
|
||||
end
|
||||
|
||||
---@type CompStruct
|
||||
self.components = self.render_data:create_component({
|
||||
{ name = "path" },
|
||||
{
|
||||
name = "conflicting",
|
||||
{ name = "title" },
|
||||
conflicting_files,
|
||||
{ name = "margin" },
|
||||
},
|
||||
{
|
||||
name = "working",
|
||||
{ name = "title" },
|
||||
working_files,
|
||||
{ name = "margin" },
|
||||
},
|
||||
{
|
||||
name = "staged",
|
||||
{ name = "title" },
|
||||
staged_files,
|
||||
{ name = "margin" },
|
||||
},
|
||||
{
|
||||
name = "info",
|
||||
{ name = "title" },
|
||||
{ name = "entries" },
|
||||
},
|
||||
})
|
||||
|
||||
self.constrain_cursor = renderer.create_cursor_constraint({
|
||||
self.components.conflicting.files.comp,
|
||||
self.components.working.files.comp,
|
||||
self.components.staged.files.comp,
|
||||
})
|
||||
end
|
||||
|
||||
---@return FileEntry[]
|
||||
function FilePanel:ordered_file_list()
|
||||
if self.listing_style == "list" then
|
||||
local list = {}
|
||||
|
||||
for _, file in self.files:iter() do
|
||||
list[#list + 1] = file
|
||||
end
|
||||
|
||||
return list
|
||||
else
|
||||
local nodes = utils.vec_join(
|
||||
self.files.conflicting_tree.root:leaves(),
|
||||
self.files.working_tree.root:leaves(),
|
||||
self.files.staged_tree.root:leaves()
|
||||
)
|
||||
|
||||
return vim.tbl_map(function(node)
|
||||
return node.data
|
||||
end, nodes) --[[@as vector ]]
|
||||
end
|
||||
end
|
||||
|
||||
function FilePanel:set_cur_file(file)
|
||||
if self.cur_file then
|
||||
self.cur_file:set_active(false)
|
||||
end
|
||||
|
||||
self.cur_file = file
|
||||
if self.cur_file then
|
||||
self.cur_file:set_active(true)
|
||||
end
|
||||
end
|
||||
|
||||
function FilePanel:prev_file()
|
||||
local files = self:ordered_file_list()
|
||||
if not self.cur_file and self.files:len() > 0 then
|
||||
self:set_cur_file(files[1])
|
||||
return self.cur_file
|
||||
end
|
||||
|
||||
local i = utils.vec_indexof(files, self.cur_file)
|
||||
if i ~= -1 then
|
||||
self:set_cur_file(files[(i - vim.v.count1 - 1) % #files + 1])
|
||||
return self.cur_file
|
||||
end
|
||||
end
|
||||
|
||||
function FilePanel:next_file()
|
||||
local files = self:ordered_file_list()
|
||||
if not self.cur_file and self.files:len() > 0 then
|
||||
self:set_cur_file(files[1])
|
||||
return self.cur_file
|
||||
end
|
||||
|
||||
local i = utils.vec_indexof(files, self.cur_file)
|
||||
if i ~= -1 then
|
||||
self:set_cur_file(files[(i + vim.v.count1 - 1) % #files + 1])
|
||||
return self.cur_file
|
||||
end
|
||||
end
|
||||
|
||||
---Get the file entry under the cursor.
|
||||
---@return (FileEntry|DirData)?
|
||||
function FilePanel:get_item_at_cursor()
|
||||
if not self:is_open() and self:buf_loaded() then return end
|
||||
|
||||
local line = api.nvim_win_get_cursor(self.winid)[1]
|
||||
local comp = self.components.comp:get_comp_on_line(line)
|
||||
if comp and comp.name == "file" then
|
||||
return comp.context
|
||||
elseif comp and comp.name == "dir_name" then
|
||||
return comp.parent.context
|
||||
end
|
||||
end
|
||||
|
||||
---Get the parent directory data of the item under the cursor.
|
||||
---@return DirData?
|
||||
---@return RenderComponent?
|
||||
function FilePanel:get_dir_at_cursor()
|
||||
if self.listing_style ~= "tree" then return end
|
||||
if not self:is_open() and self:buf_loaded() then return end
|
||||
|
||||
local line = api.nvim_win_get_cursor(self.winid)[1]
|
||||
local comp = self.components.comp:get_comp_on_line(line)
|
||||
|
||||
if not comp then return end
|
||||
|
||||
if comp.name == "dir_name" then
|
||||
local dir_comp = comp.parent
|
||||
return dir_comp.context, dir_comp
|
||||
elseif comp.name == "file" then
|
||||
local dir_comp = comp.parent.parent
|
||||
return dir_comp.context, dir_comp
|
||||
end
|
||||
end
|
||||
|
||||
function FilePanel:highlight_file(file)
|
||||
if not (self:is_open() and self:buf_loaded()) then
|
||||
return
|
||||
end
|
||||
|
||||
if self.listing_style == "list" then
|
||||
for _, file_list in ipairs({
|
||||
self.components.conflicting.files,
|
||||
self.components.working.files,
|
||||
self.components.staged.files,
|
||||
}) do
|
||||
for _, comp_struct in ipairs(file_list) do
|
||||
if file == comp_struct.comp.context then
|
||||
utils.set_cursor(self.winid, comp_struct.comp.lstart + 1, 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
else -- tree
|
||||
for _, comp_struct in ipairs({
|
||||
self.components.conflicting.files,
|
||||
self.components.working.files,
|
||||
self.components.staged.files,
|
||||
}) do
|
||||
comp_struct.comp:deep_some(function(cur)
|
||||
if file == cur.context then
|
||||
local was_concealed = false
|
||||
local dir = cur.parent.parent
|
||||
|
||||
while dir and dir.name == "directory" do
|
||||
if dir.context and dir.context.collapsed then
|
||||
was_concealed = true
|
||||
dir.context.collapsed = false
|
||||
end
|
||||
|
||||
dir = utils.tbl_access(dir, { "parent", "parent" })
|
||||
end
|
||||
|
||||
if was_concealed then
|
||||
self:render()
|
||||
self:redraw()
|
||||
end
|
||||
|
||||
utils.set_cursor(self.winid, cur.lstart + 1, 0)
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
-- Needed to update the cursorline highlight when the panel is not focused.
|
||||
utils.update_win(self.winid)
|
||||
end
|
||||
|
||||
function FilePanel:highlight_cur_file()
|
||||
if self.cur_file then
|
||||
self:highlight_file(self.cur_file)
|
||||
end
|
||||
end
|
||||
|
||||
function FilePanel:highlight_prev_file()
|
||||
if not (self:is_open() and self:buf_loaded()) or self.files:len() == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
pcall(
|
||||
api.nvim_win_set_cursor,
|
||||
self.winid,
|
||||
{ self.constrain_cursor(self.winid, -vim.v.count1), 0 }
|
||||
)
|
||||
utils.update_win(self.winid)
|
||||
end
|
||||
|
||||
function FilePanel:highlight_next_file()
|
||||
if not (self:is_open() and self:buf_loaded()) or self.files:len() == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
pcall(api.nvim_win_set_cursor, self.winid, {
|
||||
self.constrain_cursor(self.winid, vim.v.count1),
|
||||
0,
|
||||
})
|
||||
utils.update_win(self.winid)
|
||||
end
|
||||
|
||||
function FilePanel:reconstrain_cursor()
|
||||
if not (self:is_open() and self:buf_loaded()) or self.files:len() == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
pcall(api.nvim_win_set_cursor, self.winid, {
|
||||
self.constrain_cursor(self.winid, 0),
|
||||
0,
|
||||
})
|
||||
end
|
||||
|
||||
---@param item DirData|any
|
||||
---@param open boolean
|
||||
function FilePanel:set_item_fold(item, open)
|
||||
if type(item.collapsed) == "boolean" and open == item.collapsed then
|
||||
item.collapsed = not open
|
||||
self:render()
|
||||
self:redraw()
|
||||
|
||||
if item.collapsed then
|
||||
self.components.comp:deep_some(function(comp, _, _)
|
||||
if comp.context == item then
|
||||
utils.set_cursor(self.winid, comp.lstart + 1)
|
||||
return true
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function FilePanel:toggle_item_fold(item)
|
||||
self:set_item_fold(item, item.collapsed)
|
||||
end
|
||||
|
||||
function FilePanel:render()
|
||||
require("diffview.scene.views.diff.render")(self)
|
||||
end
|
||||
|
||||
M.FilePanel = FilePanel
|
||||
return M
|
||||
@ -0,0 +1,321 @@
|
||||
local async = require("diffview.async")
|
||||
local lazy = require("diffview.lazy")
|
||||
|
||||
local EventName = lazy.access("diffview.events", "EventName") ---@type EventName|LazyModule
|
||||
local RevType = lazy.access("diffview.vcs.rev", "RevType") ---@type RevType|LazyModule
|
||||
local actions = lazy.require("diffview.actions") ---@module "diffview.actions"
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
local vcs_utils = lazy.require("diffview.vcs.utils") ---@module "diffview.vcs.utils"
|
||||
|
||||
local api = vim.api
|
||||
local await = async.await
|
||||
|
||||
---@param view DiffView
|
||||
return function(view)
|
||||
return {
|
||||
tab_enter = function()
|
||||
local file = view.panel.cur_file
|
||||
if file then
|
||||
view:set_file(file, false, true)
|
||||
end
|
||||
|
||||
if view.ready then
|
||||
view:update_files()
|
||||
end
|
||||
end,
|
||||
tab_leave = function()
|
||||
local file = view.panel.cur_file
|
||||
|
||||
if file then
|
||||
file.layout:detach_files()
|
||||
end
|
||||
|
||||
for _, f in view.panel.files:iter() do
|
||||
f.layout:restore_winopts()
|
||||
end
|
||||
end,
|
||||
buf_write_post = function()
|
||||
if view.adapter:has_local(view.left, view.right) then
|
||||
view.update_needed = true
|
||||
if api.nvim_get_current_tabpage() == view.tabpage then
|
||||
view:update_files()
|
||||
end
|
||||
end
|
||||
end,
|
||||
file_open_new = function(_, entry)
|
||||
api.nvim_win_call(view.cur_layout:get_main_win().id, function()
|
||||
utils.set_cursor(0, 1, 0)
|
||||
|
||||
if view.cur_entry and view.cur_entry.kind == "conflicting" then
|
||||
actions.next_conflict()
|
||||
vim.cmd("norm! zz")
|
||||
end
|
||||
end)
|
||||
|
||||
view.cur_layout:sync_scroll()
|
||||
end,
|
||||
---@diagnostic disable-next-line: unused-local
|
||||
files_updated = function(_, files)
|
||||
view.initialized = true
|
||||
end,
|
||||
close = function()
|
||||
if view.panel:is_focused() then
|
||||
view.panel:close()
|
||||
elseif view:is_cur_tabpage() then
|
||||
view:close()
|
||||
end
|
||||
end,
|
||||
select_next_entry = function()
|
||||
view:next_file(true)
|
||||
end,
|
||||
select_prev_entry = function()
|
||||
view:prev_file(true)
|
||||
end,
|
||||
next_entry = function()
|
||||
view.panel:highlight_next_file()
|
||||
end,
|
||||
prev_entry = function()
|
||||
view.panel:highlight_prev_file()
|
||||
end,
|
||||
select_entry = function()
|
||||
if view.panel:is_open() then
|
||||
---@type any
|
||||
local item = view.panel:get_item_at_cursor()
|
||||
if item then
|
||||
if type(item.collapsed) == "boolean" then
|
||||
view.panel:toggle_item_fold(item)
|
||||
else
|
||||
view:set_file(item, false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
focus_entry = function()
|
||||
if view.panel:is_open() then
|
||||
---@type any
|
||||
local item = view.panel:get_item_at_cursor()
|
||||
if item then
|
||||
if type(item.collapsed) == "boolean" then
|
||||
view.panel:toggle_item_fold(item)
|
||||
else
|
||||
view:set_file(item, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
open_commit_log = function()
|
||||
if view.left.type == RevType.STAGE and view.right.type == RevType.LOCAL then
|
||||
utils.info("Changes not committed yet. No log available for these changes.")
|
||||
return
|
||||
end
|
||||
|
||||
local range = view.adapter.Rev.to_range(view.left, view.right)
|
||||
|
||||
if range then
|
||||
view.commit_log_panel:update(range)
|
||||
end
|
||||
end,
|
||||
toggle_stage_entry = function()
|
||||
if not (view.left.type == RevType.STAGE and view.right.type == RevType.LOCAL) then
|
||||
return
|
||||
end
|
||||
|
||||
local item = view:infer_cur_file(true)
|
||||
if item then
|
||||
local success
|
||||
if item.kind == "working" or item.kind == "conflicting" then
|
||||
success = view.adapter:add_files({ item.path })
|
||||
elseif item.kind == "staged" then
|
||||
success = view.adapter:reset_files({ item.path })
|
||||
end
|
||||
|
||||
if not success then
|
||||
utils.err(("Failed to stage/unstage file: '%s'"):format(item.path))
|
||||
return
|
||||
end
|
||||
|
||||
if type(item.collapsed) == "boolean" then
|
||||
---@cast item DirData
|
||||
---@type FileTree
|
||||
local tree
|
||||
|
||||
if item.kind == "conflicting" then
|
||||
tree = view.panel.files.conflicting_tree
|
||||
elseif item.kind == "working" then
|
||||
tree = view.panel.files.working_tree
|
||||
else
|
||||
tree = view.panel.files.staged_tree
|
||||
end
|
||||
|
||||
---@type Node
|
||||
local item_node
|
||||
tree.root:deep_some(function(node, _, _)
|
||||
if node == item._node then
|
||||
item_node = node
|
||||
return true
|
||||
end
|
||||
end)
|
||||
|
||||
if item_node then
|
||||
local next_leaf = item_node:next_leaf()
|
||||
if next_leaf then
|
||||
view:set_file(next_leaf.data)
|
||||
else
|
||||
view:set_file(view.panel.files[1])
|
||||
end
|
||||
end
|
||||
else
|
||||
view.panel:set_cur_file(item)
|
||||
view:next_file()
|
||||
end
|
||||
|
||||
view:update_files(
|
||||
vim.schedule_wrap(function()
|
||||
view.panel:highlight_cur_file()
|
||||
end)
|
||||
)
|
||||
view.emitter:emit(EventName.FILES_STAGED, view)
|
||||
end
|
||||
end,
|
||||
stage_all = function()
|
||||
local args = vim.tbl_map(function(file)
|
||||
return file.path
|
||||
end, utils.vec_join(view.files.working, view.files.conflicting))
|
||||
|
||||
if #args > 0 then
|
||||
local success = view.adapter:add_files(args)
|
||||
|
||||
if not success then
|
||||
utils.err("Failed to stage files!")
|
||||
return
|
||||
end
|
||||
|
||||
view:update_files(function()
|
||||
view.panel:highlight_cur_file()
|
||||
end)
|
||||
view.emitter:emit(EventName.FILES_STAGED, view)
|
||||
end
|
||||
end,
|
||||
unstage_all = function()
|
||||
local success = view.adapter:reset_files()
|
||||
|
||||
if not success then
|
||||
utils.err("Failed to unstage files!")
|
||||
return
|
||||
end
|
||||
|
||||
view:update_files()
|
||||
view.emitter:emit(EventName.FILES_STAGED, view)
|
||||
end,
|
||||
restore_entry = async.void(function()
|
||||
if view.right.type ~= RevType.LOCAL then
|
||||
utils.err("The right side of the diff is not local! Aborting file restoration.")
|
||||
return
|
||||
end
|
||||
|
||||
local commit
|
||||
|
||||
if view.left.type ~= RevType.STAGE then
|
||||
commit = view.left.commit
|
||||
end
|
||||
|
||||
local file = view:infer_cur_file()
|
||||
if not file then return end
|
||||
|
||||
local bufid = utils.find_file_buffer(file.path)
|
||||
|
||||
if bufid and vim.bo[bufid].modified then
|
||||
utils.err("The file is open with unsaved changes! Aborting file restoration.")
|
||||
return
|
||||
end
|
||||
|
||||
await(vcs_utils.restore_file(view.adapter, file.path, file.kind, commit))
|
||||
view:update_files()
|
||||
end),
|
||||
listing_style = function()
|
||||
if view.panel.listing_style == "list" then
|
||||
view.panel.listing_style = "tree"
|
||||
else
|
||||
view.panel.listing_style = "list"
|
||||
end
|
||||
view.panel:update_components()
|
||||
view.panel:render()
|
||||
view.panel:redraw()
|
||||
end,
|
||||
toggle_flatten_dirs = function()
|
||||
view.panel.tree_options.flatten_dirs = not view.panel.tree_options.flatten_dirs
|
||||
view.panel:update_components()
|
||||
view.panel:render()
|
||||
view.panel:redraw()
|
||||
end,
|
||||
focus_files = function()
|
||||
view.panel:focus()
|
||||
end,
|
||||
toggle_files = function()
|
||||
view.panel:toggle(true)
|
||||
end,
|
||||
refresh_files = function()
|
||||
view:update_files()
|
||||
end,
|
||||
open_all_folds = function()
|
||||
if not view.panel:is_focused() or view.panel.listing_style ~= "tree" then return end
|
||||
|
||||
for _, file_set in ipairs({
|
||||
view.panel.components.conflicting.files,
|
||||
view.panel.components.working.files,
|
||||
view.panel.components.staged.files,
|
||||
}) do
|
||||
file_set.comp:deep_some(function(comp, _, _)
|
||||
if comp.name == "directory" then
|
||||
(comp.context --[[@as DirData ]]).collapsed = false
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
view.panel:render()
|
||||
view.panel:redraw()
|
||||
end,
|
||||
close_all_folds = function()
|
||||
if not view.panel:is_focused() or view.panel.listing_style ~= "tree" then return end
|
||||
|
||||
for _, file_set in ipairs({
|
||||
view.panel.components.conflicting.files,
|
||||
view.panel.components.working.files,
|
||||
view.panel.components.staged.files,
|
||||
}) do
|
||||
file_set.comp:deep_some(function(comp, _, _)
|
||||
if comp.name == "directory" then
|
||||
(comp.context --[[@as DirData ]]).collapsed = true
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
view.panel:render()
|
||||
view.panel:redraw()
|
||||
end,
|
||||
open_fold = function()
|
||||
if not view.panel:is_focused() then return end
|
||||
local dir = view.panel:get_dir_at_cursor()
|
||||
if dir then view.panel:set_item_fold(dir, true) end
|
||||
end,
|
||||
close_fold = function()
|
||||
if not view.panel:is_focused() then return end
|
||||
local dir, comp = view.panel:get_dir_at_cursor()
|
||||
if dir and comp then
|
||||
if not dir.collapsed then
|
||||
view.panel:set_item_fold(dir, false)
|
||||
else
|
||||
local dir_parent = utils.tbl_access(comp, "parent.parent")
|
||||
if dir_parent and dir_parent.name == "directory" then
|
||||
view.panel:set_item_fold(dir_parent.context, false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
toggle_fold = function()
|
||||
if not view.panel:is_focused() then return end
|
||||
local dir = view.panel:get_dir_at_cursor()
|
||||
if dir then view.panel:toggle_item_fold(dir) end
|
||||
end,
|
||||
}
|
||||
end
|
||||
@ -0,0 +1,204 @@
|
||||
local config = require("diffview.config")
|
||||
local hl = require("diffview.hl")
|
||||
local utils = require("diffview.utils")
|
||||
|
||||
local pl = utils.path
|
||||
|
||||
---@param comp RenderComponent
|
||||
---@param show_path boolean
|
||||
---@param depth integer|nil
|
||||
local function render_file(comp, show_path, depth)
|
||||
---@type FileEntry
|
||||
local file = comp.context
|
||||
|
||||
comp:add_text(file.status .. " ", hl.get_git_hl(file.status))
|
||||
|
||||
if depth then
|
||||
comp:add_text(string.rep(" ", depth * 2 + 2))
|
||||
end
|
||||
|
||||
local icon, icon_hl = hl.get_file_icon(file.basename, file.extension)
|
||||
comp:add_text(icon, icon_hl)
|
||||
comp:add_text(file.basename, file.active and "DiffviewFilePanelSelected" or "DiffviewFilePanelFileName")
|
||||
|
||||
if file.stats then
|
||||
if file.stats.additions then
|
||||
comp:add_text(" " .. file.stats.additions, "DiffviewFilePanelInsertions")
|
||||
comp:add_text(", ")
|
||||
comp:add_text(tostring(file.stats.deletions), "DiffviewFilePanelDeletions")
|
||||
elseif file.stats.conflicts then
|
||||
local has_conflicts = file.stats.conflicts > 0
|
||||
comp:add_text(
|
||||
" " .. (has_conflicts and file.stats.conflicts or config.get_config().signs.done),
|
||||
has_conflicts and "DiffviewFilePanelConflicts" or "DiffviewFilePanelInsertions"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
if file.kind == "conflicting" and not (file.stats and file.stats.conflicts) then
|
||||
comp:add_text(" !", "DiffviewFilePanelConflicts")
|
||||
end
|
||||
|
||||
if show_path then
|
||||
comp:add_text(" " .. file.parent_path, "DiffviewFilePanelPath")
|
||||
end
|
||||
|
||||
comp:ln()
|
||||
end
|
||||
|
||||
---@param comp RenderComponent
|
||||
local function render_file_list(comp)
|
||||
for _, file_comp in ipairs(comp.components) do
|
||||
render_file(file_comp, true)
|
||||
end
|
||||
end
|
||||
|
||||
---@param ctx DirData
|
||||
---@param tree_options TreeOptions
|
||||
---@return string
|
||||
local function get_dir_status_text(ctx, tree_options)
|
||||
local folder_statuses = tree_options.folder_statuses
|
||||
|
||||
if folder_statuses == "always" or (folder_statuses == "only_folded" and ctx.collapsed) then
|
||||
return ctx.status
|
||||
end
|
||||
|
||||
return " "
|
||||
end
|
||||
|
||||
---@param depth integer
|
||||
---@param comp RenderComponent
|
||||
local function render_file_tree_recurse(depth, comp)
|
||||
local conf = config.get_config()
|
||||
|
||||
if comp.name == "file" then
|
||||
render_file(comp, false, depth)
|
||||
return
|
||||
end
|
||||
|
||||
if comp.name ~= "directory" then return end
|
||||
|
||||
-- Directory component structure:
|
||||
-- {
|
||||
-- name = "directory",
|
||||
-- context = <DirData>,
|
||||
-- { name = "dir_name" },
|
||||
-- { name = "items", ...<files> },
|
||||
-- }
|
||||
|
||||
local dir = comp.components[1]
|
||||
local items = comp.components[2]
|
||||
local ctx = comp.context --[[@as DirData ]]
|
||||
|
||||
dir:add_text(
|
||||
get_dir_status_text(ctx, conf.file_panel.tree_options) .. " ",
|
||||
hl.get_git_hl(ctx.status)
|
||||
)
|
||||
dir:add_text(string.rep(" ", depth * 2))
|
||||
dir:add_text(ctx.collapsed and conf.signs.fold_closed or conf.signs.fold_open, "DiffviewNonText")
|
||||
|
||||
if conf.use_icons then
|
||||
dir:add_text(
|
||||
" " .. (ctx.collapsed and conf.icons.folder_closed or conf.icons.folder_open) .. " ",
|
||||
"DiffviewFolderSign"
|
||||
)
|
||||
end
|
||||
|
||||
dir:add_text(ctx.name, "DiffviewFolderName")
|
||||
dir:ln()
|
||||
|
||||
if not ctx.collapsed then
|
||||
for _, item in ipairs(items.components) do
|
||||
render_file_tree_recurse(depth + 1, item)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param comp RenderComponent
|
||||
local function render_file_tree(comp)
|
||||
for _, c in ipairs(comp.components) do
|
||||
render_file_tree_recurse(0, c)
|
||||
end
|
||||
end
|
||||
|
||||
---@param listing_style "list"|"tree"
|
||||
---@param comp RenderComponent
|
||||
local function render_files(listing_style, comp)
|
||||
if listing_style == "list" then
|
||||
return render_file_list(comp)
|
||||
end
|
||||
render_file_tree(comp)
|
||||
end
|
||||
|
||||
---@param panel FilePanel
|
||||
return function(panel)
|
||||
if not panel.render_data then
|
||||
return
|
||||
end
|
||||
|
||||
panel.render_data:clear()
|
||||
local conf = config.get_config()
|
||||
local width = panel:infer_width()
|
||||
|
||||
local comp = panel.components.path.comp
|
||||
|
||||
comp:add_line(
|
||||
pl:truncate(pl:vim_fnamemodify(panel.adapter.ctx.toplevel, ":~"), width - 6),
|
||||
"DiffviewFilePanelRootPath"
|
||||
)
|
||||
|
||||
if conf.show_help_hints and panel.help_mapping then
|
||||
comp:add_text("Help: ", "DiffviewFilePanelPath")
|
||||
comp:add_line(panel.help_mapping, "DiffviewFilePanelCounter")
|
||||
comp:add_line()
|
||||
end
|
||||
|
||||
if #panel.files.conflicting > 0 then
|
||||
comp = panel.components.conflicting.title.comp
|
||||
comp:add_text("Conflicts ", "DiffviewFilePanelTitle")
|
||||
comp:add_text("(" .. #panel.files.conflicting .. ")", "DiffviewFilePanelCounter")
|
||||
comp:ln()
|
||||
|
||||
render_files(panel.listing_style, panel.components.conflicting.files.comp)
|
||||
panel.components.conflicting.margin.comp:add_line()
|
||||
end
|
||||
|
||||
local has_other_files = #panel.files.conflicting > 0 or #panel.files.staged > 0
|
||||
|
||||
-- Don't show the 'Changes' section if it's empty and we have other visible
|
||||
-- sections.
|
||||
if #panel.files.working > 0 or not has_other_files then
|
||||
comp = panel.components.working.title.comp
|
||||
comp:add_text("Changes ", "DiffviewFilePanelTitle")
|
||||
comp:add_text("(" .. #panel.files.working .. ")", "DiffviewFilePanelCounter")
|
||||
comp:ln()
|
||||
|
||||
render_files(panel.listing_style, panel.components.working.files.comp)
|
||||
panel.components.working.margin.comp:add_line()
|
||||
end
|
||||
|
||||
if #panel.files.staged > 0 then
|
||||
comp = panel.components.staged.title.comp
|
||||
comp:add_text("Staged changes ", "DiffviewFilePanelTitle")
|
||||
comp:add_text("(" .. #panel.files.staged .. ")", "DiffviewFilePanelCounter")
|
||||
comp:ln()
|
||||
|
||||
render_files(panel.listing_style, panel.components.staged.files.comp)
|
||||
panel.components.staged.margin.comp:add_line()
|
||||
end
|
||||
|
||||
if panel.rev_pretty_name or (panel.path_args and #panel.path_args > 0) then
|
||||
local extra_info = utils.vec_join({ panel.rev_pretty_name }, panel.path_args or {})
|
||||
|
||||
comp = panel.components.info.title.comp
|
||||
comp:add_line("Showing changes for:", "DiffviewFilePanelTitle")
|
||||
|
||||
comp = panel.components.info.entries.comp
|
||||
|
||||
for _, arg in ipairs(extra_info) do
|
||||
local relpath = pl:relative(arg, panel.adapter.ctx.toplevel)
|
||||
if relpath == "" then relpath = "." end
|
||||
comp:add_line(pl:truncate(relpath, width - 5), "DiffviewFilePanelPath")
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,551 @@
|
||||
local async = require("diffview.async")
|
||||
local lazy = require("diffview.lazy")
|
||||
|
||||
local FHOptionPanel = lazy.access("diffview.scene.views.file_history.option_panel", "FHOptionPanel") ---@type FHOptionPanel|LazyModule
|
||||
local JobStatus = lazy.access("diffview.vcs.utils", "JobStatus") ---@type JobStatus|LazyModule
|
||||
local LogEntry = lazy.access("diffview.vcs.log_entry", "LogEntry") ---@type LogEntry|LazyModule
|
||||
local Panel = lazy.access("diffview.ui.panel", "Panel") ---@type Panel|LazyModule
|
||||
local PerfTimer = lazy.access("diffview.perf", "PerfTimer") ---@type PerfTimer|LazyModule
|
||||
local Signal = lazy.access("diffview.control", "Signal") ---@type Signal|LazyModule
|
||||
local WorkPool = lazy.access("diffview.control", "WorkPool") ---@type WorkPool|LazyModule
|
||||
local config = lazy.require("diffview.config") ---@module "diffview.config"
|
||||
local debounce = lazy.require("diffview.debounce") ---@module "diffview.debounce"
|
||||
local oop = lazy.require("diffview.oop") ---@module "diffview.oop"
|
||||
local panel_renderer = lazy.require("diffview.scene.views.file_history.render") ---@module "diffview.scene.views.file_history.render"
|
||||
local renderer = lazy.require("diffview.renderer") ---@module "diffview.renderer"
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
|
||||
local api = vim.api
|
||||
local await = async.await
|
||||
local fmt = string.format
|
||||
local logger = DiffviewGlobal.logger
|
||||
|
||||
local M = {}
|
||||
|
||||
---@type PerfTimer
|
||||
local perf_render = PerfTimer("[FileHistoryPanel] render")
|
||||
---@type PerfTimer
|
||||
local perf_update = PerfTimer("[FileHistoryPanel] update")
|
||||
|
||||
---@alias FileHistoryPanel.CurItem { [1]: LogEntry, [2]: FileEntry }
|
||||
|
||||
---@class FileHistoryPanel : Panel
|
||||
---@field parent FileHistoryView
|
||||
---@field adapter VCSAdapter
|
||||
---@field entries LogEntry[]
|
||||
---@field rev_range RevRange
|
||||
---@field log_options ConfigLogOptions
|
||||
---@field cur_item FileHistoryPanel.CurItem
|
||||
---@field single_file boolean
|
||||
---@field work_pool WorkPool
|
||||
---@field shutdown Signal
|
||||
---@field updating boolean
|
||||
---@field render_data RenderData
|
||||
---@field option_panel FHOptionPanel
|
||||
---@field option_mapping string
|
||||
---@field help_mapping string
|
||||
---@field components CompStruct
|
||||
---@field constrain_cursor function
|
||||
local FileHistoryPanel = oop.create_class("FileHistoryPanel", Panel.__get())
|
||||
|
||||
FileHistoryPanel.winopts = vim.tbl_extend("force", Panel.winopts, {
|
||||
cursorline = true,
|
||||
winhl = {
|
||||
"EndOfBuffer:DiffviewEndOfBuffer",
|
||||
"Normal:DiffviewNormal",
|
||||
"CursorLine:DiffviewCursorLine",
|
||||
"WinSeparator:DiffviewWinSeparator",
|
||||
"SignColumn:DiffviewNormal",
|
||||
"StatusLine:DiffviewStatusLine",
|
||||
"StatusLineNC:DiffviewStatuslineNC",
|
||||
},
|
||||
})
|
||||
|
||||
FileHistoryPanel.bufopts = vim.tbl_extend("force", Panel.bufopts, {
|
||||
filetype = "DiffviewFileHistory",
|
||||
})
|
||||
|
||||
---@class FileHistoryPanel.init.Opt
|
||||
---@field parent FileHistoryView
|
||||
---@field adapter VCSAdapter
|
||||
---@field entries LogEntry[]
|
||||
---@field log_options LogOptions
|
||||
|
||||
---FileHistoryPanel constructor.
|
||||
---@param opt FileHistoryPanel.init.Opt
|
||||
function FileHistoryPanel:init(opt)
|
||||
local conf = config.get_config()
|
||||
|
||||
self:super({
|
||||
config = conf.file_history_panel.win_config,
|
||||
bufname = "DiffviewFileHistoryPanel",
|
||||
})
|
||||
|
||||
self.parent = opt.parent
|
||||
self.adapter = opt.adapter
|
||||
self.entries = opt.entries
|
||||
self.cur_item = {}
|
||||
self.single_file = opt.entries[1] and opt.entries[1].single_file
|
||||
self.work_pool = WorkPool()
|
||||
self.shutdown = Signal()
|
||||
self.updating = false
|
||||
self.option_panel = FHOptionPanel(self, self.adapter.flags)
|
||||
self.log_options = {
|
||||
single_file = vim.tbl_extend(
|
||||
"force",
|
||||
conf.file_history_panel.log_options[self.adapter.config_key].single_file,
|
||||
opt.log_options
|
||||
),
|
||||
multi_file = vim.tbl_extend(
|
||||
"force",
|
||||
conf.file_history_panel.log_options[self.adapter.config_key].multi_file,
|
||||
opt.log_options
|
||||
),
|
||||
}
|
||||
|
||||
self:on_autocmd("BufNew", {
|
||||
callback = function()
|
||||
self:setup_buffer()
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
---@override
|
||||
function FileHistoryPanel:open()
|
||||
FileHistoryPanel.super_class.open(self)
|
||||
vim.cmd("wincmd =")
|
||||
end
|
||||
|
||||
---@override
|
||||
---@param self FileHistoryPanel
|
||||
FileHistoryPanel.destroy = async.sync_void(function(self)
|
||||
self.shutdown:send()
|
||||
|
||||
await(self.work_pool)
|
||||
await(async.scheduler())
|
||||
|
||||
for _, entry in ipairs(self.entries) do
|
||||
entry:destroy()
|
||||
end
|
||||
|
||||
self.entries = nil
|
||||
self.cur_item = nil
|
||||
self.option_panel:destroy()
|
||||
self.option_panel = nil
|
||||
self.render_data:destroy()
|
||||
|
||||
if self.components then
|
||||
renderer.destroy_comp_struct(self.components)
|
||||
end
|
||||
|
||||
FileHistoryPanel.super_class.destroy(self)
|
||||
end)
|
||||
|
||||
function FileHistoryPanel:setup_buffer()
|
||||
local conf = config.get_config()
|
||||
local default_opt = { silent = true, nowait = true, buffer = self.bufid }
|
||||
|
||||
for _, mapping in ipairs(conf.keymaps.file_history_panel) do
|
||||
local opt = vim.tbl_extend("force", default_opt, mapping[4] or {}, { buffer = self.bufid })
|
||||
vim.keymap.set(mapping[1], mapping[2], mapping[3], opt)
|
||||
end
|
||||
|
||||
local option_keymap = config.find_option_keymap(conf.keymaps.file_history_panel)
|
||||
if option_keymap then self.option_mapping = option_keymap[2] end
|
||||
|
||||
local help_keymap = config.find_help_keymap(conf.keymaps.file_history_panel)
|
||||
if help_keymap then self.help_mapping = help_keymap[2] end
|
||||
end
|
||||
|
||||
function FileHistoryPanel:update_components()
|
||||
self.render_data:destroy()
|
||||
if self.components then
|
||||
renderer.destroy_comp_struct(self.components)
|
||||
end
|
||||
|
||||
local entry_schema = { name = "entries" }
|
||||
for i, entry in ipairs(utils.vec_slice(self.entries)) do
|
||||
if self.updating and i > 128 then
|
||||
break
|
||||
end
|
||||
table.insert(entry_schema, {
|
||||
name = "entry",
|
||||
context = entry,
|
||||
{ name = "commit" },
|
||||
{ name = "files" },
|
||||
})
|
||||
end
|
||||
|
||||
---@type CompStruct
|
||||
self.components = self.render_data:create_component({
|
||||
{ name = "header" },
|
||||
{
|
||||
name = "log",
|
||||
{ name = "title" },
|
||||
entry_schema,
|
||||
},
|
||||
})
|
||||
|
||||
self.constrain_cursor = renderer.create_cursor_constraint({ self.components.log.entries.comp })
|
||||
end
|
||||
|
||||
---@param self FileHistoryPanel
|
||||
---@param callback function
|
||||
FileHistoryPanel.update_entries = async.wrap(function(self, callback)
|
||||
perf_update:reset()
|
||||
local checkout = self.work_pool:check_in()
|
||||
|
||||
for _, entry in ipairs(self.entries) do
|
||||
entry:destroy()
|
||||
end
|
||||
|
||||
panel_renderer.clear_cache(self)
|
||||
self.cur_item = {}
|
||||
self.entries = {}
|
||||
self.updating = true
|
||||
|
||||
local stream = self.adapter:file_history({
|
||||
log_opt = self.log_options,
|
||||
layout_opt = { default_layout = self.parent.get_default_layout() },
|
||||
})
|
||||
|
||||
self:sync()
|
||||
|
||||
local render = debounce.throttle_render(
|
||||
15,
|
||||
function()
|
||||
if self.shutdown:check() then return end
|
||||
if not self:cur_file() then
|
||||
self:update_components()
|
||||
self.parent:next_item()
|
||||
else
|
||||
self:sync()
|
||||
end
|
||||
|
||||
vim.cmd("redraw")
|
||||
end
|
||||
)
|
||||
|
||||
local ret = {}
|
||||
|
||||
for _, item in stream:iter() do
|
||||
if self.shutdown:check() then
|
||||
stream:close(self.shutdown:new_consumer())
|
||||
ret = { nil, JobStatus.KILLED }
|
||||
break
|
||||
end
|
||||
|
||||
---@type JobStatus, LogEntry?, string?
|
||||
local status, entry, msg = unpack(item, 1, 3)
|
||||
|
||||
if status == JobStatus.ERROR then
|
||||
utils.err(fmt("Updating file history failed! Error message: %s", msg), true)
|
||||
ret = { nil, JobStatus.ERROR, msg }
|
||||
break
|
||||
elseif status == JobStatus.SUCCESS then
|
||||
ret = { self.entries, status }
|
||||
perf_update:time()
|
||||
logger:fmt_info(
|
||||
"[FileHistory] Completed update for %d entries successfully (%.3f ms).",
|
||||
#self.entries,
|
||||
perf_update.final_time
|
||||
)
|
||||
elseif status == JobStatus.PROGRESS then
|
||||
---@cast entry -?
|
||||
local was_empty = #self.entries == 0
|
||||
self.entries[#self.entries+1] = entry
|
||||
|
||||
if was_empty then
|
||||
self.single_file = self.entries[1].single_file
|
||||
end
|
||||
|
||||
render()
|
||||
else
|
||||
error("Unexpected state!")
|
||||
end
|
||||
end
|
||||
|
||||
await(async.scheduler())
|
||||
self.updating = false
|
||||
|
||||
if not self.shutdown:check() then
|
||||
self:sync()
|
||||
self.option_panel:sync()
|
||||
vim.cmd("redraw")
|
||||
end
|
||||
|
||||
checkout:send()
|
||||
callback(unpack(ret, 1, 3))
|
||||
end)
|
||||
|
||||
function FileHistoryPanel:num_items()
|
||||
if self.single_file then
|
||||
return #self.entries
|
||||
else
|
||||
local count = 0
|
||||
|
||||
for _, entry in ipairs(self.entries) do
|
||||
count = count + #entry.files
|
||||
end
|
||||
|
||||
return count
|
||||
end
|
||||
end
|
||||
|
||||
---@return FileEntry[]
|
||||
function FileHistoryPanel:list_files()
|
||||
local files = {}
|
||||
|
||||
for _, entry in ipairs(self.entries) do
|
||||
for _, file in ipairs(entry.files) do
|
||||
table.insert(files, file)
|
||||
end
|
||||
end
|
||||
|
||||
return files
|
||||
end
|
||||
|
||||
---@param file FileEntry
|
||||
function FileHistoryPanel:find_entry(file)
|
||||
for _, entry in ipairs(self.entries) do
|
||||
for _, f in ipairs(entry.files) do
|
||||
if f == file then
|
||||
return entry
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Get the log or file entry under the cursor.
|
||||
---@return (LogEntry|FileEntry)?
|
||||
function FileHistoryPanel:get_item_at_cursor()
|
||||
if not self:is_open() and self:buf_loaded() then return end
|
||||
|
||||
local cursor = api.nvim_win_get_cursor(self.winid)
|
||||
local line = cursor[1]
|
||||
local comp = self.components.comp:get_comp_on_line(line)
|
||||
|
||||
if comp and (comp.name == "commit" or comp.name == "files") then
|
||||
local entry = comp.parent.context --[[@as table ]]
|
||||
|
||||
if comp.name == "files" then
|
||||
return entry.files[line - comp.lstart]
|
||||
end
|
||||
|
||||
return entry
|
||||
end
|
||||
end
|
||||
|
||||
---Get the parent log entry of the item under the cursor.
|
||||
---@return LogEntry?
|
||||
function FileHistoryPanel:get_log_entry_at_cursor()
|
||||
local item = self:get_item_at_cursor()
|
||||
if not item then return end
|
||||
|
||||
if item:instanceof(LogEntry.__get()) then
|
||||
return item --[[@as LogEntry ]]
|
||||
end
|
||||
|
||||
return self:find_entry(item --[[@as FileEntry ]])
|
||||
end
|
||||
|
||||
---@param new_item FileHistoryPanel.CurItem
|
||||
function FileHistoryPanel:set_cur_item(new_item)
|
||||
if self.cur_item[2] then
|
||||
self.cur_item[2]:set_active(false)
|
||||
end
|
||||
|
||||
self.cur_item = new_item
|
||||
|
||||
if self.cur_item and self.cur_item[2] then
|
||||
self.cur_item[2]:set_active(true)
|
||||
end
|
||||
end
|
||||
|
||||
function FileHistoryPanel:set_entry_from_file(item)
|
||||
local file = self.cur_item[2]
|
||||
|
||||
if item:instanceof(LogEntry.__get()) then
|
||||
self:set_cur_item({ item, item.files[1] })
|
||||
else
|
||||
local entry = self:find_entry(file)
|
||||
|
||||
if entry then
|
||||
self:set_cur_item({ entry, file })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function FileHistoryPanel:cur_file()
|
||||
return self.cur_item[2]
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param entry_idx integer
|
||||
---@param file_idx integer
|
||||
---@param offset integer
|
||||
---@return LogEntry?
|
||||
---@return FileEntry?
|
||||
function FileHistoryPanel:_get_entry_by_file_offset(entry_idx, file_idx, offset)
|
||||
local cur_entry = self.entries[entry_idx]
|
||||
|
||||
if cur_entry.files[file_idx + offset] then
|
||||
return cur_entry, cur_entry.files[file_idx + offset]
|
||||
end
|
||||
|
||||
local sign = utils.sign(offset)
|
||||
local delta = math.abs(offset) - (sign > 0 and #cur_entry.files - file_idx or file_idx - 1)
|
||||
local i = (entry_idx + (sign > 0 and 0 or -2)) % #self.entries + 1
|
||||
|
||||
while i ~= entry_idx do
|
||||
local files = self.entries[i].files
|
||||
|
||||
if (#files - delta) >= 0 then
|
||||
local target_file = sign > 0 and files[delta] or files[#files - (delta - 1)]
|
||||
return self.entries[i], target_file
|
||||
end
|
||||
|
||||
delta = delta - #files
|
||||
i = (i + (sign > 0 and 0 or -2)) % #self.entries + 1
|
||||
end
|
||||
end
|
||||
|
||||
function FileHistoryPanel:set_file_by_offset(offset)
|
||||
if self:num_items() == 0 then return end
|
||||
|
||||
local entry, file = self.cur_item[1], self.cur_item[2]
|
||||
|
||||
if not (entry and file) and self:num_items() > 0 then
|
||||
self:set_cur_item({ self.entries[1], self.entries[1].files[1] })
|
||||
return self.cur_item[2]
|
||||
end
|
||||
|
||||
if self:num_items() > 1 then
|
||||
local entry_idx = utils.vec_indexof(self.entries, entry)
|
||||
local file_idx = utils.vec_indexof(entry.files, file)
|
||||
|
||||
if entry_idx ~= -1 and file_idx ~= -1 then
|
||||
local next_entry, next_file = self:_get_entry_by_file_offset(entry_idx, file_idx, offset)
|
||||
self:set_cur_item({ next_entry, next_file })
|
||||
|
||||
if next_entry ~= entry then
|
||||
self:set_entry_fold(entry, false)
|
||||
end
|
||||
|
||||
return self.cur_item[2]
|
||||
end
|
||||
else
|
||||
self:set_cur_item({ self.entries[1], self.entries[1].files[1] })
|
||||
return self.cur_item[2]
|
||||
end
|
||||
end
|
||||
|
||||
function FileHistoryPanel:prev_file()
|
||||
return self:set_file_by_offset(-vim.v.count1)
|
||||
end
|
||||
|
||||
function FileHistoryPanel:next_file()
|
||||
return self:set_file_by_offset(vim.v.count1)
|
||||
end
|
||||
|
||||
---@param item LogEntry|FileEntry
|
||||
function FileHistoryPanel:highlight_item(item)
|
||||
if not (self:is_open() and self:buf_loaded()) then return end
|
||||
|
||||
if item:instanceof(LogEntry.__get()) then
|
||||
---@cast item LogEntry
|
||||
for _, comp_struct in ipairs(self.components.log.entries) do
|
||||
if comp_struct.comp.context == item then
|
||||
pcall(api.nvim_win_set_cursor, self.winid, { comp_struct.comp.lstart, 0 })
|
||||
end
|
||||
end
|
||||
else
|
||||
---@cast item FileEntry
|
||||
for _, comp_struct in ipairs(self.components.log.entries) do
|
||||
local i = utils.vec_indexof(comp_struct.comp.context.files, item)
|
||||
|
||||
if i ~= -1 then
|
||||
if self.single_file then
|
||||
pcall(api.nvim_win_set_cursor, self.winid, { comp_struct.comp.lstart + 1, 0 })
|
||||
else
|
||||
if comp_struct.comp.context.folded then
|
||||
comp_struct.comp.context.folded = false
|
||||
self:render()
|
||||
self:redraw()
|
||||
end
|
||||
|
||||
pcall(api.nvim_win_set_cursor, self.winid, { comp_struct.comp.lstart + i + 1, 0 })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Needed to update the cursorline highlight when the panel is not focused.
|
||||
utils.update_win(self.winid)
|
||||
end
|
||||
|
||||
function FileHistoryPanel:highlight_prev_item()
|
||||
if not (self:is_open() and self:buf_loaded()) or #self.entries == 0 then return end
|
||||
|
||||
pcall(api.nvim_win_set_cursor, self.winid, {
|
||||
self.constrain_cursor(self.winid, -vim.v.count1),
|
||||
0,
|
||||
})
|
||||
|
||||
utils.update_win(self.winid)
|
||||
end
|
||||
|
||||
function FileHistoryPanel:highlight_next_file()
|
||||
if not (self:is_open() and self:buf_loaded()) or #self.entries == 0 then return end
|
||||
|
||||
pcall(api.nvim_win_set_cursor, self.winid, {
|
||||
self.constrain_cursor(self.winid, vim.v.count1),
|
||||
0,
|
||||
})
|
||||
|
||||
utils.update_win(self.winid)
|
||||
end
|
||||
|
||||
---@param entry LogEntry
|
||||
---@param open boolean
|
||||
function FileHistoryPanel:set_entry_fold(entry, open)
|
||||
if not self.single_file and open == entry.folded then
|
||||
entry.folded = not open
|
||||
self:render()
|
||||
self:redraw()
|
||||
|
||||
if entry.folded then
|
||||
-- Set the cursor at the top of the log entry
|
||||
self.components.log.entries.comp:some(function(comp, _, _)
|
||||
if comp.context == entry then
|
||||
utils.set_cursor(self.winid, comp.lstart + 1)
|
||||
return true
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param entry LogEntry
|
||||
function FileHistoryPanel:toggle_entry_fold(entry)
|
||||
self:set_entry_fold(entry, entry.folded)
|
||||
end
|
||||
|
||||
function FileHistoryPanel:render()
|
||||
perf_render:reset()
|
||||
panel_renderer.file_history_panel(self)
|
||||
perf_render:time()
|
||||
logger:lvl(10):debug(perf_render)
|
||||
end
|
||||
|
||||
---@return LogOptions
|
||||
function FileHistoryPanel:get_log_options()
|
||||
if self.single_file then
|
||||
return self.log_options.single_file
|
||||
else
|
||||
return self.log_options.multi_file
|
||||
end
|
||||
end
|
||||
|
||||
M.FileHistoryPanel = FileHistoryPanel
|
||||
return M
|
||||
@ -0,0 +1,262 @@
|
||||
local async = require("diffview.async")
|
||||
local lazy = require("diffview.lazy")
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
local CommitLogPanel = lazy.access("diffview.ui.panels.commit_log_panel", "CommitLogPanel") ---@type CommitLogPanel|LazyModule
|
||||
local EventName = lazy.access("diffview.events", "EventName") ---@type EventName|LazyModule
|
||||
local FileHistoryPanel = lazy.access("diffview.scene.views.file_history.file_history_panel", "FileHistoryPanel") ---@type FileHistoryPanel|LazyModule
|
||||
local JobStatus = lazy.access("diffview.vcs.utils", "JobStatus") ---@type JobStatus|LazyModule
|
||||
local LogEntry = lazy.access("diffview.vcs.log_entry", "LogEntry") ---@type LogEntry|LazyModule
|
||||
local StandardView = lazy.access("diffview.scene.views.standard.standard_view", "StandardView") ---@type StandardView|LazyModule
|
||||
local config = lazy.require("diffview.config") ---@module "diffview.config"
|
||||
|
||||
local api = vim.api
|
||||
local await = async.await
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class FileHistoryView : StandardView
|
||||
---@operator call:FileHistoryView
|
||||
---@field adapter VCSAdapter
|
||||
---@field panel FileHistoryPanel
|
||||
---@field commit_log_panel CommitLogPanel
|
||||
---@field valid boolean
|
||||
local FileHistoryView = oop.create_class("FileHistoryView", StandardView.__get())
|
||||
|
||||
function FileHistoryView:init(opt)
|
||||
self.valid = false
|
||||
self.adapter = opt.adapter
|
||||
|
||||
self:super({
|
||||
panel = FileHistoryPanel({
|
||||
parent = self,
|
||||
adapter = self.adapter,
|
||||
entries = {},
|
||||
log_options = opt.log_options,
|
||||
}),
|
||||
})
|
||||
|
||||
self.valid = true
|
||||
end
|
||||
|
||||
function FileHistoryView:post_open()
|
||||
self.commit_log_panel = CommitLogPanel(self.adapter, {
|
||||
name = ("diffview://%s/log/%d/%s"):format(self.adapter.ctx.dir, self.tabpage, "commit_log"),
|
||||
})
|
||||
|
||||
self:init_event_listeners()
|
||||
|
||||
vim.schedule(function()
|
||||
self:file_safeguard()
|
||||
|
||||
---@diagnostic disable-next-line: unused-local
|
||||
self.panel:update_entries(function(entries, status)
|
||||
if status < JobStatus.ERROR and not self.panel:cur_file() then
|
||||
local file = self.panel:next_file()
|
||||
if file then
|
||||
self:set_file(file)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
self.ready = true
|
||||
end)
|
||||
end
|
||||
|
||||
---@override
|
||||
function FileHistoryView:close()
|
||||
if not self.closing:check() then
|
||||
self.closing:send()
|
||||
|
||||
for _, entry in ipairs(self.panel.entries or {}) do
|
||||
entry:destroy()
|
||||
end
|
||||
|
||||
self.commit_log_panel:destroy()
|
||||
FileHistoryView.super_class.close(self)
|
||||
end
|
||||
end
|
||||
|
||||
---@return FileEntry?
|
||||
function FileHistoryView:cur_file()
|
||||
return self.panel.cur_item[2]
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param self FileHistoryView
|
||||
---@param file FileEntry
|
||||
FileHistoryView._set_file = async.void(function(self, file)
|
||||
self.panel:render()
|
||||
self.panel:redraw()
|
||||
vim.cmd("redraw")
|
||||
|
||||
self.cur_layout:detach_files()
|
||||
local cur_entry = self.cur_entry
|
||||
self.emitter:emit("file_open_pre", file, cur_entry)
|
||||
self.nulled = false
|
||||
|
||||
await(self:use_entry(file))
|
||||
|
||||
local log_options = self.panel:get_log_options()
|
||||
|
||||
-- For line tracing diffs: create custom folds derived from the diff patch
|
||||
-- hunks. Should not be used with custom `++base` as then we won't know
|
||||
-- where to create the custom folds in the base file.
|
||||
if log_options.L and next(log_options.L) and not log_options.base then
|
||||
local log_entry = self.panel.cur_item[1]
|
||||
local diff = log_entry:get_diff(file.path)
|
||||
|
||||
if diff and not file:has_patch_folds() then
|
||||
file:update_patch_folds(diff)
|
||||
|
||||
for _, win in ipairs(self.cur_layout.windows) do
|
||||
win:use_winopts({ foldmethod = "manual" })
|
||||
win:apply_custom_folds()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self.emitter:emit("file_open_post", file, cur_entry)
|
||||
|
||||
if not self.cur_entry.opened then
|
||||
self.cur_entry.opened = true
|
||||
DiffviewGlobal.emitter:emit("file_open_new", file)
|
||||
end
|
||||
end)
|
||||
|
||||
function FileHistoryView:next_item()
|
||||
self:ensure_layout()
|
||||
|
||||
if self:file_safeguard() then return end
|
||||
|
||||
if self.panel:num_items() > 1 or self.nulled then
|
||||
local cur = self.panel:next_file()
|
||||
|
||||
if cur then
|
||||
self.panel:highlight_item(cur)
|
||||
self.nulled = false
|
||||
self:_set_file(cur)
|
||||
|
||||
return cur
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function FileHistoryView:prev_item()
|
||||
self:ensure_layout()
|
||||
|
||||
if self:file_safeguard() then return end
|
||||
|
||||
if self.panel:num_items() > 1 or self.nulled then
|
||||
local cur = self.panel:prev_file()
|
||||
|
||||
if cur then
|
||||
self.panel:highlight_item(cur)
|
||||
self.nulled = false
|
||||
self:_set_file(cur)
|
||||
|
||||
return cur
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param self FileHistoryView
|
||||
---@param file FileEntry
|
||||
---@param focus? boolean
|
||||
FileHistoryView.set_file = async.void(function(self, file, focus)
|
||||
---@diagnostic disable: invisible
|
||||
self:ensure_layout()
|
||||
|
||||
if self:file_safeguard() or not file then return end
|
||||
|
||||
local entry = self.panel:find_entry(file)
|
||||
|
||||
if entry then
|
||||
self.panel:set_cur_item({ entry, file })
|
||||
self.panel:highlight_item(file)
|
||||
self.nulled = false
|
||||
await(self:_set_file(file))
|
||||
|
||||
if focus then
|
||||
api.nvim_set_current_win(self.cur_layout:get_main_win().id)
|
||||
end
|
||||
end
|
||||
---@diagnostic enable: invisible
|
||||
end)
|
||||
|
||||
|
||||
---Ensures there are files to load, and loads the null buffer otherwise.
|
||||
---@return boolean
|
||||
function FileHistoryView:file_safeguard()
|
||||
if self.panel:num_items() == 0 then
|
||||
local cur = self.panel.cur_item[2]
|
||||
|
||||
if cur then
|
||||
cur.layout:detach_files()
|
||||
end
|
||||
|
||||
self.cur_layout:open_null()
|
||||
self.nulled = true
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function FileHistoryView:on_files_staged(callback)
|
||||
self.emitter:on(EventName.FILES_STAGED, callback)
|
||||
end
|
||||
|
||||
function FileHistoryView:init_event_listeners()
|
||||
local listeners = require("diffview.scene.views.file_history.listeners")(self)
|
||||
for event, callback in pairs(listeners) do
|
||||
self.emitter:on(event, callback)
|
||||
end
|
||||
end
|
||||
|
||||
---Infer the current selected file. If the file panel is focused: return the
|
||||
---file entry under the cursor. Otherwise return the file open in the view.
|
||||
---Returns nil if no file is open in the view, or there is no entry under the
|
||||
---cursor in the file panel.
|
||||
---@return FileEntry?
|
||||
function FileHistoryView:infer_cur_file()
|
||||
if self.panel:is_focused() then
|
||||
local item = self.panel:get_item_at_cursor()
|
||||
|
||||
if LogEntry.__get():ancestorof(item) then
|
||||
---@cast item LogEntry
|
||||
return item.files[1]
|
||||
end
|
||||
|
||||
return item --[[@as FileEntry ]]
|
||||
end
|
||||
|
||||
return self.panel.cur_item[2]
|
||||
end
|
||||
|
||||
---Check whether or not the instantiation was successful.
|
||||
---@return boolean
|
||||
function FileHistoryView:is_valid()
|
||||
return self.valid
|
||||
end
|
||||
|
||||
---@override
|
||||
function FileHistoryView.get_default_layout_name()
|
||||
return config.get_config().view.file_history.layout
|
||||
end
|
||||
|
||||
---@override
|
||||
---@return Layout # (class) The default layout class.
|
||||
function FileHistoryView.get_default_layout()
|
||||
local name = FileHistoryView.get_default_layout_name()
|
||||
|
||||
if name == -1 then
|
||||
return FileHistoryView.get_default_diff2()
|
||||
end
|
||||
|
||||
return config.name_to_layout(name --[[@as string ]])
|
||||
end
|
||||
|
||||
M.FileHistoryView = FileHistoryView
|
||||
return M
|
||||
@ -0,0 +1,198 @@
|
||||
local async = require("diffview.async")
|
||||
local lazy = require("diffview.lazy")
|
||||
|
||||
local DiffView = lazy.access("diffview.scene.views.diff.diff_view", "DiffView") ---@type DiffView|LazyModule
|
||||
local JobStatus = lazy.access("diffview.vcs.utils", "JobStatus") ---@type JobStatus|LazyModule
|
||||
local lib = lazy.require("diffview.lib") ---@module "diffview.lib"
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
local vcs_utils = lazy.require("diffview.vcs.utils") ---@module "diffview.vcs.utils"
|
||||
|
||||
local await = async.await
|
||||
|
||||
---@param view FileHistoryView
|
||||
return function(view)
|
||||
return {
|
||||
tab_enter = function()
|
||||
local file = view.panel.cur_item[2]
|
||||
if file then
|
||||
view:set_file(file)
|
||||
end
|
||||
end,
|
||||
tab_leave = function()
|
||||
local file = view.panel.cur_item[2]
|
||||
|
||||
if file then
|
||||
file.layout:detach_files()
|
||||
end
|
||||
|
||||
for _, entry in ipairs(view.panel.entries) do
|
||||
for _, f in ipairs(entry.files) do
|
||||
f.layout:restore_winopts()
|
||||
end
|
||||
end
|
||||
end,
|
||||
file_open_new = function(_, entry)
|
||||
utils.set_cursor(view.cur_layout:get_main_win().id, 1, 0)
|
||||
view.cur_layout:sync_scroll()
|
||||
end,
|
||||
open_in_diffview = function()
|
||||
local file = view:infer_cur_file()
|
||||
|
||||
if file then
|
||||
local layout = file.layout --[[@as Diff2 ]]
|
||||
|
||||
local new_view = DiffView({
|
||||
adapter = view.adapter,
|
||||
rev_arg = view.adapter:rev_to_pretty_string(layout.a.file.rev, layout.b.file.rev),
|
||||
left = layout.a.file.rev,
|
||||
right = layout.b.file.rev,
|
||||
options = { selected_file = file.absolute_path },
|
||||
})
|
||||
|
||||
lib.add_view(new_view)
|
||||
new_view:open()
|
||||
end
|
||||
end,
|
||||
select_next_entry = function()
|
||||
view:next_item()
|
||||
end,
|
||||
select_prev_entry = function()
|
||||
view:prev_item()
|
||||
end,
|
||||
next_entry = function()
|
||||
view.panel:highlight_next_file()
|
||||
end,
|
||||
prev_entry = function()
|
||||
view.panel:highlight_prev_item()
|
||||
end,
|
||||
select_entry = function()
|
||||
if view.panel:is_focused() then
|
||||
local item = view.panel:get_item_at_cursor()
|
||||
if item then
|
||||
if item.files then
|
||||
if view.panel.single_file then
|
||||
view:set_file(item.files[1], false)
|
||||
else
|
||||
view.panel:toggle_entry_fold(item --[[@as LogEntry ]])
|
||||
end
|
||||
else
|
||||
view:set_file(item, false)
|
||||
end
|
||||
end
|
||||
elseif view.panel.option_panel:is_focused() then
|
||||
local option = view.panel.option_panel:get_item_at_cursor()
|
||||
if option then
|
||||
view.panel.option_panel.emitter:emit("set_option", option.key)
|
||||
end
|
||||
end
|
||||
end,
|
||||
focus_entry = function()
|
||||
if view.panel:is_focused() then
|
||||
local item = view.panel:get_item_at_cursor()
|
||||
if item then
|
||||
if item.files then
|
||||
if view.panel.single_file then
|
||||
view:set_file(item.files[1], true)
|
||||
else
|
||||
view.panel:toggle_entry_fold(item --[[@as LogEntry ]])
|
||||
end
|
||||
else
|
||||
view:set_file(item, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end,
|
||||
open_commit_log = function()
|
||||
local file = view:infer_cur_file()
|
||||
if file then
|
||||
local entry = view.panel:find_entry(file)
|
||||
if entry then
|
||||
view.commit_log_panel:update(view.adapter.Rev.to_range(entry.commit.hash))
|
||||
end
|
||||
end
|
||||
end,
|
||||
focus_files = function()
|
||||
view.panel:focus()
|
||||
end,
|
||||
toggle_files = function()
|
||||
view.panel:toggle(true)
|
||||
end,
|
||||
refresh_files = function()
|
||||
view.panel:update_entries(function(_, status)
|
||||
if status >= JobStatus.ERROR then
|
||||
return
|
||||
end
|
||||
if not view:cur_file() then
|
||||
view:next_item()
|
||||
end
|
||||
end)
|
||||
end,
|
||||
open_all_folds = function()
|
||||
if view.panel:is_focused() and not view.panel.single_file then
|
||||
for _, entry in ipairs(view.panel.entries) do
|
||||
entry.folded = false
|
||||
end
|
||||
view.panel:render()
|
||||
view.panel:redraw()
|
||||
end
|
||||
end,
|
||||
close_all_folds = function()
|
||||
if view.panel:is_focused() and not view.panel.single_file then
|
||||
for _, entry in ipairs(view.panel.entries) do
|
||||
entry.folded = true
|
||||
end
|
||||
view.panel:render()
|
||||
view.panel:redraw()
|
||||
end
|
||||
end,
|
||||
open_fold = function()
|
||||
if view.panel.single_file or not view.panel:is_focused() then return end
|
||||
local entry = view.panel:get_log_entry_at_cursor()
|
||||
if entry then view.panel:set_entry_fold(entry, true) end
|
||||
end,
|
||||
close_fold = function()
|
||||
if view.panel.single_file or not view.panel:is_focused() then return end
|
||||
local entry = view.panel:get_log_entry_at_cursor()
|
||||
if entry then view.panel:set_entry_fold(entry, false) end
|
||||
end,
|
||||
toggle_fold = function()
|
||||
if view.panel.single_file or not view.panel:is_focused() then return end
|
||||
local entry = view.panel:get_log_entry_at_cursor()
|
||||
if entry then view.panel:toggle_entry_fold(entry) end
|
||||
end,
|
||||
close = function()
|
||||
if view.panel.option_panel:is_focused() then
|
||||
view.panel.option_panel:close()
|
||||
elseif view.panel:is_focused() then
|
||||
view.panel:close()
|
||||
elseif view:is_cur_tabpage() then
|
||||
view:close()
|
||||
end
|
||||
end,
|
||||
options = function()
|
||||
view.panel.option_panel:focus()
|
||||
end,
|
||||
copy_hash = function()
|
||||
if view.panel:is_focused() then
|
||||
local item = view.panel:get_item_at_cursor()
|
||||
if item then
|
||||
vim.fn.setreg("+", item.commit.hash)
|
||||
utils.info(string.format("Copied '%s' to the clipboard.", item.commit.hash))
|
||||
end
|
||||
end
|
||||
end,
|
||||
restore_entry = async.void(function()
|
||||
local item = view:infer_cur_file()
|
||||
if not item then return end
|
||||
|
||||
local bufid = utils.find_file_buffer(item.path)
|
||||
|
||||
if bufid and vim.bo[bufid].modified then
|
||||
utils.err("The file is open with unsaved changes! Aborting file restoration.")
|
||||
return
|
||||
end
|
||||
|
||||
await(vcs_utils.restore_file(view.adapter, item.path, item.kind, item.commit.hash))
|
||||
end),
|
||||
}
|
||||
end
|
||||
@ -0,0 +1,232 @@
|
||||
local lazy = require("diffview.lazy")
|
||||
|
||||
local EventEmitter = lazy.access("diffview.events", "EventEmitter") ---@type EventEmitter|LazyModule
|
||||
local JobStatus = lazy.access("diffview.vcs.utils", "JobStatus") ---@type JobStatus|LazyModule
|
||||
local Panel = lazy.access("diffview.ui.panel", "Panel") ---@type Panel|LazyModule
|
||||
local arg_parser = lazy.require("diffview.arg_parser") ---@module "diffview.arg_parser"
|
||||
local config = lazy.require("diffview.config") ---@module "diffview.config"
|
||||
local oop = lazy.require("diffview.oop") ---@module "diffview.oop"
|
||||
local panel_renderer = lazy.require("diffview.scene.views.file_history.render") ---@module "diffview.scene.views.file_history.render"
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
|
||||
local api = vim.api
|
||||
local M = {}
|
||||
|
||||
---@class FHOptionPanel : Panel
|
||||
---@field parent FileHistoryPanel
|
||||
---@field emitter EventEmitter
|
||||
---@field render_data RenderData
|
||||
---@field option_state LogOptions
|
||||
---@field components CompStruct
|
||||
local FHOptionPanel = oop.create_class("FHOptionPanel", Panel.__get())
|
||||
|
||||
FHOptionPanel.winopts = vim.tbl_extend("force", Panel.winopts, {
|
||||
cursorline = true,
|
||||
winhl = {
|
||||
"EndOfBuffer:DiffviewEndOfBuffer",
|
||||
"Normal:DiffviewNormal",
|
||||
"CursorLine:DiffviewCursorLine",
|
||||
"WinSeparator:DiffviewWinSeparator",
|
||||
"SignColumn:DiffviewNormal",
|
||||
"StatusLine:DiffviewStatusLine",
|
||||
"StatusLineNC:DiffviewStatuslineNC",
|
||||
},
|
||||
})
|
||||
|
||||
FHOptionPanel.bufopts = {
|
||||
swapfile = false,
|
||||
buftype = "nofile",
|
||||
modifiable = false,
|
||||
filetype = "DiffviewFileHistory",
|
||||
bufhidden = "hide",
|
||||
}
|
||||
|
||||
---FHOptionPanel constructor.
|
||||
---@param parent FileHistoryPanel
|
||||
function FHOptionPanel:init(parent)
|
||||
self:super({
|
||||
---@type PanelSplitSpec
|
||||
config = {
|
||||
position = "bottom",
|
||||
height = #parent.adapter.flags.switches + #parent.adapter.flags.options + 4,
|
||||
},
|
||||
bufname = "DiffviewFHOptionPanel",
|
||||
})
|
||||
self.parent = parent
|
||||
self.emitter = EventEmitter()
|
||||
self.flags = parent.adapter.flags
|
||||
|
||||
---@param option_name string
|
||||
self.emitter:on("set_option", function(_, option_name)
|
||||
local log_options = self.parent:get_log_options()
|
||||
local cur_value = log_options[option_name]
|
||||
|
||||
if self.flags.switches[option_name] then
|
||||
self:_set_option(option_name, not cur_value)
|
||||
self:render()
|
||||
self:redraw()
|
||||
|
||||
elseif self.flags.options[option_name] then
|
||||
local o = self.flags.options[option_name]
|
||||
|
||||
if o.select then
|
||||
vim.ui.select(o.select, {
|
||||
prompt = o:render_prompt(),
|
||||
format_item = function(item)
|
||||
return item == "" and "<unset>" or item
|
||||
end,
|
||||
}, function(choice)
|
||||
if choice then
|
||||
self:_set_option(option_name, choice)
|
||||
end
|
||||
|
||||
self:render()
|
||||
self:redraw()
|
||||
end)
|
||||
|
||||
else
|
||||
local completion = type(o.completion) == "function" and o.completion(self) or o.completion
|
||||
|
||||
utils.input(o:render_prompt(), {
|
||||
default = o:render_default(cur_value),
|
||||
completion = type(completion) == "function" and function(_, cmd_line, cur_pos)
|
||||
---@cast completion fun(ctx: CmdLineContext): string[]
|
||||
local ctx = arg_parser.scan(cmd_line, { cur_pos = cur_pos })
|
||||
return arg_parser.process_candidates(completion(ctx), ctx, true)
|
||||
end or completion,
|
||||
callback = function(response)
|
||||
if response ~= "__INPUT_CANCELLED__" then
|
||||
local values = response == nil and { "" } or arg_parser.scan(response).args
|
||||
|
||||
if o.transform then
|
||||
values = o:transform(values)
|
||||
end
|
||||
|
||||
if not o.expect_list then
|
||||
---@cast values string
|
||||
values = values[1]
|
||||
end
|
||||
|
||||
self:_set_option(option_name, values)
|
||||
end
|
||||
|
||||
self:render()
|
||||
self:redraw()
|
||||
end,
|
||||
})
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
self:on_autocmd("BufNew", {
|
||||
callback = function()
|
||||
self:setup_buffer()
|
||||
end,
|
||||
})
|
||||
|
||||
self:on_autocmd("WinClosed", {
|
||||
callback = function()
|
||||
if not vim.deep_equal(self.option_state, self.parent:get_log_options()) then
|
||||
vim.schedule(function ()
|
||||
self.option_state = nil
|
||||
self.winid = nil
|
||||
self.parent:update_entries(function(_, status)
|
||||
if status >= JobStatus.ERROR then
|
||||
return
|
||||
end
|
||||
if not self.parent:cur_file() then
|
||||
self.parent.parent:next_item()
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
---@private
|
||||
function FHOptionPanel:_set_option(name, value)
|
||||
self.parent.log_options.single_file[name] = value
|
||||
self.parent.log_options.multi_file[name] = value
|
||||
end
|
||||
|
||||
---@override
|
||||
function FHOptionPanel:open()
|
||||
FHOptionPanel.super_class.open(self)
|
||||
self.option_state = utils.tbl_deep_clone(self.parent:get_log_options())
|
||||
|
||||
api.nvim_win_call(self.winid, function()
|
||||
vim.cmd("norm! zb")
|
||||
end)
|
||||
end
|
||||
|
||||
function FHOptionPanel:setup_buffer()
|
||||
local conf = config.get_config()
|
||||
local default_opt = { silent = true, buffer = self.bufid }
|
||||
for _, mapping in ipairs(conf.keymaps.option_panel) do
|
||||
local opt = vim.tbl_extend("force", default_opt, mapping[4] or {}, { buffer = self.bufid })
|
||||
vim.keymap.set(mapping[1], mapping[2], mapping[3], opt)
|
||||
end
|
||||
|
||||
for _, group in pairs(self.flags) do
|
||||
---@cast group FlagOption[]
|
||||
for option_name, v in pairs(group) do
|
||||
vim.keymap.set(
|
||||
"n",
|
||||
v.keymap,
|
||||
function()
|
||||
self.emitter:emit("set_option", option_name)
|
||||
end,
|
||||
{ silent = true, buffer = self.bufid }
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function FHOptionPanel:update_components()
|
||||
local switch_schema = {}
|
||||
local option_schema = {}
|
||||
for _, option in ipairs(self.flags.switches) do
|
||||
table.insert(switch_schema, { name = "switch", context = { option = option, }, })
|
||||
end
|
||||
for _, option in ipairs(self.flags.options) do
|
||||
table.insert(option_schema, { name = "option", context = { option = option }, })
|
||||
end
|
||||
|
||||
---@type CompStruct
|
||||
self.components = self.render_data:create_component({
|
||||
{
|
||||
name = "switches",
|
||||
{ name = "title" },
|
||||
{ name = "items", unpack(switch_schema) },
|
||||
},
|
||||
{
|
||||
name = "options",
|
||||
{ name = "title" },
|
||||
{ name = "items", unpack(option_schema) },
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
---Get the file entry under the cursor.
|
||||
---@return FlagOption?
|
||||
function FHOptionPanel:get_item_at_cursor()
|
||||
if not (self:is_open() and self:buf_loaded()) then
|
||||
return
|
||||
end
|
||||
|
||||
local cursor = api.nvim_win_get_cursor(self.winid)
|
||||
local line = cursor[1]
|
||||
|
||||
local comp = self.components.comp:get_comp_on_line(line)
|
||||
if comp and (comp.name == "switch" or comp.name == "option") then
|
||||
return comp.context.option
|
||||
end
|
||||
end
|
||||
|
||||
function FHOptionPanel:render()
|
||||
panel_renderer.fh_option_panel(self)
|
||||
end
|
||||
|
||||
M.FHOptionPanel = FHOptionPanel
|
||||
return M
|
||||
@ -0,0 +1,318 @@
|
||||
local PerfTimer = require("diffview.perf").PerfTimer
|
||||
local config = require("diffview.config")
|
||||
local hl = require("diffview.hl")
|
||||
local utils = require("diffview.utils")
|
||||
|
||||
local fmt = string.format
|
||||
local logger = DiffviewGlobal.logger
|
||||
local perf = PerfTimer("[FileHistoryPanel] Render internal")
|
||||
local pl = utils.path
|
||||
|
||||
local cache = setmetatable({}, { __mode = "k" })
|
||||
|
||||
---@param comp RenderComponent
|
||||
---@param files FileEntry[]
|
||||
local function render_files(comp, files)
|
||||
for i, file in ipairs(files) do
|
||||
comp:add_text(i == #files and "└ " or "│ ", "DiffviewNonText")
|
||||
|
||||
if file:is_null_entry() then
|
||||
comp:add_text(
|
||||
"No diff",
|
||||
file.active and "DiffviewFilePanelSelected" or "DiffviewFilePanelFileName"
|
||||
)
|
||||
else
|
||||
if file.status then
|
||||
comp:add_text(file.status .. " ", hl.get_git_hl(file.status))
|
||||
else
|
||||
comp:add_text("-" .. " ", "DiffviewNonText")
|
||||
end
|
||||
|
||||
local icon, icon_hl = hl.get_file_icon(file.basename, file.extension)
|
||||
comp:add_text(icon, icon_hl)
|
||||
|
||||
if #file.parent_path > 0 then
|
||||
comp:add_text(file.parent_path .. "/", "DiffviewFilePanelPath")
|
||||
end
|
||||
|
||||
comp:add_text(file.basename, file.active and "DiffviewFilePanelSelected" or "DiffviewFilePanelFileName")
|
||||
|
||||
if file.stats then
|
||||
comp:add_text(" " .. file.stats.additions, "DiffviewFilePanelInsertions")
|
||||
comp:add_text(", ")
|
||||
comp:add_text(tostring(file.stats.deletions), "DiffviewFilePanelDeletions")
|
||||
end
|
||||
end
|
||||
|
||||
comp:ln()
|
||||
end
|
||||
|
||||
perf:lap("files")
|
||||
end
|
||||
|
||||
---@param panel FileHistoryPanel
|
||||
---@param parent CompStruct RenderComponent struct
|
||||
---@param entries LogEntry[]
|
||||
---@param updating boolean
|
||||
local function render_entries(panel, parent, entries, updating)
|
||||
local c = config.get_config()
|
||||
local max_num_files = -1
|
||||
local max_len_stats = -1
|
||||
|
||||
for _, entry in ipairs(entries) do
|
||||
if #entry.files > max_num_files then
|
||||
max_num_files = #entry.files
|
||||
end
|
||||
|
||||
if entry.stats then
|
||||
local adds = tostring(entry.stats.additions)
|
||||
local dels = tostring(entry.stats.deletions)
|
||||
local l = 7
|
||||
local w = l - (#adds + #dels)
|
||||
if w < 1 then
|
||||
l = (#adds + #dels) - ((#adds + #dels) % 2) + 2
|
||||
end
|
||||
max_len_stats = l > max_len_stats and l or max_len_stats
|
||||
end
|
||||
end
|
||||
|
||||
for i, entry in ipairs(entries) do
|
||||
if i > #parent or (updating and i > 128) then
|
||||
break
|
||||
end
|
||||
|
||||
local entry_struct = parent[i]
|
||||
local comp = entry_struct.commit.comp
|
||||
|
||||
if not entry.single_file then
|
||||
comp:add_text((entry.folded and c.signs.fold_closed or c.signs.fold_open) .. " ", "CursorLineNr")
|
||||
end
|
||||
|
||||
if entry.status then
|
||||
comp:add_text(entry.status, hl.get_git_hl(entry.status))
|
||||
else
|
||||
comp:add_text("-", "DiffviewNonText")
|
||||
end
|
||||
|
||||
if not entry.single_file then
|
||||
local s_num_files = tostring(max_num_files)
|
||||
|
||||
if entry.nulled then
|
||||
comp:add_text(utils.str_center_pad("empty", #s_num_files + 7), "DiffviewFilePanelCounter")
|
||||
else
|
||||
comp:add_text(
|
||||
fmt(
|
||||
" %s file%s",
|
||||
utils.str_left_pad(tostring(#entry.files), #s_num_files),
|
||||
#entry.files > 1 and "s" or " "
|
||||
),
|
||||
"DiffviewFilePanelCounter"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
if max_len_stats ~= -1 then
|
||||
local adds = { "-", "DiffviewNonText" }
|
||||
local dels = { "-", "DiffviewNonText" }
|
||||
|
||||
if entry.stats and entry.stats.additions then
|
||||
adds = { tostring(entry.stats.additions), "DiffviewFilePanelInsertions" }
|
||||
end
|
||||
|
||||
if entry.stats and entry.stats.deletions then
|
||||
dels = { tostring(entry.stats.deletions), "DiffviewFilePanelDeletions" }
|
||||
end
|
||||
|
||||
comp:add_text(" | ", "DiffviewNonText")
|
||||
comp:add_text(unpack(adds))
|
||||
comp:add_text(string.rep(" ", max_len_stats - (#adds[1] + #dels[1])))
|
||||
comp:add_text(unpack(dels))
|
||||
comp:add_text(" |", "DiffviewNonText")
|
||||
end
|
||||
|
||||
if entry.commit.hash then
|
||||
comp:add_text(" " .. entry.commit.hash:sub(1, 8), "DiffviewHash")
|
||||
end
|
||||
|
||||
if (entry.commit --[[@as GitCommit ]]).reflog_selector then
|
||||
comp:add_text((" %s"):format((entry.commit --[[@as GitCommit ]]).reflog_selector), "DiffviewReflogSelector")
|
||||
end
|
||||
|
||||
if entry.commit.ref_names then
|
||||
comp:add_text((" (%s)"):format(entry.commit.ref_names), "DiffviewReference")
|
||||
end
|
||||
|
||||
local subject = utils.str_trunc(entry.commit.subject, 72)
|
||||
|
||||
if subject == "" then
|
||||
subject = "[empty message]"
|
||||
end
|
||||
|
||||
comp:add_text(
|
||||
" " .. subject,
|
||||
panel.cur_item[1] == entry and "DiffviewFilePanelSelected" or "DiffviewFilePanelFileName"
|
||||
)
|
||||
|
||||
if entry.commit then
|
||||
-- 3 months
|
||||
local date = (
|
||||
os.difftime(os.time(), entry.commit.time) > 60 * 60 * 24 * 30 * 3
|
||||
and entry.commit.iso_date
|
||||
or entry.commit.rel_date
|
||||
)
|
||||
comp:add_text(" " .. entry.commit.author .. ", " .. date, "DiffviewFilePanelPath")
|
||||
end
|
||||
|
||||
comp:ln()
|
||||
perf:lap("entry " .. entry.commit.hash:sub(1, 7))
|
||||
|
||||
if not entry.single_file and not entry.folded then
|
||||
render_files(entry_struct.files.comp, entry.files)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param panel FileHistoryPanel
|
||||
local function prepare_panel_cache(panel)
|
||||
local c = {}
|
||||
cache[panel] = c
|
||||
c.root_path = panel.state.form == "column"
|
||||
and pl:truncate(
|
||||
pl:vim_fnamemodify(panel.adapter.ctx.toplevel, ":~"),
|
||||
panel:infer_width() - 6
|
||||
)
|
||||
or pl:vim_fnamemodify(panel.adapter.ctx.toplevel, ":~")
|
||||
c.args = table.concat(panel.log_options.single_file.path_args, " ")
|
||||
end
|
||||
|
||||
return {
|
||||
---@param panel FileHistoryPanel
|
||||
file_history_panel = function(panel)
|
||||
if not panel.render_data then
|
||||
return
|
||||
end
|
||||
|
||||
perf:reset()
|
||||
panel.render_data:clear()
|
||||
|
||||
if not cache[panel] then
|
||||
prepare_panel_cache(panel)
|
||||
end
|
||||
|
||||
local conf = config.get_config()
|
||||
local comp = panel.components.header.comp
|
||||
local log_options = panel:get_log_options()
|
||||
local cached = cache[panel]
|
||||
|
||||
-- root path
|
||||
comp:add_text(cached.root_path, "DiffviewFilePanelRootPath")
|
||||
comp:ln()
|
||||
|
||||
if panel.single_file then
|
||||
if #panel.entries > 0 then
|
||||
local file = panel.entries[1].files[1]
|
||||
|
||||
-- file path
|
||||
local icon, icon_hl = hl.get_file_icon(file.basename, file.extension)
|
||||
comp:add_text(icon, icon_hl)
|
||||
|
||||
if #file.parent_path > 0 then
|
||||
comp:add_text(file.parent_path .. "/", "DiffviewFilePanelPath")
|
||||
end
|
||||
|
||||
comp:add_text(file.basename, "DiffviewFilePanelFileName")
|
||||
comp:ln()
|
||||
end
|
||||
elseif #cached.args > 0 then
|
||||
comp:add_text("Showing history for: ", "DiffviewFilePanelPath")
|
||||
comp:add_text(cached.args, "DiffviewFilePanelFileName")
|
||||
comp:ln()
|
||||
end
|
||||
|
||||
if log_options.rev_range and log_options.rev_range ~= "" then
|
||||
comp:add_text("Revision range: ", "DiffviewFilePanelPath")
|
||||
comp:add_text(log_options.rev_range, "DiffviewFilePanelFileName")
|
||||
comp:ln()
|
||||
end
|
||||
|
||||
if panel.option_mapping then
|
||||
comp:add_text("Options: ", "DiffviewFilePanelPath")
|
||||
comp:add_text(panel.option_mapping, "DiffviewFilePanelCounter")
|
||||
comp:ln()
|
||||
end
|
||||
|
||||
if conf.show_help_hints and panel.help_mapping then
|
||||
comp:add_text("Help: ", "DiffviewFilePanelPath")
|
||||
comp:add_text(panel.help_mapping, "DiffviewFilePanelCounter")
|
||||
comp:ln()
|
||||
end
|
||||
|
||||
-- title
|
||||
comp = panel.components.log.title.comp
|
||||
comp:add_line()
|
||||
comp:add_text("File History ", "DiffviewFilePanelTitle")
|
||||
comp:add_text("(" .. #panel.entries .. ")", "DiffviewFilePanelCounter")
|
||||
|
||||
if panel.updating then
|
||||
comp:add_text(" (Updating...)", "DiffviewDim1")
|
||||
end
|
||||
|
||||
comp:ln()
|
||||
perf:lap("header")
|
||||
|
||||
if #panel.entries > 0 then
|
||||
render_entries(panel, panel.components.log.entries, panel.entries, panel.updating)
|
||||
end
|
||||
|
||||
perf:time()
|
||||
logger:lvl(10):debug(perf)
|
||||
end,
|
||||
|
||||
---@param panel FHOptionPanel
|
||||
fh_option_panel = function(panel)
|
||||
if not panel.render_data then
|
||||
return
|
||||
end
|
||||
|
||||
panel.render_data:clear()
|
||||
|
||||
local comp = panel.components.switches.title.comp
|
||||
local log_options = panel.parent:get_log_options()
|
||||
|
||||
comp:add_line("Switches", "DiffviewFilePanelTitle")
|
||||
|
||||
for _, item in ipairs(panel.components.switches.items) do
|
||||
comp = item.comp
|
||||
local option = comp.context.option --[[@as FlagOption ]]
|
||||
local enabled = log_options[option.key] --[[@as boolean ]]
|
||||
|
||||
comp:add_text(" " .. option.keymap .. " ", "DiffviewSecondary")
|
||||
comp:add_text(option.desc .. " (", "DiffviewFilePanelFileName")
|
||||
comp:add_text(option.flag_name, enabled and "DiffviewFilePanelCounter" or "DiffviewDim1")
|
||||
comp:add_text(")", "DiffviewFilePanelFileName")
|
||||
comp:ln()
|
||||
end
|
||||
|
||||
comp = panel.components.options.title.comp
|
||||
comp:add_line()
|
||||
comp:add_line("Options", "DiffviewFilePanelTitle")
|
||||
|
||||
for _, item in ipairs(panel.components.options.items) do
|
||||
comp = item.comp
|
||||
local option = comp.context.option --[[@as FlagOption ]]
|
||||
local value = log_options[option.key] or ""
|
||||
|
||||
comp:add_text(" " .. option.keymap .. " ", "DiffviewSecondary")
|
||||
comp:add_text(option.desc .. " (", "DiffviewFilePanelFileName")
|
||||
|
||||
local empty, display_value = option:render_display(value)
|
||||
comp:add_text(display_value, not empty and "DiffviewFilePanelCounter" or "DiffviewDim1")
|
||||
|
||||
comp:add_text(")", "DiffviewFilePanelFileName")
|
||||
comp:ln()
|
||||
end
|
||||
end,
|
||||
clear_cache = function(panel)
|
||||
cache[panel] = nil
|
||||
end,
|
||||
}
|
||||
@ -0,0 +1,182 @@
|
||||
local async = require("diffview.async")
|
||||
local lazy = require("diffview.lazy")
|
||||
|
||||
local Diff1 = lazy.access("diffview.scene.layouts.diff_1", "Diff1") ---@type Diff1|LazyModule
|
||||
local Diff2 = lazy.access("diffview.scene.layouts.diff_2", "Diff2") ---@type Diff2|LazyModule
|
||||
local Diff3 = lazy.access("diffview.scene.layouts.diff_3", "Diff3") ---@type Diff3|LazyModule
|
||||
local Diff4 = lazy.access("diffview.scene.layouts.diff_4", "Diff4") ---@type Diff4|LazyModule
|
||||
local Panel = lazy.access("diffview.ui.panel", "Panel") ---@type Panel|LazyModule
|
||||
local View = lazy.access("diffview.scene.view", "View") ---@type View|LazyModule
|
||||
local config = lazy.require("diffview.config") ---@module "diffview.config"
|
||||
local oop = lazy.require("diffview.oop") ---@module "diffview.oop"
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
|
||||
local api = vim.api
|
||||
local await = async.await
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class StandardView : View
|
||||
---@field panel Panel
|
||||
---@field winopts table
|
||||
---@field nulled boolean
|
||||
---@field cur_layout Layout
|
||||
---@field cur_entry FileEntry
|
||||
---@field layouts table<Layout, Layout>
|
||||
local StandardView = oop.create_class("StandardView", View.__get())
|
||||
|
||||
---StandardView constructor
|
||||
function StandardView:init(opt)
|
||||
opt = opt or {}
|
||||
self:super(opt)
|
||||
self.nulled = utils.sate(opt.nulled, false)
|
||||
self.panel = opt.panel or Panel()
|
||||
self.layouts = opt.layouts or {}
|
||||
self.winopts = opt.winopts or {
|
||||
diff1 = { a = {} },
|
||||
diff2 = { a = {}, b = {} },
|
||||
diff3 = { a = {}, b = {}, c = {} },
|
||||
diff4 = { a = {}, b = {}, c = {}, d = {} },
|
||||
}
|
||||
|
||||
self.emitter:on("post_layout", utils.bind(self.post_layout, self))
|
||||
end
|
||||
|
||||
---@override
|
||||
function StandardView:close()
|
||||
self.panel:destroy()
|
||||
View.close(self)
|
||||
end
|
||||
|
||||
---@override
|
||||
function StandardView:init_layout()
|
||||
local first_init = not vim.t[self.tabpage].diffview_view_initialized
|
||||
local curwin = api.nvim_get_current_win()
|
||||
|
||||
self:use_layout(StandardView.get_temp_layout())
|
||||
self.cur_layout:create()
|
||||
vim.t[self.tabpage].diffview_view_initialized = true
|
||||
|
||||
if first_init then
|
||||
api.nvim_win_close(curwin, false)
|
||||
end
|
||||
|
||||
self.panel:focus()
|
||||
self.emitter:emit("post_layout")
|
||||
end
|
||||
|
||||
function StandardView:post_layout()
|
||||
if config.get_config().enhanced_diff_hl then
|
||||
self.winopts.diff2.a.winhl = {
|
||||
"DiffAdd:DiffviewDiffAddAsDelete",
|
||||
"DiffDelete:DiffviewDiffDeleteDim",
|
||||
"DiffChange:DiffviewDiffChange",
|
||||
"DiffText:DiffviewDiffText",
|
||||
}
|
||||
self.winopts.diff2.b.winhl = {
|
||||
"DiffDelete:DiffviewDiffDeleteDim",
|
||||
"DiffAdd:DiffviewDiffAdd",
|
||||
"DiffChange:DiffviewDiffChange",
|
||||
"DiffText:DiffviewDiffText",
|
||||
}
|
||||
end
|
||||
|
||||
DiffviewGlobal.emitter:emit("view_post_layout", self)
|
||||
end
|
||||
|
||||
---@override
|
||||
---Ensure both left and right windows exist in the view's tabpage.
|
||||
function StandardView:ensure_layout()
|
||||
if self.cur_layout then
|
||||
self.cur_layout:ensure()
|
||||
else
|
||||
self:init_layout()
|
||||
end
|
||||
end
|
||||
|
||||
---@param layout Layout
|
||||
function StandardView:use_layout(layout)
|
||||
self.cur_layout = layout:clone()
|
||||
self.layouts[layout.class] = self.cur_layout
|
||||
|
||||
self.cur_layout.pivot_producer = function()
|
||||
local was_open = self.panel:is_open()
|
||||
local was_only_win = was_open and #utils.tabpage_list_normal_wins(self.tabpage) == 1
|
||||
self.panel:close()
|
||||
|
||||
-- If the panel was the only window before closing, then a temp window was
|
||||
-- already created by `Panel:close()`.
|
||||
if not was_only_win then
|
||||
vim.cmd("1windo aboveleft vsp")
|
||||
end
|
||||
|
||||
local pivot = api.nvim_get_current_win()
|
||||
|
||||
if was_open then
|
||||
self.panel:open()
|
||||
end
|
||||
|
||||
return pivot
|
||||
end
|
||||
end
|
||||
|
||||
---@param self StandardView
|
||||
---@param entry FileEntry
|
||||
StandardView.use_entry = async.void(function(self, entry)
|
||||
local layout_key
|
||||
|
||||
if entry.layout:instanceof(Diff1.__get()) then
|
||||
layout_key = "diff1"
|
||||
elseif entry.layout:instanceof(Diff2.__get()) then
|
||||
layout_key = "diff2"
|
||||
elseif entry.layout:instanceof(Diff3.__get()) then
|
||||
layout_key = "diff3"
|
||||
elseif entry.layout:instanceof(Diff4.__get()) then
|
||||
layout_key = "diff4"
|
||||
end
|
||||
|
||||
for _, sym in ipairs({ "a", "b", "c", "d" }) do
|
||||
if entry.layout[sym] then
|
||||
entry.layout[sym].file.winopts = vim.tbl_extend(
|
||||
"force",
|
||||
entry.layout[sym].file.winopts,
|
||||
self.winopts[layout_key][sym] or {}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
local old_layout = self.cur_layout
|
||||
self.cur_entry = entry
|
||||
|
||||
if entry.layout.class == self.cur_layout.class then
|
||||
self.cur_layout.emitter = entry.layout.emitter
|
||||
await(self.cur_layout:use_entry(entry))
|
||||
else
|
||||
if self.layouts[entry.layout.class] then
|
||||
self.cur_layout = self.layouts[entry.layout.class]
|
||||
self.cur_layout.emitter = entry.layout.emitter
|
||||
else
|
||||
self:use_layout(entry.layout)
|
||||
self.cur_layout.emitter = entry.layout.emitter
|
||||
end
|
||||
|
||||
await(self.cur_layout:use_entry(entry))
|
||||
local future = self.cur_layout:create()
|
||||
old_layout:destroy()
|
||||
|
||||
-- Wait for files to be created + opened
|
||||
await(future)
|
||||
|
||||
if not vim.o.equalalways then
|
||||
vim.cmd("wincmd =")
|
||||
end
|
||||
|
||||
if self.cur_layout:is_focused() then
|
||||
self.cur_layout:get_main_win():focus()
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
M.StandardView = StandardView
|
||||
|
||||
return M
|
||||
@ -0,0 +1,340 @@
|
||||
local async = require("diffview.async")
|
||||
local lazy = require("diffview.lazy")
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
local EventEmitter = lazy.access("diffview.events", "EventEmitter") ---@type EventEmitter|LazyModule
|
||||
local File = lazy.access("diffview.vcs.file", "File") ---@type vcs.File|LazyModule
|
||||
local FileHistoryView = lazy.access("diffview.scene.views.file_history.file_history_view", "FileHistoryView") ---@type FileHistoryView|LazyModule
|
||||
local RevType = lazy.access("diffview.vcs.rev", "RevType") ---@type RevType|LazyModule
|
||||
local config = lazy.require("diffview.config") ---@module "diffview.config"
|
||||
local lib = lazy.require("diffview.lib") ---@module "diffview.lib"
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
|
||||
local api = vim.api
|
||||
local await, pawait = async.await, async.pawait
|
||||
local fmt = string.format
|
||||
local logger = DiffviewGlobal.logger
|
||||
|
||||
local M = {}
|
||||
|
||||
local HAS_NVIM_0_8 = vim.fn.has("nvim-0.8") == 1
|
||||
|
||||
---@class Window : diffview.Object
|
||||
---@field id integer
|
||||
---@field file vcs.File
|
||||
---@field parent Layout
|
||||
---@field emitter EventEmitter
|
||||
local Window = oop.create_class("Window")
|
||||
|
||||
Window.winopt_store = {}
|
||||
|
||||
---@class Window.init.opt
|
||||
---@field id integer
|
||||
---@field file vcs.File
|
||||
---@field parent Layout
|
||||
|
||||
---@param opt Window.init.opt
|
||||
function Window:init(opt)
|
||||
self.id = opt.id
|
||||
self.file = opt.file
|
||||
self.parent = opt.parent
|
||||
self.emitter = EventEmitter()
|
||||
|
||||
self.emitter:on("post_open", utils.bind(self.post_open, self))
|
||||
end
|
||||
|
||||
function Window:destroy()
|
||||
self:_restore_winopts()
|
||||
self:close(true)
|
||||
end
|
||||
|
||||
function Window:clone()
|
||||
return Window({ file = self.file })
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
function Window:is_valid()
|
||||
return self.id and api.nvim_win_is_valid(self.id)
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
function Window:is_file_open()
|
||||
return self:is_valid()
|
||||
and self.file
|
||||
and self.file:is_valid()
|
||||
and api.nvim_win_get_buf(self.id) == self.file.bufnr
|
||||
end
|
||||
|
||||
---@param force? boolean
|
||||
function Window:close(force)
|
||||
if self:is_valid() then
|
||||
api.nvim_win_close(self.id, not not force)
|
||||
self:set_id(nil)
|
||||
end
|
||||
end
|
||||
|
||||
function Window:focus()
|
||||
if self:is_valid() then
|
||||
api.nvim_set_current_win(self.id)
|
||||
end
|
||||
end
|
||||
|
||||
function Window:is_focused()
|
||||
return self:is_valid() and api.nvim_get_current_win() == self.id
|
||||
end
|
||||
|
||||
function Window:post_open()
|
||||
self:apply_custom_folds()
|
||||
end
|
||||
|
||||
---@param self Window
|
||||
---@param callback fun(ok: boolean)
|
||||
Window.load_file = async.wrap(function(self, callback)
|
||||
assert(self.file)
|
||||
|
||||
if self.file:is_valid() then return callback(true) end
|
||||
|
||||
local ok, err = pawait(self.file.create_buffer, self.file)
|
||||
|
||||
if ok and not self.file:is_valid() then
|
||||
-- The buffer may have been destroyed during the await
|
||||
ok = false
|
||||
err = "The file buffer is invalid!"
|
||||
end
|
||||
|
||||
if not ok then
|
||||
logger:error(err)
|
||||
utils.err(fmt("Failed to create diff buffer: '%s:%s'", self.file.rev, self.file.path), true)
|
||||
end
|
||||
|
||||
callback(ok)
|
||||
end)
|
||||
|
||||
---@private
|
||||
function Window:open_fallback()
|
||||
self.emitter:emit("pre_open")
|
||||
|
||||
File.load_null_buffer(self.id)
|
||||
self:apply_null_winopts()
|
||||
|
||||
if self:show_winbar_info() then
|
||||
vim.wo[self.id].winbar = self.file.winbar
|
||||
end
|
||||
|
||||
self.emitter:emit("post_open")
|
||||
end
|
||||
|
||||
---@param self Window
|
||||
Window.open_file = async.void(function(self)
|
||||
---@diagnostic disable: invisible
|
||||
assert(self.file)
|
||||
|
||||
if not (self:is_valid() and self.file.active) then return end
|
||||
|
||||
if not self.file:is_valid() then
|
||||
local ok = await(self:load_file())
|
||||
await(async.scheduler())
|
||||
|
||||
-- Ensure validity after await
|
||||
if not (self:is_valid() and self.file.active) then return end
|
||||
|
||||
if not ok then
|
||||
self:open_fallback()
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
self.emitter:emit("pre_open")
|
||||
|
||||
local conf = config.get_config()
|
||||
api.nvim_win_set_buf(self.id, self.file.bufnr)
|
||||
|
||||
if self.file.rev.type == RevType.LOCAL then
|
||||
self:_save_winopts()
|
||||
end
|
||||
|
||||
if self:is_nulled() then
|
||||
self:apply_null_winopts()
|
||||
else
|
||||
self:apply_file_winopts()
|
||||
end
|
||||
|
||||
local view = lib.get_current_view()
|
||||
local disable_diagnostics = false
|
||||
|
||||
if self.file.kind == "conflicting" then
|
||||
disable_diagnostics = conf.view.merge_tool.disable_diagnostics
|
||||
elseif view and FileHistoryView.__get():ancestorof(view) then
|
||||
disable_diagnostics = conf.view.file_history.disable_diagnostics
|
||||
else
|
||||
disable_diagnostics = conf.view.default.disable_diagnostics
|
||||
end
|
||||
|
||||
self.file:attach_buffer(false, {
|
||||
keymaps = config.get_layout_keymaps(self.parent),
|
||||
disable_diagnostics = disable_diagnostics,
|
||||
})
|
||||
|
||||
if self:show_winbar_info() then
|
||||
vim.wo[self.id].winbar = self.file.winbar
|
||||
end
|
||||
|
||||
self.emitter:emit("post_open")
|
||||
|
||||
api.nvim_win_call(self.id, function()
|
||||
DiffviewGlobal.emitter:emit("diff_buf_win_enter", self.file.bufnr, self.id, {
|
||||
symbol = self.file.symbol,
|
||||
layout_name = self.parent.name,
|
||||
})
|
||||
end)
|
||||
---@diagnostic enable: invisible
|
||||
end)
|
||||
|
||||
---@return boolean
|
||||
function Window:show_winbar_info()
|
||||
if self.file and self.file.winbar and HAS_NVIM_0_8 then
|
||||
local conf = config.get_config()
|
||||
local view = lib.get_current_view()
|
||||
|
||||
if self.file.kind == "conflicting" then
|
||||
return conf.view.merge_tool.winbar_info
|
||||
else
|
||||
if view and view.class == FileHistoryView.__get() then
|
||||
return conf.view.file_history.winbar_info
|
||||
else
|
||||
return conf.view.default.winbar_info
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function Window:is_nulled()
|
||||
return self:is_valid() and api.nvim_win_get_buf(self.id) == File.NULL_FILE.bufnr
|
||||
end
|
||||
|
||||
function Window:open_null()
|
||||
if self:is_valid() then
|
||||
self.emitter:emit("pre_open")
|
||||
File.load_null_buffer(self.id)
|
||||
end
|
||||
end
|
||||
|
||||
function Window:detach_file()
|
||||
if self.file and self.file:is_valid() then
|
||||
self.file:detach_buffer()
|
||||
end
|
||||
end
|
||||
|
||||
---Check if the file buffer is in use in the current view's layout.
|
||||
---@private
|
||||
---@return boolean
|
||||
function Window:_is_file_in_use()
|
||||
local view = lib.get_current_view() --[[@as StandardView? ]]
|
||||
|
||||
if view and view.cur_layout ~= self.parent then
|
||||
local main = view.cur_layout:get_main_win()
|
||||
return main.file.bufnr and main.file.bufnr == self.file.bufnr
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function Window:_save_winopts()
|
||||
if Window.winopt_store[self.file.bufnr] then return end
|
||||
|
||||
Window.winopt_store[self.file.bufnr] = {}
|
||||
api.nvim_win_call(self.id, function()
|
||||
for option, _ in pairs(self.file.winopts) do
|
||||
Window.winopt_store[self.file.bufnr][option] = vim.o[option]
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function Window:_restore_winopts()
|
||||
if
|
||||
Window.winopt_store[self.file.bufnr]
|
||||
and api.nvim_buf_is_loaded(self.file.bufnr)
|
||||
and not self:_is_file_in_use()
|
||||
then
|
||||
utils.no_win_event_call(function()
|
||||
local winid = utils.temp_win(self.file.bufnr)
|
||||
utils.set_local(winid, Window.winopt_store[self.file.bufnr])
|
||||
|
||||
if HAS_NVIM_0_8 then
|
||||
vim.wo[winid].winbar = nil
|
||||
end
|
||||
|
||||
api.nvim_win_close(winid, true)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function Window:apply_file_winopts()
|
||||
assert(self.file)
|
||||
if self.file.winopts then
|
||||
utils.set_local(self.id, self.file.winopts)
|
||||
end
|
||||
end
|
||||
|
||||
function Window:apply_null_winopts()
|
||||
if File.NULL_FILE.winopts then
|
||||
utils.set_local(self.id, File.NULL_FILE.winopts)
|
||||
end
|
||||
|
||||
local file_winhl = utils.tbl_access(self, "file.winopts.winhl")
|
||||
if file_winhl then
|
||||
utils.set_local(self.id, { winhl = file_winhl })
|
||||
end
|
||||
end
|
||||
|
||||
---Use the given map of local options. These options are saved and restored
|
||||
---when the file gets unloaded.
|
||||
---@param opts WindowOptions
|
||||
function Window:use_winopts(opts)
|
||||
if not self:is_file_open() then
|
||||
self.emitter:once("post_open", utils.bind(self.use_winopts, self, opts))
|
||||
return
|
||||
end
|
||||
|
||||
local opt_store = utils.tbl_ensure(Window.winopt_store, { self.file.bufnr })
|
||||
|
||||
api.nvim_win_call(self.id, function()
|
||||
for option, v in pairs(opts) do
|
||||
if opt_store[option] == nil then
|
||||
opt_store[option] = vim.o[option]
|
||||
end
|
||||
|
||||
self.file.winopts[option] = v
|
||||
utils.set_local(self.id, { [option] = v })
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function Window:apply_custom_folds()
|
||||
if self.file.custom_folds
|
||||
and not self:is_nulled()
|
||||
and vim.wo[self.id].foldmethod == "manual"
|
||||
then
|
||||
api.nvim_win_call(self.id, function()
|
||||
pcall(vim.cmd, "norm! zE") -- Delete all folds in the window
|
||||
|
||||
for _, fold in ipairs(self.file.custom_folds) do
|
||||
vim.cmd(fmt("%d,%dfold", fold[1], fold[2]))
|
||||
-- print(fmt("%d,%dfold", fold[1], fold[2]))
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function Window:set_id(id)
|
||||
self.id = id
|
||||
end
|
||||
|
||||
function Window:set_file(file)
|
||||
self.file = file
|
||||
end
|
||||
|
||||
M.Window = Window
|
||||
return M
|
||||
@ -0,0 +1,320 @@
|
||||
local async = require("diffview.async")
|
||||
local lazy = require("diffview.lazy")
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
local control = lazy.require("diffview.control") ---@module "diffview.control"
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
|
||||
local await = async.await
|
||||
|
||||
local M = {}
|
||||
|
||||
---@generic T
|
||||
---@class Stream<T> : diffview.Object
|
||||
---@operator call : Stream
|
||||
---@field src Stream.SrcFunc
|
||||
---@field head integer
|
||||
---@field drained boolean
|
||||
local Stream = oop.create_class("Stream")
|
||||
M.Stream = Stream
|
||||
|
||||
Stream.EOF = oop.Symbol("Stream.EOF");
|
||||
|
||||
---@alias Stream.SrcFunc fun(): (item: unknown, continue: boolean?)
|
||||
|
||||
---@param src table|Stream.SrcFunc
|
||||
function Stream:init(src)
|
||||
self.src = self:create_src(src)
|
||||
self.head = 1
|
||||
self.drained = false
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param src table|function
|
||||
function Stream:create_src(src)
|
||||
if type(src) == "table" then
|
||||
if utils.islist(src) then
|
||||
local itr = ipairs(src)
|
||||
|
||||
return function()
|
||||
local _, v = itr(src, self.head - 1)
|
||||
return v
|
||||
end
|
||||
else
|
||||
error("Unimplemented!")
|
||||
end
|
||||
else
|
||||
return src
|
||||
end
|
||||
end
|
||||
|
||||
---@return unknown? item
|
||||
---@return integer? index
|
||||
function Stream:next()
|
||||
if self.drained then
|
||||
error("Attempted to consume a drained stream!")
|
||||
end
|
||||
|
||||
local idx = self.head
|
||||
local v, cont = self.src()
|
||||
|
||||
if v == Stream.EOF or (v == nil and not cont) then
|
||||
self.drained = true
|
||||
return Stream.EOF, nil
|
||||
end
|
||||
|
||||
self.head = self.head + 1
|
||||
|
||||
return v, idx
|
||||
end
|
||||
|
||||
---@param n? integer
|
||||
function Stream:skip(n)
|
||||
if not n then
|
||||
self:next()
|
||||
return
|
||||
end
|
||||
|
||||
for _ = 1, n do self:next() end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
---@return fun(): (index: integer, item: unknown)
|
||||
function Stream:iter()
|
||||
return function()
|
||||
local v, i = self:next()
|
||||
---@diagnostic disable-next-line: missing-return-value, return-type-mismatch
|
||||
if v == Stream.EOF then return nil end
|
||||
---@cast i -?
|
||||
return i, v
|
||||
end
|
||||
end
|
||||
|
||||
---@return unknown[]
|
||||
function Stream:collect()
|
||||
local ret = {}
|
||||
for i, v in self:iter() do ret[i] = v end
|
||||
return ret
|
||||
end
|
||||
|
||||
---@param first? integer (default: 1)
|
||||
---@param last? integer (default: math.huge)
|
||||
---@return Stream
|
||||
function Stream:slice(first, last)
|
||||
if first == nil then first = 1 end
|
||||
if last == nil then last = math.huge end
|
||||
|
||||
return Stream(function()
|
||||
if self.head > last then return nil, false end
|
||||
if first > self.head then
|
||||
self:skip(first - self.head)
|
||||
end
|
||||
|
||||
return (self:next())
|
||||
end)
|
||||
end
|
||||
|
||||
---@param f fun(item: unknown): unknown
|
||||
---@return Stream
|
||||
function Stream:map(f)
|
||||
return Stream(function()
|
||||
local v = self:next()
|
||||
|
||||
while v ~= Stream.EOF do
|
||||
v = f(v)
|
||||
if v ~= nil then break end
|
||||
v = self:next()
|
||||
end
|
||||
|
||||
if v == Stream.EOF then
|
||||
return nil, false
|
||||
end
|
||||
|
||||
return v
|
||||
end)
|
||||
end
|
||||
|
||||
---@param f fun(item: unknown): boolean
|
||||
---@return Stream
|
||||
function Stream:filter(f)
|
||||
return self:map(function(item)
|
||||
if not f(item) then
|
||||
return nil
|
||||
end
|
||||
|
||||
return item
|
||||
end)
|
||||
end
|
||||
|
||||
---@generic T
|
||||
---@param f fun(acc: unknown, cur: unknown): T # Reducer
|
||||
---@param init? any # Initial value of the accumulator. Defaults to the next value in the stream.
|
||||
---@return T
|
||||
function Stream:reduce(f, init)
|
||||
local acc = init
|
||||
if not acc then acc = self:next() end
|
||||
for _, v in self:iter() do acc = f(acc, v) end
|
||||
|
||||
return acc
|
||||
end
|
||||
|
||||
---@class AsyncStream : Stream, Waitable
|
||||
---@operator call : AsyncStream
|
||||
local AsyncStream = oop.create_class("AsyncStream", Stream)
|
||||
M.AsyncStream = AsyncStream
|
||||
|
||||
AsyncStream.next = async.sync_wrap(
|
||||
---@param self AsyncStream
|
||||
---@param callback function
|
||||
function(self, callback)
|
||||
if self.drained then
|
||||
error("Attempted to consume a drained stream!")
|
||||
end
|
||||
|
||||
local idx = self.head
|
||||
local v, cont = await(self.src())
|
||||
|
||||
if v == Stream.EOF or (v == nil and not cont) then
|
||||
self.drained = true
|
||||
callback(Stream.EOF, nil)
|
||||
return
|
||||
end
|
||||
|
||||
self.head = self.head + 1
|
||||
|
||||
callback(v, idx)
|
||||
end
|
||||
)
|
||||
|
||||
AsyncStream.await = async.sync_wrap(
|
||||
---@param self AsyncStream
|
||||
---@param callback function
|
||||
function(self, callback)
|
||||
callback(self:collect())
|
||||
end
|
||||
)
|
||||
|
||||
---@enum StreamState
|
||||
local StreamState = oop.enum({
|
||||
OPEN = 1,
|
||||
CLOSING = 2,
|
||||
CLOSED = 3,
|
||||
})
|
||||
|
||||
---@class AsyncListStream : AsyncStream
|
||||
---@operator call : AsyncListStream
|
||||
---@field private data unknown[]
|
||||
---@field private state { [AsyncListStream.EventKind]: { listeners: function[], args?: unknown[] } }
|
||||
---@field private flow_state StreamState
|
||||
---@field private sem Semaphore
|
||||
---@field private close_listeners? (fun(...))[]
|
||||
---@field private post_close_listeners (fun())[]
|
||||
---@field private on_close_args? unknown[]
|
||||
local AsyncListStream = oop.create_class("AsyncListStream", AsyncStream)
|
||||
M.AsyncListStream = AsyncListStream
|
||||
|
||||
---@alias AsyncListStream.EventKind "on_close"|"on_post_close"
|
||||
|
||||
function AsyncListStream:init(opt)
|
||||
opt = opt or {}
|
||||
|
||||
self.data = {}
|
||||
self.state = {
|
||||
on_close = { listeners = { opt.on_close } },
|
||||
on_post_close = { listeners = { opt.on_post_close } },
|
||||
}
|
||||
self.flow_state = StreamState.OPEN
|
||||
self.sem = control.Semaphore(1)
|
||||
|
||||
local src = async.wrap(function(callback)
|
||||
if self.data[self.head] == nil then
|
||||
self.resume = callback
|
||||
return
|
||||
end
|
||||
|
||||
callback(self.data[self.head])
|
||||
end)
|
||||
|
||||
self:super(src)
|
||||
end
|
||||
|
||||
---Append the given items to the end of the stream. Pushing `Stream.EOF` will
|
||||
---close the stream.
|
||||
function AsyncListStream:push(...)
|
||||
if self:is_closed() then return end
|
||||
local args = { ... }
|
||||
local permit = await(self.sem:acquire()) --[[@as Permit ]]
|
||||
|
||||
for i = 1, select("#", ...) do
|
||||
if args[i] ~= nil then
|
||||
if args[i] == Stream.EOF then
|
||||
if self.flow_state ~= StreamState.CLOSING then
|
||||
self.flow_state = StreamState.CLOSING
|
||||
|
||||
-- Release permit while calling 'on_close' callbacks so that they're
|
||||
-- able to invoke some final pushes before fully closing the stream.
|
||||
permit:forget()
|
||||
self:invoke_listeners("on_close")
|
||||
permit = await(self.sem:acquire()) --[[@as Permit ]]
|
||||
|
||||
self.data[#self.data+1] = args[i]
|
||||
self.flow_state = StreamState.CLOSED
|
||||
|
||||
self:invoke_listeners("on_post_close")
|
||||
|
||||
break
|
||||
end
|
||||
else
|
||||
self.data[#self.data+1] = args[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
permit:forget()
|
||||
|
||||
if self.resume then
|
||||
local resume = self.resume
|
||||
self.resume = nil
|
||||
resume(self.data[self.head])
|
||||
end
|
||||
end
|
||||
|
||||
---@param ... any Arguments to pass to the `on_close` callback.
|
||||
function AsyncListStream:close(...)
|
||||
if self:is_closed() then return end
|
||||
self.state.on_close.args = utils.tbl_pack(...)
|
||||
self:push(Stream.EOF)
|
||||
end
|
||||
|
||||
function AsyncListStream:is_closed()
|
||||
return self.flow_state == StreamState.CLOSED
|
||||
end
|
||||
|
||||
---@param callback function
|
||||
function AsyncListStream:on_close(callback)
|
||||
table.insert(self.state.on_close.listeners, callback)
|
||||
end
|
||||
|
||||
---@param callback function
|
||||
function AsyncListStream:on_post_close(callback)
|
||||
table.insert(self.state.on_post_close.listeners, callback)
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param kind AsyncListStream.EventKind
|
||||
function AsyncListStream:invoke_listeners(kind)
|
||||
local event_state = self.state[kind]
|
||||
|
||||
for _, listener in ipairs(event_state.listeners) do
|
||||
if event_state.args then
|
||||
listener(utils.tbl_unpack(event_state.args))
|
||||
else
|
||||
listener()
|
||||
end
|
||||
end
|
||||
|
||||
event_state.args = nil
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,323 @@
|
||||
local helpers = require("diffview.tests.helpers")
|
||||
|
||||
local eq, neq = helpers.eq, helpers.neq
|
||||
|
||||
-- Windows path standards:
|
||||
-- https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats
|
||||
|
||||
describe("diffview.path", function()
|
||||
local PathLib = require("diffview.path").PathLib
|
||||
|
||||
describe("convert()", function()
|
||||
it("converts to the default sep when non is specified", function()
|
||||
local pl = PathLib({ os = "unix" })
|
||||
|
||||
eq("/foo/bar/baz", pl:convert("/foo/bar/baz"))
|
||||
eq("/foo/bar/baz", pl:convert([[\foo\bar\baz]]))
|
||||
eq("/foo/bar/baz", pl:convert("////foo///bar//baz"))
|
||||
eq("/foo/bar/baz", pl:convert([[\\\\foo\\//\bar\\baz]]))
|
||||
|
||||
pl = PathLib({ os = "windows" })
|
||||
|
||||
eq([[C:\foo\bar\baz]], pl:convert([[C:\foo\bar\baz]]))
|
||||
eq([[C:\foo\bar\baz]], pl:convert([[C:/foo/bar/baz]]))
|
||||
eq([[C:\foo\bar\baz]], pl:convert([[C:\\\\foo\\\bar\\baz]]))
|
||||
eq([[C:\foo\bar\baz]], pl:convert([[C:/foo//\\//bar//baz]]))
|
||||
eq([[\foo\bar\baz]], pl:convert([[\foo\bar\baz]]))
|
||||
eq([[\foo\bar\baz]], pl:convert([[/foo/bar/baz]]))
|
||||
|
||||
-- Windows UNC paths
|
||||
eq([[\\wsl.localhost\foo\bar\baz]], pl:convert([[\\wsl.localhost\foo\bar\baz]]))
|
||||
eq([[\\wsl.localhost\foo\bar\baz]], pl:convert([[\\wsl.localhost/foo/bar/baz]]))
|
||||
eq([[\\]], pl:convert([[\\]]))
|
||||
|
||||
-- Windows DOS Device paths
|
||||
eq([[\\.\foo\bar\baz]], pl:convert([[\\.\foo\bar\baz]]))
|
||||
eq([[\\.\foo\bar\baz]], pl:convert([[\\.\foo/bar/baz]]))
|
||||
eq([[\\.\]], pl:convert([[\\.\]]))
|
||||
|
||||
eq([[\\?\foo\bar\baz]], pl:convert([[\\?\foo\bar\baz]]))
|
||||
eq([[\\?\foo\bar\baz]], pl:convert([[\\?\foo/bar/baz]]))
|
||||
eq([[\\?\]], pl:convert([[\\?\]]))
|
||||
end)
|
||||
|
||||
it("converts to the specified sep", function()
|
||||
local pl = PathLib({ os = "unix" })
|
||||
|
||||
eq([[/foo/bar/baz]], pl:convert([[/foo/bar/baz]], "/"))
|
||||
eq([[/foo/bar/baz]], pl:convert([[\foo\bar\baz]], "/"))
|
||||
eq([[\foo\bar\baz]], pl:convert([[/foo/bar/baz]], "\\"))
|
||||
eq([[\foo\bar\baz]], pl:convert([[\foo\bar\baz]], "\\"))
|
||||
|
||||
pl = PathLib({ os = "windows" })
|
||||
|
||||
eq([[C:\foo\bar\baz]], pl:convert([[C:\foo\bar\baz]], "\\"))
|
||||
eq([[C:\foo\bar\baz]], pl:convert([[C:/foo/bar/baz]], "\\"))
|
||||
eq([[C:/foo/bar/baz]], pl:convert([[C:\foo\bar\baz]], "/"))
|
||||
eq([[C:/foo/bar/baz]], pl:convert([[C:/foo/bar/baz]], "/"))
|
||||
|
||||
-- UNC
|
||||
eq([[\\wsl.localhost\foo\bar\baz]], pl:convert([[\\wsl.localhost\foo\bar\baz]], "\\"))
|
||||
eq([[\\wsl.localhost\foo\bar\baz]], pl:convert([[//wsl.localhost/foo/bar/baz]], "\\"))
|
||||
eq([[//wsl.localhost/foo/bar/baz]], pl:convert([[\\wsl.localhost\foo\bar\baz]], "/"))
|
||||
eq([[//wsl.localhost/foo/bar/baz]], pl:convert([[//wsl.localhost/foo/bar/baz]], "/"))
|
||||
|
||||
-- DOS Device
|
||||
eq([[\\.\foo\bar\baz]], pl:convert([[\\.\foo\bar\baz]], "\\"))
|
||||
eq([[\\.\foo\bar\baz]], pl:convert([[//./foo/bar/baz]], "\\"))
|
||||
eq([[//./foo/bar/baz]], pl:convert([[\\.\foo\bar\baz]], "/"))
|
||||
eq([[//./foo/bar/baz]], pl:convert([[//./foo/bar/baz]], "/"))
|
||||
|
||||
eq([[\\?\foo\bar\baz]], pl:convert([[\\?\foo\bar\baz]], "\\"))
|
||||
eq([[\\?\foo\bar\baz]], pl:convert([[//?/foo/bar/baz]], "\\"))
|
||||
eq([[//?/foo/bar/baz]], pl:convert([[\\?\foo\bar\baz]], "/"))
|
||||
eq([[//?/foo/bar/baz]], pl:convert([[//?/foo/bar/baz]], "/"))
|
||||
end)
|
||||
|
||||
it("handles URI's correctly", function()
|
||||
local pl = PathLib({ os = "unix" })
|
||||
|
||||
eq("test:///foo/bar/baz", pl:convert("test:///foo/bar/baz"))
|
||||
eq("test://foo/bar/baz", pl:convert("test://foo/bar/baz"))
|
||||
eq("test:///foo/bar/baz", pl:convert([[test://\foo\bar\baz]]))
|
||||
|
||||
pl = PathLib({ os = "windows" })
|
||||
|
||||
eq("test:///foo/bar/baz", pl:convert("test:///foo/bar/baz"))
|
||||
eq("test://foo/bar/baz", pl:convert("test://foo/bar/baz"))
|
||||
eq("test:///foo/bar/baz", pl:convert([[test://\foo\bar\baz]]))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("is_abs()", function()
|
||||
it("works for UNIX paths", function()
|
||||
local pl = PathLib({ os = "unix" })
|
||||
|
||||
eq(true, pl:is_abs("/foo/bar/baz"))
|
||||
eq(true, pl:is_abs("/"))
|
||||
eq(false, pl:is_abs("foo/bar/baz"))
|
||||
eq(false, pl:is_abs(""))
|
||||
end)
|
||||
|
||||
it("works for Windows paths", function()
|
||||
local pl = PathLib({ os = "windows" })
|
||||
|
||||
-- fs
|
||||
eq(true, pl:is_abs("C:/foo/bar/baz"))
|
||||
eq(true, pl:is_abs("C:/"))
|
||||
eq(true, pl:is_abs("/"))
|
||||
eq(false, pl:is_abs("foo/bar/baz"))
|
||||
eq(false, pl:is_abs(""))
|
||||
|
||||
-- UNC
|
||||
eq(true, pl:is_abs([[\\wsl.localhost\Ubuntu1804]]))
|
||||
eq(true, pl:is_abs([[\\]]))
|
||||
|
||||
-- DOS Device
|
||||
eq(true, pl:is_abs([[\\.\foo\bar\baz]]))
|
||||
eq(true, pl:is_abs([[\\.\]]))
|
||||
|
||||
eq(true, pl:is_abs([[\\?\foo\bar\baz]]))
|
||||
eq(true, pl:is_abs([[\\?\]]))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("absolute()", function()
|
||||
it("works for UNIX paths", function()
|
||||
local pl = PathLib({ os = "unix" })
|
||||
|
||||
eq([[/foo/bar/baz]], pl:absolute([[bar/baz]], [[/foo]]))
|
||||
eq([[/foo/bar/baz]], pl:absolute([[/foo/bar/baz]], [[/foo]]))
|
||||
end)
|
||||
|
||||
it("works for Windows paths", function()
|
||||
local pl = PathLib({ os = "windows" })
|
||||
|
||||
eq([[C:\foo\bar\baz]], pl:absolute([[bar\baz]], [[C:\foo]]))
|
||||
eq([[C:\foo\bar\baz]], pl:absolute([[C:\foo\bar\baz]], [[C:\foo]]))
|
||||
|
||||
eq([[C:\foo\bar\baz]], pl:absolute([[\foo\bar\baz]], [[C:\lorem\ipsum]]))
|
||||
eq([[D:\foo\bar\baz]], pl:absolute([[\foo\bar\baz]], [[D:\lorem\ipsum]]))
|
||||
|
||||
eq([[\\wsl.localhost\foo\bar\baz]], pl:absolute([[bar\baz]], [[\\wsl.localhost\foo]]))
|
||||
eq([[\\wsl.localhost\foo\bar\baz]], pl:absolute([[\\wsl.localhost\foo\bar\baz]], [[\\wsl.localhost\foo]]))
|
||||
|
||||
eq([[\\.\foo\bar\baz]], pl:absolute([[bar\baz]], [[\\.\foo]]))
|
||||
eq([[\\.\foo\bar\baz]], pl:absolute([[\\.\foo\bar\baz]], [[\\.\foo]]))
|
||||
|
||||
eq([[\\?\foo\bar\baz]], pl:absolute([[bar\baz]], [[\\?\foo]]))
|
||||
eq([[\\?\foo\bar\baz]], pl:absolute([[\\?\foo\bar\baz]], [[\\?\foo]]))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("is_root()", function()
|
||||
it("works for UNIX paths", function()
|
||||
local pl = PathLib({ os = "unix" })
|
||||
|
||||
eq(true, pl:is_root("/"))
|
||||
eq(false, pl:is_root("/foo"))
|
||||
end)
|
||||
|
||||
it("works for Windows paths", function()
|
||||
local pl = PathLib({ os = "windows" })
|
||||
|
||||
eq(true, pl:is_root([[C:\]]))
|
||||
eq(true, pl:is_root([[C:]]))
|
||||
eq(true, pl:is_root([[\]]))
|
||||
eq(false, pl:is_root([[C:\foo]]))
|
||||
|
||||
eq(true, pl:is_root([[\\]]))
|
||||
eq(false, pl:is_root([[\\foo]]))
|
||||
|
||||
eq(true, pl:is_root([[\\.\]]))
|
||||
eq(false, pl:is_root([[\\.\foo]]))
|
||||
|
||||
eq(true, pl:is_root([[\\?\]]))
|
||||
eq(false, pl:is_root([[\\?\foo]]))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("root()", function()
|
||||
it("works for UNIX paths", function()
|
||||
local pl = PathLib({ os = "unix" })
|
||||
|
||||
eq("/", pl:root("/"))
|
||||
eq("/", pl:root("/foo"))
|
||||
eq(nil, pl:root("foo/bar/baz"))
|
||||
end)
|
||||
|
||||
it("works for Windows paths", function()
|
||||
local pl = PathLib({ os = "windows" })
|
||||
|
||||
eq(nil, pl:root([[foo\bar\baz]]))
|
||||
|
||||
eq([[C:]], pl:root([[C:]]))
|
||||
eq([[C:]], pl:root([[C:\foo\bar\baz]]))
|
||||
eq([[\]], pl:root([[\]]))
|
||||
eq([[\]], pl:root([[\foo\bar\baz]]))
|
||||
|
||||
eq([[\\]], pl:root([[\\]]))
|
||||
eq([[\\]], pl:root([[\\foo\bar\baz]]))
|
||||
|
||||
eq([[\\.\]], pl:root([[\\.\]]))
|
||||
eq([[\\.\]], pl:root([[\\.\foo\bar\baz]]))
|
||||
|
||||
eq([[\\?\]], pl:root([[\\?\]]))
|
||||
eq([[\\?\]], pl:root([[\\?\foo\bar\baz]]))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("normalize()", function()
|
||||
it("works for UNIX paths", function()
|
||||
local pl = PathLib({ os = "unix" })
|
||||
|
||||
eq("foo/bar/baz", pl:normalize("foo/bar/././baz", { cwd = "/lorem/ipsum/dolor" }))
|
||||
eq("foo/baz", pl:normalize("foo/bar/../baz", { cwd = "/lorem/ipsum/dolor" }))
|
||||
eq("/lorem/ipsum/baz", pl:normalize("foo/../../baz", { cwd = "/lorem/ipsum/dolor" }))
|
||||
eq(".", pl:normalize("foo/..", { cwd = "/lorem/ipsum/dolor" }))
|
||||
end)
|
||||
|
||||
it("works for Windows paths", function()
|
||||
local pl = PathLib({ os = "windows" })
|
||||
|
||||
-- Resolves relative drive
|
||||
eq([[D:\foo\bar\baz]], pl:normalize([[\foo\bar\baz]], { cwd = [[D:\lorem\ipsum\dolor]] }))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("expand()", function()
|
||||
local save_env = {}
|
||||
|
||||
before_each(function()
|
||||
local env = {
|
||||
HOME = "/lorem/ipsum/dolor",
|
||||
VAR_FOO = "EXPANDED_FOO",
|
||||
VAR_BAR = "EXPANDED_BAR",
|
||||
}
|
||||
for k, v in pairs(env) do
|
||||
save_env[k] = vim.env[k] or ""
|
||||
vim.env[k] = v
|
||||
end
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
for k, v in pairs(save_env) do vim.env[k] = v end
|
||||
end)
|
||||
|
||||
it("works", function()
|
||||
local pl = PathLib({ os = "unix" })
|
||||
|
||||
eq("/lorem/ipsum/dolor/foo", pl:expand("~/foo"))
|
||||
eq("foo/EXPANDED_FOO/EXPANDED_BAR/baz", pl:expand("foo/$VAR_FOO/$VAR_BAR/baz"))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("join()", function()
|
||||
it("works for UNIX paths", function()
|
||||
local pl = PathLib({ os = "unix" })
|
||||
|
||||
eq([[/foo/bar/baz]], pl:join({ "/", "foo", "bar", "baz" }))
|
||||
eq([[/foo/bar/baz]], pl:join({ "/foo/bar", "baz" }))
|
||||
eq([[/foo/bar/baz]], pl:join({ "/", "foo/", "/bar///", "/baz" }))
|
||||
eq([[foo/bar/baz]], pl:join({ "", "foo", "bar", "baz" }))
|
||||
end)
|
||||
|
||||
it("works for Windows paths", function()
|
||||
local pl = PathLib({ os = "windows" })
|
||||
|
||||
eq([[C:\foo\bar\baz]], pl:join({ "C:", "foo", "bar", "baz" }))
|
||||
eq([[C:\foo\bar\baz]], pl:join({ "C:\\foo\\bar", "baz" }))
|
||||
eq([[C:\foo\bar\baz]], pl:join({ "C:\\", "foo\\", "\\bar\\\\", "\\baz" }))
|
||||
eq([[\foo\bar\baz]], pl:join({ "\\", "foo", "bar", "baz" }))
|
||||
eq([[foo\bar\baz]], pl:join({ "", "foo", "bar", "baz" }))
|
||||
|
||||
eq([[\\foo\bar\baz]], pl:join({ [[\\]], "foo", "bar", "baz" }))
|
||||
eq([[\\foo\bar\baz]], pl:join({ [[\\foo\\bar]], "baz" }))
|
||||
eq([[\\foo\bar\baz]], pl:join({ [[\\]], "foo\\", "\\bar\\\\", "\\baz" }))
|
||||
|
||||
eq([[\\.\foo\bar\baz]], pl:join({ [[\\.\]], "foo", "bar", "baz" }))
|
||||
eq([[\\.\foo\bar\baz]], pl:join({ [[\\.\foo\\bar]], "baz" }))
|
||||
eq([[\\.\foo\bar\baz]], pl:join({ [[\\.\]], "foo\\", "\\bar\\\\", "\\baz" }))
|
||||
|
||||
eq([[\\?\foo\bar\baz]], pl:join({ [[\\?\]], "foo", "bar", "baz" }))
|
||||
eq([[\\?\foo\bar\baz]], pl:join({ [[\\?\foo\\bar]], "baz" }))
|
||||
eq([[\\?\foo\bar\baz]], pl:join({ [[\\?\]], "foo\\", "\\bar\\\\", "\\baz" }))
|
||||
end)
|
||||
|
||||
it("works for URIs", function()
|
||||
local pl = PathLib({ os = "unix" })
|
||||
|
||||
eq([[test:///foo/bar/baz]], pl:join({ "test://", "/", "foo", "bar", "baz"}))
|
||||
eq([[test://foo/bar/baz]], pl:join({ "test://", "foo", "bar", "baz"}))
|
||||
eq([[test://foo/bar/baz]], pl:join({ "test://", "foo/", "//bar/", "baz"}))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("explode()", function()
|
||||
it("works for UNIX paths", function()
|
||||
local pl = PathLib({ os = "unix" })
|
||||
|
||||
eq({ "/", "foo", "bar", "baz" }, pl:explode("/foo/bar/baz"))
|
||||
eq({ "foo", "bar", "baz" }, pl:explode("foo/bar/baz"))
|
||||
end)
|
||||
|
||||
it("works for Windows paths", function()
|
||||
local pl = PathLib({ os = "windows" })
|
||||
|
||||
eq({ "C:", "foo", "bar", "baz" }, pl:explode([[C:\foo\bar\baz]]))
|
||||
eq({ "foo", "bar", "baz" }, pl:explode([[foo\bar\baz]]))
|
||||
|
||||
eq({ [[\]], "foo", "bar", "baz" }, pl:explode([[\foo\bar\baz]]))
|
||||
eq({ [[\\]], "foo", "bar", "baz" }, pl:explode([[\\foo\bar\baz]]))
|
||||
eq({ [[\\.\]], "foo", "bar", "baz" }, pl:explode([[\\.\foo\bar\baz]]))
|
||||
eq({ [[\\?\]], "foo", "bar", "baz" }, pl:explode([[\\?\foo\bar\baz]]))
|
||||
end)
|
||||
|
||||
it("works for URIs", function()
|
||||
local pl = PathLib({ os = "unix" })
|
||||
|
||||
eq({ "test://", "/", "foo", "bar", "baz" }, pl:explode("test:///foo/bar/baz"))
|
||||
eq({ "test://", "foo", "bar", "baz" }, pl:explode("test://foo/bar/baz"))
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
@ -0,0 +1,245 @@
|
||||
local async = require("diffview.async")
|
||||
local helpers = require("diffview.tests.helpers")
|
||||
|
||||
local await = async.await
|
||||
local async_test = helpers.async_test
|
||||
local eq, neq = helpers.eq, helpers.neq
|
||||
|
||||
describe("diffview.stream", function()
|
||||
local Stream = require("diffview.stream").Stream
|
||||
local AsyncStream = require("diffview.stream").AsyncStream
|
||||
local AsyncListStream = require("diffview.stream").AsyncListStream
|
||||
|
||||
local arr1 = {
|
||||
{ name = "a", i = 1 },
|
||||
{ name = "b", i = 2 },
|
||||
{ name = "c", i = 3 },
|
||||
{ name = "d", i = 4 },
|
||||
{ name = "e", i = 5 },
|
||||
}
|
||||
|
||||
describe("Stream", function()
|
||||
it("can collect a simple array", function()
|
||||
eq(arr1, Stream(arr1):collect())
|
||||
end)
|
||||
|
||||
it("can iterate a simple array", function()
|
||||
local s0 = {}
|
||||
local s1 = {}
|
||||
|
||||
for i, v in ipairs(arr1) do
|
||||
s0[#s0+1] = { i, v }
|
||||
end
|
||||
|
||||
for i, v in Stream(arr1):iter() do
|
||||
s1[#s1 + 1] = { i, v }
|
||||
end
|
||||
|
||||
eq(s0, s1)
|
||||
end)
|
||||
|
||||
it("can slice", function()
|
||||
eq(
|
||||
vim.list_slice(arr1, 2, 4),
|
||||
Stream(arr1):slice(2, 4):collect()
|
||||
)
|
||||
end)
|
||||
|
||||
it("can map", function()
|
||||
local function f(item)
|
||||
return item.name:upper():rep(5)
|
||||
end
|
||||
|
||||
eq(
|
||||
vim.tbl_map(f, arr1),
|
||||
Stream(arr1):map(f):collect()
|
||||
)
|
||||
end)
|
||||
|
||||
it("can filter", function()
|
||||
local function f(item)
|
||||
return item.i % 2 ~= 0
|
||||
end
|
||||
|
||||
eq(
|
||||
vim.tbl_filter(f, arr1),
|
||||
Stream(arr1):filter(f):collect()
|
||||
)
|
||||
end)
|
||||
|
||||
it("can reduce without an init value", function()
|
||||
eq(
|
||||
"abcde",
|
||||
Stream({ "a", "b", "c", "d", "e" }):reduce(function(acc, cur)
|
||||
return acc .. cur
|
||||
end)
|
||||
)
|
||||
end)
|
||||
|
||||
it("can reduce with an init value", function()
|
||||
eq(
|
||||
"init abcde",
|
||||
Stream(arr1):reduce(function(acc, cur)
|
||||
return acc .. cur.name
|
||||
end, "init ")
|
||||
)
|
||||
end)
|
||||
|
||||
it("can run a pipeline of transforms", function()
|
||||
eq(
|
||||
"AAAAACCCCCEEEEE",
|
||||
Stream(arr1)
|
||||
:filter(function(item)
|
||||
return item.i % 2 ~= 0
|
||||
end)
|
||||
:map(function(item)
|
||||
return item.name:upper():rep(5)
|
||||
end)
|
||||
:reduce(function(acc, cur)
|
||||
return acc .. cur
|
||||
end, "")
|
||||
)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("AsyncStream", function()
|
||||
local function mock_iter(src_arr)
|
||||
src_arr = src_arr or arr1
|
||||
local iter = ipairs(src_arr)
|
||||
local i = 0
|
||||
|
||||
return async.wrap(function(callback)
|
||||
if i == #src_arr then return callback(nil) end
|
||||
await(async.timeout(1))
|
||||
local _, ret = iter(src_arr, i)
|
||||
i = i + 1
|
||||
return callback(ret)
|
||||
end)
|
||||
end
|
||||
|
||||
it("can iterate", async_test(function()
|
||||
local s0 = {}
|
||||
local s1 = {}
|
||||
|
||||
for i, v in ipairs(arr1) do
|
||||
s0[#s0+1] = { i, v }
|
||||
end
|
||||
|
||||
for i, v in AsyncStream(mock_iter(arr1)):iter() do
|
||||
s1[#s1 + 1] = { i, v }
|
||||
end
|
||||
|
||||
eq(s0, s1)
|
||||
end))
|
||||
|
||||
it("can be awaited", async_test(function()
|
||||
local stream = AsyncStream(mock_iter(arr1))
|
||||
eq(arr1, await(stream))
|
||||
end))
|
||||
end)
|
||||
|
||||
describe("AsyncListStream", function()
|
||||
local mock_worker = async.void(function(stream, src_array)
|
||||
for _, v in ipairs(src_array or arr1) do
|
||||
await(async.timeout(10))
|
||||
stream:push(v)
|
||||
end
|
||||
stream:close()
|
||||
end)
|
||||
|
||||
it("can iterate", async_test(function()
|
||||
local s0 = {}
|
||||
local s1 = {}
|
||||
|
||||
for i, v in ipairs(arr1) do
|
||||
s0[#s0+1] = { i, v }
|
||||
end
|
||||
|
||||
local stream = AsyncListStream()
|
||||
mock_worker(stream)
|
||||
|
||||
for i, v in stream:iter() do
|
||||
s1[#s1 + 1] = { i, v }
|
||||
end
|
||||
|
||||
eq(s0, s1)
|
||||
end))
|
||||
|
||||
it("can be awaited", async_test(function()
|
||||
local stream = AsyncListStream()
|
||||
mock_worker(stream)
|
||||
|
||||
eq(arr1, await(stream))
|
||||
end))
|
||||
|
||||
it("can close early", async_test(function()
|
||||
local stream = AsyncListStream()
|
||||
mock_worker(stream)
|
||||
|
||||
local ret = {}
|
||||
for i, v in stream:iter() do
|
||||
ret[i] = v
|
||||
if i == 3 then stream:close() end
|
||||
end
|
||||
|
||||
eq(vim.list_slice(arr1, 1, 3), ret)
|
||||
end))
|
||||
|
||||
it("can't push items after close", async_test(function()
|
||||
local stream = AsyncListStream()
|
||||
stream:push(1, 2, 3)
|
||||
stream:close()
|
||||
stream:push(4, 5, 6)
|
||||
|
||||
eq({ 1, 2, 3 }, stream:collect())
|
||||
end))
|
||||
|
||||
it("can push final items during on_close()", async_test(function()
|
||||
local final_arr = { "final_1", "final_2", "final_3" }
|
||||
local stream
|
||||
stream = AsyncListStream({
|
||||
on_close = function()
|
||||
stream:push(unpack(final_arr))
|
||||
end,
|
||||
})
|
||||
mock_worker(stream)
|
||||
|
||||
local ret = {}
|
||||
for i, v in stream:iter() do
|
||||
ret[i] = v
|
||||
if i == 3 then stream:close() end
|
||||
end
|
||||
|
||||
eq(
|
||||
vim.list_extend(vim.list_slice(arr1, 1, 3), final_arr),
|
||||
ret
|
||||
)
|
||||
end))
|
||||
|
||||
it("calls on_close() callbacks with the appropriate args", async.sync_wrap(function(done)
|
||||
local stream = AsyncListStream({
|
||||
on_close = function(...)
|
||||
eq({ nil, 1, nil, 2, 3 }, { ... })
|
||||
done()
|
||||
end,
|
||||
})
|
||||
stream:close(nil, 1, nil, 2, 3)
|
||||
end))
|
||||
|
||||
it("calls the event callbacks in the appropriate order", async_test(function()
|
||||
local ret = {}
|
||||
local stream = AsyncListStream({
|
||||
on_close = function()
|
||||
table.insert(ret, 1)
|
||||
end,
|
||||
on_post_close = function()
|
||||
table.insert(ret, 2)
|
||||
end,
|
||||
})
|
||||
|
||||
stream:close()
|
||||
await(stream)
|
||||
eq({ 1, 2 }, ret)
|
||||
end))
|
||||
end)
|
||||
end)
|
||||
@ -0,0 +1,32 @@
|
||||
local assert = require("luassert")
|
||||
local async = require("diffview.async")
|
||||
|
||||
local await, pawait = async.await, async.pawait
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.eq(a, b)
|
||||
if a == nil or b == nil then return assert.are.equal(a, b) end
|
||||
return assert.are.same(a, b)
|
||||
end
|
||||
|
||||
function M.neq(a, b)
|
||||
if a == nil or b == nil then return assert.are_not.equal(a, b) end
|
||||
return assert.are_not.same(a, b)
|
||||
end
|
||||
|
||||
---@param test_func function
|
||||
function M.async_test(test_func)
|
||||
local afunc = async.void(test_func)
|
||||
|
||||
return function(...)
|
||||
local ok, err = pawait(afunc(...))
|
||||
await(async.scheduler())
|
||||
|
||||
if not ok then
|
||||
error(err)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1 @@
|
||||
require("diffview.bootstrap")
|
||||
@ -0,0 +1,22 @@
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class Model : diffview.Object
|
||||
local Model = oop.create_class("Model")
|
||||
|
||||
---@diagnostic disable unused-local
|
||||
|
||||
---@abstract
|
||||
---@param data table
|
||||
---@return CompSchema
|
||||
function Model:create_comp_schema(data) oop.abstract_stub() end
|
||||
|
||||
---@abstract
|
||||
---@param render_data RenderData
|
||||
function Model:render(render_data) oop.abstract_stub() end
|
||||
|
||||
---@diagnostic enable unused-local
|
||||
|
||||
M.Model = Model
|
||||
return M
|
||||
@ -0,0 +1,155 @@
|
||||
local oop = require("diffview.oop")
|
||||
local utils = require("diffview.utils")
|
||||
local Node = require("diffview.ui.models.file_tree.node").Node
|
||||
local Model = require("diffview.ui.model").Model
|
||||
|
||||
local pl = utils.path
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class DirData
|
||||
---@field name string
|
||||
---@field path string
|
||||
---@field kind vcs.FileKind
|
||||
---@field collapsed boolean
|
||||
---@field status string
|
||||
---@field _node Node
|
||||
|
||||
---@class FileTree : Model
|
||||
---@field root Node
|
||||
local FileTree = oop.create_class("FileTree", Model)
|
||||
|
||||
---FileTree constructor
|
||||
---@param files FileEntry[]?
|
||||
function FileTree:init(files)
|
||||
self.root = Node("__ROOT__")
|
||||
|
||||
for _, file in ipairs(files or {}) do
|
||||
self:add_file_entry(file)
|
||||
end
|
||||
end
|
||||
|
||||
---@param file FileEntry
|
||||
function FileTree:add_file_entry(file)
|
||||
local parts = pl:explode(file.path)
|
||||
local cur_node = self.root
|
||||
|
||||
local path = parts[1]
|
||||
|
||||
-- Create missing intermediate pathname components
|
||||
for i = 1, #parts - 1 do
|
||||
local name = parts[i]
|
||||
|
||||
if i > 1 then
|
||||
path = pl:join(path, parts[i])
|
||||
end
|
||||
|
||||
if not cur_node.children[name] then
|
||||
---@type DirData
|
||||
local dir_data = {
|
||||
name = name,
|
||||
path = path,
|
||||
kind = file.kind,
|
||||
collapsed = false,
|
||||
status = " ", -- updated later in FileTree:update_statuses()
|
||||
}
|
||||
cur_node = cur_node:add_child(Node(name, dir_data))
|
||||
else
|
||||
cur_node = cur_node.children[name]
|
||||
end
|
||||
end
|
||||
|
||||
cur_node:add_child(Node(parts[#parts], file))
|
||||
end
|
||||
|
||||
---@param a string
|
||||
---@param b string
|
||||
---@return string
|
||||
local function combine_statuses(a, b)
|
||||
if a == " " or a == "?" or a == "!" or a == b then
|
||||
return b
|
||||
end
|
||||
|
||||
return "M"
|
||||
end
|
||||
|
||||
function FileTree:update_statuses()
|
||||
---@return string the node's status
|
||||
local function recurse(node)
|
||||
if not node:has_children() then
|
||||
return node.data.status
|
||||
end
|
||||
|
||||
local parent_status = " "
|
||||
|
||||
for _, child in ipairs(node.children) do
|
||||
local child_status = recurse(child)
|
||||
parent_status = combine_statuses(parent_status, child_status)
|
||||
end
|
||||
|
||||
node.data.status = parent_status
|
||||
|
||||
return parent_status
|
||||
end
|
||||
|
||||
for _, node in ipairs(self.root.children) do
|
||||
recurse(node)
|
||||
end
|
||||
end
|
||||
|
||||
function FileTree:create_comp_schema(data)
|
||||
self.root:sort()
|
||||
---@type CompSchema
|
||||
local schema = {}
|
||||
|
||||
---@param parent CompSchema
|
||||
---@param node Node
|
||||
local function recurse(parent, node)
|
||||
if not node:has_children() then
|
||||
parent[#parent + 1] = { name = "file", context = node.data }
|
||||
return
|
||||
end
|
||||
|
||||
---@type DirData
|
||||
local dir_data = node.data
|
||||
|
||||
if data.flatten_dirs then
|
||||
while #node.children == 1 and node.children[1]:has_children() do
|
||||
---@type DirData
|
||||
local subdir_data = node.children[1].data
|
||||
dir_data = {
|
||||
name = pl:join(dir_data.name, subdir_data.name),
|
||||
path = subdir_data.path,
|
||||
kind = subdir_data.kind,
|
||||
collapsed = dir_data.collapsed and subdir_data.collapsed,
|
||||
status = dir_data.status,
|
||||
_node = node,
|
||||
}
|
||||
node = node.children[1]
|
||||
end
|
||||
end
|
||||
|
||||
local items = { name = "items" }
|
||||
local struct = {
|
||||
name = "directory",
|
||||
context = dir_data,
|
||||
{ name = "dir_name" },
|
||||
items,
|
||||
}
|
||||
parent[#parent + 1] = struct
|
||||
|
||||
for _, child in ipairs(node.children) do
|
||||
recurse(items, child)
|
||||
end
|
||||
end
|
||||
|
||||
for _, node in ipairs(self.root.children) do
|
||||
recurse(schema, node)
|
||||
end
|
||||
|
||||
return schema
|
||||
end
|
||||
|
||||
M.FileTree = FileTree
|
||||
|
||||
return M
|
||||
@ -0,0 +1,221 @@
|
||||
local oop = require("diffview.oop")
|
||||
local utils = require("diffview.utils")
|
||||
local M = {}
|
||||
|
||||
---@class Node : diffview.Object
|
||||
---@field parent Node
|
||||
---@field name string
|
||||
---@field data any
|
||||
---@field children Node[]
|
||||
---@field depth integer|nil
|
||||
local Node = oop.create_class("Node")
|
||||
|
||||
---Node constructor
|
||||
---@param name string
|
||||
---@param data any|nil
|
||||
function Node:init(name, data)
|
||||
self.name = name
|
||||
self.data = data
|
||||
self.children = {}
|
||||
|
||||
if self.data then
|
||||
self.data._node = self
|
||||
end
|
||||
end
|
||||
|
||||
---Adds a child if it doesn not already exist.
|
||||
---@param child Node
|
||||
---@return Node
|
||||
function Node:add_child(child)
|
||||
if not self.children[child.name] then
|
||||
self.children[child.name] = child
|
||||
self.children[#self.children + 1] = child
|
||||
child.parent = self
|
||||
end
|
||||
|
||||
return self.children[child.name]
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
function Node:has_children()
|
||||
for _ in pairs(self.children) do
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
---Compare against another node alphabetically and case-insensitive by node names.
|
||||
---Directory nodes come before file nodes.
|
||||
---@param a Node
|
||||
---@param b Node
|
||||
---@return boolean true if node a comes before node b
|
||||
function Node.comparator(a, b)
|
||||
if a:has_children() == b:has_children() then
|
||||
return string.lower(a.name) < string.lower(b.name)
|
||||
else
|
||||
return a:has_children()
|
||||
end
|
||||
end
|
||||
|
||||
function Node:sort()
|
||||
for _, child in ipairs(self.children) do
|
||||
child:sort()
|
||||
end
|
||||
|
||||
utils.merge_sort(self.children, Node.comparator)
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
function Node:is_root()
|
||||
return not self.parent
|
||||
end
|
||||
|
||||
---@param callback fun(node: Node, i: integer, parent: Node): boolean?
|
||||
function Node:some(callback)
|
||||
for i, child in ipairs(self.children) do
|
||||
if callback(child, i, self) then
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param callback fun(node: Node, i: integer, parent: Node): boolean?
|
||||
function Node:deep_some(callback)
|
||||
local function wrap(node, i, parent)
|
||||
if callback(node, i, parent) then
|
||||
return true
|
||||
else
|
||||
return node:some(wrap)
|
||||
end
|
||||
end
|
||||
|
||||
self:some(wrap)
|
||||
end
|
||||
|
||||
---@return Node[]
|
||||
function Node:leaves()
|
||||
local leaves = {}
|
||||
|
||||
self:deep_some(function(node)
|
||||
if #node.children == 0 then
|
||||
leaves[#leaves + 1] = node
|
||||
end
|
||||
return false
|
||||
end)
|
||||
|
||||
return leaves
|
||||
end
|
||||
|
||||
---@return Node?
|
||||
function Node:first_leaf()
|
||||
if #self.children == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local cur = self
|
||||
|
||||
while cur:has_children() do
|
||||
cur = cur.children[1]
|
||||
end
|
||||
|
||||
return cur
|
||||
end
|
||||
|
||||
---@return Node?
|
||||
function Node:last_leaf()
|
||||
if #self.children == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local cur = self
|
||||
|
||||
while cur:has_children() do
|
||||
cur = cur.children[#cur.children]
|
||||
end
|
||||
|
||||
return cur
|
||||
end
|
||||
|
||||
---@return Node?
|
||||
function Node:next_leaf()
|
||||
if not self.parent then
|
||||
return
|
||||
end
|
||||
|
||||
local cur = self:has_children() and self:group_parent() or self
|
||||
local sibling = cur:next_sibling()
|
||||
|
||||
if sibling then
|
||||
if not sibling:has_children() then
|
||||
return sibling
|
||||
else
|
||||
return sibling:first_leaf()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@return Node?
|
||||
function Node:prev_leaf()
|
||||
if not self.parent then
|
||||
return
|
||||
end
|
||||
|
||||
local cur = self:has_children() and self:group_parent() or self
|
||||
local sibling = cur:prev_sibling()
|
||||
|
||||
if sibling then
|
||||
if not sibling:has_children() then
|
||||
return sibling
|
||||
else
|
||||
return sibling:last_leaf()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@return Node?
|
||||
function Node:next_sibling()
|
||||
if not self.parent then
|
||||
return
|
||||
end
|
||||
|
||||
local i = utils.vec_indexof(self.parent.children, self)
|
||||
|
||||
if i > -1 and i < #self.parent.children then
|
||||
return self.parent.children[i + 1]
|
||||
end
|
||||
end
|
||||
|
||||
---@return Node?
|
||||
function Node:prev_sibling()
|
||||
if not self.parent then
|
||||
return
|
||||
end
|
||||
|
||||
local i = utils.vec_indexof(self.parent.children, self)
|
||||
|
||||
if i > 1 and #self.parent.children > 1 then
|
||||
return self.parent.children[i - 1]
|
||||
end
|
||||
end
|
||||
|
||||
---Get the closest parent that has more than one child, or is a child of the
|
||||
---root node.
|
||||
---@return Node?
|
||||
function Node:group_parent()
|
||||
if self:is_root() then
|
||||
return
|
||||
end
|
||||
|
||||
local cur = self:has_children() and self or self.parent
|
||||
|
||||
while not cur.parent:is_root() and #cur.parent.children == 1 do
|
||||
cur = cur.parent
|
||||
end
|
||||
|
||||
return cur
|
||||
end
|
||||
|
||||
M.Node = Node
|
||||
|
||||
return M
|
||||
@ -0,0 +1,566 @@
|
||||
local EventEmitter = require("diffview.events").EventEmitter
|
||||
local File = require("diffview.vcs.file").File
|
||||
local PerfTimer = require("diffview.perf").PerfTimer
|
||||
local oop = require("diffview.oop")
|
||||
local renderer = require("diffview.renderer")
|
||||
local utils = require("diffview.utils")
|
||||
|
||||
local api = vim.api
|
||||
local logger = DiffviewGlobal.logger
|
||||
local pl = utils.path
|
||||
|
||||
local M = {}
|
||||
|
||||
local uid_counter = 0
|
||||
|
||||
---@alias PanelConfig PanelFloatSpec|PanelSplitSpec
|
||||
---@alias PanelType "split"|"float"
|
||||
|
||||
---@type PerfTimer
|
||||
local perf = PerfTimer("[Panel] redraw")
|
||||
|
||||
---@class Panel : diffview.Object
|
||||
---@field type PanelType
|
||||
---@field config_producer PanelConfig|fun(): PanelConfig
|
||||
---@field state table
|
||||
---@field bufid integer
|
||||
---@field winid integer
|
||||
---@field render_data RenderData
|
||||
---@field components any
|
||||
---@field bufname string
|
||||
---@field au_event_map table<string, function[]>
|
||||
---@field init_buffer_opts function Abstract
|
||||
---@field update_components function Abstract
|
||||
---@field render function Abstract
|
||||
local Panel = oop.create_class("Panel")
|
||||
|
||||
Panel.winopts = {
|
||||
relativenumber = false,
|
||||
number = false,
|
||||
list = false,
|
||||
winfixwidth = true,
|
||||
winfixheight = true,
|
||||
foldenable = false,
|
||||
spell = false,
|
||||
wrap = false,
|
||||
signcolumn = "yes",
|
||||
colorcolumn = "",
|
||||
foldmethod = "manual",
|
||||
foldcolumn = "0",
|
||||
scrollbind = false,
|
||||
cursorbind = false,
|
||||
diff = false,
|
||||
}
|
||||
|
||||
Panel.bufopts = {
|
||||
swapfile = false,
|
||||
buftype = "nofile",
|
||||
modifiable = false,
|
||||
bufhidden = "hide",
|
||||
modeline = false,
|
||||
undolevels = -1,
|
||||
}
|
||||
|
||||
Panel.default_type = "split"
|
||||
|
||||
---@class PanelSplitSpec
|
||||
---@field type "split"
|
||||
---@field position "left"|"top"|"right"|"bottom"
|
||||
---@field relative "editor"|"win"
|
||||
---@field win integer
|
||||
---@field width? integer
|
||||
---@field height? integer
|
||||
---@field win_opts WindowOptions
|
||||
|
||||
---@type PanelSplitSpec
|
||||
Panel.default_config_split = {
|
||||
type = "split",
|
||||
position = "left",
|
||||
relative = "editor",
|
||||
win = 0,
|
||||
win_opts = {}
|
||||
}
|
||||
|
||||
---@class PanelFloatSpec
|
||||
---@field type "float"
|
||||
---@field relative "editor"|"win"|"cursor"
|
||||
---@field win integer
|
||||
---@field anchor "NW"|"NE"|"SW"|"SE"
|
||||
---@field width integer
|
||||
---@field height integer
|
||||
---@field row number
|
||||
---@field col number
|
||||
---@field zindex integer
|
||||
---@field style "minimal"
|
||||
---@field border "none"|"single"|"double"|"rounded"|"solid"|"shadow"|string[]
|
||||
---@field win_opts WindowOptions
|
||||
|
||||
---@type PanelFloatSpec
|
||||
Panel.default_config_float = {
|
||||
type = "float",
|
||||
relative = "editor",
|
||||
row = 0,
|
||||
col = 0,
|
||||
zindex = 50,
|
||||
style = "minimal",
|
||||
border = "single",
|
||||
win_opts = {}
|
||||
}
|
||||
|
||||
Panel.au = {
|
||||
---@type integer
|
||||
group = api.nvim_create_augroup("diffview_panels", {}),
|
||||
---@type EventEmitter
|
||||
emitter = EventEmitter(),
|
||||
---@type table<string, integer> Map of autocmd event names to its created autocmd ID.
|
||||
events = {},
|
||||
---Delete all autocmds with no subscribed listeners.
|
||||
prune = function()
|
||||
for event, id in pairs(Panel.au.events) do
|
||||
if #(Panel.au.emitter:get(event) or {}) == 0 then
|
||||
api.nvim_del_autocmd(id)
|
||||
Panel.au.events[event] = nil
|
||||
end
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
---@class PanelSpec
|
||||
---@field type PanelType
|
||||
---@field config PanelConfig|fun(): PanelConfig
|
||||
---@field bufname string
|
||||
|
||||
---@param opt PanelSpec
|
||||
function Panel:init(opt)
|
||||
self.config_producer = opt.config or {}
|
||||
self.state = {}
|
||||
self.bufname = opt.bufname or "DiffviewPanel"
|
||||
self.au_event_map = {}
|
||||
end
|
||||
|
||||
---Produce and validate config.
|
||||
---@return PanelConfig
|
||||
function Panel:get_config()
|
||||
local config
|
||||
|
||||
if vim.is_callable(self.config_producer) then
|
||||
config = self.config_producer()
|
||||
elseif type(self.config_producer) == "table" then
|
||||
config = utils.tbl_deep_clone(self.config_producer)
|
||||
end
|
||||
|
||||
---@cast config table
|
||||
|
||||
local default_config = self:get_default_config(config.type)
|
||||
config = vim.tbl_deep_extend("force", default_config, config or {}) --[[@as table ]]
|
||||
|
||||
local function valid_enum(arg, values, optional)
|
||||
return {
|
||||
arg,
|
||||
function(v) return (optional and v == nil) or vim.tbl_contains(values, v) end,
|
||||
table.concat(vim.tbl_map(function(v) return ([['%s']]):format(v) end, values), "|"),
|
||||
}
|
||||
end
|
||||
|
||||
vim.validate({ type = valid_enum(config.type, { "split", "float" }) })
|
||||
|
||||
if config.type == "split" then
|
||||
---@cast config PanelSplitSpec
|
||||
self.state.form = vim.tbl_contains({ "top", "bottom" }, config.position) and "row" or "column"
|
||||
|
||||
vim.validate({
|
||||
position = valid_enum(config.position, { "left", "top", "right", "bottom" }),
|
||||
relative = valid_enum(config.relative, { "editor", "win" }),
|
||||
width = { config.width, "number", true },
|
||||
height = { config.height, "number", true },
|
||||
win_opts = { config.win_opts, "table" }
|
||||
})
|
||||
else
|
||||
---@cast config PanelFloatSpec
|
||||
local border = { "none", "single", "double", "rounded", "solid", "shadow" }
|
||||
|
||||
vim.validate({
|
||||
relative = valid_enum(config.relative, { "editor", "win", "cursor" }),
|
||||
win = { config.win, "n", true },
|
||||
anchor = valid_enum(config.anchor, { "NW", "NE", "SW", "SE" }, true),
|
||||
width = { config.width, "n", false },
|
||||
height = { config.height, "n", false },
|
||||
row = { config.row, "n", false },
|
||||
col = { config.col, "n", false },
|
||||
zindex = { config.zindex, "n", true },
|
||||
style = valid_enum(config.style, { "minimal" }, true),
|
||||
win_opts = { config.win_opts, "table" },
|
||||
border = {
|
||||
config.border,
|
||||
function(v)
|
||||
if v == nil then return true end
|
||||
|
||||
if type(v) == "table" then
|
||||
return #v >= 2
|
||||
end
|
||||
|
||||
return vim.tbl_contains(border, v)
|
||||
end,
|
||||
("%s or a list of length >=2"):format(
|
||||
table.concat(vim.tbl_map(function(v)
|
||||
return ([['%s']]):format(v)
|
||||
end, border), "|")
|
||||
)
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
return config
|
||||
end
|
||||
|
||||
---@param tabpage? integer
|
||||
---@return boolean
|
||||
function Panel:is_open(tabpage)
|
||||
local valid = self.winid and api.nvim_win_is_valid(self.winid)
|
||||
if not valid then
|
||||
self.winid = nil
|
||||
elseif tabpage then
|
||||
return vim.tbl_contains(api.nvim_tabpage_list_wins(tabpage), self.winid)
|
||||
end
|
||||
return valid
|
||||
end
|
||||
|
||||
function Panel:is_focused()
|
||||
return self:is_open() and api.nvim_get_current_win() == self.winid
|
||||
end
|
||||
|
||||
---@param no_open? boolean Don't open the panel if it's closed.
|
||||
function Panel:focus(no_open)
|
||||
if self:is_open() then
|
||||
api.nvim_set_current_win(self.winid)
|
||||
elseif not no_open then
|
||||
self:open()
|
||||
api.nvim_set_current_win(self.winid)
|
||||
end
|
||||
end
|
||||
|
||||
function Panel:resize()
|
||||
if not self:is_open(0) then
|
||||
return
|
||||
end
|
||||
|
||||
local config = self:get_config()
|
||||
|
||||
if config.type == "split" then
|
||||
if self.state.form == "column" and config.width then
|
||||
api.nvim_win_set_width(self.winid, config.width)
|
||||
elseif self.state.form == "row" and config.height then
|
||||
api.nvim_win_set_height(self.winid, config.height)
|
||||
end
|
||||
elseif config.type == "float" then
|
||||
api.nvim_win_set_width(self.winid, config.width)
|
||||
api.nvim_win_set_height(self.winid, config.height)
|
||||
end
|
||||
end
|
||||
|
||||
function Panel:open()
|
||||
if not self:buf_loaded() then
|
||||
self:init_buffer()
|
||||
end
|
||||
if self:is_open() then
|
||||
return
|
||||
end
|
||||
|
||||
local config = self:get_config()
|
||||
|
||||
if config.type == "split" then
|
||||
local split_dir = vim.tbl_contains({ "top", "left" }, config.position) and "aboveleft" or "belowright"
|
||||
local split_cmd = self.state.form == "row" and "sp" or "vsp"
|
||||
local rel_winid = config.relative == "win"
|
||||
and api.nvim_win_is_valid(config.win or -1)
|
||||
and config.win
|
||||
or 0
|
||||
|
||||
api.nvim_win_call(rel_winid, function()
|
||||
vim.cmd(split_dir .. " " .. split_cmd)
|
||||
self.winid = api.nvim_get_current_win()
|
||||
api.nvim_win_set_buf(self.winid, self.bufid)
|
||||
|
||||
if config.relative == "editor" then
|
||||
local dir = ({ left = "H", bottom = "J", top = "K", right = "L" })[config.position]
|
||||
vim.cmd("wincmd " .. dir)
|
||||
vim.cmd("wincmd =")
|
||||
end
|
||||
end)
|
||||
|
||||
elseif config.type == "float" then
|
||||
self.winid = vim.api.nvim_open_win(self.bufid, false, utils.sanitize_float_config(config))
|
||||
if self.winid == 0 then
|
||||
self.winid = nil
|
||||
error("[diffview.nvim] Failed to open float panel window!")
|
||||
end
|
||||
end
|
||||
|
||||
self:resize()
|
||||
utils.set_local(self.winid, self.class.winopts)
|
||||
utils.set_local(self.winid, config.win_opts)
|
||||
end
|
||||
|
||||
function Panel:close()
|
||||
if self:is_open() then
|
||||
local num_wins = api.nvim_tabpage_list_wins(api.nvim_win_get_tabpage(self.winid))
|
||||
|
||||
if #num_wins == 1 then
|
||||
-- Ensure that the tabpage doesn't close if the panel is the last window.
|
||||
api.nvim_win_call(self.winid, function()
|
||||
vim.cmd("sp")
|
||||
File.load_null_buffer(0)
|
||||
end)
|
||||
elseif self:is_focused() then
|
||||
vim.cmd("wincmd p")
|
||||
end
|
||||
|
||||
pcall(api.nvim_win_close, self.winid, true)
|
||||
end
|
||||
end
|
||||
|
||||
function Panel:destroy()
|
||||
self:close()
|
||||
if self:buf_loaded() then
|
||||
api.nvim_buf_delete(self.bufid, { force = true })
|
||||
end
|
||||
|
||||
-- Disable autocmd listeners
|
||||
for _, cbs in pairs(self.au_event_map) do
|
||||
for _, cb in ipairs(cbs) do
|
||||
Panel.au.emitter:off(cb)
|
||||
end
|
||||
end
|
||||
Panel.au.prune()
|
||||
end
|
||||
|
||||
---@param focus? boolean Focus the panel if it's opened.
|
||||
function Panel:toggle(focus)
|
||||
if self:is_open() then
|
||||
self:close()
|
||||
elseif focus then
|
||||
self:focus()
|
||||
else
|
||||
self:open()
|
||||
end
|
||||
end
|
||||
|
||||
function Panel:buf_loaded()
|
||||
return self.bufid and api.nvim_buf_is_loaded(self.bufid)
|
||||
end
|
||||
|
||||
function Panel:init_buffer()
|
||||
local bn = api.nvim_create_buf(false, false)
|
||||
|
||||
for k, v in pairs(self.class.bufopts) do
|
||||
api.nvim_buf_set_option(bn, k, v)
|
||||
end
|
||||
|
||||
local bufname
|
||||
if pl:is_abs(self.bufname) or pl:is_uri(self.bufname) then
|
||||
bufname = self.bufname
|
||||
else
|
||||
bufname = string.format("diffview:///panels/%d/%s", Panel.next_uid(), self.bufname)
|
||||
end
|
||||
|
||||
local ok = pcall(api.nvim_buf_set_name, bn, bufname)
|
||||
if not ok then
|
||||
utils.wipe_named_buffer(bufname)
|
||||
api.nvim_buf_set_name(bn, bufname)
|
||||
end
|
||||
|
||||
self.bufid = bn
|
||||
self.render_data = renderer.RenderData(bufname)
|
||||
|
||||
api.nvim_buf_call(self.bufid, function()
|
||||
vim.api.nvim_exec_autocmds({ "BufNew", "BufFilePre" }, {
|
||||
group = Panel.au.group,
|
||||
buffer = self.bufid,
|
||||
modeline = false,
|
||||
})
|
||||
end)
|
||||
|
||||
self:update_components()
|
||||
self:render()
|
||||
self:redraw()
|
||||
|
||||
return bn
|
||||
end
|
||||
|
||||
function Panel:update_components() oop.abstract_stub() end
|
||||
|
||||
function Panel:render() oop.abstract_stub() end
|
||||
|
||||
function Panel:redraw()
|
||||
if not self.render_data then
|
||||
return
|
||||
end
|
||||
perf:reset()
|
||||
renderer.render(self.bufid, self.render_data)
|
||||
perf:time()
|
||||
logger:lvl(10):debug(perf)
|
||||
end
|
||||
|
||||
---Update components, render and redraw.
|
||||
function Panel:sync()
|
||||
if self:buf_loaded() then
|
||||
self:update_components()
|
||||
self:render()
|
||||
self:redraw()
|
||||
end
|
||||
end
|
||||
|
||||
---@class PanelAutocmdSpec
|
||||
---@field callback function
|
||||
---@field once? boolean
|
||||
|
||||
---@param event string|string[]
|
||||
---@param opts PanelAutocmdSpec
|
||||
function Panel:on_autocmd(event, opts)
|
||||
if type(event) ~= "table" then
|
||||
event = { event }
|
||||
end
|
||||
|
||||
local callback = function(_, state)
|
||||
local win_match, buf_match
|
||||
if state.event:match("^Win") then
|
||||
if vim.tbl_contains({ "WinLeave", "WinEnter" }, state.event)
|
||||
and api.nvim_get_current_win() == self.winid
|
||||
then
|
||||
buf_match = state.buf
|
||||
else
|
||||
win_match = tonumber(state.match)
|
||||
end
|
||||
elseif state.event:match("^Buf") then
|
||||
buf_match = state.buf
|
||||
end
|
||||
|
||||
if (win_match and win_match == self.winid)
|
||||
or (buf_match and buf_match == self.bufid) then
|
||||
opts.callback(state)
|
||||
end
|
||||
end
|
||||
|
||||
for _, e in ipairs(event) do
|
||||
if not self.au_event_map[e] then
|
||||
self.au_event_map[e] = {}
|
||||
end
|
||||
table.insert(self.au_event_map[e], callback)
|
||||
|
||||
if not Panel.au.events[e] then
|
||||
Panel.au.events[e] = api.nvim_create_autocmd(e, {
|
||||
group = Panel.au.group,
|
||||
callback = function(state)
|
||||
Panel.au.emitter:emit(e, state)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
if opts.once then
|
||||
Panel.au.emitter:once(e, callback)
|
||||
else
|
||||
Panel.au.emitter:on(e, callback)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Unsubscribe an autocmd listener. If no event is given, the callback is
|
||||
---disabled for all events.
|
||||
---@param callback function
|
||||
---@param event? string
|
||||
function Panel:off_autocmd(callback, event)
|
||||
for e, cbs in pairs(self.au_event_map) do
|
||||
if (event == nil or event == e) and utils.vec_indexof(cbs, callback) ~= -1 then
|
||||
Panel.au.emitter:off(callback, event)
|
||||
end
|
||||
Panel.au.prune()
|
||||
end
|
||||
end
|
||||
|
||||
function Panel:get_default_config(panel_type)
|
||||
local producer = self.class["default_config_" .. (panel_type or self.class.default_type)]
|
||||
|
||||
local config
|
||||
if vim.is_callable(producer) then
|
||||
config = producer()
|
||||
elseif type(producer) == "table" then
|
||||
config = producer
|
||||
end
|
||||
|
||||
return config
|
||||
end
|
||||
|
||||
---@return integer?
|
||||
function Panel:get_width()
|
||||
if self:is_open() then
|
||||
return api.nvim_win_get_width(self.winid)
|
||||
end
|
||||
end
|
||||
|
||||
---@return integer?
|
||||
function Panel:get_height()
|
||||
if self:is_open() then
|
||||
return api.nvim_win_get_height(self.winid)
|
||||
end
|
||||
end
|
||||
|
||||
function Panel:infer_width()
|
||||
local cur_width = self:get_width()
|
||||
if cur_width then return cur_width end
|
||||
|
||||
local config = self:get_config()
|
||||
if config.width then return config.width end
|
||||
|
||||
-- PanelFloatSpec requires both width and height to be defined. If we get
|
||||
-- here then the panel is a split.
|
||||
---@cast config PanelSplitSpec
|
||||
|
||||
if config.win and api.nvim_win_is_valid(config.win) then
|
||||
if self.state.form == "row" then
|
||||
return api.nvim_win_get_width(config.win)
|
||||
elseif self.state.form == "column" then
|
||||
return math.floor(api.nvim_win_get_width(config.win) / 2)
|
||||
end
|
||||
end
|
||||
|
||||
if self.state.form == "row" then
|
||||
return vim.o.columns
|
||||
end
|
||||
|
||||
return math.floor(vim.o.columns / 2)
|
||||
end
|
||||
|
||||
function Panel:infer_height()
|
||||
local cur_height = self:get_height()
|
||||
if cur_height then return cur_height end
|
||||
|
||||
local config = self:get_config()
|
||||
if config.height then return config.height end
|
||||
|
||||
-- PanelFloatSpec requires both width and height to be defined. If we get
|
||||
-- here then the panel is a split.
|
||||
---@cast config PanelSplitSpec
|
||||
|
||||
if config.win and api.nvim_win_is_valid(config.win) then
|
||||
if self.state.form == "row" then
|
||||
return math.floor(api.nvim_win_get_height(config.win) / 2)
|
||||
elseif self.state.form == "column" then
|
||||
return api.nvim_win_get_height(config.win)
|
||||
end
|
||||
end
|
||||
|
||||
if self.state.form == "row" then
|
||||
return math.floor(vim.o.lines / 2)
|
||||
end
|
||||
|
||||
return vim.o.lines
|
||||
end
|
||||
|
||||
function Panel.next_uid()
|
||||
local uid = uid_counter
|
||||
uid_counter = uid_counter + 1
|
||||
return uid
|
||||
end
|
||||
|
||||
M.Panel = Panel
|
||||
return M
|
||||
@ -0,0 +1,121 @@
|
||||
local Job = require("diffview.job").Job
|
||||
local Panel = require("diffview.ui.panel").Panel
|
||||
local async = require("diffview.async")
|
||||
local get_user_config = require("diffview.config").get_config
|
||||
local oop = require("diffview.oop")
|
||||
local utils = require("diffview.utils")
|
||||
|
||||
local await = async.await
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class CommitLogPanel : Panel
|
||||
---@field adapter VCSAdapter
|
||||
---@field args string[]
|
||||
---@field job_out string[]
|
||||
local CommitLogPanel = oop.create_class("CommitLogPanel", Panel)
|
||||
|
||||
CommitLogPanel.winopts = vim.tbl_extend("force", Panel.winopts, {
|
||||
wrap = true,
|
||||
breakindent = true,
|
||||
})
|
||||
|
||||
CommitLogPanel.bufopts = vim.tbl_extend("force", Panel.bufopts, {
|
||||
buftype = "nowrite",
|
||||
filetype = "git",
|
||||
})
|
||||
|
||||
CommitLogPanel.default_type = "float"
|
||||
|
||||
CommitLogPanel.default_config_split = vim.tbl_extend("force", Panel.default_config_split, {
|
||||
position = "bottom",
|
||||
height = 14,
|
||||
})
|
||||
|
||||
CommitLogPanel.default_config_float = function()
|
||||
local c = vim.deepcopy(Panel.default_config_float)
|
||||
local viewport_width = vim.o.columns
|
||||
local viewport_height = vim.o.lines
|
||||
c.width = math.min(100, viewport_width)
|
||||
c.height = math.min(24, viewport_height)
|
||||
c.col = math.floor(viewport_width * 0.5 - c.width * 0.5)
|
||||
c.row = math.floor(viewport_height * 0.5 - c.height * 0.5)
|
||||
|
||||
return c
|
||||
end
|
||||
|
||||
---@class CommitLogPanelSpec
|
||||
---@field config PanelConfig
|
||||
---@field args string[]
|
||||
---@field name string
|
||||
|
||||
---@param adapter VCSAdapter
|
||||
---@param opt CommitLogPanelSpec
|
||||
function CommitLogPanel:init(adapter, opt)
|
||||
self:super({
|
||||
bufname = opt.name,
|
||||
config = opt.config or get_user_config().commit_log_panel.win_config,
|
||||
})
|
||||
|
||||
self.adapter = adapter
|
||||
self.args = opt.args or { "-n256" }
|
||||
|
||||
self:on_autocmd("BufWinEnter" , {
|
||||
callback = function()
|
||||
vim.bo[self.bufid].bufhidden = "wipe"
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
---@param self CommitLogPanel
|
||||
---@param args string|string[]
|
||||
CommitLogPanel.update = async.void(function(self, args)
|
||||
if type(args) ~= "table" then
|
||||
args = { args }
|
||||
end
|
||||
|
||||
local job = Job({
|
||||
command = self.adapter:bin(),
|
||||
args = self.adapter:get_log_args(args or self.args),
|
||||
cwd = self.adapter.ctx.toplevel,
|
||||
})
|
||||
|
||||
local ok = await(job)
|
||||
await(async.scheduler())
|
||||
|
||||
if not ok then
|
||||
utils.err("Failed to open log!")
|
||||
return
|
||||
end
|
||||
|
||||
self.job_out = utils.vec_slice(job.stdout)
|
||||
|
||||
if not next(self.job_out) then
|
||||
utils.info("No log content available for these changes.")
|
||||
return
|
||||
end
|
||||
|
||||
if not self:is_open() then
|
||||
self:init_buffer()
|
||||
else
|
||||
self:render()
|
||||
self:redraw()
|
||||
end
|
||||
|
||||
self:focus()
|
||||
vim.cmd("norm! gg")
|
||||
end)
|
||||
|
||||
function CommitLogPanel:update_components()
|
||||
end
|
||||
|
||||
function CommitLogPanel:render()
|
||||
self.render_data:clear()
|
||||
|
||||
if self.job_out then
|
||||
self.render_data.lines = utils.vec_slice(self.job_out)
|
||||
end
|
||||
end
|
||||
|
||||
M.CommitLogPanel = CommitLogPanel
|
||||
return M
|
||||
@ -0,0 +1,231 @@
|
||||
local Panel = require("diffview.ui.panel").Panel
|
||||
local get_user_config = require("diffview.config").get_config
|
||||
local oop = require("diffview.oop")
|
||||
local utils = require("diffview.utils")
|
||||
|
||||
local api = vim.api
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class HelpPanel : Panel
|
||||
---@field parent StandardView
|
||||
---@field keymap_groups string[]
|
||||
---@field state table
|
||||
local HelpPanel = oop.create_class("HelpPanel", Panel)
|
||||
|
||||
HelpPanel.winopts = vim.tbl_extend("force", Panel.winopts, {
|
||||
wrap = false,
|
||||
breakindent = true,
|
||||
signcolumn = "no",
|
||||
})
|
||||
|
||||
HelpPanel.bufopts = vim.tbl_extend("force", Panel.bufopts, {
|
||||
buftype = "nofile",
|
||||
})
|
||||
|
||||
HelpPanel.default_type = "float"
|
||||
|
||||
---@class HelpPanelSpec
|
||||
---@field config PanelConfig
|
||||
---@field name string
|
||||
|
||||
---@param parent StandardView
|
||||
---@param keymap_groups string[]
|
||||
---@param opt HelpPanelSpec
|
||||
function HelpPanel:init(parent, keymap_groups, opt)
|
||||
opt = opt or {}
|
||||
self:super({
|
||||
bufname = opt.name,
|
||||
config = opt.config or function()
|
||||
local c = vim.deepcopy(Panel.default_config_float)
|
||||
local viewport_width = vim.o.columns
|
||||
local viewport_height = vim.o.lines
|
||||
c.col = math.floor(viewport_width * 0.5 - self.state.width * 0.5)
|
||||
c.row = math.floor(viewport_height * 0.5 - self.state.height * 0.5)
|
||||
c.width = self.state.width
|
||||
c.height = self.state.height
|
||||
|
||||
return c
|
||||
end,
|
||||
})
|
||||
|
||||
self.parent = parent
|
||||
self.keymap_groups = keymap_groups
|
||||
self.lines = {}
|
||||
self.state = {
|
||||
width = 50,
|
||||
height = 4,
|
||||
}
|
||||
|
||||
self:on_autocmd("BufWinEnter", {
|
||||
callback = function()
|
||||
vim.bo[self.bufid].bufhidden = "wipe"
|
||||
end,
|
||||
})
|
||||
|
||||
self:on_autocmd("WinLeave", {
|
||||
callback = function()
|
||||
self:close()
|
||||
end,
|
||||
})
|
||||
|
||||
parent.emitter:on("close", function(e)
|
||||
if self:is_focused() then
|
||||
self:close()
|
||||
e:stop_propagation()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function HelpPanel:apply_cmd()
|
||||
local row, _ = unpack(vim.api.nvim_win_get_cursor(0))
|
||||
local comp = self.components.comp:get_comp_on_line(row)
|
||||
|
||||
if comp then
|
||||
local mapping = comp.context.mapping
|
||||
local last_winid = vim.fn.win_getid(vim.fn.winnr("#"))
|
||||
|
||||
if mapping then
|
||||
api.nvim_win_call(last_winid, function()
|
||||
api.nvim_feedkeys(utils.t(mapping[2]), "m", false)
|
||||
end)
|
||||
|
||||
self:close()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function HelpPanel:init_buffer()
|
||||
HelpPanel.super_class.init_buffer(self)
|
||||
local conf = get_user_config().keymaps
|
||||
local default_opt = { silent = true, nowait = true, buffer = self.bufid }
|
||||
|
||||
for _, mapping in ipairs(conf.help_panel) do
|
||||
local map_opt = vim.tbl_extend("force", default_opt, mapping[4] or {}, { buffer = self.bufid })
|
||||
vim.keymap.set(mapping[1], mapping[2], mapping[3], map_opt)
|
||||
end
|
||||
|
||||
vim.keymap.set("n", "<cr>", function()
|
||||
self:apply_cmd()
|
||||
end, default_opt)
|
||||
end
|
||||
|
||||
function HelpPanel:update_components()
|
||||
local keymaps = get_user_config().keymaps
|
||||
local width = 50
|
||||
local height = 0
|
||||
local sections = { name = "sections" }
|
||||
|
||||
for _, group in ipairs(self.keymap_groups) do
|
||||
local maps = keymaps[group]
|
||||
|
||||
if not maps then
|
||||
utils.err(("help_panel :: Unknown keymap group '%s'!"):format(group))
|
||||
else
|
||||
maps = utils.tbl_fmap(maps, function(v)
|
||||
if v[1] ~= "n" then return nil end
|
||||
local desc = v[4] and v[4].desc
|
||||
|
||||
if not desc then
|
||||
if type(v[3]) == "string" then
|
||||
desc = v[3]
|
||||
elseif type(v[3]) == "function" then
|
||||
local info = debug.getinfo(v[3], "S")
|
||||
desc = ("<Lua @ %s:%d>"):format(info.short_src, info.linedefined)
|
||||
end
|
||||
end
|
||||
|
||||
local ret = utils.tbl_clone(v)
|
||||
ret[5] = desc
|
||||
|
||||
return ret
|
||||
end)
|
||||
|
||||
if #maps == 0 then goto continue end
|
||||
|
||||
-- Sort mappings by description
|
||||
table.sort(maps, function(a, b)
|
||||
a, b = a[5], b[5]
|
||||
-- Ensure lua functions are sorted last
|
||||
if a:match("^<Lua") then a = "~" .. a end
|
||||
if b:match("^<Lua") then b = "~" .. b end
|
||||
return a < b
|
||||
end)
|
||||
|
||||
local items = { name = "items" }
|
||||
local section_schema = {
|
||||
name = "section",
|
||||
{
|
||||
name = "section_heading",
|
||||
context = {
|
||||
label = group:upper():gsub("_", "-")
|
||||
},
|
||||
},
|
||||
items,
|
||||
}
|
||||
|
||||
for _, mapping in ipairs(maps) do
|
||||
local desc = mapping[5]
|
||||
|
||||
if desc ~= "diffview_ignore" then
|
||||
width = math.max(width, 14 + 4 + #mapping[5] + 2)
|
||||
table.insert(items, {
|
||||
name = "item",
|
||||
context = {
|
||||
label_lhs = ("%14s"):format(mapping[2]),
|
||||
label_rhs = desc,
|
||||
mapping = mapping,
|
||||
},
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
height = height + #items + 3
|
||||
table.insert(sections, section_schema)
|
||||
end
|
||||
|
||||
::continue::
|
||||
end
|
||||
|
||||
self.state.width = width
|
||||
self.state.height = height + 1
|
||||
self.components = self.render_data:create_component({
|
||||
{ name = "heading" },
|
||||
sections,
|
||||
})
|
||||
end
|
||||
|
||||
function HelpPanel:render()
|
||||
self.render_data:clear()
|
||||
|
||||
local s = ""
|
||||
|
||||
-- Heading
|
||||
local comp = self.components.heading.comp
|
||||
s = "Keymap Overview — <CR> To Use"
|
||||
s = string.rep(" ", math.floor(self.state.width * 0.5 - vim.str_utfindex(s) * 0.5)) .. s
|
||||
comp:add_line(s, "DiffviewFilePanelTitle")
|
||||
|
||||
for _, section in ipairs(self.components.sections) do
|
||||
---@cast section CompStruct
|
||||
|
||||
-- Section heading
|
||||
comp = section.section_heading.comp
|
||||
comp:add_line()
|
||||
s = string.rep(" ", math.floor(self.state.width * 0.5 - #comp.context.label * 0.5)) .. comp.context.label
|
||||
comp:add_line(s, "Statement")
|
||||
comp:add_line(("%14s CALLBACK"):format("KEYS"), "DiffviewFilePanelCounter")
|
||||
|
||||
for _, item in ipairs(section.items) do
|
||||
---@cast item CompStruct
|
||||
comp = item.comp
|
||||
comp:add_text(comp.context.label_lhs, "DiffviewSecondary")
|
||||
comp:add_text(" -> ", "DiffviewNonText")
|
||||
comp:add_text(comp.context.label_rhs)
|
||||
comp:ln()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
M.HelpPanel = HelpPanel
|
||||
return M
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,402 @@
|
||||
local async = require("diffview.async")
|
||||
local lazy = require("diffview.lazy")
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
local AsyncListStream = lazy.access("diffview.stream", "AsyncListStream") ---@type AsyncListStream|LazyModule
|
||||
local Job = lazy.access("diffview.job", "Job") ---@type diffview.Job|LazyModule
|
||||
local Rev = lazy.access("diffview.vcs.rev", "Rev") ---@type Rev|LazyModule
|
||||
local RevType = lazy.access("diffview.vcs.rev", "RevType") ---@type RevType|LazyModule
|
||||
local arg_parser = lazy.require("diffview.arg_parser") ---@module "diffview.arg_parser"
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
local vcs_utils = lazy.require("diffview.vcs.utils") ---@module "diffview.vcs.utils"
|
||||
|
||||
local await = async.await
|
||||
local logger = DiffviewGlobal.logger
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class vcs.adapter.LayoutOpt
|
||||
---@field default_layout Diff2
|
||||
---@field merge_layout Layout
|
||||
|
||||
---@class vcs.adapter.VCSAdapter.Bootstrap
|
||||
---@field done boolean # Did the bootstrapping
|
||||
---@field ok boolean # Bootstrapping was successful
|
||||
---@field version table
|
||||
---@field version_string string
|
||||
---@field target_version table
|
||||
---@field target_version_string string
|
||||
|
||||
---@class vcs.adapter.VCSAdapter.Flags
|
||||
---@field switches FlagOption[]
|
||||
---@field options FlagOption[]
|
||||
|
||||
---@class vcs.adapter.VCSAdapter.Ctx
|
||||
---@field toplevel string # VCS repository toplevel directory
|
||||
---@field dir string # VCS directory
|
||||
---@field path_args string[] # Resolved path arguments
|
||||
|
||||
---@class VCSAdapter: diffview.Object
|
||||
---@field bootstrap vcs.adapter.VCSAdapter.Bootstrap
|
||||
---@field ctx vcs.adapter.VCSAdapter.Ctx
|
||||
---@field flags vcs.adapter.VCSAdapter.Flags
|
||||
local VCSAdapter = oop.create_class("VCSAdapter")
|
||||
|
||||
VCSAdapter.Rev = Rev
|
||||
VCSAdapter.config_key = nil
|
||||
VCSAdapter.bootstrap = {
|
||||
done = false,
|
||||
ok = false,
|
||||
version = {},
|
||||
}
|
||||
|
||||
function VCSAdapter.run_bootstrap()
|
||||
VCSAdapter.bootstrap.done = true
|
||||
VCSAdapter.bootstrap.ok = false
|
||||
end
|
||||
|
||||
---@diagnostic disable: unused-local, missing-return
|
||||
|
||||
---@abstract
|
||||
---@param path_args string[] # Raw path args
|
||||
---@param cpath string? # Cwd path given by the `-C` flag option
|
||||
---@return string[] path_args # Resolved path args
|
||||
---@return string[] top_indicators # Top-level indicators
|
||||
function VCSAdapter.get_repo_paths(path_args, cpath) oop.abstract_stub() end
|
||||
|
||||
---Try to find the top-level of a working tree by using the given indicative
|
||||
---paths.
|
||||
---@abstract
|
||||
---@param top_indicators string[] A list of paths that might indicate what working tree we are in.
|
||||
---@return string? err
|
||||
---@return string toplevel # Absolute path
|
||||
function VCSAdapter.find_toplevel(top_indicators) oop.abstract_stub() end
|
||||
|
||||
---@diagnostic enable: unused-local, missing-return
|
||||
|
||||
---@class vcs.adapter.VCSAdapter.Opt
|
||||
---@field cpath string? # CWD path
|
||||
---@field toplevel string # VCS toplevel path
|
||||
---@field path_args string[] # Extra path arguments
|
||||
|
||||
function VCSAdapter:init()
|
||||
self.ctx = {}
|
||||
self.comp = {
|
||||
file_history = arg_parser.FlagValueMap(),
|
||||
open = arg_parser.FlagValueMap(),
|
||||
}
|
||||
end
|
||||
|
||||
---@diagnostic disable: unused-local, missing-return
|
||||
|
||||
---@param path string
|
||||
---@param rev Rev
|
||||
---@return boolean -- True if the file was binary for the given rev, or it didn't exist.
|
||||
function VCSAdapter:is_binary(path, rev)
|
||||
oop.abstract_stub()
|
||||
end
|
||||
|
||||
---Initialize completion parameters
|
||||
function VCSAdapter:init_completion()
|
||||
oop.abstract_stub()
|
||||
end
|
||||
|
||||
---@class RevCompletionSpec
|
||||
---@field accept_range boolean
|
||||
|
||||
---Completion for revisions.
|
||||
---@param arg_lead string
|
||||
---@param opt? RevCompletionSpec
|
||||
---@return string[]
|
||||
function VCSAdapter:rev_candidates(arg_lead, opt)
|
||||
oop.abstract_stub()
|
||||
end
|
||||
|
||||
---@return Rev?
|
||||
function VCSAdapter:head_rev()
|
||||
oop.abstract_stub()
|
||||
end
|
||||
|
||||
---Get the hash for a file's blob in a given rev.
|
||||
---@param path string
|
||||
---@param rev_arg string?
|
||||
---@return string?
|
||||
function VCSAdapter:file_blob_hash(path, rev_arg)
|
||||
oop.abstract_stub()
|
||||
end
|
||||
|
||||
---@return string[] # path to binary for VCS command
|
||||
function VCSAdapter:get_command()
|
||||
oop.abstract_stub()
|
||||
end
|
||||
|
||||
---@diagnostic enable: unused-local, missing-return
|
||||
|
||||
---@return string cmd The VCS binary.
|
||||
function VCSAdapter:bin()
|
||||
return self:get_command()[1]
|
||||
end
|
||||
|
||||
---@return string[] args The default VCS args.
|
||||
function VCSAdapter:args()
|
||||
return utils.vec_slice(self:get_command(), 2)
|
||||
end
|
||||
|
||||
|
||||
---Execute a VCS command synchronously.
|
||||
---@param args string[]
|
||||
---@param cwd_or_opt? string|utils.job.Opt
|
||||
---@return string[] stdout
|
||||
---@return integer code
|
||||
---@return string[] stderr
|
||||
---@overload fun(self: VCSAdapter, args: string[], cwd?: string)
|
||||
---@overload fun(self: VCSAdapter, args: string[], opt?: utils.job.Opt)
|
||||
function VCSAdapter:exec_sync(args, cwd_or_opt)
|
||||
if not self.class.bootstrap.done then self.class.run_bootstrap() end
|
||||
|
||||
local cmd = utils.flatten({ self:get_command(), args })
|
||||
|
||||
if not self.class.bootstrap.ok then
|
||||
logger:error(
|
||||
("[VCSAdapter] Can't exec adapter command because bootstrap failed! Cmd: %s")
|
||||
:format(table.concat(cmd, " "))
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
return utils.job(cmd, cwd_or_opt)
|
||||
end
|
||||
|
||||
|
||||
---@param thread thread
|
||||
---@param ok boolean
|
||||
---@param result any
|
||||
---@return boolean ok
|
||||
---@return any result
|
||||
function VCSAdapter:handle_co(thread, ok, result)
|
||||
if not ok then
|
||||
local err_msg = utils.vec_join(
|
||||
"Coroutine failed!",
|
||||
debug.traceback(thread, result, 1)
|
||||
)
|
||||
utils.err(err_msg, true)
|
||||
logger:error(table.concat(err_msg, "\n"))
|
||||
end
|
||||
return ok, result
|
||||
end
|
||||
|
||||
-- File History
|
||||
|
||||
---@diagnostic disable: unused-local, missing-return
|
||||
|
||||
---@param path string
|
||||
---@param rev Rev?
|
||||
---@return string[] args to show commit content
|
||||
function VCSAdapter:get_show_args(path, rev)
|
||||
oop.abstract_stub()
|
||||
end
|
||||
|
||||
---@param args string[]
|
||||
---@return string[] args to show commit log message
|
||||
function VCSAdapter:get_log_args(args)
|
||||
oop.abstract_stub()
|
||||
end
|
||||
|
||||
---@class vcs.MergeContext
|
||||
---@field ours { hash: string, ref_names: string? }
|
||||
---@field theirs { hash: string, ref_names: string? }
|
||||
---@field base { hash: string, ref_names: string? }
|
||||
|
||||
---@return vcs.MergeContext?
|
||||
function VCSAdapter:get_merge_context()
|
||||
oop.abstract_stub()
|
||||
end
|
||||
|
||||
---@param range? { [1]: integer, [2]: integer }
|
||||
---@param paths string[]
|
||||
---@param argo ArgObject
|
||||
---@return string[] # Options to show file history
|
||||
function VCSAdapter:file_history_options(range, paths, argo)
|
||||
oop.abstract_stub()
|
||||
end
|
||||
|
||||
---@param self VCSAdapter
|
||||
---@param out_stream AsyncListStream
|
||||
---@param opt vcs.adapter.FileHistoryWorkerSpec
|
||||
VCSAdapter.file_history_worker = async.void(function(self, out_stream, opt)
|
||||
oop.abstract_stub()
|
||||
end)
|
||||
|
||||
---@diagnostic enable: unused-local, missing-return
|
||||
|
||||
---@class vcs.adapter.FileHistoryWorkerSpec
|
||||
---@field log_opt ConfigLogOptions
|
||||
---@field layout_opt vcs.adapter.LayoutOpt
|
||||
|
||||
---@param opt vcs.adapter.FileHistoryWorkerSpec
|
||||
---@return AsyncListStream out_stream
|
||||
function VCSAdapter:file_history(opt)
|
||||
local out_stream = AsyncListStream()
|
||||
self:file_history_worker(out_stream, opt)
|
||||
|
||||
return out_stream
|
||||
end
|
||||
|
||||
-- Diff View
|
||||
|
||||
---@diagnostic disable: unused-local, missing-return
|
||||
|
||||
---Convert revs to rev args.
|
||||
---@param left Rev
|
||||
---@param right Rev
|
||||
---@return string[]
|
||||
function VCSAdapter:rev_to_args(left, right)
|
||||
oop.abstract_stub()
|
||||
end
|
||||
|
||||
---Restore a file to the requested state
|
||||
---@param path string # file to restore
|
||||
---@param kind '"staged"'|'"working"'
|
||||
---@param commit string
|
||||
---@return string? Command to undo the restore
|
||||
function VCSAdapter:restore_file(path, kind, commit)
|
||||
oop.abstract_stub()
|
||||
end
|
||||
|
||||
---Add file(s)
|
||||
---@param paths string[]
|
||||
---@return boolean # add was successful
|
||||
function VCSAdapter:add_files(paths)
|
||||
oop.abstract_stub()
|
||||
end
|
||||
|
||||
---Reset file(s)
|
||||
---@param paths string[]?
|
||||
---@return boolean # reset was successful
|
||||
function VCSAdapter:reset_files(paths)
|
||||
oop.abstract_stub()
|
||||
end
|
||||
|
||||
---@param argo ArgObject
|
||||
---@return {left: string, right: string, options: string[]}
|
||||
function VCSAdapter:diffview_options(argo)
|
||||
oop.abstract_stub()
|
||||
end
|
||||
|
||||
---@class VCSAdapter.show_untracked.Opt
|
||||
---@field dv_opt? DiffViewOptions
|
||||
---@field revs? { left: Rev, right: Rev }
|
||||
|
||||
---Check whether untracked files should be listed.
|
||||
---@param opt? VCSAdapter.show_untracked.Opt
|
||||
---@return boolean
|
||||
function VCSAdapter:show_untracked(opt)
|
||||
oop.abstract_stub()
|
||||
end
|
||||
|
||||
---Restore file
|
||||
---@param self VCSAdapter
|
||||
---@param path string
|
||||
---@param kind vcs.FileKind
|
||||
---@param commit string?
|
||||
---@return boolean success
|
||||
---@return string? undo # If the adapter supports it: a command that will undo the restoration.
|
||||
VCSAdapter.file_restore = async.void(function(self, path, kind, commit)
|
||||
oop.abstract_stub()
|
||||
end)
|
||||
|
||||
---Update the index entry for a given file with the contents of an index buffer.
|
||||
---@param file vcs.File
|
||||
---@return boolean success
|
||||
function VCSAdapter:stage_index_file(file)
|
||||
oop.abstract_stub()
|
||||
end
|
||||
|
||||
---@param self VCSAdapter
|
||||
---@param left Rev
|
||||
---@param right Rev
|
||||
---@param args string[]
|
||||
---@param kind vcs.FileKind
|
||||
---@param opt vcs.adapter.LayoutOpt
|
||||
---@param callback function
|
||||
VCSAdapter.tracked_files = async.wrap(function(self, left, right, args, kind, opt, callback)
|
||||
oop.abstract_stub()
|
||||
end)
|
||||
|
||||
---@param self VCSAdapter
|
||||
---@param left Rev
|
||||
---@param right Rev
|
||||
---@param opt vcs.adapter.LayoutOpt
|
||||
---@param callback? function
|
||||
VCSAdapter.untracked_files = async.wrap(function(self, left, right, opt, callback)
|
||||
oop.abstract_stub()
|
||||
end)
|
||||
|
||||
---@diagnostic enable: unused-local, missing-return
|
||||
|
||||
---@param self VCSAdapter
|
||||
---@param path string
|
||||
---@param rev? Rev
|
||||
---@param callback fun(stderr: string[]?, stdout: string[]?)
|
||||
VCSAdapter.show = async.wrap(function(self, path, rev, callback)
|
||||
local job
|
||||
job = Job({
|
||||
command = self:bin(),
|
||||
args = self:get_show_args(path, rev),
|
||||
cwd = self.ctx.toplevel,
|
||||
retry = 2,
|
||||
fail_cond = Job.FAIL_COND.on_empty,
|
||||
log_opt = { label = "VCSAdapter:show()" },
|
||||
on_exit = async.void(function(_, ok, err)
|
||||
if not ok or job.code ~= 0 then
|
||||
callback(utils.vec_join(err, job.stderr), nil)
|
||||
return
|
||||
end
|
||||
|
||||
callback(nil, job.stdout)
|
||||
end),
|
||||
})
|
||||
-- Problem: Running multiple 'show' jobs simultaneously may cause them to fail
|
||||
-- silently.
|
||||
-- Solution: queue them and run them one after another.
|
||||
await(vcs_utils.queue_sync_job(job))
|
||||
end)
|
||||
|
||||
---Convert revs to string representation.
|
||||
---@param left Rev
|
||||
---@param right Rev
|
||||
---@return string|nil
|
||||
function VCSAdapter:rev_to_pretty_string(left, right)
|
||||
if left.track_head and right.type == RevType.LOCAL then
|
||||
return nil
|
||||
elseif left.commit and right.type == RevType.LOCAL then
|
||||
return left:abbrev()
|
||||
elseif left.commit and right.commit then
|
||||
return left:abbrev() .. ".." .. right:abbrev()
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---Check if any of the given revs are LOCAL.
|
||||
---@param left Rev
|
||||
---@param right Rev
|
||||
---@return boolean
|
||||
function VCSAdapter:has_local(left, right)
|
||||
return left.type == RevType.LOCAL or right.type == RevType.LOCAL
|
||||
end
|
||||
|
||||
VCSAdapter.flags = {
|
||||
---@type FlagOption[]
|
||||
switches = {},
|
||||
---@type FlagOption[]
|
||||
options = {},
|
||||
}
|
||||
|
||||
---@param arg_lead string
|
||||
---@return string[]
|
||||
function VCSAdapter:path_candidates(arg_lead)
|
||||
return vim.fn.getcompletion(arg_lead, "file", 0)
|
||||
end
|
||||
|
||||
M.VCSAdapter = VCSAdapter
|
||||
return M
|
||||
@ -0,0 +1,81 @@
|
||||
local lazy = require("diffview.lazy")
|
||||
local oop = require('diffview.oop')
|
||||
|
||||
local Commit = lazy.access("diffview.vcs.commit", "Commit") ---@type Commit|LazyModule
|
||||
local RevType = lazy.access("diffview.vcs.rev", "RevType") ---@type RevType|LazyModule
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
|
||||
local M = {}
|
||||
|
||||
|
||||
---@class GitCommit : Commit
|
||||
---@field reflog_selector? string
|
||||
local GitCommit = oop.create_class("GitCommit", Commit.__get())
|
||||
|
||||
function GitCommit:init(opt)
|
||||
self:super(opt)
|
||||
|
||||
self.reflog_selector = opt.reflog_selector ~= "" and opt.reflog_selector or nil
|
||||
|
||||
if opt.time_offset then
|
||||
self.time_offset = Commit.parse_time_offset(opt.time_offset)
|
||||
self.time = self.time - self.time_offset
|
||||
else
|
||||
self.time_offset = 0
|
||||
end
|
||||
|
||||
self.iso_date = Commit.time_to_iso(self.time, self.time_offset)
|
||||
end
|
||||
|
||||
---@param rev_arg string
|
||||
---@param adapter GitAdapter
|
||||
---@return GitCommit?
|
||||
function GitCommit.from_rev_arg(rev_arg, adapter)
|
||||
local out, code = adapter:exec_sync({
|
||||
"show",
|
||||
"--pretty=format:%H %P%n%an%n%ad%n%ar%n %s",
|
||||
"--date=raw",
|
||||
"--name-status",
|
||||
rev_arg,
|
||||
"--",
|
||||
}, adapter.ctx.toplevel)
|
||||
|
||||
if code ~= 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local right_hash, _, _ = unpack(utils.str_split(out[1]))
|
||||
local time, time_offset = unpack(utils.str_split(out[3]))
|
||||
|
||||
return GitCommit({
|
||||
hash = right_hash,
|
||||
author = out[2],
|
||||
time = tonumber(time),
|
||||
time_offset = time_offset,
|
||||
rel_date = out[4],
|
||||
subject = out[5]:sub(3),
|
||||
})
|
||||
end
|
||||
|
||||
---@param rev Rev
|
||||
---@param adapter GitAdapter
|
||||
---@return GitCommit?
|
||||
function GitCommit.from_rev(rev, adapter)
|
||||
assert(rev.type == RevType.COMMIT, "Rev must be of type COMMIT!")
|
||||
|
||||
return GitCommit.from_rev_arg(rev.commit, adapter)
|
||||
end
|
||||
|
||||
function GitCommit.parse_time_offset(iso_date)
|
||||
local sign, h, m = vim.trim(iso_date):match("([+-])(%d%d):?(%d%d)$")
|
||||
local offset = tonumber(h) * 60 * 60 + tonumber(m) * 60
|
||||
|
||||
if sign == "-" then
|
||||
offset = -offset
|
||||
end
|
||||
|
||||
return offset
|
||||
end
|
||||
|
||||
M.GitCommit = GitCommit
|
||||
return M
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,148 @@
|
||||
local oop = require("diffview.oop")
|
||||
local Rev = require('diffview.vcs.rev').Rev
|
||||
local RevType = require('diffview.vcs.rev').RevType
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class GitRev : Rev
|
||||
local GitRev = oop.create_class("GitRev", Rev)
|
||||
|
||||
-- The special SHA for git's empty tree.
|
||||
GitRev.NULL_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
|
||||
|
||||
---GitRev constructor
|
||||
---@param rev_type RevType
|
||||
---@param revision string|number Commit SHA or stage number.
|
||||
---@param track_head? boolean
|
||||
function GitRev:init(rev_type, revision, track_head)
|
||||
local t = type(revision)
|
||||
|
||||
assert(
|
||||
revision == nil or t == "string" or t == "number",
|
||||
"'revision' must be one of: nil, string, number!"
|
||||
)
|
||||
if t == "string" then
|
||||
assert(revision ~= "", "'revision' cannot be an empty string!")
|
||||
elseif t == "number" then
|
||||
assert(
|
||||
revision >= 0 and revision <= 3,
|
||||
"'revision' must be a valid stage number ([0-3])!"
|
||||
)
|
||||
end
|
||||
|
||||
t = type(track_head)
|
||||
assert(t == "boolean" or t == "nil", "'track_head' must be of type boolean!")
|
||||
|
||||
self.type = rev_type
|
||||
self.track_head = track_head or false
|
||||
|
||||
if type(revision) == "string" then
|
||||
---@cast revision string
|
||||
self.commit = revision
|
||||
elseif type(revision) == "number" then
|
||||
---@cast revision number
|
||||
self.stage = revision
|
||||
end
|
||||
|
||||
if self.type == RevType.STAGE and not self.stage then
|
||||
self.stage = 0
|
||||
end
|
||||
end
|
||||
|
||||
---@param rev_from GitRev|string
|
||||
---@param rev_to? GitRev|string
|
||||
---@return string?
|
||||
function GitRev.to_range(rev_from, rev_to)
|
||||
if type(rev_from) ~= "string" and rev_from.type ~= RevType.COMMIT then
|
||||
-- The range between either LOCAL or STAGE, and any other rev, will always
|
||||
-- be empty.
|
||||
return nil
|
||||
end
|
||||
|
||||
local name_from = type(rev_from) == "string" and rev_from or rev_from:object_name()
|
||||
local name_to
|
||||
|
||||
if rev_to then
|
||||
if type(rev_to) == "string" then
|
||||
name_to = rev_to
|
||||
else
|
||||
-- If the rev is either of type LOCAL or STAGE, just fall back to HEAD.
|
||||
name_to = rev_to.type == RevType.COMMIT and rev_to:object_name() or "HEAD"
|
||||
end
|
||||
end
|
||||
|
||||
if not name_to then
|
||||
return name_from .. "^!"
|
||||
else
|
||||
return name_from .. ".." .. name_to
|
||||
end
|
||||
end
|
||||
|
||||
---@param name string
|
||||
---@param adapter GitAdapter
|
||||
---@return Rev?
|
||||
function GitRev.from_name(name, adapter)
|
||||
local out, code = adapter:exec_sync({ "rev-parse", "--revs-only", name }, adapter.ctx.toplevel)
|
||||
|
||||
if code ~= 0 then
|
||||
return
|
||||
end
|
||||
|
||||
return GitRev(RevType.COMMIT, out[1]:gsub("^%^", ""))
|
||||
end
|
||||
|
||||
---@param adapter GitAdapter
|
||||
---@return Rev?
|
||||
function GitRev.earliest_commit(adapter)
|
||||
local out, code = adapter:exec_sync({
|
||||
"rev-list", "--max-parents=0", "--first-parent", "HEAD"
|
||||
}, adapter.ctx.toplevel)
|
||||
|
||||
if code ~= 0 then
|
||||
return
|
||||
end
|
||||
|
||||
return GitRev(RevType.COMMIT, ({ out[1]:gsub("^%^", "") })[1])
|
||||
end
|
||||
|
||||
---Create a new commit rev with the special empty tree SHA.
|
||||
---@return Rev
|
||||
function GitRev.new_null_tree()
|
||||
return GitRev(RevType.COMMIT, GitRev.NULL_TREE_SHA)
|
||||
end
|
||||
|
||||
---Determine if this rev is currently the head.
|
||||
---@param adapter GitAdapter
|
||||
---@return boolean?
|
||||
function GitRev:is_head(adapter)
|
||||
if self.type ~= RevType.COMMIT then
|
||||
return false
|
||||
end
|
||||
|
||||
local out, code = adapter:exec_sync({ "rev-parse", "HEAD", "--" }, adapter.ctx.toplevel)
|
||||
|
||||
if code ~= 0 or not (out[2] ~= nil or out[1] and out[1] ~= "") then
|
||||
return
|
||||
end
|
||||
|
||||
return self.commit == vim.trim(out[1]):gsub("^%^", "")
|
||||
end
|
||||
|
||||
---@param abbrev_len? integer
|
||||
---@return string
|
||||
function GitRev:object_name(abbrev_len)
|
||||
if self.type == RevType.COMMIT then
|
||||
if abbrev_len then
|
||||
return self.commit:sub(1, abbrev_len)
|
||||
end
|
||||
|
||||
return self.commit
|
||||
elseif self.type == RevType.STAGE then
|
||||
return ":" .. self.stage
|
||||
end
|
||||
|
||||
return "UNKNOWN"
|
||||
end
|
||||
|
||||
M.GitRev = GitRev
|
||||
return M
|
||||
@ -0,0 +1,42 @@
|
||||
local lazy = require("diffview.lazy")
|
||||
local oop = require('diffview.oop')
|
||||
local utils = require("diffview.utils")
|
||||
local Commit = require('diffview.vcs.commit').Commit
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class HgCommit : Commit
|
||||
local HgCommit = oop.create_class('HgCommit', Commit)
|
||||
|
||||
function HgCommit:init(opt)
|
||||
self:super(opt)
|
||||
|
||||
if opt.time_offset then
|
||||
self.time_offset = HgCommit.parse_time_offset(opt.time_offset)
|
||||
self.time = self.time - self.time_offset
|
||||
else
|
||||
self.time_offset = 0
|
||||
end
|
||||
|
||||
self.iso_date = Commit.time_to_iso(self.time, self.time_offset)
|
||||
end
|
||||
|
||||
---@param iso_date string?
|
||||
function HgCommit.parse_time_offset(iso_date)
|
||||
if not iso_date or iso_date == "" then
|
||||
return 0
|
||||
end
|
||||
|
||||
local sign, offset = vim.trim(iso_date):match("([+-])(%d+)")
|
||||
|
||||
offset = tonumber(offset)
|
||||
|
||||
if sign == "-" then
|
||||
offset = -offset
|
||||
end
|
||||
|
||||
return offset
|
||||
end
|
||||
|
||||
M.HgCommit = HgCommit
|
||||
return M
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,71 @@
|
||||
local oop = require("diffview.oop")
|
||||
local Rev = require('diffview.vcs.rev').Rev
|
||||
local RevType = require('diffview.vcs.rev').RevType
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class HgRev : Rev
|
||||
local HgRev = oop.create_class("HgRev", Rev)
|
||||
|
||||
HgRev.NULL_TREE_SHA = "0000000000000000000000000000000000000000"
|
||||
|
||||
function HgRev:init(rev_type, revision, track_head)
|
||||
local t = type(revision)
|
||||
|
||||
assert(
|
||||
revision == nil or t == "string" or t == "number",
|
||||
"'revision' must be one of: nil, string, number!"
|
||||
)
|
||||
if t == "string" then
|
||||
assert(revision ~= "", "'revision' cannot be an empty string!")
|
||||
end
|
||||
|
||||
t = type(track_head)
|
||||
assert(t == "boolean" or t == "nil", "'track_head' must be of type boolean!")
|
||||
|
||||
self.type = rev_type
|
||||
self.track_head = track_head or false
|
||||
|
||||
self.commit = revision
|
||||
end
|
||||
|
||||
function HgRev.new_null_tree()
|
||||
return HgRev(RevType.COMMIT, HgRev.NULL_TREE_SHA)
|
||||
end
|
||||
|
||||
function HgRev:object_name(abbrev_len)
|
||||
if self.commit then
|
||||
if abbrev_len then
|
||||
return self.commit:sub(1, abbrev_len)
|
||||
end
|
||||
|
||||
return self.commit
|
||||
end
|
||||
|
||||
return "UNKNOWN"
|
||||
end
|
||||
|
||||
---@param rev_from HgRev|string
|
||||
---@param rev_to HgRev|string
|
||||
---@return string?
|
||||
function HgRev.to_range(rev_from, rev_to)
|
||||
local name_from = type(rev_from) == "string" and rev_from or rev_from:object_name()
|
||||
local name_to
|
||||
|
||||
if rev_to then
|
||||
if type(rev_to) == "string" then
|
||||
name_to = rev_to
|
||||
elseif rev_to.type == RevType.COMMIT then
|
||||
name_to = rev_to:object_name()
|
||||
end
|
||||
end
|
||||
|
||||
if name_from and name_to then
|
||||
return name_from .. "::" .. name_to
|
||||
else
|
||||
return name_from .. "::" .. name_from
|
||||
end
|
||||
end
|
||||
|
||||
M.HgRev = HgRev
|
||||
return M
|
||||
@ -0,0 +1,83 @@
|
||||
local lazy = require("diffview.lazy")
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
local RevType = lazy.access("diffview.vcs.rev", "RevType") ---@type RevType|LazyModule
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class Commit : diffview.Object
|
||||
---@field hash string
|
||||
---@field author string
|
||||
---@field time number
|
||||
---@field time_offset number
|
||||
---@field date string
|
||||
---@field iso_date string
|
||||
---@field rel_date string
|
||||
---@field ref_names string
|
||||
---@field subject string
|
||||
---@field body string
|
||||
---@field diff? diff.FileEntry[]
|
||||
local Commit = oop.create_class("Commit")
|
||||
|
||||
function Commit:init(opt)
|
||||
self.hash = opt.hash
|
||||
self.author = opt.author
|
||||
self.time = opt.time
|
||||
self.rel_date = opt.rel_date
|
||||
self.ref_names = opt.ref_names ~= "" and opt.ref_names or nil
|
||||
self.subject = opt.subject
|
||||
self.body = opt.body
|
||||
self.diff = opt.diff
|
||||
end
|
||||
|
||||
---@diagnostic disable: unused-local, missing-return
|
||||
|
||||
---@param rev_arg string
|
||||
---@param adapter VCSAdapter
|
||||
---@return Commit?
|
||||
function Commit.from_rev_arg(rev_arg, adapter)
|
||||
oop.abstract_stub()
|
||||
end
|
||||
|
||||
---@diagnostic enable: unused-local, missing-return
|
||||
|
||||
---@param rev Rev
|
||||
---@param adapter VCSAdapter
|
||||
---@return Commit?
|
||||
function Commit.from_rev(rev, adapter)
|
||||
assert(rev.type == RevType.COMMIT, "Rev must be of type COMMIT!")
|
||||
|
||||
return Commit.from_rev_arg(rev.commit, adapter)
|
||||
end
|
||||
|
||||
function Commit.parse_time_offset(iso_date)
|
||||
local sign, h, m = vim.trim(iso_date):match("([+-])(%d%d):?(%d%d)$")
|
||||
local offset = tonumber(h) * 60 * 60 + tonumber(m) * 60
|
||||
|
||||
if sign == "-" then
|
||||
offset = -offset
|
||||
end
|
||||
|
||||
return offset
|
||||
end
|
||||
|
||||
function Commit.time_to_iso(time, time_offset)
|
||||
local iso = os.date("%Y-%m-%d %H:%M:%S", time + time_offset)
|
||||
local sign = utils.sign(time_offset)
|
||||
time_offset = math.abs(time_offset)
|
||||
local tm = (time_offset - (time_offset % 60)) / 60
|
||||
local m = tm % 60
|
||||
local h = (tm - (tm % 60)) / 60
|
||||
|
||||
return string.format(
|
||||
"%s %s%s%s",
|
||||
iso,
|
||||
sign < 0 and "-" or "+",
|
||||
utils.str_left_pad(tostring(h), 2, "0"),
|
||||
utils.str_left_pad(tostring(m), 2, "0")
|
||||
)
|
||||
end
|
||||
|
||||
M.Commit = Commit
|
||||
return M
|
||||
@ -0,0 +1,475 @@
|
||||
local async = require("diffview.async")
|
||||
local lazy = require("diffview.lazy")
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
local GitRev = lazy.access("diffview.vcs.adapters.git.rev", "GitRev") ---@type GitRev|LazyModule
|
||||
local RevType = lazy.access("diffview.vcs.rev", "RevType") ---@type RevType|LazyModule
|
||||
local config = lazy.require("diffview.config") ---@module "diffview.config"
|
||||
local lib = lazy.require("diffview.lib") ---@module "diffview.lib"
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
|
||||
local await = async.await
|
||||
local fmt = string.format
|
||||
local pl = lazy.access(utils, "path") ---@type PathLib
|
||||
|
||||
local api = vim.api
|
||||
local M = {}
|
||||
|
||||
local HAS_NVIM_0_10 = vim.fn.has("nvim-0.10") == 1
|
||||
|
||||
---@alias git.FileDataProducer fun(kind: vcs.FileKind, path: string, pos: "left"|"right"): string[]
|
||||
|
||||
---@class CustomFolds
|
||||
---@field type string
|
||||
---@field [integer] { [1]: integer, [2]: integer }
|
||||
|
||||
---@class vcs.File : diffview.Object
|
||||
---@field adapter GitAdapter
|
||||
---@field path string
|
||||
---@field absolute_path string
|
||||
---@field parent_path string
|
||||
---@field basename string
|
||||
---@field extension string
|
||||
---@field kind vcs.FileKind
|
||||
---@field nulled boolean
|
||||
---@field rev Rev
|
||||
---@field blob_hash string?
|
||||
---@field commit Commit?
|
||||
---@field symbol string?
|
||||
---@field get_data git.FileDataProducer?
|
||||
---@field bufnr integer
|
||||
---@field binary boolean
|
||||
---@field active boolean
|
||||
---@field ready boolean
|
||||
---@field winbar string?
|
||||
---@field winopts WindowOptions
|
||||
---@field custom_folds? CustomFolds
|
||||
local File = oop.create_class("vcs.File")
|
||||
|
||||
---@type table<integer, vcs.File.AttachState>
|
||||
File.attached = {}
|
||||
|
||||
---@type table<string, table<string, integer>>
|
||||
File.index_bufmap = {}
|
||||
|
||||
---@static
|
||||
File.bufopts = {
|
||||
buftype = "nowrite",
|
||||
modifiable = false,
|
||||
swapfile = false,
|
||||
bufhidden = "hide",
|
||||
undolevels = -1,
|
||||
}
|
||||
|
||||
---File constructor
|
||||
---@param opt table
|
||||
function File:init(opt)
|
||||
self.adapter = opt.adapter
|
||||
self.path = opt.path
|
||||
self.absolute_path = pl:absolute(opt.path, opt.adapter.ctx.toplevel)
|
||||
self.parent_path = pl:parent(opt.path) or ""
|
||||
self.basename = pl:basename(opt.path)
|
||||
self.extension = pl:extension(opt.path)
|
||||
self.kind = opt.kind
|
||||
self.binary = utils.sate(opt.binary)
|
||||
self.nulled = not not opt.nulled
|
||||
self.rev = opt.rev
|
||||
self.commit = opt.commit
|
||||
self.symbol = opt.symbol
|
||||
self.get_data = opt.get_data
|
||||
self.active = false
|
||||
self.ready = false
|
||||
|
||||
self.winopts = opt.winopts or {
|
||||
diff = true,
|
||||
scrollbind = true,
|
||||
cursorbind = true,
|
||||
foldmethod = "diff",
|
||||
scrollopt = { "ver", "hor", "jump" },
|
||||
foldcolumn = "1",
|
||||
foldlevel = 0,
|
||||
foldenable = true,
|
||||
winhl = {
|
||||
"DiffAdd:DiffviewDiffAdd",
|
||||
"DiffDelete:DiffviewDiffDelete",
|
||||
"DiffChange:DiffviewDiffChange",
|
||||
"DiffText:DiffviewDiffText",
|
||||
},
|
||||
}
|
||||
|
||||
-- Set winbar info
|
||||
if self.rev then
|
||||
local winbar, label
|
||||
|
||||
if self.rev.type == RevType.LOCAL then
|
||||
winbar = " WORKING TREE - ${path}"
|
||||
elseif self.rev.type == RevType.COMMIT then
|
||||
winbar = " ${object_path}"
|
||||
elseif self.rev.type == RevType.STAGE then
|
||||
if self.kind == "conflicting" then
|
||||
label = ({
|
||||
[1] = "(Common ancestor) ",
|
||||
[2] = "(Current changes) ",
|
||||
[3] = "(Incoming changes) ",
|
||||
})[self.rev.stage] or ""
|
||||
end
|
||||
|
||||
winbar = " INDEX ${label}- ${object_path}"
|
||||
end
|
||||
|
||||
if winbar then
|
||||
self.winbar = utils.str_template(winbar, {
|
||||
path = self.path,
|
||||
object_path = self.rev:object_name(10) .. ":" .. self.path,
|
||||
label = label or "",
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param force? boolean Also delete buffers for LOCAL files.
|
||||
function File:destroy(force)
|
||||
self.active = false
|
||||
self:detach_buffer()
|
||||
|
||||
if force or self.rev.type ~= RevType.LOCAL and not lib.is_buf_in_use(self.bufnr, { self }) then
|
||||
File.safe_delete_buf(self.bufnr)
|
||||
end
|
||||
end
|
||||
|
||||
function File:post_buf_created()
|
||||
local view = require("diffview.lib").get_current_view()
|
||||
|
||||
if view then
|
||||
view.emitter:on("diff_buf_win_enter", function(_, bufnr, winid, ctx)
|
||||
if bufnr == self.bufnr then
|
||||
api.nvim_win_call(winid, function()
|
||||
DiffviewGlobal.emitter:emit("diff_buf_read", self.bufnr, ctx)
|
||||
end)
|
||||
|
||||
return true
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function File:_create_local_buffer()
|
||||
self.bufnr = utils.find_file_buffer(self.absolute_path)
|
||||
|
||||
if not self.bufnr then
|
||||
local winid = utils.temp_win()
|
||||
assert(winid ~= 0, "Failed to create temporary window!")
|
||||
|
||||
api.nvim_win_call(winid, function()
|
||||
vim.cmd("edit " .. vim.fn.fnameescape(self.absolute_path))
|
||||
self.bufnr = api.nvim_get_current_buf()
|
||||
vim.bo[self.bufnr].bufhidden = "hide"
|
||||
end)
|
||||
|
||||
api.nvim_win_close(winid, true)
|
||||
else
|
||||
-- NOTE: LSP servers might load buffers in the background and unlist
|
||||
-- them. Explicitly set the buffer as listed when loading it here.
|
||||
vim.bo[self.bufnr].buflisted = true
|
||||
end
|
||||
|
||||
self:post_buf_created()
|
||||
end
|
||||
|
||||
---@private
|
||||
---@param self vcs.File
|
||||
---@param callback (fun(err?: string[], data?: string[]))
|
||||
File.produce_data = async.wrap(function(self, callback)
|
||||
if self.get_data and vim.is_callable(self.get_data) then
|
||||
local pos = self.symbol == "a" and "left" or "right"
|
||||
local data = self.get_data(self.kind, self.path, pos)
|
||||
callback(nil, data)
|
||||
else
|
||||
local err, data = await(self.adapter:show(self.path, self.rev))
|
||||
|
||||
if err then
|
||||
callback(err)
|
||||
return
|
||||
end
|
||||
|
||||
callback(nil, data)
|
||||
end
|
||||
end)
|
||||
|
||||
---@param self vcs.File
|
||||
---@param callback function
|
||||
File.create_buffer = async.wrap(function(self, callback)
|
||||
---@diagnostic disable: invisible
|
||||
await(async.scheduler())
|
||||
|
||||
if self == File.NULL_FILE then
|
||||
callback(File._get_null_buffer())
|
||||
return
|
||||
elseif self:is_valid() then
|
||||
callback(self.bufnr)
|
||||
return
|
||||
end
|
||||
|
||||
if self.binary == nil and not config.get_config().diff_binaries then
|
||||
self.binary = self.adapter:is_binary(self.path, self.rev)
|
||||
end
|
||||
|
||||
if self.nulled or self.binary then
|
||||
self.bufnr = File._get_null_buffer()
|
||||
self:post_buf_created()
|
||||
callback(self.bufnr)
|
||||
return
|
||||
end
|
||||
|
||||
if self.rev.type == RevType.LOCAL then
|
||||
self:_create_local_buffer()
|
||||
callback(self.bufnr)
|
||||
return
|
||||
end
|
||||
|
||||
local context
|
||||
if self.rev.type == RevType.COMMIT then
|
||||
context = self.rev:abbrev(11)
|
||||
elseif self.rev.type == RevType.STAGE then
|
||||
context = fmt(":%d:", self.rev.stage)
|
||||
elseif self.rev.type == RevType.CUSTOM then
|
||||
context = "[custom]"
|
||||
end
|
||||
|
||||
local fullname = pl:join("diffview://", self.adapter.ctx.dir, context, self.path)
|
||||
|
||||
self.bufnr = utils.find_named_buffer(fullname)
|
||||
|
||||
if self.bufnr then
|
||||
callback(self.bufnr)
|
||||
return
|
||||
end
|
||||
|
||||
-- Create buffer and set name *before* calling `produce_data()` to ensure
|
||||
-- that multiple file instances won't ever try to create the same file.
|
||||
self.bufnr = api.nvim_create_buf(false, false)
|
||||
api.nvim_buf_set_name(self.bufnr, fullname)
|
||||
|
||||
local err, lines = await(self:produce_data())
|
||||
if err then error(table.concat(err, "\n")) end
|
||||
|
||||
await(async.scheduler())
|
||||
|
||||
-- Revalidate buffer in case the file was destroyed before `produce_data()`
|
||||
-- returned.
|
||||
if not api.nvim_buf_is_valid(self.bufnr) then
|
||||
error("The buffer has been invalidated!")
|
||||
return
|
||||
end
|
||||
local bufopts = vim.deepcopy(File.bufopts)
|
||||
|
||||
if self.rev.type == RevType.STAGE and self.rev.stage == 0 then
|
||||
self.blob_hash = self.adapter:file_blob_hash(self.path)
|
||||
bufopts.modifiable = true
|
||||
bufopts.buftype = nil
|
||||
bufopts.undolevels = nil
|
||||
utils.tbl_set(File.index_bufmap, { self.adapter.ctx.toplevel, self.path }, self.bufnr)
|
||||
|
||||
api.nvim_create_autocmd("BufWriteCmd", {
|
||||
buffer = self.bufnr,
|
||||
nested = true,
|
||||
callback = function()
|
||||
self.adapter:stage_index_file(self)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
for option, value in pairs(bufopts) do
|
||||
api.nvim_buf_set_option(self.bufnr, option, value)
|
||||
end
|
||||
|
||||
local last_modifiable = vim.bo[self.bufnr].modifiable
|
||||
local last_modified = vim.bo[self.bufnr].modified
|
||||
vim.bo[self.bufnr].modifiable = true
|
||||
api.nvim_buf_set_lines(self.bufnr, 0, -1, false, lines)
|
||||
|
||||
api.nvim_buf_call(self.bufnr, function()
|
||||
vim.cmd("filetype detect")
|
||||
end)
|
||||
|
||||
vim.bo[self.bufnr].modifiable = last_modifiable
|
||||
vim.bo[self.bufnr].modified = last_modified
|
||||
self:post_buf_created()
|
||||
callback(self.bufnr)
|
||||
---@diagnostic enable: invisible
|
||||
end)
|
||||
|
||||
function File:is_valid()
|
||||
return self.bufnr and api.nvim_buf_is_valid(self.bufnr)
|
||||
end
|
||||
|
||||
---@param t1 table
|
||||
---@param t2 table
|
||||
---@return vcs.File.AttachState
|
||||
local function prepare_attach_opt(t1, t2)
|
||||
local res = vim.tbl_extend("keep", t1, {
|
||||
keymaps = {},
|
||||
disable_diagnostics = false,
|
||||
})
|
||||
|
||||
for k, v in pairs(t2) do
|
||||
local t = type(res[k])
|
||||
|
||||
if t == "boolean" then
|
||||
res[k] = res[k] or v
|
||||
elseif t == "table" and type(v) == "table" then
|
||||
res[k] = vim.tbl_extend("force", res[k], v)
|
||||
else
|
||||
res[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
return res
|
||||
end
|
||||
|
||||
---@class vcs.File.AttachState
|
||||
---@field keymaps table
|
||||
---@field disable_diagnostics boolean
|
||||
|
||||
---@param force? boolean
|
||||
---@param opt? vcs.File.AttachState
|
||||
function File:attach_buffer(force, opt)
|
||||
if self.bufnr then
|
||||
local new_opt = false
|
||||
local cur_state = File.attached[self.bufnr] or {}
|
||||
local state = prepare_attach_opt(cur_state, opt or {})
|
||||
|
||||
if opt then
|
||||
new_opt = not vim.deep_equal(cur_state or {}, opt)
|
||||
end
|
||||
|
||||
if force or new_opt or not cur_state then
|
||||
local conf = config.get_config()
|
||||
|
||||
-- Keymaps
|
||||
state.keymaps = config.extend_keymaps(conf.keymaps.view, state.keymaps)
|
||||
local default_map_opt = { silent = true, nowait = true, buffer = self.bufnr }
|
||||
|
||||
for _, mapping in ipairs(state.keymaps) do
|
||||
local map_opt = vim.tbl_extend("force", default_map_opt, mapping[4] or {}, { buffer = self.bufnr })
|
||||
vim.keymap.set(mapping[1], mapping[2], mapping[3], map_opt)
|
||||
end
|
||||
|
||||
-- Diagnostics
|
||||
if state.disable_diagnostics then
|
||||
if HAS_NVIM_0_10 then
|
||||
vim.diagnostic.enable(false, { bufnr = self.bufnr })
|
||||
else
|
||||
---@diagnostic disable-next-line: deprecated
|
||||
vim.diagnostic.disable(self.bufnr)
|
||||
end
|
||||
end
|
||||
|
||||
File.attached[self.bufnr] = state
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function File:detach_buffer()
|
||||
if self.bufnr then
|
||||
local state = File.attached[self.bufnr]
|
||||
|
||||
if state then
|
||||
-- Keymaps
|
||||
for lhs, mapping in pairs(state.keymaps) do
|
||||
if type(lhs) == "number" then
|
||||
local modes = type(mapping[1]) == "table" and mapping[1] or { mapping[1] }
|
||||
for _, mode in ipairs(modes) do
|
||||
pcall(api.nvim_buf_del_keymap, self.bufnr, mode, mapping[2])
|
||||
end
|
||||
else
|
||||
pcall(api.nvim_buf_del_keymap, self.bufnr, "n", lhs)
|
||||
end
|
||||
end
|
||||
|
||||
-- Diagnostics
|
||||
if state.disable_diagnostics then
|
||||
if HAS_NVIM_0_10 then
|
||||
vim.diagnostic.enable(true, { bufnr = self.bufnr })
|
||||
else
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
vim.diagnostic.enable(self.bufnr)
|
||||
end
|
||||
end
|
||||
|
||||
File.attached[self.bufnr] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function File:dispose_buffer()
|
||||
if self.bufnr and api.nvim_buf_is_loaded(self.bufnr) then
|
||||
self:detach_buffer()
|
||||
|
||||
if not lib.is_buf_in_use(self.bufnr, { self }) then
|
||||
File.safe_delete_buf(self.bufnr)
|
||||
end
|
||||
|
||||
self.bufnr = nil
|
||||
end
|
||||
end
|
||||
|
||||
function File.safe_delete_buf(bufnr)
|
||||
if not bufnr or bufnr == File.NULL_FILE.bufnr or not api.nvim_buf_is_loaded(bufnr) then
|
||||
return
|
||||
end
|
||||
|
||||
for _, winid in ipairs(utils.win_find_buf(bufnr, 0)) do
|
||||
File.load_null_buffer(winid)
|
||||
end
|
||||
|
||||
pcall(api.nvim_buf_delete, bufnr, { force = true })
|
||||
end
|
||||
|
||||
---@static Get the bufid of the null buffer. Create it if it's not loaded.
|
||||
---@return integer
|
||||
function File._get_null_buffer()
|
||||
if not api.nvim_buf_is_loaded(File.NULL_FILE.bufnr or -1) then
|
||||
local bn = api.nvim_create_buf(false, false)
|
||||
for option, value in pairs(File.bufopts) do
|
||||
api.nvim_buf_set_option(bn, option, value)
|
||||
end
|
||||
|
||||
local bufname = "diffview://null"
|
||||
local ok = pcall(api.nvim_buf_set_name, bn, bufname)
|
||||
if not ok then
|
||||
utils.wipe_named_buffer(bufname)
|
||||
api.nvim_buf_set_name(bn, bufname)
|
||||
end
|
||||
|
||||
File.NULL_FILE.bufnr = bn
|
||||
end
|
||||
|
||||
return File.NULL_FILE.bufnr
|
||||
end
|
||||
|
||||
---@static
|
||||
function File.load_null_buffer(winid)
|
||||
local bn = File._get_null_buffer()
|
||||
api.nvim_win_set_buf(winid, bn)
|
||||
File.NULL_FILE:attach_buffer()
|
||||
end
|
||||
|
||||
---@type vcs.File
|
||||
File.NULL_FILE = File({
|
||||
-- NOTE: consider changing this adapter to be an actual adapter instance
|
||||
adapter = {
|
||||
ctx = {
|
||||
toplevel = "diffview://",
|
||||
},
|
||||
},
|
||||
path = "null",
|
||||
kind = "working",
|
||||
status = "X",
|
||||
binary = false,
|
||||
nulled = true,
|
||||
rev = GitRev.new_null_tree(),
|
||||
})
|
||||
|
||||
M.File = File
|
||||
return M
|
||||
@ -0,0 +1,92 @@
|
||||
local oop = require("diffview.oop")
|
||||
local FileTree = require("diffview.ui.models.file_tree.file_tree").FileTree
|
||||
local M = {}
|
||||
|
||||
---@alias vcs.FileKind "conflicting"|"working"|"staged"
|
||||
|
||||
---@class FileDict : diffview.Object
|
||||
---@field [integer] FileEntry
|
||||
---@field sets FileEntry[][]
|
||||
---@field conflicting FileEntry[]
|
||||
---@field working FileEntry[]
|
||||
---@field staged FileEntry[]
|
||||
---@field conflicting_tree FileTree
|
||||
---@field working_tree FileTree
|
||||
---@field staged_tree FileTree
|
||||
local FileDict = oop.create_class("FileDict")
|
||||
|
||||
---FileDict constructor.
|
||||
function FileDict:init()
|
||||
self.conflicting = {}
|
||||
self.working = {}
|
||||
self.staged = {}
|
||||
self.sets = { self.conflicting, self.working, self.staged }
|
||||
self:update_file_trees()
|
||||
end
|
||||
|
||||
function FileDict:__index(k)
|
||||
if type(k) == "number" then
|
||||
local offset = 0
|
||||
|
||||
for _, set in ipairs(self.sets) do
|
||||
if k - offset <= #set then
|
||||
return set[k - offset]
|
||||
end
|
||||
|
||||
offset = offset + #set
|
||||
end
|
||||
else
|
||||
return FileDict[k]
|
||||
end
|
||||
end
|
||||
|
||||
function FileDict:update_file_trees()
|
||||
self.conflicting_tree = FileTree(self.conflicting)
|
||||
self.working_tree = FileTree(self.working)
|
||||
self.staged_tree = FileTree(self.staged)
|
||||
end
|
||||
|
||||
function FileDict:len()
|
||||
local l = 0
|
||||
for _, set in ipairs(self.sets) do l = l + #set end
|
||||
|
||||
return l
|
||||
end
|
||||
|
||||
function FileDict:iter()
|
||||
local i = 0
|
||||
local n = self:len()
|
||||
|
||||
---@return integer?, FileEntry?
|
||||
return function()
|
||||
i = i + 1
|
||||
|
||||
if i <= n then
|
||||
return i, self[i]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param files FileEntry[]
|
||||
function FileDict:set_conflicting(files)
|
||||
for i = 1, math.max(#self.conflicting, #files) do
|
||||
self.conflicting[i] = files[i] or nil
|
||||
end
|
||||
end
|
||||
|
||||
---@param files FileEntry[]
|
||||
function FileDict:set_working(files)
|
||||
for i = 1, math.max(#self.working, #files) do
|
||||
self.working[i] = files[i] or nil
|
||||
end
|
||||
end
|
||||
|
||||
---@param files FileEntry[]
|
||||
function FileDict:set_staged(files)
|
||||
for i = 1, math.max(#self.staged, #files) do
|
||||
self.staged[i] = files[i] or nil
|
||||
end
|
||||
end
|
||||
|
||||
M.FileDict = FileDict
|
||||
return M
|
||||
@ -0,0 +1,147 @@
|
||||
local lazy = require("diffview.lazy")
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
|
||||
local M = {}
|
||||
|
||||
---@alias FlagOption.CompletionWrapper fun(parent: FHOptionPanel): fun(ctx: CmdLineContext): string[]
|
||||
|
||||
---@class FlagOption
|
||||
---@field flag_name string
|
||||
---@field keymap string
|
||||
---@field desc string
|
||||
---@field key string
|
||||
---@field expect_list boolean
|
||||
---@field prompt_label string
|
||||
---@field prompt_fmt string
|
||||
---@field value_fmt string
|
||||
---@field display_fmt string
|
||||
---@field select? string[]
|
||||
---@field completion? string|FlagOption.CompletionWrapper
|
||||
local FlagOption = oop.create_class("FlagOption")
|
||||
|
||||
---@class FlagOption.init.Opt
|
||||
---@field flag_name string
|
||||
---@field keymap string
|
||||
---@field desc string
|
||||
---@field key string
|
||||
---@field expect_list boolean
|
||||
---@field prompt_label string
|
||||
---@field prompt_fmt string
|
||||
---@field value_fmt string
|
||||
---@field display_fmt string
|
||||
---@field select? string[]
|
||||
---@field completion? string|FlagOption.CompletionWrapper
|
||||
---@field transform function
|
||||
---@field prepare_values function
|
||||
---@field render_prompt function
|
||||
---@field render_value function
|
||||
---@field render_display function
|
||||
---@field render_default function
|
||||
|
||||
---@param keymap string
|
||||
---@param flag_name string
|
||||
---@param desc string
|
||||
---@param opt FlagOption.init.Opt
|
||||
function FlagOption:init(keymap, flag_name, desc, opt)
|
||||
opt = opt or {}
|
||||
|
||||
self.keymap = keymap
|
||||
self.flag_name = flag_name
|
||||
self.desc = desc
|
||||
self.key = opt.key or utils.str_match(flag_name, {
|
||||
"^%-%-?([^=]+)=?",
|
||||
"^%+%+?([^=]+)=?",
|
||||
}):gsub("%-", "_")
|
||||
self.select = opt.select
|
||||
self.completion = opt.completion
|
||||
self.expect_list = utils.sate(opt.expect_list, false)
|
||||
self.prompt_label = opt.prompt_label or ""
|
||||
self.prompt_fmt = opt.prompt_fmt or "${label}${flag_name}"
|
||||
self.value_fmt = opt.value_fmt or "${flag_name}${value}"
|
||||
self.display_fmt = opt.display_fmt or "${values}"
|
||||
self.transform = opt.transform or self.transform
|
||||
self.render_prompt = opt.render_prompt or self.render_prompt
|
||||
self.render_value = opt.render_value or self.render_value
|
||||
self.render_display = opt.render_display or self.render_display
|
||||
self.render_default = opt.render_default or self.render_default
|
||||
end
|
||||
|
||||
---@param values any|any[]
|
||||
---@return string[]
|
||||
function FlagOption:prepare_values(values)
|
||||
if values == nil then
|
||||
return {}
|
||||
elseif type(values) ~= "table" then
|
||||
return { tostring(values) }
|
||||
else
|
||||
return vim.tbl_map(tostring, values)
|
||||
end
|
||||
end
|
||||
|
||||
---Transform the values given by the user.
|
||||
---@param values any|any[]
|
||||
function FlagOption:transform(values)
|
||||
return utils.tbl_fmap(self:prepare_values(values), function(v)
|
||||
v = utils.str_match(v, { "^" .. vim.pesc(self.flag_name) .. "(.*)", ".*" })
|
||||
if v == "" then return nil end
|
||||
return v
|
||||
end)
|
||||
end
|
||||
|
||||
function FlagOption:render_prompt()
|
||||
return utils.str_template(self.prompt_fmt, {
|
||||
label = self.prompt_label and self.prompt_label .. " " or "",
|
||||
flag_name = self.flag_name .. " ",
|
||||
}):sub(1, -2)
|
||||
end
|
||||
|
||||
---Render a single option value
|
||||
---@param value string
|
||||
function FlagOption:render_value(value)
|
||||
value = value:gsub("\\", "\\\\")
|
||||
return utils.str_template(self.value_fmt, {
|
||||
flag_name = self.flag_name,
|
||||
value = utils.str_quote(value, { only_if_whitespace = true }),
|
||||
})
|
||||
end
|
||||
|
||||
---Render the displayed text for the panel.
|
||||
---@param values any|any[]
|
||||
---@return boolean empty
|
||||
---@return string rendered_text
|
||||
function FlagOption:render_display(values)
|
||||
values = self:prepare_values(values)
|
||||
if #values == 0 or (#values == 1 and values[1] == "") then
|
||||
return true, self.flag_name
|
||||
end
|
||||
|
||||
local quoted = table.concat(vim.tbl_map(function(v)
|
||||
return self:render_value(v)
|
||||
end, values), " ")
|
||||
|
||||
return false, utils.str_template(self.display_fmt, {
|
||||
flag_name = self.flag_name,
|
||||
values = quoted,
|
||||
})
|
||||
end
|
||||
|
||||
---Render the default text for |input()|.
|
||||
---@param values any|any[]
|
||||
function FlagOption:render_default(values)
|
||||
values = self:prepare_values(values)
|
||||
|
||||
local ret = vim.tbl_map(function(v)
|
||||
return self:render_value(v)
|
||||
end, values)
|
||||
|
||||
if #ret > 0 then
|
||||
ret[1] = ret[1]:match("^" .. vim.pesc(self.flag_name) .. "(.*)") or ret[1]
|
||||
end
|
||||
|
||||
return table.concat(ret, " ")
|
||||
end
|
||||
|
||||
M.FlagOption = FlagOption
|
||||
return M
|
||||
@ -0,0 +1,49 @@
|
||||
local GitAdapter = require('diffview.vcs.adapters.git').GitAdapter
|
||||
local HgAdapter = require('diffview.vcs.adapters.hg').HgAdapter
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class vcs.init.get_adapter.Opt
|
||||
---@field top_indicators string[]?
|
||||
---@field cmd_ctx vcs.init.get_adapter.Opt.Cmd_Ctx? # Context data from a command call.
|
||||
|
||||
---@class vcs.init.get_adapter.Opt.Cmd_Ctx
|
||||
---@field path_args string[] # Raw path args
|
||||
---@field cpath string? # Cwd path given by the `-C` flag option
|
||||
|
||||
---@param opt vcs.init.get_adapter.Opt
|
||||
---@return string? err
|
||||
---@return VCSAdapter? adapter
|
||||
function M.get_adapter(opt)
|
||||
local adapter_kinds = { GitAdapter, HgAdapter }
|
||||
|
||||
if not opt.cmd_ctx then
|
||||
opt.cmd_ctx = {}
|
||||
end
|
||||
|
||||
for _, kind in ipairs(adapter_kinds) do
|
||||
local path_args
|
||||
local top_indicators = opt.top_indicators
|
||||
|
||||
if not kind.bootstrap.done then kind.run_bootstrap() end
|
||||
if not kind.bootstrap.ok then goto continue end
|
||||
|
||||
if not top_indicators then
|
||||
path_args, top_indicators = kind.get_repo_paths(opt.cmd_ctx.path_args, opt.cmd_ctx.cpath)
|
||||
end
|
||||
|
||||
local err, toplevel = kind.find_toplevel(top_indicators)
|
||||
|
||||
if not err then
|
||||
-- Create a new adapter instance. Store the resolved path args and the
|
||||
-- cpath in the adapter context.
|
||||
return kind.create(toplevel, path_args, opt.cmd_ctx.cpath)
|
||||
end
|
||||
|
||||
::continue::
|
||||
end
|
||||
|
||||
return "Not a repo (or any parent), or no supported VCS adapter!"
|
||||
end
|
||||
|
||||
return M
|
||||
@ -0,0 +1,105 @@
|
||||
local lazy = require("diffview.lazy")
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
local FileEntry = lazy.access("diffview.scene.file_entry", "FileEntry") ---@type FileEntry
|
||||
local utils = lazy.require("diffview.utils") ---@module "diffview.utils"
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class LogEntry : diffview.Object
|
||||
---@operator call : LogEntry
|
||||
---@field path_args string[]
|
||||
---@field commit Commit
|
||||
---@field files FileEntry[]
|
||||
---@field status string
|
||||
---@field stats GitStats
|
||||
---@field single_file boolean
|
||||
---@field folded boolean
|
||||
---@field nulled boolean
|
||||
local LogEntry = oop.create_class("LogEntry")
|
||||
|
||||
function LogEntry:init(opt)
|
||||
self.path_args = opt.path_args
|
||||
self.commit = opt.commit
|
||||
self.files = opt.files
|
||||
self.folded = true
|
||||
self.single_file = opt.single_file
|
||||
self.nulled = utils.sate(opt.nulled, false)
|
||||
self:update_status()
|
||||
self:update_stats()
|
||||
end
|
||||
|
||||
function LogEntry:destroy()
|
||||
for _, file in ipairs(self.files) do
|
||||
file:destroy()
|
||||
end
|
||||
end
|
||||
|
||||
function LogEntry:update_status()
|
||||
self.status = nil
|
||||
local missing_status = 0
|
||||
|
||||
for _, file in ipairs(self.files) do
|
||||
if not file.status then
|
||||
missing_status = missing_status + 1
|
||||
else
|
||||
if self.status and file.status ~= self.status then
|
||||
self.status = "M"
|
||||
return
|
||||
elseif self.status ~= file.status then
|
||||
self.status = file.status
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if missing_status < #self.files and not self.status then
|
||||
self.status = "X"
|
||||
end
|
||||
end
|
||||
|
||||
function LogEntry:update_stats()
|
||||
self.stats = { additions = 0, deletions = 0 }
|
||||
local missing_stats = 0
|
||||
|
||||
for _, file in ipairs(self.files) do
|
||||
if not file.stats then
|
||||
missing_stats = missing_stats + 1
|
||||
else
|
||||
self.stats.additions = self.stats.additions + file.stats.additions
|
||||
self.stats.deletions = self.stats.deletions + file.stats.deletions
|
||||
end
|
||||
end
|
||||
|
||||
if missing_stats == #self.files then
|
||||
self.stats = nil
|
||||
end
|
||||
end
|
||||
|
||||
---@param path string
|
||||
---@return diff.FileEntry?
|
||||
function LogEntry:get_diff(path)
|
||||
if not self.commit.diff then return nil end
|
||||
|
||||
for _, diff_entry in ipairs(self.commit.diff) do
|
||||
if path == (diff_entry.path_new or diff_entry.path_old) then
|
||||
return diff_entry
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param adapter VCSAdapter
|
||||
---@param opt table
|
||||
---@return LogEntry
|
||||
function LogEntry.new_null_entry(adapter, opt)
|
||||
opt = opt or {}
|
||||
|
||||
return LogEntry(
|
||||
vim.tbl_extend("force", opt, {
|
||||
nulled = true,
|
||||
files = { FileEntry.new_null_entry(adapter) },
|
||||
})
|
||||
)
|
||||
end
|
||||
|
||||
M.LogEntry = LogEntry
|
||||
return M
|
||||
@ -0,0 +1,125 @@
|
||||
local oop = require("diffview.oop")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@enum RevType
|
||||
local RevType = oop.enum({
|
||||
LOCAL = 1,
|
||||
COMMIT = 2,
|
||||
STAGE = 3,
|
||||
CUSTOM = 4,
|
||||
})
|
||||
|
||||
---@alias RevRange { first: Rev, last: Rev }
|
||||
|
||||
---@class Rev : diffview.Object
|
||||
---@field type integer
|
||||
---@field commit? string A commit SHA.
|
||||
---@field stage? integer A stage number.
|
||||
---@field track_head boolean If true, indicates that the rev should be updated when HEAD changes.
|
||||
local Rev = oop.create_class("Rev")
|
||||
|
||||
---Rev constructor
|
||||
---@param rev_type RevType
|
||||
---@param revision string|number Commit SHA or stage number.
|
||||
---@param track_head? boolean
|
||||
function Rev:init(rev_type, revision, track_head)
|
||||
local t = type(revision)
|
||||
|
||||
assert(
|
||||
revision == nil or t == "string" or t == "number",
|
||||
"'revision' must be one of: nil, string, number!"
|
||||
)
|
||||
if t == "string" then
|
||||
assert(revision ~= "", "'revision' cannot be an empty string!")
|
||||
elseif t == "number" then
|
||||
assert(
|
||||
revision >= 0 and revision <= 3,
|
||||
"'revision' must be a valid stage number ([0-3])!"
|
||||
)
|
||||
end
|
||||
|
||||
t = type(track_head)
|
||||
assert(t == "boolean" or t == "nil", "'track_head' must be of type boolean!")
|
||||
|
||||
self.type = rev_type
|
||||
self.track_head = track_head or false
|
||||
|
||||
if type(revision) == "string" then
|
||||
---@cast revision string
|
||||
self.commit = revision
|
||||
elseif type(revision) == "number" then
|
||||
---@cast revision number
|
||||
self.stage = revision
|
||||
end
|
||||
end
|
||||
|
||||
function Rev:__tostring()
|
||||
if self.type == RevType.COMMIT or self.type == RevType.STAGE then
|
||||
return self:object_name()
|
||||
elseif self.type == RevType.LOCAL then
|
||||
return "LOCAL"
|
||||
elseif self.type == RevType.CUSTOM then
|
||||
return "CUSTOM"
|
||||
end
|
||||
end
|
||||
|
||||
---@diagnostic disable: unused-local, missing-return
|
||||
|
||||
---Get the argument describing the range between the two given revs. If a
|
||||
---single rev is given, the returned argument describes the *range* of the
|
||||
---single commit pointed to by that rev.
|
||||
---@abstract
|
||||
---@param rev_from Rev|string
|
||||
---@param rev_to? Rev|string
|
||||
---@return string?
|
||||
function Rev.to_range(rev_from, rev_to) oop.abstract_stub() end
|
||||
|
||||
---@param name string
|
||||
---@param adapter? VCSAdapter
|
||||
---@return Rev?
|
||||
function Rev.from_name(name, adapter)
|
||||
oop.abstract_stub()
|
||||
end
|
||||
|
||||
---@param adapter VCSAdapter
|
||||
---@return Rev?
|
||||
function Rev.earliest_commit(adapter)
|
||||
oop.abstract_stub()
|
||||
end
|
||||
|
||||
---Create a new commit rev with the special empty tree SHA.
|
||||
---@return Rev
|
||||
function Rev.new_null_tree()
|
||||
oop.abstract_stub()
|
||||
end
|
||||
|
||||
---Determine if this rev is currently the head.
|
||||
---@param adapter VCSAdapter
|
||||
---@return boolean?
|
||||
function Rev:is_head(adapter)
|
||||
oop.abstract_stub()
|
||||
end
|
||||
|
||||
---@param abbrev_len? integer
|
||||
---@return string
|
||||
function Rev:object_name(abbrev_len)
|
||||
oop.abstract_stub()
|
||||
end
|
||||
|
||||
---@diagnostic enable: unused-local, missing-return
|
||||
|
||||
---Get an abbreviated commit SHA. Returns `nil` if this Rev is not a commit.
|
||||
---@param length integer|nil
|
||||
---@return string|nil
|
||||
function Rev:abbrev(length)
|
||||
if self.commit then
|
||||
return self.commit:sub(1, length or 7)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
M.RevType = RevType
|
||||
M.Rev = Rev
|
||||
|
||||
return M
|
||||
@ -0,0 +1,623 @@
|
||||
local FileDict = require("diffview.vcs.file_dict").FileDict
|
||||
local RevType = require("diffview.vcs.rev").RevType
|
||||
local Scanner = require("diffview.scanner")
|
||||
local Semaphore = require("diffview.control").Semaphore
|
||||
local async = require("diffview.async")
|
||||
local oop = require("diffview.oop")
|
||||
local utils = require("diffview.utils")
|
||||
|
||||
local api = vim.api
|
||||
local await = async.await
|
||||
local fmt = string.format
|
||||
local logger = DiffviewGlobal.logger
|
||||
|
||||
local M = {}
|
||||
|
||||
---@enum JobStatus
|
||||
local JobStatus = oop.enum({
|
||||
SUCCESS = 1,
|
||||
PROGRESS = 2,
|
||||
ERROR = 3,
|
||||
KILLED = 4,
|
||||
FATAL = 5,
|
||||
})
|
||||
|
||||
---@type diffview.Job[]
|
||||
local sync_jobs = {}
|
||||
local job_queue_sem = Semaphore(1)
|
||||
|
||||
---@param job diffview.Job
|
||||
M.resume_sync_queue = async.void(function(job)
|
||||
local permit = await(job_queue_sem:acquire()) --[[@as Permit ]]
|
||||
local idx = utils.vec_indexof(sync_jobs, job)
|
||||
if idx > -1 then
|
||||
table.remove(sync_jobs, idx)
|
||||
end
|
||||
permit:forget()
|
||||
|
||||
if sync_jobs[1] and not sync_jobs[1]:is_started() then
|
||||
sync_jobs[1]:start()
|
||||
end
|
||||
end)
|
||||
|
||||
---@param job diffview.Job
|
||||
M.queue_sync_job = async.void(function(job)
|
||||
job:on_exit(function()
|
||||
M.resume_sync_queue(job)
|
||||
end)
|
||||
|
||||
local permit = await(job_queue_sem:acquire()) --[[@as Permit ]]
|
||||
table.insert(sync_jobs, job)
|
||||
|
||||
if #sync_jobs == 1 then
|
||||
job:start()
|
||||
end
|
||||
|
||||
permit:forget()
|
||||
end)
|
||||
|
||||
---Get a list of files modified between two revs.
|
||||
---@param adapter VCSAdapter
|
||||
---@param left Rev
|
||||
---@param right Rev
|
||||
---@param path_args string[]|nil
|
||||
---@param dv_opt DiffViewOptions
|
||||
---@param opt vcs.adapter.LayoutOpt
|
||||
---@param callback function
|
||||
---@return string[]? err
|
||||
---@return FileDict?
|
||||
M.diff_file_list = async.wrap(function(adapter, left, right, path_args, dv_opt, opt, callback)
|
||||
---@type FileDict
|
||||
local files = FileDict()
|
||||
local rev_args = adapter:rev_to_args(left, right)
|
||||
local errors = {}
|
||||
|
||||
;(function()
|
||||
local err, tfiles, tconflicts = await(
|
||||
adapter:tracked_files(
|
||||
left,
|
||||
right,
|
||||
utils.vec_join(rev_args, "--", path_args),
|
||||
"working",
|
||||
opt
|
||||
)
|
||||
)
|
||||
|
||||
if err then
|
||||
errors[#errors+1] = err
|
||||
utils.err("Failed to get git status for tracked files!", true)
|
||||
return
|
||||
end
|
||||
|
||||
files:set_working(tfiles)
|
||||
files:set_conflicting(tconflicts)
|
||||
|
||||
if not adapter:show_untracked({
|
||||
dv_opt = dv_opt,
|
||||
revs = { left = left, right = right },
|
||||
})
|
||||
then return end
|
||||
|
||||
---@diagnostic disable-next-line: redefined-local
|
||||
local err, ufiles = await(adapter:untracked_files(left, right, opt))
|
||||
|
||||
if err then
|
||||
errors[#errors+1] = err
|
||||
utils.err("Failed to get git status for untracked files!", true)
|
||||
else
|
||||
files:set_working(utils.vec_join(files.working, ufiles))
|
||||
|
||||
utils.merge_sort(files.working, function(a, b)
|
||||
return a.path:lower() < b.path:lower()
|
||||
end)
|
||||
end
|
||||
end)()
|
||||
|
||||
if left.type == RevType.STAGE and right.type == RevType.LOCAL then
|
||||
local left_rev = adapter:head_rev() or adapter.Rev.new_null_tree()
|
||||
local right_rev = adapter.Rev(RevType.STAGE, 0)
|
||||
---@diagnostic disable-next-line: redefined-local
|
||||
local err, tfiles = await(
|
||||
adapter:tracked_files(
|
||||
left_rev,
|
||||
right_rev,
|
||||
utils.vec_join("--cached", left_rev.commit, "--", path_args),
|
||||
"staged",
|
||||
opt
|
||||
)
|
||||
)
|
||||
|
||||
if err then
|
||||
errors[#errors+1] = err
|
||||
utils.err("Failed to get git status for staged files!", true)
|
||||
else
|
||||
files:set_staged(tfiles)
|
||||
end
|
||||
end
|
||||
|
||||
if #errors > 0 then
|
||||
callback(utils.vec_join(unpack(errors)), nil)
|
||||
return
|
||||
end
|
||||
|
||||
files:update_file_trees()
|
||||
callback(nil, files)
|
||||
end, 7)
|
||||
|
||||
---Restore a file to the state it was in, in a given commit / rev. If no commit
|
||||
---is given, unstaged files are restored to the state in index, and staged files
|
||||
---are restored to the state in HEAD. The file will also be written into the
|
||||
---object database such that the action can be undone.
|
||||
---@param adapter VCSAdapter
|
||||
---@param path string
|
||||
---@param kind vcs.FileKind
|
||||
---@param commit? string
|
||||
M.restore_file = async.void(function(adapter, path, kind, commit)
|
||||
local ok, undo = await(adapter:file_restore(path, kind, commit))
|
||||
|
||||
if not ok then
|
||||
utils.err("Failed to revert file! See ':DiffviewLog' for details.", true)
|
||||
return
|
||||
end
|
||||
|
||||
local rev_name = (commit and commit:sub(1, 11)) or (kind == "staged" and "HEAD" or "index")
|
||||
local msg = fmt("File restored from %s. %s", rev_name, undo and ("Undo with " .. undo) or "")
|
||||
|
||||
logger:info(msg)
|
||||
utils.info(msg, true)
|
||||
end)
|
||||
|
||||
--[[
|
||||
Standard change:
|
||||
|
||||
diff --git a/lua/diffview/health.lua b/lua/diffview/health.lua
|
||||
index c05dcda..07bdd33 100644
|
||||
--- a/lua/diffview/health.lua
|
||||
+++ b/lua/diffview/health.lua
|
||||
@@ -48,7 +48,7 @@ function M.check()
|
||||
|
||||
Rename with change:
|
||||
|
||||
diff --git a/test/index_watcher_spec.lua b/test/gitdir_watcher_spec.lua
|
||||
similarity index 94%
|
||||
rename from test/index_watcher_spec.lua
|
||||
rename to test/gitdir_watcher_spec.lua
|
||||
index 008beab..66116dc 100644
|
||||
--- a/test/index_watcher_spec.lua
|
||||
+++ b/test/gitdir_watcher_spec.lua
|
||||
@@ -17,7 +17,7 @@ local get_buf_name = helpers.curbufmeths.get_name
|
||||
--]]
|
||||
|
||||
local DIFF_HEADER = [[^diff %-%-git ]]
|
||||
local DIFF_SIMILARITY = [[^similarity index (%d+)%%]]
|
||||
local DIFF_INDEX = { [[^index (%x-)%.%.(%x-) (%d+)]], [[^index (%x-)%.%.(%x-)]] }
|
||||
local DIFF_PATH_OLD = { [[^%-%-%- a/(.*)]], [[^%-%-%- (/dev/null)]] }
|
||||
local DIFF_PATH_NEW = { [[^%+%+%+ b/(.*)]], [[^%+%+%+ (/dev/null)]] }
|
||||
local DIFF_HUNK_HEADER = [[^@@+ %-(%d+),(%d+) %+(%d+),(%d+) @@+]]
|
||||
|
||||
---@class diff.Hunk
|
||||
---@field old_row integer
|
||||
---@field old_size integer
|
||||
---@field new_row integer
|
||||
---@field new_size integer
|
||||
---@field common_content string[]
|
||||
---@field old_content { [1]: integer, [2]: string[] }[]
|
||||
---@field new_content { [1]: integer, [2]: string[] }[]
|
||||
|
||||
---@param scanner Scanner
|
||||
---@param old_row integer
|
||||
---@param old_size integer
|
||||
---@param new_row integer
|
||||
---@param new_size integer
|
||||
---@return diff.Hunk
|
||||
local function parse_diff_hunk(scanner, old_row, old_size, new_row, new_size)
|
||||
local ret = {
|
||||
old_row = old_row,
|
||||
old_size = old_size,
|
||||
new_row = new_row,
|
||||
new_size = new_size,
|
||||
common_content = {},
|
||||
old_content = {},
|
||||
new_content = {},
|
||||
}
|
||||
|
||||
local common_idx, old_offset, new_offset = 1, 0, 0
|
||||
local line = scanner:peek_line()
|
||||
local cur_start = (line or ""):match("^([%+%- ])")
|
||||
|
||||
while cur_start do
|
||||
line = scanner:next_line() --[[@as string ]]
|
||||
|
||||
if cur_start == " " then
|
||||
ret.common_content[#ret.common_content + 1] = line:sub(2) or ""
|
||||
common_idx = common_idx + 1
|
||||
|
||||
elseif cur_start == "-" then
|
||||
local content = { line:sub(2) or "" }
|
||||
|
||||
while (scanner:peek_line() or ""):sub(1, 1) == "-" do
|
||||
content[#content + 1] = scanner:next_line():sub(2) or ""
|
||||
end
|
||||
|
||||
ret.old_content[#ret.old_content + 1] = { common_idx + old_offset, content }
|
||||
old_offset = old_offset + #content
|
||||
|
||||
elseif cur_start == "+" then
|
||||
local content = { line:sub(2) or "" }
|
||||
|
||||
while (scanner:peek_line() or ""):sub(1, 1) == "+" do
|
||||
content[#content + 1] = scanner:next_line():sub(2) or ""
|
||||
end
|
||||
|
||||
ret.new_content[#ret.new_content + 1] = { common_idx + new_offset, content }
|
||||
new_offset = new_offset + #content
|
||||
end
|
||||
|
||||
cur_start = (scanner:peek_line() or ""):match("^([%+%- ])")
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
---@class diff.FileEntry
|
||||
---@field renamed boolean
|
||||
---@field similarity? integer
|
||||
---@field dissimilarity? integer
|
||||
---@field index_old? integer
|
||||
---@field index_new? integer
|
||||
---@field mode? integer
|
||||
---@field old_mode? integer
|
||||
---@field new_mode? integer
|
||||
---@field deleted_file_mode? integer
|
||||
---@field new_file_mode? integer
|
||||
---@field path_old? string
|
||||
---@field path_new? string
|
||||
---@field hunks diff.Hunk[]
|
||||
|
||||
---@param scanner Scanner
|
||||
---@return diff.FileEntry
|
||||
local function parse_file_diff(scanner)
|
||||
---@type diff.FileEntry
|
||||
local ret = { renamed = false, hunks = {} }
|
||||
|
||||
-- The current line will here be the diff header
|
||||
|
||||
-- Extended git diff headers
|
||||
while scanner:peek_line() and
|
||||
not utils.str_match(scanner:peek_line() or "", { DIFF_HEADER, DIFF_HUNK_HEADER })
|
||||
do
|
||||
-- Extended header lines:
|
||||
-- old mode <mode>
|
||||
-- new mode <mode>
|
||||
-- deleted file mode <mode>
|
||||
-- new file mode <mode>
|
||||
-- copy from <path>
|
||||
-- copy to <path>
|
||||
-- rename from <path>
|
||||
-- rename to <path>
|
||||
-- similarity index <number>
|
||||
-- dissimilarity index <number>
|
||||
-- index <hash>..<hash> <mode>
|
||||
--
|
||||
-- Note: Combined diffs have even more variations
|
||||
|
||||
local last_line_idx = scanner:cur_line_idx()
|
||||
|
||||
-- Similarity
|
||||
local similarity = (scanner:peek_line() or ""):match(DIFF_SIMILARITY)
|
||||
if similarity then
|
||||
ret.similarity = tonumber(similarity) or -1
|
||||
scanner:next_line()
|
||||
end
|
||||
|
||||
-- Dissimilarity
|
||||
local dissimilarity = (scanner:peek_line() or ""):match([[^dissimilarity index (%d+)%%]])
|
||||
if dissimilarity then
|
||||
ret.dissimilarity = tonumber(dissimilarity) or -1
|
||||
scanner:next_line()
|
||||
end
|
||||
|
||||
-- Renames
|
||||
local rename_from = (scanner:peek_line() or ""):match([[^rename from (.*)]])
|
||||
if rename_from then
|
||||
ret.renamed = true
|
||||
ret.path_old = rename_from
|
||||
scanner:skip_line()
|
||||
ret.path_new = (scanner:next_line() or ""):match([[^rename to (.*)]])
|
||||
end
|
||||
|
||||
-- Copies
|
||||
local copy_from = (scanner:peek_line() or ""):match([[^copy from (.*)]])
|
||||
if copy_from then
|
||||
ret.path_old = copy_from
|
||||
scanner:skip_line()
|
||||
ret.path_new = (scanner:next_line() or ""):match([[^copy to (.*)]])
|
||||
end
|
||||
|
||||
-- Old mode
|
||||
local old_mode = (scanner:peek_line() or ""):match([[^old mode (%d+)]])
|
||||
if old_mode then
|
||||
ret.old_mode = old_mode
|
||||
scanner:next_line()
|
||||
end
|
||||
|
||||
-- New mode
|
||||
local new_mode = (scanner:peek_line() or ""):match([[^new mode (%d+)]])
|
||||
if new_mode then
|
||||
ret.new_mode = new_mode
|
||||
scanner:next_line()
|
||||
end
|
||||
|
||||
-- Deleted file
|
||||
local deleted_file_mode = (scanner:peek_line() or ""):match([[^deleted file mode (%d+)]])
|
||||
if deleted_file_mode then
|
||||
ret.old_file_mode = deleted_file_mode
|
||||
scanner:next_line()
|
||||
end
|
||||
|
||||
-- New file
|
||||
local new_file_mode = (scanner:peek_line() or ""):match([[^new file mode (%d+)]])
|
||||
if new_file_mode then
|
||||
ret.new_file_mode = new_file_mode
|
||||
scanner:next_line()
|
||||
end
|
||||
|
||||
-- Index
|
||||
local index_old, index_new, mode = utils.str_match(scanner:peek_line() or "", DIFF_INDEX)
|
||||
if index_old then
|
||||
ret.index_old = index_old
|
||||
ret.index_new = index_new
|
||||
ret.mode = mode
|
||||
scanner:next_line()
|
||||
end
|
||||
|
||||
-- Paths
|
||||
local path_old = utils.str_match(scanner:peek_line() or "", DIFF_PATH_OLD)
|
||||
if path_old then
|
||||
if not ret.path_old then
|
||||
ret.path_old = path_old ~= "/dev/null" and path_old or nil
|
||||
scanner:skip_line()
|
||||
local path_new = utils.str_match(scanner:next_line() or "", DIFF_PATH_NEW)
|
||||
ret.path_new = path_new ~= "/dev/null" and path_new or nil
|
||||
else
|
||||
scanner:skip_line(2)
|
||||
end
|
||||
end
|
||||
|
||||
if last_line_idx == scanner:cur_line_idx() then
|
||||
-- Non-git patches don't have the extended header lines
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- Hunks
|
||||
local line = scanner:peek_line()
|
||||
while line and not line:match(DIFF_HEADER) do
|
||||
local old_row, old_size, new_row, new_size = line:match(DIFF_HUNK_HEADER)
|
||||
scanner:next_line() -- Current line is now the hunk header
|
||||
|
||||
if old_row then
|
||||
table.insert(ret.hunks, parse_diff_hunk(
|
||||
scanner,
|
||||
tonumber(old_row) or -1,
|
||||
tonumber(old_size) or -1,
|
||||
tonumber(new_row) or -1,
|
||||
tonumber(new_size) or -1
|
||||
))
|
||||
end
|
||||
|
||||
line = scanner:peek_line()
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
---Parse a diff patch.
|
||||
---@param lines string[]
|
||||
---@return diff.FileEntry[]
|
||||
function M.parse_diff(lines)
|
||||
local ret = {}
|
||||
local scanner = Scanner(lines)
|
||||
|
||||
while scanner:peek_line() do
|
||||
local line = scanner:next_line() --[[@as string ]]
|
||||
-- TODO: Diff headers and patch format can take a few different forms. I.e. combined diffs
|
||||
if line:match(DIFF_HEADER) then
|
||||
table.insert(ret, parse_file_diff(scanner))
|
||||
end
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
---Build either the old or the new version of a diff hunk.
|
||||
---@param hunk diff.Hunk
|
||||
---@param version "old"|"new"
|
||||
---@return string[]
|
||||
function M.diff_build_hunk(hunk, version)
|
||||
local vcontent = version == "old" and hunk.old_content or hunk.new_content
|
||||
local size = version == "old" and hunk.old_size or hunk.new_size
|
||||
local common_idx = 1
|
||||
local chunk_idx = 1
|
||||
|
||||
local ret = {}
|
||||
local i = 1
|
||||
|
||||
while i <= size do
|
||||
local chunk = vcontent[chunk_idx]
|
||||
|
||||
if chunk and chunk[1] == i then
|
||||
for _, line in ipairs(chunk[2]) do
|
||||
ret[#ret + 1] = line
|
||||
end
|
||||
|
||||
i = i + (#chunk[2] - 1)
|
||||
chunk_idx = chunk_idx + 1
|
||||
else
|
||||
ret[#ret + 1] = hunk.common_content[common_idx]
|
||||
common_idx = common_idx + 1
|
||||
end
|
||||
|
||||
i = i + 1
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
local CONFLICT_START = [[^<<<<<<< ]]
|
||||
local CONFLICT_BASE = [[^||||||| ]]
|
||||
local CONFLICT_SEP = [[^=======$]]
|
||||
local CONFLICT_END = [[^>>>>>>> ]]
|
||||
|
||||
---@class ConflictRegion
|
||||
---@field first integer
|
||||
---@field last integer
|
||||
---@field ours { first: integer, last: integer, content?: string[] }
|
||||
---@field base { first: integer, last: integer, content?: string[] }
|
||||
---@field theirs { first: integer, last: integer, content?: string[] }
|
||||
|
||||
---@param lines string[]
|
||||
---@param winid? integer
|
||||
---@return ConflictRegion[] conflicts
|
||||
---@return ConflictRegion? cur_conflict The conflict under the cursor in the given window.
|
||||
---@return integer cur_conflict_idx Index of the current conflict. Will be 0 if the cursor is before the first conflict, and `#conflicts + 1` if the cursor is after the last conflict.
|
||||
function M.parse_conflicts(lines, winid)
|
||||
local ret = {}
|
||||
local has_start, has_base, has_sep = false, false, false
|
||||
local cur, cursor, cur_conflict, cur_idx
|
||||
|
||||
if winid and api.nvim_win_is_valid(winid) then
|
||||
cursor = api.nvim_win_get_cursor(winid)
|
||||
end
|
||||
|
||||
local function handle(data)
|
||||
local first = math.min(
|
||||
data.ours.first or math.huge,
|
||||
data.base.first or math.huge,
|
||||
data.theirs.first or math.huge
|
||||
)
|
||||
|
||||
if first == math.huge then return end
|
||||
|
||||
local last = math.max(
|
||||
data.ours.last or -1,
|
||||
data.base.last or -1,
|
||||
data.theirs.last or -1
|
||||
)
|
||||
|
||||
if last == -1 then return end
|
||||
|
||||
if data.ours.first and data.ours.last and data.ours.first < data.ours.last then
|
||||
data.ours.content = utils.vec_slice(lines, data.ours.first + 1, data.ours.last)
|
||||
end
|
||||
|
||||
if data.base.first and data.base.last and data.base.first < data.base.last then
|
||||
data.base.content = utils.vec_slice(lines, data.base.first + 1, data.base.last)
|
||||
end
|
||||
|
||||
if data.theirs.first and data.theirs.last and data.theirs.first < data.theirs.last - 1 then
|
||||
data.theirs.content = utils.vec_slice(lines, data.theirs.first + 1, data.theirs.last - 1)
|
||||
end
|
||||
|
||||
if cursor then
|
||||
if not cur_conflict and cursor[1] >= first and cursor[1] <= last then
|
||||
cur_conflict = data
|
||||
cur_idx = #ret + 1
|
||||
elseif cursor[1] > last then
|
||||
cur_idx = (cur_idx or 0) + 1
|
||||
end
|
||||
end
|
||||
|
||||
data.first = first
|
||||
data.last = last
|
||||
ret[#ret + 1] = data
|
||||
end
|
||||
|
||||
local function new_cur()
|
||||
return {
|
||||
ours = {},
|
||||
base = {},
|
||||
theirs = {},
|
||||
}
|
||||
end
|
||||
|
||||
cur = new_cur()
|
||||
|
||||
for i, line in ipairs(lines) do
|
||||
if line:match(CONFLICT_START) then
|
||||
if has_start then
|
||||
handle(cur)
|
||||
cur, has_start, has_base, has_sep = new_cur(), false, false, false
|
||||
end
|
||||
|
||||
has_start = true
|
||||
cur.ours.first = i
|
||||
cur.ours.last = i
|
||||
elseif line:match(CONFLICT_BASE) then
|
||||
if has_base then
|
||||
handle(cur)
|
||||
cur, has_start, has_base, has_sep = new_cur(), false, false, false
|
||||
end
|
||||
|
||||
has_base = true
|
||||
cur.base.first = i
|
||||
cur.ours.last = i - 1
|
||||
elseif line:match(CONFLICT_SEP) then
|
||||
if has_sep then
|
||||
handle(cur)
|
||||
cur, has_start, has_base, has_sep = new_cur(), false, false, false
|
||||
end
|
||||
|
||||
has_sep = true
|
||||
cur.theirs.first = i
|
||||
cur.theirs.last = i
|
||||
|
||||
if has_base then
|
||||
cur.base.last = i - 1
|
||||
else
|
||||
cur.ours.last = i - 1
|
||||
end
|
||||
elseif line:match(CONFLICT_END) then
|
||||
if not has_sep then
|
||||
if has_base then
|
||||
cur.base.last = i - 1
|
||||
elseif has_start then
|
||||
cur.ours.last = i - 1
|
||||
end
|
||||
end
|
||||
|
||||
cur.theirs.first = cur.theirs.first or i
|
||||
cur.theirs.last = i
|
||||
handle(cur)
|
||||
cur, has_start, has_base, has_sep = new_cur(), false, false, false
|
||||
end
|
||||
end
|
||||
|
||||
handle(cur)
|
||||
|
||||
if cursor and cur_idx then
|
||||
if cursor[1] > ret[#ret].last then
|
||||
cur_idx = #ret + 1
|
||||
end
|
||||
end
|
||||
|
||||
return ret, cur_conflict, cur_idx or 0
|
||||
end
|
||||
|
||||
---@param version { major: integer, minor: integer, patch: integer }
|
||||
---@param required { major: integer, minor: integer, patch: integer }
|
||||
---@return boolean
|
||||
function M.check_semver(version, required)
|
||||
if version.major ~= required.major then
|
||||
return version.major > required.major
|
||||
elseif version.minor ~= required.minor then
|
||||
return version.minor > required.minor
|
||||
elseif version.patch ~= required.patch then
|
||||
return version.patch > required.patch
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
M.JobStatus = JobStatus
|
||||
return M
|
||||
@ -0,0 +1,57 @@
|
||||
if vim.g.diffview_nvim_loaded or not require("diffview.bootstrap") then
|
||||
return
|
||||
end
|
||||
|
||||
vim.g.diffview_nvim_loaded = 1
|
||||
|
||||
local lazy = require("diffview.lazy")
|
||||
|
||||
---@module "diffview"
|
||||
local arg_parser = lazy.require("diffview.arg_parser") ---@module "diffview.arg_parser"
|
||||
local diffview = lazy.require("diffview") ---@module "diffview"
|
||||
|
||||
local api = vim.api
|
||||
local command = api.nvim_create_user_command
|
||||
|
||||
-- NOTE: Need this wrapper around the completion function becuase it doesn't
|
||||
-- exist yet.
|
||||
local function completion(...)
|
||||
return diffview.completion(...)
|
||||
end
|
||||
|
||||
-- Create commands
|
||||
command("DiffviewOpen", function(ctx)
|
||||
diffview.open(arg_parser.scan(ctx.args).args)
|
||||
end, { nargs = "*", complete = completion })
|
||||
|
||||
command("DiffviewFileHistory", function(ctx)
|
||||
local range
|
||||
|
||||
if ctx.range > 0 then
|
||||
range = { ctx.line1, ctx.line2 }
|
||||
end
|
||||
|
||||
diffview.file_history(range, arg_parser.scan(ctx.args).args)
|
||||
end, { nargs = "*", complete = completion, range = true })
|
||||
|
||||
command("DiffviewClose", function()
|
||||
diffview.close()
|
||||
end, { nargs = 0, bang = true })
|
||||
|
||||
command("DiffviewFocusFiles", function()
|
||||
diffview.emit("focus_files")
|
||||
end, { nargs = 0, bang = true })
|
||||
|
||||
command("DiffviewToggleFiles", function()
|
||||
diffview.emit("toggle_files")
|
||||
end, { nargs = 0, bang = true })
|
||||
|
||||
command("DiffviewRefresh", function()
|
||||
diffview.emit("refresh_files")
|
||||
end, { nargs = 0, bang = true })
|
||||
|
||||
command("DiffviewLog", function()
|
||||
vim.cmd(("sp %s | norm! G"):format(
|
||||
vim.fn.fnameescape(DiffviewGlobal.logger.outfile)
|
||||
))
|
||||
end, { nargs = 0, bang = true })
|
||||
@ -0,0 +1,37 @@
|
||||
local M = {}
|
||||
|
||||
function M.root(root)
|
||||
local f = debug.getinfo(1, "S").source:sub(2)
|
||||
return vim.fn.fnamemodify(f, ":p:h:h") .. "/" .. (root or "")
|
||||
end
|
||||
|
||||
---@param plugin string
|
||||
function M.load(plugin)
|
||||
local name = plugin:match(".*/(.*)")
|
||||
local package_root = M.root(".tests/site/pack/deps/start/")
|
||||
if not vim.loop.fs_stat(package_root .. name) then
|
||||
print("Installing " .. plugin)
|
||||
vim.fn.mkdir(package_root, "p")
|
||||
vim.fn.system({
|
||||
"git",
|
||||
"clone",
|
||||
"--depth=1",
|
||||
"https://github.com/" .. plugin .. ".git",
|
||||
package_root .. "/" .. name,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
function M.setup()
|
||||
vim.cmd([[set runtimepath=$VIMRUNTIME]])
|
||||
vim.opt.runtimepath:append(M.root())
|
||||
vim.opt.packpath = { M.root(".tests/site") }
|
||||
M.load("nvim-lua/plenary.nvim")
|
||||
vim.env.XDG_CONFIG_HOME = M.root(".tests/config")
|
||||
vim.env.XDG_DATA_HOME = M.root(".tests/data")
|
||||
vim.env.XDG_STATE_HOME = M.root(".tests/state")
|
||||
vim.env.XDG_CACHE_HOME = M.root(".tests/cache")
|
||||
require("diffview.tests.init")
|
||||
end
|
||||
|
||||
M.setup()
|
||||
@ -0,0 +1,7 @@
|
||||
column_width = 100
|
||||
line_endings = "Unix"
|
||||
indent_type = "Spaces"
|
||||
indent_width = 2
|
||||
quote_style = "AutoPreferDouble"
|
||||
call_parentheses = "Always"
|
||||
collapse_simple_statement = "Always"
|
||||
Reference in New Issue
Block a user